From 575a8bd9c84edcbf222a0327d3d5f575afe8c4d0 Mon Sep 17 00:00:00 2001
From: シアノン <xianon@hotmail.co.jp>
Date: Fri, 5 Nov 2021 11:55:00 +0900
Subject: コントロールパネルのジョブキューに個々のジョブが表示されないのを修正
 (#7941)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/pages/admin/queue.chart.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/client/pages/admin/queue.chart.vue')

diff --git a/src/client/pages/admin/queue.chart.vue b/src/client/pages/admin/queue.chart.vue
index 084181a606..38cd0b10ef 100644
--- a/src/client/pages/admin/queue.chart.vue
+++ b/src/client/pages/admin/queue.chart.vue
@@ -55,8 +55,8 @@ export default defineComponent({
 		const jobs = ref([]);
 
 		onMounted(() => {
-			os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
-				jobs.value = jobs;
+			os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
+				jobs.value = result;
 			});
 
 			const onStats = (stats) => {
-- 
cgit v1.2.3-freya


From 0e4a111f81cceed275d9bec2695f6e401fb654d8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 12 Nov 2021 02:02:25 +0900
Subject: refactoring

Resolve #7779
---
 .gitignore                                         |    4 +-
 .imgbotconfig                                      |    5 -
 .mocharc.json                                      |    7 -
 assets/api-doc.png                                 |  Bin 5542 -> 0 bytes
 assets/apple-touch-icon.png                        |  Bin 9828 -> 0 bytes
 assets/client/about-icon.png                       |  Bin 20528 -> 0 bytes
 assets/client/fedi.jpg                             |  Bin 77752 -> 0 bytes
 assets/client/label-red.svg                        |    6 -
 assets/client/label.svg                            |    6 -
 assets/client/misskey.svg                          |    7 -
 assets/client/remove.png                           |  Bin 424 -> 0 bytes
 .../client/room/furnitures/banknote/banknote.blend |  Bin 716076 -> 0 bytes
 .../client/room/furnitures/banknote/banknote.glb   |  Bin 48988 -> 0 bytes
 assets/client/room/furnitures/banknote/tex.png     |  Bin 43794 -> 0 bytes
 assets/client/room/furnitures/bed/bed.blend        |  Bin 573584 -> 0 bytes
 assets/client/room/furnitures/bed/bed.glb          |  Bin 8944 -> 0 bytes
 assets/client/room/furnitures/bin/bin.blend        |  Bin 695512 -> 0 bytes
 assets/client/room/furnitures/bin/bin.glb          |  Bin 22140 -> 0 bytes
 assets/client/room/furnitures/book/book.blend      |  Bin 511864 -> 0 bytes
 assets/client/room/furnitures/book/book.glb        |  Bin 4304 -> 0 bytes
 assets/client/room/furnitures/book2/barcode.png    |  Bin 3505 -> 0 bytes
 assets/client/room/furnitures/book2/book2.blend    |  Bin 521456 -> 0 bytes
 assets/client/room/furnitures/book2/book2.glb      |  Bin 82920 -> 0 bytes
 .../client/room/furnitures/book2/texture.afdesign  |  Bin 875434 -> 0 bytes
 assets/client/room/furnitures/book2/texture.png    |  Bin 61019 -> 0 bytes
 assets/client/room/furnitures/book2/uv.png         |  Bin 8611 -> 0 bytes
 .../furnitures/cardboard-box/cardboard-box.blend   |  Bin 515036 -> 0 bytes
 .../furnitures/cardboard-box/cardboard-box.glb     |  Bin 4568 -> 0 bytes
 .../furnitures/cardboard-box2/cardboard-box2.blend |  Bin 545788 -> 0 bytes
 .../furnitures/cardboard-box2/cardboard-box2.glb   |  Bin 24084 -> 0 bytes
 .../room/furnitures/cardboard-box2/texture.png     |  Bin 16879 -> 0 bytes
 .../client/room/furnitures/cardboard-box2/uv.png   |  Bin 4630 -> 0 bytes
 .../furnitures/cardboard-box3/cardboard-box3.blend |  Bin 547868 -> 0 bytes
 .../furnitures/cardboard-box3/cardboard-box3.glb   |  Bin 23668 -> 0 bytes
 .../room/furnitures/cardboard-box3/texture.png     |  Bin 16204 -> 0 bytes
 .../room/furnitures/cardboard-box3/texture.xcf     |  Bin 242066 -> 0 bytes
 .../client/room/furnitures/cardboard-box3/uv.png   |  Bin 3598 -> 0 bytes
 .../furnitures/carpet-stripe/carpet-stripe.blend   |  Bin 481784 -> 0 bytes
 .../furnitures/carpet-stripe/carpet-stripe.glb     |  Bin 4956 -> 0 bytes
 assets/client/room/furnitures/chair/chair.blend    |  Bin 500696 -> 0 bytes
 assets/client/room/furnitures/chair/chair.glb      |  Bin 4252 -> 0 bytes
 assets/client/room/furnitures/chair2/chair2.blend  |  Bin 523036 -> 0 bytes
 assets/client/room/furnitures/chair2/chair2.glb    |  Bin 6140 -> 0 bytes
 .../room/furnitures/color-box/color-box.blend      |  Bin 631168 -> 0 bytes
 .../client/room/furnitures/color-box/color-box.glb |  Bin 7492 -> 0 bytes
 .../room/furnitures/corkboard/corkboard.blend      |  Bin 565404 -> 0 bytes
 .../client/room/furnitures/corkboard/corkboard.glb |  Bin 7180 -> 0 bytes
 assets/client/room/furnitures/cube/cube.blend      |  Bin 646184 -> 0 bytes
 assets/client/room/furnitures/cube/cube.glb        |  Bin 188392 -> 0 bytes
 .../room/furnitures/cup-noodle/cup-noodle.blend    |  Bin 855164 -> 0 bytes
 .../room/furnitures/cup-noodle/cup-noodle.glb      |  Bin 318332 -> 0 bytes
 .../client/room/furnitures/cup-noodle/noodle.png   |  Bin 297154 -> 0 bytes
 assets/client/room/furnitures/desk/desk.blend      |  Bin 541980 -> 0 bytes
 assets/client/room/furnitures/desk/desk.glb        |  Bin 10184 -> 0 bytes
 .../client/room/furnitures/doll-ai/doll-ai.blend   |  Bin 1155028 -> 0 bytes
 assets/client/room/furnitures/doll-ai/doll-ai.glb  |  Bin 112160 -> 0 bytes
 .../client/room/furnitures/doll-ai/doll_ai_tex.png |  Bin 10712 -> 0 bytes
 .../furnitures/energy-drink/energy-drink.blend     |  Bin 724764 -> 0 bytes
 .../room/furnitures/energy-drink/energy-drink.glb  |  Bin 145836 -> 0 bytes
 .../room/furnitures/energy-drink/texture.afdesign  |  Bin 824566 -> 0 bytes
 .../room/furnitures/energy-drink/texture.png       |  Bin 127510 -> 0 bytes
 assets/client/room/furnitures/energy-drink/uv.png  |  Bin 22142 -> 0 bytes
 assets/client/room/furnitures/eraser/cover.png     |  Bin 8336 -> 0 bytes
 assets/client/room/furnitures/eraser/cover.psd     |  Bin 219123 -> 0 bytes
 assets/client/room/furnitures/eraser/eraser-uv.png |  Bin 11164 -> 0 bytes
 assets/client/room/furnitures/eraser/eraser.blend  |  Bin 527996 -> 0 bytes
 assets/client/room/furnitures/eraser/eraser.glb    |  Bin 15748 -> 0 bytes
 .../furnitures/facial-tissue/facial-tissue-uv.png  |  Bin 4470 -> 0 bytes
 .../furnitures/facial-tissue/facial-tissue.blend   |  Bin 533220 -> 0 bytes
 .../furnitures/facial-tissue/facial-tissue.glb     |  Bin 8332 -> 0 bytes
 .../furnitures/facial-tissue/facial-tissue.png     |  Bin 688 -> 0 bytes
 .../furnitures/facial-tissue/facial-tissue.psd     |  Bin 111968 -> 0 bytes
 assets/client/room/furnitures/fan/fan.blend        |  Bin 772732 -> 0 bytes
 assets/client/room/furnitures/fan/fan.glb          |  Bin 51612 -> 0 bytes
 .../furnitures/holo-display/holo-display.blend     |  Bin 788456 -> 0 bytes
 .../room/furnitures/holo-display/holo-display.glb  |  Bin 148928 -> 0 bytes
 .../client/room/furnitures/holo-display/ray-uv.png |  Bin 20901 -> 0 bytes
 assets/client/room/furnitures/holo-display/ray.png |  Bin 104293 -> 0 bytes
 .../client/room/furnitures/keyboard/keyboard.blend |  Bin 1005588 -> 0 bytes
 .../client/room/furnitures/keyboard/keyboard.glb   |  Bin 132360 -> 0 bytes
 .../room/furnitures/low-table/low-table.blend      |  Bin 497336 -> 0 bytes
 .../client/room/furnitures/low-table/low-table.glb |  Bin 2364 -> 0 bytes
 assets/client/room/furnitures/mat/mat.blend        |  Bin 489304 -> 0 bytes
 assets/client/room/furnitures/mat/mat.glb          |  Bin 1700 -> 0 bytes
 assets/client/room/furnitures/milk/milk-uv.png     |  Bin 16577 -> 0 bytes
 assets/client/room/furnitures/milk/milk.blend      |  Bin 539836 -> 0 bytes
 assets/client/room/furnitures/milk/milk.glb        |  Bin 8456 -> 0 bytes
 assets/client/room/furnitures/milk/milk.png        |  Bin 658 -> 0 bytes
 assets/client/room/furnitures/milk/milk.psd        |  Bin 125415 -> 0 bytes
 .../client/room/furnitures/monitor/monitor.blend   |  Bin 586820 -> 0 bytes
 assets/client/room/furnitures/monitor/monitor.glb  |  Bin 35300 -> 0 bytes
 assets/client/room/furnitures/monitor/monitor.psd  |  Bin 3692204 -> 0 bytes
 .../client/room/furnitures/monitor/screen-uv.png   |  Bin 1495 -> 0 bytes
 assets/client/room/furnitures/monitor/screen.jpg   |  Bin 24842 -> 0 bytes
 assets/client/room/furnitures/moon/moon.blend      |  Bin 725536 -> 0 bytes
 assets/client/room/furnitures/moon/moon.glb        |  Bin 122112 -> 0 bytes
 assets/client/room/furnitures/moon/moon.jpg        |  Bin 87151 -> 0 bytes
 .../client/room/furnitures/mousepad/mousepad.blend |  Bin 495008 -> 0 bytes
 .../client/room/furnitures/mousepad/mousepad.glb   |  Bin 4360 -> 0 bytes
 .../client/room/furnitures/pc/motherboard-uv.png   |  Bin 2920 -> 0 bytes
 .../client/room/furnitures/pc/motherboard-uv.psd   |  Bin 7131432 -> 0 bytes
 assets/client/room/furnitures/pc/motherboard.jpg   |  Bin 64600 -> 0 bytes
 assets/client/room/furnitures/pc/pc.blend          |  Bin 818152 -> 0 bytes
 assets/client/room/furnitures/pc/pc.glb            |  Bin 103620 -> 0 bytes
 assets/client/room/furnitures/pencil/pencil.blend  |  Bin 683032 -> 0 bytes
 assets/client/room/furnitures/pencil/pencil.glb    |  Bin 7788 -> 0 bytes
 .../client/room/furnitures/photoframe/photo-uv.png |  Bin 2639 -> 0 bytes
 assets/client/room/furnitures/photoframe/photo.jpg |  Bin 30500 -> 0 bytes
 .../room/furnitures/photoframe/photoframe.blend    |  Bin 551416 -> 0 bytes
 .../room/furnitures/photoframe/photoframe.glb      |  Bin 37612 -> 0 bytes
 assets/client/room/furnitures/piano/piano.blend    |  Bin 713228 -> 0 bytes
 assets/client/room/furnitures/piano/piano.glb      |  Bin 73320 -> 0 bytes
 .../client/room/furnitures/pinguin/pinguin.blend   |  Bin 778976 -> 0 bytes
 assets/client/room/furnitures/pinguin/pinguin.glb  |  Bin 60172 -> 0 bytes
 .../client/room/furnitures/plant/plant-soil-uv.png |  Bin 4509 -> 0 bytes
 assets/client/room/furnitures/plant/plant-soil.png |  Bin 10852 -> 0 bytes
 assets/client/room/furnitures/plant/plant-soil.psd |  Bin 390188 -> 0 bytes
 assets/client/room/furnitures/plant/plant.blend    |  Bin 604488 -> 0 bytes
 assets/client/room/furnitures/plant/plant.glb      |  Bin 27664 -> 0 bytes
 assets/client/room/furnitures/plant2/plant2.blend  |  Bin 1060816 -> 0 bytes
 assets/client/room/furnitures/plant2/plant2.glb    |  Bin 133212 -> 0 bytes
 assets/client/room/furnitures/plant2/soil.png      |  Bin 10852 -> 0 bytes
 .../client/room/furnitures/poster-h/poster-h.blend |  Bin 667604 -> 0 bytes
 .../client/room/furnitures/poster-h/poster-h.glb   |  Bin 1280 -> 0 bytes
 assets/client/room/furnitures/poster-h/uv.png      |  Bin 3786 -> 0 bytes
 .../client/room/furnitures/poster-v/poster-v.blend |  Bin 650740 -> 0 bytes
 .../client/room/furnitures/poster-v/poster-v.glb   |  Bin 1280 -> 0 bytes
 assets/client/room/furnitures/poster-v/uv.png      |  Bin 3795 -> 0 bytes
 .../client/room/furnitures/pudding/pudding.blend   |  Bin 694784 -> 0 bytes
 assets/client/room/furnitures/pudding/pudding.glb  |  Bin 10612 -> 0 bytes
 .../room/furnitures/rubik-cube/rubik-cube.blend    |  Bin 745960 -> 0 bytes
 .../room/furnitures/rubik-cube/rubik-cube.glb      |  Bin 67688 -> 0 bytes
 assets/client/room/furnitures/server/rack-uv.png   |  Bin 6705 -> 0 bytes
 assets/client/room/furnitures/server/rack.png      |  Bin 7046 -> 0 bytes
 assets/client/room/furnitures/server/server.blend  |  Bin 684372 -> 0 bytes
 assets/client/room/furnitures/server/server.glb    |  Bin 149480 -> 0 bytes
 assets/client/room/furnitures/server/server.png    |  Bin 116866 -> 0 bytes
 assets/client/room/furnitures/server/uv.png        |  Bin 4012 -> 0 bytes
 assets/client/room/furnitures/sofa/sofa.blend      |  Bin 711488 -> 0 bytes
 assets/client/room/furnitures/sofa/sofa.glb        |  Bin 37656 -> 0 bytes
 assets/client/room/furnitures/spiral/spiral.blend  |  Bin 1241416 -> 0 bytes
 assets/client/room/furnitures/spiral/spiral.glb    |  Bin 251788 -> 0 bytes
 assets/client/room/furnitures/tv/screen-uv.png     |  Bin 3792 -> 0 bytes
 assets/client/room/furnitures/tv/tv.blend          |  Bin 587640 -> 0 bytes
 assets/client/room/furnitures/tv/tv.glb            |  Bin 8580 -> 0 bytes
 .../room/furnitures/wall-clock/wall-clock.blend    |  Bin 574128 -> 0 bytes
 .../room/furnitures/wall-clock/wall-clock.glb      |  Bin 47848 -> 0 bytes
 assets/client/room/rooms/default/default.blend     |  Bin 618364 -> 0 bytes
 assets/client/room/rooms/default/default.glb       |  Bin 12280 -> 0 bytes
 assets/client/room/rooms/washitsu/husuma-uv.png    |  Bin 4037 -> 0 bytes
 assets/client/room/rooms/washitsu/husuma.png       |  Bin 2539 -> 0 bytes
 .../room/rooms/washitsu/tatami-single1600.png      |  Bin 1173343 -> 0 bytes
 assets/client/room/rooms/washitsu/tatami-uv.png    |  Bin 4075 -> 0 bytes
 assets/client/room/rooms/washitsu/tatami.afdesign  |  Bin 2501804 -> 0 bytes
 assets/client/room/rooms/washitsu/tatami.png       |  Bin 313722 -> 0 bytes
 assets/client/room/rooms/washitsu/washitsu.blend   |  Bin 664000 -> 0 bytes
 assets/client/room/rooms/washitsu/washitsu.glb     |  Bin 415180 -> 0 bytes
 assets/client/sounds/aisha/1.mp3                   |  Bin 34480 -> 0 bytes
 assets/client/sounds/aisha/2.mp3                   |  Bin 24031 -> 0 bytes
 assets/client/sounds/aisha/3.mp3                   |  Bin 29256 -> 0 bytes
 assets/client/sounds/noizenecio/kick_gaba.mp3      |  Bin 18866 -> 0 bytes
 assets/client/sounds/noizenecio/kick_gaba2.mp3     |  Bin 27144 -> 0 bytes
 assets/client/sounds/syuilo/down.mp3               |  Bin 18240 -> 0 bytes
 assets/client/sounds/syuilo/kick.mp3               |  Bin 15672 -> 0 bytes
 assets/client/sounds/syuilo/pirori-square-wet.mp3  |  Bin 139200 -> 0 bytes
 assets/client/sounds/syuilo/pirori-wet.mp3         |  Bin 139200 -> 0 bytes
 assets/client/sounds/syuilo/pirori.mp3             |  Bin 19200 -> 0 bytes
 assets/client/sounds/syuilo/poi1.mp3               |  Bin 18240 -> 0 bytes
 assets/client/sounds/syuilo/poi2.mp3               |  Bin 18240 -> 0 bytes
 assets/client/sounds/syuilo/pope1.mp3              |  Bin 18240 -> 0 bytes
 assets/client/sounds/syuilo/pope2.mp3              |  Bin 18240 -> 0 bytes
 assets/client/sounds/syuilo/popo.mp3               |  Bin 18240 -> 0 bytes
 assets/client/sounds/syuilo/queue-jammed.mp3       |  Bin 351466 -> 0 bytes
 assets/client/sounds/syuilo/reverved.mp3           |  Bin 276480 -> 0 bytes
 assets/client/sounds/syuilo/ryukyu.mp3             |  Bin 139200 -> 0 bytes
 assets/client/sounds/syuilo/snare.mp3              |  Bin 26121 -> 0 bytes
 assets/client/sounds/syuilo/square-pico.mp3        |  Bin 19200 -> 0 bytes
 assets/client/sounds/syuilo/triple.mp3             |  Bin 18240 -> 0 bytes
 assets/client/sounds/syuilo/up.mp3                 |  Bin 18240 -> 0 bytes
 assets/client/sounds/syuilo/waon.mp3               |  Bin 18240 -> 0 bytes
 assets/client/sparkle-spritesheet.png              |  Bin 185 -> 0 bytes
 assets/client/unread.svg                           |    7 -
 assets/favicon.ico                                 |  Bin 90022 -> 0 bytes
 assets/favicon.png                                 |  Bin 9348 -> 0 bytes
 assets/icons/192.png                               |  Bin 7629 -> 0 bytes
 assets/icons/512.png                               |  Bin 20527 -> 0 bytes
 assets/mi-white.png                                |  Bin 18767 -> 0 bytes
 assets/redoc.html                                  |   24 -
 assets/robots.txt                                  |    4 -
 assets/user-unknown.png                            |  Bin 3518 -> 0 bytes
 gulpfile.js                                        |   71 +
 gulpfile.ts                                        |   80 -
 index.js                                           |   17 -
 migration/1000000000000-Init.ts                    |  485 --
 migration/1556348509290-Pages.ts                   |   31 -
 migration/1556746559567-UserProfile.ts             |   16 -
 migration/1557476068003-PinnedUsers.ts             |   13 -
 migration/1557761316509-AddSomeUrls.ts             |   16 -
 migration/1557932705754-ObjectStorageSetting.ts    |   31 -
 migration/1558072954435-PageLike.ts                |   23 -
 migration/1558103093633-UserGroup.ts               |   41 -
 migration/1558257926829-UserGroupInvite.ts         |   25 -
 migration/1558266512381-UserListJoining.ts         |   13 -
 migration/1561706992953-webauthn.ts                |   29 -
 migration/1561873850023-ChartIndexes.ts            |  201 -
 migration/1562422242907-PasswordLessLogin.ts       |   13 -
 migration/1562444565093-PinnedPage.ts              |   17 -
 migration/1562448332510-PageTitleHideOption.ts     |   13 -
 migration/1562869971568-ModerationLog.ts           |   17 -
 migration/1563757595828-UsedUsername.ts            |   13 -
 migration/1565634203341-room.ts                    |   13 -
 migration/1571220798684-CustomEmojiCategory.ts     |   13 -
 migration/1572760203493-nodeinfo.ts                |   29 -
 migration/1576269851876-TalkFederationId.ts        |   14 -
 migration/1576869585998-ProxyRemoteFiles.ts        |   14 -
 migration/1579267006611-v12.ts                     |   34 -
 migration/1579270193251-v12-2.ts                   |   14 -
 migration/1579282808087-v12-3.ts                   |   14 -
 migration/1579544426412-v12-4.ts                   |   16 -
 migration/1579977526288-v12-5.ts                   |   54 -
 migration/1579993013959-v12-6.ts                   |   18 -
 migration/1580069531114-v12-7.ts                   |   24 -
 migration/1580148575182-v12-8.ts                   |   16 -
 migration/1580154400017-v12-9.ts                   |   14 -
 migration/1580276619901-v12-10.ts                  |   19 -
 migration/1580331224276-v12-11.ts                  |   18 -
 migration/1580508795118-v12-12.ts                  |   46 -
 migration/1580543501339-v12-13.ts                  |   14 -
 migration/1580864313253-v12-14.ts                  |   20 -
 migration/1581526429287-user-group-invitation.ts   |   38 -
 migration/1581695816408-user-group-antenna.ts      |   28 -
 .../1581708415836-drive-user-folder-id-index.ts    |   14 -
 migration/1581979837262-promo.ts                   |   28 -
 migration/1582019042083-featured-injecttion.ts     |   14 -
 migration/1582210532752-antenna-exclude.ts         |   14 -
 migration/1582875306439-note-reaction-length.ts    |   14 -
 migration/1585361548360-miauth.ts                  |   36 -
 migration/1585385921215-custom-notification.ts     |   48 -
 migration/1585772678853-ap-url.ts                  |   15 -
 .../1586624197029-AddObjectStorageUseProxy.ts      |   14 -
 migration/1586641139527-remote-reaction.ts         |   12 -
 migration/1586708940386-pageAiScript.ts            |   14 -
 migration/1588044505511-hCaptcha.ts                |   18 -
 migration/1589023282116-pubRelay.ts                |   18 -
 migration/1595075960584-blurhash.ts                |   14 -
 .../1595077605646-blurhash-for-avatar-banner.ts    |   20 -
 migration/1595676934834-instance-icon-url.ts       |   14 -
 migration/1595771249699-word-mute.ts               |   30 -
 migration/1595782306083-word-mute2.ts              |   18 -
 migration/1596548170836-channel.ts                 |   58 -
 migration/1596786425167-channel2.ts                |   14 -
 .../1597230137744-objectStorageSetPublicRead.ts    |   14 -
 .../1597236229720-IncludingNotificationTypes.ts    |   16 -
 migration/1597385880794-add-sensitive-index.ts     |   14 -
 migration/1597459042300-channel-unread.ts          |   27 -
 migration/1597893996136-ChannelNoteIdDescIndex.ts  |   16 -
 migration/1600353287890-mutingNotificationTypes.ts |   20 -
 .../1603094348345-refine-abuse-user-report.ts      |   32 -
 .../1603095701770-refine-abuse-user-report2.ts     |   20 -
 migration/1603776877564-instance-theme-color.ts    |   14 -
 migration/1603781553011-instance-favicon.ts        |   14 -
 migration/1604821689616-delete-auto-watch.ts       |   14 -
 migration/1605408848373-clip-description.ts        |   15 -
 migration/1605408971051-comments.ts                |  434 -
 migration/1605585339718-instance-pinned-pages.ts   |   14 -
 migration/1605965516823-instance-images.ts         |   16 -
 migration/1606191203881-no-crawle.ts               |   16 -
 migration/1607151207216-instance-pinned-clip.ts    |   14 -
 migration/1607353487793-isExplorable.ts            |   18 -
 migration/1610277136869-registry.ts                |   22 -
 migration/1610277585759-registry2.ts               |   16 -
 migration/1610283021566-registry3.ts               |   14 -
 migration/1611354329133-followersUri.ts            |   16 -
 migration/1611397665007-gallery.ts                 |   40 -
 .../1611547387175-objectStorageS3ForcePathStyle.ts |   14 -
 migration/1612619156584-announcement-email.ts      |   14 -
 migration/1613155914446-emailNotificationTypes.ts  |   14 -
 migration/1613181457597-user-lang.ts               |   14 -
 .../1613503367223-use-bigint-for-driveUsage.ts     |   15 -
 migration/1615965918224-chart-v2.ts                |  218 -
 migration/1615966519402-chart-v2-2.ts              |   22 -
 migration/1618637372000-user-last-active-date.ts   |   16 -
 migration/1618639857000-user-hide-online-status.ts |   14 -
 migration/1619942102890-password-reset.ts          |   20 -
 migration/1620019354680-ad.ts                      |   18 -
 migration/1620364649428-ad2.ts                     |   14 -
 migration/1621479946000-add-note-indexes.ts        |   16 -
 ...622679304522-user-profile-description-length.ts |   13 -
 migration/1622681548499-log-message-length.ts      |   12 -
 migration/1629004542760-chart-reindex.ts           |  182 -
 migration/1629024377804-deepl-integration.ts       |   14 -
 migration/1629288472000-fix-channel-userId.ts      |   14 -
 migration/1629512953000-user-is-deleted.ts         |   15 -
 migration/1629778475000-deepl-integration2.ts      |   14 -
 .../1633068642000-email-required-for-signup.ts     |   14 -
 migration/1633071909016-user-pending.ts            |   16 -
 migration/1634486652000-user-public-reactions.ts   |   14 -
 migration/1634902659689-delete-log.ts              |   13 -
 migration/1635500777168-note-thread-mute.ts        |   26 -
 migration/1636197624383-ff-visibility.ts           |   16 -
 ormconfig.js                                       |   17 -
 package.json                                       |  229 +-
 packages/backend/.eslintrc                         |    6 +
 packages/backend/.mocharc.json                     |    7 +
 packages/backend/.npmrc                            |    2 +
 packages/backend/@types/hcaptcha.d.ts              |   11 +
 packages/backend/@types/http-signature.d.ts        |   77 +
 packages/backend/@types/jsrsasign.d.ts             |  800 ++
 packages/backend/@types/koa-json-body.d.ts         |   15 +
 packages/backend/@types/koa-slow.d.ts              |   14 +
 packages/backend/@types/langmap.d.ts               |   10 +
 packages/backend/@types/ms.d.ts                    |   12 +
 packages/backend/@types/os-utils.d.ts              |   30 +
 packages/backend/@types/package.json.d.ts          |   10 +
 packages/backend/@types/probe-image-size.d.ts      |   27 +
 packages/backend/assets/api-doc.png                |  Bin 0 -> 5542 bytes
 packages/backend/assets/apple-touch-icon.png       |  Bin 0 -> 9828 bytes
 packages/backend/assets/favicon.ico                |  Bin 0 -> 90022 bytes
 packages/backend/assets/favicon.png                |  Bin 0 -> 9348 bytes
 packages/backend/assets/icons/192.png              |  Bin 0 -> 7629 bytes
 packages/backend/assets/icons/512.png              |  Bin 0 -> 20527 bytes
 packages/backend/assets/mi-white.png               |  Bin 0 -> 18767 bytes
 packages/backend/assets/redoc.html                 |   24 +
 packages/backend/assets/robots.txt                 |    4 +
 packages/backend/assets/user-unknown.png           |  Bin 0 -> 3518 bytes
 packages/backend/migration/1000000000000-Init.js   |  483 ++
 packages/backend/migration/1556348509290-Pages.js  |   29 +
 .../backend/migration/1556746559567-UserProfile.js |   14 +
 .../backend/migration/1557476068003-PinnedUsers.js |   11 +
 .../backend/migration/1557761316509-AddSomeUrls.js |   15 +
 .../1557932705754-ObjectStorageSetting.js          |   29 +
 .../backend/migration/1558072954435-PageLike.js    |   21 +
 .../backend/migration/1558103093633-UserGroup.js   |   39 +
 .../migration/1558257926829-UserGroupInvite.js     |   23 +
 .../migration/1558266512381-UserListJoining.js     |   11 +
 .../backend/migration/1561706992953-webauthn.js    |   27 +
 .../migration/1561873850023-ChartIndexes.js        |  199 +
 .../migration/1562422242907-PasswordLessLogin.js   |   11 +
 .../backend/migration/1562444565093-PinnedPage.js  |   15 +
 .../migration/1562448332510-PageTitleHideOption.js |   11 +
 .../migration/1562869971568-ModerationLog.js       |   15 +
 .../migration/1563757595828-UsedUsername.js        |   11 +
 packages/backend/migration/1565634203341-room.js   |   11 +
 .../migration/1571220798684-CustomEmojiCategory.js |   11 +
 .../backend/migration/1572760203493-nodeinfo.js    |   27 +
 .../migration/1576269851876-TalkFederationId.js    |   14 +
 .../migration/1576869585998-ProxyRemoteFiles.js    |   14 +
 packages/backend/migration/1579267006611-v12.js    |   34 +
 packages/backend/migration/1579270193251-v12-2.js  |   14 +
 packages/backend/migration/1579282808087-v12-3.js  |   14 +
 packages/backend/migration/1579544426412-v12-4.js  |   16 +
 packages/backend/migration/1579977526288-v12-5.js  |   54 +
 packages/backend/migration/1579993013959-v12-6.js  |   18 +
 packages/backend/migration/1580069531114-v12-7.js  |   24 +
 packages/backend/migration/1580148575182-v12-8.js  |   16 +
 packages/backend/migration/1580154400017-v12-9.js  |   14 +
 packages/backend/migration/1580276619901-v12-10.js |   19 +
 packages/backend/migration/1580331224276-v12-11.js |   18 +
 packages/backend/migration/1580508795118-v12-12.js |   46 +
 packages/backend/migration/1580543501339-v12-13.js |   14 +
 packages/backend/migration/1580864313253-v12-14.js |   20 +
 .../1581526429287-user-group-invitation.js         |   38 +
 .../migration/1581695816408-user-group-antenna.js  |   28 +
 .../1581708415836-drive-user-folder-id-index.js    |   14 +
 packages/backend/migration/1581979837262-promo.js  |   28 +
 .../migration/1582019042083-featured-injecttion.js |   14 +
 .../migration/1582210532752-antenna-exclude.js     |   14 +
 .../1582875306439-note-reaction-length.js          |   14 +
 packages/backend/migration/1585361548360-miauth.js |   36 +
 .../migration/1585385921215-custom-notification.js |   48 +
 packages/backend/migration/1585772678853-ap-url.js |   14 +
 .../1586624197029-AddObjectStorageUseProxy.js      |   14 +
 .../migration/1586641139527-remote-reaction.js     |   14 +
 .../migration/1586708940386-pageAiScript.js        |   14 +
 .../backend/migration/1588044505511-hCaptcha.js    |   18 +
 .../backend/migration/1589023282116-pubRelay.js    |   18 +
 .../backend/migration/1595075960584-blurhash.js    |   14 +
 .../1595077605646-blurhash-for-avatar-banner.js    |   20 +
 .../migration/1595676934834-instance-icon-url.js   |   14 +
 .../backend/migration/1595771249699-word-mute.js   |   30 +
 .../backend/migration/1595782306083-word-mute2.js  |   18 +
 .../backend/migration/1596548170836-channel.js     |   58 +
 .../backend/migration/1596786425167-channel2.js    |   14 +
 .../1597230137744-objectStorageSetPublicRead.js    |   14 +
 .../1597236229720-IncludingNotificationTypes.js    |   16 +
 .../migration/1597385880794-add-sensitive-index.js |   14 +
 .../migration/1597459042300-channel-unread.js      |   27 +
 .../1597893996136-ChannelNoteIdDescIndex.js        |   16 +
 .../1600353287890-mutingNotificationTypes.js       |   20 +
 .../1603094348345-refine-abuse-user-report.js      |   32 +
 .../1603095701770-refine-abuse-user-report2.js     |   20 +
 .../1603776877564-instance-theme-color.js          |   14 +
 .../migration/1603781553011-instance-favicon.js    |   14 +
 .../migration/1604821689616-delete-auto-watch.js   |   14 +
 .../migration/1605408848373-clip-description.js    |   14 +
 .../backend/migration/1605408971051-comments.js    |  434 +
 .../1605585339718-instance-pinned-pages.js         |   14 +
 .../migration/1605965516823-instance-images.js     |   16 +
 .../backend/migration/1606191203881-no-crawle.js   |   16 +
 .../1607151207216-instance-pinned-clip.js          |   14 +
 .../migration/1607353487793-isExplorable.js        |   18 +
 .../backend/migration/1610277136869-registry.js    |   22 +
 .../backend/migration/1610277585759-registry2.js   |   16 +
 .../backend/migration/1610283021566-registry3.js   |   14 +
 .../migration/1611354329133-followersUri.js        |   16 +
 .../backend/migration/1611397665007-gallery.js     |   40 +
 .../1611547387175-objectStorageS3ForcePathStyle.js |   14 +
 .../migration/1612619156584-announcement-email.js  |   14 +
 .../1613155914446-emailNotificationTypes.js        |   14 +
 .../backend/migration/1613181457597-user-lang.js   |   14 +
 .../1613503367223-use-bigint-for-driveUsage.js     |   15 +
 .../backend/migration/1615965918224-chart-v2.js    |  217 +
 .../backend/migration/1615966519402-chart-v2-2.js  |   22 +
 .../1618637372000-user-last-active-date.js         |   16 +
 .../1618639857000-user-hide-online-status.js       |   14 +
 .../migration/1619942102890-password-reset.js      |   20 +
 packages/backend/migration/1620019354680-ad.js     |   18 +
 packages/backend/migration/1620364649428-ad2.js    |   14 +
 .../migration/1621479946000-add-note-indexes.js    |   16 +
 ...622679304522-user-profile-description-length.js |   14 +
 .../migration/1622681548499-log-message-length.js  |   14 +
 .../migration/1629004542760-chart-reindex.js       |  182 +
 .../migration/1629024377804-deepl-integration.js   |   14 +
 .../migration/1629288472000-fix-channel-userId.js  |   14 +
 .../migration/1629512953000-user-is-deleted.js     |   15 +
 .../migration/1629778475000-deepl-integration2.js  |   14 +
 .../1633068642000-email-required-for-signup.js     |   14 +
 .../migration/1633071909016-user-pending.js        |   16 +
 .../1634486652000-user-public-reactions.js         |   14 +
 .../backend/migration/1634902659689-delete-log.js  |   13 +
 .../migration/1635500777168-note-thread-mute.js    |   26 +
 .../migration/1636197624383-ff-visibility.js       |   16 +
 packages/backend/ormconfig.js                      |   17 +
 packages/backend/package.json                      |  199 +
 packages/backend/src/boot/index.ts                 |   79 +
 packages/backend/src/boot/master.ts                |  194 +
 packages/backend/src/boot/worker.ts                |   20 +
 packages/backend/src/config/index.ts               |    3 +
 packages/backend/src/config/load.ts                |   61 +
 packages/backend/src/config/types.ts               |   85 +
 packages/backend/src/const.ts                      |    2 +
 packages/backend/src/daemons/janitor.ts            |   20 +
 packages/backend/src/daemons/queue-stats.ts        |   60 +
 packages/backend/src/daemons/server-stats.ts       |   79 +
 packages/backend/src/db/elasticsearch.ts           |   56 +
 packages/backend/src/db/logger.ts                  |    3 +
 packages/backend/src/db/postgre.ts                 |  225 +
 packages/backend/src/db/redis.ts                   |   19 +
 packages/backend/src/env.ts                        |   20 +
 packages/backend/src/games/reversi/core.ts         |  263 +
 packages/backend/src/games/reversi/maps.ts         |  896 +++
 packages/backend/src/games/reversi/package.json    |   18 +
 packages/backend/src/games/reversi/tsconfig.json   |   21 +
 packages/backend/src/global.d.ts                   |    1 +
 packages/backend/src/index.ts                      |   11 +
 packages/backend/src/mfm/from-html.ts              |  209 +
 packages/backend/src/mfm/to-html.ts                |  153 +
 packages/backend/src/misc/antenna-cache.ts         |   36 +
 packages/backend/src/misc/api-permissions.ts       |   35 +
 packages/backend/src/misc/app-lock.ts              |   31 +
 packages/backend/src/misc/before-shutdown.ts       |   92 +
 packages/backend/src/misc/cache.ts                 |   43 +
 packages/backend/src/misc/cafy-id.ts               |   32 +
 packages/backend/src/misc/captcha.ts               |   56 +
 packages/backend/src/misc/check-hit-antenna.ts     |   90 +
 packages/backend/src/misc/check-word-mute.ts       |   39 +
 packages/backend/src/misc/content-disposition.ts   |    6 +
 packages/backend/src/misc/convert-host.ts          |   26 +
 packages/backend/src/misc/count-same-renotes.ts    |   15 +
 packages/backend/src/misc/create-temp.ts           |   10 +
 packages/backend/src/misc/detect-url-mime.ts       |   15 +
 packages/backend/src/misc/download-text-file.ts    |   25 +
 packages/backend/src/misc/download-url.ts          |   87 +
 packages/backend/src/misc/emoji-regex.ts           |    3 +
 .../src/misc/extract-custom-emojis-from-mfm.ts     |   10 +
 packages/backend/src/misc/extract-hashtags.ts      |    9 +
 packages/backend/src/misc/extract-mentions.ts      |   11 +
 packages/backend/src/misc/fetch-meta.ts            |   35 +
 packages/backend/src/misc/fetch-proxy-account.ts   |    9 +
 packages/backend/src/misc/fetch.ts                 |  141 +
 packages/backend/src/misc/gen-avatar.ts            |   90 +
 packages/backend/src/misc/gen-id.ts                |   21 +
 packages/backend/src/misc/gen-key-pair.ts          |   36 +
 packages/backend/src/misc/get-file-info.ts         |  196 +
 packages/backend/src/misc/get-note-summary.ts      |   54 +
 packages/backend/src/misc/get-reaction-emoji.ts    |   16 +
 packages/backend/src/misc/hard-limits.ts           |   14 +
 packages/backend/src/misc/i18n.ts                  |   29 +
 packages/backend/src/misc/id/aid.ts                |   25 +
 packages/backend/src/misc/id/meid.ts               |   26 +
 packages/backend/src/misc/id/meidg.ts              |   28 +
 packages/backend/src/misc/id/object-id.ts          |   26 +
 packages/backend/src/misc/identifiable-error.ts    |   13 +
 .../backend/src/misc/is-blocker-user-related.ts    |   15 +
 .../src/misc/is-duplicate-key-value-error.ts       |    3 +
 packages/backend/src/misc/is-muted-user-related.ts |   15 +
 packages/backend/src/misc/is-quote.ts              |    5 +
 packages/backend/src/misc/keypair-store.ts         |   10 +
 packages/backend/src/misc/normalize-for-search.ts  |    6 +
 packages/backend/src/misc/nyaize.ts                |   15 +
 packages/backend/src/misc/populate-emojis.ts       |  124 +
 packages/backend/src/misc/reaction-lib.ts          |  129 +
 packages/backend/src/misc/safe-for-sql.ts          |    3 +
 packages/backend/src/misc/schema.ts                |  107 +
 packages/backend/src/misc/secure-rndstr.ts         |   21 +
 packages/backend/src/misc/show-machine-info.ts     |   13 +
 packages/backend/src/misc/simple-schema.ts         |   15 +
 packages/backend/src/misc/truncate.ts              |   11 +
 .../src/models/entities/abuse-user-report.ts       |   74 +
 .../backend/src/models/entities/access-token.ts    |   96 +
 packages/backend/src/models/entities/ad.ts         |   59 +
 .../src/models/entities/announcement-read.ts       |   36 +
 .../backend/src/models/entities/announcement.ts    |   43 +
 .../backend/src/models/entities/antenna-note.ts    |   43 +
 packages/backend/src/models/entities/antenna.ts    |   99 +
 packages/backend/src/models/entities/app.ts        |   60 +
 .../src/models/entities/attestation-challenge.ts   |   46 +
 .../backend/src/models/entities/auth-session.ts    |   43 +
 packages/backend/src/models/entities/blocking.ts   |   42 +
 .../src/models/entities/channel-following.ts       |   43 +
 .../src/models/entities/channel-note-pining.ts     |   35 +
 packages/backend/src/models/entities/channel.ts    |   75 +
 packages/backend/src/models/entities/clip-note.ts  |   37 +
 packages/backend/src/models/entities/clip.ts       |   44 +
 packages/backend/src/models/entities/drive-file.ts |  164 +
 .../backend/src/models/entities/drive-folder.ts    |   49 +
 packages/backend/src/models/entities/emoji.ts      |   51 +
 .../backend/src/models/entities/follow-request.ts  |   85 +
 packages/backend/src/models/entities/following.ts  |   80 +
 .../backend/src/models/entities/gallery-like.ts    |   33 +
 .../backend/src/models/entities/gallery-post.ts    |   79 +
 .../src/models/entities/games/reversi/game.ts      |  133 +
 .../src/models/entities/games/reversi/matching.ts  |   35 +
 packages/backend/src/models/entities/hashtag.ts    |   87 +
 packages/backend/src/models/entities/instance.ts   |  180 +
 .../src/models/entities/messaging-message.ts       |   89 +
 packages/backend/src/models/entities/meta.ts       |  423 +
 .../backend/src/models/entities/moderation-log.ts  |   32 +
 packages/backend/src/models/entities/muted-note.ts |   48 +
 packages/backend/src/models/entities/muting.ts     |   42 +
 .../backend/src/models/entities/note-favorite.ts   |   35 +
 .../backend/src/models/entities/note-reaction.ts   |   44 +
 .../src/models/entities/note-thread-muting.ts      |   33 +
 .../backend/src/models/entities/note-unread.ts     |   63 +
 .../backend/src/models/entities/note-watching.ts   |   52 +
 packages/backend/src/models/entities/note.ts       |  247 +
 .../backend/src/models/entities/notification.ts    |  172 +
 packages/backend/src/models/entities/page-like.ts  |   33 +
 packages/backend/src/models/entities/page.ts       |  121 +
 .../src/models/entities/password-reset-request.ts  |   30 +
 packages/backend/src/models/entities/poll-vote.ts  |   40 +
 packages/backend/src/models/entities/poll.ts       |   72 +
 packages/backend/src/models/entities/promo-note.ts |   28 +
 packages/backend/src/models/entities/promo-read.ts |   35 +
 .../src/models/entities/registration-tickets.ts    |   17 +
 .../backend/src/models/entities/registry-item.ts   |   58 +
 packages/backend/src/models/entities/relay.ts      |   19 +
 packages/backend/src/models/entities/signin.ts     |   35 +
 .../backend/src/models/entities/sw-subscription.ts |   37 +
 .../backend/src/models/entities/used-username.ts   |   20 +
 .../src/models/entities/user-group-invitation.ts   |   42 +
 .../src/models/entities/user-group-joining.ts      |   42 +
 packages/backend/src/models/entities/user-group.ts |   46 +
 .../backend/src/models/entities/user-keypair.ts    |   33 +
 .../src/models/entities/user-list-joining.ts       |   42 +
 packages/backend/src/models/entities/user-list.ts  |   33 +
 .../src/models/entities/user-note-pining.ts        |   35 +
 .../backend/src/models/entities/user-pending.ts    |   32 +
 .../backend/src/models/entities/user-profile.ts    |  215 +
 .../backend/src/models/entities/user-publickey.ts  |   34 +
 .../src/models/entities/user-security-key.ts       |   48 +
 packages/backend/src/models/entities/user.ts       |  250 +
 packages/backend/src/models/id.ts                  |    4 +
 packages/backend/src/models/index.ts               |  130 +
 .../src/models/repositories/abuse-user-report.ts   |   38 +
 .../backend/src/models/repositories/antenna.ts     |  124 +
 packages/backend/src/models/repositories/app.ts    |   75 +
 .../src/models/repositories/auth-session.ts        |   21 +
 .../backend/src/models/repositories/blocking.ts    |   60 +
 .../backend/src/models/repositories/channel.ts     |   95 +
 packages/backend/src/models/repositories/clip.ts   |   70 +
 .../backend/src/models/repositories/drive-file.ts  |  249 +
 .../src/models/repositories/drive-folder.ts        |   91 +
 packages/backend/src/models/repositories/emoji.ts  |   65 +
 .../src/models/repositories/federation-instance.ts |  106 +
 .../src/models/repositories/follow-request.ts      |   20 +
 .../backend/src/models/repositories/following.ts   |  124 +
 .../src/models/repositories/gallery-like.ts        |   25 +
 .../src/models/repositories/gallery-post.ts        |  111 +
 .../src/models/repositories/games/reversi/game.ts  |  191 +
 .../models/repositories/games/reversi/matching.ts  |   69 +
 .../backend/src/models/repositories/hashtag.ts     |   62 +
 .../src/models/repositories/messaging-message.ts   |  119 +
 .../src/models/repositories/moderation-logs.ts     |   30 +
 packages/backend/src/models/repositories/muting.ts |   60 +
 .../src/models/repositories/note-favorite.ts       |   56 +
 .../src/models/repositories/note-reaction.ts       |   60 +
 packages/backend/src/models/repositories/note.ts   |  512 ++
 .../src/models/repositories/notification.ts        |  175 +
 .../backend/src/models/repositories/page-like.ts   |   26 +
 packages/backend/src/models/repositories/page.ts   |  142 +
 packages/backend/src/models/repositories/queue.ts  |   30 +
 packages/backend/src/models/repositories/relay.ts  |    6 +
 packages/backend/src/models/repositories/signin.ts |   11 +
 .../models/repositories/user-group-invitation.ts   |   23 +
 .../backend/src/models/repositories/user-group.ts  |   61 +
 .../backend/src/models/repositories/user-list.ts   |   55 +
 packages/backend/src/models/repositories/user.ts   |  659 ++
 packages/backend/src/prelude/README.md             |    3 +
 packages/backend/src/prelude/array.ts              |  138 +
 packages/backend/src/prelude/await-all.ts          |   23 +
 packages/backend/src/prelude/math.ts               |    3 +
 packages/backend/src/prelude/maybe.ts              |   20 +
 packages/backend/src/prelude/relation.ts           |    5 +
 packages/backend/src/prelude/string.ts             |   15 +
 packages/backend/src/prelude/symbol.ts             |    1 +
 packages/backend/src/prelude/time.ts               |   39 +
 packages/backend/src/prelude/url.ts                |   13 +
 packages/backend/src/prelude/xml.ts                |   41 +
 packages/backend/src/queue/get-job-info.ts         |   15 +
 packages/backend/src/queue/index.ts                |  255 +
 packages/backend/src/queue/initialize.ts           |   33 +
 packages/backend/src/queue/logger.ts               |    3 +
 .../src/queue/processors/db/delete-account.ts      |   94 +
 .../src/queue/processors/db/delete-drive-files.ts  |   56 +
 .../src/queue/processors/db/export-blocking.ts     |   94 +
 .../src/queue/processors/db/export-following.ts    |   94 +
 .../backend/src/queue/processors/db/export-mute.ts |   94 +
 .../src/queue/processors/db/export-notes.ts        |  133 +
 .../src/queue/processors/db/export-user-lists.ts   |   71 +
 .../src/queue/processors/db/import-blocking.ts     |   74 +
 .../src/queue/processors/db/import-following.ts    |   73 +
 .../src/queue/processors/db/import-muting.ts       |   83 +
 .../src/queue/processors/db/import-user-lists.ts   |   80 +
 packages/backend/src/queue/processors/db/index.ts  |   33 +
 packages/backend/src/queue/processors/deliver.ts   |   94 +
 packages/backend/src/queue/processors/inbox.ts     |  149 +
 .../object-storage/clean-remote-files.ts           |   50 +
 .../queue/processors/object-storage/delete-file.ts |   11 +
 .../src/queue/processors/object-storage/index.ts   |   15 +
 .../backend/src/queue/processors/system/index.ts   |   12 +
 .../src/queue/processors/system/resync-charts.ts   |   21 +
 packages/backend/src/queue/queues.ts               |    9 +
 packages/backend/src/queue/types.ts                |   44 +
 .../backend/src/remote/activitypub/ap-request.ts   |  104 +
 .../backend/src/remote/activitypub/audience.ts     |   92 +
 .../backend/src/remote/activitypub/db-resolver.ts  |  140 +
 .../src/remote/activitypub/deliver-manager.ts      |  131 +
 .../src/remote/activitypub/kernel/accept/follow.ts |   29 +
 .../src/remote/activitypub/kernel/accept/index.ts  |   24 +
 .../src/remote/activitypub/kernel/add/index.ts     |   23 +
 .../remote/activitypub/kernel/announce/index.ts    |   19 +
 .../src/remote/activitypub/kernel/announce/note.ts |   67 +
 .../src/remote/activitypub/kernel/block/index.ts   |   22 +
 .../src/remote/activitypub/kernel/create/index.ts  |   43 +
 .../src/remote/activitypub/kernel/create/note.ts   |   44 +
 .../src/remote/activitypub/kernel/delete/actor.ts  |   26 +
 .../src/remote/activitypub/kernel/delete/index.ts  |   49 +
 .../src/remote/activitypub/kernel/delete/note.ts   |   41 +
 .../src/remote/activitypub/kernel/flag/index.ts    |   30 +
 .../src/remote/activitypub/kernel/follow.ts        |   20 +
 .../backend/src/remote/activitypub/kernel/index.ts |   71 +
 .../backend/src/remote/activitypub/kernel/like.ts  |   21 +
 .../src/remote/activitypub/kernel/move/index.ts    |    0
 .../backend/src/remote/activitypub/kernel/read.ts  |   27 +
 .../src/remote/activitypub/kernel/reject/follow.ts |   29 +
 .../src/remote/activitypub/kernel/reject/index.ts  |   24 +
 .../src/remote/activitypub/kernel/remove/index.ts  |   23 +
 .../src/remote/activitypub/kernel/undo/announce.ts |   17 +
 .../src/remote/activitypub/kernel/undo/block.ts    |   20 +
 .../src/remote/activitypub/kernel/undo/follow.ts   |   41 +
 .../src/remote/activitypub/kernel/undo/index.ts    |   34 +
 .../src/remote/activitypub/kernel/undo/like.ts     |   21 +
 .../src/remote/activitypub/kernel/update/index.ts  |   34 +
 packages/backend/src/remote/activitypub/logger.ts  |    3 +
 .../src/remote/activitypub/misc/contexts.ts        |  526 ++
 .../src/remote/activitypub/misc/get-note-html.ts   |   10 +
 .../src/remote/activitypub/misc/html-to-mfm.ts     |    9 +
 .../src/remote/activitypub/misc/ld-signature.ts    |  134 +
 .../backend/src/remote/activitypub/models/icon.ts  |    5 +
 .../src/remote/activitypub/models/identifier.ts    |    5 +
 .../backend/src/remote/activitypub/models/image.ts |   62 +
 .../src/remote/activitypub/models/mention.ts       |   24 +
 .../backend/src/remote/activitypub/models/note.ts  |  356 +
 .../src/remote/activitypub/models/person.ts        |  494 ++
 .../src/remote/activitypub/models/question.ts      |   83 +
 .../backend/src/remote/activitypub/models/tag.ts   |   18 +
 packages/backend/src/remote/activitypub/perform.ts |    7 +
 .../src/remote/activitypub/renderer/accept.ts      |    8 +
 .../backend/src/remote/activitypub/renderer/add.ts |    9 +
 .../src/remote/activitypub/renderer/announce.ts    |   29 +
 .../src/remote/activitypub/renderer/block.ts       |    8 +
 .../src/remote/activitypub/renderer/create.ts      |   17 +
 .../src/remote/activitypub/renderer/delete.ts      |    9 +
 .../src/remote/activitypub/renderer/document.ts    |    9 +
 .../src/remote/activitypub/renderer/emoji.ts       |   14 +
 .../remote/activitypub/renderer/follow-relay.ts    |   14 +
 .../src/remote/activitypub/renderer/follow-user.ts |   12 +
 .../src/remote/activitypub/renderer/follow.ts      |   15 +
 .../src/remote/activitypub/renderer/hashtag.ts     |    7 +
 .../src/remote/activitypub/renderer/image.ts       |    9 +
 .../src/remote/activitypub/renderer/index.ts       |   59 +
 .../backend/src/remote/activitypub/renderer/key.ts |   14 +
 .../src/remote/activitypub/renderer/like.ts        |   30 +
 .../src/remote/activitypub/renderer/mention.ts     |    9 +
 .../src/remote/activitypub/renderer/note.ts        |  168 +
 .../renderer/ordered-collection-page.ts            |   23 +
 .../activitypub/renderer/ordered-collection.ts     |   21 +
 .../src/remote/activitypub/renderer/person.ts      |   89 +
 .../src/remote/activitypub/renderer/question.ts    |   23 +
 .../src/remote/activitypub/renderer/read.ts        |    9 +
 .../src/remote/activitypub/renderer/reject.ts      |    8 +
 .../src/remote/activitypub/renderer/remove.ts      |    9 +
 .../src/remote/activitypub/renderer/tombstone.ts   |    4 +
 .../src/remote/activitypub/renderer/undo.ts        |   13 +
 .../src/remote/activitypub/renderer/update.ts      |   15 +
 .../src/remote/activitypub/renderer/vote.ts        |   23 +
 packages/backend/src/remote/activitypub/request.ts |   58 +
 .../backend/src/remote/activitypub/resolver.ts     |   73 +
 packages/backend/src/remote/activitypub/type.ts    |  289 +
 packages/backend/src/remote/logger.ts              |    3 +
 packages/backend/src/remote/resolve-user.ts        |  110 +
 packages/backend/src/remote/webfinger.ts           |   34 +
 packages/backend/src/server/activitypub.ts         |  227 +
 .../backend/src/server/activitypub/featured.ts     |   41 +
 .../backend/src/server/activitypub/followers.ts    |  102 +
 .../backend/src/server/activitypub/following.ts    |  103 +
 packages/backend/src/server/activitypub/outbox.ts  |  108 +
 packages/backend/src/server/api/2fa.ts             |  422 +
 packages/backend/src/server/api/api-handler.ts     |   51 +
 packages/backend/src/server/api/authenticate.ts    |   62 +
 packages/backend/src/server/api/call.ts            |  109 +
 .../src/server/api/common/generate-block-query.ts  |   42 +
 .../server/api/common/generate-channel-query.ts    |   24 +
 .../server/api/common/generate-muted-note-query.ts |   13 +
 .../api/common/generate-muted-note-thread-query.ts |   17 +
 .../server/api/common/generate-muted-user-query.ts |   40 +
 .../api/common/generate-native-user-token.ts       |    3 +
 .../server/api/common/generate-replies-query.ts    |   27 +
 .../server/api/common/generate-visibility-query.ts |   40 +
 packages/backend/src/server/api/common/getters.ts  |   56 +
 .../src/server/api/common/inject-featured.ts       |   56 +
 .../backend/src/server/api/common/inject-promo.ts  |   34 +
 .../src/server/api/common/is-native-token.ts       |    1 +
 .../src/server/api/common/make-pagination-query.ts |   28 +
 .../server/api/common/read-messaging-message.ts    |  122 +
 .../src/server/api/common/read-notification.ts     |   43 +
 packages/backend/src/server/api/common/signin.ts   |   44 +
 packages/backend/src/server/api/common/signup.ts   |  113 +
 packages/backend/src/server/api/define.ts          |   87 +
 packages/backend/src/server/api/endpoints.ts       |  124 +
 .../api/endpoints/admin/abuse-user-reports.ts      |  134 +
 .../server/api/endpoints/admin/accounts/create.ts  |   51 +
 .../server/api/endpoints/admin/accounts/delete.ts  |   58 +
 .../src/server/api/endpoints/admin/ad/create.ts    |   49 +
 .../src/server/api/endpoints/admin/ad/delete.ts    |   34 +
 .../src/server/api/endpoints/admin/ad/list.ts      |   36 +
 .../src/server/api/endpoints/admin/ad/update.ts    |   63 +
 .../api/endpoints/admin/announcements/create.ts    |   71 +
 .../api/endpoints/admin/announcements/delete.ts    |   34 +
 .../api/endpoints/admin/announcements/list.ts      |   84 +
 .../api/endpoints/admin/announcements/update.ts    |   48 +
 .../endpoints/admin/delete-all-files-of-a-user.ts  |   28 +
 .../src/server/api/endpoints/admin/delete-logs.ts  |   13 +
 .../endpoints/admin/drive/clean-remote-files.ts    |   13 +
 .../server/api/endpoints/admin/drive/cleanup.ts    |   21 +
 .../src/server/api/endpoints/admin/drive/files.ts  |   81 +
 .../server/api/endpoints/admin/drive/show-file.ts  |  180 +
 .../src/server/api/endpoints/admin/emoji/add.ts    |   64 +
 .../src/server/api/endpoints/admin/emoji/copy.ts   |   81 +
 .../api/endpoints/admin/emoji/list-remote.ts       |   99 +
 .../src/server/api/endpoints/admin/emoji/list.ts   |   98 +
 .../src/server/api/endpoints/admin/emoji/remove.ts |   42 +
 .../src/server/api/endpoints/admin/emoji/update.ts |   54 +
 .../endpoints/admin/federation/delete-all-files.ts |   27 +
 .../federation/refresh-remote-instance-metadata.ts |   28 +
 .../admin/federation/remove-all-following.ts       |   32 +
 .../endpoints/admin/federation/update-instance.ts  |   33 +
 .../server/api/endpoints/admin/get-index-stats.ts  |   26 +
 .../server/api/endpoints/admin/get-table-stats.ts  |   45 +
 .../src/server/api/endpoints/admin/invite.ts       |   44 +
 .../server/api/endpoints/admin/moderators/add.ts   |   33 +
 .../api/endpoints/admin/moderators/remove.ts       |   29 +
 .../src/server/api/endpoints/admin/promo/create.ts |   57 +
 .../src/server/api/endpoints/admin/queue/clear.ts  |   18 +
 .../api/endpoints/admin/queue/deliver-delayed.ts   |   55 +
 .../api/endpoints/admin/queue/inbox-delayed.ts     |   55 +
 .../src/server/api/endpoints/admin/queue/jobs.ts   |   81 +
 .../src/server/api/endpoints/admin/queue/stats.ts  |   44 +
 .../src/server/api/endpoints/admin/relays/add.ts   |   63 +
 .../src/server/api/endpoints/admin/relays/list.ts  |   47 +
 .../server/api/endpoints/admin/relays/remove.ts    |   20 +
 .../server/api/endpoints/admin/reset-password.ts   |   59 +
 .../endpoints/admin/resolve-abuse-user-report.ts   |   30 +
 .../src/server/api/endpoints/admin/resync-chart.ts |   21 +
 .../src/server/api/endpoints/admin/send-email.ts   |   26 +
 .../src/server/api/endpoints/admin/server-info.ts  |  119 +
 .../api/endpoints/admin/show-moderation-logs.ts    |   74 +
 .../src/server/api/endpoints/admin/show-user.ts    |  177 +
 .../src/server/api/endpoints/admin/show-users.ts   |  119 +
 .../src/server/api/endpoints/admin/silence-user.ts |   38 +
 .../src/server/api/endpoints/admin/suspend-user.ts |   84 +
 .../server/api/endpoints/admin/unsilence-user.ts   |   34 +
 .../server/api/endpoints/admin/unsuspend-user.ts   |   37 +
 .../src/server/api/endpoints/admin/update-meta.ts  |  608 ++
 .../src/server/api/endpoints/admin/vacuum.ts       |   36 +
 .../src/server/api/endpoints/announcements.ts      |   92 +
 .../src/server/api/endpoints/antennas/create.ts    |  127 +
 .../src/server/api/endpoints/antennas/delete.ts    |   43 +
 .../src/server/api/endpoints/antennas/list.ts      |   28 +
 .../src/server/api/endpoints/antennas/notes.ts     |   93 +
 .../src/server/api/endpoints/antennas/show.ts      |   47 +
 .../src/server/api/endpoints/antennas/update.ts    |  143 +
 .../backend/src/server/api/endpoints/ap/get.ts     |   36 +
 .../backend/src/server/api/endpoints/ap/show.ts    |  190 +
 .../backend/src/server/api/endpoints/app/create.ts |   63 +
 .../backend/src/server/api/endpoints/app/show.ts   |   51 +
 .../src/server/api/endpoints/auth/accept.ts        |   76 +
 .../server/api/endpoints/auth/session/generate.ts  |   70 +
 .../src/server/api/endpoints/auth/session/show.ts  |   58 +
 .../server/api/endpoints/auth/session/userkey.ts   |   98 +
 .../src/server/api/endpoints/blocking/create.ts    |   89 +
 .../src/server/api/endpoints/blocking/delete.ts    |   85 +
 .../src/server/api/endpoints/blocking/list.ts      |   49 +
 .../src/server/api/endpoints/channels/create.ts    |   68 +
 .../src/server/api/endpoints/channels/featured.ts  |   28 +
 .../src/server/api/endpoints/channels/follow.ts    |   48 +
 .../src/server/api/endpoints/channels/followed.ts  |   49 +
 .../src/server/api/endpoints/channels/owned.ts     |   49 +
 .../src/server/api/endpoints/channels/pin-note.ts  |    0
 .../src/server/api/endpoints/channels/show.ts      |   43 +
 .../src/server/api/endpoints/channels/timeline.ts  |   85 +
 .../src/server/api/endpoints/channels/unfollow.ts  |   45 +
 .../src/server/api/endpoints/channels/update.ts    |   94 +
 .../server/api/endpoints/charts/active-users.ts    |   30 +
 .../src/server/api/endpoints/charts/drive.ts       |   30 +
 .../src/server/api/endpoints/charts/federation.ts  |   30 +
 .../src/server/api/endpoints/charts/hashtag.ts     |   34 +
 .../src/server/api/endpoints/charts/instance.ts    |   34 +
 .../src/server/api/endpoints/charts/network.ts     |   30 +
 .../src/server/api/endpoints/charts/notes.ts       |   30 +
 .../src/server/api/endpoints/charts/user/drive.ts  |   35 +
 .../server/api/endpoints/charts/user/following.ts  |   35 +
 .../src/server/api/endpoints/charts/user/notes.ts  |   35 +
 .../server/api/endpoints/charts/user/reactions.ts  |   35 +
 .../src/server/api/endpoints/charts/users.ts       |   30 +
 .../src/server/api/endpoints/clips/add-note.ts     |   76 +
 .../src/server/api/endpoints/clips/create.ts       |   45 +
 .../src/server/api/endpoints/clips/delete.ts       |   40 +
 .../backend/src/server/api/endpoints/clips/list.ts |   28 +
 .../src/server/api/endpoints/clips/notes.ts        |   93 +
 .../backend/src/server/api/endpoints/clips/show.ts |   50 +
 .../src/server/api/endpoints/clips/update.ts       |   65 +
 packages/backend/src/server/api/endpoints/drive.ts |   38 +
 .../src/server/api/endpoints/drive/files.ts        |   70 +
 .../api/endpoints/drive/files/attached-notes.ts    |   57 +
 .../api/endpoints/drive/files/check-existence.ts   |   31 +
 .../src/server/api/endpoints/drive/files/create.ts |   89 +
 .../src/server/api/endpoints/drive/files/delete.ts |   53 +
 .../api/endpoints/drive/files/find-by-hash.ts      |   36 +
 .../src/server/api/endpoints/drive/files/find.ts   |   43 +
 .../src/server/api/endpoints/drive/files/show.ts   |   84 +
 .../src/server/api/endpoints/drive/files/update.ts |  116 +
 .../api/endpoints/drive/files/upload-from-url.ts   |   64 +
 .../src/server/api/endpoints/drive/folders.ts      |   58 +
 .../server/api/endpoints/drive/folders/create.ts   |   72 +
 .../server/api/endpoints/drive/folders/delete.ts   |   60 +
 .../src/server/api/endpoints/drive/folders/find.ts |   43 +
 .../src/server/api/endpoints/drive/folders/show.ts |   49 +
 .../server/api/endpoints/drive/folders/update.ts   |  123 +
 .../src/server/api/endpoints/drive/stream.ts       |   59 +
 .../api/endpoints/email-address/available.ts       |   34 +
 .../backend/src/server/api/endpoints/endpoint.ts   |   26 +
 .../backend/src/server/api/endpoints/endpoints.ts  |   30 +
 .../src/server/api/endpoints/federation/dns.ts     |   43 +
 .../server/api/endpoints/federation/followers.ts   |   51 +
 .../server/api/endpoints/federation/following.ts   |   51 +
 .../server/api/endpoints/federation/instances.ts   |  149 +
 .../api/endpoints/federation/show-instance.ts      |   29 +
 .../api/endpoints/federation/update-remote-user.ts |   22 +
 .../src/server/api/endpoints/federation/users.ts   |   51 +
 .../src/server/api/endpoints/following/create.ts   |  100 +
 .../src/server/api/endpoints/following/delete.ts   |   82 +
 .../api/endpoints/following/requests/accept.ts     |   48 +
 .../api/endpoints/following/requests/cancel.ts     |   58 +
 .../api/endpoints/following/requests/list.ts       |   44 +
 .../api/endpoints/following/requests/reject.ts     |   40 +
 .../src/server/api/endpoints/gallery/featured.ts   |   29 +
 .../src/server/api/endpoints/gallery/popular.ts    |   28 +
 .../src/server/api/endpoints/gallery/posts.ts      |   43 +
 .../server/api/endpoints/gallery/posts/create.ts   |   77 +
 .../server/api/endpoints/gallery/posts/delete.ts   |   40 +
 .../src/server/api/endpoints/gallery/posts/like.ts |   71 +
 .../src/server/api/endpoints/gallery/posts/show.ts |   43 +
 .../server/api/endpoints/gallery/posts/unlike.ts   |   54 +
 .../server/api/endpoints/gallery/posts/update.ts   |   82 +
 .../server/api/endpoints/games/reversi/games.ts    |  156 +
 .../api/endpoints/games/reversi/games/show.ts      |  168 +
 .../api/endpoints/games/reversi/games/surrender.ts |   67 +
 .../api/endpoints/games/reversi/invitations.ts     |   58 +
 .../server/api/endpoints/games/reversi/match.ts    |  108 +
 .../api/endpoints/games/reversi/match/cancel.ts    |   14 +
 .../server/api/endpoints/get-online-users-count.ts |   23 +
 .../src/server/api/endpoints/hashtags/list.ts      |   95 +
 .../src/server/api/endpoints/hashtags/search.ts    |   46 +
 .../src/server/api/endpoints/hashtags/show.ts      |   40 +
 .../src/server/api/endpoints/hashtags/trend.ts     |  146 +
 .../src/server/api/endpoints/hashtags/users.ts     |   89 +
 packages/backend/src/server/api/endpoints/i.ts     |   26 +
 .../backend/src/server/api/endpoints/i/2fa/done.ts |   41 +
 .../src/server/api/endpoints/i/2fa/key-done.ts     |  150 +
 .../server/api/endpoints/i/2fa/password-less.ts    |   21 +
 .../src/server/api/endpoints/i/2fa/register-key.ts |   59 +
 .../src/server/api/endpoints/i/2fa/register.ts     |   54 +
 .../src/server/api/endpoints/i/2fa/remove-key.ts   |   45 +
 .../src/server/api/endpoints/i/2fa/unregister.ts   |   32 +
 .../backend/src/server/api/endpoints/i/apps.ts     |   43 +
 .../src/server/api/endpoints/i/authorized-apps.ts  |   44 +
 .../src/server/api/endpoints/i/change-password.ts  |   39 +
 .../src/server/api/endpoints/i/delete-account.ts   |   48 +
 .../src/server/api/endpoints/i/export-blocking.ts  |   16 +
 .../src/server/api/endpoints/i/export-following.ts |   16 +
 .../src/server/api/endpoints/i/export-mute.ts      |   16 +
 .../src/server/api/endpoints/i/export-notes.ts     |   16 +
 .../server/api/endpoints/i/export-user-lists.ts    |   16 +
 .../src/server/api/endpoints/i/favorites.ts        |   50 +
 .../src/server/api/endpoints/i/gallery/likes.ts    |   57 +
 .../src/server/api/endpoints/i/gallery/posts.ts    |   49 +
 .../api/endpoints/i/get-word-muted-notes-count.ts  |   33 +
 .../src/server/api/endpoints/i/import-blocking.ts  |   60 +
 .../src/server/api/endpoints/i/import-following.ts |   59 +
 .../src/server/api/endpoints/i/import-muting.ts    |   60 +
 .../server/api/endpoints/i/import-user-lists.ts    |   59 +
 .../src/server/api/endpoints/i/notifications.ts    |  138 +
 .../src/server/api/endpoints/i/page-likes.ts       |   57 +
 .../backend/src/server/api/endpoints/i/pages.ts    |   49 +
 packages/backend/src/server/api/endpoints/i/pin.ts |   59 +
 .../api/endpoints/i/read-all-messaging-messages.ts |   37 +
 .../api/endpoints/i/read-all-unread-notes.ts       |   25 +
 .../server/api/endpoints/i/read-announcement.ts    |   60 +
 .../src/server/api/endpoints/i/regenerate-token.ts |   44 +
 .../src/server/api/endpoints/i/registry/get-all.ts |   33 +
 .../server/api/endpoints/i/registry/get-detail.ts  |   48 +
 .../src/server/api/endpoints/i/registry/get.ts     |   45 +
 .../api/endpoints/i/registry/keys-with-type.ts     |   41 +
 .../src/server/api/endpoints/i/registry/keys.ts    |   28 +
 .../src/server/api/endpoints/i/registry/remove.ts  |   45 +
 .../src/server/api/endpoints/i/registry/scopes.ts  |   29 +
 .../src/server/api/endpoints/i/registry/set.ts     |   61 +
 .../src/server/api/endpoints/i/revoke-token.ts     |   31 +
 .../src/server/api/endpoints/i/signin-history.ts   |   35 +
 .../backend/src/server/api/endpoints/i/unpin.ts    |   45 +
 .../src/server/api/endpoints/i/update-email.ts     |   94 +
 .../backend/src/server/api/endpoints/i/update.ts   |  294 +
 .../server/api/endpoints/i/user-group-invites.ts   |   61 +
 .../src/server/api/endpoints/messaging/history.ts  |   94 +
 .../src/server/api/endpoints/messaging/messages.ts |  148 +
 .../api/endpoints/messaging/messages/create.ts     |  148 +
 .../api/endpoints/messaging/messages/delete.ts     |   48 +
 .../api/endpoints/messaging/messages/read.ts       |   48 +
 packages/backend/src/server/api/endpoints/meta.ts  |  598 ++
 .../src/server/api/endpoints/miauth/gen-token.ts   |   72 +
 .../src/server/api/endpoints/mute/create.ts        |   83 +
 .../src/server/api/endpoints/mute/delete.ts        |   73 +
 .../backend/src/server/api/endpoints/mute/list.ts  |   49 +
 .../backend/src/server/api/endpoints/my/apps.ts    |   86 +
 packages/backend/src/server/api/endpoints/notes.ts |   94 +
 .../src/server/api/endpoints/notes/children.ts     |   72 +
 .../src/server/api/endpoints/notes/clips.ts        |   55 +
 .../src/server/api/endpoints/notes/conversation.ts |   81 +
 .../src/server/api/endpoints/notes/create.ts       |  299 +
 .../src/server/api/endpoints/notes/delete.ts       |   56 +
 .../server/api/endpoints/notes/favorites/create.ts |   61 +
 .../server/api/endpoints/notes/favorites/delete.ts |   55 +
 .../src/server/api/endpoints/notes/featured.ts     |   64 +
 .../server/api/endpoints/notes/global-timeline.ts  |  101 +
 .../server/api/endpoints/notes/hybrid-timeline.ts  |  158 +
 .../server/api/endpoints/notes/local-timeline.ts   |  129 +
 .../src/server/api/endpoints/notes/mentions.ts     |   88 +
 .../api/endpoints/notes/polls/recommendation.ts    |   77 +
 .../src/server/api/endpoints/notes/polls/vote.ts   |  170 +
 .../src/server/api/endpoints/notes/reactions.ts    |   90 +
 .../server/api/endpoints/notes/reactions/create.ts |   57 +
 .../server/api/endpoints/notes/reactions/delete.ts |   52 +
 .../src/server/api/endpoints/notes/renotes.ts      |   76 +
 .../src/server/api/endpoints/notes/replies.ts      |   61 +
 .../server/api/endpoints/notes/search-by-tag.ts    |  134 +
 .../src/server/api/endpoints/notes/search.ts       |  152 +
 .../backend/src/server/api/endpoints/notes/show.ts |   43 +
 .../src/server/api/endpoints/notes/state.ts        |   69 +
 .../api/endpoints/notes/thread-muting/create.ts    |   54 +
 .../api/endpoints/notes/thread-muting/delete.ts    |   40 +
 .../src/server/api/endpoints/notes/timeline.ts     |  150 +
 .../src/server/api/endpoints/notes/translate.ts    |   89 +
 .../src/server/api/endpoints/notes/unrenote.ts     |   52 +
 .../api/endpoints/notes/user-list-timeline.ts      |  147 +
 .../server/api/endpoints/notes/watching/create.ts  |   37 +
 .../server/api/endpoints/notes/watching/delete.ts  |   37 +
 .../server/api/endpoints/notifications/create.ts   |   37 +
 .../endpoints/notifications/mark-all-as-read.ts    |   24 +
 .../src/server/api/endpoints/notifications/read.ts |   42 +
 .../backend/src/server/api/endpoints/page-push.ts  |   50 +
 .../src/server/api/endpoints/pages/create.ts       |  128 +
 .../src/server/api/endpoints/pages/delete.ts       |   45 +
 .../src/server/api/endpoints/pages/featured.ts     |   29 +
 .../backend/src/server/api/endpoints/pages/like.ts |   71 +
 .../backend/src/server/api/endpoints/pages/show.ts |   65 +
 .../src/server/api/endpoints/pages/unlike.ts       |   54 +
 .../src/server/api/endpoints/pages/update.ts       |  141 +
 packages/backend/src/server/api/endpoints/ping.ts  |   27 +
 .../src/server/api/endpoints/pinned-users.ts       |   32 +
 .../backend/src/server/api/endpoints/promo/read.ts |   50 +
 .../server/api/endpoints/request-reset-password.ts |   73 +
 .../backend/src/server/api/endpoints/reset-db.ts   |   23 +
 .../src/server/api/endpoints/reset-password.ts     |   45 +
 .../backend/src/server/api/endpoints/room/show.ts  |  159 +
 .../src/server/api/endpoints/room/update.ts        |   51 +
 .../src/server/api/endpoints/server-info.ts        |   35 +
 packages/backend/src/server/api/endpoints/stats.ts |   83 +
 .../src/server/api/endpoints/sw/register.ts        |   74 +
 .../src/server/api/endpoints/sw/unregister.ts      |   22 +
 .../src/server/api/endpoints/username/available.ts |   40 +
 packages/backend/src/server/api/endpoints/users.ts |  101 +
 .../src/server/api/endpoints/users/clips.ts        |   40 +
 .../src/server/api/endpoints/users/followers.ts    |  104 +
 .../src/server/api/endpoints/users/following.ts    |  104 +
 .../server/api/endpoints/users/gallery/posts.ts    |   39 +
 .../users/get-frequently-replied-users.ts          |  105 +
 .../server/api/endpoints/users/groups/create.ts    |   45 +
 .../server/api/endpoints/users/groups/delete.ts    |   40 +
 .../endpoints/users/groups/invitations/accept.ts   |   54 +
 .../endpoints/users/groups/invitations/reject.ts   |   44 +
 .../server/api/endpoints/users/groups/invite.ts    |  102 +
 .../server/api/endpoints/users/groups/joined.ts    |   36 +
 .../src/server/api/endpoints/users/groups/leave.ts |   50 +
 .../src/server/api/endpoints/users/groups/owned.ts |   28 +
 .../src/server/api/endpoints/users/groups/pull.ts  |   69 +
 .../src/server/api/endpoints/users/groups/show.ts  |   55 +
 .../server/api/endpoints/users/groups/transfer.ts  |   83 +
 .../server/api/endpoints/users/groups/update.ts    |   55 +
 .../src/server/api/endpoints/users/lists/create.ts |   36 +
 .../src/server/api/endpoints/users/lists/delete.ts |   40 +
 .../src/server/api/endpoints/users/lists/list.ts   |   28 +
 .../src/server/api/endpoints/users/lists/pull.ts   |   62 +
 .../src/server/api/endpoints/users/lists/push.ts   |   92 +
 .../src/server/api/endpoints/users/lists/show.ts   |   47 +
 .../src/server/api/endpoints/users/lists/update.ts |   55 +
 .../src/server/api/endpoints/users/notes.ts        |  144 +
 .../src/server/api/endpoints/users/pages.ts        |   40 +
 .../src/server/api/endpoints/users/reactions.ts    |   79 +
 .../server/api/endpoints/users/recommendation.ts   |   63 +
 .../src/server/api/endpoints/users/relation.ts     |  111 +
 .../src/server/api/endpoints/users/report-abuse.ts |   90 +
 .../endpoints/users/search-by-username-and-host.ts |  116 +
 .../src/server/api/endpoints/users/search.ts       |  127 +
 .../backend/src/server/api/endpoints/users/show.ts |  105 +
 .../src/server/api/endpoints/users/stats.ts        |  144 +
 packages/backend/src/server/api/error.ts           |   28 +
 packages/backend/src/server/api/index.ts           |  113 +
 packages/backend/src/server/api/limiter.ts         |   83 +
 packages/backend/src/server/api/logger.ts          |    3 +
 packages/backend/src/server/api/openapi/errors.ts  |   69 +
 .../backend/src/server/api/openapi/gen-spec.ts     |  235 +
 packages/backend/src/server/api/openapi/schemas.ts |   56 +
 packages/backend/src/server/api/private/signin.ts  |  232 +
 .../src/server/api/private/signup-pending.ts       |   35 +
 packages/backend/src/server/api/private/signup.ts  |  112 +
 packages/backend/src/server/api/service/discord.ts |  286 +
 packages/backend/src/server/api/service/github.ts  |  257 +
 packages/backend/src/server/api/service/twitter.ts |  194 +
 packages/backend/src/server/api/stream/channel.ts  |   62 +
 .../src/server/api/stream/channels/admin.ts        |   16 +
 .../src/server/api/stream/channels/antenna.ts      |   45 +
 .../src/server/api/stream/channels/channel.ts      |   92 +
 .../src/server/api/stream/channels/drive.ts        |   16 +
 .../api/stream/channels/games/reversi-game.ts      |  372 +
 .../server/api/stream/channels/games/reversi.ts    |   33 +
 .../server/api/stream/channels/global-timeline.ts  |   73 +
 .../src/server/api/stream/channels/hashtag.ts      |   53 +
 .../server/api/stream/channels/home-timeline.ts    |   81 +
 .../server/api/stream/channels/hybrid-timeline.ts  |   89 +
 .../src/server/api/stream/channels/index.ts        |   37 +
 .../server/api/stream/channels/local-timeline.ts   |   74 +
 .../backend/src/server/api/stream/channels/main.ts |   43 +
 .../server/api/stream/channels/messaging-index.ts  |   16 +
 .../src/server/api/stream/channels/messaging.ts    |  106 +
 .../src/server/api/stream/channels/queue-stats.ts  |   41 +
 .../src/server/api/stream/channels/server-stats.ts |   41 +
 .../src/server/api/stream/channels/user-list.ts    |   92 +
 packages/backend/src/server/api/stream/index.ts    |  421 +
 packages/backend/src/server/api/stream/types.ts    |  299 +
 packages/backend/src/server/api/streaming.ts       |   67 +
 .../backend/src/server/file/assets/bad-egg.png     |  Bin 0 -> 1676 bytes
 .../src/server/file/assets/cache-expired.png       |  Bin 0 -> 6048 bytes
 packages/backend/src/server/file/assets/dummy.png  |  Bin 0 -> 6285 bytes
 .../src/server/file/assets/not-an-image.png        |  Bin 0 -> 2780 bytes
 .../server/file/assets/thumbnail-not-available.png |  Bin 0 -> 5705 bytes
 .../backend/src/server/file/assets/tombstone.png   |  Bin 0 -> 5028 bytes
 packages/backend/src/server/file/index.ts          |   41 +
 .../backend/src/server/file/send-drive-file.ts     |  126 +
 packages/backend/src/server/index.ts               |  179 +
 packages/backend/src/server/nodeinfo.ts            |  101 +
 packages/backend/src/server/proxy/index.ts         |   26 +
 packages/backend/src/server/proxy/proxy-media.ts   |   51 +
 packages/backend/src/server/web/bios.css           |   40 +
 packages/backend/src/server/web/bios.js            |   87 +
 packages/backend/src/server/web/boot.js            |  166 +
 packages/backend/src/server/web/cli.css            |   19 +
 packages/backend/src/server/web/cli.js             |   55 +
 packages/backend/src/server/web/feed.ts            |   58 +
 packages/backend/src/server/web/index.ts           |  414 +
 packages/backend/src/server/web/manifest.json      |   28 +
 packages/backend/src/server/web/manifest.ts        |   15 +
 packages/backend/src/server/web/style.css          |   29 +
 packages/backend/src/server/web/url-preview.ts     |   53 +
 packages/backend/src/server/web/views/base.pug     |   60 +
 packages/backend/src/server/web/views/bios.pug     |   20 +
 packages/backend/src/server/web/views/channel.pug  |   21 +
 packages/backend/src/server/web/views/cli.pug      |   21 +
 packages/backend/src/server/web/views/clip.pug     |   33 +
 packages/backend/src/server/web/views/flush.pug    |   47 +
 .../backend/src/server/web/views/gallery-post.pug  |   35 +
 .../backend/src/server/web/views/info-card.pug     |   50 +
 packages/backend/src/server/web/views/note.pug     |   43 +
 packages/backend/src/server/web/views/page.pug     |   33 +
 packages/backend/src/server/web/views/user.pug     |   42 +
 packages/backend/src/server/well-known.ts          |  149 +
 .../backend/src/services/add-note-to-antenna.ts    |   54 +
 packages/backend/src/services/blocking/create.ts   |  129 +
 packages/backend/src/services/blocking/delete.ts   |   29 +
 .../services/chart/charts/classes/active-users.ts  |   47 +
 .../src/services/chart/charts/classes/drive.ts     |   91 +
 .../services/chart/charts/classes/federation.ts    |   62 +
 .../src/services/chart/charts/classes/hashtag.ts   |   47 +
 .../src/services/chart/charts/classes/instance.ts  |  217 +
 .../src/services/chart/charts/classes/network.ts   |   45 +
 .../src/services/chart/charts/classes/notes.ts     |   97 +
 .../chart/charts/classes/per-user-drive.ts         |   64 +
 .../chart/charts/classes/per-user-following.ts     |  121 +
 .../chart/charts/classes/per-user-notes.ts         |   72 +
 .../chart/charts/classes/per-user-reactions.ts     |   44 +
 .../services/chart/charts/classes/test-grouped.ts  |   58 +
 .../services/chart/charts/classes/test-unique.ts   |   36 +
 .../src/services/chart/charts/classes/test.ts      |   69 +
 .../src/services/chart/charts/classes/users.ts     |   76 +
 .../services/chart/charts/schemas/active-users.ts  |   35 +
 .../src/services/chart/charts/schemas/drive.ts     |   68 +
 .../services/chart/charts/schemas/federation.ts    |   29 +
 .../src/services/chart/charts/schemas/hashtag.ts   |   35 +
 .../src/services/chart/charts/schemas/instance.ts  |  157 +
 .../src/services/chart/charts/schemas/network.ts   |   31 +
 .../src/services/chart/charts/schemas/notes.ts     |   56 +
 .../chart/charts/schemas/per-user-drive.ts         |   55 +
 .../chart/charts/schemas/per-user-following.ts     |   86 +
 .../chart/charts/schemas/per-user-notes.ts         |   43 +
 .../chart/charts/schemas/per-user-reactions.ts     |   31 +
 .../services/chart/charts/schemas/test-grouped.ts  |   28 +
 .../services/chart/charts/schemas/test-unique.ts   |   16 +
 .../src/services/chart/charts/schemas/test.ts      |   28 +
 .../src/services/chart/charts/schemas/users.ts     |   44 +
 packages/backend/src/services/chart/core.ts        |  563 ++
 packages/backend/src/services/chart/entities.ts    |   15 +
 packages/backend/src/services/chart/index.ts       |   50 +
 .../backend/src/services/create-notification.ts    |   61 +
 .../backend/src/services/create-system-user.ts     |   67 +
 packages/backend/src/services/drive/add-file.ts    |  466 ++
 packages/backend/src/services/drive/delete-file.ts |  103 +
 .../src/services/drive/generate-video-thumbnail.ts |   37 +
 .../backend/src/services/drive/image-processor.ts  |  107 +
 .../backend/src/services/drive/internal-storage.ts |   35 +
 packages/backend/src/services/drive/logger.ts      |    3 +
 packages/backend/src/services/drive/s3.ts          |   24 +
 .../backend/src/services/drive/upload-from-url.ts  |   62 +
 .../src/services/fetch-instance-metadata.ts        |  265 +
 packages/backend/src/services/following/create.ts  |  180 +
 packages/backend/src/services/following/delete.ts  |   69 +
 .../src/services/following/requests/accept-all.ts  |   18 +
 .../src/services/following/requests/accept.ts      |   31 +
 .../src/services/following/requests/cancel.ts      |   36 +
 .../src/services/following/requests/create.ts      |   63 +
 .../src/services/following/requests/reject.ts      |   46 +
 packages/backend/src/services/i/pin.ts             |   92 +
 packages/backend/src/services/i/update.ts          |   19 +
 .../backend/src/services/insert-moderation-log.ts  |   13 +
 packages/backend/src/services/instance-actor.ts    |   27 +
 packages/backend/src/services/logger.ts            |  127 +
 packages/backend/src/services/messages/create.ts   |  108 +
 packages/backend/src/services/messages/delete.ts   |   30 +
 packages/backend/src/services/note/create.ts       |  645 ++
 packages/backend/src/services/note/delete.ts       |  137 +
 packages/backend/src/services/note/polls/update.ts |   22 +
 packages/backend/src/services/note/polls/vote.ts   |   81 +
 .../backend/src/services/note/reaction/create.ts   |  135 +
 .../backend/src/services/note/reaction/delete.ts   |   58 +
 packages/backend/src/services/note/read.ts         |  132 +
 packages/backend/src/services/note/unread.ts       |   55 +
 packages/backend/src/services/note/unwatch.ts      |   10 +
 packages/backend/src/services/note/watch.ts        |   20 +
 packages/backend/src/services/push-notification.ts |   53 +
 .../src/services/register-or-fetch-instance-doc.ts |   34 +
 packages/backend/src/services/relay.ts             |   94 +
 .../src/services/send-email-notification.ts        |   31 +
 packages/backend/src/services/send-email.ts        |  123 +
 packages/backend/src/services/stream.ts            |  129 +
 packages/backend/src/services/suspend-user.ts      |   34 +
 packages/backend/src/services/unsuspend-user.ts    |   35 +
 packages/backend/src/services/update-hashtag.ts    |  128 +
 packages/backend/src/services/user-list/push.ts    |   27 +
 .../src/services/validate-email-for-account.ts     |   34 +
 packages/backend/src/types.ts                      |    7 +
 packages/backend/tools/accept-migration.js         |   26 +
 packages/backend/tools/accept-migration.js.map     |    1 +
 packages/backend/tools/accept-migration.ts         |   25 +
 packages/backend/tools/add-emoji.js                |   30 +
 packages/backend/tools/add-emoji.js.map            |    1 +
 packages/backend/tools/add-emoji.ts                |   30 +
 packages/backend/tools/demote-admin.js             |   30 +
 packages/backend/tools/demote-admin.js.map         |    1 +
 packages/backend/tools/demote-admin.ts             |   32 +
 packages/backend/tools/mark-admin.js               |   30 +
 packages/backend/tools/mark-admin.js.map           |    1 +
 packages/backend/tools/mark-admin.ts               |   32 +
 packages/backend/tools/refresh-question.js         |   14 +
 packages/backend/tools/refresh-question.js.map     |    1 +
 packages/backend/tools/refresh-question.ts         |   14 +
 packages/backend/tools/resync-remote-user.js       |   26 +
 packages/backend/tools/resync-remote-user.js.map   |    1 +
 packages/backend/tools/resync-remote-user.ts       |   30 +
 packages/backend/tools/show-signin-history.js      |   47 +
 packages/backend/tools/show-signin-history.js.map  |    1 +
 packages/backend/tools/show-signin-history.ts      |   56 +
 packages/backend/tsconfig.json                     |   43 +
 packages/backend/yarn.lock                         | 8274 ++++++++++++++++++++
 packages/client/.eslintrc                          |   32 +
 packages/client/.npmrc                             |    2 +
 packages/client/@types/global.d.ts                 |   10 +
 packages/client/@types/vue.d.ts                    |    5 +
 packages/client/assets/about-icon.png              |  Bin 0 -> 20528 bytes
 packages/client/assets/fedi.jpg                    |  Bin 0 -> 77752 bytes
 packages/client/assets/label-red.svg               |    6 +
 packages/client/assets/label.svg                   |    6 +
 packages/client/assets/misskey.svg                 |    7 +
 packages/client/assets/remove.png                  |  Bin 0 -> 424 bytes
 .../assets/room/furnitures/banknote/banknote.blend |  Bin 0 -> 716076 bytes
 .../assets/room/furnitures/banknote/banknote.glb   |  Bin 0 -> 48988 bytes
 .../client/assets/room/furnitures/banknote/tex.png |  Bin 0 -> 43794 bytes
 .../client/assets/room/furnitures/bed/bed.blend    |  Bin 0 -> 573584 bytes
 packages/client/assets/room/furnitures/bed/bed.glb |  Bin 0 -> 8944 bytes
 .../client/assets/room/furnitures/bin/bin.blend    |  Bin 0 -> 695512 bytes
 packages/client/assets/room/furnitures/bin/bin.glb |  Bin 0 -> 22140 bytes
 .../client/assets/room/furnitures/book/book.blend  |  Bin 0 -> 511864 bytes
 .../client/assets/room/furnitures/book/book.glb    |  Bin 0 -> 4304 bytes
 .../assets/room/furnitures/book2/barcode.png       |  Bin 0 -> 3505 bytes
 .../assets/room/furnitures/book2/book2.blend       |  Bin 0 -> 521456 bytes
 .../client/assets/room/furnitures/book2/book2.glb  |  Bin 0 -> 82920 bytes
 .../assets/room/furnitures/book2/texture.afdesign  |  Bin 0 -> 875434 bytes
 .../assets/room/furnitures/book2/texture.png       |  Bin 0 -> 61019 bytes
 .../client/assets/room/furnitures/book2/uv.png     |  Bin 0 -> 8611 bytes
 .../furnitures/cardboard-box/cardboard-box.blend   |  Bin 0 -> 515036 bytes
 .../furnitures/cardboard-box/cardboard-box.glb     |  Bin 0 -> 4568 bytes
 .../furnitures/cardboard-box2/cardboard-box2.blend |  Bin 0 -> 545788 bytes
 .../furnitures/cardboard-box2/cardboard-box2.glb   |  Bin 0 -> 24084 bytes
 .../room/furnitures/cardboard-box2/texture.png     |  Bin 0 -> 16879 bytes
 .../assets/room/furnitures/cardboard-box2/uv.png   |  Bin 0 -> 4630 bytes
 .../furnitures/cardboard-box3/cardboard-box3.blend |  Bin 0 -> 547868 bytes
 .../furnitures/cardboard-box3/cardboard-box3.glb   |  Bin 0 -> 23668 bytes
 .../room/furnitures/cardboard-box3/texture.png     |  Bin 0 -> 16204 bytes
 .../room/furnitures/cardboard-box3/texture.xcf     |  Bin 0 -> 242066 bytes
 .../assets/room/furnitures/cardboard-box3/uv.png   |  Bin 0 -> 3598 bytes
 .../furnitures/carpet-stripe/carpet-stripe.blend   |  Bin 0 -> 481784 bytes
 .../furnitures/carpet-stripe/carpet-stripe.glb     |  Bin 0 -> 4956 bytes
 .../assets/room/furnitures/chair/chair.blend       |  Bin 0 -> 500696 bytes
 .../client/assets/room/furnitures/chair/chair.glb  |  Bin 0 -> 4252 bytes
 .../assets/room/furnitures/chair2/chair2.blend     |  Bin 0 -> 523036 bytes
 .../assets/room/furnitures/chair2/chair2.glb       |  Bin 0 -> 6140 bytes
 .../room/furnitures/color-box/color-box.blend      |  Bin 0 -> 631168 bytes
 .../assets/room/furnitures/color-box/color-box.glb |  Bin 0 -> 7492 bytes
 .../room/furnitures/corkboard/corkboard.blend      |  Bin 0 -> 565404 bytes
 .../assets/room/furnitures/corkboard/corkboard.glb |  Bin 0 -> 7180 bytes
 .../client/assets/room/furnitures/cube/cube.blend  |  Bin 0 -> 646184 bytes
 .../client/assets/room/furnitures/cube/cube.glb    |  Bin 0 -> 188392 bytes
 .../room/furnitures/cup-noodle/cup-noodle.blend    |  Bin 0 -> 855164 bytes
 .../room/furnitures/cup-noodle/cup-noodle.glb      |  Bin 0 -> 318332 bytes
 .../assets/room/furnitures/cup-noodle/noodle.png   |  Bin 0 -> 297154 bytes
 .../client/assets/room/furnitures/desk/desk.blend  |  Bin 0 -> 541980 bytes
 .../client/assets/room/furnitures/desk/desk.glb    |  Bin 0 -> 10184 bytes
 .../assets/room/furnitures/doll-ai/doll-ai.blend   |  Bin 0 -> 1155028 bytes
 .../assets/room/furnitures/doll-ai/doll-ai.glb     |  Bin 0 -> 112160 bytes
 .../assets/room/furnitures/doll-ai/doll_ai_tex.png |  Bin 0 -> 10712 bytes
 .../furnitures/energy-drink/energy-drink.blend     |  Bin 0 -> 724764 bytes
 .../room/furnitures/energy-drink/energy-drink.glb  |  Bin 0 -> 145836 bytes
 .../room/furnitures/energy-drink/texture.afdesign  |  Bin 0 -> 824566 bytes
 .../room/furnitures/energy-drink/texture.png       |  Bin 0 -> 127510 bytes
 .../assets/room/furnitures/energy-drink/uv.png     |  Bin 0 -> 22142 bytes
 .../client/assets/room/furnitures/eraser/cover.png |  Bin 0 -> 8336 bytes
 .../client/assets/room/furnitures/eraser/cover.psd |  Bin 0 -> 219123 bytes
 .../assets/room/furnitures/eraser/eraser-uv.png    |  Bin 0 -> 11164 bytes
 .../assets/room/furnitures/eraser/eraser.blend     |  Bin 0 -> 527996 bytes
 .../assets/room/furnitures/eraser/eraser.glb       |  Bin 0 -> 15748 bytes
 .../furnitures/facial-tissue/facial-tissue-uv.png  |  Bin 0 -> 4470 bytes
 .../furnitures/facial-tissue/facial-tissue.blend   |  Bin 0 -> 533220 bytes
 .../furnitures/facial-tissue/facial-tissue.glb     |  Bin 0 -> 8332 bytes
 .../furnitures/facial-tissue/facial-tissue.png     |  Bin 0 -> 688 bytes
 .../furnitures/facial-tissue/facial-tissue.psd     |  Bin 0 -> 111968 bytes
 .../client/assets/room/furnitures/fan/fan.blend    |  Bin 0 -> 772732 bytes
 packages/client/assets/room/furnitures/fan/fan.glb |  Bin 0 -> 51612 bytes
 .../furnitures/holo-display/holo-display.blend     |  Bin 0 -> 788456 bytes
 .../room/furnitures/holo-display/holo-display.glb  |  Bin 0 -> 148928 bytes
 .../assets/room/furnitures/holo-display/ray-uv.png |  Bin 0 -> 20901 bytes
 .../assets/room/furnitures/holo-display/ray.png    |  Bin 0 -> 104293 bytes
 .../assets/room/furnitures/keyboard/keyboard.blend |  Bin 0 -> 1005588 bytes
 .../assets/room/furnitures/keyboard/keyboard.glb   |  Bin 0 -> 132360 bytes
 .../room/furnitures/low-table/low-table.blend      |  Bin 0 -> 497336 bytes
 .../assets/room/furnitures/low-table/low-table.glb |  Bin 0 -> 2364 bytes
 .../client/assets/room/furnitures/mat/mat.blend    |  Bin 0 -> 489304 bytes
 packages/client/assets/room/furnitures/mat/mat.glb |  Bin 0 -> 1700 bytes
 .../client/assets/room/furnitures/milk/milk-uv.png |  Bin 0 -> 16577 bytes
 .../client/assets/room/furnitures/milk/milk.blend  |  Bin 0 -> 539836 bytes
 .../client/assets/room/furnitures/milk/milk.glb    |  Bin 0 -> 8456 bytes
 .../client/assets/room/furnitures/milk/milk.png    |  Bin 0 -> 658 bytes
 .../client/assets/room/furnitures/milk/milk.psd    |  Bin 0 -> 125415 bytes
 .../assets/room/furnitures/monitor/monitor.blend   |  Bin 0 -> 586820 bytes
 .../assets/room/furnitures/monitor/monitor.glb     |  Bin 0 -> 35300 bytes
 .../assets/room/furnitures/monitor/monitor.psd     |  Bin 0 -> 3692204 bytes
 .../assets/room/furnitures/monitor/screen-uv.png   |  Bin 0 -> 1495 bytes
 .../assets/room/furnitures/monitor/screen.jpg      |  Bin 0 -> 24842 bytes
 .../client/assets/room/furnitures/moon/moon.blend  |  Bin 0 -> 725536 bytes
 .../client/assets/room/furnitures/moon/moon.glb    |  Bin 0 -> 122112 bytes
 .../client/assets/room/furnitures/moon/moon.jpg    |  Bin 0 -> 87151 bytes
 .../assets/room/furnitures/mousepad/mousepad.blend |  Bin 0 -> 495008 bytes
 .../assets/room/furnitures/mousepad/mousepad.glb   |  Bin 0 -> 4360 bytes
 .../assets/room/furnitures/pc/motherboard-uv.png   |  Bin 0 -> 2920 bytes
 .../assets/room/furnitures/pc/motherboard-uv.psd   |  Bin 0 -> 7131432 bytes
 .../assets/room/furnitures/pc/motherboard.jpg      |  Bin 0 -> 64600 bytes
 packages/client/assets/room/furnitures/pc/pc.blend |  Bin 0 -> 818152 bytes
 packages/client/assets/room/furnitures/pc/pc.glb   |  Bin 0 -> 103620 bytes
 .../assets/room/furnitures/pencil/pencil.blend     |  Bin 0 -> 683032 bytes
 .../assets/room/furnitures/pencil/pencil.glb       |  Bin 0 -> 7788 bytes
 .../assets/room/furnitures/photoframe/photo-uv.png |  Bin 0 -> 2639 bytes
 .../assets/room/furnitures/photoframe/photo.jpg    |  Bin 0 -> 30500 bytes
 .../room/furnitures/photoframe/photoframe.blend    |  Bin 0 -> 551416 bytes
 .../room/furnitures/photoframe/photoframe.glb      |  Bin 0 -> 37612 bytes
 .../assets/room/furnitures/piano/piano.blend       |  Bin 0 -> 713228 bytes
 .../client/assets/room/furnitures/piano/piano.glb  |  Bin 0 -> 73320 bytes
 .../assets/room/furnitures/pinguin/pinguin.blend   |  Bin 0 -> 778976 bytes
 .../assets/room/furnitures/pinguin/pinguin.glb     |  Bin 0 -> 60172 bytes
 .../assets/room/furnitures/plant/plant-soil-uv.png |  Bin 0 -> 4509 bytes
 .../assets/room/furnitures/plant/plant-soil.png    |  Bin 0 -> 10852 bytes
 .../assets/room/furnitures/plant/plant-soil.psd    |  Bin 0 -> 390188 bytes
 .../assets/room/furnitures/plant/plant.blend       |  Bin 0 -> 604488 bytes
 .../client/assets/room/furnitures/plant/plant.glb  |  Bin 0 -> 27664 bytes
 .../assets/room/furnitures/plant2/plant2.blend     |  Bin 0 -> 1060816 bytes
 .../assets/room/furnitures/plant2/plant2.glb       |  Bin 0 -> 133212 bytes
 .../client/assets/room/furnitures/plant2/soil.png  |  Bin 0 -> 10852 bytes
 .../assets/room/furnitures/poster-h/poster-h.blend |  Bin 0 -> 667604 bytes
 .../assets/room/furnitures/poster-h/poster-h.glb   |  Bin 0 -> 1280 bytes
 .../client/assets/room/furnitures/poster-h/uv.png  |  Bin 0 -> 3786 bytes
 .../assets/room/furnitures/poster-v/poster-v.blend |  Bin 0 -> 650740 bytes
 .../assets/room/furnitures/poster-v/poster-v.glb   |  Bin 0 -> 1280 bytes
 .../client/assets/room/furnitures/poster-v/uv.png  |  Bin 0 -> 3795 bytes
 .../assets/room/furnitures/pudding/pudding.blend   |  Bin 0 -> 694784 bytes
 .../assets/room/furnitures/pudding/pudding.glb     |  Bin 0 -> 10612 bytes
 .../room/furnitures/rubik-cube/rubik-cube.blend    |  Bin 0 -> 745960 bytes
 .../room/furnitures/rubik-cube/rubik-cube.glb      |  Bin 0 -> 67688 bytes
 .../assets/room/furnitures/server/rack-uv.png      |  Bin 0 -> 6705 bytes
 .../client/assets/room/furnitures/server/rack.png  |  Bin 0 -> 7046 bytes
 .../assets/room/furnitures/server/server.blend     |  Bin 0 -> 684372 bytes
 .../assets/room/furnitures/server/server.glb       |  Bin 0 -> 149480 bytes
 .../assets/room/furnitures/server/server.png       |  Bin 0 -> 116866 bytes
 .../client/assets/room/furnitures/server/uv.png    |  Bin 0 -> 4012 bytes
 .../client/assets/room/furnitures/sofa/sofa.blend  |  Bin 0 -> 711488 bytes
 .../client/assets/room/furnitures/sofa/sofa.glb    |  Bin 0 -> 37656 bytes
 .../assets/room/furnitures/spiral/spiral.blend     |  Bin 0 -> 1241416 bytes
 .../assets/room/furnitures/spiral/spiral.glb       |  Bin 0 -> 251788 bytes
 .../client/assets/room/furnitures/tv/screen-uv.png |  Bin 0 -> 3792 bytes
 packages/client/assets/room/furnitures/tv/tv.blend |  Bin 0 -> 587640 bytes
 packages/client/assets/room/furnitures/tv/tv.glb   |  Bin 0 -> 8580 bytes
 .../room/furnitures/wall-clock/wall-clock.blend    |  Bin 0 -> 574128 bytes
 .../room/furnitures/wall-clock/wall-clock.glb      |  Bin 0 -> 47848 bytes
 .../client/assets/room/rooms/default/default.blend |  Bin 0 -> 618364 bytes
 .../client/assets/room/rooms/default/default.glb   |  Bin 0 -> 12280 bytes
 .../assets/room/rooms/washitsu/husuma-uv.png       |  Bin 0 -> 4037 bytes
 .../client/assets/room/rooms/washitsu/husuma.png   |  Bin 0 -> 2539 bytes
 .../room/rooms/washitsu/tatami-single1600.png      |  Bin 0 -> 1173343 bytes
 .../assets/room/rooms/washitsu/tatami-uv.png       |  Bin 0 -> 4075 bytes
 .../assets/room/rooms/washitsu/tatami.afdesign     |  Bin 0 -> 2501804 bytes
 .../client/assets/room/rooms/washitsu/tatami.png   |  Bin 0 -> 313722 bytes
 .../assets/room/rooms/washitsu/washitsu.blend      |  Bin 0 -> 664000 bytes
 .../client/assets/room/rooms/washitsu/washitsu.glb |  Bin 0 -> 415180 bytes
 packages/client/assets/sounds/aisha/1.mp3          |  Bin 0 -> 34480 bytes
 packages/client/assets/sounds/aisha/2.mp3          |  Bin 0 -> 24031 bytes
 packages/client/assets/sounds/aisha/3.mp3          |  Bin 0 -> 29256 bytes
 .../client/assets/sounds/noizenecio/kick_gaba.mp3  |  Bin 0 -> 18866 bytes
 .../client/assets/sounds/noizenecio/kick_gaba2.mp3 |  Bin 0 -> 27144 bytes
 packages/client/assets/sounds/syuilo/down.mp3      |  Bin 0 -> 18240 bytes
 packages/client/assets/sounds/syuilo/kick.mp3      |  Bin 0 -> 15672 bytes
 .../assets/sounds/syuilo/pirori-square-wet.mp3     |  Bin 0 -> 139200 bytes
 .../client/assets/sounds/syuilo/pirori-wet.mp3     |  Bin 0 -> 139200 bytes
 packages/client/assets/sounds/syuilo/pirori.mp3    |  Bin 0 -> 19200 bytes
 packages/client/assets/sounds/syuilo/poi1.mp3      |  Bin 0 -> 18240 bytes
 packages/client/assets/sounds/syuilo/poi2.mp3      |  Bin 0 -> 18240 bytes
 packages/client/assets/sounds/syuilo/pope1.mp3     |  Bin 0 -> 18240 bytes
 packages/client/assets/sounds/syuilo/pope2.mp3     |  Bin 0 -> 18240 bytes
 packages/client/assets/sounds/syuilo/popo.mp3      |  Bin 0 -> 18240 bytes
 .../client/assets/sounds/syuilo/queue-jammed.mp3   |  Bin 0 -> 351466 bytes
 packages/client/assets/sounds/syuilo/reverved.mp3  |  Bin 0 -> 276480 bytes
 packages/client/assets/sounds/syuilo/ryukyu.mp3    |  Bin 0 -> 139200 bytes
 packages/client/assets/sounds/syuilo/snare.mp3     |  Bin 0 -> 26121 bytes
 .../client/assets/sounds/syuilo/square-pico.mp3    |  Bin 0 -> 19200 bytes
 packages/client/assets/sounds/syuilo/triple.mp3    |  Bin 0 -> 18240 bytes
 packages/client/assets/sounds/syuilo/up.mp3        |  Bin 0 -> 18240 bytes
 packages/client/assets/sounds/syuilo/waon.mp3      |  Bin 0 -> 18240 bytes
 packages/client/assets/sparkle-spritesheet.png     |  Bin 0 -> 185 bytes
 packages/client/assets/unread.svg                  |    7 +
 packages/client/package.json                       |  163 +
 packages/client/src/account.ts                     |  211 +
 .../client/src/components/abuse-report-window.vue  |   79 +
 packages/client/src/components/analog-clock.vue    |  150 +
 packages/client/src/components/autocomplete.vue    |  501 ++
 packages/client/src/components/avatars.vue         |   30 +
 packages/client/src/components/captcha.vue         |  123 +
 .../src/components/channel-follow-button.vue       |  140 +
 packages/client/src/components/channel-preview.vue |  165 +
 packages/client/src/components/chart.vue           |  691 ++
 packages/client/src/components/code-core.vue       |   35 +
 packages/client/src/components/code.vue            |   27 +
 packages/client/src/components/cw-button.vue       |   70 +
 .../client/src/components/date-separated-list.vue  |  188 +
 packages/client/src/components/debobigego/base.vue |   65 +
 .../client/src/components/debobigego/button.vue    |   81 +
 .../src/components/debobigego/debobigego.scss      |   52 +
 .../client/src/components/debobigego/group.vue     |   78 +
 packages/client/src/components/debobigego/info.vue |   47 +
 .../client/src/components/debobigego/input.vue     |  292 +
 .../src/components/debobigego/key-value-view.vue   |   38 +
 packages/client/src/components/debobigego/link.vue |  103 +
 .../src/components/debobigego/object-view.vue      |  102 +
 .../src/components/debobigego/pagination.vue       |   42 +
 .../client/src/components/debobigego/radios.vue    |  112 +
 .../client/src/components/debobigego/range.vue     |  122 +
 .../client/src/components/debobigego/select.vue    |  145 +
 .../client/src/components/debobigego/suspense.vue  |  101 +
 .../client/src/components/debobigego/switch.vue    |  132 +
 .../client/src/components/debobigego/textarea.vue  |  161 +
 .../client/src/components/debobigego/tuple.vue     |   36 +
 packages/client/src/components/dialog.vue          |  212 +
 .../client/src/components/drive-file-thumbnail.vue |  108 +
 .../client/src/components/drive-select-dialog.vue  |   70 +
 packages/client/src/components/drive-window.vue    |   44 +
 packages/client/src/components/drive.file.vue      |  374 +
 packages/client/src/components/drive.folder.vue    |  326 +
 .../client/src/components/drive.nav-folder.vue     |  135 +
 packages/client/src/components/drive.vue           |  784 ++
 .../client/src/components/emoji-picker-dialog.vue  |   76 +
 .../client/src/components/emoji-picker-window.vue  |  197 +
 .../client/src/components/emoji-picker.section.vue |   50 +
 packages/client/src/components/emoji-picker.vue    |  501 ++
 packages/client/src/components/featured-photos.vue |   32 +
 packages/client/src/components/file-type-icon.vue  |   28 +
 packages/client/src/components/follow-button.vue   |  210 +
 packages/client/src/components/forgot-password.vue |   84 +
 packages/client/src/components/form-dialog.vue     |  125 +
 packages/client/src/components/form/input.vue      |  315 +
 packages/client/src/components/form/radio.vue      |  122 +
 packages/client/src/components/form/radios.vue     |   54 +
 packages/client/src/components/form/range.vue      |  139 +
 packages/client/src/components/form/section.vue    |   31 +
 packages/client/src/components/form/select.vue     |  312 +
 packages/client/src/components/form/slot.vue       |   50 +
 packages/client/src/components/form/switch.vue     |  150 +
 packages/client/src/components/form/textarea.vue   |  252 +
 packages/client/src/components/formula-core.vue    |   34 +
 packages/client/src/components/formula.vue         |   23 +
 .../client/src/components/gallery-post-preview.vue |  126 +
 packages/client/src/components/global/a.vue        |  138 +
 packages/client/src/components/global/acct.vue     |   38 +
 packages/client/src/components/global/ad.vue       |  200 +
 packages/client/src/components/global/avatar.vue   |  163 +
 packages/client/src/components/global/ellipsis.vue |   34 +
 packages/client/src/components/global/emoji.vue    |  125 +
 packages/client/src/components/global/error.vue    |   46 +
 packages/client/src/components/global/header.vue   |  360 +
 packages/client/src/components/global/i18n.ts      |   42 +
 packages/client/src/components/global/loading.vue  |   92 +
 .../global/misskey-flavored-markdown.vue           |  157 +
 packages/client/src/components/global/spacer.vue   |   76 +
 .../src/components/global/sticky-container.vue     |   74 +
 packages/client/src/components/global/time.vue     |   73 +
 packages/client/src/components/global/url.vue      |  142 +
 .../client/src/components/global/user-name.vue     |   20 +
 packages/client/src/components/google.vue          |   64 +
 packages/client/src/components/image-viewer.vue    |   85 +
 .../client/src/components/img-with-blurhash.vue    |  100 +
 packages/client/src/components/index.ts            |   37 +
 packages/client/src/components/instance-stats.vue  |   80 +
 packages/client/src/components/instance-ticker.vue |   62 +
 packages/client/src/components/launch-pad.vue      |  152 +
 packages/client/src/components/link.vue            |   92 +
 packages/client/src/components/media-banner.vue    |  107 +
 packages/client/src/components/media-caption.vue   |  259 +
 packages/client/src/components/media-image.vue     |  155 +
 packages/client/src/components/media-list.vue      |  167 +
 packages/client/src/components/media-video.vue     |   97 +
 packages/client/src/components/mention.vue         |   84 +
 packages/client/src/components/mfm.ts              |  321 +
 packages/client/src/components/mini-chart.vue      |   90 +
 .../client/src/components/modal-page-window.vue    |  223 +
 packages/client/src/components/note-detailed.vue   | 1229 +++
 packages/client/src/components/note-header.vue     |  115 +
 packages/client/src/components/note-preview.vue    |   98 +
 packages/client/src/components/note-simple.vue     |  113 +
 packages/client/src/components/note.sub.vue        |  146 +
 packages/client/src/components/note.vue            | 1228 +++
 packages/client/src/components/notes.vue           |  130 +
 .../src/components/notification-setting-window.vue |   99 +
 packages/client/src/components/notification.vue    |  362 +
 packages/client/src/components/notifications.vue   |  159 +
 packages/client/src/components/number-diff.vue     |   47 +
 packages/client/src/components/page-preview.vue    |  162 +
 packages/client/src/components/page-window.vue     |  167 +
 packages/client/src/components/page/page.block.vue |   44 +
 .../client/src/components/page/page.button.vue     |   66 +
 .../client/src/components/page/page.canvas.vue     |   49 +
 .../client/src/components/page/page.counter.vue    |   52 +
 packages/client/src/components/page/page.if.vue    |   31 +
 packages/client/src/components/page/page.image.vue |   40 +
 packages/client/src/components/page/page.note.vue  |   47 +
 .../src/components/page/page.number-input.vue      |   55 +
 packages/client/src/components/page/page.post.vue  |  109 +
 .../src/components/page/page.radio-button.vue      |   45 +
 .../client/src/components/page/page.section.vue    |   60 +
 .../client/src/components/page/page.switch.vue     |   55 +
 .../client/src/components/page/page.text-input.vue |   55 +
 packages/client/src/components/page/page.text.vue  |   68 +
 .../src/components/page/page.textarea-input.vue    |   47 +
 .../client/src/components/page/page.textarea.vue   |   39 +
 packages/client/src/components/page/page.vue       |   86 +
 packages/client/src/components/particle.vue        |  114 +
 packages/client/src/components/poll-editor.vue     |  251 +
 packages/client/src/components/poll.vue            |  174 +
 .../client/src/components/post-form-attaches.vue   |  193 +
 .../client/src/components/post-form-dialog.vue     |   19 +
 packages/client/src/components/post-form.vue       |  980 +++
 packages/client/src/components/queue-chart.vue     |  232 +
 packages/client/src/components/reaction-icon.vue   |   25 +
 .../client/src/components/reaction-tooltip.vue     |   51 +
 .../src/components/reactions-viewer.details.vue    |   91 +
 .../src/components/reactions-viewer.reaction.vue   |  183 +
 .../client/src/components/reactions-viewer.vue     |   48 +
 packages/client/src/components/remote-caution.vue  |   35 +
 packages/client/src/components/sample.vue          |  116 +
 packages/client/src/components/signin-dialog.vue   |   42 +
 packages/client/src/components/signin.vue          |  240 +
 packages/client/src/components/signup-dialog.vue   |   50 +
 packages/client/src/components/signup.vue          |  268 +
 packages/client/src/components/sparkle.vue         |  179 +
 .../client/src/components/sub-note-content.vue     |   62 +
 packages/client/src/components/tab.vue             |   73 +
 .../src/components/taskmanager.api-window.vue      |   72 +
 packages/client/src/components/taskmanager.vue     |  233 +
 packages/client/src/components/timeline.vue        |  183 +
 packages/client/src/components/toast.vue           |   73 +
 .../src/components/token-generate-window.vue       |  117 +
 packages/client/src/components/ui/button.vue       |  262 +
 packages/client/src/components/ui/container.vue    |  262 +
 packages/client/src/components/ui/context-menu.vue |   97 +
 packages/client/src/components/ui/folder.vue       |  156 +
 packages/client/src/components/ui/hr.vue           |   16 +
 packages/client/src/components/ui/info.vue         |   45 +
 packages/client/src/components/ui/menu.vue         |  278 +
 packages/client/src/components/ui/modal-window.vue |  148 +
 packages/client/src/components/ui/modal.vue        |  292 +
 packages/client/src/components/ui/pagination.vue   |   69 +
 packages/client/src/components/ui/popup-menu.vue   |   42 +
 packages/client/src/components/ui/popup.vue        |  213 +
 packages/client/src/components/ui/super-menu.vue   |  148 +
 packages/client/src/components/ui/tooltip.vue      |   92 +
 packages/client/src/components/ui/window.vue       |  525 ++
 packages/client/src/components/updated.vue         |   62 +
 .../client/src/components/url-preview-popup.vue    |   60 +
 packages/client/src/components/url-preview.vue     |  334 +
 packages/client/src/components/user-info.vue       |  142 +
 packages/client/src/components/user-list.vue       |   91 +
 .../src/components/user-online-indicator.vue       |   50 +
 packages/client/src/components/user-preview.vue    |  192 +
 .../client/src/components/user-select-dialog.vue   |  199 +
 packages/client/src/components/users-dialog.vue    |  147 +
 .../client/src/components/visibility-picker.vue    |  167 +
 packages/client/src/components/waiting-dialog.vue  |   92 +
 packages/client/src/components/widgets.vue         |  152 +
 packages/client/src/config.ts                      |   15 +
 packages/client/src/directives/anim.ts             |   18 +
 packages/client/src/directives/appear.ts           |   22 +
 packages/client/src/directives/click-anime.ts      |   29 +
 packages/client/src/directives/follow-append.ts    |   35 +
 packages/client/src/directives/get-size.ts         |   34 +
 packages/client/src/directives/hotkey.ts           |   24 +
 packages/client/src/directives/index.ts            |   26 +
 packages/client/src/directives/particle.ts         |   18 +
 packages/client/src/directives/size.ts             |   68 +
 packages/client/src/directives/sticky-container.ts |   15 +
 packages/client/src/directives/tooltip.ts          |   87 +
 packages/client/src/directives/user-preview.ts     |  118 +
 packages/client/src/emojilist.json                 | 1749 +++++
 packages/client/src/events.ts                      |    4 +
 packages/client/src/filters/bytes.ts               |    9 +
 packages/client/src/filters/note.ts                |    3 +
 packages/client/src/filters/number.ts              |    1 +
 packages/client/src/filters/user.ts                |   15 +
 packages/client/src/i18n.ts                        |   13 +
 packages/client/src/init.ts                        |  420 +
 packages/client/src/instance.ts                    |   52 +
 packages/client/src/menu.ts                        |  224 +
 packages/client/src/os.ts                          |  501 ++
 packages/client/src/pages/_error_.vue              |   94 +
 packages/client/src/pages/_loading_.vue            |   10 +
 packages/client/src/pages/about-misskey.vue        |  238 +
 packages/client/src/pages/about.vue                |  123 +
 packages/client/src/pages/admin/abuses.vue         |  170 +
 packages/client/src/pages/admin/ads.vue            |  138 +
 packages/client/src/pages/admin/announcements.vue  |  125 +
 packages/client/src/pages/admin/bot-protection.vue |  138 +
 packages/client/src/pages/admin/database.vue       |   61 +
 packages/client/src/pages/admin/email-settings.vue |  128 +
 .../client/src/pages/admin/emoji-edit-dialog.vue   |  120 +
 packages/client/src/pages/admin/emojis.vue         |  263 +
 packages/client/src/pages/admin/file-dialog.vue    |  129 +
 packages/client/src/pages/admin/files-settings.vue |   93 +
 packages/client/src/pages/admin/files.vue          |  209 +
 packages/client/src/pages/admin/index.vue          |  388 +
 packages/client/src/pages/admin/instance-block.vue |   72 +
 packages/client/src/pages/admin/instance.vue       |  321 +
 .../src/pages/admin/integrations-discord.vue       |   85 +
 .../client/src/pages/admin/integrations-github.vue |   85 +
 .../src/pages/admin/integrations-twitter.vue       |   85 +
 packages/client/src/pages/admin/integrations.vue   |   74 +
 packages/client/src/pages/admin/metrics.vue        |  472 ++
 packages/client/src/pages/admin/object-storage.vue |  155 +
 packages/client/src/pages/admin/other-settings.vue |   83 +
 packages/client/src/pages/admin/overview.vue       |  236 +
 packages/client/src/pages/admin/proxy-account.vue  |   87 +
 packages/client/src/pages/admin/queue.chart.vue    |  102 +
 packages/client/src/pages/admin/queue.vue          |   73 +
 packages/client/src/pages/admin/relays.vue         |   99 +
 packages/client/src/pages/admin/security.vue       |   83 +
 packages/client/src/pages/admin/service-worker.vue |   85 +
 packages/client/src/pages/admin/settings.vue       |  151 +
 packages/client/src/pages/admin/users.vue          |  254 +
 .../client/src/pages/advanced-theme-editor.vue     |  352 +
 packages/client/src/pages/announcements.vue        |   74 +
 packages/client/src/pages/antenna-timeline.vue     |  147 +
 packages/client/src/pages/api-console.vue          |   93 +
 packages/client/src/pages/auth.form.vue            |   60 +
 packages/client/src/pages/auth.vue                 |   95 +
 packages/client/src/pages/channel-editor.vue       |  129 +
 packages/client/src/pages/channel.vue              |  186 +
 packages/client/src/pages/channels.vue             |   77 +
 packages/client/src/pages/clip.vue                 |  154 +
 packages/client/src/pages/drive.vue                |   28 +
 packages/client/src/pages/emojis.category.vue      |  135 +
 packages/client/src/pages/emojis.emoji.vue         |   94 +
 packages/client/src/pages/emojis.vue               |   36 +
 packages/client/src/pages/explore.vue              |  261 +
 packages/client/src/pages/favorites.vue            |   60 +
 packages/client/src/pages/featured.vue             |   43 +
 packages/client/src/pages/federation.vue           |  265 +
 packages/client/src/pages/follow-requests.vue      |  153 +
 packages/client/src/pages/follow.vue               |   65 +
 packages/client/src/pages/gallery/edit.vue         |  168 +
 packages/client/src/pages/gallery/index.vue        |  152 +
 packages/client/src/pages/gallery/post.vue         |  282 +
 packages/client/src/pages/instance-info.vue        |  238 +
 packages/client/src/pages/mentions.vue             |   42 +
 packages/client/src/pages/messages.vue             |   45 +
 packages/client/src/pages/messaging/index.vue      |  307 +
 .../src/pages/messaging/messaging-room.form.vue    |  348 +
 .../src/pages/messaging/messaging-room.message.vue |  350 +
 .../client/src/pages/messaging/messaging-room.vue  |  470 ++
 packages/client/src/pages/mfm-cheat-sheet.vue      |  365 +
 packages/client/src/pages/miauth.vue               |  100 +
 packages/client/src/pages/my-antennas/create.vue   |   51 +
 packages/client/src/pages/my-antennas/edit.vue     |   56 +
 packages/client/src/pages/my-antennas/editor.vue   |  190 +
 packages/client/src/pages/my-antennas/index.vue    |   71 +
 packages/client/src/pages/my-clips/index.vue       |  104 +
 packages/client/src/pages/my-groups/group.vue      |  184 +
 packages/client/src/pages/my-groups/index.vue      |  121 +
 packages/client/src/pages/my-lists/index.vue       |   88 +
 packages/client/src/pages/my-lists/list.vue        |  170 +
 packages/client/src/pages/not-found.vue            |   25 +
 packages/client/src/pages/note.vue                 |  209 +
 packages/client/src/pages/notifications.vue        |   88 +
 .../page-editor/els/page-editor.el.button.vue      |   84 +
 .../page-editor/els/page-editor.el.canvas.vue      |   50 +
 .../page-editor/els/page-editor.el.counter.vue     |   46 +
 .../pages/page-editor/els/page-editor.el.if.vue    |   84 +
 .../pages/page-editor/els/page-editor.el.image.vue |   72 +
 .../pages/page-editor/els/page-editor.el.note.vue  |   65 +
 .../els/page-editor.el.number-input.vue            |   46 +
 .../pages/page-editor/els/page-editor.el.post.vue  |   43 +
 .../els/page-editor.el.radio-button.vue            |   50 +
 .../page-editor/els/page-editor.el.section.vue     |   96 +
 .../page-editor/els/page-editor.el.switch.vue      |   46 +
 .../page-editor/els/page-editor.el.text-input.vue  |   39 +
 .../pages/page-editor/els/page-editor.el.text.vue  |   57 +
 .../els/page-editor.el.textarea-input.vue          |   40 +
 .../page-editor/els/page-editor.el.textarea.vue    |   57 +
 .../src/pages/page-editor/page-editor.blocks.vue   |   78 +
 .../pages/page-editor/page-editor.container.vue    |  159 +
 .../pages/page-editor/page-editor.script-block.vue |  281 +
 .../client/src/pages/page-editor/page-editor.vue   |  561 ++
 packages/client/src/pages/page.vue                 |  311 +
 packages/client/src/pages/pages.vue                |   96 +
 packages/client/src/pages/preview.vue              |   32 +
 packages/client/src/pages/reset-password.vue       |   69 +
 packages/client/src/pages/reversi/game.board.vue   |  528 ++
 packages/client/src/pages/reversi/game.setting.vue |  390 +
 packages/client/src/pages/reversi/game.vue         |   76 +
 packages/client/src/pages/reversi/index.vue        |  279 +
 packages/client/src/pages/room/preview.vue         |  107 +
 packages/client/src/pages/room/room.vue            |  285 +
 packages/client/src/pages/scratchpad.vue           |  149 +
 packages/client/src/pages/search.vue               |   53 +
 packages/client/src/pages/settings/2fa.vue         |  247 +
 .../client/src/pages/settings/account-info.vue     |  185 +
 packages/client/src/pages/settings/accounts.vue    |  149 +
 packages/client/src/pages/settings/api.vue         |   65 +
 packages/client/src/pages/settings/apps.vue        |  113 +
 packages/client/src/pages/settings/custom-css.vue  |   73 +
 packages/client/src/pages/settings/deck.vue        |  107 +
 .../client/src/pages/settings/delete-account.vue   |   68 +
 packages/client/src/pages/settings/drive.vue       |  147 +
 .../client/src/pages/settings/email-address.vue    |   70 +
 .../src/pages/settings/email-notification.vue      |   91 +
 packages/client/src/pages/settings/email.vue       |   66 +
 .../src/pages/settings/experimental-features.vue   |   52 +
 packages/client/src/pages/settings/general.vue     |  223 +
 .../client/src/pages/settings/import-export.vue    |  112 +
 packages/client/src/pages/settings/index.vue       |  326 +
 packages/client/src/pages/settings/integration.vue |  141 +
 packages/client/src/pages/settings/menu.vue        |  117 +
 packages/client/src/pages/settings/mute-block.vue  |   85 +
 .../client/src/pages/settings/notifications.vue    |   77 +
 packages/client/src/pages/settings/other.vue       |   97 +
 .../client/src/pages/settings/plugin.install.vue   |  147 +
 .../client/src/pages/settings/plugin.manage.vue    |  115 +
 packages/client/src/pages/settings/plugin.vue      |   44 +
 packages/client/src/pages/settings/privacy.vue     |  120 +
 packages/client/src/pages/settings/profile.vue     |  281 +
 packages/client/src/pages/settings/reaction.vue    |  152 +
 .../client/src/pages/settings/registry.keys.vue    |  114 +
 .../client/src/pages/settings/registry.value.vue   |  149 +
 packages/client/src/pages/settings/registry.vue    |   90 +
 packages/client/src/pages/settings/security.vue    |  158 +
 packages/client/src/pages/settings/sounds.vue      |  155 +
 .../client/src/pages/settings/theme.install.vue    |  105 +
 .../client/src/pages/settings/theme.manage.vue     |  105 +
 packages/client/src/pages/settings/theme.vue       |  424 +
 packages/client/src/pages/settings/update.vue      |   95 +
 packages/client/src/pages/settings/word-mute.vue   |  110 +
 packages/client/src/pages/share.vue                |  184 +
 packages/client/src/pages/signup-complete.vue      |   50 +
 packages/client/src/pages/tag.vue                  |   57 +
 packages/client/src/pages/test.vue                 |  259 +
 packages/client/src/pages/theme-editor.vue         |  306 +
 packages/client/src/pages/timeline.tutorial.vue    |  131 +
 packages/client/src/pages/timeline.vue             |  225 +
 packages/client/src/pages/user-ap-info.vue         |  124 +
 packages/client/src/pages/user-info.vue            |  245 +
 packages/client/src/pages/user-list-timeline.vue   |  147 +
 packages/client/src/pages/user/clips.vue           |   50 +
 packages/client/src/pages/user/follow-list.vue     |   65 +
 packages/client/src/pages/user/gallery.vue         |   56 +
 packages/client/src/pages/user/index.activity.vue  |   34 +
 packages/client/src/pages/user/index.photos.vue    |  107 +
 packages/client/src/pages/user/index.timeline.vue  |   68 +
 packages/client/src/pages/user/index.vue           |  829 ++
 packages/client/src/pages/user/pages.vue           |   49 +
 packages/client/src/pages/user/reactions.vue       |   81 +
 packages/client/src/pages/v.vue                    |   29 +
 packages/client/src/pages/welcome.entrance.a.vue   |  320 +
 packages/client/src/pages/welcome.entrance.b.vue   |  236 +
 packages/client/src/pages/welcome.entrance.c.vue   |  305 +
 packages/client/src/pages/welcome.setup.vue        |  102 +
 packages/client/src/pages/welcome.timeline.vue     |   99 +
 packages/client/src/pages/welcome.vue              |   38 +
 packages/client/src/pizzax.ts                      |  153 +
 packages/client/src/plugin.ts                      |  124 +
 packages/client/src/router.ts                      |  149 +
 packages/client/src/scripts/2fa.ts                 |   33 +
 packages/client/src/scripts/aiscript/api.ts        |   44 +
 packages/client/src/scripts/array.ts               |  138 +
 packages/client/src/scripts/autocomplete.ts        |  276 +
 packages/client/src/scripts/check-word-mute.ts     |   26 +
 packages/client/src/scripts/collect-page-vars.ts   |   48 +
 packages/client/src/scripts/contains.ts            |    9 +
 packages/client/src/scripts/copy-to-clipboard.ts   |   33 +
 packages/client/src/scripts/emojilist.ts           |    7 +
 .../src/scripts/extract-avg-color-from-blurhash.ts |    9 +
 packages/client/src/scripts/extract-mentions.ts    |   11 +
 .../client/src/scripts/extract-url-from-mfm.ts     |   19 +
 packages/client/src/scripts/focus.ts               |   27 +
 packages/client/src/scripts/form.ts                |   31 +
 packages/client/src/scripts/format-time-string.ts  |   50 +
 packages/client/src/scripts/games/reversi/core.ts  |  263 +
 packages/client/src/scripts/games/reversi/maps.ts  |  896 +++
 .../client/src/scripts/games/reversi/package.json  |   18 +
 .../client/src/scripts/games/reversi/tsconfig.json |   21 +
 packages/client/src/scripts/gen-search-query.ts    |   31 +
 packages/client/src/scripts/get-account-from-id.ts |    7 +
 packages/client/src/scripts/get-md5.ts             |   10 +
 packages/client/src/scripts/get-note-summary.ts    |   55 +
 .../client/src/scripts/get-static-image-url.ts     |   16 +
 packages/client/src/scripts/get-user-menu.ts       |  205 +
 packages/client/src/scripts/hotkey.ts              |   88 +
 packages/client/src/scripts/hpml/block.ts          |  109 +
 packages/client/src/scripts/hpml/evaluator.ts      |  234 +
 packages/client/src/scripts/hpml/expr.ts           |   79 +
 packages/client/src/scripts/hpml/index.ts          |  103 +
 packages/client/src/scripts/hpml/lib.ts            |  246 +
 packages/client/src/scripts/hpml/type-checker.ts   |  189 +
 packages/client/src/scripts/i18n.ts                |   29 +
 packages/client/src/scripts/idb-proxy.ts           |   37 +
 packages/client/src/scripts/initialize-sw.ts       |   68 +
 packages/client/src/scripts/is-device-darkmode.ts  |    3 +
 packages/client/src/scripts/is-device-touch.ts     |    1 +
 packages/client/src/scripts/is-mobile.ts           |    2 +
 packages/client/src/scripts/keycode.ts             |   33 +
 packages/client/src/scripts/loading.ts             |   11 +
 packages/client/src/scripts/login-id.ts            |   11 +
 packages/client/src/scripts/lookup-user.ts         |   37 +
 packages/client/src/scripts/mfm-tags.ts            |    1 +
 packages/client/src/scripts/paging.ts              |  246 +
 packages/client/src/scripts/physics.ts             |  152 +
 packages/client/src/scripts/please-login.ts        |   14 +
 packages/client/src/scripts/popout.ts              |   22 +
 packages/client/src/scripts/reaction-picker.ts     |   41 +
 packages/client/src/scripts/room/furniture.ts      |   21 +
 packages/client/src/scripts/room/furnitures.json5  |  407 +
 packages/client/src/scripts/room/room.ts           |  775 ++
 packages/client/src/scripts/scroll.ts              |   80 +
 packages/client/src/scripts/search.ts              |   64 +
 packages/client/src/scripts/select-file.ts         |   89 +
 .../client/src/scripts/show-suspended-dialog.ts    |   10 +
 packages/client/src/scripts/sound.ts               |   34 +
 packages/client/src/scripts/sticky-sidebar.ts      |   50 +
 packages/client/src/scripts/theme-editor.ts        |   81 +
 packages/client/src/scripts/theme.ts               |  127 +
 packages/client/src/scripts/time.ts                |   39 +
 packages/client/src/scripts/twemoji-base.ts        |    1 +
 packages/client/src/scripts/unison-reload.ts       |   15 +
 packages/client/src/scripts/url.ts                 |   13 +
 packages/client/src/store.ts                       |  318 +
 packages/client/src/style.scss                     |  562 ++
 packages/client/src/sw/compose-notification.ts     |  103 +
 packages/client/src/sw/sw.ts                       |  123 +
 packages/client/src/symbols.ts                     |    1 +
 packages/client/src/theme-store.ts                 |   34 +
 packages/client/src/themes/_dark.json5             |   90 +
 packages/client/src/themes/_light.json5            |   90 +
 packages/client/src/themes/d-astro.json5           |   78 +
 packages/client/src/themes/d-black.json5           |   17 +
 packages/client/src/themes/d-botanical.json5       |   26 +
 packages/client/src/themes/d-dark.json5            |   26 +
 packages/client/src/themes/d-future.json5          |   27 +
 packages/client/src/themes/d-persimmon.json5       |   25 +
 packages/client/src/themes/d-pumpkin.json5         |   88 +
 packages/client/src/themes/l-apricot.json5         |   22 +
 packages/client/src/themes/l-light.json5           |   20 +
 packages/client/src/themes/l-rainy.json5           |   21 +
 packages/client/src/themes/l-sushi.json5           |   18 +
 packages/client/src/themes/l-vivid.json5           |   82 +
 packages/client/src/ui/_common_/common.vue         |   89 +
 packages/client/src/ui/_common_/sidebar.vue        |  388 +
 .../client/src/ui/_common_/stream-indicator.vue    |   70 +
 packages/client/src/ui/_common_/upload.vue         |  134 +
 .../client/src/ui/chat/date-separated-list.vue     |  163 +
 packages/client/src/ui/chat/header-clock.vue       |   62 +
 packages/client/src/ui/chat/index.vue              |  467 ++
 packages/client/src/ui/chat/note-header.vue        |  112 +
 packages/client/src/ui/chat/note-preview.vue       |  112 +
 packages/client/src/ui/chat/note.sub.vue           |  137 +
 packages/client/src/ui/chat/note.vue               | 1144 +++
 packages/client/src/ui/chat/notes.vue              |   94 +
 packages/client/src/ui/chat/pages/channel.vue      |  259 +
 packages/client/src/ui/chat/pages/timeline.vue     |  221 +
 packages/client/src/ui/chat/post-form.vue          |  772 ++
 packages/client/src/ui/chat/side.vue               |  157 +
 packages/client/src/ui/chat/store.ts               |   17 +
 packages/client/src/ui/chat/sub-note-content.vue   |   62 +
 packages/client/src/ui/chat/widgets.vue            |   62 +
 packages/client/src/ui/classic.header.vue          |  210 +
 packages/client/src/ui/classic.side.vue            |  158 +
 packages/client/src/ui/classic.sidebar.vue         |  263 +
 packages/client/src/ui/classic.vue                 |  471 ++
 packages/client/src/ui/classic.widgets.vue         |   84 +
 packages/client/src/ui/deck.vue                    |  229 +
 packages/client/src/ui/deck/antenna-column.vue     |   80 +
 packages/client/src/ui/deck/column-core.vue        |   52 +
 packages/client/src/ui/deck/column.vue             |  408 +
 packages/client/src/ui/deck/deck-store.ts          |  298 +
 packages/client/src/ui/deck/direct-column.vue      |   55 +
 packages/client/src/ui/deck/list-column.vue        |   80 +
 packages/client/src/ui/deck/main-column.vue        |   91 +
 packages/client/src/ui/deck/mentions-column.vue    |   52 +
 .../client/src/ui/deck/notifications-column.vue    |   53 +
 packages/client/src/ui/deck/tl-column.vue          |  137 +
 packages/client/src/ui/deck/widgets-column.vue     |   71 +
 packages/client/src/ui/desktop.vue                 |   70 +
 packages/client/src/ui/universal.vue               |  402 +
 packages/client/src/ui/universal.widgets.vue       |   79 +
 packages/client/src/ui/visitor.vue                 |   19 +
 packages/client/src/ui/visitor/a.vue               |  260 +
 packages/client/src/ui/visitor/b.vue               |  282 +
 packages/client/src/ui/visitor/header.vue          |  228 +
 packages/client/src/ui/visitor/kanban.vue          |  256 +
 packages/client/src/ui/zen.vue                     |  106 +
 packages/client/src/widgets/activity.calendar.vue  |   85 +
 packages/client/src/widgets/activity.chart.vue     |  107 +
 packages/client/src/widgets/activity.vue           |   82 +
 packages/client/src/widgets/aichan.vue             |   59 +
 packages/client/src/widgets/aiscript.vue           |  163 +
 packages/client/src/widgets/button.vue             |   95 +
 packages/client/src/widgets/calendar.vue           |  204 +
 packages/client/src/widgets/clock.vue              |   55 +
 packages/client/src/widgets/define.ts              |   75 +
 packages/client/src/widgets/digital-clock.vue      |   79 +
 packages/client/src/widgets/federation.vue         |  145 +
 packages/client/src/widgets/index.ts               |   45 +
 packages/client/src/widgets/job-queue.vue          |  183 +
 packages/client/src/widgets/memo.vue               |  106 +
 packages/client/src/widgets/notifications.vue      |   65 +
 packages/client/src/widgets/online-users.vue       |   67 +
 packages/client/src/widgets/photos.vue             |  113 +
 packages/client/src/widgets/post-form.vue          |   23 +
 packages/client/src/widgets/rss.vue                |   89 +
 .../client/src/widgets/server-metric/cpu-mem.vue   |  174 +
 packages/client/src/widgets/server-metric/cpu.vue  |   76 +
 packages/client/src/widgets/server-metric/disk.vue |   70 +
 .../client/src/widgets/server-metric/index.vue     |   82 +
 packages/client/src/widgets/server-metric/mem.vue  |   85 +
 packages/client/src/widgets/server-metric/net.vue  |  148 +
 packages/client/src/widgets/server-metric/pie.vue  |   65 +
 packages/client/src/widgets/slideshow.vue          |  167 +
 packages/client/src/widgets/timeline.vue           |  116 +
 packages/client/src/widgets/trends.vue             |  111 +
 packages/client/tsconfig.json                      |   39 +
 packages/client/webpack.config.js                  |  192 +
 packages/client/yarn.lock                          | 6874 ++++++++++++++++
 packages/meta.json                                 |    3 +
 src/.eslintrc                                      |    6 -
 src/@types/hcaptcha.d.ts                           |   11 -
 src/@types/http-signature.d.ts                     |   77 -
 src/@types/jsrsasign.d.ts                          |  800 --
 src/@types/koa-json-body.d.ts                      |   15 -
 src/@types/koa-slow.d.ts                           |   14 -
 src/@types/langmap.d.ts                            |   10 -
 src/@types/ms.d.ts                                 |   12 -
 src/@types/os-utils.d.ts                           |   30 -
 src/@types/package.json.d.ts                       |   10 -
 src/@types/probe-image-size.d.ts                   |   27 -
 src/boot/index.ts                                  |   79 -
 src/boot/master.ts                                 |  194 -
 src/boot/worker.ts                                 |   20 -
 src/client/.eslintrc                               |   32 -
 src/client/@types/global.d.ts                      |    8 -
 src/client/@types/vue.d.ts                         |    5 -
 src/client/account.ts                              |  211 -
 src/client/components/abuse-report-window.vue      |   79 -
 src/client/components/analog-clock.vue             |  150 -
 src/client/components/autocomplete.vue             |  502 --
 src/client/components/avatars.vue                  |   30 -
 src/client/components/captcha.vue                  |  123 -
 src/client/components/channel-follow-button.vue    |  140 -
 src/client/components/channel-preview.vue          |  165 -
 src/client/components/chart.vue                    |  691 --
 src/client/components/code-core.vue                |   35 -
 src/client/components/code.vue                     |   27 -
 src/client/components/cw-button.vue                |   70 -
 src/client/components/date-separated-list.vue      |  188 -
 src/client/components/debobigego/base.vue          |   65 -
 src/client/components/debobigego/button.vue        |   81 -
 src/client/components/debobigego/debobigego.scss   |   52 -
 src/client/components/debobigego/group.vue         |   78 -
 src/client/components/debobigego/info.vue          |   47 -
 src/client/components/debobigego/input.vue         |  292 -
 .../components/debobigego/key-value-view.vue       |   38 -
 src/client/components/debobigego/link.vue          |  103 -
 src/client/components/debobigego/object-view.vue   |  102 -
 src/client/components/debobigego/pagination.vue    |   42 -
 src/client/components/debobigego/radios.vue        |  112 -
 src/client/components/debobigego/range.vue         |  122 -
 src/client/components/debobigego/select.vue        |  145 -
 src/client/components/debobigego/suspense.vue      |  101 -
 src/client/components/debobigego/switch.vue        |  132 -
 src/client/components/debobigego/textarea.vue      |  161 -
 src/client/components/debobigego/tuple.vue         |   36 -
 src/client/components/dialog.vue                   |  212 -
 src/client/components/drive-file-thumbnail.vue     |  108 -
 src/client/components/drive-select-dialog.vue      |   70 -
 src/client/components/drive-window.vue             |   44 -
 src/client/components/drive.file.vue               |  374 -
 src/client/components/drive.folder.vue             |  326 -
 src/client/components/drive.nav-folder.vue         |  135 -
 src/client/components/drive.vue                    |  784 --
 src/client/components/emoji-picker-dialog.vue      |   76 -
 src/client/components/emoji-picker-window.vue      |  197 -
 src/client/components/emoji-picker.section.vue     |   50 -
 src/client/components/emoji-picker.vue             |  501 --
 src/client/components/featured-photos.vue          |   32 -
 src/client/components/file-type-icon.vue           |   28 -
 src/client/components/follow-button.vue            |  210 -
 src/client/components/forgot-password.vue          |   84 -
 src/client/components/form-dialog.vue              |  125 -
 src/client/components/form/input.vue               |  315 -
 src/client/components/form/radio.vue               |  122 -
 src/client/components/form/radios.vue              |   54 -
 src/client/components/form/range.vue               |  139 -
 src/client/components/form/section.vue             |   31 -
 src/client/components/form/select.vue              |  312 -
 src/client/components/form/slot.vue                |   50 -
 src/client/components/form/switch.vue              |  150 -
 src/client/components/form/textarea.vue            |  252 -
 src/client/components/formula-core.vue             |   34 -
 src/client/components/formula.vue                  |   23 -
 src/client/components/gallery-post-preview.vue     |  126 -
 src/client/components/global/a.vue                 |  138 -
 src/client/components/global/acct.vue              |   38 -
 src/client/components/global/ad.vue                |  200 -
 src/client/components/global/avatar.vue            |  163 -
 src/client/components/global/ellipsis.vue          |   34 -
 src/client/components/global/emoji.vue             |  125 -
 src/client/components/global/error.vue             |   46 -
 src/client/components/global/header.vue            |  360 -
 src/client/components/global/i18n.ts               |   42 -
 src/client/components/global/loading.vue           |   92 -
 .../global/misskey-flavored-markdown.vue           |  157 -
 src/client/components/global/spacer.vue            |   76 -
 src/client/components/global/sticky-container.vue  |   74 -
 src/client/components/global/time.vue              |   73 -
 src/client/components/global/url.vue               |  142 -
 src/client/components/global/user-name.vue         |   20 -
 src/client/components/google.vue                   |   64 -
 src/client/components/image-viewer.vue             |   85 -
 src/client/components/img-with-blurhash.vue        |  100 -
 src/client/components/index.ts                     |   37 -
 src/client/components/instance-stats.vue           |   80 -
 src/client/components/instance-ticker.vue          |   62 -
 src/client/components/launch-pad.vue               |  152 -
 src/client/components/link.vue                     |   92 -
 src/client/components/media-banner.vue             |  107 -
 src/client/components/media-caption.vue            |  260 -
 src/client/components/media-image.vue              |  155 -
 src/client/components/media-list.vue               |  167 -
 src/client/components/media-video.vue              |   97 -
 src/client/components/mention.vue                  |   86 -
 src/client/components/mfm.ts                       |  321 -
 src/client/components/mini-chart.vue               |   90 -
 src/client/components/modal-page-window.vue        |  223 -
 src/client/components/note-detailed.vue            | 1229 ---
 src/client/components/note-header.vue              |  115 -
 src/client/components/note-preview.vue             |   98 -
 src/client/components/note-simple.vue              |  113 -
 src/client/components/note.sub.vue                 |  146 -
 src/client/components/note.vue                     | 1228 ---
 src/client/components/notes.vue                    |  130 -
 .../components/notification-setting-window.vue     |   99 -
 src/client/components/notification.vue             |  362 -
 src/client/components/notifications.vue            |  159 -
 src/client/components/number-diff.vue              |   47 -
 src/client/components/page-preview.vue             |  162 -
 src/client/components/page-window.vue              |  167 -
 src/client/components/page/page.block.vue          |   44 -
 src/client/components/page/page.button.vue         |   66 -
 src/client/components/page/page.canvas.vue         |   49 -
 src/client/components/page/page.counter.vue        |   52 -
 src/client/components/page/page.if.vue             |   31 -
 src/client/components/page/page.image.vue          |   40 -
 src/client/components/page/page.note.vue           |   47 -
 src/client/components/page/page.number-input.vue   |   55 -
 src/client/components/page/page.post.vue           |  109 -
 src/client/components/page/page.radio-button.vue   |   45 -
 src/client/components/page/page.section.vue        |   60 -
 src/client/components/page/page.switch.vue         |   55 -
 src/client/components/page/page.text-input.vue     |   55 -
 src/client/components/page/page.text.vue           |   68 -
 src/client/components/page/page.textarea-input.vue |   47 -
 src/client/components/page/page.textarea.vue       |   39 -
 src/client/components/page/page.vue                |   86 -
 src/client/components/particle.vue                 |  114 -
 src/client/components/poll-editor.vue              |  251 -
 src/client/components/poll.vue                     |  174 -
 src/client/components/post-form-attaches.vue       |  193 -
 src/client/components/post-form-dialog.vue         |   19 -
 src/client/components/post-form.vue                |  980 ---
 src/client/components/queue-chart.vue              |  232 -
 src/client/components/reaction-icon.vue            |   25 -
 src/client/components/reaction-tooltip.vue         |   51 -
 src/client/components/reactions-viewer.details.vue |   91 -
 .../components/reactions-viewer.reaction.vue       |  183 -
 src/client/components/reactions-viewer.vue         |   48 -
 src/client/components/remote-caution.vue           |   35 -
 src/client/components/sample.vue                   |  116 -
 src/client/components/signin-dialog.vue            |   42 -
 src/client/components/signin.vue                   |  240 -
 src/client/components/signup-dialog.vue            |   50 -
 src/client/components/signup.vue                   |  268 -
 src/client/components/sparkle.vue                  |  180 -
 src/client/components/sub-note-content.vue         |   62 -
 src/client/components/tab.vue                      |   73 -
 src/client/components/taskmanager.api-window.vue   |   72 -
 src/client/components/taskmanager.vue              |  233 -
 src/client/components/timeline.vue                 |  183 -
 src/client/components/toast.vue                    |   73 -
 src/client/components/token-generate-window.vue    |  117 -
 src/client/components/ui/button.vue                |  262 -
 src/client/components/ui/container.vue             |  262 -
 src/client/components/ui/context-menu.vue          |   97 -
 src/client/components/ui/folder.vue                |  156 -
 src/client/components/ui/hr.vue                    |   16 -
 src/client/components/ui/info.vue                  |   45 -
 src/client/components/ui/menu.vue                  |  278 -
 src/client/components/ui/modal-window.vue          |  148 -
 src/client/components/ui/modal.vue                 |  292 -
 src/client/components/ui/pagination.vue            |   69 -
 src/client/components/ui/popup-menu.vue            |   42 -
 src/client/components/ui/popup.vue                 |  213 -
 src/client/components/ui/super-menu.vue            |  148 -
 src/client/components/ui/tooltip.vue               |   92 -
 src/client/components/ui/window.vue                |  525 --
 src/client/components/updated.vue                  |   62 -
 src/client/components/url-preview-popup.vue        |   60 -
 src/client/components/url-preview.vue              |  334 -
 src/client/components/user-info.vue                |  144 -
 src/client/components/user-list.vue                |   91 -
 src/client/components/user-online-indicator.vue    |   50 -
 src/client/components/user-preview.vue             |  192 -
 src/client/components/user-select-dialog.vue       |  199 -
 src/client/components/users-dialog.vue             |  147 -
 src/client/components/visibility-picker.vue        |  167 -
 src/client/components/waiting-dialog.vue           |   92 -
 src/client/components/widgets.vue                  |  152 -
 src/client/config.ts                               |   15 -
 src/client/directives/anim.ts                      |   18 -
 src/client/directives/appear.ts                    |   22 -
 src/client/directives/click-anime.ts               |   29 -
 src/client/directives/follow-append.ts             |   35 -
 src/client/directives/get-size.ts                  |   34 -
 src/client/directives/hotkey.ts                    |   24 -
 src/client/directives/index.ts                     |   26 -
 src/client/directives/particle.ts                  |   18 -
 src/client/directives/size.ts                      |   68 -
 src/client/directives/sticky-container.ts          |   15 -
 src/client/directives/tooltip.ts                   |   87 -
 src/client/directives/user-preview.ts              |  118 -
 src/client/events.ts                               |    4 -
 src/client/filters/bytes.ts                        |    9 -
 src/client/filters/note.ts                         |    3 -
 src/client/filters/number.ts                       |    1 -
 src/client/filters/user.ts                         |   15 -
 src/client/i18n.ts                                 |   13 -
 src/client/init.ts                                 |  420 -
 src/client/instance.ts                             |   52 -
 src/client/menu.ts                                 |  224 -
 src/client/os.ts                                   |  501 --
 src/client/pages/_error_.vue                       |   94 -
 src/client/pages/_loading_.vue                     |   10 -
 src/client/pages/about-misskey.vue                 |  238 -
 src/client/pages/about.vue                         |  123 -
 src/client/pages/admin/abuses.vue                  |  170 -
 src/client/pages/admin/ads.vue                     |  138 -
 src/client/pages/admin/announcements.vue           |  125 -
 src/client/pages/admin/bot-protection.vue          |  138 -
 src/client/pages/admin/database.vue                |   61 -
 src/client/pages/admin/email-settings.vue          |  128 -
 src/client/pages/admin/emoji-edit-dialog.vue       |  120 -
 src/client/pages/admin/emojis.vue                  |  263 -
 src/client/pages/admin/file-dialog.vue             |  129 -
 src/client/pages/admin/files-settings.vue          |   93 -
 src/client/pages/admin/files.vue                   |  209 -
 src/client/pages/admin/index.vue                   |  388 -
 src/client/pages/admin/instance-block.vue          |   72 -
 src/client/pages/admin/instance.vue                |  321 -
 src/client/pages/admin/integrations-discord.vue    |   85 -
 src/client/pages/admin/integrations-github.vue     |   85 -
 src/client/pages/admin/integrations-twitter.vue    |   85 -
 src/client/pages/admin/integrations.vue            |   74 -
 src/client/pages/admin/metrics.vue                 |  472 --
 src/client/pages/admin/object-storage.vue          |  155 -
 src/client/pages/admin/other-settings.vue          |   83 -
 src/client/pages/admin/overview.vue                |  236 -
 src/client/pages/admin/proxy-account.vue           |   87 -
 src/client/pages/admin/queue.chart.vue             |  102 -
 src/client/pages/admin/queue.vue                   |   73 -
 src/client/pages/admin/relays.vue                  |   99 -
 src/client/pages/admin/security.vue                |   83 -
 src/client/pages/admin/service-worker.vue          |   85 -
 src/client/pages/admin/settings.vue                |  151 -
 src/client/pages/admin/users.vue                   |  254 -
 src/client/pages/advanced-theme-editor.vue         |  352 -
 src/client/pages/announcements.vue                 |   74 -
 src/client/pages/antenna-timeline.vue              |  147 -
 src/client/pages/api-console.vue                   |   93 -
 src/client/pages/auth.form.vue                     |   60 -
 src/client/pages/auth.vue                          |   95 -
 src/client/pages/channel-editor.vue                |  129 -
 src/client/pages/channel.vue                       |  186 -
 src/client/pages/channels.vue                      |   77 -
 src/client/pages/clip.vue                          |  154 -
 src/client/pages/drive.vue                         |   28 -
 src/client/pages/emojis.category.vue               |  135 -
 src/client/pages/emojis.emoji.vue                  |   94 -
 src/client/pages/emojis.vue                        |   36 -
 src/client/pages/explore.vue                       |  261 -
 src/client/pages/favorites.vue                     |   60 -
 src/client/pages/featured.vue                      |   43 -
 src/client/pages/federation.vue                    |  265 -
 src/client/pages/follow-requests.vue               |  153 -
 src/client/pages/follow.vue                        |   65 -
 src/client/pages/gallery/edit.vue                  |  168 -
 src/client/pages/gallery/index.vue                 |  152 -
 src/client/pages/gallery/post.vue                  |  282 -
 src/client/pages/instance-info.vue                 |  238 -
 src/client/pages/mentions.vue                      |   42 -
 src/client/pages/messages.vue                      |   45 -
 src/client/pages/messaging/index.vue               |  307 -
 src/client/pages/messaging/messaging-room.form.vue |  348 -
 .../pages/messaging/messaging-room.message.vue     |  350 -
 src/client/pages/messaging/messaging-room.vue      |  470 --
 src/client/pages/mfm-cheat-sheet.vue               |  365 -
 src/client/pages/miauth.vue                        |  100 -
 src/client/pages/my-antennas/create.vue            |   51 -
 src/client/pages/my-antennas/edit.vue              |   56 -
 src/client/pages/my-antennas/editor.vue            |  190 -
 src/client/pages/my-antennas/index.vue             |   71 -
 src/client/pages/my-clips/index.vue                |  104 -
 src/client/pages/my-groups/group.vue               |  184 -
 src/client/pages/my-groups/index.vue               |  121 -
 src/client/pages/my-lists/index.vue                |   88 -
 src/client/pages/my-lists/list.vue                 |  170 -
 src/client/pages/not-found.vue                     |   25 -
 src/client/pages/note.vue                          |  209 -
 src/client/pages/notifications.vue                 |   88 -
 .../page-editor/els/page-editor.el.button.vue      |   84 -
 .../page-editor/els/page-editor.el.canvas.vue      |   50 -
 .../page-editor/els/page-editor.el.counter.vue     |   46 -
 .../pages/page-editor/els/page-editor.el.if.vue    |   84 -
 .../pages/page-editor/els/page-editor.el.image.vue |   72 -
 .../pages/page-editor/els/page-editor.el.note.vue  |   65 -
 .../els/page-editor.el.number-input.vue            |   46 -
 .../pages/page-editor/els/page-editor.el.post.vue  |   43 -
 .../els/page-editor.el.radio-button.vue            |   50 -
 .../page-editor/els/page-editor.el.section.vue     |   96 -
 .../page-editor/els/page-editor.el.switch.vue      |   46 -
 .../page-editor/els/page-editor.el.text-input.vue  |   39 -
 .../pages/page-editor/els/page-editor.el.text.vue  |   57 -
 .../els/page-editor.el.textarea-input.vue          |   40 -
 .../page-editor/els/page-editor.el.textarea.vue    |   57 -
 .../pages/page-editor/page-editor.blocks.vue       |   78 -
 .../pages/page-editor/page-editor.container.vue    |  159 -
 .../pages/page-editor/page-editor.script-block.vue |  281 -
 src/client/pages/page-editor/page-editor.vue       |  561 --
 src/client/pages/page.vue                          |  311 -
 src/client/pages/pages.vue                         |   96 -
 src/client/pages/preview.vue                       |   32 -
 src/client/pages/reset-password.vue                |   69 -
 src/client/pages/reversi/game.board.vue            |  528 --
 src/client/pages/reversi/game.setting.vue          |  390 -
 src/client/pages/reversi/game.vue                  |   76 -
 src/client/pages/reversi/index.vue                 |  279 -
 src/client/pages/room/preview.vue                  |  107 -
 src/client/pages/room/room.vue                     |  285 -
 src/client/pages/scratchpad.vue                    |  149 -
 src/client/pages/search.vue                        |   53 -
 src/client/pages/settings/2fa.vue                  |  247 -
 src/client/pages/settings/account-info.vue         |  185 -
 src/client/pages/settings/accounts.vue             |  149 -
 src/client/pages/settings/api.vue                  |   65 -
 src/client/pages/settings/apps.vue                 |  113 -
 src/client/pages/settings/custom-css.vue           |   73 -
 src/client/pages/settings/deck.vue                 |  107 -
 src/client/pages/settings/delete-account.vue       |   68 -
 src/client/pages/settings/drive.vue                |  147 -
 src/client/pages/settings/email-address.vue        |   70 -
 src/client/pages/settings/email-notification.vue   |   91 -
 src/client/pages/settings/email.vue                |   66 -
 .../pages/settings/experimental-features.vue       |   52 -
 src/client/pages/settings/general.vue              |  223 -
 src/client/pages/settings/import-export.vue        |  112 -
 src/client/pages/settings/index.vue                |  326 -
 src/client/pages/settings/integration.vue          |  141 -
 src/client/pages/settings/menu.vue                 |  117 -
 src/client/pages/settings/mute-block.vue           |   85 -
 src/client/pages/settings/notifications.vue        |   77 -
 src/client/pages/settings/other.vue                |   97 -
 src/client/pages/settings/plugin.install.vue       |  147 -
 src/client/pages/settings/plugin.manage.vue        |  115 -
 src/client/pages/settings/plugin.vue               |   44 -
 src/client/pages/settings/privacy.vue              |  120 -
 src/client/pages/settings/profile.vue              |  281 -
 src/client/pages/settings/reaction.vue             |  152 -
 src/client/pages/settings/registry.keys.vue        |  114 -
 src/client/pages/settings/registry.value.vue       |  149 -
 src/client/pages/settings/registry.vue             |   90 -
 src/client/pages/settings/security.vue             |  158 -
 src/client/pages/settings/sounds.vue               |  155 -
 src/client/pages/settings/theme.install.vue        |  105 -
 src/client/pages/settings/theme.manage.vue         |  105 -
 src/client/pages/settings/theme.vue                |  424 -
 src/client/pages/settings/update.vue               |   95 -
 src/client/pages/settings/word-mute.vue            |  110 -
 src/client/pages/share.vue                         |  184 -
 src/client/pages/signup-complete.vue               |   50 -
 src/client/pages/tag.vue                           |   57 -
 src/client/pages/test.vue                          |  259 -
 src/client/pages/theme-editor.vue                  |  306 -
 src/client/pages/timeline.tutorial.vue             |  131 -
 src/client/pages/timeline.vue                      |  225 -
 src/client/pages/user-ap-info.vue                  |  124 -
 src/client/pages/user-info.vue                     |  245 -
 src/client/pages/user-list-timeline.vue            |  147 -
 src/client/pages/user/clips.vue                    |   50 -
 src/client/pages/user/follow-list.vue              |   65 -
 src/client/pages/user/gallery.vue                  |   56 -
 src/client/pages/user/index.activity.vue           |   34 -
 src/client/pages/user/index.photos.vue             |  107 -
 src/client/pages/user/index.timeline.vue           |   68 -
 src/client/pages/user/index.vue                    |  829 --
 src/client/pages/user/pages.vue                    |   49 -
 src/client/pages/user/reactions.vue                |   81 -
 src/client/pages/v.vue                             |   29 -
 src/client/pages/welcome.entrance.a.vue            |  320 -
 src/client/pages/welcome.entrance.b.vue            |  236 -
 src/client/pages/welcome.entrance.c.vue            |  305 -
 src/client/pages/welcome.setup.vue                 |  102 -
 src/client/pages/welcome.timeline.vue              |   99 -
 src/client/pages/welcome.vue                       |   38 -
 src/client/pizzax.ts                               |  153 -
 src/client/plugin.ts                               |  124 -
 src/client/router.ts                               |  149 -
 src/client/scripts/2fa.ts                          |   33 -
 src/client/scripts/aiscript/api.ts                 |   44 -
 src/client/scripts/autocomplete.ts                 |  276 -
 src/client/scripts/check-word-mute.ts              |   26 -
 src/client/scripts/collect-page-vars.ts            |   48 -
 src/client/scripts/contains.ts                     |    9 -
 src/client/scripts/copy-to-clipboard.ts            |   33 -
 .../scripts/extract-avg-color-from-blurhash.ts     |    9 -
 src/client/scripts/focus.ts                        |   27 -
 src/client/scripts/form.ts                         |   31 -
 src/client/scripts/gen-search-query.ts             |   31 -
 src/client/scripts/get-account-from-id.ts          |    7 -
 src/client/scripts/get-md5.ts                      |   10 -
 src/client/scripts/get-static-image-url.ts         |   16 -
 src/client/scripts/get-user-menu.ts                |  205 -
 src/client/scripts/hotkey.ts                       |   88 -
 src/client/scripts/hpml/block.ts                   |  109 -
 src/client/scripts/hpml/evaluator.ts               |  234 -
 src/client/scripts/hpml/expr.ts                    |   79 -
 src/client/scripts/hpml/index.ts                   |  103 -
 src/client/scripts/hpml/lib.ts                     |  246 -
 src/client/scripts/hpml/type-checker.ts            |  189 -
 src/client/scripts/idb-proxy.ts                    |   37 -
 src/client/scripts/initialize-sw.ts                |   68 -
 src/client/scripts/is-device-darkmode.ts           |    3 -
 src/client/scripts/is-device-touch.ts              |    1 -
 src/client/scripts/is-mobile.ts                    |    2 -
 src/client/scripts/keycode.ts                      |   33 -
 src/client/scripts/loading.ts                      |   11 -
 src/client/scripts/login-id.ts                     |   11 -
 src/client/scripts/lookup-user.ts                  |   37 -
 src/client/scripts/paging.ts                       |  246 -
 src/client/scripts/physics.ts                      |  152 -
 src/client/scripts/please-login.ts                 |   14 -
 src/client/scripts/popout.ts                       |   22 -
 src/client/scripts/reaction-picker.ts              |   41 -
 src/client/scripts/room/furniture.ts               |   21 -
 src/client/scripts/room/furnitures.json5           |  407 -
 src/client/scripts/room/room.ts                    |  775 --
 src/client/scripts/scroll.ts                       |   80 -
 src/client/scripts/search.ts                       |   64 -
 src/client/scripts/select-file.ts                  |   89 -
 src/client/scripts/show-suspended-dialog.ts        |   10 -
 src/client/scripts/sound.ts                        |   34 -
 src/client/scripts/sticky-sidebar.ts               |   50 -
 src/client/scripts/theme-editor.ts                 |   81 -
 src/client/scripts/theme.ts                        |  127 -
 src/client/scripts/unison-reload.ts                |   15 -
 src/client/store.ts                                |  318 -
 src/client/style.scss                              |  562 --
 src/client/sw/compose-notification.ts              |   99 -
 src/client/sw/sw.ts                                |  123 -
 src/client/symbols.ts                              |    1 -
 src/client/theme-store.ts                          |   34 -
 src/client/themes/_dark.json5                      |   90 -
 src/client/themes/_light.json5                     |   90 -
 src/client/themes/d-astro.json5                    |   78 -
 src/client/themes/d-black.json5                    |   17 -
 src/client/themes/d-botanical.json5                |   26 -
 src/client/themes/d-dark.json5                     |   26 -
 src/client/themes/d-future.json5                   |   27 -
 src/client/themes/d-persimmon.json5                |   25 -
 src/client/themes/d-pumpkin.json5                  |   88 -
 src/client/themes/l-apricot.json5                  |   22 -
 src/client/themes/l-light.json5                    |   20 -
 src/client/themes/l-rainy.json5                    |   21 -
 src/client/themes/l-sushi.json5                    |   18 -
 src/client/themes/l-vivid.json5                    |   82 -
 src/client/tsconfig.json                           |   42 -
 src/client/ui/_common_/common.vue                  |   89 -
 src/client/ui/_common_/sidebar.vue                 |  388 -
 src/client/ui/_common_/stream-indicator.vue        |   70 -
 src/client/ui/_common_/upload.vue                  |  134 -
 src/client/ui/chat/date-separated-list.vue         |  163 -
 src/client/ui/chat/header-clock.vue                |   62 -
 src/client/ui/chat/index.vue                       |  467 --
 src/client/ui/chat/note-header.vue                 |  112 -
 src/client/ui/chat/note-preview.vue                |  112 -
 src/client/ui/chat/note.sub.vue                    |  137 -
 src/client/ui/chat/note.vue                        | 1144 ---
 src/client/ui/chat/notes.vue                       |   94 -
 src/client/ui/chat/pages/channel.vue               |  259 -
 src/client/ui/chat/pages/timeline.vue              |  221 -
 src/client/ui/chat/post-form.vue                   |  773 --
 src/client/ui/chat/side.vue                        |  157 -
 src/client/ui/chat/store.ts                        |   17 -
 src/client/ui/chat/sub-note-content.vue            |   62 -
 src/client/ui/chat/widgets.vue                     |   62 -
 src/client/ui/classic.header.vue                   |  210 -
 src/client/ui/classic.side.vue                     |  158 -
 src/client/ui/classic.sidebar.vue                  |  263 -
 src/client/ui/classic.vue                          |  471 --
 src/client/ui/classic.widgets.vue                  |   84 -
 src/client/ui/deck.vue                             |  229 -
 src/client/ui/deck/antenna-column.vue              |   80 -
 src/client/ui/deck/column-core.vue                 |   52 -
 src/client/ui/deck/column.vue                      |  408 -
 src/client/ui/deck/deck-store.ts                   |  298 -
 src/client/ui/deck/direct-column.vue               |   55 -
 src/client/ui/deck/list-column.vue                 |   80 -
 src/client/ui/deck/main-column.vue                 |   91 -
 src/client/ui/deck/mentions-column.vue             |   52 -
 src/client/ui/deck/notifications-column.vue        |   53 -
 src/client/ui/deck/tl-column.vue                   |  137 -
 src/client/ui/deck/widgets-column.vue              |   71 -
 src/client/ui/desktop.vue                          |   70 -
 src/client/ui/universal.vue                        |  402 -
 src/client/ui/universal.widgets.vue                |   79 -
 src/client/ui/visitor.vue                          |   19 -
 src/client/ui/visitor/a.vue                        |  260 -
 src/client/ui/visitor/b.vue                        |  282 -
 src/client/ui/visitor/header.vue                   |  228 -
 src/client/ui/visitor/kanban.vue                   |  256 -
 src/client/ui/zen.vue                              |  106 -
 src/client/widgets/activity.calendar.vue           |   85 -
 src/client/widgets/activity.chart.vue              |  107 -
 src/client/widgets/activity.vue                    |   82 -
 src/client/widgets/aichan.vue                      |   59 -
 src/client/widgets/aiscript.vue                    |  163 -
 src/client/widgets/button.vue                      |   95 -
 src/client/widgets/calendar.vue                    |  204 -
 src/client/widgets/clock.vue                       |   55 -
 src/client/widgets/define.ts                       |   75 -
 src/client/widgets/digital-clock.vue               |   79 -
 src/client/widgets/federation.vue                  |  145 -
 src/client/widgets/index.ts                        |   45 -
 src/client/widgets/job-queue.vue                   |  183 -
 src/client/widgets/memo.vue                        |  106 -
 src/client/widgets/notifications.vue               |   65 -
 src/client/widgets/online-users.vue                |   67 -
 src/client/widgets/photos.vue                      |  113 -
 src/client/widgets/post-form.vue                   |   23 -
 src/client/widgets/rss.vue                         |   89 -
 src/client/widgets/server-metric/cpu-mem.vue       |  174 -
 src/client/widgets/server-metric/cpu.vue           |   76 -
 src/client/widgets/server-metric/disk.vue          |   70 -
 src/client/widgets/server-metric/index.vue         |   82 -
 src/client/widgets/server-metric/mem.vue           |   85 -
 src/client/widgets/server-metric/net.vue           |  148 -
 src/client/widgets/server-metric/pie.vue           |   65 -
 src/client/widgets/slideshow.vue                   |  167 -
 src/client/widgets/timeline.vue                    |  116 -
 src/client/widgets/trends.vue                      |  111 -
 src/config/index.ts                                |    3 -
 src/config/load.ts                                 |   61 -
 src/config/types.ts                                |   85 -
 src/const.ts                                       |    2 -
 src/daemons/janitor.ts                             |   20 -
 src/daemons/queue-stats.ts                         |   60 -
 src/daemons/server-stats.ts                        |   79 -
 src/db/elasticsearch.ts                            |   56 -
 src/db/logger.ts                                   |    3 -
 src/db/postgre.ts                                  |  225 -
 src/db/redis.ts                                    |   19 -
 src/emojilist.json                                 | 1749 -----
 src/env.ts                                         |   20 -
 src/games/reversi/core.ts                          |  263 -
 src/games/reversi/maps.ts                          |  896 ---
 src/games/reversi/package.json                     |   18 -
 src/games/reversi/tsconfig.json                    |   21 -
 src/global.d.ts                                    |    1 -
 src/index.ts                                       |   13 -
 src/meta.json                                      |    3 -
 src/mfm/fn-name-list.ts                            |   23 -
 src/mfm/from-html.ts                               |  209 -
 src/mfm/to-html.ts                                 |  159 -
 src/misc/acct.ts                                   |   11 -
 src/misc/antenna-cache.ts                          |   36 -
 src/misc/api-permissions.ts                        |   35 -
 src/misc/app-lock.ts                               |   31 -
 src/misc/before-shutdown.ts                        |   92 -
 src/misc/cache.ts                                  |   43 -
 src/misc/cafy-id.ts                                |   32 -
 src/misc/captcha.ts                                |   56 -
 src/misc/check-hit-antenna.ts                      |   90 -
 src/misc/check-word-mute.ts                        |   39 -
 src/misc/content-disposition.ts                    |    6 -
 src/misc/convert-host.ts                           |   26 -
 src/misc/count-same-renotes.ts                     |   15 -
 src/misc/create-temp.ts                            |   10 -
 src/misc/detect-url-mime.ts                        |   15 -
 src/misc/download-text-file.ts                     |   25 -
 src/misc/download-url.ts                           |   87 -
 src/misc/emoji-regex.ts                            |    3 -
 src/misc/emojilist.ts                              |    7 -
 src/misc/extract-custom-emojis-from-mfm.ts         |   10 -
 src/misc/extract-hashtags.ts                       |    9 -
 src/misc/extract-mentions.ts                       |   11 -
 src/misc/extract-url-from-mfm.ts                   |   19 -
 src/misc/fetch-meta.ts                             |   35 -
 src/misc/fetch-proxy-account.ts                    |    9 -
 src/misc/fetch.ts                                  |  141 -
 src/misc/format-time-string.ts                     |   50 -
 src/misc/gen-avatar.ts                             |   90 -
 src/misc/gen-id.ts                                 |   21 -
 src/misc/gen-key-pair.ts                           |   36 -
 src/misc/get-file-info.ts                          |  196 -
 src/misc/get-note-summary.ts                       |   52 -
 src/misc/get-reaction-emoji.ts                     |   16 -
 src/misc/get-user-name.ts                          |    5 -
 src/misc/hard-limits.ts                            |   14 -
 src/misc/i18n.ts                                   |   29 -
 src/misc/id/aid.ts                                 |   25 -
 src/misc/id/meid.ts                                |   26 -
 src/misc/id/meidg.ts                               |   28 -
 src/misc/id/object-id.ts                           |   26 -
 src/misc/identifiable-error.ts                     |   13 -
 src/misc/is-blocker-user-related.ts                |   15 -
 src/misc/is-duplicate-key-value-error.ts           |    3 -
 src/misc/is-muted-user-related.ts                  |   15 -
 src/misc/is-quote.ts                               |    5 -
 src/misc/keypair-store.ts                          |   10 -
 src/misc/license.ts                                |   19 -
 src/misc/normalize-for-search.ts                   |    6 -
 src/misc/nyaize.ts                                 |   15 -
 src/misc/populate-emojis.ts                        |  124 -
 src/misc/reaction-lib.ts                           |  129 -
 src/misc/safe-for-sql.ts                           |    3 -
 src/misc/schema.ts                                 |  107 -
 src/misc/secure-rndstr.ts                          |   21 -
 src/misc/show-machine-info.ts                      |   13 -
 src/misc/simple-schema.ts                          |   15 -
 src/misc/truncate.ts                               |   11 -
 src/misc/twemoji-base.ts                           |    1 -
 src/models/entities/abuse-user-report.ts           |   74 -
 src/models/entities/access-token.ts                |   96 -
 src/models/entities/ad.ts                          |   59 -
 src/models/entities/announcement-read.ts           |   36 -
 src/models/entities/announcement.ts                |   43 -
 src/models/entities/antenna-note.ts                |   43 -
 src/models/entities/antenna.ts                     |   99 -
 src/models/entities/app.ts                         |   60 -
 src/models/entities/attestation-challenge.ts       |   46 -
 src/models/entities/auth-session.ts                |   43 -
 src/models/entities/blocking.ts                    |   42 -
 src/models/entities/channel-following.ts           |   43 -
 src/models/entities/channel-note-pining.ts         |   35 -
 src/models/entities/channel.ts                     |   75 -
 src/models/entities/clip-note.ts                   |   37 -
 src/models/entities/clip.ts                        |   44 -
 src/models/entities/drive-file.ts                  |  164 -
 src/models/entities/drive-folder.ts                |   49 -
 src/models/entities/emoji.ts                       |   51 -
 src/models/entities/follow-request.ts              |   85 -
 src/models/entities/following.ts                   |   80 -
 src/models/entities/gallery-like.ts                |   33 -
 src/models/entities/gallery-post.ts                |   79 -
 src/models/entities/games/reversi/game.ts          |  133 -
 src/models/entities/games/reversi/matching.ts      |   35 -
 src/models/entities/hashtag.ts                     |   87 -
 src/models/entities/instance.ts                    |  180 -
 src/models/entities/messaging-message.ts           |   89 -
 src/models/entities/meta.ts                        |  423 -
 src/models/entities/moderation-log.ts              |   32 -
 src/models/entities/muted-note.ts                  |   48 -
 src/models/entities/muting.ts                      |   42 -
 src/models/entities/note-favorite.ts               |   35 -
 src/models/entities/note-reaction.ts               |   44 -
 src/models/entities/note-thread-muting.ts          |   33 -
 src/models/entities/note-unread.ts                 |   63 -
 src/models/entities/note-watching.ts               |   52 -
 src/models/entities/note.ts                        |  247 -
 src/models/entities/notification.ts                |  172 -
 src/models/entities/page-like.ts                   |   33 -
 src/models/entities/page.ts                        |  121 -
 src/models/entities/password-reset-request.ts      |   30 -
 src/models/entities/poll-vote.ts                   |   40 -
 src/models/entities/poll.ts                        |   72 -
 src/models/entities/promo-note.ts                  |   28 -
 src/models/entities/promo-read.ts                  |   35 -
 src/models/entities/registration-tickets.ts        |   17 -
 src/models/entities/registry-item.ts               |   58 -
 src/models/entities/relay.ts                       |   19 -
 src/models/entities/signin.ts                      |   35 -
 src/models/entities/sw-subscription.ts             |   37 -
 src/models/entities/used-username.ts               |   20 -
 src/models/entities/user-group-invitation.ts       |   42 -
 src/models/entities/user-group-joining.ts          |   42 -
 src/models/entities/user-group.ts                  |   46 -
 src/models/entities/user-keypair.ts                |   33 -
 src/models/entities/user-list-joining.ts           |   42 -
 src/models/entities/user-list.ts                   |   33 -
 src/models/entities/user-note-pining.ts            |   35 -
 src/models/entities/user-pending.ts                |   32 -
 src/models/entities/user-profile.ts                |  215 -
 src/models/entities/user-publickey.ts              |   34 -
 src/models/entities/user-security-key.ts           |   48 -
 src/models/entities/user.ts                        |  250 -
 src/models/id.ts                                   |    4 -
 src/models/index.ts                                |  130 -
 src/models/repositories/abuse-user-report.ts       |   38 -
 src/models/repositories/antenna.ts                 |  124 -
 src/models/repositories/app.ts                     |   75 -
 src/models/repositories/auth-session.ts            |   21 -
 src/models/repositories/blocking.ts                |   60 -
 src/models/repositories/channel.ts                 |   95 -
 src/models/repositories/clip.ts                    |   70 -
 src/models/repositories/drive-file.ts              |  249 -
 src/models/repositories/drive-folder.ts            |   91 -
 src/models/repositories/emoji.ts                   |   65 -
 src/models/repositories/federation-instance.ts     |  106 -
 src/models/repositories/follow-request.ts          |   20 -
 src/models/repositories/following.ts               |  124 -
 src/models/repositories/gallery-like.ts            |   25 -
 src/models/repositories/gallery-post.ts            |  111 -
 src/models/repositories/games/reversi/game.ts      |  191 -
 src/models/repositories/games/reversi/matching.ts  |   69 -
 src/models/repositories/hashtag.ts                 |   62 -
 src/models/repositories/messaging-message.ts       |  119 -
 src/models/repositories/moderation-logs.ts         |   30 -
 src/models/repositories/muting.ts                  |   60 -
 src/models/repositories/note-favorite.ts           |   56 -
 src/models/repositories/note-reaction.ts           |   60 -
 src/models/repositories/note.ts                    |  512 --
 src/models/repositories/notification.ts            |  175 -
 src/models/repositories/page-like.ts               |   26 -
 src/models/repositories/page.ts                    |  142 -
 src/models/repositories/queue.ts                   |   30 -
 src/models/repositories/relay.ts                   |    6 -
 src/models/repositories/signin.ts                  |   11 -
 src/models/repositories/user-group-invitation.ts   |   23 -
 src/models/repositories/user-group.ts              |   61 -
 src/models/repositories/user-list.ts               |   55 -
 src/models/repositories/user.ts                    |  659 --
 src/prelude/README.md                              |    3 -
 src/prelude/array.ts                               |  138 -
 src/prelude/await-all.ts                           |   23 -
 src/prelude/math.ts                                |    3 -
 src/prelude/maybe.ts                               |   20 -
 src/prelude/relation.ts                            |    5 -
 src/prelude/string.ts                              |   15 -
 src/prelude/symbol.ts                              |    1 -
 src/prelude/time.ts                                |   39 -
 src/prelude/url.ts                                 |   13 -
 src/prelude/xml.ts                                 |   41 -
 src/queue/get-job-info.ts                          |   15 -
 src/queue/index.ts                                 |  255 -
 src/queue/initialize.ts                            |   33 -
 src/queue/logger.ts                                |    3 -
 src/queue/processors/db/delete-account.ts          |   94 -
 src/queue/processors/db/delete-drive-files.ts      |   56 -
 src/queue/processors/db/export-blocking.ts         |   94 -
 src/queue/processors/db/export-following.ts        |   94 -
 src/queue/processors/db/export-mute.ts             |   94 -
 src/queue/processors/db/export-notes.ts            |  133 -
 src/queue/processors/db/export-user-lists.ts       |   71 -
 src/queue/processors/db/import-blocking.ts         |   74 -
 src/queue/processors/db/import-following.ts        |   73 -
 src/queue/processors/db/import-muting.ts           |   83 -
 src/queue/processors/db/import-user-lists.ts       |   80 -
 src/queue/processors/db/index.ts                   |   33 -
 src/queue/processors/deliver.ts                    |   94 -
 src/queue/processors/inbox.ts                      |  149 -
 .../object-storage/clean-remote-files.ts           |   50 -
 src/queue/processors/object-storage/delete-file.ts |   11 -
 src/queue/processors/object-storage/index.ts       |   15 -
 src/queue/processors/system/index.ts               |   12 -
 src/queue/processors/system/resync-charts.ts       |   21 -
 src/queue/queues.ts                                |    9 -
 src/queue/types.ts                                 |   44 -
 src/remote/activitypub/ap-request.ts               |  104 -
 src/remote/activitypub/audience.ts                 |   92 -
 src/remote/activitypub/db-resolver.ts              |  140 -
 src/remote/activitypub/deliver-manager.ts          |  131 -
 src/remote/activitypub/kernel/accept/follow.ts     |   29 -
 src/remote/activitypub/kernel/accept/index.ts      |   24 -
 src/remote/activitypub/kernel/add/index.ts         |   23 -
 src/remote/activitypub/kernel/announce/index.ts    |   19 -
 src/remote/activitypub/kernel/announce/note.ts     |   67 -
 src/remote/activitypub/kernel/block/index.ts       |   22 -
 src/remote/activitypub/kernel/create/index.ts      |   43 -
 src/remote/activitypub/kernel/create/note.ts       |   44 -
 src/remote/activitypub/kernel/delete/actor.ts      |   26 -
 src/remote/activitypub/kernel/delete/index.ts      |   49 -
 src/remote/activitypub/kernel/delete/note.ts       |   41 -
 src/remote/activitypub/kernel/flag/index.ts        |   30 -
 src/remote/activitypub/kernel/follow.ts            |   20 -
 src/remote/activitypub/kernel/index.ts             |   71 -
 src/remote/activitypub/kernel/like.ts              |   21 -
 src/remote/activitypub/kernel/move/index.ts        |    0
 src/remote/activitypub/kernel/read.ts              |   27 -
 src/remote/activitypub/kernel/reject/follow.ts     |   29 -
 src/remote/activitypub/kernel/reject/index.ts      |   24 -
 src/remote/activitypub/kernel/remove/index.ts      |   23 -
 src/remote/activitypub/kernel/undo/announce.ts     |   17 -
 src/remote/activitypub/kernel/undo/block.ts        |   20 -
 src/remote/activitypub/kernel/undo/follow.ts       |   41 -
 src/remote/activitypub/kernel/undo/index.ts        |   34 -
 src/remote/activitypub/kernel/undo/like.ts         |   21 -
 src/remote/activitypub/kernel/update/index.ts      |   34 -
 src/remote/activitypub/logger.ts                   |    3 -
 src/remote/activitypub/misc/contexts.ts            |  526 --
 src/remote/activitypub/misc/get-note-html.ts       |   11 -
 src/remote/activitypub/misc/html-to-mfm.ts         |    9 -
 src/remote/activitypub/misc/ld-signature.ts        |  134 -
 src/remote/activitypub/models/icon.ts              |    5 -
 src/remote/activitypub/models/identifier.ts        |    5 -
 src/remote/activitypub/models/image.ts             |   62 -
 src/remote/activitypub/models/mention.ts           |   24 -
 src/remote/activitypub/models/note.ts              |  356 -
 src/remote/activitypub/models/person.ts            |  494 --
 src/remote/activitypub/models/question.ts          |   83 -
 src/remote/activitypub/models/tag.ts               |   18 -
 src/remote/activitypub/perform.ts                  |    7 -
 src/remote/activitypub/renderer/accept.ts          |    8 -
 src/remote/activitypub/renderer/add.ts             |    9 -
 src/remote/activitypub/renderer/announce.ts        |   29 -
 src/remote/activitypub/renderer/block.ts           |    8 -
 src/remote/activitypub/renderer/create.ts          |   17 -
 src/remote/activitypub/renderer/delete.ts          |    9 -
 src/remote/activitypub/renderer/document.ts        |    9 -
 src/remote/activitypub/renderer/emoji.ts           |   14 -
 src/remote/activitypub/renderer/follow-relay.ts    |   14 -
 src/remote/activitypub/renderer/follow-user.ts     |   12 -
 src/remote/activitypub/renderer/follow.ts          |   15 -
 src/remote/activitypub/renderer/hashtag.ts         |    7 -
 src/remote/activitypub/renderer/image.ts           |    9 -
 src/remote/activitypub/renderer/index.ts           |   59 -
 src/remote/activitypub/renderer/key.ts             |   14 -
 src/remote/activitypub/renderer/like.ts            |   30 -
 src/remote/activitypub/renderer/mention.ts         |    9 -
 src/remote/activitypub/renderer/note.ts            |  168 -
 .../renderer/ordered-collection-page.ts            |   23 -
 .../activitypub/renderer/ordered-collection.ts     |   21 -
 src/remote/activitypub/renderer/person.ts          |   90 -
 src/remote/activitypub/renderer/question.ts        |   23 -
 src/remote/activitypub/renderer/read.ts            |    9 -
 src/remote/activitypub/renderer/reject.ts          |    8 -
 src/remote/activitypub/renderer/remove.ts          |    9 -
 src/remote/activitypub/renderer/tombstone.ts       |    4 -
 src/remote/activitypub/renderer/undo.ts            |   13 -
 src/remote/activitypub/renderer/update.ts          |   15 -
 src/remote/activitypub/renderer/vote.ts            |   23 -
 src/remote/activitypub/request.ts                  |   58 -
 src/remote/activitypub/resolver.ts                 |   73 -
 src/remote/activitypub/type.ts                     |  289 -
 src/remote/logger.ts                               |    3 -
 src/remote/resolve-user.ts                         |  110 -
 src/remote/webfinger.ts                            |   34 -
 src/server/activitypub.ts                          |  227 -
 src/server/activitypub/featured.ts                 |   41 -
 src/server/activitypub/followers.ts                |  102 -
 src/server/activitypub/following.ts                |  103 -
 src/server/activitypub/outbox.ts                   |  108 -
 src/server/api/2fa.ts                              |  422 -
 src/server/api/api-handler.ts                      |   51 -
 src/server/api/authenticate.ts                     |   62 -
 src/server/api/call.ts                             |  109 -
 src/server/api/common/generate-block-query.ts      |   42 -
 src/server/api/common/generate-channel-query.ts    |   24 -
 src/server/api/common/generate-muted-note-query.ts |   13 -
 .../api/common/generate-muted-note-thread-query.ts |   17 -
 src/server/api/common/generate-muted-user-query.ts |   40 -
 .../api/common/generate-native-user-token.ts       |    3 -
 src/server/api/common/generate-replies-query.ts    |   27 -
 src/server/api/common/generate-visibility-query.ts |   40 -
 src/server/api/common/getters.ts                   |   56 -
 src/server/api/common/inject-featured.ts           |   56 -
 src/server/api/common/inject-promo.ts              |   34 -
 src/server/api/common/is-native-token.ts           |    1 -
 src/server/api/common/make-pagination-query.ts     |   28 -
 src/server/api/common/read-messaging-message.ts    |  122 -
 src/server/api/common/read-notification.ts         |   43 -
 src/server/api/common/signin.ts                    |   44 -
 src/server/api/common/signup.ts                    |  113 -
 src/server/api/define.ts                           |   87 -
 src/server/api/endpoints.ts                        |  124 -
 .../api/endpoints/admin/abuse-user-reports.ts      |  134 -
 src/server/api/endpoints/admin/accounts/create.ts  |   51 -
 src/server/api/endpoints/admin/accounts/delete.ts  |   58 -
 src/server/api/endpoints/admin/ad/create.ts        |   49 -
 src/server/api/endpoints/admin/ad/delete.ts        |   34 -
 src/server/api/endpoints/admin/ad/list.ts          |   36 -
 src/server/api/endpoints/admin/ad/update.ts        |   63 -
 .../api/endpoints/admin/announcements/create.ts    |   71 -
 .../api/endpoints/admin/announcements/delete.ts    |   34 -
 .../api/endpoints/admin/announcements/list.ts      |   84 -
 .../api/endpoints/admin/announcements/update.ts    |   48 -
 .../endpoints/admin/delete-all-files-of-a-user.ts  |   28 -
 src/server/api/endpoints/admin/delete-logs.ts      |   13 -
 .../endpoints/admin/drive/clean-remote-files.ts    |   13 -
 src/server/api/endpoints/admin/drive/cleanup.ts    |   21 -
 src/server/api/endpoints/admin/drive/files.ts      |   81 -
 src/server/api/endpoints/admin/drive/show-file.ts  |  180 -
 src/server/api/endpoints/admin/emoji/add.ts        |   64 -
 src/server/api/endpoints/admin/emoji/copy.ts       |   81 -
 .../api/endpoints/admin/emoji/list-remote.ts       |   99 -
 src/server/api/endpoints/admin/emoji/list.ts       |   98 -
 src/server/api/endpoints/admin/emoji/remove.ts     |   42 -
 src/server/api/endpoints/admin/emoji/update.ts     |   54 -
 .../endpoints/admin/federation/delete-all-files.ts |   27 -
 .../federation/refresh-remote-instance-metadata.ts |   28 -
 .../admin/federation/remove-all-following.ts       |   32 -
 .../endpoints/admin/federation/update-instance.ts  |   33 -
 src/server/api/endpoints/admin/get-index-stats.ts  |   26 -
 src/server/api/endpoints/admin/get-table-stats.ts  |   45 -
 src/server/api/endpoints/admin/invite.ts           |   44 -
 src/server/api/endpoints/admin/moderators/add.ts   |   33 -
 .../api/endpoints/admin/moderators/remove.ts       |   29 -
 src/server/api/endpoints/admin/promo/create.ts     |   57 -
 src/server/api/endpoints/admin/queue/clear.ts      |   18 -
 .../api/endpoints/admin/queue/deliver-delayed.ts   |   55 -
 .../api/endpoints/admin/queue/inbox-delayed.ts     |   55 -
 src/server/api/endpoints/admin/queue/jobs.ts       |   81 -
 src/server/api/endpoints/admin/queue/stats.ts      |   44 -
 src/server/api/endpoints/admin/relays/add.ts       |   63 -
 src/server/api/endpoints/admin/relays/list.ts      |   47 -
 src/server/api/endpoints/admin/relays/remove.ts    |   20 -
 src/server/api/endpoints/admin/reset-password.ts   |   59 -
 .../endpoints/admin/resolve-abuse-user-report.ts   |   30 -
 src/server/api/endpoints/admin/resync-chart.ts     |   21 -
 src/server/api/endpoints/admin/send-email.ts       |   26 -
 src/server/api/endpoints/admin/server-info.ts      |  119 -
 .../api/endpoints/admin/show-moderation-logs.ts    |   74 -
 src/server/api/endpoints/admin/show-user.ts        |  177 -
 src/server/api/endpoints/admin/show-users.ts       |  119 -
 src/server/api/endpoints/admin/silence-user.ts     |   38 -
 src/server/api/endpoints/admin/suspend-user.ts     |   84 -
 src/server/api/endpoints/admin/unsilence-user.ts   |   34 -
 src/server/api/endpoints/admin/unsuspend-user.ts   |   37 -
 src/server/api/endpoints/admin/update-meta.ts      |  608 --
 src/server/api/endpoints/admin/vacuum.ts           |   36 -
 src/server/api/endpoints/announcements.ts          |   92 -
 src/server/api/endpoints/antennas/create.ts        |  127 -
 src/server/api/endpoints/antennas/delete.ts        |   43 -
 src/server/api/endpoints/antennas/list.ts          |   28 -
 src/server/api/endpoints/antennas/notes.ts         |   93 -
 src/server/api/endpoints/antennas/show.ts          |   47 -
 src/server/api/endpoints/antennas/update.ts        |  143 -
 src/server/api/endpoints/ap/get.ts                 |   36 -
 src/server/api/endpoints/ap/show.ts                |  190 -
 src/server/api/endpoints/app/create.ts             |   63 -
 src/server/api/endpoints/app/show.ts               |   51 -
 src/server/api/endpoints/auth/accept.ts            |   76 -
 src/server/api/endpoints/auth/session/generate.ts  |   70 -
 src/server/api/endpoints/auth/session/show.ts      |   58 -
 src/server/api/endpoints/auth/session/userkey.ts   |   98 -
 src/server/api/endpoints/blocking/create.ts        |   89 -
 src/server/api/endpoints/blocking/delete.ts        |   85 -
 src/server/api/endpoints/blocking/list.ts          |   49 -
 src/server/api/endpoints/channels/create.ts        |   68 -
 src/server/api/endpoints/channels/featured.ts      |   28 -
 src/server/api/endpoints/channels/follow.ts        |   48 -
 src/server/api/endpoints/channels/followed.ts      |   49 -
 src/server/api/endpoints/channels/owned.ts         |   49 -
 src/server/api/endpoints/channels/pin-note.ts      |    0
 src/server/api/endpoints/channels/show.ts          |   43 -
 src/server/api/endpoints/channels/timeline.ts      |   85 -
 src/server/api/endpoints/channels/unfollow.ts      |   45 -
 src/server/api/endpoints/channels/update.ts        |   94 -
 src/server/api/endpoints/charts/active-users.ts    |   30 -
 src/server/api/endpoints/charts/drive.ts           |   30 -
 src/server/api/endpoints/charts/federation.ts      |   30 -
 src/server/api/endpoints/charts/hashtag.ts         |   34 -
 src/server/api/endpoints/charts/instance.ts        |   34 -
 src/server/api/endpoints/charts/network.ts         |   30 -
 src/server/api/endpoints/charts/notes.ts           |   30 -
 src/server/api/endpoints/charts/user/drive.ts      |   35 -
 src/server/api/endpoints/charts/user/following.ts  |   35 -
 src/server/api/endpoints/charts/user/notes.ts      |   35 -
 src/server/api/endpoints/charts/user/reactions.ts  |   35 -
 src/server/api/endpoints/charts/users.ts           |   30 -
 src/server/api/endpoints/clips/add-note.ts         |   76 -
 src/server/api/endpoints/clips/create.ts           |   45 -
 src/server/api/endpoints/clips/delete.ts           |   40 -
 src/server/api/endpoints/clips/list.ts             |   28 -
 src/server/api/endpoints/clips/notes.ts            |   93 -
 src/server/api/endpoints/clips/show.ts             |   50 -
 src/server/api/endpoints/clips/update.ts           |   65 -
 src/server/api/endpoints/drive.ts                  |   38 -
 src/server/api/endpoints/drive/files.ts            |   70 -
 .../api/endpoints/drive/files/attached-notes.ts    |   57 -
 .../api/endpoints/drive/files/check-existence.ts   |   31 -
 src/server/api/endpoints/drive/files/create.ts     |   89 -
 src/server/api/endpoints/drive/files/delete.ts     |   53 -
 .../api/endpoints/drive/files/find-by-hash.ts      |   36 -
 src/server/api/endpoints/drive/files/find.ts       |   43 -
 src/server/api/endpoints/drive/files/show.ts       |   84 -
 src/server/api/endpoints/drive/files/update.ts     |  116 -
 .../api/endpoints/drive/files/upload-from-url.ts   |   64 -
 src/server/api/endpoints/drive/folders.ts          |   58 -
 src/server/api/endpoints/drive/folders/create.ts   |   72 -
 src/server/api/endpoints/drive/folders/delete.ts   |   60 -
 src/server/api/endpoints/drive/folders/find.ts     |   43 -
 src/server/api/endpoints/drive/folders/show.ts     |   49 -
 src/server/api/endpoints/drive/folders/update.ts   |  123 -
 src/server/api/endpoints/drive/stream.ts           |   59 -
 .../api/endpoints/email-address/available.ts       |   34 -
 src/server/api/endpoints/endpoint.ts               |   26 -
 src/server/api/endpoints/endpoints.ts              |   30 -
 src/server/api/endpoints/federation/dns.ts         |   43 -
 src/server/api/endpoints/federation/followers.ts   |   51 -
 src/server/api/endpoints/federation/following.ts   |   51 -
 src/server/api/endpoints/federation/instances.ts   |  149 -
 .../api/endpoints/federation/show-instance.ts      |   29 -
 .../api/endpoints/federation/update-remote-user.ts |   22 -
 src/server/api/endpoints/federation/users.ts       |   51 -
 src/server/api/endpoints/following/create.ts       |  100 -
 src/server/api/endpoints/following/delete.ts       |   82 -
 .../api/endpoints/following/requests/accept.ts     |   48 -
 .../api/endpoints/following/requests/cancel.ts     |   58 -
 .../api/endpoints/following/requests/list.ts       |   44 -
 .../api/endpoints/following/requests/reject.ts     |   40 -
 src/server/api/endpoints/gallery/featured.ts       |   29 -
 src/server/api/endpoints/gallery/popular.ts        |   28 -
 src/server/api/endpoints/gallery/posts.ts          |   43 -
 src/server/api/endpoints/gallery/posts/create.ts   |   77 -
 src/server/api/endpoints/gallery/posts/delete.ts   |   40 -
 src/server/api/endpoints/gallery/posts/like.ts     |   71 -
 src/server/api/endpoints/gallery/posts/show.ts     |   43 -
 src/server/api/endpoints/gallery/posts/unlike.ts   |   54 -
 src/server/api/endpoints/gallery/posts/update.ts   |   82 -
 src/server/api/endpoints/games/reversi/games.ts    |  156 -
 .../api/endpoints/games/reversi/games/show.ts      |  168 -
 .../api/endpoints/games/reversi/games/surrender.ts |   67 -
 .../api/endpoints/games/reversi/invitations.ts     |   58 -
 src/server/api/endpoints/games/reversi/match.ts    |  108 -
 .../api/endpoints/games/reversi/match/cancel.ts    |   14 -
 src/server/api/endpoints/get-online-users-count.ts |   23 -
 src/server/api/endpoints/hashtags/list.ts          |   95 -
 src/server/api/endpoints/hashtags/search.ts        |   46 -
 src/server/api/endpoints/hashtags/show.ts          |   40 -
 src/server/api/endpoints/hashtags/trend.ts         |  146 -
 src/server/api/endpoints/hashtags/users.ts         |   89 -
 src/server/api/endpoints/i.ts                      |   26 -
 src/server/api/endpoints/i/2fa/done.ts             |   41 -
 src/server/api/endpoints/i/2fa/key-done.ts         |  150 -
 src/server/api/endpoints/i/2fa/password-less.ts    |   21 -
 src/server/api/endpoints/i/2fa/register-key.ts     |   59 -
 src/server/api/endpoints/i/2fa/register.ts         |   54 -
 src/server/api/endpoints/i/2fa/remove-key.ts       |   45 -
 src/server/api/endpoints/i/2fa/unregister.ts       |   32 -
 src/server/api/endpoints/i/apps.ts                 |   43 -
 src/server/api/endpoints/i/authorized-apps.ts      |   44 -
 src/server/api/endpoints/i/change-password.ts      |   39 -
 src/server/api/endpoints/i/delete-account.ts       |   48 -
 src/server/api/endpoints/i/export-blocking.ts      |   16 -
 src/server/api/endpoints/i/export-following.ts     |   16 -
 src/server/api/endpoints/i/export-mute.ts          |   16 -
 src/server/api/endpoints/i/export-notes.ts         |   16 -
 src/server/api/endpoints/i/export-user-lists.ts    |   16 -
 src/server/api/endpoints/i/favorites.ts            |   50 -
 src/server/api/endpoints/i/gallery/likes.ts        |   57 -
 src/server/api/endpoints/i/gallery/posts.ts        |   49 -
 .../api/endpoints/i/get-word-muted-notes-count.ts  |   33 -
 src/server/api/endpoints/i/import-blocking.ts      |   60 -
 src/server/api/endpoints/i/import-following.ts     |   59 -
 src/server/api/endpoints/i/import-muting.ts        |   60 -
 src/server/api/endpoints/i/import-user-lists.ts    |   59 -
 src/server/api/endpoints/i/notifications.ts        |  138 -
 src/server/api/endpoints/i/page-likes.ts           |   57 -
 src/server/api/endpoints/i/pages.ts                |   49 -
 src/server/api/endpoints/i/pin.ts                  |   59 -
 .../api/endpoints/i/read-all-messaging-messages.ts |   37 -
 .../api/endpoints/i/read-all-unread-notes.ts       |   25 -
 src/server/api/endpoints/i/read-announcement.ts    |   60 -
 src/server/api/endpoints/i/regenerate-token.ts     |   44 -
 src/server/api/endpoints/i/registry/get-all.ts     |   33 -
 src/server/api/endpoints/i/registry/get-detail.ts  |   48 -
 src/server/api/endpoints/i/registry/get.ts         |   45 -
 .../api/endpoints/i/registry/keys-with-type.ts     |   41 -
 src/server/api/endpoints/i/registry/keys.ts        |   28 -
 src/server/api/endpoints/i/registry/remove.ts      |   45 -
 src/server/api/endpoints/i/registry/scopes.ts      |   29 -
 src/server/api/endpoints/i/registry/set.ts         |   61 -
 src/server/api/endpoints/i/revoke-token.ts         |   31 -
 src/server/api/endpoints/i/signin-history.ts       |   35 -
 src/server/api/endpoints/i/unpin.ts                |   45 -
 src/server/api/endpoints/i/update-email.ts         |   94 -
 src/server/api/endpoints/i/update.ts               |  294 -
 src/server/api/endpoints/i/user-group-invites.ts   |   61 -
 src/server/api/endpoints/messaging/history.ts      |   94 -
 src/server/api/endpoints/messaging/messages.ts     |  148 -
 .../api/endpoints/messaging/messages/create.ts     |  148 -
 .../api/endpoints/messaging/messages/delete.ts     |   48 -
 .../api/endpoints/messaging/messages/read.ts       |   48 -
 src/server/api/endpoints/meta.ts                   |  598 --
 src/server/api/endpoints/miauth/gen-token.ts       |   72 -
 src/server/api/endpoints/mute/create.ts            |   83 -
 src/server/api/endpoints/mute/delete.ts            |   73 -
 src/server/api/endpoints/mute/list.ts              |   49 -
 src/server/api/endpoints/my/apps.ts                |   86 -
 src/server/api/endpoints/notes.ts                  |   94 -
 src/server/api/endpoints/notes/children.ts         |   72 -
 src/server/api/endpoints/notes/clips.ts            |   55 -
 src/server/api/endpoints/notes/conversation.ts     |   81 -
 src/server/api/endpoints/notes/create.ts           |  299 -
 src/server/api/endpoints/notes/delete.ts           |   56 -
 src/server/api/endpoints/notes/favorites/create.ts |   61 -
 src/server/api/endpoints/notes/favorites/delete.ts |   55 -
 src/server/api/endpoints/notes/featured.ts         |   64 -
 src/server/api/endpoints/notes/global-timeline.ts  |  101 -
 src/server/api/endpoints/notes/hybrid-timeline.ts  |  158 -
 src/server/api/endpoints/notes/local-timeline.ts   |  129 -
 src/server/api/endpoints/notes/mentions.ts         |   88 -
 .../api/endpoints/notes/polls/recommendation.ts    |   77 -
 src/server/api/endpoints/notes/polls/vote.ts       |  170 -
 src/server/api/endpoints/notes/reactions.ts        |   90 -
 src/server/api/endpoints/notes/reactions/create.ts |   57 -
 src/server/api/endpoints/notes/reactions/delete.ts |   52 -
 src/server/api/endpoints/notes/renotes.ts          |   76 -
 src/server/api/endpoints/notes/replies.ts          |   61 -
 src/server/api/endpoints/notes/search-by-tag.ts    |  134 -
 src/server/api/endpoints/notes/search.ts           |  152 -
 src/server/api/endpoints/notes/show.ts             |   43 -
 src/server/api/endpoints/notes/state.ts            |   69 -
 .../api/endpoints/notes/thread-muting/create.ts    |   54 -
 .../api/endpoints/notes/thread-muting/delete.ts    |   40 -
 src/server/api/endpoints/notes/timeline.ts         |  150 -
 src/server/api/endpoints/notes/translate.ts        |   89 -
 src/server/api/endpoints/notes/unrenote.ts         |   52 -
 .../api/endpoints/notes/user-list-timeline.ts      |  147 -
 src/server/api/endpoints/notes/watching/create.ts  |   37 -
 src/server/api/endpoints/notes/watching/delete.ts  |   37 -
 src/server/api/endpoints/notifications/create.ts   |   37 -
 .../endpoints/notifications/mark-all-as-read.ts    |   24 -
 src/server/api/endpoints/notifications/read.ts     |   42 -
 src/server/api/endpoints/page-push.ts              |   50 -
 src/server/api/endpoints/pages/create.ts           |  128 -
 src/server/api/endpoints/pages/delete.ts           |   45 -
 src/server/api/endpoints/pages/featured.ts         |   29 -
 src/server/api/endpoints/pages/like.ts             |   71 -
 src/server/api/endpoints/pages/show.ts             |   65 -
 src/server/api/endpoints/pages/unlike.ts           |   54 -
 src/server/api/endpoints/pages/update.ts           |  141 -
 src/server/api/endpoints/ping.ts                   |   27 -
 src/server/api/endpoints/pinned-users.ts           |   32 -
 src/server/api/endpoints/promo/read.ts             |   50 -
 src/server/api/endpoints/request-reset-password.ts |   73 -
 src/server/api/endpoints/reset-db.ts               |   23 -
 src/server/api/endpoints/reset-password.ts         |   45 -
 src/server/api/endpoints/room/show.ts              |  159 -
 src/server/api/endpoints/room/update.ts            |   51 -
 src/server/api/endpoints/server-info.ts            |   35 -
 src/server/api/endpoints/stats.ts                  |   83 -
 src/server/api/endpoints/sw/register.ts            |   74 -
 src/server/api/endpoints/sw/unregister.ts          |   22 -
 src/server/api/endpoints/username/available.ts     |   40 -
 src/server/api/endpoints/users.ts                  |  101 -
 src/server/api/endpoints/users/clips.ts            |   40 -
 src/server/api/endpoints/users/followers.ts        |  104 -
 src/server/api/endpoints/users/following.ts        |  104 -
 src/server/api/endpoints/users/gallery/posts.ts    |   39 -
 .../users/get-frequently-replied-users.ts          |  105 -
 src/server/api/endpoints/users/groups/create.ts    |   45 -
 src/server/api/endpoints/users/groups/delete.ts    |   40 -
 .../endpoints/users/groups/invitations/accept.ts   |   54 -
 .../endpoints/users/groups/invitations/reject.ts   |   44 -
 src/server/api/endpoints/users/groups/invite.ts    |  102 -
 src/server/api/endpoints/users/groups/joined.ts    |   36 -
 src/server/api/endpoints/users/groups/leave.ts     |   50 -
 src/server/api/endpoints/users/groups/owned.ts     |   28 -
 src/server/api/endpoints/users/groups/pull.ts      |   69 -
 src/server/api/endpoints/users/groups/show.ts      |   55 -
 src/server/api/endpoints/users/groups/transfer.ts  |   83 -
 src/server/api/endpoints/users/groups/update.ts    |   55 -
 src/server/api/endpoints/users/lists/create.ts     |   36 -
 src/server/api/endpoints/users/lists/delete.ts     |   40 -
 src/server/api/endpoints/users/lists/list.ts       |   28 -
 src/server/api/endpoints/users/lists/pull.ts       |   62 -
 src/server/api/endpoints/users/lists/push.ts       |   92 -
 src/server/api/endpoints/users/lists/show.ts       |   47 -
 src/server/api/endpoints/users/lists/update.ts     |   55 -
 src/server/api/endpoints/users/notes.ts            |  144 -
 src/server/api/endpoints/users/pages.ts            |   40 -
 src/server/api/endpoints/users/reactions.ts        |   79 -
 src/server/api/endpoints/users/recommendation.ts   |   63 -
 src/server/api/endpoints/users/relation.ts         |  111 -
 src/server/api/endpoints/users/report-abuse.ts     |   90 -
 .../endpoints/users/search-by-username-and-host.ts |  116 -
 src/server/api/endpoints/users/search.ts           |  127 -
 src/server/api/endpoints/users/show.ts             |  105 -
 src/server/api/endpoints/users/stats.ts            |  144 -
 src/server/api/error.ts                            |   28 -
 src/server/api/index.ts                            |  113 -
 src/server/api/limiter.ts                          |   83 -
 src/server/api/logger.ts                           |    3 -
 src/server/api/openapi/description.ts              |   51 -
 src/server/api/openapi/errors.ts                   |   69 -
 src/server/api/openapi/gen-spec.ts                 |  237 -
 src/server/api/openapi/schemas.ts                  |   56 -
 src/server/api/private/signin.ts                   |  232 -
 src/server/api/private/signup-pending.ts           |   35 -
 src/server/api/private/signup.ts                   |  112 -
 src/server/api/service/discord.ts                  |  286 -
 src/server/api/service/github.ts                   |  257 -
 src/server/api/service/twitter.ts                  |  194 -
 src/server/api/stream/channel.ts                   |   62 -
 src/server/api/stream/channels/admin.ts            |   16 -
 src/server/api/stream/channels/antenna.ts          |   45 -
 src/server/api/stream/channels/channel.ts          |   92 -
 src/server/api/stream/channels/drive.ts            |   16 -
 .../api/stream/channels/games/reversi-game.ts      |  372 -
 src/server/api/stream/channels/games/reversi.ts    |   33 -
 src/server/api/stream/channels/global-timeline.ts  |   73 -
 src/server/api/stream/channels/hashtag.ts          |   53 -
 src/server/api/stream/channels/home-timeline.ts    |   81 -
 src/server/api/stream/channels/hybrid-timeline.ts  |   89 -
 src/server/api/stream/channels/index.ts            |   37 -
 src/server/api/stream/channels/local-timeline.ts   |   74 -
 src/server/api/stream/channels/main.ts             |   43 -
 src/server/api/stream/channels/messaging-index.ts  |   16 -
 src/server/api/stream/channels/messaging.ts        |  106 -
 src/server/api/stream/channels/queue-stats.ts      |   41 -
 src/server/api/stream/channels/server-stats.ts     |   41 -
 src/server/api/stream/channels/user-list.ts        |   92 -
 src/server/api/stream/index.ts                     |  421 -
 src/server/api/stream/types.ts                     |  299 -
 src/server/api/streaming.ts                        |   67 -
 src/server/file/assets/bad-egg.png                 |  Bin 1676 -> 0 bytes
 src/server/file/assets/cache-expired.png           |  Bin 6048 -> 0 bytes
 src/server/file/assets/dummy.png                   |  Bin 6285 -> 0 bytes
 src/server/file/assets/not-an-image.png            |  Bin 2780 -> 0 bytes
 src/server/file/assets/thumbnail-not-available.png |  Bin 5705 -> 0 bytes
 src/server/file/assets/tombstone.png               |  Bin 5028 -> 0 bytes
 src/server/file/index.ts                           |   41 -
 src/server/file/send-drive-file.ts                 |  126 -
 src/server/index.ts                                |  179 -
 src/server/nodeinfo.ts                             |  101 -
 src/server/proxy/index.ts                          |   26 -
 src/server/proxy/proxy-media.ts                    |   51 -
 src/server/web/bios.css                            |   40 -
 src/server/web/bios.js                             |   87 -
 src/server/web/boot.js                             |  166 -
 src/server/web/cli.css                             |   19 -
 src/server/web/cli.js                              |   55 -
 src/server/web/feed.ts                             |   58 -
 src/server/web/index.ts                            |  419 -
 src/server/web/manifest.json                       |   28 -
 src/server/web/manifest.ts                         |   15 -
 src/server/web/style.css                           |   29 -
 src/server/web/url-preview.ts                      |   53 -
 src/server/web/views/base.pug                      |   60 -
 src/server/web/views/bios.pug                      |   20 -
 src/server/web/views/channel.pug                   |   21 -
 src/server/web/views/cli.pug                       |   21 -
 src/server/web/views/clip.pug                      |   33 -
 src/server/web/views/flush.pug                     |   47 -
 src/server/web/views/gallery-post.pug              |   35 -
 src/server/web/views/info-card.pug                 |   50 -
 src/server/web/views/note.pug                      |   43 -
 src/server/web/views/page.pug                      |   33 -
 src/server/web/views/user.pug                      |   42 -
 src/server/well-known.ts                           |  149 -
 src/services/add-note-to-antenna.ts                |   54 -
 src/services/blocking/create.ts                    |  129 -
 src/services/blocking/delete.ts                    |   29 -
 src/services/chart/charts/classes/active-users.ts  |   47 -
 src/services/chart/charts/classes/drive.ts         |   91 -
 src/services/chart/charts/classes/federation.ts    |   62 -
 src/services/chart/charts/classes/hashtag.ts       |   47 -
 src/services/chart/charts/classes/instance.ts      |  217 -
 src/services/chart/charts/classes/network.ts       |   45 -
 src/services/chart/charts/classes/notes.ts         |   97 -
 .../chart/charts/classes/per-user-drive.ts         |   64 -
 .../chart/charts/classes/per-user-following.ts     |  121 -
 .../chart/charts/classes/per-user-notes.ts         |   72 -
 .../chart/charts/classes/per-user-reactions.ts     |   44 -
 src/services/chart/charts/classes/test-grouped.ts  |   58 -
 src/services/chart/charts/classes/test-unique.ts   |   36 -
 src/services/chart/charts/classes/test.ts          |   69 -
 src/services/chart/charts/classes/users.ts         |   76 -
 src/services/chart/charts/schemas/active-users.ts  |   35 -
 src/services/chart/charts/schemas/drive.ts         |   68 -
 src/services/chart/charts/schemas/federation.ts    |   29 -
 src/services/chart/charts/schemas/hashtag.ts       |   35 -
 src/services/chart/charts/schemas/instance.ts      |  157 -
 src/services/chart/charts/schemas/network.ts       |   31 -
 src/services/chart/charts/schemas/notes.ts         |   56 -
 .../chart/charts/schemas/per-user-drive.ts         |   55 -
 .../chart/charts/schemas/per-user-following.ts     |   86 -
 .../chart/charts/schemas/per-user-notes.ts         |   43 -
 .../chart/charts/schemas/per-user-reactions.ts     |   31 -
 src/services/chart/charts/schemas/test-grouped.ts  |   28 -
 src/services/chart/charts/schemas/test-unique.ts   |   16 -
 src/services/chart/charts/schemas/test.ts          |   28 -
 src/services/chart/charts/schemas/users.ts         |   44 -
 src/services/chart/core.ts                         |  563 --
 src/services/chart/entities.ts                     |   15 -
 src/services/chart/index.ts                        |   50 -
 src/services/create-notification.ts                |   61 -
 src/services/create-system-user.ts                 |   67 -
 src/services/drive/add-file.ts                     |  466 --
 src/services/drive/delete-file.ts                  |  103 -
 src/services/drive/generate-video-thumbnail.ts     |   37 -
 src/services/drive/image-processor.ts              |  107 -
 src/services/drive/internal-storage.ts             |   35 -
 src/services/drive/logger.ts                       |    3 -
 src/services/drive/s3.ts                           |   24 -
 src/services/drive/upload-from-url.ts              |   62 -
 src/services/fetch-instance-metadata.ts            |  265 -
 src/services/following/create.ts                   |  180 -
 src/services/following/delete.ts                   |   69 -
 src/services/following/requests/accept-all.ts      |   18 -
 src/services/following/requests/accept.ts          |   31 -
 src/services/following/requests/cancel.ts          |   36 -
 src/services/following/requests/create.ts          |   63 -
 src/services/following/requests/reject.ts          |   46 -
 src/services/i/pin.ts                              |   92 -
 src/services/i/update.ts                           |   19 -
 src/services/insert-moderation-log.ts              |   13 -
 src/services/instance-actor.ts                     |   27 -
 src/services/logger.ts                             |  127 -
 src/services/messages/create.ts                    |  108 -
 src/services/messages/delete.ts                    |   30 -
 src/services/note/create.ts                        |  646 --
 src/services/note/delete.ts                        |  137 -
 src/services/note/polls/update.ts                  |   22 -
 src/services/note/polls/vote.ts                    |   81 -
 src/services/note/reaction/create.ts               |  135 -
 src/services/note/reaction/delete.ts               |   58 -
 src/services/note/read.ts                          |  132 -
 src/services/note/unread.ts                        |   55 -
 src/services/note/unwatch.ts                       |   10 -
 src/services/note/watch.ts                         |   20 -
 src/services/push-notification.ts                  |   53 -
 src/services/register-or-fetch-instance-doc.ts     |   34 -
 src/services/relay.ts                              |   94 -
 src/services/send-email-notification.ts            |   31 -
 src/services/send-email.ts                         |  123 -
 src/services/stream.ts                             |  129 -
 src/services/suspend-user.ts                       |   34 -
 src/services/unsuspend-user.ts                     |   35 -
 src/services/update-hashtag.ts                     |  128 -
 src/services/user-list/push.ts                     |   27 -
 src/services/validate-email-for-account.ts         |   34 -
 src/tools/accept-migration.ts                      |   25 -
 src/tools/add-emoji.ts                             |   30 -
 src/tools/demote-admin.ts                          |   32 -
 src/tools/mark-admin.ts                            |   32 -
 src/tools/refresh-question.ts                      |   14 -
 src/tools/resync-remote-user.ts                    |   30 -
 src/tools/show-signin-history.ts                   |   56 -
 src/tsconfig.json                                  |   46 -
 src/types.ts                                       |    7 -
 src/well-known-services.ts                         |    4 -
 vetur.config.js                                    |   41 -
 webpack.config.ts                                  |  193 -
 yarn.lock                                          | 8092 +------------------
 3279 files changed, 150760 insertions(+), 141708 deletions(-)
 delete mode 100644 .imgbotconfig
 delete mode 100644 .mocharc.json
 delete mode 100644 assets/api-doc.png
 delete mode 100644 assets/apple-touch-icon.png
 delete mode 100644 assets/client/about-icon.png
 delete mode 100644 assets/client/fedi.jpg
 delete mode 100644 assets/client/label-red.svg
 delete mode 100644 assets/client/label.svg
 delete mode 100644 assets/client/misskey.svg
 delete mode 100644 assets/client/remove.png
 delete mode 100644 assets/client/room/furnitures/banknote/banknote.blend
 delete mode 100644 assets/client/room/furnitures/banknote/banknote.glb
 delete mode 100644 assets/client/room/furnitures/banknote/tex.png
 delete mode 100644 assets/client/room/furnitures/bed/bed.blend
 delete mode 100644 assets/client/room/furnitures/bed/bed.glb
 delete mode 100644 assets/client/room/furnitures/bin/bin.blend
 delete mode 100644 assets/client/room/furnitures/bin/bin.glb
 delete mode 100644 assets/client/room/furnitures/book/book.blend
 delete mode 100644 assets/client/room/furnitures/book/book.glb
 delete mode 100644 assets/client/room/furnitures/book2/barcode.png
 delete mode 100644 assets/client/room/furnitures/book2/book2.blend
 delete mode 100644 assets/client/room/furnitures/book2/book2.glb
 delete mode 100644 assets/client/room/furnitures/book2/texture.afdesign
 delete mode 100644 assets/client/room/furnitures/book2/texture.png
 delete mode 100644 assets/client/room/furnitures/book2/uv.png
 delete mode 100644 assets/client/room/furnitures/cardboard-box/cardboard-box.blend
 delete mode 100644 assets/client/room/furnitures/cardboard-box/cardboard-box.glb
 delete mode 100644 assets/client/room/furnitures/cardboard-box2/cardboard-box2.blend
 delete mode 100644 assets/client/room/furnitures/cardboard-box2/cardboard-box2.glb
 delete mode 100644 assets/client/room/furnitures/cardboard-box2/texture.png
 delete mode 100644 assets/client/room/furnitures/cardboard-box2/uv.png
 delete mode 100644 assets/client/room/furnitures/cardboard-box3/cardboard-box3.blend
 delete mode 100644 assets/client/room/furnitures/cardboard-box3/cardboard-box3.glb
 delete mode 100644 assets/client/room/furnitures/cardboard-box3/texture.png
 delete mode 100644 assets/client/room/furnitures/cardboard-box3/texture.xcf
 delete mode 100644 assets/client/room/furnitures/cardboard-box3/uv.png
 delete mode 100644 assets/client/room/furnitures/carpet-stripe/carpet-stripe.blend
 delete mode 100644 assets/client/room/furnitures/carpet-stripe/carpet-stripe.glb
 delete mode 100644 assets/client/room/furnitures/chair/chair.blend
 delete mode 100644 assets/client/room/furnitures/chair/chair.glb
 delete mode 100644 assets/client/room/furnitures/chair2/chair2.blend
 delete mode 100644 assets/client/room/furnitures/chair2/chair2.glb
 delete mode 100644 assets/client/room/furnitures/color-box/color-box.blend
 delete mode 100644 assets/client/room/furnitures/color-box/color-box.glb
 delete mode 100644 assets/client/room/furnitures/corkboard/corkboard.blend
 delete mode 100644 assets/client/room/furnitures/corkboard/corkboard.glb
 delete mode 100644 assets/client/room/furnitures/cube/cube.blend
 delete mode 100644 assets/client/room/furnitures/cube/cube.glb
 delete mode 100644 assets/client/room/furnitures/cup-noodle/cup-noodle.blend
 delete mode 100644 assets/client/room/furnitures/cup-noodle/cup-noodle.glb
 delete mode 100644 assets/client/room/furnitures/cup-noodle/noodle.png
 delete mode 100644 assets/client/room/furnitures/desk/desk.blend
 delete mode 100644 assets/client/room/furnitures/desk/desk.glb
 delete mode 100644 assets/client/room/furnitures/doll-ai/doll-ai.blend
 delete mode 100644 assets/client/room/furnitures/doll-ai/doll-ai.glb
 delete mode 100644 assets/client/room/furnitures/doll-ai/doll_ai_tex.png
 delete mode 100644 assets/client/room/furnitures/energy-drink/energy-drink.blend
 delete mode 100644 assets/client/room/furnitures/energy-drink/energy-drink.glb
 delete mode 100644 assets/client/room/furnitures/energy-drink/texture.afdesign
 delete mode 100644 assets/client/room/furnitures/energy-drink/texture.png
 delete mode 100644 assets/client/room/furnitures/energy-drink/uv.png
 delete mode 100644 assets/client/room/furnitures/eraser/cover.png
 delete mode 100644 assets/client/room/furnitures/eraser/cover.psd
 delete mode 100644 assets/client/room/furnitures/eraser/eraser-uv.png
 delete mode 100644 assets/client/room/furnitures/eraser/eraser.blend
 delete mode 100644 assets/client/room/furnitures/eraser/eraser.glb
 delete mode 100644 assets/client/room/furnitures/facial-tissue/facial-tissue-uv.png
 delete mode 100644 assets/client/room/furnitures/facial-tissue/facial-tissue.blend
 delete mode 100644 assets/client/room/furnitures/facial-tissue/facial-tissue.glb
 delete mode 100644 assets/client/room/furnitures/facial-tissue/facial-tissue.png
 delete mode 100644 assets/client/room/furnitures/facial-tissue/facial-tissue.psd
 delete mode 100644 assets/client/room/furnitures/fan/fan.blend
 delete mode 100644 assets/client/room/furnitures/fan/fan.glb
 delete mode 100644 assets/client/room/furnitures/holo-display/holo-display.blend
 delete mode 100644 assets/client/room/furnitures/holo-display/holo-display.glb
 delete mode 100644 assets/client/room/furnitures/holo-display/ray-uv.png
 delete mode 100644 assets/client/room/furnitures/holo-display/ray.png
 delete mode 100644 assets/client/room/furnitures/keyboard/keyboard.blend
 delete mode 100644 assets/client/room/furnitures/keyboard/keyboard.glb
 delete mode 100644 assets/client/room/furnitures/low-table/low-table.blend
 delete mode 100644 assets/client/room/furnitures/low-table/low-table.glb
 delete mode 100644 assets/client/room/furnitures/mat/mat.blend
 delete mode 100644 assets/client/room/furnitures/mat/mat.glb
 delete mode 100644 assets/client/room/furnitures/milk/milk-uv.png
 delete mode 100644 assets/client/room/furnitures/milk/milk.blend
 delete mode 100644 assets/client/room/furnitures/milk/milk.glb
 delete mode 100644 assets/client/room/furnitures/milk/milk.png
 delete mode 100644 assets/client/room/furnitures/milk/milk.psd
 delete mode 100644 assets/client/room/furnitures/monitor/monitor.blend
 delete mode 100644 assets/client/room/furnitures/monitor/monitor.glb
 delete mode 100644 assets/client/room/furnitures/monitor/monitor.psd
 delete mode 100644 assets/client/room/furnitures/monitor/screen-uv.png
 delete mode 100644 assets/client/room/furnitures/monitor/screen.jpg
 delete mode 100644 assets/client/room/furnitures/moon/moon.blend
 delete mode 100644 assets/client/room/furnitures/moon/moon.glb
 delete mode 100644 assets/client/room/furnitures/moon/moon.jpg
 delete mode 100644 assets/client/room/furnitures/mousepad/mousepad.blend
 delete mode 100644 assets/client/room/furnitures/mousepad/mousepad.glb
 delete mode 100644 assets/client/room/furnitures/pc/motherboard-uv.png
 delete mode 100644 assets/client/room/furnitures/pc/motherboard-uv.psd
 delete mode 100644 assets/client/room/furnitures/pc/motherboard.jpg
 delete mode 100644 assets/client/room/furnitures/pc/pc.blend
 delete mode 100644 assets/client/room/furnitures/pc/pc.glb
 delete mode 100644 assets/client/room/furnitures/pencil/pencil.blend
 delete mode 100644 assets/client/room/furnitures/pencil/pencil.glb
 delete mode 100644 assets/client/room/furnitures/photoframe/photo-uv.png
 delete mode 100644 assets/client/room/furnitures/photoframe/photo.jpg
 delete mode 100644 assets/client/room/furnitures/photoframe/photoframe.blend
 delete mode 100644 assets/client/room/furnitures/photoframe/photoframe.glb
 delete mode 100644 assets/client/room/furnitures/piano/piano.blend
 delete mode 100644 assets/client/room/furnitures/piano/piano.glb
 delete mode 100644 assets/client/room/furnitures/pinguin/pinguin.blend
 delete mode 100644 assets/client/room/furnitures/pinguin/pinguin.glb
 delete mode 100644 assets/client/room/furnitures/plant/plant-soil-uv.png
 delete mode 100644 assets/client/room/furnitures/plant/plant-soil.png
 delete mode 100644 assets/client/room/furnitures/plant/plant-soil.psd
 delete mode 100644 assets/client/room/furnitures/plant/plant.blend
 delete mode 100644 assets/client/room/furnitures/plant/plant.glb
 delete mode 100644 assets/client/room/furnitures/plant2/plant2.blend
 delete mode 100644 assets/client/room/furnitures/plant2/plant2.glb
 delete mode 100644 assets/client/room/furnitures/plant2/soil.png
 delete mode 100644 assets/client/room/furnitures/poster-h/poster-h.blend
 delete mode 100644 assets/client/room/furnitures/poster-h/poster-h.glb
 delete mode 100644 assets/client/room/furnitures/poster-h/uv.png
 delete mode 100644 assets/client/room/furnitures/poster-v/poster-v.blend
 delete mode 100644 assets/client/room/furnitures/poster-v/poster-v.glb
 delete mode 100644 assets/client/room/furnitures/poster-v/uv.png
 delete mode 100644 assets/client/room/furnitures/pudding/pudding.blend
 delete mode 100644 assets/client/room/furnitures/pudding/pudding.glb
 delete mode 100644 assets/client/room/furnitures/rubik-cube/rubik-cube.blend
 delete mode 100644 assets/client/room/furnitures/rubik-cube/rubik-cube.glb
 delete mode 100644 assets/client/room/furnitures/server/rack-uv.png
 delete mode 100644 assets/client/room/furnitures/server/rack.png
 delete mode 100644 assets/client/room/furnitures/server/server.blend
 delete mode 100644 assets/client/room/furnitures/server/server.glb
 delete mode 100644 assets/client/room/furnitures/server/server.png
 delete mode 100644 assets/client/room/furnitures/server/uv.png
 delete mode 100644 assets/client/room/furnitures/sofa/sofa.blend
 delete mode 100644 assets/client/room/furnitures/sofa/sofa.glb
 delete mode 100644 assets/client/room/furnitures/spiral/spiral.blend
 delete mode 100644 assets/client/room/furnitures/spiral/spiral.glb
 delete mode 100644 assets/client/room/furnitures/tv/screen-uv.png
 delete mode 100644 assets/client/room/furnitures/tv/tv.blend
 delete mode 100644 assets/client/room/furnitures/tv/tv.glb
 delete mode 100644 assets/client/room/furnitures/wall-clock/wall-clock.blend
 delete mode 100644 assets/client/room/furnitures/wall-clock/wall-clock.glb
 delete mode 100644 assets/client/room/rooms/default/default.blend
 delete mode 100644 assets/client/room/rooms/default/default.glb
 delete mode 100644 assets/client/room/rooms/washitsu/husuma-uv.png
 delete mode 100644 assets/client/room/rooms/washitsu/husuma.png
 delete mode 100644 assets/client/room/rooms/washitsu/tatami-single1600.png
 delete mode 100644 assets/client/room/rooms/washitsu/tatami-uv.png
 delete mode 100644 assets/client/room/rooms/washitsu/tatami.afdesign
 delete mode 100644 assets/client/room/rooms/washitsu/tatami.png
 delete mode 100644 assets/client/room/rooms/washitsu/washitsu.blend
 delete mode 100644 assets/client/room/rooms/washitsu/washitsu.glb
 delete mode 100644 assets/client/sounds/aisha/1.mp3
 delete mode 100644 assets/client/sounds/aisha/2.mp3
 delete mode 100644 assets/client/sounds/aisha/3.mp3
 delete mode 100644 assets/client/sounds/noizenecio/kick_gaba.mp3
 delete mode 100644 assets/client/sounds/noizenecio/kick_gaba2.mp3
 delete mode 100644 assets/client/sounds/syuilo/down.mp3
 delete mode 100644 assets/client/sounds/syuilo/kick.mp3
 delete mode 100644 assets/client/sounds/syuilo/pirori-square-wet.mp3
 delete mode 100644 assets/client/sounds/syuilo/pirori-wet.mp3
 delete mode 100644 assets/client/sounds/syuilo/pirori.mp3
 delete mode 100644 assets/client/sounds/syuilo/poi1.mp3
 delete mode 100644 assets/client/sounds/syuilo/poi2.mp3
 delete mode 100644 assets/client/sounds/syuilo/pope1.mp3
 delete mode 100644 assets/client/sounds/syuilo/pope2.mp3
 delete mode 100644 assets/client/sounds/syuilo/popo.mp3
 delete mode 100644 assets/client/sounds/syuilo/queue-jammed.mp3
 delete mode 100644 assets/client/sounds/syuilo/reverved.mp3
 delete mode 100644 assets/client/sounds/syuilo/ryukyu.mp3
 delete mode 100644 assets/client/sounds/syuilo/snare.mp3
 delete mode 100644 assets/client/sounds/syuilo/square-pico.mp3
 delete mode 100644 assets/client/sounds/syuilo/triple.mp3
 delete mode 100644 assets/client/sounds/syuilo/up.mp3
 delete mode 100644 assets/client/sounds/syuilo/waon.mp3
 delete mode 100644 assets/client/sparkle-spritesheet.png
 delete mode 100644 assets/client/unread.svg
 delete mode 100644 assets/favicon.ico
 delete mode 100644 assets/favicon.png
 delete mode 100644 assets/icons/192.png
 delete mode 100644 assets/icons/512.png
 delete mode 100644 assets/mi-white.png
 delete mode 100644 assets/redoc.html
 delete mode 100644 assets/robots.txt
 delete mode 100644 assets/user-unknown.png
 create mode 100644 gulpfile.js
 delete mode 100644 gulpfile.ts
 delete mode 100644 index.js
 delete mode 100644 migration/1000000000000-Init.ts
 delete mode 100644 migration/1556348509290-Pages.ts
 delete mode 100644 migration/1556746559567-UserProfile.ts
 delete mode 100644 migration/1557476068003-PinnedUsers.ts
 delete mode 100644 migration/1557761316509-AddSomeUrls.ts
 delete mode 100644 migration/1557932705754-ObjectStorageSetting.ts
 delete mode 100644 migration/1558072954435-PageLike.ts
 delete mode 100644 migration/1558103093633-UserGroup.ts
 delete mode 100644 migration/1558257926829-UserGroupInvite.ts
 delete mode 100644 migration/1558266512381-UserListJoining.ts
 delete mode 100644 migration/1561706992953-webauthn.ts
 delete mode 100644 migration/1561873850023-ChartIndexes.ts
 delete mode 100644 migration/1562422242907-PasswordLessLogin.ts
 delete mode 100644 migration/1562444565093-PinnedPage.ts
 delete mode 100644 migration/1562448332510-PageTitleHideOption.ts
 delete mode 100644 migration/1562869971568-ModerationLog.ts
 delete mode 100644 migration/1563757595828-UsedUsername.ts
 delete mode 100644 migration/1565634203341-room.ts
 delete mode 100644 migration/1571220798684-CustomEmojiCategory.ts
 delete mode 100644 migration/1572760203493-nodeinfo.ts
 delete mode 100644 migration/1576269851876-TalkFederationId.ts
 delete mode 100644 migration/1576869585998-ProxyRemoteFiles.ts
 delete mode 100644 migration/1579267006611-v12.ts
 delete mode 100644 migration/1579270193251-v12-2.ts
 delete mode 100644 migration/1579282808087-v12-3.ts
 delete mode 100644 migration/1579544426412-v12-4.ts
 delete mode 100644 migration/1579977526288-v12-5.ts
 delete mode 100644 migration/1579993013959-v12-6.ts
 delete mode 100644 migration/1580069531114-v12-7.ts
 delete mode 100644 migration/1580148575182-v12-8.ts
 delete mode 100644 migration/1580154400017-v12-9.ts
 delete mode 100644 migration/1580276619901-v12-10.ts
 delete mode 100644 migration/1580331224276-v12-11.ts
 delete mode 100644 migration/1580508795118-v12-12.ts
 delete mode 100644 migration/1580543501339-v12-13.ts
 delete mode 100644 migration/1580864313253-v12-14.ts
 delete mode 100644 migration/1581526429287-user-group-invitation.ts
 delete mode 100644 migration/1581695816408-user-group-antenna.ts
 delete mode 100644 migration/1581708415836-drive-user-folder-id-index.ts
 delete mode 100644 migration/1581979837262-promo.ts
 delete mode 100644 migration/1582019042083-featured-injecttion.ts
 delete mode 100644 migration/1582210532752-antenna-exclude.ts
 delete mode 100644 migration/1582875306439-note-reaction-length.ts
 delete mode 100644 migration/1585361548360-miauth.ts
 delete mode 100644 migration/1585385921215-custom-notification.ts
 delete mode 100644 migration/1585772678853-ap-url.ts
 delete mode 100644 migration/1586624197029-AddObjectStorageUseProxy.ts
 delete mode 100644 migration/1586641139527-remote-reaction.ts
 delete mode 100644 migration/1586708940386-pageAiScript.ts
 delete mode 100644 migration/1588044505511-hCaptcha.ts
 delete mode 100644 migration/1589023282116-pubRelay.ts
 delete mode 100644 migration/1595075960584-blurhash.ts
 delete mode 100644 migration/1595077605646-blurhash-for-avatar-banner.ts
 delete mode 100644 migration/1595676934834-instance-icon-url.ts
 delete mode 100644 migration/1595771249699-word-mute.ts
 delete mode 100644 migration/1595782306083-word-mute2.ts
 delete mode 100644 migration/1596548170836-channel.ts
 delete mode 100644 migration/1596786425167-channel2.ts
 delete mode 100644 migration/1597230137744-objectStorageSetPublicRead.ts
 delete mode 100644 migration/1597236229720-IncludingNotificationTypes.ts
 delete mode 100644 migration/1597385880794-add-sensitive-index.ts
 delete mode 100644 migration/1597459042300-channel-unread.ts
 delete mode 100644 migration/1597893996136-ChannelNoteIdDescIndex.ts
 delete mode 100644 migration/1600353287890-mutingNotificationTypes.ts
 delete mode 100644 migration/1603094348345-refine-abuse-user-report.ts
 delete mode 100644 migration/1603095701770-refine-abuse-user-report2.ts
 delete mode 100644 migration/1603776877564-instance-theme-color.ts
 delete mode 100644 migration/1603781553011-instance-favicon.ts
 delete mode 100644 migration/1604821689616-delete-auto-watch.ts
 delete mode 100644 migration/1605408848373-clip-description.ts
 delete mode 100644 migration/1605408971051-comments.ts
 delete mode 100644 migration/1605585339718-instance-pinned-pages.ts
 delete mode 100644 migration/1605965516823-instance-images.ts
 delete mode 100644 migration/1606191203881-no-crawle.ts
 delete mode 100644 migration/1607151207216-instance-pinned-clip.ts
 delete mode 100644 migration/1607353487793-isExplorable.ts
 delete mode 100644 migration/1610277136869-registry.ts
 delete mode 100644 migration/1610277585759-registry2.ts
 delete mode 100644 migration/1610283021566-registry3.ts
 delete mode 100644 migration/1611354329133-followersUri.ts
 delete mode 100644 migration/1611397665007-gallery.ts
 delete mode 100644 migration/1611547387175-objectStorageS3ForcePathStyle.ts
 delete mode 100644 migration/1612619156584-announcement-email.ts
 delete mode 100644 migration/1613155914446-emailNotificationTypes.ts
 delete mode 100644 migration/1613181457597-user-lang.ts
 delete mode 100644 migration/1613503367223-use-bigint-for-driveUsage.ts
 delete mode 100644 migration/1615965918224-chart-v2.ts
 delete mode 100644 migration/1615966519402-chart-v2-2.ts
 delete mode 100644 migration/1618637372000-user-last-active-date.ts
 delete mode 100644 migration/1618639857000-user-hide-online-status.ts
 delete mode 100644 migration/1619942102890-password-reset.ts
 delete mode 100644 migration/1620019354680-ad.ts
 delete mode 100644 migration/1620364649428-ad2.ts
 delete mode 100644 migration/1621479946000-add-note-indexes.ts
 delete mode 100644 migration/1622679304522-user-profile-description-length.ts
 delete mode 100644 migration/1622681548499-log-message-length.ts
 delete mode 100644 migration/1629004542760-chart-reindex.ts
 delete mode 100644 migration/1629024377804-deepl-integration.ts
 delete mode 100644 migration/1629288472000-fix-channel-userId.ts
 delete mode 100644 migration/1629512953000-user-is-deleted.ts
 delete mode 100644 migration/1629778475000-deepl-integration2.ts
 delete mode 100644 migration/1633068642000-email-required-for-signup.ts
 delete mode 100644 migration/1633071909016-user-pending.ts
 delete mode 100644 migration/1634486652000-user-public-reactions.ts
 delete mode 100644 migration/1634902659689-delete-log.ts
 delete mode 100644 migration/1635500777168-note-thread-mute.ts
 delete mode 100644 migration/1636197624383-ff-visibility.ts
 delete mode 100644 ormconfig.js
 create mode 100644 packages/backend/.eslintrc
 create mode 100644 packages/backend/.mocharc.json
 create mode 100644 packages/backend/.npmrc
 create mode 100644 packages/backend/@types/hcaptcha.d.ts
 create mode 100644 packages/backend/@types/http-signature.d.ts
 create mode 100644 packages/backend/@types/jsrsasign.d.ts
 create mode 100644 packages/backend/@types/koa-json-body.d.ts
 create mode 100644 packages/backend/@types/koa-slow.d.ts
 create mode 100644 packages/backend/@types/langmap.d.ts
 create mode 100644 packages/backend/@types/ms.d.ts
 create mode 100644 packages/backend/@types/os-utils.d.ts
 create mode 100644 packages/backend/@types/package.json.d.ts
 create mode 100644 packages/backend/@types/probe-image-size.d.ts
 create mode 100644 packages/backend/assets/api-doc.png
 create mode 100644 packages/backend/assets/apple-touch-icon.png
 create mode 100644 packages/backend/assets/favicon.ico
 create mode 100644 packages/backend/assets/favicon.png
 create mode 100644 packages/backend/assets/icons/192.png
 create mode 100644 packages/backend/assets/icons/512.png
 create mode 100644 packages/backend/assets/mi-white.png
 create mode 100644 packages/backend/assets/redoc.html
 create mode 100644 packages/backend/assets/robots.txt
 create mode 100644 packages/backend/assets/user-unknown.png
 create mode 100644 packages/backend/migration/1000000000000-Init.js
 create mode 100644 packages/backend/migration/1556348509290-Pages.js
 create mode 100644 packages/backend/migration/1556746559567-UserProfile.js
 create mode 100644 packages/backend/migration/1557476068003-PinnedUsers.js
 create mode 100644 packages/backend/migration/1557761316509-AddSomeUrls.js
 create mode 100644 packages/backend/migration/1557932705754-ObjectStorageSetting.js
 create mode 100644 packages/backend/migration/1558072954435-PageLike.js
 create mode 100644 packages/backend/migration/1558103093633-UserGroup.js
 create mode 100644 packages/backend/migration/1558257926829-UserGroupInvite.js
 create mode 100644 packages/backend/migration/1558266512381-UserListJoining.js
 create mode 100644 packages/backend/migration/1561706992953-webauthn.js
 create mode 100644 packages/backend/migration/1561873850023-ChartIndexes.js
 create mode 100644 packages/backend/migration/1562422242907-PasswordLessLogin.js
 create mode 100644 packages/backend/migration/1562444565093-PinnedPage.js
 create mode 100644 packages/backend/migration/1562448332510-PageTitleHideOption.js
 create mode 100644 packages/backend/migration/1562869971568-ModerationLog.js
 create mode 100644 packages/backend/migration/1563757595828-UsedUsername.js
 create mode 100644 packages/backend/migration/1565634203341-room.js
 create mode 100644 packages/backend/migration/1571220798684-CustomEmojiCategory.js
 create mode 100644 packages/backend/migration/1572760203493-nodeinfo.js
 create mode 100644 packages/backend/migration/1576269851876-TalkFederationId.js
 create mode 100644 packages/backend/migration/1576869585998-ProxyRemoteFiles.js
 create mode 100644 packages/backend/migration/1579267006611-v12.js
 create mode 100644 packages/backend/migration/1579270193251-v12-2.js
 create mode 100644 packages/backend/migration/1579282808087-v12-3.js
 create mode 100644 packages/backend/migration/1579544426412-v12-4.js
 create mode 100644 packages/backend/migration/1579977526288-v12-5.js
 create mode 100644 packages/backend/migration/1579993013959-v12-6.js
 create mode 100644 packages/backend/migration/1580069531114-v12-7.js
 create mode 100644 packages/backend/migration/1580148575182-v12-8.js
 create mode 100644 packages/backend/migration/1580154400017-v12-9.js
 create mode 100644 packages/backend/migration/1580276619901-v12-10.js
 create mode 100644 packages/backend/migration/1580331224276-v12-11.js
 create mode 100644 packages/backend/migration/1580508795118-v12-12.js
 create mode 100644 packages/backend/migration/1580543501339-v12-13.js
 create mode 100644 packages/backend/migration/1580864313253-v12-14.js
 create mode 100644 packages/backend/migration/1581526429287-user-group-invitation.js
 create mode 100644 packages/backend/migration/1581695816408-user-group-antenna.js
 create mode 100644 packages/backend/migration/1581708415836-drive-user-folder-id-index.js
 create mode 100644 packages/backend/migration/1581979837262-promo.js
 create mode 100644 packages/backend/migration/1582019042083-featured-injecttion.js
 create mode 100644 packages/backend/migration/1582210532752-antenna-exclude.js
 create mode 100644 packages/backend/migration/1582875306439-note-reaction-length.js
 create mode 100644 packages/backend/migration/1585361548360-miauth.js
 create mode 100644 packages/backend/migration/1585385921215-custom-notification.js
 create mode 100644 packages/backend/migration/1585772678853-ap-url.js
 create mode 100644 packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js
 create mode 100644 packages/backend/migration/1586641139527-remote-reaction.js
 create mode 100644 packages/backend/migration/1586708940386-pageAiScript.js
 create mode 100644 packages/backend/migration/1588044505511-hCaptcha.js
 create mode 100644 packages/backend/migration/1589023282116-pubRelay.js
 create mode 100644 packages/backend/migration/1595075960584-blurhash.js
 create mode 100644 packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js
 create mode 100644 packages/backend/migration/1595676934834-instance-icon-url.js
 create mode 100644 packages/backend/migration/1595771249699-word-mute.js
 create mode 100644 packages/backend/migration/1595782306083-word-mute2.js
 create mode 100644 packages/backend/migration/1596548170836-channel.js
 create mode 100644 packages/backend/migration/1596786425167-channel2.js
 create mode 100644 packages/backend/migration/1597230137744-objectStorageSetPublicRead.js
 create mode 100644 packages/backend/migration/1597236229720-IncludingNotificationTypes.js
 create mode 100644 packages/backend/migration/1597385880794-add-sensitive-index.js
 create mode 100644 packages/backend/migration/1597459042300-channel-unread.js
 create mode 100644 packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js
 create mode 100644 packages/backend/migration/1600353287890-mutingNotificationTypes.js
 create mode 100644 packages/backend/migration/1603094348345-refine-abuse-user-report.js
 create mode 100644 packages/backend/migration/1603095701770-refine-abuse-user-report2.js
 create mode 100644 packages/backend/migration/1603776877564-instance-theme-color.js
 create mode 100644 packages/backend/migration/1603781553011-instance-favicon.js
 create mode 100644 packages/backend/migration/1604821689616-delete-auto-watch.js
 create mode 100644 packages/backend/migration/1605408848373-clip-description.js
 create mode 100644 packages/backend/migration/1605408971051-comments.js
 create mode 100644 packages/backend/migration/1605585339718-instance-pinned-pages.js
 create mode 100644 packages/backend/migration/1605965516823-instance-images.js
 create mode 100644 packages/backend/migration/1606191203881-no-crawle.js
 create mode 100644 packages/backend/migration/1607151207216-instance-pinned-clip.js
 create mode 100644 packages/backend/migration/1607353487793-isExplorable.js
 create mode 100644 packages/backend/migration/1610277136869-registry.js
 create mode 100644 packages/backend/migration/1610277585759-registry2.js
 create mode 100644 packages/backend/migration/1610283021566-registry3.js
 create mode 100644 packages/backend/migration/1611354329133-followersUri.js
 create mode 100644 packages/backend/migration/1611397665007-gallery.js
 create mode 100644 packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js
 create mode 100644 packages/backend/migration/1612619156584-announcement-email.js
 create mode 100644 packages/backend/migration/1613155914446-emailNotificationTypes.js
 create mode 100644 packages/backend/migration/1613181457597-user-lang.js
 create mode 100644 packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js
 create mode 100644 packages/backend/migration/1615965918224-chart-v2.js
 create mode 100644 packages/backend/migration/1615966519402-chart-v2-2.js
 create mode 100644 packages/backend/migration/1618637372000-user-last-active-date.js
 create mode 100644 packages/backend/migration/1618639857000-user-hide-online-status.js
 create mode 100644 packages/backend/migration/1619942102890-password-reset.js
 create mode 100644 packages/backend/migration/1620019354680-ad.js
 create mode 100644 packages/backend/migration/1620364649428-ad2.js
 create mode 100644 packages/backend/migration/1621479946000-add-note-indexes.js
 create mode 100644 packages/backend/migration/1622679304522-user-profile-description-length.js
 create mode 100644 packages/backend/migration/1622681548499-log-message-length.js
 create mode 100644 packages/backend/migration/1629004542760-chart-reindex.js
 create mode 100644 packages/backend/migration/1629024377804-deepl-integration.js
 create mode 100644 packages/backend/migration/1629288472000-fix-channel-userId.js
 create mode 100644 packages/backend/migration/1629512953000-user-is-deleted.js
 create mode 100644 packages/backend/migration/1629778475000-deepl-integration2.js
 create mode 100644 packages/backend/migration/1633068642000-email-required-for-signup.js
 create mode 100644 packages/backend/migration/1633071909016-user-pending.js
 create mode 100644 packages/backend/migration/1634486652000-user-public-reactions.js
 create mode 100644 packages/backend/migration/1634902659689-delete-log.js
 create mode 100644 packages/backend/migration/1635500777168-note-thread-mute.js
 create mode 100644 packages/backend/migration/1636197624383-ff-visibility.js
 create mode 100644 packages/backend/ormconfig.js
 create mode 100644 packages/backend/package.json
 create mode 100644 packages/backend/src/boot/index.ts
 create mode 100644 packages/backend/src/boot/master.ts
 create mode 100644 packages/backend/src/boot/worker.ts
 create mode 100644 packages/backend/src/config/index.ts
 create mode 100644 packages/backend/src/config/load.ts
 create mode 100644 packages/backend/src/config/types.ts
 create mode 100644 packages/backend/src/const.ts
 create mode 100644 packages/backend/src/daemons/janitor.ts
 create mode 100644 packages/backend/src/daemons/queue-stats.ts
 create mode 100644 packages/backend/src/daemons/server-stats.ts
 create mode 100644 packages/backend/src/db/elasticsearch.ts
 create mode 100644 packages/backend/src/db/logger.ts
 create mode 100644 packages/backend/src/db/postgre.ts
 create mode 100644 packages/backend/src/db/redis.ts
 create mode 100644 packages/backend/src/env.ts
 create mode 100644 packages/backend/src/games/reversi/core.ts
 create mode 100644 packages/backend/src/games/reversi/maps.ts
 create mode 100644 packages/backend/src/games/reversi/package.json
 create mode 100644 packages/backend/src/games/reversi/tsconfig.json
 create mode 100644 packages/backend/src/global.d.ts
 create mode 100644 packages/backend/src/index.ts
 create mode 100644 packages/backend/src/mfm/from-html.ts
 create mode 100644 packages/backend/src/mfm/to-html.ts
 create mode 100644 packages/backend/src/misc/antenna-cache.ts
 create mode 100644 packages/backend/src/misc/api-permissions.ts
 create mode 100644 packages/backend/src/misc/app-lock.ts
 create mode 100644 packages/backend/src/misc/before-shutdown.ts
 create mode 100644 packages/backend/src/misc/cache.ts
 create mode 100644 packages/backend/src/misc/cafy-id.ts
 create mode 100644 packages/backend/src/misc/captcha.ts
 create mode 100644 packages/backend/src/misc/check-hit-antenna.ts
 create mode 100644 packages/backend/src/misc/check-word-mute.ts
 create mode 100644 packages/backend/src/misc/content-disposition.ts
 create mode 100644 packages/backend/src/misc/convert-host.ts
 create mode 100644 packages/backend/src/misc/count-same-renotes.ts
 create mode 100644 packages/backend/src/misc/create-temp.ts
 create mode 100644 packages/backend/src/misc/detect-url-mime.ts
 create mode 100644 packages/backend/src/misc/download-text-file.ts
 create mode 100644 packages/backend/src/misc/download-url.ts
 create mode 100644 packages/backend/src/misc/emoji-regex.ts
 create mode 100644 packages/backend/src/misc/extract-custom-emojis-from-mfm.ts
 create mode 100644 packages/backend/src/misc/extract-hashtags.ts
 create mode 100644 packages/backend/src/misc/extract-mentions.ts
 create mode 100644 packages/backend/src/misc/fetch-meta.ts
 create mode 100644 packages/backend/src/misc/fetch-proxy-account.ts
 create mode 100644 packages/backend/src/misc/fetch.ts
 create mode 100644 packages/backend/src/misc/gen-avatar.ts
 create mode 100644 packages/backend/src/misc/gen-id.ts
 create mode 100644 packages/backend/src/misc/gen-key-pair.ts
 create mode 100644 packages/backend/src/misc/get-file-info.ts
 create mode 100644 packages/backend/src/misc/get-note-summary.ts
 create mode 100644 packages/backend/src/misc/get-reaction-emoji.ts
 create mode 100644 packages/backend/src/misc/hard-limits.ts
 create mode 100644 packages/backend/src/misc/i18n.ts
 create mode 100644 packages/backend/src/misc/id/aid.ts
 create mode 100644 packages/backend/src/misc/id/meid.ts
 create mode 100644 packages/backend/src/misc/id/meidg.ts
 create mode 100644 packages/backend/src/misc/id/object-id.ts
 create mode 100644 packages/backend/src/misc/identifiable-error.ts
 create mode 100644 packages/backend/src/misc/is-blocker-user-related.ts
 create mode 100644 packages/backend/src/misc/is-duplicate-key-value-error.ts
 create mode 100644 packages/backend/src/misc/is-muted-user-related.ts
 create mode 100644 packages/backend/src/misc/is-quote.ts
 create mode 100644 packages/backend/src/misc/keypair-store.ts
 create mode 100644 packages/backend/src/misc/normalize-for-search.ts
 create mode 100644 packages/backend/src/misc/nyaize.ts
 create mode 100644 packages/backend/src/misc/populate-emojis.ts
 create mode 100644 packages/backend/src/misc/reaction-lib.ts
 create mode 100644 packages/backend/src/misc/safe-for-sql.ts
 create mode 100644 packages/backend/src/misc/schema.ts
 create mode 100644 packages/backend/src/misc/secure-rndstr.ts
 create mode 100644 packages/backend/src/misc/show-machine-info.ts
 create mode 100644 packages/backend/src/misc/simple-schema.ts
 create mode 100644 packages/backend/src/misc/truncate.ts
 create mode 100644 packages/backend/src/models/entities/abuse-user-report.ts
 create mode 100644 packages/backend/src/models/entities/access-token.ts
 create mode 100644 packages/backend/src/models/entities/ad.ts
 create mode 100644 packages/backend/src/models/entities/announcement-read.ts
 create mode 100644 packages/backend/src/models/entities/announcement.ts
 create mode 100644 packages/backend/src/models/entities/antenna-note.ts
 create mode 100644 packages/backend/src/models/entities/antenna.ts
 create mode 100644 packages/backend/src/models/entities/app.ts
 create mode 100644 packages/backend/src/models/entities/attestation-challenge.ts
 create mode 100644 packages/backend/src/models/entities/auth-session.ts
 create mode 100644 packages/backend/src/models/entities/blocking.ts
 create mode 100644 packages/backend/src/models/entities/channel-following.ts
 create mode 100644 packages/backend/src/models/entities/channel-note-pining.ts
 create mode 100644 packages/backend/src/models/entities/channel.ts
 create mode 100644 packages/backend/src/models/entities/clip-note.ts
 create mode 100644 packages/backend/src/models/entities/clip.ts
 create mode 100644 packages/backend/src/models/entities/drive-file.ts
 create mode 100644 packages/backend/src/models/entities/drive-folder.ts
 create mode 100644 packages/backend/src/models/entities/emoji.ts
 create mode 100644 packages/backend/src/models/entities/follow-request.ts
 create mode 100644 packages/backend/src/models/entities/following.ts
 create mode 100644 packages/backend/src/models/entities/gallery-like.ts
 create mode 100644 packages/backend/src/models/entities/gallery-post.ts
 create mode 100644 packages/backend/src/models/entities/games/reversi/game.ts
 create mode 100644 packages/backend/src/models/entities/games/reversi/matching.ts
 create mode 100644 packages/backend/src/models/entities/hashtag.ts
 create mode 100644 packages/backend/src/models/entities/instance.ts
 create mode 100644 packages/backend/src/models/entities/messaging-message.ts
 create mode 100644 packages/backend/src/models/entities/meta.ts
 create mode 100644 packages/backend/src/models/entities/moderation-log.ts
 create mode 100644 packages/backend/src/models/entities/muted-note.ts
 create mode 100644 packages/backend/src/models/entities/muting.ts
 create mode 100644 packages/backend/src/models/entities/note-favorite.ts
 create mode 100644 packages/backend/src/models/entities/note-reaction.ts
 create mode 100644 packages/backend/src/models/entities/note-thread-muting.ts
 create mode 100644 packages/backend/src/models/entities/note-unread.ts
 create mode 100644 packages/backend/src/models/entities/note-watching.ts
 create mode 100644 packages/backend/src/models/entities/note.ts
 create mode 100644 packages/backend/src/models/entities/notification.ts
 create mode 100644 packages/backend/src/models/entities/page-like.ts
 create mode 100644 packages/backend/src/models/entities/page.ts
 create mode 100644 packages/backend/src/models/entities/password-reset-request.ts
 create mode 100644 packages/backend/src/models/entities/poll-vote.ts
 create mode 100644 packages/backend/src/models/entities/poll.ts
 create mode 100644 packages/backend/src/models/entities/promo-note.ts
 create mode 100644 packages/backend/src/models/entities/promo-read.ts
 create mode 100644 packages/backend/src/models/entities/registration-tickets.ts
 create mode 100644 packages/backend/src/models/entities/registry-item.ts
 create mode 100644 packages/backend/src/models/entities/relay.ts
 create mode 100644 packages/backend/src/models/entities/signin.ts
 create mode 100644 packages/backend/src/models/entities/sw-subscription.ts
 create mode 100644 packages/backend/src/models/entities/used-username.ts
 create mode 100644 packages/backend/src/models/entities/user-group-invitation.ts
 create mode 100644 packages/backend/src/models/entities/user-group-joining.ts
 create mode 100644 packages/backend/src/models/entities/user-group.ts
 create mode 100644 packages/backend/src/models/entities/user-keypair.ts
 create mode 100644 packages/backend/src/models/entities/user-list-joining.ts
 create mode 100644 packages/backend/src/models/entities/user-list.ts
 create mode 100644 packages/backend/src/models/entities/user-note-pining.ts
 create mode 100644 packages/backend/src/models/entities/user-pending.ts
 create mode 100644 packages/backend/src/models/entities/user-profile.ts
 create mode 100644 packages/backend/src/models/entities/user-publickey.ts
 create mode 100644 packages/backend/src/models/entities/user-security-key.ts
 create mode 100644 packages/backend/src/models/entities/user.ts
 create mode 100644 packages/backend/src/models/id.ts
 create mode 100644 packages/backend/src/models/index.ts
 create mode 100644 packages/backend/src/models/repositories/abuse-user-report.ts
 create mode 100644 packages/backend/src/models/repositories/antenna.ts
 create mode 100644 packages/backend/src/models/repositories/app.ts
 create mode 100644 packages/backend/src/models/repositories/auth-session.ts
 create mode 100644 packages/backend/src/models/repositories/blocking.ts
 create mode 100644 packages/backend/src/models/repositories/channel.ts
 create mode 100644 packages/backend/src/models/repositories/clip.ts
 create mode 100644 packages/backend/src/models/repositories/drive-file.ts
 create mode 100644 packages/backend/src/models/repositories/drive-folder.ts
 create mode 100644 packages/backend/src/models/repositories/emoji.ts
 create mode 100644 packages/backend/src/models/repositories/federation-instance.ts
 create mode 100644 packages/backend/src/models/repositories/follow-request.ts
 create mode 100644 packages/backend/src/models/repositories/following.ts
 create mode 100644 packages/backend/src/models/repositories/gallery-like.ts
 create mode 100644 packages/backend/src/models/repositories/gallery-post.ts
 create mode 100644 packages/backend/src/models/repositories/games/reversi/game.ts
 create mode 100644 packages/backend/src/models/repositories/games/reversi/matching.ts
 create mode 100644 packages/backend/src/models/repositories/hashtag.ts
 create mode 100644 packages/backend/src/models/repositories/messaging-message.ts
 create mode 100644 packages/backend/src/models/repositories/moderation-logs.ts
 create mode 100644 packages/backend/src/models/repositories/muting.ts
 create mode 100644 packages/backend/src/models/repositories/note-favorite.ts
 create mode 100644 packages/backend/src/models/repositories/note-reaction.ts
 create mode 100644 packages/backend/src/models/repositories/note.ts
 create mode 100644 packages/backend/src/models/repositories/notification.ts
 create mode 100644 packages/backend/src/models/repositories/page-like.ts
 create mode 100644 packages/backend/src/models/repositories/page.ts
 create mode 100644 packages/backend/src/models/repositories/queue.ts
 create mode 100644 packages/backend/src/models/repositories/relay.ts
 create mode 100644 packages/backend/src/models/repositories/signin.ts
 create mode 100644 packages/backend/src/models/repositories/user-group-invitation.ts
 create mode 100644 packages/backend/src/models/repositories/user-group.ts
 create mode 100644 packages/backend/src/models/repositories/user-list.ts
 create mode 100644 packages/backend/src/models/repositories/user.ts
 create mode 100644 packages/backend/src/prelude/README.md
 create mode 100644 packages/backend/src/prelude/array.ts
 create mode 100644 packages/backend/src/prelude/await-all.ts
 create mode 100644 packages/backend/src/prelude/math.ts
 create mode 100644 packages/backend/src/prelude/maybe.ts
 create mode 100644 packages/backend/src/prelude/relation.ts
 create mode 100644 packages/backend/src/prelude/string.ts
 create mode 100644 packages/backend/src/prelude/symbol.ts
 create mode 100644 packages/backend/src/prelude/time.ts
 create mode 100644 packages/backend/src/prelude/url.ts
 create mode 100644 packages/backend/src/prelude/xml.ts
 create mode 100644 packages/backend/src/queue/get-job-info.ts
 create mode 100644 packages/backend/src/queue/index.ts
 create mode 100644 packages/backend/src/queue/initialize.ts
 create mode 100644 packages/backend/src/queue/logger.ts
 create mode 100644 packages/backend/src/queue/processors/db/delete-account.ts
 create mode 100644 packages/backend/src/queue/processors/db/delete-drive-files.ts
 create mode 100644 packages/backend/src/queue/processors/db/export-blocking.ts
 create mode 100644 packages/backend/src/queue/processors/db/export-following.ts
 create mode 100644 packages/backend/src/queue/processors/db/export-mute.ts
 create mode 100644 packages/backend/src/queue/processors/db/export-notes.ts
 create mode 100644 packages/backend/src/queue/processors/db/export-user-lists.ts
 create mode 100644 packages/backend/src/queue/processors/db/import-blocking.ts
 create mode 100644 packages/backend/src/queue/processors/db/import-following.ts
 create mode 100644 packages/backend/src/queue/processors/db/import-muting.ts
 create mode 100644 packages/backend/src/queue/processors/db/import-user-lists.ts
 create mode 100644 packages/backend/src/queue/processors/db/index.ts
 create mode 100644 packages/backend/src/queue/processors/deliver.ts
 create mode 100644 packages/backend/src/queue/processors/inbox.ts
 create mode 100644 packages/backend/src/queue/processors/object-storage/clean-remote-files.ts
 create mode 100644 packages/backend/src/queue/processors/object-storage/delete-file.ts
 create mode 100644 packages/backend/src/queue/processors/object-storage/index.ts
 create mode 100644 packages/backend/src/queue/processors/system/index.ts
 create mode 100644 packages/backend/src/queue/processors/system/resync-charts.ts
 create mode 100644 packages/backend/src/queue/queues.ts
 create mode 100644 packages/backend/src/queue/types.ts
 create mode 100644 packages/backend/src/remote/activitypub/ap-request.ts
 create mode 100644 packages/backend/src/remote/activitypub/audience.ts
 create mode 100644 packages/backend/src/remote/activitypub/db-resolver.ts
 create mode 100644 packages/backend/src/remote/activitypub/deliver-manager.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/accept/follow.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/accept/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/add/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/announce/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/announce/note.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/block/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/create/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/create/note.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/delete/actor.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/delete/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/delete/note.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/flag/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/follow.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/like.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/move/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/read.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/reject/follow.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/reject/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/remove/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/undo/announce.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/undo/block.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/undo/follow.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/undo/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/undo/like.ts
 create mode 100644 packages/backend/src/remote/activitypub/kernel/update/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/logger.ts
 create mode 100644 packages/backend/src/remote/activitypub/misc/contexts.ts
 create mode 100644 packages/backend/src/remote/activitypub/misc/get-note-html.ts
 create mode 100644 packages/backend/src/remote/activitypub/misc/html-to-mfm.ts
 create mode 100644 packages/backend/src/remote/activitypub/misc/ld-signature.ts
 create mode 100644 packages/backend/src/remote/activitypub/models/icon.ts
 create mode 100644 packages/backend/src/remote/activitypub/models/identifier.ts
 create mode 100644 packages/backend/src/remote/activitypub/models/image.ts
 create mode 100644 packages/backend/src/remote/activitypub/models/mention.ts
 create mode 100644 packages/backend/src/remote/activitypub/models/note.ts
 create mode 100644 packages/backend/src/remote/activitypub/models/person.ts
 create mode 100644 packages/backend/src/remote/activitypub/models/question.ts
 create mode 100644 packages/backend/src/remote/activitypub/models/tag.ts
 create mode 100644 packages/backend/src/remote/activitypub/perform.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/accept.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/add.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/announce.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/block.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/create.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/delete.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/document.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/emoji.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/follow-relay.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/follow-user.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/follow.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/hashtag.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/image.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/index.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/key.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/like.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/mention.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/note.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/ordered-collection.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/person.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/question.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/read.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/reject.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/remove.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/tombstone.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/undo.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/update.ts
 create mode 100644 packages/backend/src/remote/activitypub/renderer/vote.ts
 create mode 100644 packages/backend/src/remote/activitypub/request.ts
 create mode 100644 packages/backend/src/remote/activitypub/resolver.ts
 create mode 100644 packages/backend/src/remote/activitypub/type.ts
 create mode 100644 packages/backend/src/remote/logger.ts
 create mode 100644 packages/backend/src/remote/resolve-user.ts
 create mode 100644 packages/backend/src/remote/webfinger.ts
 create mode 100644 packages/backend/src/server/activitypub.ts
 create mode 100644 packages/backend/src/server/activitypub/featured.ts
 create mode 100644 packages/backend/src/server/activitypub/followers.ts
 create mode 100644 packages/backend/src/server/activitypub/following.ts
 create mode 100644 packages/backend/src/server/activitypub/outbox.ts
 create mode 100644 packages/backend/src/server/api/2fa.ts
 create mode 100644 packages/backend/src/server/api/api-handler.ts
 create mode 100644 packages/backend/src/server/api/authenticate.ts
 create mode 100644 packages/backend/src/server/api/call.ts
 create mode 100644 packages/backend/src/server/api/common/generate-block-query.ts
 create mode 100644 packages/backend/src/server/api/common/generate-channel-query.ts
 create mode 100644 packages/backend/src/server/api/common/generate-muted-note-query.ts
 create mode 100644 packages/backend/src/server/api/common/generate-muted-note-thread-query.ts
 create mode 100644 packages/backend/src/server/api/common/generate-muted-user-query.ts
 create mode 100644 packages/backend/src/server/api/common/generate-native-user-token.ts
 create mode 100644 packages/backend/src/server/api/common/generate-replies-query.ts
 create mode 100644 packages/backend/src/server/api/common/generate-visibility-query.ts
 create mode 100644 packages/backend/src/server/api/common/getters.ts
 create mode 100644 packages/backend/src/server/api/common/inject-featured.ts
 create mode 100644 packages/backend/src/server/api/common/inject-promo.ts
 create mode 100644 packages/backend/src/server/api/common/is-native-token.ts
 create mode 100644 packages/backend/src/server/api/common/make-pagination-query.ts
 create mode 100644 packages/backend/src/server/api/common/read-messaging-message.ts
 create mode 100644 packages/backend/src/server/api/common/read-notification.ts
 create mode 100644 packages/backend/src/server/api/common/signin.ts
 create mode 100644 packages/backend/src/server/api/common/signup.ts
 create mode 100644 packages/backend/src/server/api/define.ts
 create mode 100644 packages/backend/src/server/api/endpoints.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/accounts/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/ad/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/ad/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/ad/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/ad/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/announcements/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/announcements/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/announcements/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/announcements/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/delete-logs.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/drive/files.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/add.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/remove.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/invite.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/moderators/add.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/moderators/remove.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/promo/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/queue/clear.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/queue/stats.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/relays/add.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/relays/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/relays/remove.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/reset-password.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/resync-chart.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/send-email.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/server-info.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/show-user.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/show-users.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/silence-user.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/suspend-user.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/update-meta.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/vacuum.ts
 create mode 100644 packages/backend/src/server/api/endpoints/announcements.ts
 create mode 100644 packages/backend/src/server/api/endpoints/antennas/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/antennas/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/antennas/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/antennas/notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/antennas/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/antennas/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/ap/get.ts
 create mode 100644 packages/backend/src/server/api/endpoints/ap/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/app/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/app/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/auth/accept.ts
 create mode 100644 packages/backend/src/server/api/endpoints/auth/session/generate.ts
 create mode 100644 packages/backend/src/server/api/endpoints/auth/session/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/auth/session/userkey.ts
 create mode 100644 packages/backend/src/server/api/endpoints/blocking/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/blocking/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/blocking/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/featured.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/follow.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/followed.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/owned.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/pin-note.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/timeline.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/unfollow.ts
 create mode 100644 packages/backend/src/server/api/endpoints/channels/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/active-users.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/drive.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/federation.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/hashtag.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/instance.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/network.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/user/drive.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/user/following.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/user/notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/user/reactions.ts
 create mode 100644 packages/backend/src/server/api/endpoints/charts/users.ts
 create mode 100644 packages/backend/src/server/api/endpoints/clips/add-note.ts
 create mode 100644 packages/backend/src/server/api/endpoints/clips/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/clips/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/clips/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/clips/notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/clips/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/clips/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/find.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/folders.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/folders/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/folders/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/folders/find.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/folders/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/folders/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/drive/stream.ts
 create mode 100644 packages/backend/src/server/api/endpoints/email-address/available.ts
 create mode 100644 packages/backend/src/server/api/endpoints/endpoint.ts
 create mode 100644 packages/backend/src/server/api/endpoints/endpoints.ts
 create mode 100644 packages/backend/src/server/api/endpoints/federation/dns.ts
 create mode 100644 packages/backend/src/server/api/endpoints/federation/followers.ts
 create mode 100644 packages/backend/src/server/api/endpoints/federation/following.ts
 create mode 100644 packages/backend/src/server/api/endpoints/federation/instances.ts
 create mode 100644 packages/backend/src/server/api/endpoints/federation/show-instance.ts
 create mode 100644 packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
 create mode 100644 packages/backend/src/server/api/endpoints/federation/users.ts
 create mode 100644 packages/backend/src/server/api/endpoints/following/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/following/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/following/requests/accept.ts
 create mode 100644 packages/backend/src/server/api/endpoints/following/requests/cancel.ts
 create mode 100644 packages/backend/src/server/api/endpoints/following/requests/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/following/requests/reject.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/featured.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/popular.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/posts.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/like.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
 create mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/games/reversi/games.ts
 create mode 100644 packages/backend/src/server/api/endpoints/games/reversi/games/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/games/reversi/games/surrender.ts
 create mode 100644 packages/backend/src/server/api/endpoints/games/reversi/invitations.ts
 create mode 100644 packages/backend/src/server/api/endpoints/games/reversi/match.ts
 create mode 100644 packages/backend/src/server/api/endpoints/games/reversi/match/cancel.ts
 create mode 100644 packages/backend/src/server/api/endpoints/get-online-users-count.ts
 create mode 100644 packages/backend/src/server/api/endpoints/hashtags/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/hashtags/search.ts
 create mode 100644 packages/backend/src/server/api/endpoints/hashtags/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/hashtags/trend.ts
 create mode 100644 packages/backend/src/server/api/endpoints/hashtags/users.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/2fa/done.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/2fa/register.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/apps.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/authorized-apps.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/change-password.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/delete-account.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/export-blocking.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/export-following.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/export-mute.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/export-notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/export-user-lists.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/favorites.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/gallery/likes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/gallery/posts.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/import-blocking.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/import-following.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/import-muting.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/import-user-lists.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/notifications.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/page-likes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/pages.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/pin.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/read-announcement.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/regenerate-token.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/registry/get-all.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/registry/get.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/registry/keys.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/registry/remove.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/registry/scopes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/registry/set.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/revoke-token.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/signin-history.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/unpin.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/update-email.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/i/user-group-invites.ts
 create mode 100644 packages/backend/src/server/api/endpoints/messaging/history.ts
 create mode 100644 packages/backend/src/server/api/endpoints/messaging/messages.ts
 create mode 100644 packages/backend/src/server/api/endpoints/messaging/messages/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/messaging/messages/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/messaging/messages/read.ts
 create mode 100644 packages/backend/src/server/api/endpoints/meta.ts
 create mode 100644 packages/backend/src/server/api/endpoints/miauth/gen-token.ts
 create mode 100644 packages/backend/src/server/api/endpoints/mute/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/mute/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/mute/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/my/apps.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/children.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/clips.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/conversation.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/favorites/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/featured.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/global-timeline.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/local-timeline.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/mentions.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/polls/vote.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/reactions.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/reactions/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/renotes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/replies.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/search.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/state.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/timeline.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/translate.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/unrenote.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/watching/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notes/watching/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notifications/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
 create mode 100644 packages/backend/src/server/api/endpoints/notifications/read.ts
 create mode 100644 packages/backend/src/server/api/endpoints/page-push.ts
 create mode 100644 packages/backend/src/server/api/endpoints/pages/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/pages/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/pages/featured.ts
 create mode 100644 packages/backend/src/server/api/endpoints/pages/like.ts
 create mode 100644 packages/backend/src/server/api/endpoints/pages/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/pages/unlike.ts
 create mode 100644 packages/backend/src/server/api/endpoints/pages/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/ping.ts
 create mode 100644 packages/backend/src/server/api/endpoints/pinned-users.ts
 create mode 100644 packages/backend/src/server/api/endpoints/promo/read.ts
 create mode 100644 packages/backend/src/server/api/endpoints/request-reset-password.ts
 create mode 100644 packages/backend/src/server/api/endpoints/reset-db.ts
 create mode 100644 packages/backend/src/server/api/endpoints/reset-password.ts
 create mode 100644 packages/backend/src/server/api/endpoints/room/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/room/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/server-info.ts
 create mode 100644 packages/backend/src/server/api/endpoints/stats.ts
 create mode 100644 packages/backend/src/server/api/endpoints/sw/register.ts
 create mode 100644 packages/backend/src/server/api/endpoints/sw/unregister.ts
 create mode 100644 packages/backend/src/server/api/endpoints/username/available.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/clips.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/followers.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/following.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/gallery/posts.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/invite.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/joined.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/leave.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/owned.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/pull.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/transfer.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/groups/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/lists/create.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/lists/delete.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/lists/list.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/lists/pull.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/lists/push.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/lists/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/lists/update.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/notes.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/pages.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/reactions.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/recommendation.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/relation.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/report-abuse.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/search.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/show.ts
 create mode 100644 packages/backend/src/server/api/endpoints/users/stats.ts
 create mode 100644 packages/backend/src/server/api/error.ts
 create mode 100644 packages/backend/src/server/api/index.ts
 create mode 100644 packages/backend/src/server/api/limiter.ts
 create mode 100644 packages/backend/src/server/api/logger.ts
 create mode 100644 packages/backend/src/server/api/openapi/errors.ts
 create mode 100644 packages/backend/src/server/api/openapi/gen-spec.ts
 create mode 100644 packages/backend/src/server/api/openapi/schemas.ts
 create mode 100644 packages/backend/src/server/api/private/signin.ts
 create mode 100644 packages/backend/src/server/api/private/signup-pending.ts
 create mode 100644 packages/backend/src/server/api/private/signup.ts
 create mode 100644 packages/backend/src/server/api/service/discord.ts
 create mode 100644 packages/backend/src/server/api/service/github.ts
 create mode 100644 packages/backend/src/server/api/service/twitter.ts
 create mode 100644 packages/backend/src/server/api/stream/channel.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/admin.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/antenna.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/channel.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/drive.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/games/reversi-game.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/games/reversi.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/global-timeline.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/hashtag.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/home-timeline.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/index.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/local-timeline.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/main.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/messaging-index.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/messaging.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/queue-stats.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/server-stats.ts
 create mode 100644 packages/backend/src/server/api/stream/channels/user-list.ts
 create mode 100644 packages/backend/src/server/api/stream/index.ts
 create mode 100644 packages/backend/src/server/api/stream/types.ts
 create mode 100644 packages/backend/src/server/api/streaming.ts
 create mode 100644 packages/backend/src/server/file/assets/bad-egg.png
 create mode 100644 packages/backend/src/server/file/assets/cache-expired.png
 create mode 100644 packages/backend/src/server/file/assets/dummy.png
 create mode 100644 packages/backend/src/server/file/assets/not-an-image.png
 create mode 100644 packages/backend/src/server/file/assets/thumbnail-not-available.png
 create mode 100644 packages/backend/src/server/file/assets/tombstone.png
 create mode 100644 packages/backend/src/server/file/index.ts
 create mode 100644 packages/backend/src/server/file/send-drive-file.ts
 create mode 100644 packages/backend/src/server/index.ts
 create mode 100644 packages/backend/src/server/nodeinfo.ts
 create mode 100644 packages/backend/src/server/proxy/index.ts
 create mode 100644 packages/backend/src/server/proxy/proxy-media.ts
 create mode 100644 packages/backend/src/server/web/bios.css
 create mode 100644 packages/backend/src/server/web/bios.js
 create mode 100644 packages/backend/src/server/web/boot.js
 create mode 100644 packages/backend/src/server/web/cli.css
 create mode 100644 packages/backend/src/server/web/cli.js
 create mode 100644 packages/backend/src/server/web/feed.ts
 create mode 100644 packages/backend/src/server/web/index.ts
 create mode 100644 packages/backend/src/server/web/manifest.json
 create mode 100644 packages/backend/src/server/web/manifest.ts
 create mode 100644 packages/backend/src/server/web/style.css
 create mode 100644 packages/backend/src/server/web/url-preview.ts
 create mode 100644 packages/backend/src/server/web/views/base.pug
 create mode 100644 packages/backend/src/server/web/views/bios.pug
 create mode 100644 packages/backend/src/server/web/views/channel.pug
 create mode 100644 packages/backend/src/server/web/views/cli.pug
 create mode 100644 packages/backend/src/server/web/views/clip.pug
 create mode 100644 packages/backend/src/server/web/views/flush.pug
 create mode 100644 packages/backend/src/server/web/views/gallery-post.pug
 create mode 100644 packages/backend/src/server/web/views/info-card.pug
 create mode 100644 packages/backend/src/server/web/views/note.pug
 create mode 100644 packages/backend/src/server/web/views/page.pug
 create mode 100644 packages/backend/src/server/web/views/user.pug
 create mode 100644 packages/backend/src/server/well-known.ts
 create mode 100644 packages/backend/src/services/add-note-to-antenna.ts
 create mode 100644 packages/backend/src/services/blocking/create.ts
 create mode 100644 packages/backend/src/services/blocking/delete.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/active-users.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/drive.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/federation.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/hashtag.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/instance.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/network.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/notes.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/per-user-drive.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/per-user-following.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/per-user-notes.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/per-user-reactions.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/test-grouped.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/test-unique.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/test.ts
 create mode 100644 packages/backend/src/services/chart/charts/classes/users.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/active-users.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/drive.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/federation.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/hashtag.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/instance.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/network.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/notes.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/per-user-drive.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/per-user-following.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/per-user-notes.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/per-user-reactions.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/test-grouped.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/test-unique.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/test.ts
 create mode 100644 packages/backend/src/services/chart/charts/schemas/users.ts
 create mode 100644 packages/backend/src/services/chart/core.ts
 create mode 100644 packages/backend/src/services/chart/entities.ts
 create mode 100644 packages/backend/src/services/chart/index.ts
 create mode 100644 packages/backend/src/services/create-notification.ts
 create mode 100644 packages/backend/src/services/create-system-user.ts
 create mode 100644 packages/backend/src/services/drive/add-file.ts
 create mode 100644 packages/backend/src/services/drive/delete-file.ts
 create mode 100644 packages/backend/src/services/drive/generate-video-thumbnail.ts
 create mode 100644 packages/backend/src/services/drive/image-processor.ts
 create mode 100644 packages/backend/src/services/drive/internal-storage.ts
 create mode 100644 packages/backend/src/services/drive/logger.ts
 create mode 100644 packages/backend/src/services/drive/s3.ts
 create mode 100644 packages/backend/src/services/drive/upload-from-url.ts
 create mode 100644 packages/backend/src/services/fetch-instance-metadata.ts
 create mode 100644 packages/backend/src/services/following/create.ts
 create mode 100644 packages/backend/src/services/following/delete.ts
 create mode 100644 packages/backend/src/services/following/requests/accept-all.ts
 create mode 100644 packages/backend/src/services/following/requests/accept.ts
 create mode 100644 packages/backend/src/services/following/requests/cancel.ts
 create mode 100644 packages/backend/src/services/following/requests/create.ts
 create mode 100644 packages/backend/src/services/following/requests/reject.ts
 create mode 100644 packages/backend/src/services/i/pin.ts
 create mode 100644 packages/backend/src/services/i/update.ts
 create mode 100644 packages/backend/src/services/insert-moderation-log.ts
 create mode 100644 packages/backend/src/services/instance-actor.ts
 create mode 100644 packages/backend/src/services/logger.ts
 create mode 100644 packages/backend/src/services/messages/create.ts
 create mode 100644 packages/backend/src/services/messages/delete.ts
 create mode 100644 packages/backend/src/services/note/create.ts
 create mode 100644 packages/backend/src/services/note/delete.ts
 create mode 100644 packages/backend/src/services/note/polls/update.ts
 create mode 100644 packages/backend/src/services/note/polls/vote.ts
 create mode 100644 packages/backend/src/services/note/reaction/create.ts
 create mode 100644 packages/backend/src/services/note/reaction/delete.ts
 create mode 100644 packages/backend/src/services/note/read.ts
 create mode 100644 packages/backend/src/services/note/unread.ts
 create mode 100644 packages/backend/src/services/note/unwatch.ts
 create mode 100644 packages/backend/src/services/note/watch.ts
 create mode 100644 packages/backend/src/services/push-notification.ts
 create mode 100644 packages/backend/src/services/register-or-fetch-instance-doc.ts
 create mode 100644 packages/backend/src/services/relay.ts
 create mode 100644 packages/backend/src/services/send-email-notification.ts
 create mode 100644 packages/backend/src/services/send-email.ts
 create mode 100644 packages/backend/src/services/stream.ts
 create mode 100644 packages/backend/src/services/suspend-user.ts
 create mode 100644 packages/backend/src/services/unsuspend-user.ts
 create mode 100644 packages/backend/src/services/update-hashtag.ts
 create mode 100644 packages/backend/src/services/user-list/push.ts
 create mode 100644 packages/backend/src/services/validate-email-for-account.ts
 create mode 100644 packages/backend/src/types.ts
 create mode 100644 packages/backend/tools/accept-migration.js
 create mode 100644 packages/backend/tools/accept-migration.js.map
 create mode 100644 packages/backend/tools/accept-migration.ts
 create mode 100644 packages/backend/tools/add-emoji.js
 create mode 100644 packages/backend/tools/add-emoji.js.map
 create mode 100644 packages/backend/tools/add-emoji.ts
 create mode 100644 packages/backend/tools/demote-admin.js
 create mode 100644 packages/backend/tools/demote-admin.js.map
 create mode 100644 packages/backend/tools/demote-admin.ts
 create mode 100644 packages/backend/tools/mark-admin.js
 create mode 100644 packages/backend/tools/mark-admin.js.map
 create mode 100644 packages/backend/tools/mark-admin.ts
 create mode 100644 packages/backend/tools/refresh-question.js
 create mode 100644 packages/backend/tools/refresh-question.js.map
 create mode 100644 packages/backend/tools/refresh-question.ts
 create mode 100644 packages/backend/tools/resync-remote-user.js
 create mode 100644 packages/backend/tools/resync-remote-user.js.map
 create mode 100644 packages/backend/tools/resync-remote-user.ts
 create mode 100644 packages/backend/tools/show-signin-history.js
 create mode 100644 packages/backend/tools/show-signin-history.js.map
 create mode 100644 packages/backend/tools/show-signin-history.ts
 create mode 100644 packages/backend/tsconfig.json
 create mode 100644 packages/backend/yarn.lock
 create mode 100644 packages/client/.eslintrc
 create mode 100644 packages/client/.npmrc
 create mode 100644 packages/client/@types/global.d.ts
 create mode 100644 packages/client/@types/vue.d.ts
 create mode 100644 packages/client/assets/about-icon.png
 create mode 100644 packages/client/assets/fedi.jpg
 create mode 100644 packages/client/assets/label-red.svg
 create mode 100644 packages/client/assets/label.svg
 create mode 100644 packages/client/assets/misskey.svg
 create mode 100644 packages/client/assets/remove.png
 create mode 100644 packages/client/assets/room/furnitures/banknote/banknote.blend
 create mode 100644 packages/client/assets/room/furnitures/banknote/banknote.glb
 create mode 100644 packages/client/assets/room/furnitures/banknote/tex.png
 create mode 100644 packages/client/assets/room/furnitures/bed/bed.blend
 create mode 100644 packages/client/assets/room/furnitures/bed/bed.glb
 create mode 100644 packages/client/assets/room/furnitures/bin/bin.blend
 create mode 100644 packages/client/assets/room/furnitures/bin/bin.glb
 create mode 100644 packages/client/assets/room/furnitures/book/book.blend
 create mode 100644 packages/client/assets/room/furnitures/book/book.glb
 create mode 100644 packages/client/assets/room/furnitures/book2/barcode.png
 create mode 100644 packages/client/assets/room/furnitures/book2/book2.blend
 create mode 100644 packages/client/assets/room/furnitures/book2/book2.glb
 create mode 100644 packages/client/assets/room/furnitures/book2/texture.afdesign
 create mode 100644 packages/client/assets/room/furnitures/book2/texture.png
 create mode 100644 packages/client/assets/room/furnitures/book2/uv.png
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box/cardboard-box.blend
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box/cardboard-box.glb
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box2/texture.png
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box2/uv.png
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box3/texture.png
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box3/texture.xcf
 create mode 100644 packages/client/assets/room/furnitures/cardboard-box3/uv.png
 create mode 100644 packages/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend
 create mode 100644 packages/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb
 create mode 100644 packages/client/assets/room/furnitures/chair/chair.blend
 create mode 100644 packages/client/assets/room/furnitures/chair/chair.glb
 create mode 100644 packages/client/assets/room/furnitures/chair2/chair2.blend
 create mode 100644 packages/client/assets/room/furnitures/chair2/chair2.glb
 create mode 100644 packages/client/assets/room/furnitures/color-box/color-box.blend
 create mode 100644 packages/client/assets/room/furnitures/color-box/color-box.glb
 create mode 100644 packages/client/assets/room/furnitures/corkboard/corkboard.blend
 create mode 100644 packages/client/assets/room/furnitures/corkboard/corkboard.glb
 create mode 100644 packages/client/assets/room/furnitures/cube/cube.blend
 create mode 100644 packages/client/assets/room/furnitures/cube/cube.glb
 create mode 100644 packages/client/assets/room/furnitures/cup-noodle/cup-noodle.blend
 create mode 100644 packages/client/assets/room/furnitures/cup-noodle/cup-noodle.glb
 create mode 100644 packages/client/assets/room/furnitures/cup-noodle/noodle.png
 create mode 100644 packages/client/assets/room/furnitures/desk/desk.blend
 create mode 100644 packages/client/assets/room/furnitures/desk/desk.glb
 create mode 100644 packages/client/assets/room/furnitures/doll-ai/doll-ai.blend
 create mode 100644 packages/client/assets/room/furnitures/doll-ai/doll-ai.glb
 create mode 100644 packages/client/assets/room/furnitures/doll-ai/doll_ai_tex.png
 create mode 100644 packages/client/assets/room/furnitures/energy-drink/energy-drink.blend
 create mode 100644 packages/client/assets/room/furnitures/energy-drink/energy-drink.glb
 create mode 100644 packages/client/assets/room/furnitures/energy-drink/texture.afdesign
 create mode 100644 packages/client/assets/room/furnitures/energy-drink/texture.png
 create mode 100644 packages/client/assets/room/furnitures/energy-drink/uv.png
 create mode 100644 packages/client/assets/room/furnitures/eraser/cover.png
 create mode 100644 packages/client/assets/room/furnitures/eraser/cover.psd
 create mode 100644 packages/client/assets/room/furnitures/eraser/eraser-uv.png
 create mode 100644 packages/client/assets/room/furnitures/eraser/eraser.blend
 create mode 100644 packages/client/assets/room/furnitures/eraser/eraser.glb
 create mode 100644 packages/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png
 create mode 100644 packages/client/assets/room/furnitures/facial-tissue/facial-tissue.blend
 create mode 100644 packages/client/assets/room/furnitures/facial-tissue/facial-tissue.glb
 create mode 100644 packages/client/assets/room/furnitures/facial-tissue/facial-tissue.png
 create mode 100644 packages/client/assets/room/furnitures/facial-tissue/facial-tissue.psd
 create mode 100644 packages/client/assets/room/furnitures/fan/fan.blend
 create mode 100644 packages/client/assets/room/furnitures/fan/fan.glb
 create mode 100644 packages/client/assets/room/furnitures/holo-display/holo-display.blend
 create mode 100644 packages/client/assets/room/furnitures/holo-display/holo-display.glb
 create mode 100644 packages/client/assets/room/furnitures/holo-display/ray-uv.png
 create mode 100644 packages/client/assets/room/furnitures/holo-display/ray.png
 create mode 100644 packages/client/assets/room/furnitures/keyboard/keyboard.blend
 create mode 100644 packages/client/assets/room/furnitures/keyboard/keyboard.glb
 create mode 100644 packages/client/assets/room/furnitures/low-table/low-table.blend
 create mode 100644 packages/client/assets/room/furnitures/low-table/low-table.glb
 create mode 100644 packages/client/assets/room/furnitures/mat/mat.blend
 create mode 100644 packages/client/assets/room/furnitures/mat/mat.glb
 create mode 100644 packages/client/assets/room/furnitures/milk/milk-uv.png
 create mode 100644 packages/client/assets/room/furnitures/milk/milk.blend
 create mode 100644 packages/client/assets/room/furnitures/milk/milk.glb
 create mode 100644 packages/client/assets/room/furnitures/milk/milk.png
 create mode 100644 packages/client/assets/room/furnitures/milk/milk.psd
 create mode 100644 packages/client/assets/room/furnitures/monitor/monitor.blend
 create mode 100644 packages/client/assets/room/furnitures/monitor/monitor.glb
 create mode 100644 packages/client/assets/room/furnitures/monitor/monitor.psd
 create mode 100644 packages/client/assets/room/furnitures/monitor/screen-uv.png
 create mode 100644 packages/client/assets/room/furnitures/monitor/screen.jpg
 create mode 100644 packages/client/assets/room/furnitures/moon/moon.blend
 create mode 100644 packages/client/assets/room/furnitures/moon/moon.glb
 create mode 100644 packages/client/assets/room/furnitures/moon/moon.jpg
 create mode 100644 packages/client/assets/room/furnitures/mousepad/mousepad.blend
 create mode 100644 packages/client/assets/room/furnitures/mousepad/mousepad.glb
 create mode 100644 packages/client/assets/room/furnitures/pc/motherboard-uv.png
 create mode 100644 packages/client/assets/room/furnitures/pc/motherboard-uv.psd
 create mode 100644 packages/client/assets/room/furnitures/pc/motherboard.jpg
 create mode 100644 packages/client/assets/room/furnitures/pc/pc.blend
 create mode 100644 packages/client/assets/room/furnitures/pc/pc.glb
 create mode 100644 packages/client/assets/room/furnitures/pencil/pencil.blend
 create mode 100644 packages/client/assets/room/furnitures/pencil/pencil.glb
 create mode 100644 packages/client/assets/room/furnitures/photoframe/photo-uv.png
 create mode 100644 packages/client/assets/room/furnitures/photoframe/photo.jpg
 create mode 100644 packages/client/assets/room/furnitures/photoframe/photoframe.blend
 create mode 100644 packages/client/assets/room/furnitures/photoframe/photoframe.glb
 create mode 100644 packages/client/assets/room/furnitures/piano/piano.blend
 create mode 100644 packages/client/assets/room/furnitures/piano/piano.glb
 create mode 100644 packages/client/assets/room/furnitures/pinguin/pinguin.blend
 create mode 100644 packages/client/assets/room/furnitures/pinguin/pinguin.glb
 create mode 100644 packages/client/assets/room/furnitures/plant/plant-soil-uv.png
 create mode 100644 packages/client/assets/room/furnitures/plant/plant-soil.png
 create mode 100644 packages/client/assets/room/furnitures/plant/plant-soil.psd
 create mode 100644 packages/client/assets/room/furnitures/plant/plant.blend
 create mode 100644 packages/client/assets/room/furnitures/plant/plant.glb
 create mode 100644 packages/client/assets/room/furnitures/plant2/plant2.blend
 create mode 100644 packages/client/assets/room/furnitures/plant2/plant2.glb
 create mode 100644 packages/client/assets/room/furnitures/plant2/soil.png
 create mode 100644 packages/client/assets/room/furnitures/poster-h/poster-h.blend
 create mode 100644 packages/client/assets/room/furnitures/poster-h/poster-h.glb
 create mode 100644 packages/client/assets/room/furnitures/poster-h/uv.png
 create mode 100644 packages/client/assets/room/furnitures/poster-v/poster-v.blend
 create mode 100644 packages/client/assets/room/furnitures/poster-v/poster-v.glb
 create mode 100644 packages/client/assets/room/furnitures/poster-v/uv.png
 create mode 100644 packages/client/assets/room/furnitures/pudding/pudding.blend
 create mode 100644 packages/client/assets/room/furnitures/pudding/pudding.glb
 create mode 100644 packages/client/assets/room/furnitures/rubik-cube/rubik-cube.blend
 create mode 100644 packages/client/assets/room/furnitures/rubik-cube/rubik-cube.glb
 create mode 100644 packages/client/assets/room/furnitures/server/rack-uv.png
 create mode 100644 packages/client/assets/room/furnitures/server/rack.png
 create mode 100644 packages/client/assets/room/furnitures/server/server.blend
 create mode 100644 packages/client/assets/room/furnitures/server/server.glb
 create mode 100644 packages/client/assets/room/furnitures/server/server.png
 create mode 100644 packages/client/assets/room/furnitures/server/uv.png
 create mode 100644 packages/client/assets/room/furnitures/sofa/sofa.blend
 create mode 100644 packages/client/assets/room/furnitures/sofa/sofa.glb
 create mode 100644 packages/client/assets/room/furnitures/spiral/spiral.blend
 create mode 100644 packages/client/assets/room/furnitures/spiral/spiral.glb
 create mode 100644 packages/client/assets/room/furnitures/tv/screen-uv.png
 create mode 100644 packages/client/assets/room/furnitures/tv/tv.blend
 create mode 100644 packages/client/assets/room/furnitures/tv/tv.glb
 create mode 100644 packages/client/assets/room/furnitures/wall-clock/wall-clock.blend
 create mode 100644 packages/client/assets/room/furnitures/wall-clock/wall-clock.glb
 create mode 100644 packages/client/assets/room/rooms/default/default.blend
 create mode 100644 packages/client/assets/room/rooms/default/default.glb
 create mode 100644 packages/client/assets/room/rooms/washitsu/husuma-uv.png
 create mode 100644 packages/client/assets/room/rooms/washitsu/husuma.png
 create mode 100644 packages/client/assets/room/rooms/washitsu/tatami-single1600.png
 create mode 100644 packages/client/assets/room/rooms/washitsu/tatami-uv.png
 create mode 100644 packages/client/assets/room/rooms/washitsu/tatami.afdesign
 create mode 100644 packages/client/assets/room/rooms/washitsu/tatami.png
 create mode 100644 packages/client/assets/room/rooms/washitsu/washitsu.blend
 create mode 100644 packages/client/assets/room/rooms/washitsu/washitsu.glb
 create mode 100644 packages/client/assets/sounds/aisha/1.mp3
 create mode 100644 packages/client/assets/sounds/aisha/2.mp3
 create mode 100644 packages/client/assets/sounds/aisha/3.mp3
 create mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba.mp3
 create mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba2.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/down.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/kick.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/pirori-square-wet.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/pirori-wet.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/pirori.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/poi1.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/poi2.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/pope1.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/pope2.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/popo.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/queue-jammed.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/reverved.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/ryukyu.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/snare.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/square-pico.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/triple.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/up.mp3
 create mode 100644 packages/client/assets/sounds/syuilo/waon.mp3
 create mode 100644 packages/client/assets/sparkle-spritesheet.png
 create mode 100644 packages/client/assets/unread.svg
 create mode 100644 packages/client/package.json
 create mode 100644 packages/client/src/account.ts
 create mode 100644 packages/client/src/components/abuse-report-window.vue
 create mode 100644 packages/client/src/components/analog-clock.vue
 create mode 100644 packages/client/src/components/autocomplete.vue
 create mode 100644 packages/client/src/components/avatars.vue
 create mode 100644 packages/client/src/components/captcha.vue
 create mode 100644 packages/client/src/components/channel-follow-button.vue
 create mode 100644 packages/client/src/components/channel-preview.vue
 create mode 100644 packages/client/src/components/chart.vue
 create mode 100644 packages/client/src/components/code-core.vue
 create mode 100644 packages/client/src/components/code.vue
 create mode 100644 packages/client/src/components/cw-button.vue
 create mode 100644 packages/client/src/components/date-separated-list.vue
 create mode 100644 packages/client/src/components/debobigego/base.vue
 create mode 100644 packages/client/src/components/debobigego/button.vue
 create mode 100644 packages/client/src/components/debobigego/debobigego.scss
 create mode 100644 packages/client/src/components/debobigego/group.vue
 create mode 100644 packages/client/src/components/debobigego/info.vue
 create mode 100644 packages/client/src/components/debobigego/input.vue
 create mode 100644 packages/client/src/components/debobigego/key-value-view.vue
 create mode 100644 packages/client/src/components/debobigego/link.vue
 create mode 100644 packages/client/src/components/debobigego/object-view.vue
 create mode 100644 packages/client/src/components/debobigego/pagination.vue
 create mode 100644 packages/client/src/components/debobigego/radios.vue
 create mode 100644 packages/client/src/components/debobigego/range.vue
 create mode 100644 packages/client/src/components/debobigego/select.vue
 create mode 100644 packages/client/src/components/debobigego/suspense.vue
 create mode 100644 packages/client/src/components/debobigego/switch.vue
 create mode 100644 packages/client/src/components/debobigego/textarea.vue
 create mode 100644 packages/client/src/components/debobigego/tuple.vue
 create mode 100644 packages/client/src/components/dialog.vue
 create mode 100644 packages/client/src/components/drive-file-thumbnail.vue
 create mode 100644 packages/client/src/components/drive-select-dialog.vue
 create mode 100644 packages/client/src/components/drive-window.vue
 create mode 100644 packages/client/src/components/drive.file.vue
 create mode 100644 packages/client/src/components/drive.folder.vue
 create mode 100644 packages/client/src/components/drive.nav-folder.vue
 create mode 100644 packages/client/src/components/drive.vue
 create mode 100644 packages/client/src/components/emoji-picker-dialog.vue
 create mode 100644 packages/client/src/components/emoji-picker-window.vue
 create mode 100644 packages/client/src/components/emoji-picker.section.vue
 create mode 100644 packages/client/src/components/emoji-picker.vue
 create mode 100644 packages/client/src/components/featured-photos.vue
 create mode 100644 packages/client/src/components/file-type-icon.vue
 create mode 100644 packages/client/src/components/follow-button.vue
 create mode 100644 packages/client/src/components/forgot-password.vue
 create mode 100644 packages/client/src/components/form-dialog.vue
 create mode 100644 packages/client/src/components/form/input.vue
 create mode 100644 packages/client/src/components/form/radio.vue
 create mode 100644 packages/client/src/components/form/radios.vue
 create mode 100644 packages/client/src/components/form/range.vue
 create mode 100644 packages/client/src/components/form/section.vue
 create mode 100644 packages/client/src/components/form/select.vue
 create mode 100644 packages/client/src/components/form/slot.vue
 create mode 100644 packages/client/src/components/form/switch.vue
 create mode 100644 packages/client/src/components/form/textarea.vue
 create mode 100644 packages/client/src/components/formula-core.vue
 create mode 100644 packages/client/src/components/formula.vue
 create mode 100644 packages/client/src/components/gallery-post-preview.vue
 create mode 100644 packages/client/src/components/global/a.vue
 create mode 100644 packages/client/src/components/global/acct.vue
 create mode 100644 packages/client/src/components/global/ad.vue
 create mode 100644 packages/client/src/components/global/avatar.vue
 create mode 100644 packages/client/src/components/global/ellipsis.vue
 create mode 100644 packages/client/src/components/global/emoji.vue
 create mode 100644 packages/client/src/components/global/error.vue
 create mode 100644 packages/client/src/components/global/header.vue
 create mode 100644 packages/client/src/components/global/i18n.ts
 create mode 100644 packages/client/src/components/global/loading.vue
 create mode 100644 packages/client/src/components/global/misskey-flavored-markdown.vue
 create mode 100644 packages/client/src/components/global/spacer.vue
 create mode 100644 packages/client/src/components/global/sticky-container.vue
 create mode 100644 packages/client/src/components/global/time.vue
 create mode 100644 packages/client/src/components/global/url.vue
 create mode 100644 packages/client/src/components/global/user-name.vue
 create mode 100644 packages/client/src/components/google.vue
 create mode 100644 packages/client/src/components/image-viewer.vue
 create mode 100644 packages/client/src/components/img-with-blurhash.vue
 create mode 100644 packages/client/src/components/index.ts
 create mode 100644 packages/client/src/components/instance-stats.vue
 create mode 100644 packages/client/src/components/instance-ticker.vue
 create mode 100644 packages/client/src/components/launch-pad.vue
 create mode 100644 packages/client/src/components/link.vue
 create mode 100644 packages/client/src/components/media-banner.vue
 create mode 100644 packages/client/src/components/media-caption.vue
 create mode 100644 packages/client/src/components/media-image.vue
 create mode 100644 packages/client/src/components/media-list.vue
 create mode 100644 packages/client/src/components/media-video.vue
 create mode 100644 packages/client/src/components/mention.vue
 create mode 100644 packages/client/src/components/mfm.ts
 create mode 100644 packages/client/src/components/mini-chart.vue
 create mode 100644 packages/client/src/components/modal-page-window.vue
 create mode 100644 packages/client/src/components/note-detailed.vue
 create mode 100644 packages/client/src/components/note-header.vue
 create mode 100644 packages/client/src/components/note-preview.vue
 create mode 100644 packages/client/src/components/note-simple.vue
 create mode 100644 packages/client/src/components/note.sub.vue
 create mode 100644 packages/client/src/components/note.vue
 create mode 100644 packages/client/src/components/notes.vue
 create mode 100644 packages/client/src/components/notification-setting-window.vue
 create mode 100644 packages/client/src/components/notification.vue
 create mode 100644 packages/client/src/components/notifications.vue
 create mode 100644 packages/client/src/components/number-diff.vue
 create mode 100644 packages/client/src/components/page-preview.vue
 create mode 100644 packages/client/src/components/page-window.vue
 create mode 100644 packages/client/src/components/page/page.block.vue
 create mode 100644 packages/client/src/components/page/page.button.vue
 create mode 100644 packages/client/src/components/page/page.canvas.vue
 create mode 100644 packages/client/src/components/page/page.counter.vue
 create mode 100644 packages/client/src/components/page/page.if.vue
 create mode 100644 packages/client/src/components/page/page.image.vue
 create mode 100644 packages/client/src/components/page/page.note.vue
 create mode 100644 packages/client/src/components/page/page.number-input.vue
 create mode 100644 packages/client/src/components/page/page.post.vue
 create mode 100644 packages/client/src/components/page/page.radio-button.vue
 create mode 100644 packages/client/src/components/page/page.section.vue
 create mode 100644 packages/client/src/components/page/page.switch.vue
 create mode 100644 packages/client/src/components/page/page.text-input.vue
 create mode 100644 packages/client/src/components/page/page.text.vue
 create mode 100644 packages/client/src/components/page/page.textarea-input.vue
 create mode 100644 packages/client/src/components/page/page.textarea.vue
 create mode 100644 packages/client/src/components/page/page.vue
 create mode 100644 packages/client/src/components/particle.vue
 create mode 100644 packages/client/src/components/poll-editor.vue
 create mode 100644 packages/client/src/components/poll.vue
 create mode 100644 packages/client/src/components/post-form-attaches.vue
 create mode 100644 packages/client/src/components/post-form-dialog.vue
 create mode 100644 packages/client/src/components/post-form.vue
 create mode 100644 packages/client/src/components/queue-chart.vue
 create mode 100644 packages/client/src/components/reaction-icon.vue
 create mode 100644 packages/client/src/components/reaction-tooltip.vue
 create mode 100644 packages/client/src/components/reactions-viewer.details.vue
 create mode 100644 packages/client/src/components/reactions-viewer.reaction.vue
 create mode 100644 packages/client/src/components/reactions-viewer.vue
 create mode 100644 packages/client/src/components/remote-caution.vue
 create mode 100644 packages/client/src/components/sample.vue
 create mode 100644 packages/client/src/components/signin-dialog.vue
 create mode 100644 packages/client/src/components/signin.vue
 create mode 100644 packages/client/src/components/signup-dialog.vue
 create mode 100644 packages/client/src/components/signup.vue
 create mode 100644 packages/client/src/components/sparkle.vue
 create mode 100644 packages/client/src/components/sub-note-content.vue
 create mode 100644 packages/client/src/components/tab.vue
 create mode 100644 packages/client/src/components/taskmanager.api-window.vue
 create mode 100644 packages/client/src/components/taskmanager.vue
 create mode 100644 packages/client/src/components/timeline.vue
 create mode 100644 packages/client/src/components/toast.vue
 create mode 100644 packages/client/src/components/token-generate-window.vue
 create mode 100644 packages/client/src/components/ui/button.vue
 create mode 100644 packages/client/src/components/ui/container.vue
 create mode 100644 packages/client/src/components/ui/context-menu.vue
 create mode 100644 packages/client/src/components/ui/folder.vue
 create mode 100644 packages/client/src/components/ui/hr.vue
 create mode 100644 packages/client/src/components/ui/info.vue
 create mode 100644 packages/client/src/components/ui/menu.vue
 create mode 100644 packages/client/src/components/ui/modal-window.vue
 create mode 100644 packages/client/src/components/ui/modal.vue
 create mode 100644 packages/client/src/components/ui/pagination.vue
 create mode 100644 packages/client/src/components/ui/popup-menu.vue
 create mode 100644 packages/client/src/components/ui/popup.vue
 create mode 100644 packages/client/src/components/ui/super-menu.vue
 create mode 100644 packages/client/src/components/ui/tooltip.vue
 create mode 100644 packages/client/src/components/ui/window.vue
 create mode 100644 packages/client/src/components/updated.vue
 create mode 100644 packages/client/src/components/url-preview-popup.vue
 create mode 100644 packages/client/src/components/url-preview.vue
 create mode 100644 packages/client/src/components/user-info.vue
 create mode 100644 packages/client/src/components/user-list.vue
 create mode 100644 packages/client/src/components/user-online-indicator.vue
 create mode 100644 packages/client/src/components/user-preview.vue
 create mode 100644 packages/client/src/components/user-select-dialog.vue
 create mode 100644 packages/client/src/components/users-dialog.vue
 create mode 100644 packages/client/src/components/visibility-picker.vue
 create mode 100644 packages/client/src/components/waiting-dialog.vue
 create mode 100644 packages/client/src/components/widgets.vue
 create mode 100644 packages/client/src/config.ts
 create mode 100644 packages/client/src/directives/anim.ts
 create mode 100644 packages/client/src/directives/appear.ts
 create mode 100644 packages/client/src/directives/click-anime.ts
 create mode 100644 packages/client/src/directives/follow-append.ts
 create mode 100644 packages/client/src/directives/get-size.ts
 create mode 100644 packages/client/src/directives/hotkey.ts
 create mode 100644 packages/client/src/directives/index.ts
 create mode 100644 packages/client/src/directives/particle.ts
 create mode 100644 packages/client/src/directives/size.ts
 create mode 100644 packages/client/src/directives/sticky-container.ts
 create mode 100644 packages/client/src/directives/tooltip.ts
 create mode 100644 packages/client/src/directives/user-preview.ts
 create mode 100644 packages/client/src/emojilist.json
 create mode 100644 packages/client/src/events.ts
 create mode 100644 packages/client/src/filters/bytes.ts
 create mode 100644 packages/client/src/filters/note.ts
 create mode 100644 packages/client/src/filters/number.ts
 create mode 100644 packages/client/src/filters/user.ts
 create mode 100644 packages/client/src/i18n.ts
 create mode 100644 packages/client/src/init.ts
 create mode 100644 packages/client/src/instance.ts
 create mode 100644 packages/client/src/menu.ts
 create mode 100644 packages/client/src/os.ts
 create mode 100644 packages/client/src/pages/_error_.vue
 create mode 100644 packages/client/src/pages/_loading_.vue
 create mode 100644 packages/client/src/pages/about-misskey.vue
 create mode 100644 packages/client/src/pages/about.vue
 create mode 100644 packages/client/src/pages/admin/abuses.vue
 create mode 100644 packages/client/src/pages/admin/ads.vue
 create mode 100644 packages/client/src/pages/admin/announcements.vue
 create mode 100644 packages/client/src/pages/admin/bot-protection.vue
 create mode 100644 packages/client/src/pages/admin/database.vue
 create mode 100644 packages/client/src/pages/admin/email-settings.vue
 create mode 100644 packages/client/src/pages/admin/emoji-edit-dialog.vue
 create mode 100644 packages/client/src/pages/admin/emojis.vue
 create mode 100644 packages/client/src/pages/admin/file-dialog.vue
 create mode 100644 packages/client/src/pages/admin/files-settings.vue
 create mode 100644 packages/client/src/pages/admin/files.vue
 create mode 100644 packages/client/src/pages/admin/index.vue
 create mode 100644 packages/client/src/pages/admin/instance-block.vue
 create mode 100644 packages/client/src/pages/admin/instance.vue
 create mode 100644 packages/client/src/pages/admin/integrations-discord.vue
 create mode 100644 packages/client/src/pages/admin/integrations-github.vue
 create mode 100644 packages/client/src/pages/admin/integrations-twitter.vue
 create mode 100644 packages/client/src/pages/admin/integrations.vue
 create mode 100644 packages/client/src/pages/admin/metrics.vue
 create mode 100644 packages/client/src/pages/admin/object-storage.vue
 create mode 100644 packages/client/src/pages/admin/other-settings.vue
 create mode 100644 packages/client/src/pages/admin/overview.vue
 create mode 100644 packages/client/src/pages/admin/proxy-account.vue
 create mode 100644 packages/client/src/pages/admin/queue.chart.vue
 create mode 100644 packages/client/src/pages/admin/queue.vue
 create mode 100644 packages/client/src/pages/admin/relays.vue
 create mode 100644 packages/client/src/pages/admin/security.vue
 create mode 100644 packages/client/src/pages/admin/service-worker.vue
 create mode 100644 packages/client/src/pages/admin/settings.vue
 create mode 100644 packages/client/src/pages/admin/users.vue
 create mode 100644 packages/client/src/pages/advanced-theme-editor.vue
 create mode 100644 packages/client/src/pages/announcements.vue
 create mode 100644 packages/client/src/pages/antenna-timeline.vue
 create mode 100644 packages/client/src/pages/api-console.vue
 create mode 100644 packages/client/src/pages/auth.form.vue
 create mode 100644 packages/client/src/pages/auth.vue
 create mode 100644 packages/client/src/pages/channel-editor.vue
 create mode 100644 packages/client/src/pages/channel.vue
 create mode 100644 packages/client/src/pages/channels.vue
 create mode 100644 packages/client/src/pages/clip.vue
 create mode 100644 packages/client/src/pages/drive.vue
 create mode 100644 packages/client/src/pages/emojis.category.vue
 create mode 100644 packages/client/src/pages/emojis.emoji.vue
 create mode 100644 packages/client/src/pages/emojis.vue
 create mode 100644 packages/client/src/pages/explore.vue
 create mode 100644 packages/client/src/pages/favorites.vue
 create mode 100644 packages/client/src/pages/featured.vue
 create mode 100644 packages/client/src/pages/federation.vue
 create mode 100644 packages/client/src/pages/follow-requests.vue
 create mode 100644 packages/client/src/pages/follow.vue
 create mode 100644 packages/client/src/pages/gallery/edit.vue
 create mode 100644 packages/client/src/pages/gallery/index.vue
 create mode 100644 packages/client/src/pages/gallery/post.vue
 create mode 100644 packages/client/src/pages/instance-info.vue
 create mode 100644 packages/client/src/pages/mentions.vue
 create mode 100644 packages/client/src/pages/messages.vue
 create mode 100644 packages/client/src/pages/messaging/index.vue
 create mode 100644 packages/client/src/pages/messaging/messaging-room.form.vue
 create mode 100644 packages/client/src/pages/messaging/messaging-room.message.vue
 create mode 100644 packages/client/src/pages/messaging/messaging-room.vue
 create mode 100644 packages/client/src/pages/mfm-cheat-sheet.vue
 create mode 100644 packages/client/src/pages/miauth.vue
 create mode 100644 packages/client/src/pages/my-antennas/create.vue
 create mode 100644 packages/client/src/pages/my-antennas/edit.vue
 create mode 100644 packages/client/src/pages/my-antennas/editor.vue
 create mode 100644 packages/client/src/pages/my-antennas/index.vue
 create mode 100644 packages/client/src/pages/my-clips/index.vue
 create mode 100644 packages/client/src/pages/my-groups/group.vue
 create mode 100644 packages/client/src/pages/my-groups/index.vue
 create mode 100644 packages/client/src/pages/my-lists/index.vue
 create mode 100644 packages/client/src/pages/my-lists/list.vue
 create mode 100644 packages/client/src/pages/not-found.vue
 create mode 100644 packages/client/src/pages/note.vue
 create mode 100644 packages/client/src/pages/notifications.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.button.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.canvas.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.counter.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.if.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.image.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.note.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.number-input.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.post.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.section.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.switch.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.text-input.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.text.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.textarea-input.vue
 create mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.textarea.vue
 create mode 100644 packages/client/src/pages/page-editor/page-editor.blocks.vue
 create mode 100644 packages/client/src/pages/page-editor/page-editor.container.vue
 create mode 100644 packages/client/src/pages/page-editor/page-editor.script-block.vue
 create mode 100644 packages/client/src/pages/page-editor/page-editor.vue
 create mode 100644 packages/client/src/pages/page.vue
 create mode 100644 packages/client/src/pages/pages.vue
 create mode 100644 packages/client/src/pages/preview.vue
 create mode 100644 packages/client/src/pages/reset-password.vue
 create mode 100644 packages/client/src/pages/reversi/game.board.vue
 create mode 100644 packages/client/src/pages/reversi/game.setting.vue
 create mode 100644 packages/client/src/pages/reversi/game.vue
 create mode 100644 packages/client/src/pages/reversi/index.vue
 create mode 100644 packages/client/src/pages/room/preview.vue
 create mode 100644 packages/client/src/pages/room/room.vue
 create mode 100644 packages/client/src/pages/scratchpad.vue
 create mode 100644 packages/client/src/pages/search.vue
 create mode 100644 packages/client/src/pages/settings/2fa.vue
 create mode 100644 packages/client/src/pages/settings/account-info.vue
 create mode 100644 packages/client/src/pages/settings/accounts.vue
 create mode 100644 packages/client/src/pages/settings/api.vue
 create mode 100644 packages/client/src/pages/settings/apps.vue
 create mode 100644 packages/client/src/pages/settings/custom-css.vue
 create mode 100644 packages/client/src/pages/settings/deck.vue
 create mode 100644 packages/client/src/pages/settings/delete-account.vue
 create mode 100644 packages/client/src/pages/settings/drive.vue
 create mode 100644 packages/client/src/pages/settings/email-address.vue
 create mode 100644 packages/client/src/pages/settings/email-notification.vue
 create mode 100644 packages/client/src/pages/settings/email.vue
 create mode 100644 packages/client/src/pages/settings/experimental-features.vue
 create mode 100644 packages/client/src/pages/settings/general.vue
 create mode 100644 packages/client/src/pages/settings/import-export.vue
 create mode 100644 packages/client/src/pages/settings/index.vue
 create mode 100644 packages/client/src/pages/settings/integration.vue
 create mode 100644 packages/client/src/pages/settings/menu.vue
 create mode 100644 packages/client/src/pages/settings/mute-block.vue
 create mode 100644 packages/client/src/pages/settings/notifications.vue
 create mode 100644 packages/client/src/pages/settings/other.vue
 create mode 100644 packages/client/src/pages/settings/plugin.install.vue
 create mode 100644 packages/client/src/pages/settings/plugin.manage.vue
 create mode 100644 packages/client/src/pages/settings/plugin.vue
 create mode 100644 packages/client/src/pages/settings/privacy.vue
 create mode 100644 packages/client/src/pages/settings/profile.vue
 create mode 100644 packages/client/src/pages/settings/reaction.vue
 create mode 100644 packages/client/src/pages/settings/registry.keys.vue
 create mode 100644 packages/client/src/pages/settings/registry.value.vue
 create mode 100644 packages/client/src/pages/settings/registry.vue
 create mode 100644 packages/client/src/pages/settings/security.vue
 create mode 100644 packages/client/src/pages/settings/sounds.vue
 create mode 100644 packages/client/src/pages/settings/theme.install.vue
 create mode 100644 packages/client/src/pages/settings/theme.manage.vue
 create mode 100644 packages/client/src/pages/settings/theme.vue
 create mode 100644 packages/client/src/pages/settings/update.vue
 create mode 100644 packages/client/src/pages/settings/word-mute.vue
 create mode 100644 packages/client/src/pages/share.vue
 create mode 100644 packages/client/src/pages/signup-complete.vue
 create mode 100644 packages/client/src/pages/tag.vue
 create mode 100644 packages/client/src/pages/test.vue
 create mode 100644 packages/client/src/pages/theme-editor.vue
 create mode 100644 packages/client/src/pages/timeline.tutorial.vue
 create mode 100644 packages/client/src/pages/timeline.vue
 create mode 100644 packages/client/src/pages/user-ap-info.vue
 create mode 100644 packages/client/src/pages/user-info.vue
 create mode 100644 packages/client/src/pages/user-list-timeline.vue
 create mode 100644 packages/client/src/pages/user/clips.vue
 create mode 100644 packages/client/src/pages/user/follow-list.vue
 create mode 100644 packages/client/src/pages/user/gallery.vue
 create mode 100644 packages/client/src/pages/user/index.activity.vue
 create mode 100644 packages/client/src/pages/user/index.photos.vue
 create mode 100644 packages/client/src/pages/user/index.timeline.vue
 create mode 100644 packages/client/src/pages/user/index.vue
 create mode 100644 packages/client/src/pages/user/pages.vue
 create mode 100644 packages/client/src/pages/user/reactions.vue
 create mode 100644 packages/client/src/pages/v.vue
 create mode 100644 packages/client/src/pages/welcome.entrance.a.vue
 create mode 100644 packages/client/src/pages/welcome.entrance.b.vue
 create mode 100644 packages/client/src/pages/welcome.entrance.c.vue
 create mode 100644 packages/client/src/pages/welcome.setup.vue
 create mode 100644 packages/client/src/pages/welcome.timeline.vue
 create mode 100644 packages/client/src/pages/welcome.vue
 create mode 100644 packages/client/src/pizzax.ts
 create mode 100644 packages/client/src/plugin.ts
 create mode 100644 packages/client/src/router.ts
 create mode 100644 packages/client/src/scripts/2fa.ts
 create mode 100644 packages/client/src/scripts/aiscript/api.ts
 create mode 100644 packages/client/src/scripts/array.ts
 create mode 100644 packages/client/src/scripts/autocomplete.ts
 create mode 100644 packages/client/src/scripts/check-word-mute.ts
 create mode 100644 packages/client/src/scripts/collect-page-vars.ts
 create mode 100644 packages/client/src/scripts/contains.ts
 create mode 100644 packages/client/src/scripts/copy-to-clipboard.ts
 create mode 100644 packages/client/src/scripts/emojilist.ts
 create mode 100644 packages/client/src/scripts/extract-avg-color-from-blurhash.ts
 create mode 100644 packages/client/src/scripts/extract-mentions.ts
 create mode 100644 packages/client/src/scripts/extract-url-from-mfm.ts
 create mode 100644 packages/client/src/scripts/focus.ts
 create mode 100644 packages/client/src/scripts/form.ts
 create mode 100644 packages/client/src/scripts/format-time-string.ts
 create mode 100644 packages/client/src/scripts/games/reversi/core.ts
 create mode 100644 packages/client/src/scripts/games/reversi/maps.ts
 create mode 100644 packages/client/src/scripts/games/reversi/package.json
 create mode 100644 packages/client/src/scripts/games/reversi/tsconfig.json
 create mode 100644 packages/client/src/scripts/gen-search-query.ts
 create mode 100644 packages/client/src/scripts/get-account-from-id.ts
 create mode 100644 packages/client/src/scripts/get-md5.ts
 create mode 100644 packages/client/src/scripts/get-note-summary.ts
 create mode 100644 packages/client/src/scripts/get-static-image-url.ts
 create mode 100644 packages/client/src/scripts/get-user-menu.ts
 create mode 100644 packages/client/src/scripts/hotkey.ts
 create mode 100644 packages/client/src/scripts/hpml/block.ts
 create mode 100644 packages/client/src/scripts/hpml/evaluator.ts
 create mode 100644 packages/client/src/scripts/hpml/expr.ts
 create mode 100644 packages/client/src/scripts/hpml/index.ts
 create mode 100644 packages/client/src/scripts/hpml/lib.ts
 create mode 100644 packages/client/src/scripts/hpml/type-checker.ts
 create mode 100644 packages/client/src/scripts/i18n.ts
 create mode 100644 packages/client/src/scripts/idb-proxy.ts
 create mode 100644 packages/client/src/scripts/initialize-sw.ts
 create mode 100644 packages/client/src/scripts/is-device-darkmode.ts
 create mode 100644 packages/client/src/scripts/is-device-touch.ts
 create mode 100644 packages/client/src/scripts/is-mobile.ts
 create mode 100644 packages/client/src/scripts/keycode.ts
 create mode 100644 packages/client/src/scripts/loading.ts
 create mode 100644 packages/client/src/scripts/login-id.ts
 create mode 100644 packages/client/src/scripts/lookup-user.ts
 create mode 100644 packages/client/src/scripts/mfm-tags.ts
 create mode 100644 packages/client/src/scripts/paging.ts
 create mode 100644 packages/client/src/scripts/physics.ts
 create mode 100644 packages/client/src/scripts/please-login.ts
 create mode 100644 packages/client/src/scripts/popout.ts
 create mode 100644 packages/client/src/scripts/reaction-picker.ts
 create mode 100644 packages/client/src/scripts/room/furniture.ts
 create mode 100644 packages/client/src/scripts/room/furnitures.json5
 create mode 100644 packages/client/src/scripts/room/room.ts
 create mode 100644 packages/client/src/scripts/scroll.ts
 create mode 100644 packages/client/src/scripts/search.ts
 create mode 100644 packages/client/src/scripts/select-file.ts
 create mode 100644 packages/client/src/scripts/show-suspended-dialog.ts
 create mode 100644 packages/client/src/scripts/sound.ts
 create mode 100644 packages/client/src/scripts/sticky-sidebar.ts
 create mode 100644 packages/client/src/scripts/theme-editor.ts
 create mode 100644 packages/client/src/scripts/theme.ts
 create mode 100644 packages/client/src/scripts/time.ts
 create mode 100644 packages/client/src/scripts/twemoji-base.ts
 create mode 100644 packages/client/src/scripts/unison-reload.ts
 create mode 100644 packages/client/src/scripts/url.ts
 create mode 100644 packages/client/src/store.ts
 create mode 100644 packages/client/src/style.scss
 create mode 100644 packages/client/src/sw/compose-notification.ts
 create mode 100644 packages/client/src/sw/sw.ts
 create mode 100644 packages/client/src/symbols.ts
 create mode 100644 packages/client/src/theme-store.ts
 create mode 100644 packages/client/src/themes/_dark.json5
 create mode 100644 packages/client/src/themes/_light.json5
 create mode 100644 packages/client/src/themes/d-astro.json5
 create mode 100644 packages/client/src/themes/d-black.json5
 create mode 100644 packages/client/src/themes/d-botanical.json5
 create mode 100644 packages/client/src/themes/d-dark.json5
 create mode 100644 packages/client/src/themes/d-future.json5
 create mode 100644 packages/client/src/themes/d-persimmon.json5
 create mode 100644 packages/client/src/themes/d-pumpkin.json5
 create mode 100644 packages/client/src/themes/l-apricot.json5
 create mode 100644 packages/client/src/themes/l-light.json5
 create mode 100644 packages/client/src/themes/l-rainy.json5
 create mode 100644 packages/client/src/themes/l-sushi.json5
 create mode 100644 packages/client/src/themes/l-vivid.json5
 create mode 100644 packages/client/src/ui/_common_/common.vue
 create mode 100644 packages/client/src/ui/_common_/sidebar.vue
 create mode 100644 packages/client/src/ui/_common_/stream-indicator.vue
 create mode 100644 packages/client/src/ui/_common_/upload.vue
 create mode 100644 packages/client/src/ui/chat/date-separated-list.vue
 create mode 100644 packages/client/src/ui/chat/header-clock.vue
 create mode 100644 packages/client/src/ui/chat/index.vue
 create mode 100644 packages/client/src/ui/chat/note-header.vue
 create mode 100644 packages/client/src/ui/chat/note-preview.vue
 create mode 100644 packages/client/src/ui/chat/note.sub.vue
 create mode 100644 packages/client/src/ui/chat/note.vue
 create mode 100644 packages/client/src/ui/chat/notes.vue
 create mode 100644 packages/client/src/ui/chat/pages/channel.vue
 create mode 100644 packages/client/src/ui/chat/pages/timeline.vue
 create mode 100644 packages/client/src/ui/chat/post-form.vue
 create mode 100644 packages/client/src/ui/chat/side.vue
 create mode 100644 packages/client/src/ui/chat/store.ts
 create mode 100644 packages/client/src/ui/chat/sub-note-content.vue
 create mode 100644 packages/client/src/ui/chat/widgets.vue
 create mode 100644 packages/client/src/ui/classic.header.vue
 create mode 100644 packages/client/src/ui/classic.side.vue
 create mode 100644 packages/client/src/ui/classic.sidebar.vue
 create mode 100644 packages/client/src/ui/classic.vue
 create mode 100644 packages/client/src/ui/classic.widgets.vue
 create mode 100644 packages/client/src/ui/deck.vue
 create mode 100644 packages/client/src/ui/deck/antenna-column.vue
 create mode 100644 packages/client/src/ui/deck/column-core.vue
 create mode 100644 packages/client/src/ui/deck/column.vue
 create mode 100644 packages/client/src/ui/deck/deck-store.ts
 create mode 100644 packages/client/src/ui/deck/direct-column.vue
 create mode 100644 packages/client/src/ui/deck/list-column.vue
 create mode 100644 packages/client/src/ui/deck/main-column.vue
 create mode 100644 packages/client/src/ui/deck/mentions-column.vue
 create mode 100644 packages/client/src/ui/deck/notifications-column.vue
 create mode 100644 packages/client/src/ui/deck/tl-column.vue
 create mode 100644 packages/client/src/ui/deck/widgets-column.vue
 create mode 100644 packages/client/src/ui/desktop.vue
 create mode 100644 packages/client/src/ui/universal.vue
 create mode 100644 packages/client/src/ui/universal.widgets.vue
 create mode 100644 packages/client/src/ui/visitor.vue
 create mode 100644 packages/client/src/ui/visitor/a.vue
 create mode 100644 packages/client/src/ui/visitor/b.vue
 create mode 100644 packages/client/src/ui/visitor/header.vue
 create mode 100644 packages/client/src/ui/visitor/kanban.vue
 create mode 100644 packages/client/src/ui/zen.vue
 create mode 100644 packages/client/src/widgets/activity.calendar.vue
 create mode 100644 packages/client/src/widgets/activity.chart.vue
 create mode 100644 packages/client/src/widgets/activity.vue
 create mode 100644 packages/client/src/widgets/aichan.vue
 create mode 100644 packages/client/src/widgets/aiscript.vue
 create mode 100644 packages/client/src/widgets/button.vue
 create mode 100644 packages/client/src/widgets/calendar.vue
 create mode 100644 packages/client/src/widgets/clock.vue
 create mode 100644 packages/client/src/widgets/define.ts
 create mode 100644 packages/client/src/widgets/digital-clock.vue
 create mode 100644 packages/client/src/widgets/federation.vue
 create mode 100644 packages/client/src/widgets/index.ts
 create mode 100644 packages/client/src/widgets/job-queue.vue
 create mode 100644 packages/client/src/widgets/memo.vue
 create mode 100644 packages/client/src/widgets/notifications.vue
 create mode 100644 packages/client/src/widgets/online-users.vue
 create mode 100644 packages/client/src/widgets/photos.vue
 create mode 100644 packages/client/src/widgets/post-form.vue
 create mode 100644 packages/client/src/widgets/rss.vue
 create mode 100644 packages/client/src/widgets/server-metric/cpu-mem.vue
 create mode 100644 packages/client/src/widgets/server-metric/cpu.vue
 create mode 100644 packages/client/src/widgets/server-metric/disk.vue
 create mode 100644 packages/client/src/widgets/server-metric/index.vue
 create mode 100644 packages/client/src/widgets/server-metric/mem.vue
 create mode 100644 packages/client/src/widgets/server-metric/net.vue
 create mode 100644 packages/client/src/widgets/server-metric/pie.vue
 create mode 100644 packages/client/src/widgets/slideshow.vue
 create mode 100644 packages/client/src/widgets/timeline.vue
 create mode 100644 packages/client/src/widgets/trends.vue
 create mode 100644 packages/client/tsconfig.json
 create mode 100644 packages/client/webpack.config.js
 create mode 100644 packages/client/yarn.lock
 create mode 100644 packages/meta.json
 delete mode 100644 src/.eslintrc
 delete mode 100644 src/@types/hcaptcha.d.ts
 delete mode 100644 src/@types/http-signature.d.ts
 delete mode 100644 src/@types/jsrsasign.d.ts
 delete mode 100644 src/@types/koa-json-body.d.ts
 delete mode 100644 src/@types/koa-slow.d.ts
 delete mode 100644 src/@types/langmap.d.ts
 delete mode 100644 src/@types/ms.d.ts
 delete mode 100644 src/@types/os-utils.d.ts
 delete mode 100644 src/@types/package.json.d.ts
 delete mode 100644 src/@types/probe-image-size.d.ts
 delete mode 100644 src/boot/index.ts
 delete mode 100644 src/boot/master.ts
 delete mode 100644 src/boot/worker.ts
 delete mode 100644 src/client/.eslintrc
 delete mode 100644 src/client/@types/global.d.ts
 delete mode 100644 src/client/@types/vue.d.ts
 delete mode 100644 src/client/account.ts
 delete mode 100644 src/client/components/abuse-report-window.vue
 delete mode 100644 src/client/components/analog-clock.vue
 delete mode 100644 src/client/components/autocomplete.vue
 delete mode 100644 src/client/components/avatars.vue
 delete mode 100644 src/client/components/captcha.vue
 delete mode 100644 src/client/components/channel-follow-button.vue
 delete mode 100644 src/client/components/channel-preview.vue
 delete mode 100644 src/client/components/chart.vue
 delete mode 100644 src/client/components/code-core.vue
 delete mode 100644 src/client/components/code.vue
 delete mode 100644 src/client/components/cw-button.vue
 delete mode 100644 src/client/components/date-separated-list.vue
 delete mode 100644 src/client/components/debobigego/base.vue
 delete mode 100644 src/client/components/debobigego/button.vue
 delete mode 100644 src/client/components/debobigego/debobigego.scss
 delete mode 100644 src/client/components/debobigego/group.vue
 delete mode 100644 src/client/components/debobigego/info.vue
 delete mode 100644 src/client/components/debobigego/input.vue
 delete mode 100644 src/client/components/debobigego/key-value-view.vue
 delete mode 100644 src/client/components/debobigego/link.vue
 delete mode 100644 src/client/components/debobigego/object-view.vue
 delete mode 100644 src/client/components/debobigego/pagination.vue
 delete mode 100644 src/client/components/debobigego/radios.vue
 delete mode 100644 src/client/components/debobigego/range.vue
 delete mode 100644 src/client/components/debobigego/select.vue
 delete mode 100644 src/client/components/debobigego/suspense.vue
 delete mode 100644 src/client/components/debobigego/switch.vue
 delete mode 100644 src/client/components/debobigego/textarea.vue
 delete mode 100644 src/client/components/debobigego/tuple.vue
 delete mode 100644 src/client/components/dialog.vue
 delete mode 100644 src/client/components/drive-file-thumbnail.vue
 delete mode 100644 src/client/components/drive-select-dialog.vue
 delete mode 100644 src/client/components/drive-window.vue
 delete mode 100644 src/client/components/drive.file.vue
 delete mode 100644 src/client/components/drive.folder.vue
 delete mode 100644 src/client/components/drive.nav-folder.vue
 delete mode 100644 src/client/components/drive.vue
 delete mode 100644 src/client/components/emoji-picker-dialog.vue
 delete mode 100644 src/client/components/emoji-picker-window.vue
 delete mode 100644 src/client/components/emoji-picker.section.vue
 delete mode 100644 src/client/components/emoji-picker.vue
 delete mode 100644 src/client/components/featured-photos.vue
 delete mode 100644 src/client/components/file-type-icon.vue
 delete mode 100644 src/client/components/follow-button.vue
 delete mode 100644 src/client/components/forgot-password.vue
 delete mode 100644 src/client/components/form-dialog.vue
 delete mode 100644 src/client/components/form/input.vue
 delete mode 100644 src/client/components/form/radio.vue
 delete mode 100644 src/client/components/form/radios.vue
 delete mode 100644 src/client/components/form/range.vue
 delete mode 100644 src/client/components/form/section.vue
 delete mode 100644 src/client/components/form/select.vue
 delete mode 100644 src/client/components/form/slot.vue
 delete mode 100644 src/client/components/form/switch.vue
 delete mode 100644 src/client/components/form/textarea.vue
 delete mode 100644 src/client/components/formula-core.vue
 delete mode 100644 src/client/components/formula.vue
 delete mode 100644 src/client/components/gallery-post-preview.vue
 delete mode 100644 src/client/components/global/a.vue
 delete mode 100644 src/client/components/global/acct.vue
 delete mode 100644 src/client/components/global/ad.vue
 delete mode 100644 src/client/components/global/avatar.vue
 delete mode 100644 src/client/components/global/ellipsis.vue
 delete mode 100644 src/client/components/global/emoji.vue
 delete mode 100644 src/client/components/global/error.vue
 delete mode 100644 src/client/components/global/header.vue
 delete mode 100644 src/client/components/global/i18n.ts
 delete mode 100644 src/client/components/global/loading.vue
 delete mode 100644 src/client/components/global/misskey-flavored-markdown.vue
 delete mode 100644 src/client/components/global/spacer.vue
 delete mode 100644 src/client/components/global/sticky-container.vue
 delete mode 100644 src/client/components/global/time.vue
 delete mode 100644 src/client/components/global/url.vue
 delete mode 100644 src/client/components/global/user-name.vue
 delete mode 100644 src/client/components/google.vue
 delete mode 100644 src/client/components/image-viewer.vue
 delete mode 100644 src/client/components/img-with-blurhash.vue
 delete mode 100644 src/client/components/index.ts
 delete mode 100644 src/client/components/instance-stats.vue
 delete mode 100644 src/client/components/instance-ticker.vue
 delete mode 100644 src/client/components/launch-pad.vue
 delete mode 100644 src/client/components/link.vue
 delete mode 100644 src/client/components/media-banner.vue
 delete mode 100644 src/client/components/media-caption.vue
 delete mode 100644 src/client/components/media-image.vue
 delete mode 100644 src/client/components/media-list.vue
 delete mode 100644 src/client/components/media-video.vue
 delete mode 100644 src/client/components/mention.vue
 delete mode 100644 src/client/components/mfm.ts
 delete mode 100644 src/client/components/mini-chart.vue
 delete mode 100644 src/client/components/modal-page-window.vue
 delete mode 100644 src/client/components/note-detailed.vue
 delete mode 100644 src/client/components/note-header.vue
 delete mode 100644 src/client/components/note-preview.vue
 delete mode 100644 src/client/components/note-simple.vue
 delete mode 100644 src/client/components/note.sub.vue
 delete mode 100644 src/client/components/note.vue
 delete mode 100644 src/client/components/notes.vue
 delete mode 100644 src/client/components/notification-setting-window.vue
 delete mode 100644 src/client/components/notification.vue
 delete mode 100644 src/client/components/notifications.vue
 delete mode 100644 src/client/components/number-diff.vue
 delete mode 100644 src/client/components/page-preview.vue
 delete mode 100644 src/client/components/page-window.vue
 delete mode 100644 src/client/components/page/page.block.vue
 delete mode 100644 src/client/components/page/page.button.vue
 delete mode 100644 src/client/components/page/page.canvas.vue
 delete mode 100644 src/client/components/page/page.counter.vue
 delete mode 100644 src/client/components/page/page.if.vue
 delete mode 100644 src/client/components/page/page.image.vue
 delete mode 100644 src/client/components/page/page.note.vue
 delete mode 100644 src/client/components/page/page.number-input.vue
 delete mode 100644 src/client/components/page/page.post.vue
 delete mode 100644 src/client/components/page/page.radio-button.vue
 delete mode 100644 src/client/components/page/page.section.vue
 delete mode 100644 src/client/components/page/page.switch.vue
 delete mode 100644 src/client/components/page/page.text-input.vue
 delete mode 100644 src/client/components/page/page.text.vue
 delete mode 100644 src/client/components/page/page.textarea-input.vue
 delete mode 100644 src/client/components/page/page.textarea.vue
 delete mode 100644 src/client/components/page/page.vue
 delete mode 100644 src/client/components/particle.vue
 delete mode 100644 src/client/components/poll-editor.vue
 delete mode 100644 src/client/components/poll.vue
 delete mode 100644 src/client/components/post-form-attaches.vue
 delete mode 100644 src/client/components/post-form-dialog.vue
 delete mode 100644 src/client/components/post-form.vue
 delete mode 100644 src/client/components/queue-chart.vue
 delete mode 100644 src/client/components/reaction-icon.vue
 delete mode 100644 src/client/components/reaction-tooltip.vue
 delete mode 100644 src/client/components/reactions-viewer.details.vue
 delete mode 100644 src/client/components/reactions-viewer.reaction.vue
 delete mode 100644 src/client/components/reactions-viewer.vue
 delete mode 100644 src/client/components/remote-caution.vue
 delete mode 100644 src/client/components/sample.vue
 delete mode 100644 src/client/components/signin-dialog.vue
 delete mode 100755 src/client/components/signin.vue
 delete mode 100644 src/client/components/signup-dialog.vue
 delete mode 100644 src/client/components/signup.vue
 delete mode 100644 src/client/components/sparkle.vue
 delete mode 100644 src/client/components/sub-note-content.vue
 delete mode 100644 src/client/components/tab.vue
 delete mode 100644 src/client/components/taskmanager.api-window.vue
 delete mode 100644 src/client/components/taskmanager.vue
 delete mode 100644 src/client/components/timeline.vue
 delete mode 100644 src/client/components/toast.vue
 delete mode 100644 src/client/components/token-generate-window.vue
 delete mode 100644 src/client/components/ui/button.vue
 delete mode 100644 src/client/components/ui/container.vue
 delete mode 100644 src/client/components/ui/context-menu.vue
 delete mode 100644 src/client/components/ui/folder.vue
 delete mode 100644 src/client/components/ui/hr.vue
 delete mode 100644 src/client/components/ui/info.vue
 delete mode 100644 src/client/components/ui/menu.vue
 delete mode 100644 src/client/components/ui/modal-window.vue
 delete mode 100644 src/client/components/ui/modal.vue
 delete mode 100644 src/client/components/ui/pagination.vue
 delete mode 100644 src/client/components/ui/popup-menu.vue
 delete mode 100644 src/client/components/ui/popup.vue
 delete mode 100644 src/client/components/ui/super-menu.vue
 delete mode 100644 src/client/components/ui/tooltip.vue
 delete mode 100644 src/client/components/ui/window.vue
 delete mode 100644 src/client/components/updated.vue
 delete mode 100644 src/client/components/url-preview-popup.vue
 delete mode 100644 src/client/components/url-preview.vue
 delete mode 100644 src/client/components/user-info.vue
 delete mode 100644 src/client/components/user-list.vue
 delete mode 100644 src/client/components/user-online-indicator.vue
 delete mode 100644 src/client/components/user-preview.vue
 delete mode 100644 src/client/components/user-select-dialog.vue
 delete mode 100644 src/client/components/users-dialog.vue
 delete mode 100644 src/client/components/visibility-picker.vue
 delete mode 100644 src/client/components/waiting-dialog.vue
 delete mode 100644 src/client/components/widgets.vue
 delete mode 100644 src/client/config.ts
 delete mode 100644 src/client/directives/anim.ts
 delete mode 100644 src/client/directives/appear.ts
 delete mode 100644 src/client/directives/click-anime.ts
 delete mode 100644 src/client/directives/follow-append.ts
 delete mode 100644 src/client/directives/get-size.ts
 delete mode 100644 src/client/directives/hotkey.ts
 delete mode 100644 src/client/directives/index.ts
 delete mode 100644 src/client/directives/particle.ts
 delete mode 100644 src/client/directives/size.ts
 delete mode 100644 src/client/directives/sticky-container.ts
 delete mode 100644 src/client/directives/tooltip.ts
 delete mode 100644 src/client/directives/user-preview.ts
 delete mode 100644 src/client/events.ts
 delete mode 100644 src/client/filters/bytes.ts
 delete mode 100644 src/client/filters/note.ts
 delete mode 100644 src/client/filters/number.ts
 delete mode 100644 src/client/filters/user.ts
 delete mode 100644 src/client/i18n.ts
 delete mode 100644 src/client/init.ts
 delete mode 100644 src/client/instance.ts
 delete mode 100644 src/client/menu.ts
 delete mode 100644 src/client/os.ts
 delete mode 100644 src/client/pages/_error_.vue
 delete mode 100644 src/client/pages/_loading_.vue
 delete mode 100644 src/client/pages/about-misskey.vue
 delete mode 100644 src/client/pages/about.vue
 delete mode 100644 src/client/pages/admin/abuses.vue
 delete mode 100644 src/client/pages/admin/ads.vue
 delete mode 100644 src/client/pages/admin/announcements.vue
 delete mode 100644 src/client/pages/admin/bot-protection.vue
 delete mode 100644 src/client/pages/admin/database.vue
 delete mode 100644 src/client/pages/admin/email-settings.vue
 delete mode 100644 src/client/pages/admin/emoji-edit-dialog.vue
 delete mode 100644 src/client/pages/admin/emojis.vue
 delete mode 100644 src/client/pages/admin/file-dialog.vue
 delete mode 100644 src/client/pages/admin/files-settings.vue
 delete mode 100644 src/client/pages/admin/files.vue
 delete mode 100644 src/client/pages/admin/index.vue
 delete mode 100644 src/client/pages/admin/instance-block.vue
 delete mode 100644 src/client/pages/admin/instance.vue
 delete mode 100644 src/client/pages/admin/integrations-discord.vue
 delete mode 100644 src/client/pages/admin/integrations-github.vue
 delete mode 100644 src/client/pages/admin/integrations-twitter.vue
 delete mode 100644 src/client/pages/admin/integrations.vue
 delete mode 100644 src/client/pages/admin/metrics.vue
 delete mode 100644 src/client/pages/admin/object-storage.vue
 delete mode 100644 src/client/pages/admin/other-settings.vue
 delete mode 100644 src/client/pages/admin/overview.vue
 delete mode 100644 src/client/pages/admin/proxy-account.vue
 delete mode 100644 src/client/pages/admin/queue.chart.vue
 delete mode 100644 src/client/pages/admin/queue.vue
 delete mode 100644 src/client/pages/admin/relays.vue
 delete mode 100644 src/client/pages/admin/security.vue
 delete mode 100644 src/client/pages/admin/service-worker.vue
 delete mode 100644 src/client/pages/admin/settings.vue
 delete mode 100644 src/client/pages/admin/users.vue
 delete mode 100644 src/client/pages/advanced-theme-editor.vue
 delete mode 100644 src/client/pages/announcements.vue
 delete mode 100644 src/client/pages/antenna-timeline.vue
 delete mode 100644 src/client/pages/api-console.vue
 delete mode 100644 src/client/pages/auth.form.vue
 delete mode 100755 src/client/pages/auth.vue
 delete mode 100644 src/client/pages/channel-editor.vue
 delete mode 100644 src/client/pages/channel.vue
 delete mode 100644 src/client/pages/channels.vue
 delete mode 100644 src/client/pages/clip.vue
 delete mode 100644 src/client/pages/drive.vue
 delete mode 100644 src/client/pages/emojis.category.vue
 delete mode 100644 src/client/pages/emojis.emoji.vue
 delete mode 100644 src/client/pages/emojis.vue
 delete mode 100644 src/client/pages/explore.vue
 delete mode 100644 src/client/pages/favorites.vue
 delete mode 100644 src/client/pages/featured.vue
 delete mode 100644 src/client/pages/federation.vue
 delete mode 100644 src/client/pages/follow-requests.vue
 delete mode 100644 src/client/pages/follow.vue
 delete mode 100644 src/client/pages/gallery/edit.vue
 delete mode 100644 src/client/pages/gallery/index.vue
 delete mode 100644 src/client/pages/gallery/post.vue
 delete mode 100644 src/client/pages/instance-info.vue
 delete mode 100644 src/client/pages/mentions.vue
 delete mode 100644 src/client/pages/messages.vue
 delete mode 100644 src/client/pages/messaging/index.vue
 delete mode 100644 src/client/pages/messaging/messaging-room.form.vue
 delete mode 100644 src/client/pages/messaging/messaging-room.message.vue
 delete mode 100644 src/client/pages/messaging/messaging-room.vue
 delete mode 100644 src/client/pages/mfm-cheat-sheet.vue
 delete mode 100644 src/client/pages/miauth.vue
 delete mode 100644 src/client/pages/my-antennas/create.vue
 delete mode 100644 src/client/pages/my-antennas/edit.vue
 delete mode 100644 src/client/pages/my-antennas/editor.vue
 delete mode 100644 src/client/pages/my-antennas/index.vue
 delete mode 100644 src/client/pages/my-clips/index.vue
 delete mode 100644 src/client/pages/my-groups/group.vue
 delete mode 100644 src/client/pages/my-groups/index.vue
 delete mode 100644 src/client/pages/my-lists/index.vue
 delete mode 100644 src/client/pages/my-lists/list.vue
 delete mode 100644 src/client/pages/not-found.vue
 delete mode 100644 src/client/pages/note.vue
 delete mode 100644 src/client/pages/notifications.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.button.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.canvas.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.counter.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.if.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.image.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.note.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.number-input.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.post.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.radio-button.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.section.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.switch.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.text-input.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.text.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
 delete mode 100644 src/client/pages/page-editor/els/page-editor.el.textarea.vue
 delete mode 100644 src/client/pages/page-editor/page-editor.blocks.vue
 delete mode 100644 src/client/pages/page-editor/page-editor.container.vue
 delete mode 100644 src/client/pages/page-editor/page-editor.script-block.vue
 delete mode 100644 src/client/pages/page-editor/page-editor.vue
 delete mode 100644 src/client/pages/page.vue
 delete mode 100644 src/client/pages/pages.vue
 delete mode 100644 src/client/pages/preview.vue
 delete mode 100644 src/client/pages/reset-password.vue
 delete mode 100644 src/client/pages/reversi/game.board.vue
 delete mode 100644 src/client/pages/reversi/game.setting.vue
 delete mode 100644 src/client/pages/reversi/game.vue
 delete mode 100644 src/client/pages/reversi/index.vue
 delete mode 100644 src/client/pages/room/preview.vue
 delete mode 100644 src/client/pages/room/room.vue
 delete mode 100644 src/client/pages/scratchpad.vue
 delete mode 100644 src/client/pages/search.vue
 delete mode 100644 src/client/pages/settings/2fa.vue
 delete mode 100644 src/client/pages/settings/account-info.vue
 delete mode 100644 src/client/pages/settings/accounts.vue
 delete mode 100644 src/client/pages/settings/api.vue
 delete mode 100644 src/client/pages/settings/apps.vue
 delete mode 100644 src/client/pages/settings/custom-css.vue
 delete mode 100644 src/client/pages/settings/deck.vue
 delete mode 100644 src/client/pages/settings/delete-account.vue
 delete mode 100644 src/client/pages/settings/drive.vue
 delete mode 100644 src/client/pages/settings/email-address.vue
 delete mode 100644 src/client/pages/settings/email-notification.vue
 delete mode 100644 src/client/pages/settings/email.vue
 delete mode 100644 src/client/pages/settings/experimental-features.vue
 delete mode 100644 src/client/pages/settings/general.vue
 delete mode 100644 src/client/pages/settings/import-export.vue
 delete mode 100644 src/client/pages/settings/index.vue
 delete mode 100644 src/client/pages/settings/integration.vue
 delete mode 100644 src/client/pages/settings/menu.vue
 delete mode 100644 src/client/pages/settings/mute-block.vue
 delete mode 100644 src/client/pages/settings/notifications.vue
 delete mode 100644 src/client/pages/settings/other.vue
 delete mode 100644 src/client/pages/settings/plugin.install.vue
 delete mode 100644 src/client/pages/settings/plugin.manage.vue
 delete mode 100644 src/client/pages/settings/plugin.vue
 delete mode 100644 src/client/pages/settings/privacy.vue
 delete mode 100644 src/client/pages/settings/profile.vue
 delete mode 100644 src/client/pages/settings/reaction.vue
 delete mode 100644 src/client/pages/settings/registry.keys.vue
 delete mode 100644 src/client/pages/settings/registry.value.vue
 delete mode 100644 src/client/pages/settings/registry.vue
 delete mode 100644 src/client/pages/settings/security.vue
 delete mode 100644 src/client/pages/settings/sounds.vue
 delete mode 100644 src/client/pages/settings/theme.install.vue
 delete mode 100644 src/client/pages/settings/theme.manage.vue
 delete mode 100644 src/client/pages/settings/theme.vue
 delete mode 100644 src/client/pages/settings/update.vue
 delete mode 100644 src/client/pages/settings/word-mute.vue
 delete mode 100644 src/client/pages/share.vue
 delete mode 100644 src/client/pages/signup-complete.vue
 delete mode 100644 src/client/pages/tag.vue
 delete mode 100644 src/client/pages/test.vue
 delete mode 100644 src/client/pages/theme-editor.vue
 delete mode 100644 src/client/pages/timeline.tutorial.vue
 delete mode 100644 src/client/pages/timeline.vue
 delete mode 100644 src/client/pages/user-ap-info.vue
 delete mode 100644 src/client/pages/user-info.vue
 delete mode 100644 src/client/pages/user-list-timeline.vue
 delete mode 100644 src/client/pages/user/clips.vue
 delete mode 100644 src/client/pages/user/follow-list.vue
 delete mode 100644 src/client/pages/user/gallery.vue
 delete mode 100644 src/client/pages/user/index.activity.vue
 delete mode 100644 src/client/pages/user/index.photos.vue
 delete mode 100644 src/client/pages/user/index.timeline.vue
 delete mode 100644 src/client/pages/user/index.vue
 delete mode 100644 src/client/pages/user/pages.vue
 delete mode 100644 src/client/pages/user/reactions.vue
 delete mode 100644 src/client/pages/v.vue
 delete mode 100644 src/client/pages/welcome.entrance.a.vue
 delete mode 100644 src/client/pages/welcome.entrance.b.vue
 delete mode 100644 src/client/pages/welcome.entrance.c.vue
 delete mode 100644 src/client/pages/welcome.setup.vue
 delete mode 100644 src/client/pages/welcome.timeline.vue
 delete mode 100644 src/client/pages/welcome.vue
 delete mode 100644 src/client/pizzax.ts
 delete mode 100644 src/client/plugin.ts
 delete mode 100644 src/client/router.ts
 delete mode 100644 src/client/scripts/2fa.ts
 delete mode 100644 src/client/scripts/aiscript/api.ts
 delete mode 100644 src/client/scripts/autocomplete.ts
 delete mode 100644 src/client/scripts/check-word-mute.ts
 delete mode 100644 src/client/scripts/collect-page-vars.ts
 delete mode 100644 src/client/scripts/contains.ts
 delete mode 100644 src/client/scripts/copy-to-clipboard.ts
 delete mode 100644 src/client/scripts/extract-avg-color-from-blurhash.ts
 delete mode 100644 src/client/scripts/focus.ts
 delete mode 100644 src/client/scripts/form.ts
 delete mode 100644 src/client/scripts/gen-search-query.ts
 delete mode 100644 src/client/scripts/get-account-from-id.ts
 delete mode 100644 src/client/scripts/get-md5.ts
 delete mode 100644 src/client/scripts/get-static-image-url.ts
 delete mode 100644 src/client/scripts/get-user-menu.ts
 delete mode 100644 src/client/scripts/hotkey.ts
 delete mode 100644 src/client/scripts/hpml/block.ts
 delete mode 100644 src/client/scripts/hpml/evaluator.ts
 delete mode 100644 src/client/scripts/hpml/expr.ts
 delete mode 100644 src/client/scripts/hpml/index.ts
 delete mode 100644 src/client/scripts/hpml/lib.ts
 delete mode 100644 src/client/scripts/hpml/type-checker.ts
 delete mode 100644 src/client/scripts/idb-proxy.ts
 delete mode 100644 src/client/scripts/initialize-sw.ts
 delete mode 100644 src/client/scripts/is-device-darkmode.ts
 delete mode 100644 src/client/scripts/is-device-touch.ts
 delete mode 100644 src/client/scripts/is-mobile.ts
 delete mode 100644 src/client/scripts/keycode.ts
 delete mode 100644 src/client/scripts/loading.ts
 delete mode 100644 src/client/scripts/login-id.ts
 delete mode 100644 src/client/scripts/lookup-user.ts
 delete mode 100644 src/client/scripts/paging.ts
 delete mode 100644 src/client/scripts/physics.ts
 delete mode 100644 src/client/scripts/please-login.ts
 delete mode 100644 src/client/scripts/popout.ts
 delete mode 100644 src/client/scripts/reaction-picker.ts
 delete mode 100644 src/client/scripts/room/furniture.ts
 delete mode 100644 src/client/scripts/room/furnitures.json5
 delete mode 100644 src/client/scripts/room/room.ts
 delete mode 100644 src/client/scripts/scroll.ts
 delete mode 100644 src/client/scripts/search.ts
 delete mode 100644 src/client/scripts/select-file.ts
 delete mode 100644 src/client/scripts/show-suspended-dialog.ts
 delete mode 100644 src/client/scripts/sound.ts
 delete mode 100644 src/client/scripts/sticky-sidebar.ts
 delete mode 100644 src/client/scripts/theme-editor.ts
 delete mode 100644 src/client/scripts/theme.ts
 delete mode 100644 src/client/scripts/unison-reload.ts
 delete mode 100644 src/client/store.ts
 delete mode 100644 src/client/style.scss
 delete mode 100644 src/client/sw/compose-notification.ts
 delete mode 100644 src/client/sw/sw.ts
 delete mode 100644 src/client/symbols.ts
 delete mode 100644 src/client/theme-store.ts
 delete mode 100644 src/client/themes/_dark.json5
 delete mode 100644 src/client/themes/_light.json5
 delete mode 100644 src/client/themes/d-astro.json5
 delete mode 100644 src/client/themes/d-black.json5
 delete mode 100644 src/client/themes/d-botanical.json5
 delete mode 100644 src/client/themes/d-dark.json5
 delete mode 100644 src/client/themes/d-future.json5
 delete mode 100644 src/client/themes/d-persimmon.json5
 delete mode 100644 src/client/themes/d-pumpkin.json5
 delete mode 100644 src/client/themes/l-apricot.json5
 delete mode 100644 src/client/themes/l-light.json5
 delete mode 100644 src/client/themes/l-rainy.json5
 delete mode 100644 src/client/themes/l-sushi.json5
 delete mode 100644 src/client/themes/l-vivid.json5
 delete mode 100644 src/client/tsconfig.json
 delete mode 100644 src/client/ui/_common_/common.vue
 delete mode 100644 src/client/ui/_common_/sidebar.vue
 delete mode 100644 src/client/ui/_common_/stream-indicator.vue
 delete mode 100644 src/client/ui/_common_/upload.vue
 delete mode 100644 src/client/ui/chat/date-separated-list.vue
 delete mode 100644 src/client/ui/chat/header-clock.vue
 delete mode 100644 src/client/ui/chat/index.vue
 delete mode 100644 src/client/ui/chat/note-header.vue
 delete mode 100644 src/client/ui/chat/note-preview.vue
 delete mode 100644 src/client/ui/chat/note.sub.vue
 delete mode 100644 src/client/ui/chat/note.vue
 delete mode 100644 src/client/ui/chat/notes.vue
 delete mode 100644 src/client/ui/chat/pages/channel.vue
 delete mode 100644 src/client/ui/chat/pages/timeline.vue
 delete mode 100644 src/client/ui/chat/post-form.vue
 delete mode 100644 src/client/ui/chat/side.vue
 delete mode 100644 src/client/ui/chat/store.ts
 delete mode 100644 src/client/ui/chat/sub-note-content.vue
 delete mode 100644 src/client/ui/chat/widgets.vue
 delete mode 100644 src/client/ui/classic.header.vue
 delete mode 100644 src/client/ui/classic.side.vue
 delete mode 100644 src/client/ui/classic.sidebar.vue
 delete mode 100644 src/client/ui/classic.vue
 delete mode 100644 src/client/ui/classic.widgets.vue
 delete mode 100644 src/client/ui/deck.vue
 delete mode 100644 src/client/ui/deck/antenna-column.vue
 delete mode 100644 src/client/ui/deck/column-core.vue
 delete mode 100644 src/client/ui/deck/column.vue
 delete mode 100644 src/client/ui/deck/deck-store.ts
 delete mode 100644 src/client/ui/deck/direct-column.vue
 delete mode 100644 src/client/ui/deck/list-column.vue
 delete mode 100644 src/client/ui/deck/main-column.vue
 delete mode 100644 src/client/ui/deck/mentions-column.vue
 delete mode 100644 src/client/ui/deck/notifications-column.vue
 delete mode 100644 src/client/ui/deck/tl-column.vue
 delete mode 100644 src/client/ui/deck/widgets-column.vue
 delete mode 100644 src/client/ui/desktop.vue
 delete mode 100644 src/client/ui/universal.vue
 delete mode 100644 src/client/ui/universal.widgets.vue
 delete mode 100644 src/client/ui/visitor.vue
 delete mode 100644 src/client/ui/visitor/a.vue
 delete mode 100644 src/client/ui/visitor/b.vue
 delete mode 100644 src/client/ui/visitor/header.vue
 delete mode 100644 src/client/ui/visitor/kanban.vue
 delete mode 100644 src/client/ui/zen.vue
 delete mode 100644 src/client/widgets/activity.calendar.vue
 delete mode 100644 src/client/widgets/activity.chart.vue
 delete mode 100644 src/client/widgets/activity.vue
 delete mode 100644 src/client/widgets/aichan.vue
 delete mode 100644 src/client/widgets/aiscript.vue
 delete mode 100644 src/client/widgets/button.vue
 delete mode 100644 src/client/widgets/calendar.vue
 delete mode 100644 src/client/widgets/clock.vue
 delete mode 100644 src/client/widgets/define.ts
 delete mode 100644 src/client/widgets/digital-clock.vue
 delete mode 100644 src/client/widgets/federation.vue
 delete mode 100644 src/client/widgets/index.ts
 delete mode 100644 src/client/widgets/job-queue.vue
 delete mode 100644 src/client/widgets/memo.vue
 delete mode 100644 src/client/widgets/notifications.vue
 delete mode 100644 src/client/widgets/online-users.vue
 delete mode 100644 src/client/widgets/photos.vue
 delete mode 100644 src/client/widgets/post-form.vue
 delete mode 100644 src/client/widgets/rss.vue
 delete mode 100644 src/client/widgets/server-metric/cpu-mem.vue
 delete mode 100644 src/client/widgets/server-metric/cpu.vue
 delete mode 100644 src/client/widgets/server-metric/disk.vue
 delete mode 100644 src/client/widgets/server-metric/index.vue
 delete mode 100644 src/client/widgets/server-metric/mem.vue
 delete mode 100644 src/client/widgets/server-metric/net.vue
 delete mode 100644 src/client/widgets/server-metric/pie.vue
 delete mode 100644 src/client/widgets/slideshow.vue
 delete mode 100644 src/client/widgets/timeline.vue
 delete mode 100644 src/client/widgets/trends.vue
 delete mode 100644 src/config/index.ts
 delete mode 100644 src/config/load.ts
 delete mode 100644 src/config/types.ts
 delete mode 100644 src/const.ts
 delete mode 100644 src/daemons/janitor.ts
 delete mode 100644 src/daemons/queue-stats.ts
 delete mode 100644 src/daemons/server-stats.ts
 delete mode 100644 src/db/elasticsearch.ts
 delete mode 100644 src/db/logger.ts
 delete mode 100644 src/db/postgre.ts
 delete mode 100644 src/db/redis.ts
 delete mode 100644 src/emojilist.json
 delete mode 100644 src/env.ts
 delete mode 100644 src/games/reversi/core.ts
 delete mode 100644 src/games/reversi/maps.ts
 delete mode 100644 src/games/reversi/package.json
 delete mode 100644 src/games/reversi/tsconfig.json
 delete mode 100644 src/global.d.ts
 delete mode 100644 src/index.ts
 delete mode 100644 src/meta.json
 delete mode 100644 src/mfm/fn-name-list.ts
 delete mode 100644 src/mfm/from-html.ts
 delete mode 100644 src/mfm/to-html.ts
 delete mode 100644 src/misc/acct.ts
 delete mode 100644 src/misc/antenna-cache.ts
 delete mode 100644 src/misc/api-permissions.ts
 delete mode 100644 src/misc/app-lock.ts
 delete mode 100644 src/misc/before-shutdown.ts
 delete mode 100644 src/misc/cache.ts
 delete mode 100644 src/misc/cafy-id.ts
 delete mode 100644 src/misc/captcha.ts
 delete mode 100644 src/misc/check-hit-antenna.ts
 delete mode 100644 src/misc/check-word-mute.ts
 delete mode 100644 src/misc/content-disposition.ts
 delete mode 100644 src/misc/convert-host.ts
 delete mode 100644 src/misc/count-same-renotes.ts
 delete mode 100644 src/misc/create-temp.ts
 delete mode 100644 src/misc/detect-url-mime.ts
 delete mode 100644 src/misc/download-text-file.ts
 delete mode 100644 src/misc/download-url.ts
 delete mode 100644 src/misc/emoji-regex.ts
 delete mode 100644 src/misc/emojilist.ts
 delete mode 100644 src/misc/extract-custom-emojis-from-mfm.ts
 delete mode 100644 src/misc/extract-hashtags.ts
 delete mode 100644 src/misc/extract-mentions.ts
 delete mode 100644 src/misc/extract-url-from-mfm.ts
 delete mode 100644 src/misc/fetch-meta.ts
 delete mode 100644 src/misc/fetch-proxy-account.ts
 delete mode 100644 src/misc/fetch.ts
 delete mode 100644 src/misc/format-time-string.ts
 delete mode 100644 src/misc/gen-avatar.ts
 delete mode 100644 src/misc/gen-id.ts
 delete mode 100644 src/misc/gen-key-pair.ts
 delete mode 100644 src/misc/get-file-info.ts
 delete mode 100644 src/misc/get-note-summary.ts
 delete mode 100644 src/misc/get-reaction-emoji.ts
 delete mode 100644 src/misc/get-user-name.ts
 delete mode 100644 src/misc/hard-limits.ts
 delete mode 100644 src/misc/i18n.ts
 delete mode 100644 src/misc/id/aid.ts
 delete mode 100644 src/misc/id/meid.ts
 delete mode 100644 src/misc/id/meidg.ts
 delete mode 100644 src/misc/id/object-id.ts
 delete mode 100644 src/misc/identifiable-error.ts
 delete mode 100644 src/misc/is-blocker-user-related.ts
 delete mode 100644 src/misc/is-duplicate-key-value-error.ts
 delete mode 100644 src/misc/is-muted-user-related.ts
 delete mode 100644 src/misc/is-quote.ts
 delete mode 100644 src/misc/keypair-store.ts
 delete mode 100644 src/misc/license.ts
 delete mode 100644 src/misc/normalize-for-search.ts
 delete mode 100644 src/misc/nyaize.ts
 delete mode 100644 src/misc/populate-emojis.ts
 delete mode 100644 src/misc/reaction-lib.ts
 delete mode 100644 src/misc/safe-for-sql.ts
 delete mode 100644 src/misc/schema.ts
 delete mode 100644 src/misc/secure-rndstr.ts
 delete mode 100644 src/misc/show-machine-info.ts
 delete mode 100644 src/misc/simple-schema.ts
 delete mode 100644 src/misc/truncate.ts
 delete mode 100644 src/misc/twemoji-base.ts
 delete mode 100644 src/models/entities/abuse-user-report.ts
 delete mode 100644 src/models/entities/access-token.ts
 delete mode 100644 src/models/entities/ad.ts
 delete mode 100644 src/models/entities/announcement-read.ts
 delete mode 100644 src/models/entities/announcement.ts
 delete mode 100644 src/models/entities/antenna-note.ts
 delete mode 100644 src/models/entities/antenna.ts
 delete mode 100644 src/models/entities/app.ts
 delete mode 100644 src/models/entities/attestation-challenge.ts
 delete mode 100644 src/models/entities/auth-session.ts
 delete mode 100644 src/models/entities/blocking.ts
 delete mode 100644 src/models/entities/channel-following.ts
 delete mode 100644 src/models/entities/channel-note-pining.ts
 delete mode 100644 src/models/entities/channel.ts
 delete mode 100644 src/models/entities/clip-note.ts
 delete mode 100644 src/models/entities/clip.ts
 delete mode 100644 src/models/entities/drive-file.ts
 delete mode 100644 src/models/entities/drive-folder.ts
 delete mode 100644 src/models/entities/emoji.ts
 delete mode 100644 src/models/entities/follow-request.ts
 delete mode 100644 src/models/entities/following.ts
 delete mode 100644 src/models/entities/gallery-like.ts
 delete mode 100644 src/models/entities/gallery-post.ts
 delete mode 100644 src/models/entities/games/reversi/game.ts
 delete mode 100644 src/models/entities/games/reversi/matching.ts
 delete mode 100644 src/models/entities/hashtag.ts
 delete mode 100644 src/models/entities/instance.ts
 delete mode 100644 src/models/entities/messaging-message.ts
 delete mode 100644 src/models/entities/meta.ts
 delete mode 100644 src/models/entities/moderation-log.ts
 delete mode 100644 src/models/entities/muted-note.ts
 delete mode 100644 src/models/entities/muting.ts
 delete mode 100644 src/models/entities/note-favorite.ts
 delete mode 100644 src/models/entities/note-reaction.ts
 delete mode 100644 src/models/entities/note-thread-muting.ts
 delete mode 100644 src/models/entities/note-unread.ts
 delete mode 100644 src/models/entities/note-watching.ts
 delete mode 100644 src/models/entities/note.ts
 delete mode 100644 src/models/entities/notification.ts
 delete mode 100644 src/models/entities/page-like.ts
 delete mode 100644 src/models/entities/page.ts
 delete mode 100644 src/models/entities/password-reset-request.ts
 delete mode 100644 src/models/entities/poll-vote.ts
 delete mode 100644 src/models/entities/poll.ts
 delete mode 100644 src/models/entities/promo-note.ts
 delete mode 100644 src/models/entities/promo-read.ts
 delete mode 100644 src/models/entities/registration-tickets.ts
 delete mode 100644 src/models/entities/registry-item.ts
 delete mode 100644 src/models/entities/relay.ts
 delete mode 100644 src/models/entities/signin.ts
 delete mode 100644 src/models/entities/sw-subscription.ts
 delete mode 100644 src/models/entities/used-username.ts
 delete mode 100644 src/models/entities/user-group-invitation.ts
 delete mode 100644 src/models/entities/user-group-joining.ts
 delete mode 100644 src/models/entities/user-group.ts
 delete mode 100644 src/models/entities/user-keypair.ts
 delete mode 100644 src/models/entities/user-list-joining.ts
 delete mode 100644 src/models/entities/user-list.ts
 delete mode 100644 src/models/entities/user-note-pining.ts
 delete mode 100644 src/models/entities/user-pending.ts
 delete mode 100644 src/models/entities/user-profile.ts
 delete mode 100644 src/models/entities/user-publickey.ts
 delete mode 100644 src/models/entities/user-security-key.ts
 delete mode 100644 src/models/entities/user.ts
 delete mode 100644 src/models/id.ts
 delete mode 100644 src/models/index.ts
 delete mode 100644 src/models/repositories/abuse-user-report.ts
 delete mode 100644 src/models/repositories/antenna.ts
 delete mode 100644 src/models/repositories/app.ts
 delete mode 100644 src/models/repositories/auth-session.ts
 delete mode 100644 src/models/repositories/blocking.ts
 delete mode 100644 src/models/repositories/channel.ts
 delete mode 100644 src/models/repositories/clip.ts
 delete mode 100644 src/models/repositories/drive-file.ts
 delete mode 100644 src/models/repositories/drive-folder.ts
 delete mode 100644 src/models/repositories/emoji.ts
 delete mode 100644 src/models/repositories/federation-instance.ts
 delete mode 100644 src/models/repositories/follow-request.ts
 delete mode 100644 src/models/repositories/following.ts
 delete mode 100644 src/models/repositories/gallery-like.ts
 delete mode 100644 src/models/repositories/gallery-post.ts
 delete mode 100644 src/models/repositories/games/reversi/game.ts
 delete mode 100644 src/models/repositories/games/reversi/matching.ts
 delete mode 100644 src/models/repositories/hashtag.ts
 delete mode 100644 src/models/repositories/messaging-message.ts
 delete mode 100644 src/models/repositories/moderation-logs.ts
 delete mode 100644 src/models/repositories/muting.ts
 delete mode 100644 src/models/repositories/note-favorite.ts
 delete mode 100644 src/models/repositories/note-reaction.ts
 delete mode 100644 src/models/repositories/note.ts
 delete mode 100644 src/models/repositories/notification.ts
 delete mode 100644 src/models/repositories/page-like.ts
 delete mode 100644 src/models/repositories/page.ts
 delete mode 100644 src/models/repositories/queue.ts
 delete mode 100644 src/models/repositories/relay.ts
 delete mode 100644 src/models/repositories/signin.ts
 delete mode 100644 src/models/repositories/user-group-invitation.ts
 delete mode 100644 src/models/repositories/user-group.ts
 delete mode 100644 src/models/repositories/user-list.ts
 delete mode 100644 src/models/repositories/user.ts
 delete mode 100644 src/prelude/README.md
 delete mode 100644 src/prelude/array.ts
 delete mode 100644 src/prelude/await-all.ts
 delete mode 100644 src/prelude/math.ts
 delete mode 100644 src/prelude/maybe.ts
 delete mode 100644 src/prelude/relation.ts
 delete mode 100644 src/prelude/string.ts
 delete mode 100644 src/prelude/symbol.ts
 delete mode 100644 src/prelude/time.ts
 delete mode 100644 src/prelude/url.ts
 delete mode 100644 src/prelude/xml.ts
 delete mode 100644 src/queue/get-job-info.ts
 delete mode 100644 src/queue/index.ts
 delete mode 100644 src/queue/initialize.ts
 delete mode 100644 src/queue/logger.ts
 delete mode 100644 src/queue/processors/db/delete-account.ts
 delete mode 100644 src/queue/processors/db/delete-drive-files.ts
 delete mode 100644 src/queue/processors/db/export-blocking.ts
 delete mode 100644 src/queue/processors/db/export-following.ts
 delete mode 100644 src/queue/processors/db/export-mute.ts
 delete mode 100644 src/queue/processors/db/export-notes.ts
 delete mode 100644 src/queue/processors/db/export-user-lists.ts
 delete mode 100644 src/queue/processors/db/import-blocking.ts
 delete mode 100644 src/queue/processors/db/import-following.ts
 delete mode 100644 src/queue/processors/db/import-muting.ts
 delete mode 100644 src/queue/processors/db/import-user-lists.ts
 delete mode 100644 src/queue/processors/db/index.ts
 delete mode 100644 src/queue/processors/deliver.ts
 delete mode 100644 src/queue/processors/inbox.ts
 delete mode 100644 src/queue/processors/object-storage/clean-remote-files.ts
 delete mode 100644 src/queue/processors/object-storage/delete-file.ts
 delete mode 100644 src/queue/processors/object-storage/index.ts
 delete mode 100644 src/queue/processors/system/index.ts
 delete mode 100644 src/queue/processors/system/resync-charts.ts
 delete mode 100644 src/queue/queues.ts
 delete mode 100644 src/queue/types.ts
 delete mode 100644 src/remote/activitypub/ap-request.ts
 delete mode 100644 src/remote/activitypub/audience.ts
 delete mode 100644 src/remote/activitypub/db-resolver.ts
 delete mode 100644 src/remote/activitypub/deliver-manager.ts
 delete mode 100644 src/remote/activitypub/kernel/accept/follow.ts
 delete mode 100644 src/remote/activitypub/kernel/accept/index.ts
 delete mode 100644 src/remote/activitypub/kernel/add/index.ts
 delete mode 100644 src/remote/activitypub/kernel/announce/index.ts
 delete mode 100644 src/remote/activitypub/kernel/announce/note.ts
 delete mode 100644 src/remote/activitypub/kernel/block/index.ts
 delete mode 100644 src/remote/activitypub/kernel/create/index.ts
 delete mode 100644 src/remote/activitypub/kernel/create/note.ts
 delete mode 100644 src/remote/activitypub/kernel/delete/actor.ts
 delete mode 100644 src/remote/activitypub/kernel/delete/index.ts
 delete mode 100644 src/remote/activitypub/kernel/delete/note.ts
 delete mode 100644 src/remote/activitypub/kernel/flag/index.ts
 delete mode 100644 src/remote/activitypub/kernel/follow.ts
 delete mode 100644 src/remote/activitypub/kernel/index.ts
 delete mode 100644 src/remote/activitypub/kernel/like.ts
 delete mode 100644 src/remote/activitypub/kernel/move/index.ts
 delete mode 100644 src/remote/activitypub/kernel/read.ts
 delete mode 100644 src/remote/activitypub/kernel/reject/follow.ts
 delete mode 100644 src/remote/activitypub/kernel/reject/index.ts
 delete mode 100644 src/remote/activitypub/kernel/remove/index.ts
 delete mode 100644 src/remote/activitypub/kernel/undo/announce.ts
 delete mode 100644 src/remote/activitypub/kernel/undo/block.ts
 delete mode 100644 src/remote/activitypub/kernel/undo/follow.ts
 delete mode 100644 src/remote/activitypub/kernel/undo/index.ts
 delete mode 100644 src/remote/activitypub/kernel/undo/like.ts
 delete mode 100644 src/remote/activitypub/kernel/update/index.ts
 delete mode 100644 src/remote/activitypub/logger.ts
 delete mode 100644 src/remote/activitypub/misc/contexts.ts
 delete mode 100644 src/remote/activitypub/misc/get-note-html.ts
 delete mode 100644 src/remote/activitypub/misc/html-to-mfm.ts
 delete mode 100644 src/remote/activitypub/misc/ld-signature.ts
 delete mode 100644 src/remote/activitypub/models/icon.ts
 delete mode 100644 src/remote/activitypub/models/identifier.ts
 delete mode 100644 src/remote/activitypub/models/image.ts
 delete mode 100644 src/remote/activitypub/models/mention.ts
 delete mode 100644 src/remote/activitypub/models/note.ts
 delete mode 100644 src/remote/activitypub/models/person.ts
 delete mode 100644 src/remote/activitypub/models/question.ts
 delete mode 100644 src/remote/activitypub/models/tag.ts
 delete mode 100644 src/remote/activitypub/perform.ts
 delete mode 100644 src/remote/activitypub/renderer/accept.ts
 delete mode 100644 src/remote/activitypub/renderer/add.ts
 delete mode 100644 src/remote/activitypub/renderer/announce.ts
 delete mode 100644 src/remote/activitypub/renderer/block.ts
 delete mode 100644 src/remote/activitypub/renderer/create.ts
 delete mode 100644 src/remote/activitypub/renderer/delete.ts
 delete mode 100644 src/remote/activitypub/renderer/document.ts
 delete mode 100644 src/remote/activitypub/renderer/emoji.ts
 delete mode 100644 src/remote/activitypub/renderer/follow-relay.ts
 delete mode 100644 src/remote/activitypub/renderer/follow-user.ts
 delete mode 100644 src/remote/activitypub/renderer/follow.ts
 delete mode 100644 src/remote/activitypub/renderer/hashtag.ts
 delete mode 100644 src/remote/activitypub/renderer/image.ts
 delete mode 100644 src/remote/activitypub/renderer/index.ts
 delete mode 100644 src/remote/activitypub/renderer/key.ts
 delete mode 100644 src/remote/activitypub/renderer/like.ts
 delete mode 100644 src/remote/activitypub/renderer/mention.ts
 delete mode 100644 src/remote/activitypub/renderer/note.ts
 delete mode 100644 src/remote/activitypub/renderer/ordered-collection-page.ts
 delete mode 100644 src/remote/activitypub/renderer/ordered-collection.ts
 delete mode 100644 src/remote/activitypub/renderer/person.ts
 delete mode 100644 src/remote/activitypub/renderer/question.ts
 delete mode 100644 src/remote/activitypub/renderer/read.ts
 delete mode 100644 src/remote/activitypub/renderer/reject.ts
 delete mode 100644 src/remote/activitypub/renderer/remove.ts
 delete mode 100644 src/remote/activitypub/renderer/tombstone.ts
 delete mode 100644 src/remote/activitypub/renderer/undo.ts
 delete mode 100644 src/remote/activitypub/renderer/update.ts
 delete mode 100644 src/remote/activitypub/renderer/vote.ts
 delete mode 100644 src/remote/activitypub/request.ts
 delete mode 100644 src/remote/activitypub/resolver.ts
 delete mode 100644 src/remote/activitypub/type.ts
 delete mode 100644 src/remote/logger.ts
 delete mode 100644 src/remote/resolve-user.ts
 delete mode 100644 src/remote/webfinger.ts
 delete mode 100644 src/server/activitypub.ts
 delete mode 100644 src/server/activitypub/featured.ts
 delete mode 100644 src/server/activitypub/followers.ts
 delete mode 100644 src/server/activitypub/following.ts
 delete mode 100644 src/server/activitypub/outbox.ts
 delete mode 100644 src/server/api/2fa.ts
 delete mode 100644 src/server/api/api-handler.ts
 delete mode 100644 src/server/api/authenticate.ts
 delete mode 100644 src/server/api/call.ts
 delete mode 100644 src/server/api/common/generate-block-query.ts
 delete mode 100644 src/server/api/common/generate-channel-query.ts
 delete mode 100644 src/server/api/common/generate-muted-note-query.ts
 delete mode 100644 src/server/api/common/generate-muted-note-thread-query.ts
 delete mode 100644 src/server/api/common/generate-muted-user-query.ts
 delete mode 100644 src/server/api/common/generate-native-user-token.ts
 delete mode 100644 src/server/api/common/generate-replies-query.ts
 delete mode 100644 src/server/api/common/generate-visibility-query.ts
 delete mode 100644 src/server/api/common/getters.ts
 delete mode 100644 src/server/api/common/inject-featured.ts
 delete mode 100644 src/server/api/common/inject-promo.ts
 delete mode 100644 src/server/api/common/is-native-token.ts
 delete mode 100644 src/server/api/common/make-pagination-query.ts
 delete mode 100644 src/server/api/common/read-messaging-message.ts
 delete mode 100644 src/server/api/common/read-notification.ts
 delete mode 100644 src/server/api/common/signin.ts
 delete mode 100644 src/server/api/common/signup.ts
 delete mode 100644 src/server/api/define.ts
 delete mode 100644 src/server/api/endpoints.ts
 delete mode 100644 src/server/api/endpoints/admin/abuse-user-reports.ts
 delete mode 100644 src/server/api/endpoints/admin/accounts/create.ts
 delete mode 100644 src/server/api/endpoints/admin/accounts/delete.ts
 delete mode 100644 src/server/api/endpoints/admin/ad/create.ts
 delete mode 100644 src/server/api/endpoints/admin/ad/delete.ts
 delete mode 100644 src/server/api/endpoints/admin/ad/list.ts
 delete mode 100644 src/server/api/endpoints/admin/ad/update.ts
 delete mode 100644 src/server/api/endpoints/admin/announcements/create.ts
 delete mode 100644 src/server/api/endpoints/admin/announcements/delete.ts
 delete mode 100644 src/server/api/endpoints/admin/announcements/list.ts
 delete mode 100644 src/server/api/endpoints/admin/announcements/update.ts
 delete mode 100644 src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
 delete mode 100644 src/server/api/endpoints/admin/delete-logs.ts
 delete mode 100644 src/server/api/endpoints/admin/drive/clean-remote-files.ts
 delete mode 100644 src/server/api/endpoints/admin/drive/cleanup.ts
 delete mode 100644 src/server/api/endpoints/admin/drive/files.ts
 delete mode 100644 src/server/api/endpoints/admin/drive/show-file.ts
 delete mode 100644 src/server/api/endpoints/admin/emoji/add.ts
 delete mode 100644 src/server/api/endpoints/admin/emoji/copy.ts
 delete mode 100644 src/server/api/endpoints/admin/emoji/list-remote.ts
 delete mode 100644 src/server/api/endpoints/admin/emoji/list.ts
 delete mode 100644 src/server/api/endpoints/admin/emoji/remove.ts
 delete mode 100644 src/server/api/endpoints/admin/emoji/update.ts
 delete mode 100644 src/server/api/endpoints/admin/federation/delete-all-files.ts
 delete mode 100644 src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
 delete mode 100644 src/server/api/endpoints/admin/federation/remove-all-following.ts
 delete mode 100644 src/server/api/endpoints/admin/federation/update-instance.ts
 delete mode 100644 src/server/api/endpoints/admin/get-index-stats.ts
 delete mode 100644 src/server/api/endpoints/admin/get-table-stats.ts
 delete mode 100644 src/server/api/endpoints/admin/invite.ts
 delete mode 100644 src/server/api/endpoints/admin/moderators/add.ts
 delete mode 100644 src/server/api/endpoints/admin/moderators/remove.ts
 delete mode 100644 src/server/api/endpoints/admin/promo/create.ts
 delete mode 100644 src/server/api/endpoints/admin/queue/clear.ts
 delete mode 100644 src/server/api/endpoints/admin/queue/deliver-delayed.ts
 delete mode 100644 src/server/api/endpoints/admin/queue/inbox-delayed.ts
 delete mode 100644 src/server/api/endpoints/admin/queue/jobs.ts
 delete mode 100644 src/server/api/endpoints/admin/queue/stats.ts
 delete mode 100644 src/server/api/endpoints/admin/relays/add.ts
 delete mode 100644 src/server/api/endpoints/admin/relays/list.ts
 delete mode 100644 src/server/api/endpoints/admin/relays/remove.ts
 delete mode 100644 src/server/api/endpoints/admin/reset-password.ts
 delete mode 100644 src/server/api/endpoints/admin/resolve-abuse-user-report.ts
 delete mode 100644 src/server/api/endpoints/admin/resync-chart.ts
 delete mode 100644 src/server/api/endpoints/admin/send-email.ts
 delete mode 100644 src/server/api/endpoints/admin/server-info.ts
 delete mode 100644 src/server/api/endpoints/admin/show-moderation-logs.ts
 delete mode 100644 src/server/api/endpoints/admin/show-user.ts
 delete mode 100644 src/server/api/endpoints/admin/show-users.ts
 delete mode 100644 src/server/api/endpoints/admin/silence-user.ts
 delete mode 100644 src/server/api/endpoints/admin/suspend-user.ts
 delete mode 100644 src/server/api/endpoints/admin/unsilence-user.ts
 delete mode 100644 src/server/api/endpoints/admin/unsuspend-user.ts
 delete mode 100644 src/server/api/endpoints/admin/update-meta.ts
 delete mode 100644 src/server/api/endpoints/admin/vacuum.ts
 delete mode 100644 src/server/api/endpoints/announcements.ts
 delete mode 100644 src/server/api/endpoints/antennas/create.ts
 delete mode 100644 src/server/api/endpoints/antennas/delete.ts
 delete mode 100644 src/server/api/endpoints/antennas/list.ts
 delete mode 100644 src/server/api/endpoints/antennas/notes.ts
 delete mode 100644 src/server/api/endpoints/antennas/show.ts
 delete mode 100644 src/server/api/endpoints/antennas/update.ts
 delete mode 100644 src/server/api/endpoints/ap/get.ts
 delete mode 100644 src/server/api/endpoints/ap/show.ts
 delete mode 100644 src/server/api/endpoints/app/create.ts
 delete mode 100644 src/server/api/endpoints/app/show.ts
 delete mode 100644 src/server/api/endpoints/auth/accept.ts
 delete mode 100644 src/server/api/endpoints/auth/session/generate.ts
 delete mode 100644 src/server/api/endpoints/auth/session/show.ts
 delete mode 100644 src/server/api/endpoints/auth/session/userkey.ts
 delete mode 100644 src/server/api/endpoints/blocking/create.ts
 delete mode 100644 src/server/api/endpoints/blocking/delete.ts
 delete mode 100644 src/server/api/endpoints/blocking/list.ts
 delete mode 100644 src/server/api/endpoints/channels/create.ts
 delete mode 100644 src/server/api/endpoints/channels/featured.ts
 delete mode 100644 src/server/api/endpoints/channels/follow.ts
 delete mode 100644 src/server/api/endpoints/channels/followed.ts
 delete mode 100644 src/server/api/endpoints/channels/owned.ts
 delete mode 100644 src/server/api/endpoints/channels/pin-note.ts
 delete mode 100644 src/server/api/endpoints/channels/show.ts
 delete mode 100644 src/server/api/endpoints/channels/timeline.ts
 delete mode 100644 src/server/api/endpoints/channels/unfollow.ts
 delete mode 100644 src/server/api/endpoints/channels/update.ts
 delete mode 100644 src/server/api/endpoints/charts/active-users.ts
 delete mode 100644 src/server/api/endpoints/charts/drive.ts
 delete mode 100644 src/server/api/endpoints/charts/federation.ts
 delete mode 100644 src/server/api/endpoints/charts/hashtag.ts
 delete mode 100644 src/server/api/endpoints/charts/instance.ts
 delete mode 100644 src/server/api/endpoints/charts/network.ts
 delete mode 100644 src/server/api/endpoints/charts/notes.ts
 delete mode 100644 src/server/api/endpoints/charts/user/drive.ts
 delete mode 100644 src/server/api/endpoints/charts/user/following.ts
 delete mode 100644 src/server/api/endpoints/charts/user/notes.ts
 delete mode 100644 src/server/api/endpoints/charts/user/reactions.ts
 delete mode 100644 src/server/api/endpoints/charts/users.ts
 delete mode 100644 src/server/api/endpoints/clips/add-note.ts
 delete mode 100644 src/server/api/endpoints/clips/create.ts
 delete mode 100644 src/server/api/endpoints/clips/delete.ts
 delete mode 100644 src/server/api/endpoints/clips/list.ts
 delete mode 100644 src/server/api/endpoints/clips/notes.ts
 delete mode 100644 src/server/api/endpoints/clips/show.ts
 delete mode 100644 src/server/api/endpoints/clips/update.ts
 delete mode 100644 src/server/api/endpoints/drive.ts
 delete mode 100644 src/server/api/endpoints/drive/files.ts
 delete mode 100644 src/server/api/endpoints/drive/files/attached-notes.ts
 delete mode 100644 src/server/api/endpoints/drive/files/check-existence.ts
 delete mode 100644 src/server/api/endpoints/drive/files/create.ts
 delete mode 100644 src/server/api/endpoints/drive/files/delete.ts
 delete mode 100644 src/server/api/endpoints/drive/files/find-by-hash.ts
 delete mode 100644 src/server/api/endpoints/drive/files/find.ts
 delete mode 100644 src/server/api/endpoints/drive/files/show.ts
 delete mode 100644 src/server/api/endpoints/drive/files/update.ts
 delete mode 100644 src/server/api/endpoints/drive/files/upload-from-url.ts
 delete mode 100644 src/server/api/endpoints/drive/folders.ts
 delete mode 100644 src/server/api/endpoints/drive/folders/create.ts
 delete mode 100644 src/server/api/endpoints/drive/folders/delete.ts
 delete mode 100644 src/server/api/endpoints/drive/folders/find.ts
 delete mode 100644 src/server/api/endpoints/drive/folders/show.ts
 delete mode 100644 src/server/api/endpoints/drive/folders/update.ts
 delete mode 100644 src/server/api/endpoints/drive/stream.ts
 delete mode 100644 src/server/api/endpoints/email-address/available.ts
 delete mode 100644 src/server/api/endpoints/endpoint.ts
 delete mode 100644 src/server/api/endpoints/endpoints.ts
 delete mode 100644 src/server/api/endpoints/federation/dns.ts
 delete mode 100644 src/server/api/endpoints/federation/followers.ts
 delete mode 100644 src/server/api/endpoints/federation/following.ts
 delete mode 100644 src/server/api/endpoints/federation/instances.ts
 delete mode 100644 src/server/api/endpoints/federation/show-instance.ts
 delete mode 100644 src/server/api/endpoints/federation/update-remote-user.ts
 delete mode 100644 src/server/api/endpoints/federation/users.ts
 delete mode 100644 src/server/api/endpoints/following/create.ts
 delete mode 100644 src/server/api/endpoints/following/delete.ts
 delete mode 100644 src/server/api/endpoints/following/requests/accept.ts
 delete mode 100644 src/server/api/endpoints/following/requests/cancel.ts
 delete mode 100644 src/server/api/endpoints/following/requests/list.ts
 delete mode 100644 src/server/api/endpoints/following/requests/reject.ts
 delete mode 100644 src/server/api/endpoints/gallery/featured.ts
 delete mode 100644 src/server/api/endpoints/gallery/popular.ts
 delete mode 100644 src/server/api/endpoints/gallery/posts.ts
 delete mode 100644 src/server/api/endpoints/gallery/posts/create.ts
 delete mode 100644 src/server/api/endpoints/gallery/posts/delete.ts
 delete mode 100644 src/server/api/endpoints/gallery/posts/like.ts
 delete mode 100644 src/server/api/endpoints/gallery/posts/show.ts
 delete mode 100644 src/server/api/endpoints/gallery/posts/unlike.ts
 delete mode 100644 src/server/api/endpoints/gallery/posts/update.ts
 delete mode 100644 src/server/api/endpoints/games/reversi/games.ts
 delete mode 100644 src/server/api/endpoints/games/reversi/games/show.ts
 delete mode 100644 src/server/api/endpoints/games/reversi/games/surrender.ts
 delete mode 100644 src/server/api/endpoints/games/reversi/invitations.ts
 delete mode 100644 src/server/api/endpoints/games/reversi/match.ts
 delete mode 100644 src/server/api/endpoints/games/reversi/match/cancel.ts
 delete mode 100644 src/server/api/endpoints/get-online-users-count.ts
 delete mode 100644 src/server/api/endpoints/hashtags/list.ts
 delete mode 100644 src/server/api/endpoints/hashtags/search.ts
 delete mode 100644 src/server/api/endpoints/hashtags/show.ts
 delete mode 100644 src/server/api/endpoints/hashtags/trend.ts
 delete mode 100644 src/server/api/endpoints/hashtags/users.ts
 delete mode 100644 src/server/api/endpoints/i.ts
 delete mode 100644 src/server/api/endpoints/i/2fa/done.ts
 delete mode 100644 src/server/api/endpoints/i/2fa/key-done.ts
 delete mode 100644 src/server/api/endpoints/i/2fa/password-less.ts
 delete mode 100644 src/server/api/endpoints/i/2fa/register-key.ts
 delete mode 100644 src/server/api/endpoints/i/2fa/register.ts
 delete mode 100644 src/server/api/endpoints/i/2fa/remove-key.ts
 delete mode 100644 src/server/api/endpoints/i/2fa/unregister.ts
 delete mode 100644 src/server/api/endpoints/i/apps.ts
 delete mode 100644 src/server/api/endpoints/i/authorized-apps.ts
 delete mode 100644 src/server/api/endpoints/i/change-password.ts
 delete mode 100644 src/server/api/endpoints/i/delete-account.ts
 delete mode 100644 src/server/api/endpoints/i/export-blocking.ts
 delete mode 100644 src/server/api/endpoints/i/export-following.ts
 delete mode 100644 src/server/api/endpoints/i/export-mute.ts
 delete mode 100644 src/server/api/endpoints/i/export-notes.ts
 delete mode 100644 src/server/api/endpoints/i/export-user-lists.ts
 delete mode 100644 src/server/api/endpoints/i/favorites.ts
 delete mode 100644 src/server/api/endpoints/i/gallery/likes.ts
 delete mode 100644 src/server/api/endpoints/i/gallery/posts.ts
 delete mode 100644 src/server/api/endpoints/i/get-word-muted-notes-count.ts
 delete mode 100644 src/server/api/endpoints/i/import-blocking.ts
 delete mode 100644 src/server/api/endpoints/i/import-following.ts
 delete mode 100644 src/server/api/endpoints/i/import-muting.ts
 delete mode 100644 src/server/api/endpoints/i/import-user-lists.ts
 delete mode 100644 src/server/api/endpoints/i/notifications.ts
 delete mode 100644 src/server/api/endpoints/i/page-likes.ts
 delete mode 100644 src/server/api/endpoints/i/pages.ts
 delete mode 100644 src/server/api/endpoints/i/pin.ts
 delete mode 100644 src/server/api/endpoints/i/read-all-messaging-messages.ts
 delete mode 100644 src/server/api/endpoints/i/read-all-unread-notes.ts
 delete mode 100644 src/server/api/endpoints/i/read-announcement.ts
 delete mode 100644 src/server/api/endpoints/i/regenerate-token.ts
 delete mode 100644 src/server/api/endpoints/i/registry/get-all.ts
 delete mode 100644 src/server/api/endpoints/i/registry/get-detail.ts
 delete mode 100644 src/server/api/endpoints/i/registry/get.ts
 delete mode 100644 src/server/api/endpoints/i/registry/keys-with-type.ts
 delete mode 100644 src/server/api/endpoints/i/registry/keys.ts
 delete mode 100644 src/server/api/endpoints/i/registry/remove.ts
 delete mode 100644 src/server/api/endpoints/i/registry/scopes.ts
 delete mode 100644 src/server/api/endpoints/i/registry/set.ts
 delete mode 100644 src/server/api/endpoints/i/revoke-token.ts
 delete mode 100644 src/server/api/endpoints/i/signin-history.ts
 delete mode 100644 src/server/api/endpoints/i/unpin.ts
 delete mode 100644 src/server/api/endpoints/i/update-email.ts
 delete mode 100644 src/server/api/endpoints/i/update.ts
 delete mode 100644 src/server/api/endpoints/i/user-group-invites.ts
 delete mode 100644 src/server/api/endpoints/messaging/history.ts
 delete mode 100644 src/server/api/endpoints/messaging/messages.ts
 delete mode 100644 src/server/api/endpoints/messaging/messages/create.ts
 delete mode 100644 src/server/api/endpoints/messaging/messages/delete.ts
 delete mode 100644 src/server/api/endpoints/messaging/messages/read.ts
 delete mode 100644 src/server/api/endpoints/meta.ts
 delete mode 100644 src/server/api/endpoints/miauth/gen-token.ts
 delete mode 100644 src/server/api/endpoints/mute/create.ts
 delete mode 100644 src/server/api/endpoints/mute/delete.ts
 delete mode 100644 src/server/api/endpoints/mute/list.ts
 delete mode 100644 src/server/api/endpoints/my/apps.ts
 delete mode 100644 src/server/api/endpoints/notes.ts
 delete mode 100644 src/server/api/endpoints/notes/children.ts
 delete mode 100644 src/server/api/endpoints/notes/clips.ts
 delete mode 100644 src/server/api/endpoints/notes/conversation.ts
 delete mode 100644 src/server/api/endpoints/notes/create.ts
 delete mode 100644 src/server/api/endpoints/notes/delete.ts
 delete mode 100644 src/server/api/endpoints/notes/favorites/create.ts
 delete mode 100644 src/server/api/endpoints/notes/favorites/delete.ts
 delete mode 100644 src/server/api/endpoints/notes/featured.ts
 delete mode 100644 src/server/api/endpoints/notes/global-timeline.ts
 delete mode 100644 src/server/api/endpoints/notes/hybrid-timeline.ts
 delete mode 100644 src/server/api/endpoints/notes/local-timeline.ts
 delete mode 100644 src/server/api/endpoints/notes/mentions.ts
 delete mode 100644 src/server/api/endpoints/notes/polls/recommendation.ts
 delete mode 100644 src/server/api/endpoints/notes/polls/vote.ts
 delete mode 100644 src/server/api/endpoints/notes/reactions.ts
 delete mode 100644 src/server/api/endpoints/notes/reactions/create.ts
 delete mode 100644 src/server/api/endpoints/notes/reactions/delete.ts
 delete mode 100644 src/server/api/endpoints/notes/renotes.ts
 delete mode 100644 src/server/api/endpoints/notes/replies.ts
 delete mode 100644 src/server/api/endpoints/notes/search-by-tag.ts
 delete mode 100644 src/server/api/endpoints/notes/search.ts
 delete mode 100644 src/server/api/endpoints/notes/show.ts
 delete mode 100644 src/server/api/endpoints/notes/state.ts
 delete mode 100644 src/server/api/endpoints/notes/thread-muting/create.ts
 delete mode 100644 src/server/api/endpoints/notes/thread-muting/delete.ts
 delete mode 100644 src/server/api/endpoints/notes/timeline.ts
 delete mode 100644 src/server/api/endpoints/notes/translate.ts
 delete mode 100644 src/server/api/endpoints/notes/unrenote.ts
 delete mode 100644 src/server/api/endpoints/notes/user-list-timeline.ts
 delete mode 100644 src/server/api/endpoints/notes/watching/create.ts
 delete mode 100644 src/server/api/endpoints/notes/watching/delete.ts
 delete mode 100644 src/server/api/endpoints/notifications/create.ts
 delete mode 100644 src/server/api/endpoints/notifications/mark-all-as-read.ts
 delete mode 100644 src/server/api/endpoints/notifications/read.ts
 delete mode 100644 src/server/api/endpoints/page-push.ts
 delete mode 100644 src/server/api/endpoints/pages/create.ts
 delete mode 100644 src/server/api/endpoints/pages/delete.ts
 delete mode 100644 src/server/api/endpoints/pages/featured.ts
 delete mode 100644 src/server/api/endpoints/pages/like.ts
 delete mode 100644 src/server/api/endpoints/pages/show.ts
 delete mode 100644 src/server/api/endpoints/pages/unlike.ts
 delete mode 100644 src/server/api/endpoints/pages/update.ts
 delete mode 100644 src/server/api/endpoints/ping.ts
 delete mode 100644 src/server/api/endpoints/pinned-users.ts
 delete mode 100644 src/server/api/endpoints/promo/read.ts
 delete mode 100644 src/server/api/endpoints/request-reset-password.ts
 delete mode 100644 src/server/api/endpoints/reset-db.ts
 delete mode 100644 src/server/api/endpoints/reset-password.ts
 delete mode 100644 src/server/api/endpoints/room/show.ts
 delete mode 100644 src/server/api/endpoints/room/update.ts
 delete mode 100644 src/server/api/endpoints/server-info.ts
 delete mode 100644 src/server/api/endpoints/stats.ts
 delete mode 100644 src/server/api/endpoints/sw/register.ts
 delete mode 100644 src/server/api/endpoints/sw/unregister.ts
 delete mode 100644 src/server/api/endpoints/username/available.ts
 delete mode 100644 src/server/api/endpoints/users.ts
 delete mode 100644 src/server/api/endpoints/users/clips.ts
 delete mode 100644 src/server/api/endpoints/users/followers.ts
 delete mode 100644 src/server/api/endpoints/users/following.ts
 delete mode 100644 src/server/api/endpoints/users/gallery/posts.ts
 delete mode 100644 src/server/api/endpoints/users/get-frequently-replied-users.ts
 delete mode 100644 src/server/api/endpoints/users/groups/create.ts
 delete mode 100644 src/server/api/endpoints/users/groups/delete.ts
 delete mode 100644 src/server/api/endpoints/users/groups/invitations/accept.ts
 delete mode 100644 src/server/api/endpoints/users/groups/invitations/reject.ts
 delete mode 100644 src/server/api/endpoints/users/groups/invite.ts
 delete mode 100644 src/server/api/endpoints/users/groups/joined.ts
 delete mode 100644 src/server/api/endpoints/users/groups/leave.ts
 delete mode 100644 src/server/api/endpoints/users/groups/owned.ts
 delete mode 100644 src/server/api/endpoints/users/groups/pull.ts
 delete mode 100644 src/server/api/endpoints/users/groups/show.ts
 delete mode 100644 src/server/api/endpoints/users/groups/transfer.ts
 delete mode 100644 src/server/api/endpoints/users/groups/update.ts
 delete mode 100644 src/server/api/endpoints/users/lists/create.ts
 delete mode 100644 src/server/api/endpoints/users/lists/delete.ts
 delete mode 100644 src/server/api/endpoints/users/lists/list.ts
 delete mode 100644 src/server/api/endpoints/users/lists/pull.ts
 delete mode 100644 src/server/api/endpoints/users/lists/push.ts
 delete mode 100644 src/server/api/endpoints/users/lists/show.ts
 delete mode 100644 src/server/api/endpoints/users/lists/update.ts
 delete mode 100644 src/server/api/endpoints/users/notes.ts
 delete mode 100644 src/server/api/endpoints/users/pages.ts
 delete mode 100644 src/server/api/endpoints/users/reactions.ts
 delete mode 100644 src/server/api/endpoints/users/recommendation.ts
 delete mode 100644 src/server/api/endpoints/users/relation.ts
 delete mode 100644 src/server/api/endpoints/users/report-abuse.ts
 delete mode 100644 src/server/api/endpoints/users/search-by-username-and-host.ts
 delete mode 100644 src/server/api/endpoints/users/search.ts
 delete mode 100644 src/server/api/endpoints/users/show.ts
 delete mode 100644 src/server/api/endpoints/users/stats.ts
 delete mode 100644 src/server/api/error.ts
 delete mode 100644 src/server/api/index.ts
 delete mode 100644 src/server/api/limiter.ts
 delete mode 100644 src/server/api/logger.ts
 delete mode 100644 src/server/api/openapi/description.ts
 delete mode 100644 src/server/api/openapi/errors.ts
 delete mode 100644 src/server/api/openapi/gen-spec.ts
 delete mode 100644 src/server/api/openapi/schemas.ts
 delete mode 100644 src/server/api/private/signin.ts
 delete mode 100644 src/server/api/private/signup-pending.ts
 delete mode 100644 src/server/api/private/signup.ts
 delete mode 100644 src/server/api/service/discord.ts
 delete mode 100644 src/server/api/service/github.ts
 delete mode 100644 src/server/api/service/twitter.ts
 delete mode 100644 src/server/api/stream/channel.ts
 delete mode 100644 src/server/api/stream/channels/admin.ts
 delete mode 100644 src/server/api/stream/channels/antenna.ts
 delete mode 100644 src/server/api/stream/channels/channel.ts
 delete mode 100644 src/server/api/stream/channels/drive.ts
 delete mode 100644 src/server/api/stream/channels/games/reversi-game.ts
 delete mode 100644 src/server/api/stream/channels/games/reversi.ts
 delete mode 100644 src/server/api/stream/channels/global-timeline.ts
 delete mode 100644 src/server/api/stream/channels/hashtag.ts
 delete mode 100644 src/server/api/stream/channels/home-timeline.ts
 delete mode 100644 src/server/api/stream/channels/hybrid-timeline.ts
 delete mode 100644 src/server/api/stream/channels/index.ts
 delete mode 100644 src/server/api/stream/channels/local-timeline.ts
 delete mode 100644 src/server/api/stream/channels/main.ts
 delete mode 100644 src/server/api/stream/channels/messaging-index.ts
 delete mode 100644 src/server/api/stream/channels/messaging.ts
 delete mode 100644 src/server/api/stream/channels/queue-stats.ts
 delete mode 100644 src/server/api/stream/channels/server-stats.ts
 delete mode 100644 src/server/api/stream/channels/user-list.ts
 delete mode 100644 src/server/api/stream/index.ts
 delete mode 100644 src/server/api/stream/types.ts
 delete mode 100644 src/server/api/streaming.ts
 delete mode 100644 src/server/file/assets/bad-egg.png
 delete mode 100644 src/server/file/assets/cache-expired.png
 delete mode 100644 src/server/file/assets/dummy.png
 delete mode 100644 src/server/file/assets/not-an-image.png
 delete mode 100644 src/server/file/assets/thumbnail-not-available.png
 delete mode 100644 src/server/file/assets/tombstone.png
 delete mode 100644 src/server/file/index.ts
 delete mode 100644 src/server/file/send-drive-file.ts
 delete mode 100644 src/server/index.ts
 delete mode 100644 src/server/nodeinfo.ts
 delete mode 100644 src/server/proxy/index.ts
 delete mode 100644 src/server/proxy/proxy-media.ts
 delete mode 100644 src/server/web/bios.css
 delete mode 100644 src/server/web/bios.js
 delete mode 100644 src/server/web/boot.js
 delete mode 100644 src/server/web/cli.css
 delete mode 100644 src/server/web/cli.js
 delete mode 100644 src/server/web/feed.ts
 delete mode 100644 src/server/web/index.ts
 delete mode 100644 src/server/web/manifest.json
 delete mode 100644 src/server/web/manifest.ts
 delete mode 100644 src/server/web/style.css
 delete mode 100644 src/server/web/url-preview.ts
 delete mode 100644 src/server/web/views/base.pug
 delete mode 100644 src/server/web/views/bios.pug
 delete mode 100644 src/server/web/views/channel.pug
 delete mode 100644 src/server/web/views/cli.pug
 delete mode 100644 src/server/web/views/clip.pug
 delete mode 100644 src/server/web/views/flush.pug
 delete mode 100644 src/server/web/views/gallery-post.pug
 delete mode 100644 src/server/web/views/info-card.pug
 delete mode 100644 src/server/web/views/note.pug
 delete mode 100644 src/server/web/views/page.pug
 delete mode 100644 src/server/web/views/user.pug
 delete mode 100644 src/server/well-known.ts
 delete mode 100644 src/services/add-note-to-antenna.ts
 delete mode 100644 src/services/blocking/create.ts
 delete mode 100644 src/services/blocking/delete.ts
 delete mode 100644 src/services/chart/charts/classes/active-users.ts
 delete mode 100644 src/services/chart/charts/classes/drive.ts
 delete mode 100644 src/services/chart/charts/classes/federation.ts
 delete mode 100644 src/services/chart/charts/classes/hashtag.ts
 delete mode 100644 src/services/chart/charts/classes/instance.ts
 delete mode 100644 src/services/chart/charts/classes/network.ts
 delete mode 100644 src/services/chart/charts/classes/notes.ts
 delete mode 100644 src/services/chart/charts/classes/per-user-drive.ts
 delete mode 100644 src/services/chart/charts/classes/per-user-following.ts
 delete mode 100644 src/services/chart/charts/classes/per-user-notes.ts
 delete mode 100644 src/services/chart/charts/classes/per-user-reactions.ts
 delete mode 100644 src/services/chart/charts/classes/test-grouped.ts
 delete mode 100644 src/services/chart/charts/classes/test-unique.ts
 delete mode 100644 src/services/chart/charts/classes/test.ts
 delete mode 100644 src/services/chart/charts/classes/users.ts
 delete mode 100644 src/services/chart/charts/schemas/active-users.ts
 delete mode 100644 src/services/chart/charts/schemas/drive.ts
 delete mode 100644 src/services/chart/charts/schemas/federation.ts
 delete mode 100644 src/services/chart/charts/schemas/hashtag.ts
 delete mode 100644 src/services/chart/charts/schemas/instance.ts
 delete mode 100644 src/services/chart/charts/schemas/network.ts
 delete mode 100644 src/services/chart/charts/schemas/notes.ts
 delete mode 100644 src/services/chart/charts/schemas/per-user-drive.ts
 delete mode 100644 src/services/chart/charts/schemas/per-user-following.ts
 delete mode 100644 src/services/chart/charts/schemas/per-user-notes.ts
 delete mode 100644 src/services/chart/charts/schemas/per-user-reactions.ts
 delete mode 100644 src/services/chart/charts/schemas/test-grouped.ts
 delete mode 100644 src/services/chart/charts/schemas/test-unique.ts
 delete mode 100644 src/services/chart/charts/schemas/test.ts
 delete mode 100644 src/services/chart/charts/schemas/users.ts
 delete mode 100644 src/services/chart/core.ts
 delete mode 100644 src/services/chart/entities.ts
 delete mode 100644 src/services/chart/index.ts
 delete mode 100644 src/services/create-notification.ts
 delete mode 100644 src/services/create-system-user.ts
 delete mode 100644 src/services/drive/add-file.ts
 delete mode 100644 src/services/drive/delete-file.ts
 delete mode 100644 src/services/drive/generate-video-thumbnail.ts
 delete mode 100644 src/services/drive/image-processor.ts
 delete mode 100644 src/services/drive/internal-storage.ts
 delete mode 100644 src/services/drive/logger.ts
 delete mode 100644 src/services/drive/s3.ts
 delete mode 100644 src/services/drive/upload-from-url.ts
 delete mode 100644 src/services/fetch-instance-metadata.ts
 delete mode 100644 src/services/following/create.ts
 delete mode 100644 src/services/following/delete.ts
 delete mode 100644 src/services/following/requests/accept-all.ts
 delete mode 100644 src/services/following/requests/accept.ts
 delete mode 100644 src/services/following/requests/cancel.ts
 delete mode 100644 src/services/following/requests/create.ts
 delete mode 100644 src/services/following/requests/reject.ts
 delete mode 100644 src/services/i/pin.ts
 delete mode 100644 src/services/i/update.ts
 delete mode 100644 src/services/insert-moderation-log.ts
 delete mode 100644 src/services/instance-actor.ts
 delete mode 100644 src/services/logger.ts
 delete mode 100644 src/services/messages/create.ts
 delete mode 100644 src/services/messages/delete.ts
 delete mode 100644 src/services/note/create.ts
 delete mode 100644 src/services/note/delete.ts
 delete mode 100644 src/services/note/polls/update.ts
 delete mode 100644 src/services/note/polls/vote.ts
 delete mode 100644 src/services/note/reaction/create.ts
 delete mode 100644 src/services/note/reaction/delete.ts
 delete mode 100644 src/services/note/read.ts
 delete mode 100644 src/services/note/unread.ts
 delete mode 100644 src/services/note/unwatch.ts
 delete mode 100644 src/services/note/watch.ts
 delete mode 100644 src/services/push-notification.ts
 delete mode 100644 src/services/register-or-fetch-instance-doc.ts
 delete mode 100644 src/services/relay.ts
 delete mode 100644 src/services/send-email-notification.ts
 delete mode 100644 src/services/send-email.ts
 delete mode 100644 src/services/stream.ts
 delete mode 100644 src/services/suspend-user.ts
 delete mode 100644 src/services/unsuspend-user.ts
 delete mode 100644 src/services/update-hashtag.ts
 delete mode 100644 src/services/user-list/push.ts
 delete mode 100644 src/services/validate-email-for-account.ts
 delete mode 100644 src/tools/accept-migration.ts
 delete mode 100644 src/tools/add-emoji.ts
 delete mode 100644 src/tools/demote-admin.ts
 delete mode 100644 src/tools/mark-admin.ts
 delete mode 100644 src/tools/refresh-question.ts
 delete mode 100644 src/tools/resync-remote-user.ts
 delete mode 100644 src/tools/show-signin-history.ts
 delete mode 100644 src/tsconfig.json
 delete mode 100644 src/types.ts
 delete mode 100644 src/well-known-services.ts
 delete mode 100644 vetur.config.js
 delete mode 100644 webpack.config.ts

(limited to 'src/client/pages/admin/queue.chart.vue')

diff --git a/.gitignore b/.gitignore
index f8baa43848..9928d93aa2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@
 /.idea
 
 # Node.js
-/node_modules
+node_modules
 report.*.json
 
 # Cypress
@@ -20,7 +20,7 @@ cypress/videos
 
 # misskey
 /build
-/built
+built
 /data
 /.cache-loader
 /db
diff --git a/.imgbotconfig b/.imgbotconfig
deleted file mode 100644
index 6a1dfe1ed3..0000000000
--- a/.imgbotconfig
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-	"ignoredFiles": [
-		"test/resources/*"
-	]
-}
diff --git a/.mocharc.json b/.mocharc.json
deleted file mode 100644
index 278a5b310a..0000000000
--- a/.mocharc.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-	"extension": ["ts","js","cjs","mjs"],
-	"require": ["ts-node/register", "tsconfig-paths/register"],
-	"slow": 1000,
-	"timeout": 35000,
-	"exit": true
-}
diff --git a/assets/api-doc.png b/assets/api-doc.png
deleted file mode 100644
index 9b07f1f398..0000000000
Binary files a/assets/api-doc.png and /dev/null differ
diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png
deleted file mode 100644
index 947c513bbb..0000000000
Binary files a/assets/apple-touch-icon.png and /dev/null differ
diff --git a/assets/client/about-icon.png b/assets/client/about-icon.png
deleted file mode 100644
index afc1f0c728..0000000000
Binary files a/assets/client/about-icon.png and /dev/null differ
diff --git a/assets/client/fedi.jpg b/assets/client/fedi.jpg
deleted file mode 100644
index cbf3748eb8..0000000000
Binary files a/assets/client/fedi.jpg and /dev/null differ
diff --git a/assets/client/label-red.svg b/assets/client/label-red.svg
deleted file mode 100644
index 45996aa9ce..0000000000
--- a/assets/client/label-red.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
-	 y="0px" width="96px" height="96px" viewBox="0 0 96 96" enable-background="new 0 0 96 96" xml:space="preserve">
-<polygon fill="#ea2412" points="0,45.255 45.254,0 84.854,0 0,84.854 "/>
-</svg>
diff --git a/assets/client/label.svg b/assets/client/label.svg
deleted file mode 100644
index b1f85f3c07..0000000000
--- a/assets/client/label.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
-	 y="0px" width="96px" height="96px" viewBox="0 0 96 96" enable-background="new 0 0 96 96" xml:space="preserve">
-<polygon fill="#0B8AEA" points="0,45.255 45.254,0 84.854,0 0,84.854 "/>
-</svg>
diff --git a/assets/client/misskey.svg b/assets/client/misskey.svg
deleted file mode 100644
index 3fcb2d3ecb..0000000000
--- a/assets/client/misskey.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg width="515px" height="136px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
-    <g id="path2" transform="matrix(0.264585,0,0,0.264585,-50.0005,-50.0001)">
-        <path d="M256.418,188.976C248.558,188.944 240.758,190.308 233.379,193.013C220.308,197.613 209.533,205.888 201.091,217.802C193.02,229.329 188.977,242.195 188.977,256.409L188.977,508.89C188.977,527.332 195.52,543.29 208.576,556.732C222.032,569.803 237.99,576.331 256.418,576.331C275.259,576.331 291.204,569.803 304.274,556.747C317.73,543.291 324.441,527.332 324.441,508.89L324.441,462.983C324.584,453.04 334.824,455.655 340.01,462.983C349.695,479.767 370.251,494.19 394.193,494.105L394.193,494.119C418.119,494.019 438.005,482.196 448.375,462.983C452.304,458.354 463.377,450.455 464.52,462.983L464.52,508.89C464.52,527.332 471.047,543.29 484.104,556.732C497.574,569.803 513.511,576.331 531.953,576.331C550.78,576.331 566.739,569.803 579.809,556.747C593.265,543.291 599.977,527.332 599.977,508.89L599.977,256.409C599.977,242.195 595.752,229.329 587.309,217.802C579.224,205.874 568.653,197.613 555.597,193.013C547.912,190.314 540.228,188.976 532.543,188.976C511.788,188.976 494.301,197.046 480.073,213.188L411.636,293.281C410.107,294.438 405.006,303.247 394.178,303.247C383.379,303.247 378.868,294.439 377.325,293.296L308.297,213.188C294.47,197.046 277.173,188.976 256.418,188.976ZM682.904,188.983C666.763,188.983 652.926,194.748 641.404,206.271C630.261,217.413 624.691,231.054 624.691,247.196C624.691,263.338 630.261,277.174 641.404,288.697C652.926,299.839 666.763,305.41 682.904,305.41C699.046,305.41 712.88,299.839 724.412,288.697C735.935,277.174 741.693,263.338 741.693,247.196C741.693,231.054 735.935,217.413 724.412,206.271C712.88,194.748 699.046,188.983 682.904,188.983ZM1327.58,193.013C1311.45,193.013 1297.62,198.779 1286.1,210.302C1274.96,221.444 1269.38,235.08 1269.38,251.212L1269.38,519.72C1269.38,535.861 1274.96,549.697 1286.1,561.22C1297.62,572.362 1311.45,577.933 1327.58,577.933C1343.73,577.933 1357.55,572.362 1369.08,561.22C1380.6,549.697 1386.37,535.867 1386.37,519.734C1386.37,508.211 1387.9,502.453 1390.96,502.453C1392.51,502.453 1394.05,503.023 1395.58,504.166L1453.2,560.061C1464.72,571.203 1478.36,576.774 1494.11,576.774C1510.62,576.774 1524.45,571.009 1535.6,559.486C1546.74,547.572 1552.31,533.936 1552.31,518.575C1552.31,502.053 1546.36,488.029 1534.45,476.506C1508.32,450.765 1494.69,437.517 1493.54,436.755C1488.54,431.755 1488.73,427.53 1494.11,424.073L1495.27,423.497L1495.27,422.929L1531.57,399.875C1548.85,388.352 1557.5,372.026 1557.5,350.904C1557.5,339.381 1554.42,328.622 1548.28,318.623C1536.76,301.339 1520.43,292.691 1499.3,292.691C1487.78,292.691 1477.02,295.768 1467.04,301.911C1422.48,331.499 1399.42,346.678 1397.88,347.449C1395.2,349.363 1392.7,349.738 1390.4,348.586C1387.7,347.434 1386.35,344.939 1386.35,341.101L1386.35,251.212C1386.35,235.08 1380.59,221.444 1369.07,210.302C1357.55,198.779 1343.72,193.013 1327.58,193.013ZM1716.37,291.738C1676.42,291.738 1642.24,305.949 1613.81,334.376C1585.76,362.422 1571.74,396.227 1571.74,435.795C1571.74,475.745 1585.76,509.932 1613.81,538.359C1642.24,566.404 1676.42,580.428 1716.37,580.428C1755.94,580.428 1789.94,566.404 1818.37,538.359C1827.2,529.521 1831.62,518.773 1831.62,506.107C1831.62,493.423 1827.2,482.664 1818.37,473.827C1809.53,464.999 1798.77,460.584 1786.11,460.584C1773.42,460.584 1762.66,464.999 1753.83,473.827C1743.46,484.588 1730.97,489.963 1716.37,489.963C1701.4,489.963 1688.53,484.78 1677.77,474.41C1667.39,463.649 1662.2,450.775 1662.2,435.795C1662.2,421.206 1667.59,408.72 1678.35,398.34C1683.73,392.578 1690.26,388.74 1697.93,386.817C1699.87,386.436 1701.4,386.623 1702.55,387.385C1703.32,388.547 1702.93,389.702 1701.39,390.854L1689.87,402.953C1681.03,411.791 1676.61,422.359 1676.61,434.644C1676.61,447.319 1680.45,457.497 1688.13,465.182C1695.81,472.868 1706.57,476.705 1720.41,476.705C1730.01,476.705 1739.61,471.91 1749.21,462.311L1816.06,396.044C1824.9,387.197 1829.32,376.436 1829.32,363.77C1829.32,351.086 1824.9,340.332 1816.06,331.504C1789.17,304.992 1755.94,291.738 1716.37,291.738ZM877.977,292.668C841.947,292.194 813.839,301.679 793.662,321.133C775.996,338.036 767.168,359.358 767.168,385.089C767.549,417.363 780.035,441.565 804.624,457.697C811.918,462.687 820.941,466.72 831.693,469.796C837.083,471.72 846.111,474.02 858.777,476.705C869.919,479.391 882.023,481.886 895.088,484.191C897.774,484.962 898.924,486.312 898.543,488.236C898.543,490.541 897.58,491.691 895.657,491.691C890.667,492.072 886.059,492.266 881.831,492.266C850.328,488.81 829.001,485.927 817.859,483.622C814.793,482.851 811.535,482.463 808.078,482.463C796.165,482.463 785.787,486.884 776.949,495.721C768.502,504.178 764.282,514.551 764.282,526.836C764.282,536.825 767.352,545.854 773.494,553.92C780.027,561.986 788.486,567.169 798.866,569.473C831.13,576.778 860.317,580.428 886.429,580.428C922.16,580.428 950.013,570.825 969.992,551.617C987.277,535.094 995.925,513.775 995.925,487.653C995.925,455.388 983.626,431.187 959.037,415.045C945.972,406.598 927.915,400.45 904.869,396.612C891.042,393.927 879.518,391.427 870.3,389.112L870.3,389.127C867.605,388.356 866.067,386.818 865.686,384.513C865.686,382.59 867.224,381.44 870.3,381.059C873.757,380.678 877.415,380.678 881.262,381.059C913.146,384.135 934.652,386.823 945.794,389.127C948.861,389.889 951.931,390.271 955.007,390.271C967.301,390.271 977.674,386.051 986.121,377.604C994.959,368.767 999.379,358.393 999.379,346.49C999.379,336.109 996.109,326.894 989.576,318.837C983.043,310.761 974.79,305.566 964.81,303.261C938.298,297.5 911.788,294.042 885.285,292.89C882.813,292.77 880.379,292.7 877.977,292.668ZM1128.73,292.668C1092.7,292.194 1064.59,301.679 1044.42,321.133C1026.75,338.036 1017.92,359.358 1017.92,385.089C1018.3,417.363 1030.79,441.565 1055.38,457.697C1062.67,462.687 1071.7,466.72 1082.46,469.796C1087.84,471.72 1096.86,474.02 1109.54,476.705C1120.68,479.391 1132.79,481.886 1145.84,484.191C1148.53,484.962 1149.68,486.312 1149.3,488.236C1149.3,490.541 1148.34,491.691 1146.41,491.691C1141.42,492.072 1136.81,492.266 1132.59,492.266C1101.09,488.81 1079.77,485.927 1068.63,483.622C1065.55,482.851 1062.29,482.463 1058.83,482.463C1046.92,482.463 1036.55,486.884 1027.72,495.721C1019.26,504.178 1015.04,514.551 1015.04,526.836C1015.04,536.825 1018.11,545.854 1024.26,553.92C1030.79,561.986 1039.24,567.169 1049.62,569.473C1081.88,576.778 1111.08,580.428 1137.2,580.428C1172.92,580.428 1200.77,570.825 1220.75,551.617C1238.03,535.094 1246.68,513.775 1246.68,487.653C1246.68,455.388 1234.39,431.187 1209.81,415.045C1196.74,406.598 1178.68,400.45 1155.64,396.612C1141.81,393.927 1130.29,391.427 1121.07,389.112L1121.05,389.127C1118.37,388.356 1116.84,386.818 1116.45,384.513C1116.45,382.59 1117.99,381.44 1121.05,381.059C1124.52,380.678 1128.17,380.678 1132.01,381.059C1163.89,384.135 1185.41,386.823 1196.55,389.127C1199.62,389.889 1202.69,390.271 1205.76,390.271C1218.06,390.271 1228.43,386.051 1236.89,377.604C1245.72,368.767 1250.13,358.393 1250.13,346.49C1250.13,336.109 1246.87,326.894 1240.35,318.837C1233.81,310.761 1225.55,305.566 1215.56,303.261C1189.06,297.5 1162.55,294.042 1136.04,292.89C1133.57,292.77 1131.13,292.7 1128.73,292.668ZM1910.17,296.736C1894.04,296.736 1880.21,302.501 1868.69,314.024C1857.55,325.157 1851.98,338.793 1851.98,354.934L1851.98,435.028C1851.98,473.825 1865.8,507.05 1893.45,534.705C1921.12,562.36 1954.36,576.191 1993.15,576.191C2000.84,576.191 2007.95,575.614 2014.48,574.471C2018.32,573.699 2021,574.469 2022.53,576.774C2023.69,578.307 2023.3,580.42 2021.39,583.115C2016.39,590.029 2005.82,593.486 1989.69,593.486C1983.55,593.486 1975.68,591.949 1966.07,588.873C1956.47,585.797 1948.98,584.259 1943.6,584.259C1920.93,584.259 1904.99,594.638 1895.77,615.388C1892.32,622.302 1890.58,629.598 1890.58,637.283C1890.58,659.948 1900.77,675.892 1921.13,685.11C1941.49,694.709 1964.34,699.505 1989.69,699.505C2033.49,699.505 2068.25,686.639 2093.98,660.898C2120.11,635.166 2133.18,600.784 2133.18,557.758L2133.18,452.308C2133.94,446.157 2134.32,440.399 2134.32,435.028L2134.32,354.934C2134.32,338.802 2128.57,325.166 2117.04,314.024C2105.9,302.501 2092.27,296.736 2076.13,296.736C2059.99,296.736 2046.16,302.501 2034.63,314.024C2023.11,325.157 2017.35,338.793 2017.35,354.934L2017.35,435.028C2017.35,441.551 2015.04,447.309 2010.43,452.308C2005.83,456.918 2000.07,459.225 1993.15,459.225C1986.62,459.225 1980.86,456.918 1975.87,452.308C1971.26,447.309 1968.95,441.551 1968.95,435.028L1968.95,354.934C1968.95,338.802 1963.19,325.166 1951.67,314.024C1940.14,302.501 1926.3,296.736 1910.17,296.736ZM683.473,316.947C667.331,316.947 653.495,322.713 641.972,334.236C630.449,345.768 624.691,359.602 624.691,375.744L624.691,518.118C624.691,534.259 630.449,548.095 641.972,559.618C653.504,570.761 667.341,576.331 683.473,576.331C699.624,576.331 713.27,570.761 724.412,559.618C735.935,548.095 741.693,534.259 741.693,518.118L741.693,375.744C741.693,359.593 735.935,345.759 724.412,334.236C713.261,322.713 699.614,316.947 683.473,316.947Z" style="fill:white;fill-rule:nonzero;"/>
-    </g>
-</svg>
diff --git a/assets/client/remove.png b/assets/client/remove.png
deleted file mode 100644
index c2e222a0fc..0000000000
Binary files a/assets/client/remove.png and /dev/null differ
diff --git a/assets/client/room/furnitures/banknote/banknote.blend b/assets/client/room/furnitures/banknote/banknote.blend
deleted file mode 100644
index 60b1968a29..0000000000
Binary files a/assets/client/room/furnitures/banknote/banknote.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/banknote/banknote.glb b/assets/client/room/furnitures/banknote/banknote.glb
deleted file mode 100644
index f4ef0b91e7..0000000000
Binary files a/assets/client/room/furnitures/banknote/banknote.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/banknote/tex.png b/assets/client/room/furnitures/banknote/tex.png
deleted file mode 100644
index 9106dc1457..0000000000
Binary files a/assets/client/room/furnitures/banknote/tex.png and /dev/null differ
diff --git a/assets/client/room/furnitures/bed/bed.blend b/assets/client/room/furnitures/bed/bed.blend
deleted file mode 100644
index 731df76d0c..0000000000
Binary files a/assets/client/room/furnitures/bed/bed.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/bed/bed.glb b/assets/client/room/furnitures/bed/bed.glb
deleted file mode 100644
index f35ecb9ef4..0000000000
Binary files a/assets/client/room/furnitures/bed/bed.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/bin/bin.blend b/assets/client/room/furnitures/bin/bin.blend
deleted file mode 100644
index 8d459a0869..0000000000
Binary files a/assets/client/room/furnitures/bin/bin.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/bin/bin.glb b/assets/client/room/furnitures/bin/bin.glb
deleted file mode 100644
index b45f203802..0000000000
Binary files a/assets/client/room/furnitures/bin/bin.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/book/book.blend b/assets/client/room/furnitures/book/book.blend
deleted file mode 100644
index 0d4899d4ae..0000000000
Binary files a/assets/client/room/furnitures/book/book.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/book/book.glb b/assets/client/room/furnitures/book/book.glb
deleted file mode 100644
index 546893da06..0000000000
Binary files a/assets/client/room/furnitures/book/book.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/book2/barcode.png b/assets/client/room/furnitures/book2/barcode.png
deleted file mode 100644
index 37cfe5add3..0000000000
Binary files a/assets/client/room/furnitures/book2/barcode.png and /dev/null differ
diff --git a/assets/client/room/furnitures/book2/book2.blend b/assets/client/room/furnitures/book2/book2.blend
deleted file mode 100644
index e0fdb48101..0000000000
Binary files a/assets/client/room/furnitures/book2/book2.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/book2/book2.glb b/assets/client/room/furnitures/book2/book2.glb
deleted file mode 100644
index 2b26402f8c..0000000000
Binary files a/assets/client/room/furnitures/book2/book2.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/book2/texture.afdesign b/assets/client/room/furnitures/book2/texture.afdesign
deleted file mode 100644
index b63771607a..0000000000
Binary files a/assets/client/room/furnitures/book2/texture.afdesign and /dev/null differ
diff --git a/assets/client/room/furnitures/book2/texture.png b/assets/client/room/furnitures/book2/texture.png
deleted file mode 100644
index 5aa84f0340..0000000000
Binary files a/assets/client/room/furnitures/book2/texture.png and /dev/null differ
diff --git a/assets/client/room/furnitures/book2/uv.png b/assets/client/room/furnitures/book2/uv.png
deleted file mode 100644
index 61c4fb0400..0000000000
Binary files a/assets/client/room/furnitures/book2/uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box/cardboard-box.blend b/assets/client/room/furnitures/cardboard-box/cardboard-box.blend
deleted file mode 100644
index 3a528de32a..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box/cardboard-box.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box/cardboard-box.glb b/assets/client/room/furnitures/cardboard-box/cardboard-box.glb
deleted file mode 100644
index bed372e94f..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box/cardboard-box.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box2/cardboard-box2.blend b/assets/client/room/furnitures/cardboard-box2/cardboard-box2.blend
deleted file mode 100644
index 5f146267ac..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box2/cardboard-box2.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box2/cardboard-box2.glb b/assets/client/room/furnitures/cardboard-box2/cardboard-box2.glb
deleted file mode 100644
index 85fcb5c0b6..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box2/cardboard-box2.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box2/texture.png b/assets/client/room/furnitures/cardboard-box2/texture.png
deleted file mode 100644
index e498d8f65b..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box2/texture.png and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box2/uv.png b/assets/client/room/furnitures/cardboard-box2/uv.png
deleted file mode 100644
index d547843ee0..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box2/uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box3/cardboard-box3.blend b/assets/client/room/furnitures/cardboard-box3/cardboard-box3.blend
deleted file mode 100644
index 00681a3cfd..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box3/cardboard-box3.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box3/cardboard-box3.glb b/assets/client/room/furnitures/cardboard-box3/cardboard-box3.glb
deleted file mode 100644
index 1ef0427689..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box3/cardboard-box3.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box3/texture.png b/assets/client/room/furnitures/cardboard-box3/texture.png
deleted file mode 100644
index 56c914cb9d..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box3/texture.png and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box3/texture.xcf b/assets/client/room/furnitures/cardboard-box3/texture.xcf
deleted file mode 100644
index 7ffb3e3439..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box3/texture.xcf and /dev/null differ
diff --git a/assets/client/room/furnitures/cardboard-box3/uv.png b/assets/client/room/furnitures/cardboard-box3/uv.png
deleted file mode 100644
index 797ac509db..0000000000
Binary files a/assets/client/room/furnitures/cardboard-box3/uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/carpet-stripe/carpet-stripe.blend b/assets/client/room/furnitures/carpet-stripe/carpet-stripe.blend
deleted file mode 100644
index 750343d4f0..0000000000
Binary files a/assets/client/room/furnitures/carpet-stripe/carpet-stripe.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/carpet-stripe/carpet-stripe.glb b/assets/client/room/furnitures/carpet-stripe/carpet-stripe.glb
deleted file mode 100644
index 3066a69e35..0000000000
Binary files a/assets/client/room/furnitures/carpet-stripe/carpet-stripe.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/chair/chair.blend b/assets/client/room/furnitures/chair/chair.blend
deleted file mode 100644
index 79c29a8401..0000000000
Binary files a/assets/client/room/furnitures/chair/chair.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/chair/chair.glb b/assets/client/room/furnitures/chair/chair.glb
deleted file mode 100644
index 08ee1a0bb0..0000000000
Binary files a/assets/client/room/furnitures/chair/chair.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/chair2/chair2.blend b/assets/client/room/furnitures/chair2/chair2.blend
deleted file mode 100644
index c6a1acd96f..0000000000
Binary files a/assets/client/room/furnitures/chair2/chair2.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/chair2/chair2.glb b/assets/client/room/furnitures/chair2/chair2.glb
deleted file mode 100644
index 5ea2f3518b..0000000000
Binary files a/assets/client/room/furnitures/chair2/chair2.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/color-box/color-box.blend b/assets/client/room/furnitures/color-box/color-box.blend
deleted file mode 100644
index f96a4ff766..0000000000
Binary files a/assets/client/room/furnitures/color-box/color-box.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/color-box/color-box.glb b/assets/client/room/furnitures/color-box/color-box.glb
deleted file mode 100644
index 43f2abcae8..0000000000
Binary files a/assets/client/room/furnitures/color-box/color-box.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/corkboard/corkboard.blend b/assets/client/room/furnitures/corkboard/corkboard.blend
deleted file mode 100644
index 9a7e1878cd..0000000000
Binary files a/assets/client/room/furnitures/corkboard/corkboard.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/corkboard/corkboard.glb b/assets/client/room/furnitures/corkboard/corkboard.glb
deleted file mode 100644
index fee108fb91..0000000000
Binary files a/assets/client/room/furnitures/corkboard/corkboard.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/cube/cube.blend b/assets/client/room/furnitures/cube/cube.blend
deleted file mode 100644
index 1af5bf40a9..0000000000
Binary files a/assets/client/room/furnitures/cube/cube.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/cube/cube.glb b/assets/client/room/furnitures/cube/cube.glb
deleted file mode 100644
index 4ac8b6036d..0000000000
Binary files a/assets/client/room/furnitures/cube/cube.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/cup-noodle/cup-noodle.blend b/assets/client/room/furnitures/cup-noodle/cup-noodle.blend
deleted file mode 100644
index 37ca8868c7..0000000000
Binary files a/assets/client/room/furnitures/cup-noodle/cup-noodle.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/cup-noodle/cup-noodle.glb b/assets/client/room/furnitures/cup-noodle/cup-noodle.glb
deleted file mode 100644
index 58efb1b3b4..0000000000
Binary files a/assets/client/room/furnitures/cup-noodle/cup-noodle.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/cup-noodle/noodle.png b/assets/client/room/furnitures/cup-noodle/noodle.png
deleted file mode 100644
index 1d74e0bbe7..0000000000
Binary files a/assets/client/room/furnitures/cup-noodle/noodle.png and /dev/null differ
diff --git a/assets/client/room/furnitures/desk/desk.blend b/assets/client/room/furnitures/desk/desk.blend
deleted file mode 100644
index c88d01f0b2..0000000000
Binary files a/assets/client/room/furnitures/desk/desk.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/desk/desk.glb b/assets/client/room/furnitures/desk/desk.glb
deleted file mode 100644
index 4a58513095..0000000000
Binary files a/assets/client/room/furnitures/desk/desk.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/doll-ai/doll-ai.blend b/assets/client/room/furnitures/doll-ai/doll-ai.blend
deleted file mode 100644
index a912231ecb..0000000000
Binary files a/assets/client/room/furnitures/doll-ai/doll-ai.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/doll-ai/doll-ai.glb b/assets/client/room/furnitures/doll-ai/doll-ai.glb
deleted file mode 100644
index ec55a7bd7b..0000000000
Binary files a/assets/client/room/furnitures/doll-ai/doll-ai.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/doll-ai/doll_ai_tex.png b/assets/client/room/furnitures/doll-ai/doll_ai_tex.png
deleted file mode 100644
index 370ca5f75b..0000000000
Binary files a/assets/client/room/furnitures/doll-ai/doll_ai_tex.png and /dev/null differ
diff --git a/assets/client/room/furnitures/energy-drink/energy-drink.blend b/assets/client/room/furnitures/energy-drink/energy-drink.blend
deleted file mode 100644
index 65fc41273e..0000000000
Binary files a/assets/client/room/furnitures/energy-drink/energy-drink.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/energy-drink/energy-drink.glb b/assets/client/room/furnitures/energy-drink/energy-drink.glb
deleted file mode 100644
index 7fb1c27836..0000000000
Binary files a/assets/client/room/furnitures/energy-drink/energy-drink.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/energy-drink/texture.afdesign b/assets/client/room/furnitures/energy-drink/texture.afdesign
deleted file mode 100644
index 8c117a49b1..0000000000
Binary files a/assets/client/room/furnitures/energy-drink/texture.afdesign and /dev/null differ
diff --git a/assets/client/room/furnitures/energy-drink/texture.png b/assets/client/room/furnitures/energy-drink/texture.png
deleted file mode 100644
index 484ca0f96f..0000000000
Binary files a/assets/client/room/furnitures/energy-drink/texture.png and /dev/null differ
diff --git a/assets/client/room/furnitures/energy-drink/uv.png b/assets/client/room/furnitures/energy-drink/uv.png
deleted file mode 100644
index 2a3f20c999..0000000000
Binary files a/assets/client/room/furnitures/energy-drink/uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/eraser/cover.png b/assets/client/room/furnitures/eraser/cover.png
deleted file mode 100644
index 932a3fc62e..0000000000
Binary files a/assets/client/room/furnitures/eraser/cover.png and /dev/null differ
diff --git a/assets/client/room/furnitures/eraser/cover.psd b/assets/client/room/furnitures/eraser/cover.psd
deleted file mode 100644
index c393337833..0000000000
Binary files a/assets/client/room/furnitures/eraser/cover.psd and /dev/null differ
diff --git a/assets/client/room/furnitures/eraser/eraser-uv.png b/assets/client/room/furnitures/eraser/eraser-uv.png
deleted file mode 100644
index 89e4ea4c45..0000000000
Binary files a/assets/client/room/furnitures/eraser/eraser-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/eraser/eraser.blend b/assets/client/room/furnitures/eraser/eraser.blend
deleted file mode 100644
index 103c54fbae..0000000000
Binary files a/assets/client/room/furnitures/eraser/eraser.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/eraser/eraser.glb b/assets/client/room/furnitures/eraser/eraser.glb
deleted file mode 100644
index 016b60df20..0000000000
Binary files a/assets/client/room/furnitures/eraser/eraser.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/facial-tissue/facial-tissue-uv.png b/assets/client/room/furnitures/facial-tissue/facial-tissue-uv.png
deleted file mode 100644
index e3865ad15e..0000000000
Binary files a/assets/client/room/furnitures/facial-tissue/facial-tissue-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/facial-tissue/facial-tissue.blend b/assets/client/room/furnitures/facial-tissue/facial-tissue.blend
deleted file mode 100644
index d59f87c1ee..0000000000
Binary files a/assets/client/room/furnitures/facial-tissue/facial-tissue.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/facial-tissue/facial-tissue.glb b/assets/client/room/furnitures/facial-tissue/facial-tissue.glb
deleted file mode 100644
index 48b36ef347..0000000000
Binary files a/assets/client/room/furnitures/facial-tissue/facial-tissue.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/facial-tissue/facial-tissue.png b/assets/client/room/furnitures/facial-tissue/facial-tissue.png
deleted file mode 100644
index 7cee4b1859..0000000000
Binary files a/assets/client/room/furnitures/facial-tissue/facial-tissue.png and /dev/null differ
diff --git a/assets/client/room/furnitures/facial-tissue/facial-tissue.psd b/assets/client/room/furnitures/facial-tissue/facial-tissue.psd
deleted file mode 100644
index cd59fc007b..0000000000
Binary files a/assets/client/room/furnitures/facial-tissue/facial-tissue.psd and /dev/null differ
diff --git a/assets/client/room/furnitures/fan/fan.blend b/assets/client/room/furnitures/fan/fan.blend
deleted file mode 100644
index 8c8106e5fe..0000000000
Binary files a/assets/client/room/furnitures/fan/fan.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/fan/fan.glb b/assets/client/room/furnitures/fan/fan.glb
deleted file mode 100644
index d9367f3534..0000000000
Binary files a/assets/client/room/furnitures/fan/fan.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/holo-display/holo-display.blend b/assets/client/room/furnitures/holo-display/holo-display.blend
deleted file mode 100644
index 56d2e1f819..0000000000
Binary files a/assets/client/room/furnitures/holo-display/holo-display.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/holo-display/holo-display.glb b/assets/client/room/furnitures/holo-display/holo-display.glb
deleted file mode 100644
index 4d042a59b3..0000000000
Binary files a/assets/client/room/furnitures/holo-display/holo-display.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/holo-display/ray-uv.png b/assets/client/room/furnitures/holo-display/ray-uv.png
deleted file mode 100644
index aa7e817e0f..0000000000
Binary files a/assets/client/room/furnitures/holo-display/ray-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/holo-display/ray.png b/assets/client/room/furnitures/holo-display/ray.png
deleted file mode 100644
index 6a5d24e143..0000000000
Binary files a/assets/client/room/furnitures/holo-display/ray.png and /dev/null differ
diff --git a/assets/client/room/furnitures/keyboard/keyboard.blend b/assets/client/room/furnitures/keyboard/keyboard.blend
deleted file mode 100644
index ab33d134b3..0000000000
Binary files a/assets/client/room/furnitures/keyboard/keyboard.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/keyboard/keyboard.glb b/assets/client/room/furnitures/keyboard/keyboard.glb
deleted file mode 100644
index 15dc69f47a..0000000000
Binary files a/assets/client/room/furnitures/keyboard/keyboard.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/low-table/low-table.blend b/assets/client/room/furnitures/low-table/low-table.blend
deleted file mode 100644
index e1592174d9..0000000000
Binary files a/assets/client/room/furnitures/low-table/low-table.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/low-table/low-table.glb b/assets/client/room/furnitures/low-table/low-table.glb
deleted file mode 100644
index c69bf35d7b..0000000000
Binary files a/assets/client/room/furnitures/low-table/low-table.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/mat/mat.blend b/assets/client/room/furnitures/mat/mat.blend
deleted file mode 100644
index a1e1a68c55..0000000000
Binary files a/assets/client/room/furnitures/mat/mat.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/mat/mat.glb b/assets/client/room/furnitures/mat/mat.glb
deleted file mode 100644
index 87ccd44e1a..0000000000
Binary files a/assets/client/room/furnitures/mat/mat.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/milk/milk-uv.png b/assets/client/room/furnitures/milk/milk-uv.png
deleted file mode 100644
index 258fd54638..0000000000
Binary files a/assets/client/room/furnitures/milk/milk-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/milk/milk.blend b/assets/client/room/furnitures/milk/milk.blend
deleted file mode 100644
index 2df508d5b9..0000000000
Binary files a/assets/client/room/furnitures/milk/milk.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/milk/milk.glb b/assets/client/room/furnitures/milk/milk.glb
deleted file mode 100644
index b335fe3d02..0000000000
Binary files a/assets/client/room/furnitures/milk/milk.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/milk/milk.png b/assets/client/room/furnitures/milk/milk.png
deleted file mode 100644
index 35181c8c8c..0000000000
Binary files a/assets/client/room/furnitures/milk/milk.png and /dev/null differ
diff --git a/assets/client/room/furnitures/milk/milk.psd b/assets/client/room/furnitures/milk/milk.psd
deleted file mode 100644
index f31e439277..0000000000
Binary files a/assets/client/room/furnitures/milk/milk.psd and /dev/null differ
diff --git a/assets/client/room/furnitures/monitor/monitor.blend b/assets/client/room/furnitures/monitor/monitor.blend
deleted file mode 100644
index 6c042ccdd8..0000000000
Binary files a/assets/client/room/furnitures/monitor/monitor.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/monitor/monitor.glb b/assets/client/room/furnitures/monitor/monitor.glb
deleted file mode 100644
index fc33286a15..0000000000
Binary files a/assets/client/room/furnitures/monitor/monitor.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/monitor/monitor.psd b/assets/client/room/furnitures/monitor/monitor.psd
deleted file mode 100644
index 57afff9cd9..0000000000
Binary files a/assets/client/room/furnitures/monitor/monitor.psd and /dev/null differ
diff --git a/assets/client/room/furnitures/monitor/screen-uv.png b/assets/client/room/furnitures/monitor/screen-uv.png
deleted file mode 100644
index 35f74de8aa..0000000000
Binary files a/assets/client/room/furnitures/monitor/screen-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/monitor/screen.jpg b/assets/client/room/furnitures/monitor/screen.jpg
deleted file mode 100644
index 4004a1ede9..0000000000
Binary files a/assets/client/room/furnitures/monitor/screen.jpg and /dev/null differ
diff --git a/assets/client/room/furnitures/moon/moon.blend b/assets/client/room/furnitures/moon/moon.blend
deleted file mode 100644
index 4ff3deab8e..0000000000
Binary files a/assets/client/room/furnitures/moon/moon.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/moon/moon.glb b/assets/client/room/furnitures/moon/moon.glb
deleted file mode 100644
index 07fa7e4c02..0000000000
Binary files a/assets/client/room/furnitures/moon/moon.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/moon/moon.jpg b/assets/client/room/furnitures/moon/moon.jpg
deleted file mode 100644
index 8988ac64b9..0000000000
Binary files a/assets/client/room/furnitures/moon/moon.jpg and /dev/null differ
diff --git a/assets/client/room/furnitures/mousepad/mousepad.blend b/assets/client/room/furnitures/mousepad/mousepad.blend
deleted file mode 100644
index 14bd139c94..0000000000
Binary files a/assets/client/room/furnitures/mousepad/mousepad.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/mousepad/mousepad.glb b/assets/client/room/furnitures/mousepad/mousepad.glb
deleted file mode 100644
index 681ada49cd..0000000000
Binary files a/assets/client/room/furnitures/mousepad/mousepad.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/pc/motherboard-uv.png b/assets/client/room/furnitures/pc/motherboard-uv.png
deleted file mode 100644
index 355009fe7c..0000000000
Binary files a/assets/client/room/furnitures/pc/motherboard-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/pc/motherboard-uv.psd b/assets/client/room/furnitures/pc/motherboard-uv.psd
deleted file mode 100644
index 971f33f79e..0000000000
Binary files a/assets/client/room/furnitures/pc/motherboard-uv.psd and /dev/null differ
diff --git a/assets/client/room/furnitures/pc/motherboard.jpg b/assets/client/room/furnitures/pc/motherboard.jpg
deleted file mode 100644
index d894e4efcf..0000000000
Binary files a/assets/client/room/furnitures/pc/motherboard.jpg and /dev/null differ
diff --git a/assets/client/room/furnitures/pc/pc.blend b/assets/client/room/furnitures/pc/pc.blend
deleted file mode 100644
index 13dfec6ccc..0000000000
Binary files a/assets/client/room/furnitures/pc/pc.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/pc/pc.glb b/assets/client/room/furnitures/pc/pc.glb
deleted file mode 100644
index 44a48b18ae..0000000000
Binary files a/assets/client/room/furnitures/pc/pc.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/pencil/pencil.blend b/assets/client/room/furnitures/pencil/pencil.blend
deleted file mode 100644
index 0fc6bdd776..0000000000
Binary files a/assets/client/room/furnitures/pencil/pencil.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/pencil/pencil.glb b/assets/client/room/furnitures/pencil/pencil.glb
deleted file mode 100644
index a938b5cdcc..0000000000
Binary files a/assets/client/room/furnitures/pencil/pencil.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/photoframe/photo-uv.png b/assets/client/room/furnitures/photoframe/photo-uv.png
deleted file mode 100644
index 9b94906413..0000000000
Binary files a/assets/client/room/furnitures/photoframe/photo-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/photoframe/photo.jpg b/assets/client/room/furnitures/photoframe/photo.jpg
deleted file mode 100644
index af14f0f36a..0000000000
Binary files a/assets/client/room/furnitures/photoframe/photo.jpg and /dev/null differ
diff --git a/assets/client/room/furnitures/photoframe/photoframe.blend b/assets/client/room/furnitures/photoframe/photoframe.blend
deleted file mode 100644
index 4224cde45b..0000000000
Binary files a/assets/client/room/furnitures/photoframe/photoframe.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/photoframe/photoframe.glb b/assets/client/room/furnitures/photoframe/photoframe.glb
deleted file mode 100644
index 4255a77de6..0000000000
Binary files a/assets/client/room/furnitures/photoframe/photoframe.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/piano/piano.blend b/assets/client/room/furnitures/piano/piano.blend
deleted file mode 100644
index 7653cdf672..0000000000
Binary files a/assets/client/room/furnitures/piano/piano.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/piano/piano.glb b/assets/client/room/furnitures/piano/piano.glb
deleted file mode 100644
index 7242e78ceb..0000000000
Binary files a/assets/client/room/furnitures/piano/piano.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/pinguin/pinguin.blend b/assets/client/room/furnitures/pinguin/pinguin.blend
deleted file mode 100644
index 514c713e4c..0000000000
Binary files a/assets/client/room/furnitures/pinguin/pinguin.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/pinguin/pinguin.glb b/assets/client/room/furnitures/pinguin/pinguin.glb
deleted file mode 100644
index 6df34c06e9..0000000000
Binary files a/assets/client/room/furnitures/pinguin/pinguin.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/plant/plant-soil-uv.png b/assets/client/room/furnitures/plant/plant-soil-uv.png
deleted file mode 100644
index d4971a896c..0000000000
Binary files a/assets/client/room/furnitures/plant/plant-soil-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/plant/plant-soil.png b/assets/client/room/furnitures/plant/plant-soil.png
deleted file mode 100644
index e79ccd240e..0000000000
Binary files a/assets/client/room/furnitures/plant/plant-soil.png and /dev/null differ
diff --git a/assets/client/room/furnitures/plant/plant-soil.psd b/assets/client/room/furnitures/plant/plant-soil.psd
deleted file mode 100644
index 1457b7ea5b..0000000000
Binary files a/assets/client/room/furnitures/plant/plant-soil.psd and /dev/null differ
diff --git a/assets/client/room/furnitures/plant/plant.blend b/assets/client/room/furnitures/plant/plant.blend
deleted file mode 100644
index aa38c7b54e..0000000000
Binary files a/assets/client/room/furnitures/plant/plant.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/plant/plant.glb b/assets/client/room/furnitures/plant/plant.glb
deleted file mode 100644
index 38422b4a9b..0000000000
Binary files a/assets/client/room/furnitures/plant/plant.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/plant2/plant2.blend b/assets/client/room/furnitures/plant2/plant2.blend
deleted file mode 100644
index 6592c5d98d..0000000000
Binary files a/assets/client/room/furnitures/plant2/plant2.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/plant2/plant2.glb b/assets/client/room/furnitures/plant2/plant2.glb
deleted file mode 100644
index 223e6f5834..0000000000
Binary files a/assets/client/room/furnitures/plant2/plant2.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/plant2/soil.png b/assets/client/room/furnitures/plant2/soil.png
deleted file mode 100644
index e79ccd240e..0000000000
Binary files a/assets/client/room/furnitures/plant2/soil.png and /dev/null differ
diff --git a/assets/client/room/furnitures/poster-h/poster-h.blend b/assets/client/room/furnitures/poster-h/poster-h.blend
deleted file mode 100644
index 40f944f3c1..0000000000
Binary files a/assets/client/room/furnitures/poster-h/poster-h.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/poster-h/poster-h.glb b/assets/client/room/furnitures/poster-h/poster-h.glb
deleted file mode 100644
index c6032c1009..0000000000
Binary files a/assets/client/room/furnitures/poster-h/poster-h.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/poster-h/uv.png b/assets/client/room/furnitures/poster-h/uv.png
deleted file mode 100644
index f854231e0b..0000000000
Binary files a/assets/client/room/furnitures/poster-h/uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/poster-v/poster-v.blend b/assets/client/room/furnitures/poster-v/poster-v.blend
deleted file mode 100644
index 07fe971634..0000000000
Binary files a/assets/client/room/furnitures/poster-v/poster-v.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/poster-v/poster-v.glb b/assets/client/room/furnitures/poster-v/poster-v.glb
deleted file mode 100644
index 6e3782f193..0000000000
Binary files a/assets/client/room/furnitures/poster-v/poster-v.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/poster-v/uv.png b/assets/client/room/furnitures/poster-v/uv.png
deleted file mode 100644
index 7bb2bf809e..0000000000
Binary files a/assets/client/room/furnitures/poster-v/uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/pudding/pudding.blend b/assets/client/room/furnitures/pudding/pudding.blend
deleted file mode 100644
index bba40ce161..0000000000
Binary files a/assets/client/room/furnitures/pudding/pudding.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/pudding/pudding.glb b/assets/client/room/furnitures/pudding/pudding.glb
deleted file mode 100644
index 06c9ed80cc..0000000000
Binary files a/assets/client/room/furnitures/pudding/pudding.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/rubik-cube/rubik-cube.blend b/assets/client/room/furnitures/rubik-cube/rubik-cube.blend
deleted file mode 100644
index 6c09067e78..0000000000
Binary files a/assets/client/room/furnitures/rubik-cube/rubik-cube.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/rubik-cube/rubik-cube.glb b/assets/client/room/furnitures/rubik-cube/rubik-cube.glb
deleted file mode 100644
index d640df9b06..0000000000
Binary files a/assets/client/room/furnitures/rubik-cube/rubik-cube.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/server/rack-uv.png b/assets/client/room/furnitures/server/rack-uv.png
deleted file mode 100644
index 65bdb0ffd9..0000000000
Binary files a/assets/client/room/furnitures/server/rack-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/server/rack.png b/assets/client/room/furnitures/server/rack.png
deleted file mode 100644
index b851295cfa..0000000000
Binary files a/assets/client/room/furnitures/server/rack.png and /dev/null differ
diff --git a/assets/client/room/furnitures/server/server.blend b/assets/client/room/furnitures/server/server.blend
deleted file mode 100644
index 6675dfbdc2..0000000000
Binary files a/assets/client/room/furnitures/server/server.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/server/server.glb b/assets/client/room/furnitures/server/server.glb
deleted file mode 100644
index a8b530a2d2..0000000000
Binary files a/assets/client/room/furnitures/server/server.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/server/server.png b/assets/client/room/furnitures/server/server.png
deleted file mode 100644
index 8e9a0d716c..0000000000
Binary files a/assets/client/room/furnitures/server/server.png and /dev/null differ
diff --git a/assets/client/room/furnitures/server/uv.png b/assets/client/room/furnitures/server/uv.png
deleted file mode 100644
index ca2e747d16..0000000000
Binary files a/assets/client/room/furnitures/server/uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/sofa/sofa.blend b/assets/client/room/furnitures/sofa/sofa.blend
deleted file mode 100644
index fb5aa51a2c..0000000000
Binary files a/assets/client/room/furnitures/sofa/sofa.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/sofa/sofa.glb b/assets/client/room/furnitures/sofa/sofa.glb
deleted file mode 100644
index 6ce77d94ac..0000000000
Binary files a/assets/client/room/furnitures/sofa/sofa.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/spiral/spiral.blend b/assets/client/room/furnitures/spiral/spiral.blend
deleted file mode 100644
index 9d3be77bce..0000000000
Binary files a/assets/client/room/furnitures/spiral/spiral.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/spiral/spiral.glb b/assets/client/room/furnitures/spiral/spiral.glb
deleted file mode 100644
index ee8e3c23b1..0000000000
Binary files a/assets/client/room/furnitures/spiral/spiral.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/tv/screen-uv.png b/assets/client/room/furnitures/tv/screen-uv.png
deleted file mode 100644
index 4bb74f031f..0000000000
Binary files a/assets/client/room/furnitures/tv/screen-uv.png and /dev/null differ
diff --git a/assets/client/room/furnitures/tv/tv.blend b/assets/client/room/furnitures/tv/tv.blend
deleted file mode 100644
index 490e298e7b..0000000000
Binary files a/assets/client/room/furnitures/tv/tv.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/tv/tv.glb b/assets/client/room/furnitures/tv/tv.glb
deleted file mode 100644
index b9bd23896b..0000000000
Binary files a/assets/client/room/furnitures/tv/tv.glb and /dev/null differ
diff --git a/assets/client/room/furnitures/wall-clock/wall-clock.blend b/assets/client/room/furnitures/wall-clock/wall-clock.blend
deleted file mode 100644
index 0a61c8f01e..0000000000
Binary files a/assets/client/room/furnitures/wall-clock/wall-clock.blend and /dev/null differ
diff --git a/assets/client/room/furnitures/wall-clock/wall-clock.glb b/assets/client/room/furnitures/wall-clock/wall-clock.glb
deleted file mode 100644
index b9f0093a8d..0000000000
Binary files a/assets/client/room/furnitures/wall-clock/wall-clock.glb and /dev/null differ
diff --git a/assets/client/room/rooms/default/default.blend b/assets/client/room/rooms/default/default.blend
deleted file mode 100644
index 661154724a..0000000000
Binary files a/assets/client/room/rooms/default/default.blend and /dev/null differ
diff --git a/assets/client/room/rooms/default/default.glb b/assets/client/room/rooms/default/default.glb
deleted file mode 100644
index 3d378deee2..0000000000
Binary files a/assets/client/room/rooms/default/default.glb and /dev/null differ
diff --git a/assets/client/room/rooms/washitsu/husuma-uv.png b/assets/client/room/rooms/washitsu/husuma-uv.png
deleted file mode 100644
index ae2fca3911..0000000000
Binary files a/assets/client/room/rooms/washitsu/husuma-uv.png and /dev/null differ
diff --git a/assets/client/room/rooms/washitsu/husuma.png b/assets/client/room/rooms/washitsu/husuma.png
deleted file mode 100644
index 084cbed67c..0000000000
Binary files a/assets/client/room/rooms/washitsu/husuma.png and /dev/null differ
diff --git a/assets/client/room/rooms/washitsu/tatami-single1600.png b/assets/client/room/rooms/washitsu/tatami-single1600.png
deleted file mode 100644
index c0e684d743..0000000000
Binary files a/assets/client/room/rooms/washitsu/tatami-single1600.png and /dev/null differ
diff --git a/assets/client/room/rooms/washitsu/tatami-uv.png b/assets/client/room/rooms/washitsu/tatami-uv.png
deleted file mode 100644
index 5b16c66091..0000000000
Binary files a/assets/client/room/rooms/washitsu/tatami-uv.png and /dev/null differ
diff --git a/assets/client/room/rooms/washitsu/tatami.afdesign b/assets/client/room/rooms/washitsu/tatami.afdesign
deleted file mode 100644
index 9300a26950..0000000000
Binary files a/assets/client/room/rooms/washitsu/tatami.afdesign and /dev/null differ
diff --git a/assets/client/room/rooms/washitsu/tatami.png b/assets/client/room/rooms/washitsu/tatami.png
deleted file mode 100644
index 8894d040ae..0000000000
Binary files a/assets/client/room/rooms/washitsu/tatami.png and /dev/null differ
diff --git a/assets/client/room/rooms/washitsu/washitsu.blend b/assets/client/room/rooms/washitsu/washitsu.blend
deleted file mode 100644
index 84dc11374d..0000000000
Binary files a/assets/client/room/rooms/washitsu/washitsu.blend and /dev/null differ
diff --git a/assets/client/room/rooms/washitsu/washitsu.glb b/assets/client/room/rooms/washitsu/washitsu.glb
deleted file mode 100644
index 5b4767bc73..0000000000
Binary files a/assets/client/room/rooms/washitsu/washitsu.glb and /dev/null differ
diff --git a/assets/client/sounds/aisha/1.mp3 b/assets/client/sounds/aisha/1.mp3
deleted file mode 100644
index d8e9a2f265..0000000000
Binary files a/assets/client/sounds/aisha/1.mp3 and /dev/null differ
diff --git a/assets/client/sounds/aisha/2.mp3 b/assets/client/sounds/aisha/2.mp3
deleted file mode 100644
index 477c2eba43..0000000000
Binary files a/assets/client/sounds/aisha/2.mp3 and /dev/null differ
diff --git a/assets/client/sounds/aisha/3.mp3 b/assets/client/sounds/aisha/3.mp3
deleted file mode 100644
index fe0d8063df..0000000000
Binary files a/assets/client/sounds/aisha/3.mp3 and /dev/null differ
diff --git a/assets/client/sounds/noizenecio/kick_gaba.mp3 b/assets/client/sounds/noizenecio/kick_gaba.mp3
deleted file mode 100644
index 616b506c4f..0000000000
Binary files a/assets/client/sounds/noizenecio/kick_gaba.mp3 and /dev/null differ
diff --git a/assets/client/sounds/noizenecio/kick_gaba2.mp3 b/assets/client/sounds/noizenecio/kick_gaba2.mp3
deleted file mode 100644
index 33c2837620..0000000000
Binary files a/assets/client/sounds/noizenecio/kick_gaba2.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/down.mp3 b/assets/client/sounds/syuilo/down.mp3
deleted file mode 100644
index 4cd421139d..0000000000
Binary files a/assets/client/sounds/syuilo/down.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/kick.mp3 b/assets/client/sounds/syuilo/kick.mp3
deleted file mode 100644
index 4e0e72091c..0000000000
Binary files a/assets/client/sounds/syuilo/kick.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/pirori-square-wet.mp3 b/assets/client/sounds/syuilo/pirori-square-wet.mp3
deleted file mode 100644
index babf1fce60..0000000000
Binary files a/assets/client/sounds/syuilo/pirori-square-wet.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/pirori-wet.mp3 b/assets/client/sounds/syuilo/pirori-wet.mp3
deleted file mode 100644
index 25e2c46a64..0000000000
Binary files a/assets/client/sounds/syuilo/pirori-wet.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/pirori.mp3 b/assets/client/sounds/syuilo/pirori.mp3
deleted file mode 100644
index a745415ac0..0000000000
Binary files a/assets/client/sounds/syuilo/pirori.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/poi1.mp3 b/assets/client/sounds/syuilo/poi1.mp3
deleted file mode 100644
index 59dae90965..0000000000
Binary files a/assets/client/sounds/syuilo/poi1.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/poi2.mp3 b/assets/client/sounds/syuilo/poi2.mp3
deleted file mode 100644
index a65c653891..0000000000
Binary files a/assets/client/sounds/syuilo/poi2.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/pope1.mp3 b/assets/client/sounds/syuilo/pope1.mp3
deleted file mode 100644
index d6f53cfacc..0000000000
Binary files a/assets/client/sounds/syuilo/pope1.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/pope2.mp3 b/assets/client/sounds/syuilo/pope2.mp3
deleted file mode 100644
index fe5d95e292..0000000000
Binary files a/assets/client/sounds/syuilo/pope2.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/popo.mp3 b/assets/client/sounds/syuilo/popo.mp3
deleted file mode 100644
index a2a1605bbb..0000000000
Binary files a/assets/client/sounds/syuilo/popo.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/queue-jammed.mp3 b/assets/client/sounds/syuilo/queue-jammed.mp3
deleted file mode 100644
index 99e0c437fe..0000000000
Binary files a/assets/client/sounds/syuilo/queue-jammed.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/reverved.mp3 b/assets/client/sounds/syuilo/reverved.mp3
deleted file mode 100644
index 47588ef270..0000000000
Binary files a/assets/client/sounds/syuilo/reverved.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/ryukyu.mp3 b/assets/client/sounds/syuilo/ryukyu.mp3
deleted file mode 100644
index 9e935e3f37..0000000000
Binary files a/assets/client/sounds/syuilo/ryukyu.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/snare.mp3 b/assets/client/sounds/syuilo/snare.mp3
deleted file mode 100644
index 9244189c2d..0000000000
Binary files a/assets/client/sounds/syuilo/snare.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/square-pico.mp3 b/assets/client/sounds/syuilo/square-pico.mp3
deleted file mode 100644
index c4d8305ae7..0000000000
Binary files a/assets/client/sounds/syuilo/square-pico.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/triple.mp3 b/assets/client/sounds/syuilo/triple.mp3
deleted file mode 100644
index 54ab974d46..0000000000
Binary files a/assets/client/sounds/syuilo/triple.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/up.mp3 b/assets/client/sounds/syuilo/up.mp3
deleted file mode 100644
index 3f30867764..0000000000
Binary files a/assets/client/sounds/syuilo/up.mp3 and /dev/null differ
diff --git a/assets/client/sounds/syuilo/waon.mp3 b/assets/client/sounds/syuilo/waon.mp3
deleted file mode 100644
index a4af473861..0000000000
Binary files a/assets/client/sounds/syuilo/waon.mp3 and /dev/null differ
diff --git a/assets/client/sparkle-spritesheet.png b/assets/client/sparkle-spritesheet.png
deleted file mode 100644
index 0defccaf07..0000000000
Binary files a/assets/client/sparkle-spritesheet.png and /dev/null differ
diff --git a/assets/client/unread.svg b/assets/client/unread.svg
deleted file mode 100644
index 8c3cc9f475..0000000000
--- a/assets/client/unread.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
-	 y="0px" width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
-<circle fill="#3AA2DC" cx="16.5" cy="16.5" r="6"/>
-</svg>
diff --git a/assets/favicon.ico b/assets/favicon.ico
deleted file mode 100644
index 9be1ff6295..0000000000
Binary files a/assets/favicon.ico and /dev/null differ
diff --git a/assets/favicon.png b/assets/favicon.png
deleted file mode 100644
index b4eb18a5cb..0000000000
Binary files a/assets/favicon.png and /dev/null differ
diff --git a/assets/icons/192.png b/assets/icons/192.png
deleted file mode 100644
index 606b46d87c..0000000000
Binary files a/assets/icons/192.png and /dev/null differ
diff --git a/assets/icons/512.png b/assets/icons/512.png
deleted file mode 100644
index ba51546427..0000000000
Binary files a/assets/icons/512.png and /dev/null differ
diff --git a/assets/mi-white.png b/assets/mi-white.png
deleted file mode 100644
index 1e57da6b38..0000000000
Binary files a/assets/mi-white.png and /dev/null differ
diff --git a/assets/redoc.html b/assets/redoc.html
deleted file mode 100644
index 9ee5a95c05..0000000000
--- a/assets/redoc.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!DOCTYPE html>
-<html>
-	<head>
-		<title>Misskey API</title>
-		<!-- needed for adaptive design -->
-		<meta charset="utf-8"/>
-		<meta name="viewport" content="width=device-width, initial-scale=1">
-		<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
-
-		<!--
-		ReDoc doesn't change outer page styles
-		-->
-		<style>
-			body {
-				margin: 0;
-				padding: 0;
-			}
-		</style>
-	</head>
-	<body>
-		<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
-		<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script>
-	</body>
-</html>
diff --git a/assets/robots.txt b/assets/robots.txt
deleted file mode 100644
index dc17e04e3f..0000000000
--- a/assets/robots.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-user-agent: *
-allow: /
-
-# todo: sitemap
diff --git a/assets/user-unknown.png b/assets/user-unknown.png
deleted file mode 100644
index ab29bef2b1..0000000000
Binary files a/assets/user-unknown.png and /dev/null differ
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000000..91d3a3ade7
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,71 @@
+/**
+ * Gulp tasks
+ */
+
+const fs = require('fs');
+const gulp = require('gulp');
+const rimraf = require('rimraf');
+const replace = require('gulp-replace');
+const terser = require('gulp-terser');
+const cssnano = require('gulp-cssnano');
+
+const locales = require('./locales');
+const meta = require('./package.json');
+
+gulp.task('copy:backend:views', () =>
+	gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
+);
+
+gulp.task('copy:client:fonts', () =>
+	gulp.src('./packages/client/node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/_client_dist_/fonts/'))
+);
+
+gulp.task('copy:client:locales', cb => {
+	fs.mkdirSync('./built/_client_dist_/locales', { recursive: true });
+
+	const v = { '_version_': meta.version };
+
+	for (const [lang, locale] of Object.entries(locales)) {
+		fs.writeFileSync(`./built/_client_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8');
+	}
+
+	cb();
+});
+
+gulp.task('build:backend:script', () => {
+	return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js'])
+		.pipe(replace('VERSION', JSON.stringify(meta.version)))
+		.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
+		.pipe(terser({
+			toplevel: true
+		}))
+		.pipe(gulp.dest('./packages/backend/built/server/web/'));
+});
+
+gulp.task('build:backend:style', () => {
+	return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css'])
+		.pipe(cssnano({
+			zindex: false
+		}))
+		.pipe(gulp.dest('./packages/backend/built/server/web/'));
+});
+
+gulp.task('clean', cb =>
+	rimraf('./built', cb)
+);
+
+gulp.task('cleanall', gulp.parallel('clean', cb =>
+	rimraf('./node_modules', cb)
+));
+
+gulp.task('build', gulp.parallel(
+	'copy:client:locales', 'copy:backend:views', 'build:backend:script', 'build:backend:style', 'copy:client:fonts'
+));
+
+gulp.task('default', gulp.task('build'));
+
+gulp.task('watch', () => {
+	gulp.watch([
+		'./packages/**/*',
+	], { ignoreInitial: false }, gulp.task('build'));
+});
diff --git a/gulpfile.ts b/gulpfile.ts
deleted file mode 100644
index 2cdcbdb6fc..0000000000
--- a/gulpfile.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * Gulp tasks
- */
-
-import * as fs from 'fs';
-import * as gulp from 'gulp';
-import rimraf from 'rimraf';
-const replace = require('gulp-replace');
-const terser = require('gulp-terser');
-const cssnano = require('gulp-cssnano');
-
-const locales: { [x: string]: any } = require('./locales');
-const meta = require('./package.json');
-
-gulp.task('build:copy:views', () =>
-	gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
-);
-
-gulp.task('build:copy:fonts', () =>
-	gulp.src('./node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/assets/fonts/'))
-);
-
-gulp.task('build:copy:locales', cb => {
-	fs.mkdirSync('./built/assets/locales', { recursive: true });
-
-	const v = { '_version_': meta.version };
-
-	for (const [lang, locale] of Object.entries(locales)) {
-		fs.writeFileSync(`./built/assets/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8');
-	}
-
-	cb();
-});
-
-gulp.task('build:client:script', () => {
-	return gulp.src(['./src/server/web/boot.js', './src/server/web/bios.js', './src/server/web/cli.js'])
-		.pipe(replace('VERSION', JSON.stringify(meta.version)))
-		.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
-		.pipe(terser({
-			toplevel: true
-		}))
-		.pipe(gulp.dest('./built/server/web/'));
-});
-
-gulp.task('build:client:style', () => {
-	return gulp.src(['./src/server/web/style.css', './src/server/web/bios.css', './src/server/web/cli.css'])
-		.pipe(cssnano({
-			zindex: false
-		}))
-		.pipe(gulp.dest('./built/server/web/'));
-});
-
-gulp.task('build:copy', gulp.parallel('build:copy:locales', 'build:copy:views', 'build:client:script', 'build:client:style', 'build:copy:fonts', () =>
-	gulp.src([
-		'./src/emojilist.json',
-		'./src/**/assets/**/*',
-		'!./src/client/assets/**/*'
-	]).pipe(gulp.dest('./built/'))
-));
-
-gulp.task('clean', cb =>
-	rimraf('./built', cb)
-);
-
-gulp.task('cleanall', gulp.parallel('clean', cb =>
-	rimraf('./node_modules', cb)
-));
-
-gulp.task('build', gulp.parallel(
-	'build:copy',
-));
-
-gulp.task('default', gulp.task('build'));
-
-gulp.task('watch', () => {
-	gulp.watch([
-		'./src/**/*',
-		'!./src/client/**/*'
-	], { ignoreInitial: false }, gulp.task('build'));
-});
diff --git a/index.js b/index.js
deleted file mode 100644
index aa103e667e..0000000000
--- a/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
-import * as fs from 'fs';
-
-if (fs.existsSync('./built')) {
-	import('./built/index.js').then(built => built());
-} else {
-	console.log('Built code is not found. Probably an error occurred during a build or you just forgot to build.');
-}
-*/
-
-const fs = require('fs');
-
-if (fs.existsSync('./built')) {
-	require('./built').default();
-} else {
-	console.log('Built code is not found. Probably an error occurred during a build or you just forgot to build.');
-}
diff --git a/migration/1000000000000-Init.ts b/migration/1000000000000-Init.ts
deleted file mode 100644
index ce54d8a7ef..0000000000
--- a/migration/1000000000000-Init.ts
+++ /dev/null
@@ -1,485 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class Init1000000000000 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TYPE "log_level_enum" AS ENUM('error', 'warning', 'info', 'success', 'debug')`);
-        await queryRunner.query(`CREATE TABLE "log" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "domain" character varying(64) array NOT NULL DEFAULT '{}'::varchar[], "level" "log_level_enum" NOT NULL, "worker" character varying(8) NOT NULL, "machine" character varying(128) NOT NULL, "message" character varying(1024) NOT NULL, "data" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_350604cbdf991d5930d9e618fbd" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_8e4eb51a35d81b64dda28eed0a" ON "log" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_8cb40cfc8f3c28261e6f887b03" ON "log" ("domain") `);
-        await queryRunner.query(`CREATE INDEX "IDX_584b536b49e53ac81beb39a177" ON "log" ("level") `);
-        await queryRunner.query(`CREATE TABLE "drive_folder" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(128) NOT NULL, "userId" character varying(32), "parentId" character varying(32), CONSTRAINT "PK_7a0c089191f5ebdc214e0af808a" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_02878d441ceae15ce060b73daf" ON "drive_folder" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f4fc06e49c0171c85f1c48060d" ON "drive_folder" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_00ceffb0cdc238b3233294f08f" ON "drive_folder" ("parentId") `);
-        await queryRunner.query(`CREATE TABLE "drive_file" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32), "userHost" character varying(128), "md5" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "type" character varying(128) NOT NULL, "size" integer NOT NULL, "comment" character varying(512), "properties" jsonb NOT NULL DEFAULT '{}', "storedInternal" boolean NOT NULL, "url" character varying(512) NOT NULL, "thumbnailUrl" character varying(512), "webpublicUrl" character varying(512), "accessKey" character varying(256), "thumbnailAccessKey" character varying(256), "webpublicAccessKey" character varying(256), "uri" character varying(512), "src" character varying(512), "folderId" character varying(32), "isSensitive" boolean NOT NULL DEFAULT false, "isLink" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_43ddaaaf18c9e68029b7cbb032e" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_860fa6f6c7df5bb887249fba22" ON "drive_file" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_92779627994ac79277f070c91e" ON "drive_file" ("userHost") `);
-        await queryRunner.query(`CREATE INDEX "IDX_37bb9a1b4585f8a3beb24c62d6" ON "drive_file" ("md5") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a40b8df8c989d7db937ea27cf6" ON "drive_file" ("type") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d85a184c2540d2deba33daf642" ON "drive_file" ("accessKey") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e74022ce9a074b3866f70e0d27" ON "drive_file" ("thumbnailAccessKey") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c55b2b7c284d9fef98026fc88e" ON "drive_file" ("webpublicAccessKey") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e5848eac4940934e23dbc17581" ON "drive_file" ("uri") `);
-        await queryRunner.query(`CREATE INDEX "IDX_bb90d1956dafc4068c28aa7560" ON "drive_file" ("folderId") `);
-        await queryRunner.query(`CREATE TABLE "user" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "lastFetchedAt" TIMESTAMP WITH TIME ZONE, "username" character varying(128) NOT NULL, "usernameLower" character varying(128) NOT NULL, "name" character varying(128), "followersCount" integer NOT NULL DEFAULT 0, "followingCount" integer NOT NULL DEFAULT 0, "notesCount" integer NOT NULL DEFAULT 0, "avatarId" character varying(32), "bannerId" character varying(32), "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "avatarUrl" character varying(512), "bannerUrl" character varying(512), "avatarColor" character varying(32), "bannerColor" character varying(32), "isSuspended" boolean NOT NULL DEFAULT false, "isSilenced" boolean NOT NULL DEFAULT false, "isLocked" boolean NOT NULL DEFAULT false, "isBot" boolean NOT NULL DEFAULT false, "isCat" boolean NOT NULL DEFAULT false, "isAdmin" boolean NOT NULL DEFAULT false, "isModerator" boolean NOT NULL DEFAULT false, "isVerified" boolean NOT NULL DEFAULT false, "emojis" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "host" character varying(128), "inbox" character varying(512), "sharedInbox" character varying(512), "featured" character varying(512), "uri" character varying(512), "token" character(16), CONSTRAINT "UQ_a854e557b1b14814750c7c7b0c9" UNIQUE ("token"), CONSTRAINT "REL_58f5c71eaab331645112cf8cfa" UNIQUE ("avatarId"), CONSTRAINT "REL_afc64b53f8db3707ceb34eb28e" UNIQUE ("bannerId"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_e11e649824a45d8ed01d597fd9" ON "user" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_80ca6e6ef65fb9ef34ea8c90f4" ON "user" ("updatedAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a27b942a0d6dcff90e3ee9b5e8" ON "user" ("usernameLower") `);
-        await queryRunner.query(`CREATE INDEX "IDX_fa99d777623947a5b05f394cae" ON "user" ("tags") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3252a5df8d5bbd16b281f7799e" ON "user" ("host") `);
-        await queryRunner.query(`CREATE INDEX "IDX_be623adaa4c566baf5d29ce0c8" ON "user" ("uri") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a854e557b1b14814750c7c7b0c" ON "user" ("token") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5deb01ae162d1d70b80d064c27" ON "user" ("usernameLower", "host") `);
-        await queryRunner.query(`CREATE TABLE "app" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32), "secret" character varying(64) NOT NULL, "name" character varying(128) NOT NULL, "description" character varying(512) NOT NULL, "permission" character varying(64) array NOT NULL, "callbackUrl" character varying(512), CONSTRAINT "PK_9478629fc093d229df09e560aea" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_048a757923ed8b157e9895da53" ON "app" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3f5b0899ef90527a3462d7c2cb" ON "app" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f49922d511d666848f250663c4" ON "app" ("secret") `);
-        await queryRunner.query(`CREATE TABLE "access_token" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(128) NOT NULL, "hash" character varying(128) NOT NULL, "userId" character varying(32) NOT NULL, "appId" character varying(32) NOT NULL, CONSTRAINT "PK_f20f028607b2603deabd8182d12" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_70ba8f6af34bc924fc9e12adb8" ON "access_token" ("token") `);
-        await queryRunner.query(`CREATE INDEX "IDX_64c327441248bae40f7d92f34f" ON "access_token" ("hash") `);
-        await queryRunner.query(`CREATE INDEX "IDX_9949557d0e1b2c19e5344c171e" ON "access_token" ("userId") `);
-        await queryRunner.query(`CREATE TYPE "note_visibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`);
-        await queryRunner.query(`CREATE TABLE "note" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "replyId" character varying(32), "renoteId" character varying(32), "text" text, "name" character varying(256), "cw" character varying(512), "appId" character varying(32), "userId" character varying(32) NOT NULL, "viaMobile" boolean NOT NULL DEFAULT false, "localOnly" boolean NOT NULL DEFAULT false, "renoteCount" smallint NOT NULL DEFAULT 0, "repliesCount" smallint NOT NULL DEFAULT 0, "reactions" jsonb NOT NULL DEFAULT '{}', "visibility" "note_visibility_enum" NOT NULL, "uri" character varying(512), "score" integer NOT NULL DEFAULT 0, "fileIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "attachedFileTypes" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "mentions" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "mentionedRemoteUsers" text NOT NULL DEFAULT '[]', "emojis" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "hasPoll" boolean NOT NULL DEFAULT false, "geo" jsonb DEFAULT null, "userHost" character varying(128), "replyUserId" character varying(32), "replyUserHost" character varying(128), "renoteUserId" character varying(32), "renoteUserHost" character varying(128), CONSTRAINT "PK_96d0c172a4fba276b1bbed43058" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_17cb3553c700a4985dff5a30ff" ON "note" ("replyId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_52ccc804d7c69037d558bac4c9" ON "note" ("renoteId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_153536c67d05e9adb24e99fc2b" ON "note" ("uri") `);
-        await queryRunner.query(`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds") `);
-        await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `);
-        await queryRunner.query(`CREATE INDEX "IDX_796a8c03959361f97dc2be1d5c" ON "note" ("visibleUserIds") `);
-        await queryRunner.query(`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions") `);
-        await queryRunner.query(`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7125a826ab192eb27e11d358a5" ON "note" ("userHost") `);
-        await queryRunner.query(`CREATE TABLE "poll_vote" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "choice" integer NOT NULL, CONSTRAINT "PK_fd002d371201c472490ba89c6a0" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_0fb627e1c2f753262a74f0562d" ON "poll_vote" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_66d2bd2ee31d14bcc23069a89f" ON "poll_vote" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_aecfbd5ef60374918e63ee95fa" ON "poll_vote" ("noteId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_50bd7164c5b78f1f4a42c4d21f" ON "poll_vote" ("userId", "noteId", "choice") `);
-        await queryRunner.query(`CREATE TABLE "note_reaction" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "reaction" character varying(128) NOT NULL, CONSTRAINT "PK_767ec729b108799b587a3fcc9cf" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_13761f64257f40c5636d0ff95e" ON "note_reaction" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_45145e4953780f3cd5656f0ea6" ON "note_reaction" ("noteId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") `);
-        await queryRunner.query(`CREATE TABLE "note_watching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "noteUserId" character varying(32) NOT NULL, CONSTRAINT "PK_49286fdb23725945a74aa27d757" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_318cdf42a9cfc11f479bd802bb" ON "note_watching" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_b0134ec406e8d09a540f818288" ON "note_watching" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_03e7028ab8388a3f5e3ce2a861" ON "note_watching" ("noteId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_44499765eec6b5489d72c4253b" ON "note_watching" ("noteUserId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a42c93c69989ce1d09959df4cf" ON "note_watching" ("userId", "noteId") `);
-        await queryRunner.query(`CREATE TABLE "note_unread" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "noteUserId" character varying(32) NOT NULL, "isSpecified" boolean NOT NULL, CONSTRAINT "PK_1904eda61a784f57e6e51fa9c1f" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_56b0166d34ddae49d8ef7610bb" ON "note_unread" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e637cba4dc4410218c4251260e" ON "note_unread" ("noteId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d908433a4953cc13216cd9c274" ON "note_unread" ("userId", "noteId") `);
-        await queryRunner.query(`CREATE TABLE "notification" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "notifieeId" character varying(32) NOT NULL, "notifierId" character varying(32) NOT NULL, "type" character varying(32) NOT NULL, "isRead" boolean NOT NULL DEFAULT false, "noteId" character varying(32), "reaction" character varying(128), "choice" integer, CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON "notification" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3c601b70a1066d2c8b517094cb" ON "notification" ("notifieeId") `);
-        await queryRunner.query(`CREATE TABLE "meta" ("id" character varying(32) NOT NULL, "name" character varying(128), "description" character varying(1024), "maintainerName" character varying(128), "maintainerEmail" character varying(128), "announcements" jsonb NOT NULL DEFAULT '[]', "disableRegistration" boolean NOT NULL DEFAULT false, "disableLocalTimeline" boolean NOT NULL DEFAULT false, "disableGlobalTimeline" boolean NOT NULL DEFAULT false, "enableEmojiReaction" boolean NOT NULL DEFAULT true, "useStarForReactionFallback" boolean NOT NULL DEFAULT false, "langs" character varying(64) array NOT NULL DEFAULT '{}'::varchar[], "hiddenTags" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "blockedHosts" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "mascotImageUrl" character varying(512) DEFAULT '/assets/ai.png', "bannerUrl" character varying(512), "errorImageUrl" character varying(512) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png', "iconUrl" character varying(512), "cacheRemoteFiles" boolean NOT NULL DEFAULT true, "proxyAccount" character varying(128), "enableRecaptcha" boolean NOT NULL DEFAULT false, "recaptchaSiteKey" character varying(64), "recaptchaSecretKey" character varying(64), "localDriveCapacityMb" integer NOT NULL DEFAULT 1024, "remoteDriveCapacityMb" integer NOT NULL DEFAULT 32, "maxNoteTextLength" integer NOT NULL DEFAULT 500, "summalyProxy" character varying(128), "enableEmail" boolean NOT NULL DEFAULT false, "email" character varying(128), "smtpSecure" boolean NOT NULL DEFAULT false, "smtpHost" character varying(128), "smtpPort" integer, "smtpUser" character varying(128), "smtpPass" character varying(128), "enableServiceWorker" boolean NOT NULL DEFAULT false, "swPublicKey" character varying(128), "swPrivateKey" character varying(128), "enableTwitterIntegration" boolean NOT NULL DEFAULT false, "twitterConsumerKey" character varying(128), "twitterConsumerSecret" character varying(128), "enableGithubIntegration" boolean NOT NULL DEFAULT false, "githubClientId" character varying(128), "githubClientSecret" character varying(128), "enableDiscordIntegration" boolean NOT NULL DEFAULT false, "discordClientId" character varying(128), "discordClientSecret" character varying(128), CONSTRAINT "PK_c4c17a6c2bd7651338b60fc590b" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TABLE "following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, "followerHost" character varying(128), "followerInbox" character varying(512), "followerSharedInbox" character varying(512), "followeeHost" character varying(128), "followeeInbox" character varying(512), "followeeSharedInbox" character varying(512), CONSTRAINT "PK_c76c6e044bdf76ecf8bfb82a645" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_582f8fab771a9040a12961f3e7" ON "following" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_24e0042143a18157b234df186c" ON "following" ("followeeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_6516c5a6f3c015b4eed39978be" ON "following" ("followerId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_307be5f1d1252e0388662acb96" ON "following" ("followerId", "followeeId") `);
-        await queryRunner.query(`CREATE TABLE "instance" ("id" character varying(32) NOT NULL, "caughtAt" TIMESTAMP WITH TIME ZONE NOT NULL, "host" character varying(128) NOT NULL, "system" character varying(64), "usersCount" integer NOT NULL DEFAULT 0, "notesCount" integer NOT NULL DEFAULT 0, "followingCount" integer NOT NULL DEFAULT 0, "followersCount" integer NOT NULL DEFAULT 0, "driveUsage" integer NOT NULL DEFAULT 0, "driveFiles" integer NOT NULL DEFAULT 0, "latestRequestSentAt" TIMESTAMP WITH TIME ZONE, "latestStatus" integer, "latestRequestReceivedAt" TIMESTAMP WITH TIME ZONE, "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "isNotResponding" boolean NOT NULL DEFAULT false, "isMarkedAsClosed" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_eaf60e4a0c399c9935413e06474" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_2cd3b2a6b4cf0b910b260afe08" ON "instance" ("caughtAt") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8d5afc98982185799b160e10eb" ON "instance" ("host") `);
-        await queryRunner.query(`CREATE TABLE "muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_2e92d06c8b5c602eeb27ca9ba48" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_f86d57fbca33c7a4e6897490cc" ON "muting" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_ec96b4fed9dae517e0dbbe0675" ON "muting" ("muteeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_93060675b4a79a577f31d260c6" ON "muting" ("muterId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_1eb9d9824a630321a29fd3b290" ON "muting" ("muterId", "muteeId") `);
-        await queryRunner.query(`CREATE TABLE "sw_subscription" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "endpoint" character varying(512) NOT NULL, "auth" character varying(256) NOT NULL, "publickey" character varying(128) NOT NULL, CONSTRAINT "PK_e8f763631530051b95eb6279b91" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_97754ca6f2baff9b4abb7f853d" ON "sw_subscription" ("userId") `);
-        await queryRunner.query(`CREATE TABLE "blocking" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "blockeeId" character varying(32) NOT NULL, "blockerId" character varying(32) NOT NULL, CONSTRAINT "PK_e5d9a541cc1965ee7e048ea09dd" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_b9a354f7941c1e779f3b33aea6" ON "blocking" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_2cd4a2743a99671308f5417759" ON "blocking" ("blockeeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0627125f1a8a42c9a1929edb55" ON "blocking" ("blockerId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_98a1bc5cb30dfd159de056549f" ON "blocking" ("blockerId", "blockeeId") `);
-        await queryRunner.query(`CREATE TABLE "user_list" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, CONSTRAINT "PK_87bab75775fd9b1ff822b656402" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_b7fcefbdd1c18dce86687531f9" ON "user_list" ("userId") `);
-        await queryRunner.query(`CREATE TABLE "user_list_joining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userListId" character varying(32) NOT NULL, CONSTRAINT "PK_11abb3768da1c5f8de101c9df45" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_joining" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_joining" ("userListId") `);
-        await queryRunner.query(`CREATE TABLE "hashtag" ("id" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "mentionedUserIds" character varying(32) array NOT NULL, "mentionedUsersCount" integer NOT NULL DEFAULT 0, "mentionedLocalUserIds" character varying(32) array NOT NULL, "mentionedLocalUsersCount" integer NOT NULL DEFAULT 0, "mentionedRemoteUserIds" character varying(32) array NOT NULL, "mentionedRemoteUsersCount" integer NOT NULL DEFAULT 0, "attachedUserIds" character varying(32) array NOT NULL, "attachedUsersCount" integer NOT NULL DEFAULT 0, "attachedLocalUserIds" character varying(32) array NOT NULL, "attachedLocalUsersCount" integer NOT NULL DEFAULT 0, "attachedRemoteUserIds" character varying(32) array NOT NULL, "attachedRemoteUsersCount" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_cb36eb8af8412bfa978f1165d78" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_347fec870eafea7b26c8a73bac" ON "hashtag" ("name") `);
-        await queryRunner.query(`CREATE INDEX "IDX_2710a55f826ee236ea1a62698f" ON "hashtag" ("mentionedUsersCount") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0e206cec573f1edff4a3062923" ON "hashtag" ("mentionedLocalUsersCount") `);
-        await queryRunner.query(`CREATE INDEX "IDX_4c02d38a976c3ae132228c6fce" ON "hashtag" ("mentionedRemoteUsersCount") `);
-        await queryRunner.query(`CREATE INDEX "IDX_d57f9030cd3af7f63ffb1c267c" ON "hashtag" ("attachedUsersCount") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0c44bf4f680964145f2a68a341" ON "hashtag" ("attachedLocalUsersCount") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0b03cbcd7e6a7ce068efa8ecc2" ON "hashtag" ("attachedRemoteUsersCount") `);
-        await queryRunner.query(`CREATE TABLE "note_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_af0da35a60b9fa4463a62082b36" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_47f4b1892f5d6ba8efb3057d81" ON "note_favorite" ("userId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0f4fb9ad355f3effff221ef245" ON "note_favorite" ("userId", "noteId") `);
-        await queryRunner.query(`CREATE TABLE "abuse_user_report" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "reporterId" character varying(32) NOT NULL, "comment" character varying(512) NOT NULL, CONSTRAINT "PK_87873f5f5cc5c321a1306b2d18c" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_db2098070b2b5a523c58181f74" ON "abuse_user_report" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_d049123c413e68ca52abe73420" ON "abuse_user_report" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_04cc96756f89d0b7f9473e8cdf" ON "abuse_user_report" ("reporterId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5cd442c3b2e74fdd99dae20243" ON "abuse_user_report" ("userId", "reporterId") `);
-        await queryRunner.query(`CREATE TABLE "registration_ticket" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(64) NOT NULL, CONSTRAINT "PK_f11696b6fafcf3662d4292734f8" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0ff69e8dfa9fe31bb4a4660f59" ON "registration_ticket" ("code") `);
-        await queryRunner.query(`CREATE TABLE "messaging_message" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "recipientId" character varying(32) NOT NULL, "text" character varying(4096), "isRead" boolean NOT NULL DEFAULT false, "fileId" character varying(32), CONSTRAINT "PK_db398fd79dc95d0eb8c30456eaa" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_e21cd3646e52ef9c94aaf17c2e" ON "messaging_message" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_5377c307783fce2b6d352e1203" ON "messaging_message" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_cac14a4e3944454a5ce7daa514" ON "messaging_message" ("recipientId") `);
-        await queryRunner.query(`CREATE TABLE "signin" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "ip" character varying(128) NOT NULL, "headers" jsonb NOT NULL, "success" boolean NOT NULL, CONSTRAINT "PK_9e96ddc025712616fc492b3b588" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_2c308dbdc50d94dc625670055f" ON "signin" ("userId") `);
-        await queryRunner.query(`CREATE TABLE "auth_session" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(128) NOT NULL, "userId" character varying(32), "appId" character varying(32) NOT NULL, CONSTRAINT "PK_19354ed146424a728c1112a8cbf" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_62cb09e1129f6ec024ef66e183" ON "auth_session" ("token") `);
-        await queryRunner.query(`CREATE TABLE "follow_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, "requestId" character varying(128), "followerHost" character varying(128), "followerInbox" character varying(512), "followerSharedInbox" character varying(512), "followeeHost" character varying(128), "followeeInbox" character varying(512), "followeeSharedInbox" character varying(512), CONSTRAINT "PK_53a9aa3725f7a3deb150b39dbfc" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_12c01c0d1a79f77d9f6c15fadd" ON "follow_request" ("followeeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a7fd92dd6dc519e6fb435dd108" ON "follow_request" ("followerId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d54a512b822fac7ed52800f6b4" ON "follow_request" ("followerId", "followeeId") `);
-        await queryRunner.query(`CREATE TABLE "emoji" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "name" character varying(128) NOT NULL, "host" character varying(128), "url" character varying(512) NOT NULL, "uri" character varying(512), "type" character varying(64), "aliases" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_df74ce05e24999ee01ea0bc50a3" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_b37dafc86e9af007e3295c2781" ON "emoji" ("name") `);
-        await queryRunner.query(`CREATE INDEX "IDX_5900e907bb46516ddf2871327c" ON "emoji" ("host") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4f4d35e1256c84ae3d1f0eab10" ON "emoji" ("name", "host") `);
-        await queryRunner.query(`CREATE TABLE "reversi_game" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32) NOT NULL, "user2Id" character varying(32) NOT NULL, "user1Accepted" boolean NOT NULL DEFAULT false, "user2Accepted" boolean NOT NULL DEFAULT false, "black" integer, "isStarted" boolean NOT NULL DEFAULT false, "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "surrendered" character varying(32), "logs" jsonb NOT NULL DEFAULT '[]', "map" character varying(64) array NOT NULL, "bw" character varying(32) NOT NULL, "isLlotheo" boolean NOT NULL DEFAULT false, "canPutEverywhere" boolean NOT NULL DEFAULT false, "loopedBoard" boolean NOT NULL DEFAULT false, "form1" jsonb DEFAULT null, "form2" jsonb DEFAULT null, "crc32" character varying(32), CONSTRAINT "PK_76b30eeba71b1193ad7c5311c3f" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt") `);
-        await queryRunner.query(`CREATE TABLE "reversi_matching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "parentId" character varying(32) NOT NULL, "childId" character varying(32) NOT NULL, CONSTRAINT "PK_880bd0afbab232f21c8b9d146cf" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3b25402709dd9882048c2bbade" ON "reversi_matching" ("parentId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId") `);
-        await queryRunner.query(`CREATE TABLE "user_note_pining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_a6a2dad4ae000abce2ea9d9b103" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_bfbc6f79ba4007b4ce5097f08d" ON "user_note_pining" ("userId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_410cd649884b501c02d6e72738" ON "user_note_pining" ("userId", "noteId") `);
-        await queryRunner.query(`CREATE TYPE "poll_notevisibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`);
-        await queryRunner.query(`CREATE TABLE "poll" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE, "multiple" boolean NOT NULL, "choices" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "votes" integer array NOT NULL, "noteVisibility" "poll_notevisibility_enum" NOT NULL, "userId" character varying(32) NOT NULL, "userHost" character varying(128), CONSTRAINT "REL_da851e06d0dfe2ef397d8b1bf1" UNIQUE ("noteId"), CONSTRAINT "PK_da851e06d0dfe2ef397d8b1bf1b" PRIMARY KEY ("noteId"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7fa20a12319c7f6dc3aed98c0a" ON "poll" ("userHost") `);
-        await queryRunner.query(`CREATE TABLE "user_keypair" ("userId" character varying(32) NOT NULL, "publicKey" character varying(4096) NOT NULL, "privateKey" character varying(4096) NOT NULL, CONSTRAINT "REL_f4853eb41ab722fe05f81cedeb" UNIQUE ("userId"), CONSTRAINT "PK_f4853eb41ab722fe05f81cedeb6" PRIMARY KEY ("userId"))`);
-        await queryRunner.query(`CREATE TABLE "user_publickey" ("userId" character varying(32) NOT NULL, "keyId" character varying(256) NOT NULL, "keyPem" character varying(4096) NOT NULL, CONSTRAINT "REL_10c146e4b39b443ede016f6736" UNIQUE ("userId"), CONSTRAINT "PK_10c146e4b39b443ede016f6736d" PRIMARY KEY ("userId"))`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_171e64971c780ebd23fae140bb" ON "user_publickey" ("keyId") `);
-        await queryRunner.query(`CREATE TABLE "user_profile" ("userId" character varying(32) NOT NULL, "location" character varying(128), "birthday" character(10), "description" character varying(1024), "fields" jsonb NOT NULL DEFAULT '[]', "url" character varying(512), "email" character varying(128), "emailVerifyCode" character varying(128), "emailVerified" boolean NOT NULL DEFAULT false, "twoFactorTempSecret" character varying(128), "twoFactorSecret" character varying(128), "twoFactorEnabled" boolean NOT NULL DEFAULT false, "password" character varying(128), "clientData" jsonb NOT NULL DEFAULT '{}', "autoWatch" boolean NOT NULL DEFAULT false, "autoAcceptFollowed" boolean NOT NULL DEFAULT false, "alwaysMarkNsfw" boolean NOT NULL DEFAULT false, "carefulBot" boolean NOT NULL DEFAULT false, "twitter" boolean NOT NULL DEFAULT false, "twitterAccessToken" character varying(64) DEFAULT null, "twitterAccessTokenSecret" character varying(64) DEFAULT null, "twitterUserId" character varying(64) DEFAULT null, "twitterScreenName" character varying(64) DEFAULT null, "github" boolean NOT NULL DEFAULT false, "githubAccessToken" character varying(64) DEFAULT null, "githubId" integer DEFAULT null, "githubLogin" character varying(64) DEFAULT null, "discord" boolean NOT NULL DEFAULT false, "discordAccessToken" character varying(64) DEFAULT null, "discordRefreshToken" character varying(64) DEFAULT null, "discordExpiresDate" integer DEFAULT null, "discordId" character varying(64) DEFAULT null, "discordUsername" character varying(64) DEFAULT null, "discordDiscriminator" character varying(64) DEFAULT null, "userHost" character varying(128), CONSTRAINT "REL_51cb79b5555effaf7d69ba1cff" UNIQUE ("userId"), CONSTRAINT "PK_51cb79b5555effaf7d69ba1cff9" PRIMARY KEY ("userId"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_dce530b98e454793dac5ec2f5a" ON "user_profile" ("userHost") `);
-        await queryRunner.query(`CREATE TYPE "__chart__active_users_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__active_users" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__active_users_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_317237a9f733b970604a11e314f" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__drive_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__drive" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__drive_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_totalCount" bigint NOT NULL, "___local_totalSize" bigint NOT NULL, "___local_incCount" bigint NOT NULL, "___local_incSize" bigint NOT NULL, "___local_decCount" bigint NOT NULL, "___local_decSize" bigint NOT NULL, "___remote_totalCount" bigint NOT NULL, "___remote_totalSize" bigint NOT NULL, "___remote_incCount" bigint NOT NULL, "___remote_incSize" bigint NOT NULL, "___remote_decCount" bigint NOT NULL, "___remote_decSize" bigint NOT NULL, CONSTRAINT "PK_f96bc548a765cd4b3b354221ce7" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__federation_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__federation" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__federation_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___instance_total" bigint NOT NULL, "___instance_inc" bigint NOT NULL, "___instance_dec" bigint NOT NULL, CONSTRAINT "PK_b39dcd31a0fe1a7757e348e85fd" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__hashtag_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__hashtag" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__hashtag_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_c32f1ea2b44a5d2f7881e37f8f9" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__instance_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__instance" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__instance_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___requests_failed" bigint NOT NULL, "___requests_succeeded" bigint NOT NULL, "___requests_received" bigint NOT NULL, "___notes_total" bigint NOT NULL, "___notes_inc" bigint NOT NULL, "___notes_dec" bigint NOT NULL, "___notes_diffs_normal" bigint NOT NULL, "___notes_diffs_reply" bigint NOT NULL, "___notes_diffs_renote" bigint NOT NULL, "___users_total" bigint NOT NULL, "___users_inc" bigint NOT NULL, "___users_dec" bigint NOT NULL, "___following_total" bigint NOT NULL, "___following_inc" bigint NOT NULL, "___following_dec" bigint NOT NULL, "___followers_total" bigint NOT NULL, "___followers_inc" bigint NOT NULL, "___followers_dec" bigint NOT NULL, "___drive_totalFiles" bigint NOT NULL, "___drive_totalUsage" bigint NOT NULL, "___drive_incFiles" bigint NOT NULL, "___drive_incUsage" bigint NOT NULL, "___drive_decFiles" bigint NOT NULL, "___drive_decUsage" bigint NOT NULL, CONSTRAINT "PK_1267c67c7c2d47b4903975f2c00" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__network_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__network" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__network_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___incomingRequests" bigint NOT NULL, "___outgoingRequests" bigint NOT NULL, "___totalTime" bigint NOT NULL, "___incomingBytes" bigint NOT NULL, "___outgoingBytes" bigint NOT NULL, CONSTRAINT "PK_bc4290c2e27fad14ef0c1ca93f3" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__notes_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__notes" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__notes_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_total" bigint NOT NULL, "___local_inc" bigint NOT NULL, "___local_dec" bigint NOT NULL, "___local_diffs_normal" bigint NOT NULL, "___local_diffs_reply" bigint NOT NULL, "___local_diffs_renote" bigint NOT NULL, "___remote_total" bigint NOT NULL, "___remote_inc" bigint NOT NULL, "___remote_dec" bigint NOT NULL, "___remote_diffs_normal" bigint NOT NULL, "___remote_diffs_reply" bigint NOT NULL, "___remote_diffs_renote" bigint NOT NULL, CONSTRAINT "PK_0aec823fa85c7f901bdb3863b14" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__per_user_drive_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__per_user_drive" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_drive_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___totalCount" bigint NOT NULL, "___totalSize" bigint NOT NULL, "___incCount" bigint NOT NULL, "___incSize" bigint NOT NULL, "___decCount" bigint NOT NULL, "___decSize" bigint NOT NULL, CONSTRAINT "PK_d0ef23d24d666e1a44a0cd3d208" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__per_user_following_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__per_user_following" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_following_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_followings_total" bigint NOT NULL, "___local_followings_inc" bigint NOT NULL, "___local_followings_dec" bigint NOT NULL, "___local_followers_total" bigint NOT NULL, "___local_followers_inc" bigint NOT NULL, "___local_followers_dec" bigint NOT NULL, "___remote_followings_total" bigint NOT NULL, "___remote_followings_inc" bigint NOT NULL, "___remote_followings_dec" bigint NOT NULL, "___remote_followers_total" bigint NOT NULL, "___remote_followers_inc" bigint NOT NULL, "___remote_followers_dec" bigint NOT NULL, CONSTRAINT "PK_85bb1b540363a29c2fec83bd907" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__per_user_notes_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__per_user_notes" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_notes_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___total" bigint NOT NULL, "___inc" bigint NOT NULL, "___dec" bigint NOT NULL, "___diffs_normal" bigint NOT NULL, "___diffs_reply" bigint NOT NULL, "___diffs_renote" bigint NOT NULL, CONSTRAINT "PK_334acf6e915af2f29edc11b8e50" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__per_user_reaction_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__per_user_reaction" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_reaction_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_984f54dae441e65b633e8d27a7f" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__test_grouped_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__test_grouped" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_grouped_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo_total" bigint NOT NULL, "___foo_inc" bigint NOT NULL, "___foo_dec" bigint NOT NULL, CONSTRAINT "PK_f4a2b175d308695af30d4293272" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__test_unique_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__test_unique" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_unique_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo" bigint NOT NULL, CONSTRAINT "PK_409bac9c97cc612d8500012319d" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__test_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__test" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo_total" bigint NOT NULL, "___foo_inc" bigint NOT NULL, "___foo_dec" bigint NOT NULL, CONSTRAINT "PK_b4bc31dffbd1b785276a3ecfc1e" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE TYPE "__chart__users_span_enum" AS ENUM('hour', 'day')`);
-        await queryRunner.query(`CREATE TABLE "__chart__users" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__users_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_total" bigint NOT NULL, "___local_inc" bigint NOT NULL, "___local_dec" bigint NOT NULL, "___remote_total" bigint NOT NULL, "___remote_inc" bigint NOT NULL, "___remote_dec" bigint NOT NULL, CONSTRAINT "PK_4dfcf2c78d03524b9eb2c99d328" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`ALTER TABLE "drive_folder" ADD CONSTRAINT "FK_f4fc06e49c0171c85f1c48060d2" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "drive_folder" ADD CONSTRAINT "FK_00ceffb0cdc238b3233294f08f2" FOREIGN KEY ("parentId") REFERENCES "drive_folder"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "drive_file" ADD CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "drive_file" ADD CONSTRAINT "FK_bb90d1956dafc4068c28aa7560a" FOREIGN KEY ("folderId") REFERENCES "drive_folder"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_58f5c71eaab331645112cf8cfa5" FOREIGN KEY ("avatarId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_afc64b53f8db3707ceb34eb28e2" FOREIGN KEY ("bannerId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "app" ADD CONSTRAINT "FK_3f5b0899ef90527a3462d7c2cb3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_9949557d0e1b2c19e5344c171e9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_ec5c201576192ba8904c345c5cc" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_5b87d9d19127bd5d92026017a7b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_66d2bd2ee31d14bcc23069a89f8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_aecfbd5ef60374918e63ee95fa7" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note_reaction" ADD CONSTRAINT "FK_13761f64257f40c5636d0ff95ee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note_reaction" ADD CONSTRAINT "FK_45145e4953780f3cd5656f0ea6a" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note_watching" ADD CONSTRAINT "FK_b0134ec406e8d09a540f8182888" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note_watching" ADD CONSTRAINT "FK_03e7028ab8388a3f5e3ce2a8619" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note_unread" ADD CONSTRAINT "FK_56b0166d34ddae49d8ef7610bb9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note_unread" ADD CONSTRAINT "FK_e637cba4dc4410218c4251260e4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3c601b70a1066d2c8b517094cb9" FOREIGN KEY ("notifieeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_769cb6b73a1efe22ddf733ac453" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "following" ADD CONSTRAINT "FK_24e0042143a18157b234df186c3" FOREIGN KEY ("followeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "following" ADD CONSTRAINT "FK_6516c5a6f3c015b4eed39978be5" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "muting" ADD CONSTRAINT "FK_ec96b4fed9dae517e0dbbe0675c" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "muting" ADD CONSTRAINT "FK_93060675b4a79a577f31d260c67" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "sw_subscription" ADD CONSTRAINT "FK_97754ca6f2baff9b4abb7f853dd" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "blocking" ADD CONSTRAINT "FK_2cd4a2743a99671308f5417759e" FOREIGN KEY ("blockeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "blocking" ADD CONSTRAINT "FK_0627125f1a8a42c9a1929edb552" FOREIGN KEY ("blockerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_list" ADD CONSTRAINT "FK_b7fcefbdd1c18dce86687531f99" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_list_joining" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_list_joining" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note_favorite" ADD CONSTRAINT "FK_47f4b1892f5d6ba8efb3057d81a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note_favorite" ADD CONSTRAINT "FK_0e00498f180193423c992bc4370" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_d049123c413e68ca52abe734203" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_04cc96756f89d0b7f9473e8cdf3" FOREIGN KEY ("reporterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_5377c307783fce2b6d352e1203b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_cac14a4e3944454a5ce7daa5142" FOREIGN KEY ("recipientId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "signin" ADD CONSTRAINT "FK_2c308dbdc50d94dc625670055f7" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "FK_c072b729d71697f959bde66ade0" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "FK_dbe037d4bddd17b03a1dc778dee" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "follow_request" ADD CONSTRAINT "FK_12c01c0d1a79f77d9f6c15fadd2" FOREIGN KEY ("followeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "follow_request" ADD CONSTRAINT "FK_a7fd92dd6dc519e6fb435dd108f" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_note_pining" ADD CONSTRAINT "FK_bfbc6f79ba4007b4ce5097f08d6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_note_pining" ADD CONSTRAINT "FK_68881008f7c3588ad7ecae471cf" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`);
-        await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`);
-        await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`);
-        await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`);
-        await queryRunner.query(`ALTER TABLE "user_note_pining" DROP CONSTRAINT "FK_68881008f7c3588ad7ecae471cf"`);
-        await queryRunner.query(`ALTER TABLE "user_note_pining" DROP CONSTRAINT "FK_bfbc6f79ba4007b4ce5097f08d6"`);
-        await queryRunner.query(`ALTER TABLE "reversi_matching" DROP CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066"`);
-        await queryRunner.query(`ALTER TABLE "reversi_matching" DROP CONSTRAINT "FK_3b25402709dd9882048c2bbade0"`);
-        await queryRunner.query(`ALTER TABLE "reversi_game" DROP CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6"`);
-        await queryRunner.query(`ALTER TABLE "reversi_game" DROP CONSTRAINT "FK_f7467510c60a45ce5aca6292743"`);
-        await queryRunner.query(`ALTER TABLE "follow_request" DROP CONSTRAINT "FK_a7fd92dd6dc519e6fb435dd108f"`);
-        await queryRunner.query(`ALTER TABLE "follow_request" DROP CONSTRAINT "FK_12c01c0d1a79f77d9f6c15fadd2"`);
-        await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "FK_dbe037d4bddd17b03a1dc778dee"`);
-        await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "FK_c072b729d71697f959bde66ade0"`);
-        await queryRunner.query(`ALTER TABLE "signin" DROP CONSTRAINT "FK_2c308dbdc50d94dc625670055f7"`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b"`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_cac14a4e3944454a5ce7daa5142"`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_5377c307783fce2b6d352e1203b"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_04cc96756f89d0b7f9473e8cdf3"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_d049123c413e68ca52abe734203"`);
-        await queryRunner.query(`ALTER TABLE "note_favorite" DROP CONSTRAINT "FK_0e00498f180193423c992bc4370"`);
-        await queryRunner.query(`ALTER TABLE "note_favorite" DROP CONSTRAINT "FK_47f4b1892f5d6ba8efb3057d81a"`);
-        await queryRunner.query(`ALTER TABLE "user_list_joining" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`);
-        await queryRunner.query(`ALTER TABLE "user_list_joining" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`);
-        await queryRunner.query(`ALTER TABLE "user_list" DROP CONSTRAINT "FK_b7fcefbdd1c18dce86687531f99"`);
-        await queryRunner.query(`ALTER TABLE "blocking" DROP CONSTRAINT "FK_0627125f1a8a42c9a1929edb552"`);
-        await queryRunner.query(`ALTER TABLE "blocking" DROP CONSTRAINT "FK_2cd4a2743a99671308f5417759e"`);
-        await queryRunner.query(`ALTER TABLE "sw_subscription" DROP CONSTRAINT "FK_97754ca6f2baff9b4abb7f853dd"`);
-        await queryRunner.query(`ALTER TABLE "muting" DROP CONSTRAINT "FK_93060675b4a79a577f31d260c67"`);
-        await queryRunner.query(`ALTER TABLE "muting" DROP CONSTRAINT "FK_ec96b4fed9dae517e0dbbe0675c"`);
-        await queryRunner.query(`ALTER TABLE "following" DROP CONSTRAINT "FK_6516c5a6f3c015b4eed39978be5"`);
-        await queryRunner.query(`ALTER TABLE "following" DROP CONSTRAINT "FK_24e0042143a18157b234df186c3"`);
-        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_769cb6b73a1efe22ddf733ac453"`);
-        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`);
-        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3c601b70a1066d2c8b517094cb9"`);
-        await queryRunner.query(`ALTER TABLE "note_unread" DROP CONSTRAINT "FK_e637cba4dc4410218c4251260e4"`);
-        await queryRunner.query(`ALTER TABLE "note_unread" DROP CONSTRAINT "FK_56b0166d34ddae49d8ef7610bb9"`);
-        await queryRunner.query(`ALTER TABLE "note_watching" DROP CONSTRAINT "FK_03e7028ab8388a3f5e3ce2a8619"`);
-        await queryRunner.query(`ALTER TABLE "note_watching" DROP CONSTRAINT "FK_b0134ec406e8d09a540f8182888"`);
-        await queryRunner.query(`ALTER TABLE "note_reaction" DROP CONSTRAINT "FK_45145e4953780f3cd5656f0ea6a"`);
-        await queryRunner.query(`ALTER TABLE "note_reaction" DROP CONSTRAINT "FK_13761f64257f40c5636d0ff95ee"`);
-        await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_aecfbd5ef60374918e63ee95fa7"`);
-        await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_66d2bd2ee31d14bcc23069a89f8"`);
-        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_5b87d9d19127bd5d92026017a7b"`);
-        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_ec5c201576192ba8904c345c5cc"`);
-        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`);
-        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_9949557d0e1b2c19e5344c171e9"`);
-        await queryRunner.query(`ALTER TABLE "app" DROP CONSTRAINT "FK_3f5b0899ef90527a3462d7c2cb3"`);
-        await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_afc64b53f8db3707ceb34eb28e2"`);
-        await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_58f5c71eaab331645112cf8cfa5"`);
-        await queryRunner.query(`ALTER TABLE "drive_file" DROP CONSTRAINT "FK_bb90d1956dafc4068c28aa7560a"`);
-        await queryRunner.query(`ALTER TABLE "drive_file" DROP CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e"`);
-        await queryRunner.query(`ALTER TABLE "drive_folder" DROP CONSTRAINT "FK_00ceffb0cdc238b3233294f08f2"`);
-        await queryRunner.query(`ALTER TABLE "drive_folder" DROP CONSTRAINT "FK_f4fc06e49c0171c85f1c48060d2"`);
-        await queryRunner.query(`DROP TABLE "__chart__users"`);
-        await queryRunner.query(`DROP TYPE "__chart__users_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__test"`);
-        await queryRunner.query(`DROP TYPE "__chart__test_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__test_unique"`);
-        await queryRunner.query(`DROP TYPE "__chart__test_unique_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__test_grouped"`);
-        await queryRunner.query(`DROP TYPE "__chart__test_grouped_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__per_user_reaction"`);
-        await queryRunner.query(`DROP TYPE "__chart__per_user_reaction_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__per_user_notes"`);
-        await queryRunner.query(`DROP TYPE "__chart__per_user_notes_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__per_user_following"`);
-        await queryRunner.query(`DROP TYPE "__chart__per_user_following_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__per_user_drive"`);
-        await queryRunner.query(`DROP TYPE "__chart__per_user_drive_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__notes"`);
-        await queryRunner.query(`DROP TYPE "__chart__notes_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__network"`);
-        await queryRunner.query(`DROP TYPE "__chart__network_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__instance"`);
-        await queryRunner.query(`DROP TYPE "__chart__instance_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__hashtag"`);
-        await queryRunner.query(`DROP TYPE "__chart__hashtag_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__federation"`);
-        await queryRunner.query(`DROP TYPE "__chart__federation_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__drive"`);
-        await queryRunner.query(`DROP TYPE "__chart__drive_span_enum"`);
-        await queryRunner.query(`DROP TABLE "__chart__active_users"`);
-        await queryRunner.query(`DROP TYPE "__chart__active_users_span_enum"`);
-        await queryRunner.query(`DROP INDEX "IDX_dce530b98e454793dac5ec2f5a"`);
-        await queryRunner.query(`DROP TABLE "user_profile"`);
-        await queryRunner.query(`DROP INDEX "IDX_171e64971c780ebd23fae140bb"`);
-        await queryRunner.query(`DROP TABLE "user_publickey"`);
-        await queryRunner.query(`DROP TABLE "user_keypair"`);
-        await queryRunner.query(`DROP INDEX "IDX_7fa20a12319c7f6dc3aed98c0a"`);
-        await queryRunner.query(`DROP INDEX "IDX_0610ebcfcfb4a18441a9bcdab2"`);
-        await queryRunner.query(`DROP TABLE "poll"`);
-        await queryRunner.query(`DROP TYPE "poll_notevisibility_enum"`);
-        await queryRunner.query(`DROP INDEX "IDX_410cd649884b501c02d6e72738"`);
-        await queryRunner.query(`DROP INDEX "IDX_bfbc6f79ba4007b4ce5097f08d"`);
-        await queryRunner.query(`DROP TABLE "user_note_pining"`);
-        await queryRunner.query(`DROP INDEX "IDX_e247b23a3c9b45f89ec1299d06"`);
-        await queryRunner.query(`DROP INDEX "IDX_3b25402709dd9882048c2bbade"`);
-        await queryRunner.query(`DROP INDEX "IDX_b604d92d6c7aec38627f6eaf16"`);
-        await queryRunner.query(`DROP TABLE "reversi_matching"`);
-        await queryRunner.query(`DROP INDEX "IDX_b46ec40746efceac604142be1c"`);
-        await queryRunner.query(`DROP TABLE "reversi_game"`);
-        await queryRunner.query(`DROP INDEX "IDX_4f4d35e1256c84ae3d1f0eab10"`);
-        await queryRunner.query(`DROP INDEX "IDX_5900e907bb46516ddf2871327c"`);
-        await queryRunner.query(`DROP INDEX "IDX_b37dafc86e9af007e3295c2781"`);
-        await queryRunner.query(`DROP TABLE "emoji"`);
-        await queryRunner.query(`DROP INDEX "IDX_d54a512b822fac7ed52800f6b4"`);
-        await queryRunner.query(`DROP INDEX "IDX_a7fd92dd6dc519e6fb435dd108"`);
-        await queryRunner.query(`DROP INDEX "IDX_12c01c0d1a79f77d9f6c15fadd"`);
-        await queryRunner.query(`DROP TABLE "follow_request"`);
-        await queryRunner.query(`DROP INDEX "IDX_62cb09e1129f6ec024ef66e183"`);
-        await queryRunner.query(`DROP TABLE "auth_session"`);
-        await queryRunner.query(`DROP INDEX "IDX_2c308dbdc50d94dc625670055f"`);
-        await queryRunner.query(`DROP TABLE "signin"`);
-        await queryRunner.query(`DROP INDEX "IDX_cac14a4e3944454a5ce7daa514"`);
-        await queryRunner.query(`DROP INDEX "IDX_5377c307783fce2b6d352e1203"`);
-        await queryRunner.query(`DROP INDEX "IDX_e21cd3646e52ef9c94aaf17c2e"`);
-        await queryRunner.query(`DROP TABLE "messaging_message"`);
-        await queryRunner.query(`DROP INDEX "IDX_0ff69e8dfa9fe31bb4a4660f59"`);
-        await queryRunner.query(`DROP TABLE "registration_ticket"`);
-        await queryRunner.query(`DROP INDEX "IDX_5cd442c3b2e74fdd99dae20243"`);
-        await queryRunner.query(`DROP INDEX "IDX_04cc96756f89d0b7f9473e8cdf"`);
-        await queryRunner.query(`DROP INDEX "IDX_d049123c413e68ca52abe73420"`);
-        await queryRunner.query(`DROP INDEX "IDX_db2098070b2b5a523c58181f74"`);
-        await queryRunner.query(`DROP TABLE "abuse_user_report"`);
-        await queryRunner.query(`DROP INDEX "IDX_0f4fb9ad355f3effff221ef245"`);
-        await queryRunner.query(`DROP INDEX "IDX_47f4b1892f5d6ba8efb3057d81"`);
-        await queryRunner.query(`DROP TABLE "note_favorite"`);
-        await queryRunner.query(`DROP INDEX "IDX_0b03cbcd7e6a7ce068efa8ecc2"`);
-        await queryRunner.query(`DROP INDEX "IDX_0c44bf4f680964145f2a68a341"`);
-        await queryRunner.query(`DROP INDEX "IDX_d57f9030cd3af7f63ffb1c267c"`);
-        await queryRunner.query(`DROP INDEX "IDX_4c02d38a976c3ae132228c6fce"`);
-        await queryRunner.query(`DROP INDEX "IDX_0e206cec573f1edff4a3062923"`);
-        await queryRunner.query(`DROP INDEX "IDX_2710a55f826ee236ea1a62698f"`);
-        await queryRunner.query(`DROP INDEX "IDX_347fec870eafea7b26c8a73bac"`);
-        await queryRunner.query(`DROP TABLE "hashtag"`);
-        await queryRunner.query(`DROP INDEX "IDX_605472305f26818cc93d1baaa7"`);
-        await queryRunner.query(`DROP INDEX "IDX_d844bfc6f3f523a05189076efa"`);
-        await queryRunner.query(`DROP TABLE "user_list_joining"`);
-        await queryRunner.query(`DROP INDEX "IDX_b7fcefbdd1c18dce86687531f9"`);
-        await queryRunner.query(`DROP TABLE "user_list"`);
-        await queryRunner.query(`DROP INDEX "IDX_98a1bc5cb30dfd159de056549f"`);
-        await queryRunner.query(`DROP INDEX "IDX_0627125f1a8a42c9a1929edb55"`);
-        await queryRunner.query(`DROP INDEX "IDX_2cd4a2743a99671308f5417759"`);
-        await queryRunner.query(`DROP INDEX "IDX_b9a354f7941c1e779f3b33aea6"`);
-        await queryRunner.query(`DROP TABLE "blocking"`);
-        await queryRunner.query(`DROP INDEX "IDX_97754ca6f2baff9b4abb7f853d"`);
-        await queryRunner.query(`DROP TABLE "sw_subscription"`);
-        await queryRunner.query(`DROP INDEX "IDX_1eb9d9824a630321a29fd3b290"`);
-        await queryRunner.query(`DROP INDEX "IDX_93060675b4a79a577f31d260c6"`);
-        await queryRunner.query(`DROP INDEX "IDX_ec96b4fed9dae517e0dbbe0675"`);
-        await queryRunner.query(`DROP INDEX "IDX_f86d57fbca33c7a4e6897490cc"`);
-        await queryRunner.query(`DROP TABLE "muting"`);
-        await queryRunner.query(`DROP INDEX "IDX_8d5afc98982185799b160e10eb"`);
-        await queryRunner.query(`DROP INDEX "IDX_2cd3b2a6b4cf0b910b260afe08"`);
-        await queryRunner.query(`DROP TABLE "instance"`);
-        await queryRunner.query(`DROP INDEX "IDX_307be5f1d1252e0388662acb96"`);
-        await queryRunner.query(`DROP INDEX "IDX_6516c5a6f3c015b4eed39978be"`);
-        await queryRunner.query(`DROP INDEX "IDX_24e0042143a18157b234df186c"`);
-        await queryRunner.query(`DROP INDEX "IDX_582f8fab771a9040a12961f3e7"`);
-        await queryRunner.query(`DROP TABLE "following"`);
-        await queryRunner.query(`DROP TABLE "meta"`);
-        await queryRunner.query(`DROP INDEX "IDX_3c601b70a1066d2c8b517094cb"`);
-        await queryRunner.query(`DROP INDEX "IDX_b11a5e627c41d4dc3170f1d370"`);
-        await queryRunner.query(`DROP TABLE "notification"`);
-        await queryRunner.query(`DROP INDEX "IDX_d908433a4953cc13216cd9c274"`);
-        await queryRunner.query(`DROP INDEX "IDX_e637cba4dc4410218c4251260e"`);
-        await queryRunner.query(`DROP INDEX "IDX_56b0166d34ddae49d8ef7610bb"`);
-        await queryRunner.query(`DROP TABLE "note_unread"`);
-        await queryRunner.query(`DROP INDEX "IDX_a42c93c69989ce1d09959df4cf"`);
-        await queryRunner.query(`DROP INDEX "IDX_44499765eec6b5489d72c4253b"`);
-        await queryRunner.query(`DROP INDEX "IDX_03e7028ab8388a3f5e3ce2a861"`);
-        await queryRunner.query(`DROP INDEX "IDX_b0134ec406e8d09a540f818288"`);
-        await queryRunner.query(`DROP INDEX "IDX_318cdf42a9cfc11f479bd802bb"`);
-        await queryRunner.query(`DROP TABLE "note_watching"`);
-        await queryRunner.query(`DROP INDEX "IDX_ad0c221b25672daf2df320a817"`);
-        await queryRunner.query(`DROP INDEX "IDX_45145e4953780f3cd5656f0ea6"`);
-        await queryRunner.query(`DROP INDEX "IDX_13761f64257f40c5636d0ff95e"`);
-        await queryRunner.query(`DROP INDEX "IDX_01f4581f114e0ebd2bbb876f0b"`);
-        await queryRunner.query(`DROP TABLE "note_reaction"`);
-        await queryRunner.query(`DROP INDEX "IDX_50bd7164c5b78f1f4a42c4d21f"`);
-        await queryRunner.query(`DROP INDEX "IDX_aecfbd5ef60374918e63ee95fa"`);
-        await queryRunner.query(`DROP INDEX "IDX_66d2bd2ee31d14bcc23069a89f"`);
-        await queryRunner.query(`DROP INDEX "IDX_0fb627e1c2f753262a74f0562d"`);
-        await queryRunner.query(`DROP TABLE "poll_vote"`);
-        await queryRunner.query(`DROP INDEX "IDX_7125a826ab192eb27e11d358a5"`);
-        await queryRunner.query(`DROP INDEX "IDX_88937d94d7443d9a99a76fa5c0"`);
-        await queryRunner.query(`DROP INDEX "IDX_54ebcb6d27222913b908d56fd8"`);
-        await queryRunner.query(`DROP INDEX "IDX_796a8c03959361f97dc2be1d5c"`);
-        await queryRunner.query(`DROP INDEX "IDX_25dfc71b0369b003a4cd434d0b"`);
-        await queryRunner.query(`DROP INDEX "IDX_51c063b6a133a9cb87145450f5"`);
-        await queryRunner.query(`DROP INDEX "IDX_153536c67d05e9adb24e99fc2b"`);
-        await queryRunner.query(`DROP INDEX "IDX_5b87d9d19127bd5d92026017a7"`);
-        await queryRunner.query(`DROP INDEX "IDX_52ccc804d7c69037d558bac4c9"`);
-        await queryRunner.query(`DROP INDEX "IDX_17cb3553c700a4985dff5a30ff"`);
-        await queryRunner.query(`DROP INDEX "IDX_e7c0567f5261063592f022e9b5"`);
-        await queryRunner.query(`DROP TABLE "note"`);
-        await queryRunner.query(`DROP TYPE "note_visibility_enum"`);
-        await queryRunner.query(`DROP INDEX "IDX_9949557d0e1b2c19e5344c171e"`);
-        await queryRunner.query(`DROP INDEX "IDX_64c327441248bae40f7d92f34f"`);
-        await queryRunner.query(`DROP INDEX "IDX_70ba8f6af34bc924fc9e12adb8"`);
-        await queryRunner.query(`DROP TABLE "access_token"`);
-        await queryRunner.query(`DROP INDEX "IDX_f49922d511d666848f250663c4"`);
-        await queryRunner.query(`DROP INDEX "IDX_3f5b0899ef90527a3462d7c2cb"`);
-        await queryRunner.query(`DROP INDEX "IDX_048a757923ed8b157e9895da53"`);
-        await queryRunner.query(`DROP TABLE "app"`);
-        await queryRunner.query(`DROP INDEX "IDX_5deb01ae162d1d70b80d064c27"`);
-        await queryRunner.query(`DROP INDEX "IDX_a854e557b1b14814750c7c7b0c"`);
-        await queryRunner.query(`DROP INDEX "IDX_be623adaa4c566baf5d29ce0c8"`);
-        await queryRunner.query(`DROP INDEX "IDX_3252a5df8d5bbd16b281f7799e"`);
-        await queryRunner.query(`DROP INDEX "IDX_fa99d777623947a5b05f394cae"`);
-        await queryRunner.query(`DROP INDEX "IDX_a27b942a0d6dcff90e3ee9b5e8"`);
-        await queryRunner.query(`DROP INDEX "IDX_80ca6e6ef65fb9ef34ea8c90f4"`);
-        await queryRunner.query(`DROP INDEX "IDX_e11e649824a45d8ed01d597fd9"`);
-        await queryRunner.query(`DROP TABLE "user"`);
-        await queryRunner.query(`DROP INDEX "IDX_bb90d1956dafc4068c28aa7560"`);
-        await queryRunner.query(`DROP INDEX "IDX_e5848eac4940934e23dbc17581"`);
-        await queryRunner.query(`DROP INDEX "IDX_c55b2b7c284d9fef98026fc88e"`);
-        await queryRunner.query(`DROP INDEX "IDX_e74022ce9a074b3866f70e0d27"`);
-        await queryRunner.query(`DROP INDEX "IDX_d85a184c2540d2deba33daf642"`);
-        await queryRunner.query(`DROP INDEX "IDX_a40b8df8c989d7db937ea27cf6"`);
-        await queryRunner.query(`DROP INDEX "IDX_37bb9a1b4585f8a3beb24c62d6"`);
-        await queryRunner.query(`DROP INDEX "IDX_92779627994ac79277f070c91e"`);
-        await queryRunner.query(`DROP INDEX "IDX_860fa6f6c7df5bb887249fba22"`);
-        await queryRunner.query(`DROP INDEX "IDX_c8dfad3b72196dd1d6b5db168a"`);
-        await queryRunner.query(`DROP TABLE "drive_file"`);
-        await queryRunner.query(`DROP INDEX "IDX_00ceffb0cdc238b3233294f08f"`);
-        await queryRunner.query(`DROP INDEX "IDX_f4fc06e49c0171c85f1c48060d"`);
-        await queryRunner.query(`DROP INDEX "IDX_02878d441ceae15ce060b73daf"`);
-        await queryRunner.query(`DROP TABLE "drive_folder"`);
-        await queryRunner.query(`DROP INDEX "IDX_584b536b49e53ac81beb39a177"`);
-        await queryRunner.query(`DROP INDEX "IDX_8cb40cfc8f3c28261e6f887b03"`);
-        await queryRunner.query(`DROP INDEX "IDX_8e4eb51a35d81b64dda28eed0a"`);
-        await queryRunner.query(`DROP TABLE "log"`);
-        await queryRunner.query(`DROP TYPE "log_level_enum"`);
-    }
-
-}
diff --git a/migration/1556348509290-Pages.ts b/migration/1556348509290-Pages.ts
deleted file mode 100644
index c44b4b1f79..0000000000
--- a/migration/1556348509290-Pages.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class Pages1556348509290 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TYPE "page_visibility_enum" AS ENUM('public', 'followers', 'specified')`);
-        await queryRunner.query(`CREATE TABLE "page" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "name" character varying(256) NOT NULL, "summary" character varying(256), "alignCenter" boolean NOT NULL, "font" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "eyeCatchingImageId" character varying(32), "content" jsonb NOT NULL DEFAULT '[]', "variables" jsonb NOT NULL DEFAULT '[]', "visibility" "page_visibility_enum" NOT NULL, "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_742f4117e065c5b6ad21b37ba1f" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_af639b066dfbca78b01a920f8a" ON "page" ("updatedAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_b82c19c08afb292de4600d99e4" ON "page" ("name") `);
-        await queryRunner.query(`CREATE INDEX "IDX_ae1d917992dd0c9d9bbdad06c4" ON "page" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_90148bbc2bf0854428786bfc15" ON "page" ("visibleUserIds") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2133ef8317e4bdb839c0dcbf13" ON "page" ("userId", "name") `);
-        await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10"`);
-        await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a"`);
-        await queryRunner.query(`DROP INDEX "IDX_2133ef8317e4bdb839c0dcbf13"`);
-        await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`);
-        await queryRunner.query(`DROP INDEX "IDX_ae1d917992dd0c9d9bbdad06c4"`);
-        await queryRunner.query(`DROP INDEX "IDX_b82c19c08afb292de4600d99e4"`);
-        await queryRunner.query(`DROP INDEX "IDX_af639b066dfbca78b01a920f8a"`);
-        await queryRunner.query(`DROP INDEX "IDX_fbb4297c927a9b85e9cefa2eb1"`);
-        await queryRunner.query(`DROP TABLE "page"`);
-        await queryRunner.query(`DROP TYPE "page_visibility_enum"`);
-    }
-
-}
diff --git a/migration/1556746559567-UserProfile.ts b/migration/1556746559567-UserProfile.ts
deleted file mode 100644
index 2dc7349cc1..0000000000
--- a/migration/1556746559567-UserProfile.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class UserProfile1556746559567 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "githubId" TYPE VARCHAR(64) USING "githubId"::VARCHAR(64)`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "discordExpiresDate" TYPE VARCHAR(64) USING "discordExpiresDate"::VARCHAR(64)`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`UPDATE "user_profile" SET github = FALSE, discord = FALSE`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "githubId" TYPE INTEGER USING NULL`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "discordExpiresDate" TYPE INTEGER USING NULL`);
-    }
-
-}
diff --git a/migration/1557476068003-PinnedUsers.ts b/migration/1557476068003-PinnedUsers.ts
deleted file mode 100644
index 4e7222aafc..0000000000
--- a/migration/1557476068003-PinnedUsers.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class PinnedUsers1557476068003 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedUsers" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedUsers"`);
-    }
-
-}
diff --git a/migration/1557761316509-AddSomeUrls.ts b/migration/1557761316509-AddSomeUrls.ts
deleted file mode 100644
index 926999de6c..0000000000
--- a/migration/1557761316509-AddSomeUrls.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class AddSomeUrls1557761316509 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "ToSUrl" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "repositoryUrl" character varying(512) NOT NULL DEFAULT 'https://github.com/misskey-dev/misskey'`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "feedbackUrl" character varying(512) DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "feedbackUrl"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "repositoryUrl"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ToSUrl"`);
-    }
-}
diff --git a/migration/1557932705754-ObjectStorageSetting.ts b/migration/1557932705754-ObjectStorageSetting.ts
deleted file mode 100644
index dde6aa65f9..0000000000
--- a/migration/1557932705754-ObjectStorageSetting.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class ObjectStorageSetting1557932705754 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "useObjectStorage" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBucket" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePrefix" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBaseUrl" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageEndpoint" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageRegion" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageAccessKey" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSecretKey" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePort" integer`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseSSL" boolean NOT NULL DEFAULT true`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseSSL"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePort"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSecretKey"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageAccessKey"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRegion"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageEndpoint"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBaseUrl"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePrefix"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBucket"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useObjectStorage"`);
-    }
-
-}
diff --git a/migration/1558072954435-PageLike.ts b/migration/1558072954435-PageLike.ts
deleted file mode 100644
index 93cdb8afeb..0000000000
--- a/migration/1558072954435-PageLike.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class PageLike1558072954435 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "page_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "pageId" character varying(32) NOT NULL, CONSTRAINT "PK_813f034843af992d3ae0f43c64c" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_0e61efab7f88dbb79c9166dbb4" ON "page_like" ("userId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa" ON "page_like" ("userId", "pageId") `);
-        await queryRunner.query(`ALTER TABLE "page" ADD "likedCount" integer NOT NULL DEFAULT 0`);
-        await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_cf8782626dced3176038176a847" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_cf8782626dced3176038176a847"`);
-        await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48"`);
-        await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "likedCount"`);
-        await queryRunner.query(`DROP INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa"`);
-        await queryRunner.query(`DROP INDEX "IDX_0e61efab7f88dbb79c9166dbb4"`);
-        await queryRunner.query(`DROP TABLE "page_like"`);
-    }
-
-}
diff --git a/migration/1558103093633-UserGroup.ts b/migration/1558103093633-UserGroup.ts
deleted file mode 100644
index 04783b8dfa..0000000000
--- a/migration/1558103093633-UserGroup.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class UserGroup1558103093633 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "user_group" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "isPrivate" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c29fba6fe013ec8724378ce7c9" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_20e30aa35180e317e133d75316" ON "user_group" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3d6b372788ab01be58853003c9" ON "user_group" ("userId") `);
-        await queryRunner.query(`CREATE TABLE "user_group_joining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_15f2425885253c5507e1599cfe7" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23" ON "user_group_joining" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_67dc758bc0566985d1b3d39986" ON "user_group_joining" ("userGroupId") `);
-        await queryRunner.query(`ALTER TABLE "messaging_message" ADD "groupId" character varying(32)`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" ADD "reads" character varying(32) array NOT NULL DEFAULT '{}'::varchar[]`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" DROP NOT NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS 'The recipient user ID.'`);
-        await queryRunner.query(`CREATE INDEX "IDX_2c4be03b446884f9e9c502135b" ON "messaging_message" ("groupId") `);
-        await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_2c4be03b446884f9e9c502135be" FOREIGN KEY ("groupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_group" ADD CONSTRAINT "FK_3d6b372788ab01be58853003c93" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_67dc758bc0566985d1b3d399865" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_67dc758bc0566985d1b3d399865"`);
-        await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231"`);
-        await queryRunner.query(`ALTER TABLE "user_group" DROP CONSTRAINT "FK_3d6b372788ab01be58853003c93"`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_2c4be03b446884f9e9c502135be"`);
-        await queryRunner.query(`DROP INDEX "IDX_2c4be03b446884f9e9c502135b"`);
-        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS ''`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" SET NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "reads"`);
-        await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "groupId"`);
-        await queryRunner.query(`DROP INDEX "IDX_67dc758bc0566985d1b3d39986"`);
-        await queryRunner.query(`DROP INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23"`);
-        await queryRunner.query(`DROP TABLE "user_group_joining"`);
-        await queryRunner.query(`DROP INDEX "IDX_3d6b372788ab01be58853003c9"`);
-        await queryRunner.query(`DROP INDEX "IDX_20e30aa35180e317e133d75316"`);
-        await queryRunner.query(`DROP TABLE "user_group"`);
-    }
-
-}
diff --git a/migration/1558257926829-UserGroupInvite.ts b/migration/1558257926829-UserGroupInvite.ts
deleted file mode 100644
index a78e479228..0000000000
--- a/migration/1558257926829-UserGroupInvite.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class UserGroupInvite1558257926829 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "user_group_invite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_3893884af0d3a5f4d01e7921a97" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_1039988afa3bf991185b277fe0" ON "user_group_invite" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e10924607d058004304611a436" ON "user_group_invite" ("userGroupId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78787741f9010886796f2320a4" ON "user_group_invite" ("userId", "userGroupId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `);
-        await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_1039988afa3bf991185b277fe03" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_e10924607d058004304611a436a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_e10924607d058004304611a436a"`);
-        await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_1039988afa3bf991185b277fe03"`);
-        await queryRunner.query(`DROP INDEX "IDX_d9ecaed8c6dc43f3592c229282"`);
-        await queryRunner.query(`DROP INDEX "IDX_78787741f9010886796f2320a4"`);
-        await queryRunner.query(`DROP INDEX "IDX_e10924607d058004304611a436"`);
-        await queryRunner.query(`DROP INDEX "IDX_1039988afa3bf991185b277fe0"`);
-        await queryRunner.query(`DROP TABLE "user_group_invite"`);
-    }
-
-}
diff --git a/migration/1558266512381-UserListJoining.ts b/migration/1558266512381-UserListJoining.ts
deleted file mode 100644
index f4a917045d..0000000000
--- a/migration/1558266512381-UserListJoining.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class UserListJoining1558266512381 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_joining" ("userId", "userListId") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`DROP INDEX "IDX_90f7da835e4c10aca6853621e1"`);
-    }
-
-}
diff --git a/migration/1561706992953-webauthn.ts b/migration/1561706992953-webauthn.ts
deleted file mode 100644
index fc1f0c042f..0000000000
--- a/migration/1561706992953-webauthn.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class webauthn1561706992953 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "attestation_challenge" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "challenge" character varying(64) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "registrationChallenge" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_d0ba6786e093f1bcb497572a6b5" PRIMARY KEY ("id", "userId"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_f1a461a618fa1755692d0e0d59" ON "attestation_challenge" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_47efb914aed1f72dd39a306c7b" ON "attestation_challenge" ("challenge") `);
-        await queryRunner.query(`CREATE TABLE "user_security_key" ("id" character varying NOT NULL, "userId" character varying(32) NOT NULL, "publicKey" character varying NOT NULL, "lastUsed" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(30) NOT NULL, CONSTRAINT "PK_3e508571121ab39c5f85d10c166" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_ff9ca3b5f3ee3d0681367a9b44" ON "user_security_key" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0d7718e562dcedd0aa5cf2c9f7" ON "user_security_key" ("publicKey") `);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "securityKeysAvailable" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`ALTER TABLE "attestation_challenge" ADD CONSTRAINT "FK_f1a461a618fa1755692d0e0d592" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "user_security_key" ADD CONSTRAINT "FK_ff9ca3b5f3ee3d0681367a9b447" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_security_key" DROP CONSTRAINT "FK_ff9ca3b5f3ee3d0681367a9b447"`);
-        await queryRunner.query(`ALTER TABLE "attestation_challenge" DROP CONSTRAINT "FK_f1a461a618fa1755692d0e0d592"`);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "securityKeysAvailable"`);
-        await queryRunner.query(`DROP INDEX "IDX_0d7718e562dcedd0aa5cf2c9f7"`);
-        await queryRunner.query(`DROP INDEX "IDX_ff9ca3b5f3ee3d0681367a9b44"`);
-        await queryRunner.query(`DROP TABLE "user_security_key"`);
-        await queryRunner.query(`DROP INDEX "IDX_47efb914aed1f72dd39a306c7b"`);
-        await queryRunner.query(`DROP INDEX "IDX_f1a461a618fa1755692d0e0d59"`);
-        await queryRunner.query(`DROP TABLE "attestation_challenge"`);
-    }
-
-}
diff --git a/migration/1561873850023-ChartIndexes.ts b/migration/1561873850023-ChartIndexes.ts
deleted file mode 100644
index b870ad3b7d..0000000000
--- a/migration/1561873850023-ChartIndexes.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class ChartIndexes1561873850023 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc" ON "__chart__active_users" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_15e91a03aeeac9dbccdf43fc06" ON "__chart__active_users" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_00ed5f86db1f7efafb1978bf21" ON "__chart__active_users" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_20f57cc8f142c131340ee16742" ON "__chart__active_users" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_c26e2c1cbb6e911e0554b27416" ON "__chart__active_users" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_13565815f618a1ff53886c5b28" ON "__chart__drive" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3fa0d0f17ca72e3dc80999a032" ON "__chart__drive" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7a170f67425e62a8fabb76c872" ON "__chart__drive" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_6e1df243476e20cbf86572ecc0" ON "__chart__drive" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_06690fc959f1c9fdaf21928222" ON "__chart__drive" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_36cb699c49580d4e6c2e6159f9" ON "__chart__federation" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e447064455928cf627590ef527" ON "__chart__federation" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_76e87c7bfc5d925fcbba405d84" ON "__chart__federation" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_2d416e6af791a82e338c79d480" ON "__chart__federation" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e9cd07672b37d8966cf3709283" ON "__chart__federation" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_07747a1038c05f532a718fe1de" ON "__chart__hashtag" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_fcc181fb8283009c61cc4083ef" ON "__chart__hashtag" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_99a7d2faaef84a6f728d714ad6" ON "__chart__hashtag" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_49975586f50ed7b800fdd88fbd" ON "__chart__hashtag" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_6d6f156ceefc6bc5f273a0e370" ON "__chart__hashtag" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_6b8f34a1a64b06014b6fb66824" ON "__chart__instance" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_c12f0af4a66cdd30c2287ce8aa" ON "__chart__instance" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63" ON "__chart__instance" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_d0a4f79af5a97b08f37b547197" ON "__chart__instance" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f5448d9633cff74208d850aabe" ON "__chart__instance" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a1efd3e0048a5f2793a47360dc" ON "__chart__network" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f8dd01baeded2ffa833e0a610a" ON "__chart__network" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7b5da130992ec9df96712d4290" ON "__chart__network" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_08fac0eb3b11f04c200c0b40dd" ON "__chart__network" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_9ff6944f01acb756fdc92d7563" ON "__chart__network" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_42eb716a37d381cdf566192b2b" ON "__chart__notes" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e69096589f11e3baa98ddd64d0" ON "__chart__notes" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7036f2957151588b813185c794" ON "__chart__notes" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0c9a159c5082cbeef3ca6706b5" ON "__chart__notes" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_924fc196c80ca24bae01dd37e4" ON "__chart__notes" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_5f86db6492274e07c1a3cdf286" ON "__chart__per_user_drive" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_328f259961e60c4fa0bfcf55ca" ON "__chart__per_user_drive" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e496ca8096d28f6b9b509264dc" ON "__chart__per_user_drive" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53" ON "__chart__per_user_drive" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f2aeafde2ae6fbad38e857631b" ON "__chart__per_user_drive" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7af07790712aa3438ff6773f3b" ON "__chart__per_user_following" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f92dd6d03f8d994f29987f6214" ON "__chart__per_user_following" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_4b3593098b6edc9c5afe36b18b" ON "__chart__per_user_following" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f" ON "__chart__per_user_following" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_4db3b84c7be0d3464714f3e0b1" ON "__chart__per_user_following" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_84234bd1abb873f07329681c83" ON "__chart__per_user_notes" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_8d2cbbc8114d90d19b44d626b6" ON "__chart__per_user_notes" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_55bf20f366979f2436de99206b" ON "__chart__per_user_notes" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_046feeb12e9ef5f783f409866a" ON "__chart__per_user_notes" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f68a5ab958f9f5fa17a32ac23b" ON "__chart__per_user_notes" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f7bf4c62059764c2c2bb40fdab" ON "__chart__per_user_reaction" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_65633a106bce43fc7c5c30a5c7" ON "__chart__per_user_reaction" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_8cf3156fd7a6b15c43459c6e3b" ON "__chart__per_user_reaction" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_edeb73c09c3143a81bcb34d569" ON "__chart__per_user_reaction" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e316f01a6d24eb31db27f88262" ON "__chart__per_user_reaction" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0c641990ecf47d2545df4edb75" ON "__chart__test_grouped" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_2be7ec6cebddc14dc11e206686" ON "__chart__test_grouped" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_234dff3c0b56a6150b95431ab9" ON "__chart__test_grouped" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a5133470f4825902e170328ca5" ON "__chart__test_grouped" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_84e661abb7bd1e51b690d4b017" ON "__chart__test_grouped" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_437bab3c6061d90f6bb65fd2cc" ON "__chart__test_unique" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_5c73bf61da4f6e6f15bae88ed1" ON "__chart__test_unique" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_bbfa573a8181018851ed0b6357" ON "__chart__test_unique" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_d70c86baedc68326be11f9c0ce" ON "__chart__test_unique" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_66e1e1ecd2f29e57778af35b59" ON "__chart__test_unique" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_b070a906db04b44c67c6c2144d" ON "__chart__test" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_92255988735563f0fe4aba1f05" ON "__chart__test" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_d41cce6aee1a50bfc062038f9b" ON "__chart__test" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_c5870993e25c3d5771f91f5003" ON "__chart__test" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f170de677ea75ad4533de2723e" ON "__chart__test" ("span", "date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_845254b3eaf708ae8a6cac3026" ON "__chart__users" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7c184198ecf66a8d3ecb253ab3" ON "__chart__users" ("span") `);
-        await queryRunner.query(`CREATE INDEX "IDX_ed9b95919c672a13008e9487ee" ON "__chart__users" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f091abb24193d50c653c6b77fc" ON "__chart__users" ("span", "date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a770a57c70e668cc61590c9161" ON "__chart__users" ("span", "date", "group") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`DROP INDEX "IDX_a770a57c70e668cc61590c9161"`);
-        await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`);
-        await queryRunner.query(`DROP INDEX "IDX_f091abb24193d50c653c6b77fc"`);
-        await queryRunner.query(`DROP INDEX "IDX_ed9b95919c672a13008e9487ee"`);
-        await queryRunner.query(`DROP INDEX "IDX_7c184198ecf66a8d3ecb253ab3"`);
-        await queryRunner.query(`DROP INDEX "IDX_845254b3eaf708ae8a6cac3026"`);
-        await queryRunner.query(`DROP INDEX "IDX_f170de677ea75ad4533de2723e"`);
-        await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`);
-        await queryRunner.query(`DROP INDEX "IDX_c5870993e25c3d5771f91f5003"`);
-        await queryRunner.query(`DROP INDEX "IDX_d41cce6aee1a50bfc062038f9b"`);
-        await queryRunner.query(`DROP INDEX "IDX_92255988735563f0fe4aba1f05"`);
-        await queryRunner.query(`DROP INDEX "IDX_b070a906db04b44c67c6c2144d"`);
-        await queryRunner.query(`DROP INDEX "IDX_66e1e1ecd2f29e57778af35b59"`);
-        await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`);
-        await queryRunner.query(`DROP INDEX "IDX_d70c86baedc68326be11f9c0ce"`);
-        await queryRunner.query(`DROP INDEX "IDX_bbfa573a8181018851ed0b6357"`);
-        await queryRunner.query(`DROP INDEX "IDX_5c73bf61da4f6e6f15bae88ed1"`);
-        await queryRunner.query(`DROP INDEX "IDX_437bab3c6061d90f6bb65fd2cc"`);
-        await queryRunner.query(`DROP INDEX "IDX_84e661abb7bd1e51b690d4b017"`);
-        await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`);
-        await queryRunner.query(`DROP INDEX "IDX_a5133470f4825902e170328ca5"`);
-        await queryRunner.query(`DROP INDEX "IDX_234dff3c0b56a6150b95431ab9"`);
-        await queryRunner.query(`DROP INDEX "IDX_2be7ec6cebddc14dc11e206686"`);
-        await queryRunner.query(`DROP INDEX "IDX_0c641990ecf47d2545df4edb75"`);
-        await queryRunner.query(`DROP INDEX "IDX_e316f01a6d24eb31db27f88262"`);
-        await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`);
-        await queryRunner.query(`DROP INDEX "IDX_edeb73c09c3143a81bcb34d569"`);
-        await queryRunner.query(`DROP INDEX "IDX_8cf3156fd7a6b15c43459c6e3b"`);
-        await queryRunner.query(`DROP INDEX "IDX_65633a106bce43fc7c5c30a5c7"`);
-        await queryRunner.query(`DROP INDEX "IDX_f7bf4c62059764c2c2bb40fdab"`);
-        await queryRunner.query(`DROP INDEX "IDX_f68a5ab958f9f5fa17a32ac23b"`);
-        await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`);
-        await queryRunner.query(`DROP INDEX "IDX_046feeb12e9ef5f783f409866a"`);
-        await queryRunner.query(`DROP INDEX "IDX_55bf20f366979f2436de99206b"`);
-        await queryRunner.query(`DROP INDEX "IDX_8d2cbbc8114d90d19b44d626b6"`);
-        await queryRunner.query(`DROP INDEX "IDX_84234bd1abb873f07329681c83"`);
-        await queryRunner.query(`DROP INDEX "IDX_4db3b84c7be0d3464714f3e0b1"`);
-        await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`);
-        await queryRunner.query(`DROP INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f"`);
-        await queryRunner.query(`DROP INDEX "IDX_4b3593098b6edc9c5afe36b18b"`);
-        await queryRunner.query(`DROP INDEX "IDX_f92dd6d03f8d994f29987f6214"`);
-        await queryRunner.query(`DROP INDEX "IDX_7af07790712aa3438ff6773f3b"`);
-        await queryRunner.query(`DROP INDEX "IDX_f2aeafde2ae6fbad38e857631b"`);
-        await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`);
-        await queryRunner.query(`DROP INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53"`);
-        await queryRunner.query(`DROP INDEX "IDX_e496ca8096d28f6b9b509264dc"`);
-        await queryRunner.query(`DROP INDEX "IDX_328f259961e60c4fa0bfcf55ca"`);
-        await queryRunner.query(`DROP INDEX "IDX_5f86db6492274e07c1a3cdf286"`);
-        await queryRunner.query(`DROP INDEX "IDX_924fc196c80ca24bae01dd37e4"`);
-        await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`);
-        await queryRunner.query(`DROP INDEX "IDX_0c9a159c5082cbeef3ca6706b5"`);
-        await queryRunner.query(`DROP INDEX "IDX_7036f2957151588b813185c794"`);
-        await queryRunner.query(`DROP INDEX "IDX_e69096589f11e3baa98ddd64d0"`);
-        await queryRunner.query(`DROP INDEX "IDX_42eb716a37d381cdf566192b2b"`);
-        await queryRunner.query(`DROP INDEX "IDX_9ff6944f01acb756fdc92d7563"`);
-        await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`);
-        await queryRunner.query(`DROP INDEX "IDX_08fac0eb3b11f04c200c0b40dd"`);
-        await queryRunner.query(`DROP INDEX "IDX_7b5da130992ec9df96712d4290"`);
-        await queryRunner.query(`DROP INDEX "IDX_f8dd01baeded2ffa833e0a610a"`);
-        await queryRunner.query(`DROP INDEX "IDX_a1efd3e0048a5f2793a47360dc"`);
-        await queryRunner.query(`DROP INDEX "IDX_f5448d9633cff74208d850aabe"`);
-        await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`);
-        await queryRunner.query(`DROP INDEX "IDX_d0a4f79af5a97b08f37b547197"`);
-        await queryRunner.query(`DROP INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63"`);
-        await queryRunner.query(`DROP INDEX "IDX_c12f0af4a66cdd30c2287ce8aa"`);
-        await queryRunner.query(`DROP INDEX "IDX_6b8f34a1a64b06014b6fb66824"`);
-        await queryRunner.query(`DROP INDEX "IDX_6d6f156ceefc6bc5f273a0e370"`);
-        await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`);
-        await queryRunner.query(`DROP INDEX "IDX_49975586f50ed7b800fdd88fbd"`);
-        await queryRunner.query(`DROP INDEX "IDX_99a7d2faaef84a6f728d714ad6"`);
-        await queryRunner.query(`DROP INDEX "IDX_fcc181fb8283009c61cc4083ef"`);
-        await queryRunner.query(`DROP INDEX "IDX_07747a1038c05f532a718fe1de"`);
-        await queryRunner.query(`DROP INDEX "IDX_e9cd07672b37d8966cf3709283"`);
-        await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`);
-        await queryRunner.query(`DROP INDEX "IDX_2d416e6af791a82e338c79d480"`);
-        await queryRunner.query(`DROP INDEX "IDX_76e87c7bfc5d925fcbba405d84"`);
-        await queryRunner.query(`DROP INDEX "IDX_e447064455928cf627590ef527"`);
-        await queryRunner.query(`DROP INDEX "IDX_36cb699c49580d4e6c2e6159f9"`);
-        await queryRunner.query(`DROP INDEX "IDX_06690fc959f1c9fdaf21928222"`);
-        await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`);
-        await queryRunner.query(`DROP INDEX "IDX_6e1df243476e20cbf86572ecc0"`);
-        await queryRunner.query(`DROP INDEX "IDX_7a170f67425e62a8fabb76c872"`);
-        await queryRunner.query(`DROP INDEX "IDX_3fa0d0f17ca72e3dc80999a032"`);
-        await queryRunner.query(`DROP INDEX "IDX_13565815f618a1ff53886c5b28"`);
-        await queryRunner.query(`DROP INDEX "IDX_c26e2c1cbb6e911e0554b27416"`);
-        await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`);
-        await queryRunner.query(`DROP INDEX "IDX_20f57cc8f142c131340ee16742"`);
-        await queryRunner.query(`DROP INDEX "IDX_00ed5f86db1f7efafb1978bf21"`);
-        await queryRunner.query(`DROP INDEX "IDX_15e91a03aeeac9dbccdf43fc06"`);
-        await queryRunner.query(`DROP INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc"`);
-        await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`);
-        await queryRunner.query(`DROP INDEX "IDX_88937d94d7443d9a99a76fa5c0"`);
-        await queryRunner.query(`DROP INDEX "IDX_54ebcb6d27222913b908d56fd8"`);
-        await queryRunner.query(`DROP INDEX "IDX_796a8c03959361f97dc2be1d5c"`);
-        await queryRunner.query(`DROP INDEX "IDX_25dfc71b0369b003a4cd434d0b"`);
-        await queryRunner.query(`DROP INDEX "IDX_51c063b6a133a9cb87145450f5"`);
-        await queryRunner.query(`DROP INDEX "IDX_fa99d777623947a5b05f394cae"`);
-        await queryRunner.query(`DROP INDEX "IDX_315c779174fe8247ab324f036e"`);
-        await queryRunner.query(`DROP INDEX "IDX_c5d46cbfda48b1c33ed852e21b"`);
-        await queryRunner.query(`DROP INDEX "IDX_8cb40cfc8f3c28261e6f887b03"`);
-    }
-
-}
diff --git a/migration/1562422242907-PasswordLessLogin.ts b/migration/1562422242907-PasswordLessLogin.ts
deleted file mode 100644
index e789a34334..0000000000
--- a/migration/1562422242907-PasswordLessLogin.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class PasswordLessLogin1562422242907 implements MigrationInterface {
-
-	public async up(queryRunner: QueryRunner): Promise<any> {
-		await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "usePasswordLessLogin" boolean DEFAULT false NOT NULL`);
-	}
-
-	public async down(queryRunner: QueryRunner): Promise<any> {
-		await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "usePasswordLessLogin"`);
-	}
-
-}
diff --git a/migration/1562444565093-PinnedPage.ts b/migration/1562444565093-PinnedPage.ts
deleted file mode 100644
index 4bdee22748..0000000000
--- a/migration/1562444565093-PinnedPage.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class PinnedPage1562444565093 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "pinnedPageId" character varying(32)`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27" UNIQUE ("pinnedPageId")`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27" FOREIGN KEY ("pinnedPageId") REFERENCES "page"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27"`);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27"`);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "pinnedPageId"`);
-    }
-
-}
diff --git a/migration/1562448332510-PageTitleHideOption.ts b/migration/1562448332510-PageTitleHideOption.ts
deleted file mode 100644
index acc9b7e26f..0000000000
--- a/migration/1562448332510-PageTitleHideOption.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class PageTitleHideOption1562448332510 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "page" ADD "hideTitleWhenPinned" boolean NOT NULL DEFAULT false`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "hideTitleWhenPinned"`);
-    }
-
-}
diff --git a/migration/1562869971568-ModerationLog.ts b/migration/1562869971568-ModerationLog.ts
deleted file mode 100644
index b37f38ee5d..0000000000
--- a/migration/1562869971568-ModerationLog.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class ModerationLog1562869971568 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "moderation_log" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "type" character varying(128) NOT NULL, "info" jsonb NOT NULL, CONSTRAINT "PK_d0adca6ecfd068db83e4526cc26" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_a08ad074601d204e0f69da9a95" ON "moderation_log" ("userId") `);
-        await queryRunner.query(`ALTER TABLE "moderation_log" ADD CONSTRAINT "FK_a08ad074601d204e0f69da9a954" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "moderation_log" DROP CONSTRAINT "FK_a08ad074601d204e0f69da9a954"`);
-        await queryRunner.query(`DROP INDEX "IDX_a08ad074601d204e0f69da9a95"`);
-        await queryRunner.query(`DROP TABLE "moderation_log"`);
-    }
-
-}
diff --git a/migration/1563757595828-UsedUsername.ts b/migration/1563757595828-UsedUsername.ts
deleted file mode 100644
index a076fcc0c5..0000000000
--- a/migration/1563757595828-UsedUsername.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class UsedUsername1563757595828 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "used_username" ("username" character varying(128) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_78fd79d2d24c6ac2f4cc9a31a5d" PRIMARY KEY ("username"))`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`DROP TABLE "used_username"`);
-    }
-
-}
diff --git a/migration/1565634203341-room.ts b/migration/1565634203341-room.ts
deleted file mode 100644
index 783b375ff9..0000000000
--- a/migration/1565634203341-room.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class room1565634203341 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "room"`);
-    }
-
-}
diff --git a/migration/1571220798684-CustomEmojiCategory.ts b/migration/1571220798684-CustomEmojiCategory.ts
deleted file mode 100644
index 37f63fa3d0..0000000000
--- a/migration/1571220798684-CustomEmojiCategory.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class CustomEmojiCategory1571220798684 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "emoji" ADD "category" character varying(128)`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "category"`, undefined);
-    }
-
-}
diff --git a/migration/1572760203493-nodeinfo.ts b/migration/1572760203493-nodeinfo.ts
deleted file mode 100644
index 88d8df7239..0000000000
--- a/migration/1572760203493-nodeinfo.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class nodeinfo1572760203493 implements MigrationInterface {
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "system"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "softwareName" character varying(64) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "softwareVersion" character varying(64) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "openRegistrations" boolean DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "name" character varying(256) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "description" character varying(4096) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "maintainerName" character varying(128) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "maintainerEmail" character varying(256) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "infoUpdatedAt" TIMESTAMP WITH TIME ZONE`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "infoUpdatedAt"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "maintainerEmail"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "maintainerName"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "description"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "name"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "openRegistrations"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "softwareVersion"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "softwareName"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "system" character varying(64)`, undefined);
-    }
-
-}
diff --git a/migration/1576269851876-TalkFederationId.ts b/migration/1576269851876-TalkFederationId.ts
deleted file mode 100644
index 57e86c8b03..0000000000
--- a/migration/1576269851876-TalkFederationId.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class TalkFederationId1576269851876 implements MigrationInterface {
-    name = 'TalkFederationId1576269851876'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "messaging_message" ADD "uri" character varying(512)`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "uri"`, undefined);
-    }
-
-}
diff --git a/migration/1576869585998-ProxyRemoteFiles.ts b/migration/1576869585998-ProxyRemoteFiles.ts
deleted file mode 100644
index 1d15370bb6..0000000000
--- a/migration/1576869585998-ProxyRemoteFiles.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class ProxyRemoteFiles1576869585998 implements MigrationInterface {
-    name = 'ProxyRemoteFiles1576869585998'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT false`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`, undefined);
-    }
-
-}
diff --git a/migration/1579267006611-v12.ts b/migration/1579267006611-v12.ts
deleted file mode 100644
index 2c15283fa4..0000000000
--- a/migration/1579267006611-v12.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v121579267006611 implements MigrationInterface {
-    name = 'v121579267006611'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "announcement" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "text" character varying(8192) NOT NULL, "title" character varying(256) NOT NULL, "imageUrl" character varying(1024), CONSTRAINT "PK_e0ef0550174fd1099a308fd18a0" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_118ec703e596086fc4515acb39" ON "announcement" ("createdAt") `, undefined);
-        await queryRunner.query(`CREATE TABLE "announcement_read" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "announcementId" character varying(32) NOT NULL, CONSTRAINT "PK_4b90ad1f42681d97b2683890c5e" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_8288151386172b8109f7239ab2" ON "announcement_read" ("userId") `, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf" ON "announcement_read" ("announcementId") `, undefined);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_924fa71815cfa3941d003702a0" ON "announcement_read" ("userId", "announcementId") `, undefined);
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isVerified"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "announcements"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableEmojiReaction"`, undefined);
-        await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_8288151386172b8109f7239ab28" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe" FOREIGN KEY ("announcementId") REFERENCES "announcement"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe"`, undefined);
-        await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_8288151386172b8109f7239ab28"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "enableEmojiReaction" boolean NOT NULL DEFAULT true`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "announcements" jsonb NOT NULL DEFAULT '[]'`, undefined);
-        await queryRunner.query(`ALTER TABLE "user" ADD "isVerified" boolean NOT NULL DEFAULT false`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_924fa71815cfa3941d003702a0"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_8288151386172b8109f7239ab2"`, undefined);
-        await queryRunner.query(`DROP TABLE "announcement_read"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_118ec703e596086fc4515acb39"`, undefined);
-        await queryRunner.query(`DROP TABLE "announcement"`, undefined);
-    }
-
-}
diff --git a/migration/1579270193251-v12-2.ts b/migration/1579270193251-v12-2.ts
deleted file mode 100644
index efad0cd560..0000000000
--- a/migration/1579270193251-v12-2.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v1221579270193251 implements MigrationInterface {
-    name = 'v1221579270193251'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "announcement_read" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "announcement_read" DROP COLUMN "createdAt"`, undefined);
-    }
-
-}
diff --git a/migration/1579282808087-v12-3.ts b/migration/1579282808087-v12-3.ts
deleted file mode 100644
index a330caa978..0000000000
--- a/migration/1579282808087-v12-3.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v1231579282808087 implements MigrationInterface {
-    name = 'v1231579282808087'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "announcement" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "updatedAt"`, undefined);
-    }
-
-}
diff --git a/migration/1579544426412-v12-4.ts b/migration/1579544426412-v12-4.ts
deleted file mode 100644
index d35b25d045..0000000000
--- a/migration/1579544426412-v12-4.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v1241579544426412 implements MigrationInterface {
-    name = 'v1241579544426412'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "notification" ADD "followRequestId" character varying(32)`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_bd7fab507621e635b32cd31892c" FOREIGN KEY ("followRequestId") REFERENCES "follow_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_bd7fab507621e635b32cd31892c"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "followRequestId"`, undefined);
-    }
-
-}
diff --git a/migration/1579977526288-v12-5.ts b/migration/1579977526288-v12-5.ts
deleted file mode 100644
index 5f824a676e..0000000000
--- a/migration/1579977526288-v12-5.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v1251579977526288 implements MigrationInterface {
-    name = 'v1251579977526288'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "clip" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "isPublic" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_f0685dac8d4dd056d7255670b75" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_2b5ec6c574d6802c94c80313fb" ON "clip" ("userId") `, undefined);
-        await queryRunner.query(`CREATE TABLE "clip_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_e94cda2f40a99b57e032a1a738b" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_a012eaf5c87c65da1deb5fdbfa" ON "clip_note" ("noteId") `, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_ebe99317bbbe9968a0c6f579ad" ON "clip_note" ("clipId") `, undefined);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6fc0ec357d55a18646262fdfff" ON "clip_note" ("noteId", "clipId") `, undefined);
-        await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'list')`, undefined);
-        await queryRunner.query(`CREATE TABLE "antenna" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "src" "antenna_src_enum" NOT NULL, "userListId" character varying(32), "keywords" jsonb NOT NULL DEFAULT '[]', "withFile" boolean NOT NULL, "expression" character varying(2048), "notify" boolean NOT NULL, "hasNewNote" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_c170b99775e1dccca947c9f2d5f" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_6446c571a0e8d0f05f01c78909" ON "antenna" ("userId") `, undefined);
-        await queryRunner.query(`CREATE TABLE "antenna_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "antennaId" character varying(32) NOT NULL, CONSTRAINT "PK_fb28d94d0989a3872df19fd6ef8" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_bd0397be22147e17210940e125" ON "antenna_note" ("noteId") `, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_0d775946662d2575dfd2068a5f" ON "antenna_note" ("antennaId") `, undefined);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_335a0bf3f904406f9ef3dd51c2" ON "antenna_note" ("noteId", "antennaId") `, undefined);
-        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "geo"`, undefined);
-        await queryRunner.query(`ALTER TABLE "clip" ADD CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_6446c571a0e8d0f05f01c789096" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_0d775946662d2575dfd2068a5f5"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_bd0397be22147e17210940e125b"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_6446c571a0e8d0f05f01c789096"`, undefined);
-        await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf"`, undefined);
-        await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3"`, undefined);
-        await queryRunner.query(`ALTER TABLE "clip" DROP CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2"`, undefined);
-        await queryRunner.query(`ALTER TABLE "note" ADD "geo" jsonb`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_335a0bf3f904406f9ef3dd51c2"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_0d775946662d2575dfd2068a5f"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_bd0397be22147e17210940e125"`, undefined);
-        await queryRunner.query(`DROP TABLE "antenna_note"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_6446c571a0e8d0f05f01c78909"`, undefined);
-        await queryRunner.query(`DROP TABLE "antenna"`, undefined);
-        await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_6fc0ec357d55a18646262fdfff"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_ebe99317bbbe9968a0c6f579ad"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_a012eaf5c87c65da1deb5fdbfa"`, undefined);
-        await queryRunner.query(`DROP TABLE "clip_note"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_2b5ec6c574d6802c94c80313fb"`, undefined);
-        await queryRunner.query(`DROP TABLE "clip"`, undefined);
-    }
-
-}
diff --git a/migration/1579993013959-v12-6.ts b/migration/1579993013959-v12-6.ts
deleted file mode 100644
index 4fa4623c3e..0000000000
--- a/migration/1579993013959-v12-6.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v1261579993013959 implements MigrationInterface {
-    name = 'v1261579993013959'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "hasNewNote"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna_note" ADD "read" boolean NOT NULL DEFAULT false`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON "antenna_note" ("read") `, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`DROP INDEX "IDX_9937ea48d7ae97ffb4f3f063a4"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna_note" DROP COLUMN "read"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ADD "hasNewNote" boolean NOT NULL DEFAULT false`, undefined);
-    }
-
-}
diff --git a/migration/1580069531114-v12-7.ts b/migration/1580069531114-v12-7.ts
deleted file mode 100644
index 227e7cceb6..0000000000
--- a/migration/1580069531114-v12-7.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v1271580069531114 implements MigrationInterface {
-    name = 'v1271580069531114'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ADD "caseSensitive" boolean NOT NULL DEFAULT false`, undefined);
-        await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined);
-        await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list')`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined);
-        await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'list')`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined);
-        await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
-        await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO  "antenna_src_enum"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "caseSensitive"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
-    }
-
-}
diff --git a/migration/1580148575182-v12-8.ts b/migration/1580148575182-v12-8.ts
deleted file mode 100644
index c63bdb4eb4..0000000000
--- a/migration/1580148575182-v12-8.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v1281580148575182 implements MigrationInterface {
-    name = 'v1281580148575182'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_ec5c201576192ba8904c345c5cc"`, undefined);
-        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "appId"`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "note" ADD "appId" character varying(32)`, undefined);
-        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_ec5c201576192ba8904c345c5cc" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, undefined);
-    }
-
-}
diff --git a/migration/1580154400017-v12-9.ts b/migration/1580154400017-v12-9.ts
deleted file mode 100644
index de06d26e49..0000000000
--- a/migration/1580154400017-v12-9.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v1291580154400017 implements MigrationInterface {
-    name = 'v1291580154400017'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna" ADD "withReplies" boolean NOT NULL DEFAULT false`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "withReplies"`, undefined);
-    }
-
-}
diff --git a/migration/1580276619901-v12-10.ts b/migration/1580276619901-v12-10.ts
deleted file mode 100644
index f48f42b4ac..0000000000
--- a/migration/1580276619901-v12-10.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v12101580276619901 implements MigrationInterface {
-    name = 'v12101580276619901'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-				await queryRunner.query(`TRUNCATE TABLE "notification"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined);
-        await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD "type" "notification_type_enum" NOT NULL`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined);
-        await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD "type" character varying(32) NOT NULL`, undefined);
-    }
-
-}
diff --git a/migration/1580331224276-v12-11.ts b/migration/1580331224276-v12-11.ts
deleted file mode 100644
index 30794b80e3..0000000000
--- a/migration/1580331224276-v12-11.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v12111580331224276 implements MigrationInterface {
-    name = 'v12111580331224276'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "isMarkedAsClosed"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "isSuspended" boolean NOT NULL DEFAULT false`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`DROP INDEX "IDX_34500da2e38ac393f7bb6b299c"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "isSuspended"`, undefined);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "isMarkedAsClosed" boolean NOT NULL DEFAULT false`, undefined);
-    }
-
-}
diff --git a/migration/1580508795118-v12-12.ts b/migration/1580508795118-v12-12.ts
deleted file mode 100644
index 99791b051b..0000000000
--- a/migration/1580508795118-v12-12.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v12121580508795118 implements MigrationInterface {
-    name = 'v12121580508795118'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitter"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterAccessToken"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterAccessTokenSecret"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterUserId"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterScreenName"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "github"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubAccessToken"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubId"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubLogin"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discord"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordAccessToken"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordRefreshToken"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordExpiresDate"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordId"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordUsername"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordDiscriminator"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "integrations" jsonb NOT NULL DEFAULT '{}'`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "integrations"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordDiscriminator" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordUsername" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordId" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordExpiresDate" character varying(64)`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordRefreshToken" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordAccessToken" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discord" boolean NOT NULL DEFAULT false`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubLogin" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubId" character varying(64)`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubAccessToken" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "github" boolean NOT NULL DEFAULT false`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterScreenName" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterUserId" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterAccessTokenSecret" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterAccessToken" character varying(64) DEFAULT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitter" boolean NOT NULL DEFAULT false`, undefined);
-    }
-
-}
diff --git a/migration/1580543501339-v12-13.ts b/migration/1580543501339-v12-13.ts
deleted file mode 100644
index 7f5d8b8030..0000000000
--- a/migration/1580543501339-v12-13.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v12131580543501339 implements MigrationInterface {
-    name = 'v12131580543501339'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE INDEX "IDX_NOTE_TAGS" ON "note" USING gin ("tags")`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`DROP INDEX "IDX_NOTE_TAGS"`, undefined);
-    }
-
-}
diff --git a/migration/1580864313253-v12-14.ts b/migration/1580864313253-v12-14.ts
deleted file mode 100644
index 7bf38e5c65..0000000000
--- a/migration/1580864313253-v12-14.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class v12141580864313253 implements MigrationInterface {
-    name = 'v12141580864313253'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "proxyAccount" TO "proxyAccountId"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(32)`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad" FOREIGN KEY ("proxyAccountId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(128)`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "proxyAccountId" TO "proxyAccount"`, undefined);
-    }
-
-}
diff --git a/migration/1581526429287-user-group-invitation.ts b/migration/1581526429287-user-group-invitation.ts
deleted file mode 100644
index 26ea54e0b8..0000000000
--- a/migration/1581526429287-user-group-invitation.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class userGroupInvitation1581526429287 implements MigrationInterface {
-    name = 'userGroupInvitation1581526429287'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "user_group_invitation" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_160c63ec02bf23f6a5c5e8140d6" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_bfbc6305547539369fe73eb144" ON "user_group_invitation" ("userId") `, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_5cc8c468090e129857e9fecce5" ON "user_group_invitation" ("userGroupId") `, undefined);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e9793f65f504e5a31fbaedbf2f" ON "user_group_invitation" ("userId", "userGroupId") `, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD "userGroupInvitationId" character varying(32)`, undefined);
-        await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined);
-        await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined);
-        await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD CONSTRAINT "FK_bfbc6305547539369fe73eb144a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD CONSTRAINT "FK_5cc8c468090e129857e9fecce5a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_8fe87814e978053a53b1beb7e98" FOREIGN KEY ("userGroupInvitationId") REFERENCES "user_group_invitation"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_8fe87814e978053a53b1beb7e98"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP CONSTRAINT "FK_5cc8c468090e129857e9fecce5a"`, undefined);
-        await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP CONSTRAINT "FK_bfbc6305547539369fe73eb144a"`, undefined);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined);
-        await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined);
-        await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined);
-        await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO  "notification_type_enum"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "userGroupInvitationId"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_e9793f65f504e5a31fbaedbf2f"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_5cc8c468090e129857e9fecce5"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_bfbc6305547539369fe73eb144"`, undefined);
-        await queryRunner.query(`DROP TABLE "user_group_invitation"`, undefined);
-    }
-
-}
diff --git a/migration/1581695816408-user-group-antenna.ts b/migration/1581695816408-user-group-antenna.ts
deleted file mode 100644
index 15eb2fe11b..0000000000
--- a/migration/1581695816408-user-group-antenna.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class userGroupAntenna1581695816408 implements MigrationInterface {
-    name = 'userGroupAntenna1581695816408'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna" ADD "userGroupJoiningId" character varying(32)`, undefined);
-        await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined);
-        await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list', 'group')`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined);
-        await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb" FOREIGN KEY ("userGroupJoiningId") REFERENCES "user_group_joining"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying array NOT NULL DEFAULT '{}'`, undefined);
-        await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list')`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined);
-        await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
-        await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO  "antenna_src_enum"`, undefined);
-        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "userGroupJoiningId"`, undefined);
-    }
-
-}
diff --git a/migration/1581708415836-drive-user-folder-id-index.ts b/migration/1581708415836-drive-user-folder-id-index.ts
deleted file mode 100644
index e7a7628ff0..0000000000
--- a/migration/1581708415836-drive-user-folder-id-index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class driveUserFolderIdIndex1581708415836 implements MigrationInterface {
-    name = 'driveUserFolderIdIndex1581708415836'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE INDEX "IDX_55720b33a61a7c806a8215b825" ON "drive_file" ("userId", "folderId", "id") `, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`DROP INDEX "IDX_55720b33a61a7c806a8215b825"`, undefined);
-    }
-
-}
diff --git a/migration/1581979837262-promo.ts b/migration/1581979837262-promo.ts
deleted file mode 100644
index 2c4f25c4d7..0000000000
--- a/migration/1581979837262-promo.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class promo1581979837262 implements MigrationInterface {
-    name = 'promo1581979837262'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`CREATE TABLE "promo_note" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "REL_e263909ca4fe5d57f8d4230dd5" UNIQUE ("noteId"), CONSTRAINT "PK_e263909ca4fe5d57f8d4230dd5c" PRIMARY KEY ("noteId"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_83f0862e9bae44af52ced7099e" ON "promo_note" ("userId") `, undefined);
-        await queryRunner.query(`CREATE TABLE "promo_read" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_61917c1541002422b703318b7c9" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_9657d55550c3d37bfafaf7d4b0" ON "promo_read" ("userId") `, undefined);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2882b8a1a07c7d281a98b6db16" ON "promo_read" ("userId", "noteId") `, undefined);
-        await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4"`, undefined);
-        await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05"`, undefined);
-        await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_2882b8a1a07c7d281a98b6db16"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_9657d55550c3d37bfafaf7d4b0"`, undefined);
-        await queryRunner.query(`DROP TABLE "promo_read"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_83f0862e9bae44af52ced7099e"`, undefined);
-        await queryRunner.query(`DROP TABLE "promo_note"`, undefined);
-    }
-
-}
diff --git a/migration/1582019042083-featured-injecttion.ts b/migration/1582019042083-featured-injecttion.ts
deleted file mode 100644
index 9c84989979..0000000000
--- a/migration/1582019042083-featured-injecttion.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class featuredInjecttion1582019042083 implements MigrationInterface {
-    name = 'featuredInjecttion1582019042083'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "injectFeaturedNote" boolean NOT NULL DEFAULT true`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "injectFeaturedNote"`, undefined);
-    }
-
-}
diff --git a/migration/1582210532752-antenna-exclude.ts b/migration/1582210532752-antenna-exclude.ts
deleted file mode 100644
index bff47a3ec6..0000000000
--- a/migration/1582210532752-antenna-exclude.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class antennaExclude1582210532752 implements MigrationInterface {
-    name = 'antennaExclude1582210532752'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeKeywords" jsonb NOT NULL DEFAULT '[]'`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords"`, undefined);
-    }
-
-}
diff --git a/migration/1582875306439-note-reaction-length.ts b/migration/1582875306439-note-reaction-length.ts
deleted file mode 100644
index b9af61eb0e..0000000000
--- a/migration/1582875306439-note-reaction-length.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class noteReactionLength1582875306439 implements MigrationInterface {
-    name = 'noteReactionLength1582875306439'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(128)`, undefined);
-    }
-
-}
diff --git a/migration/1585361548360-miauth.ts b/migration/1585361548360-miauth.ts
deleted file mode 100644
index 22d6c207ff..0000000000
--- a/migration/1585361548360-miauth.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class miauth1585361548360 implements MigrationInterface {
-    name = 'miauth1585361548360'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "access_token" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD "session" character varying(128) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD "name" character varying(128) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD "description" character varying(512) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD "iconUrl" character varying(512) DEFAULT null`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD "permission" character varying(64) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD "fetched" boolean NOT NULL DEFAULT false`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP NOT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET DEFAULT null`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_bf3a053c07d9fb5d87317c56ee" ON "access_token" ("session") `, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_bf3a053c07d9fb5d87317c56ee"`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET NOT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "fetched"`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "permission"`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "iconUrl"`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "description"`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "name"`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "session"`, undefined);
-        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "lastUsedAt"`, undefined);
-    }
-
-}
diff --git a/migration/1585385921215-custom-notification.ts b/migration/1585385921215-custom-notification.ts
deleted file mode 100644
index 24c29d53e5..0000000000
--- a/migration/1585385921215-custom-notification.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class customNotification1585385921215 implements MigrationInterface {
-    name = 'customNotification1585385921215'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "notification" ADD "customBody" character varying(2048)`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD "customHeader" character varying(256)`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD "customIcon" character varying(1024)`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD "appAccessTokenId" character varying(32)`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" DROP NOT NULL`, undefined);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS 'The ID of sender user of the Notification.'`, undefined);
-        await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined);
-        await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined);
-        await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71" ON "notification" ("notifierId") `, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_33f33cc8ef29d805a97ff4628b" ON "notification" ("type") `, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_080ab397c379af09b9d2169e5b" ON "notification" ("isRead") `, undefined);
-        await queryRunner.query(`CREATE INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c" ON "notification" ("appAccessTokenId") `, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9" FOREIGN KEY ("appAccessTokenId") REFERENCES "access_token"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_080ab397c379af09b9d2169e5b"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_33f33cc8ef29d805a97ff4628b"`, undefined);
-        await queryRunner.query(`DROP INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71"`, undefined);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined);
-        await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined);
-        await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined);
-        await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO  "notification_type_enum"`, undefined);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS ''`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" SET NOT NULL`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "appAccessTokenId"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customIcon"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customHeader"`, undefined);
-        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customBody"`, undefined);
-    }
-
-}
diff --git a/migration/1585772678853-ap-url.ts b/migration/1585772678853-ap-url.ts
deleted file mode 100644
index 622d097276..0000000000
--- a/migration/1585772678853-ap-url.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/* tslint:disable:quotemark class-name indent */
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class apUrl1585772678853 implements MigrationInterface {
-    name = 'apUrl1585772678853'
-
-    public async up(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "note" ADD "url" character varying(512)`, undefined);
-     }
-
-    public async down(queryRunner: QueryRunner): Promise<any> {
-        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "url"`, undefined);
-    }
-
-}
diff --git a/migration/1586624197029-AddObjectStorageUseProxy.ts b/migration/1586624197029-AddObjectStorageUseProxy.ts
deleted file mode 100644
index deadf94834..0000000000
--- a/migration/1586624197029-AddObjectStorageUseProxy.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from 'typeorm';
-
-export class AddObjectStorageUseProxy1586624197029 implements MigrationInterface {
-		name = 'AddObjectStorageUseProxy1586624197029'
-
-		public async up(queryRunner: QueryRunner): Promise<void> {
-				await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseProxy" boolean NOT NULL DEFAULT true`, undefined);
-		}
-
-		public async down(queryRunner: QueryRunner): Promise<void> {
-				await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseProxy"`, undefined);
-		}
-
-}
diff --git a/migration/1586641139527-remote-reaction.ts b/migration/1586641139527-remote-reaction.ts
deleted file mode 100644
index 5a7fb36e35..0000000000
--- a/migration/1586641139527-remote-reaction.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class remoteReaction1586641139527 implements MigrationInterface {
-    name = 'remoteReaction1586641139527'
-    public async up(queryRunner: QueryRunner): Promise<any> {
-      await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(260)`, undefined);
-  }
-
-  public async down(queryRunner: QueryRunner): Promise<any> {
-      await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined);
-  }
-}
diff --git a/migration/1586708940386-pageAiScript.ts b/migration/1586708940386-pageAiScript.ts
deleted file mode 100644
index fdd6e76b9b..0000000000
--- a/migration/1586708940386-pageAiScript.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class pageAiScript1586708940386 implements MigrationInterface {
-    name = 'pageAiScript1586708940386'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "page" ADD "script" character varying(16384) NOT NULL DEFAULT ''`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "script"`, undefined);
-    }
-
-}
diff --git a/migration/1588044505511-hCaptcha.ts b/migration/1588044505511-hCaptcha.ts
deleted file mode 100644
index a3f4e93670..0000000000
--- a/migration/1588044505511-hCaptcha.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class hCaptcha1588044505511 implements MigrationInterface {
-    name = 'hCaptcha1588044505511'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "enableHcaptcha" boolean NOT NULL DEFAULT false`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "hcaptchaSiteKey" character varying(64)`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "hcaptchaSecretKey" character varying(64)`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hcaptchaSecretKey"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hcaptchaSiteKey"`, undefined);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableHcaptcha"`, undefined);
-    }
-
-}
diff --git a/migration/1589023282116-pubRelay.ts b/migration/1589023282116-pubRelay.ts
deleted file mode 100644
index 3b9d359915..0000000000
--- a/migration/1589023282116-pubRelay.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class pubRelay1589023282116 implements MigrationInterface {
-    name = 'pubRelay1589023282116'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TYPE "relay_status_enum" AS ENUM('requesting', 'accepted', 'rejected')`, undefined);
-        await queryRunner.query(`CREATE TABLE "relay" ("id" character varying(32) NOT NULL, "inbox" character varying(512) NOT NULL, "status" "relay_status_enum" NOT NULL, CONSTRAINT "PK_78ebc9cfddf4292633b7ba57aee" PRIMARY KEY ("id"))`, undefined);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab" ON "relay" ("inbox") `, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab"`, undefined);
-        await queryRunner.query(`DROP TABLE "relay"`, undefined);
-        await queryRunner.query(`DROP TYPE "relay_status_enum"`, undefined);
-    }
-
-}
diff --git a/migration/1595075960584-blurhash.ts b/migration/1595075960584-blurhash.ts
deleted file mode 100644
index 7c716ae172..0000000000
--- a/migration/1595075960584-blurhash.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class blurhash1595075960584 implements MigrationInterface {
-    name = 'blurhash1595075960584'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "drive_file" ADD "blurhash" character varying(128)`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "blurhash"`);
-    }
-
-}
diff --git a/migration/1595077605646-blurhash-for-avatar-banner.ts b/migration/1595077605646-blurhash-for-avatar-banner.ts
deleted file mode 100644
index fcf161c350..0000000000
--- a/migration/1595077605646-blurhash-for-avatar-banner.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class blurhashForAvatarBanner1595077605646 implements MigrationInterface {
-    name = 'blurhashForAvatarBanner1595077605646'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarColor"`);
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerColor"`);
-        await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
-        await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
-        await queryRunner.query(`ALTER TABLE "user" ADD "bannerColor" character varying(32)`);
-        await queryRunner.query(`ALTER TABLE "user" ADD "avatarColor" character varying(32)`);
-    }
-
-}
diff --git a/migration/1595676934834-instance-icon-url.ts b/migration/1595676934834-instance-icon-url.ts
deleted file mode 100644
index c75370f174..0000000000
--- a/migration/1595676934834-instance-icon-url.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class instanceIconUrl1595676934834 implements MigrationInterface {
-    name = 'instanceIconUrl1595676934834'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "instance" ADD "iconUrl" character varying(256) DEFAULT null`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "iconUrl"`);
-    }
-
-}
diff --git a/migration/1595771249699-word-mute.ts b/migration/1595771249699-word-mute.ts
deleted file mode 100644
index 1a9114d921..0000000000
--- a/migration/1595771249699-word-mute.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class wordMute1595771249699 implements MigrationInterface {
-    name = 'wordMute1595771249699'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TABLE "muted_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_897e2eff1c0b9b64e55ca1418a4" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_70ab9786313d78e4201d81cdb8" ON "muted_note" ("noteId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_d8e07aa18c2d64e86201601aec" ON "muted_note" ("userId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a8c6bfd637d3f1d67a27c48e27" ON "muted_note" ("noteId", "userId") `);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "enableWordMute" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutedWords" jsonb NOT NULL DEFAULT '[]'`);
-        await queryRunner.query(`CREATE INDEX "IDX_3befe6f999c86aff06eb0257b4" ON "user_profile" ("enableWordMute") `);
-        await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_70ab9786313d78e4201d81cdb89" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1"`);
-        await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_70ab9786313d78e4201d81cdb89"`);
-        await queryRunner.query(`DROP INDEX "IDX_3befe6f999c86aff06eb0257b4"`);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutedWords"`);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "enableWordMute"`);
-        await queryRunner.query(`DROP INDEX "IDX_a8c6bfd637d3f1d67a27c48e27"`);
-        await queryRunner.query(`DROP INDEX "IDX_d8e07aa18c2d64e86201601aec"`);
-        await queryRunner.query(`DROP INDEX "IDX_70ab9786313d78e4201d81cdb8"`);
-        await queryRunner.query(`DROP TABLE "muted_note"`);
-    }
-
-}
diff --git a/migration/1595782306083-word-mute2.ts b/migration/1595782306083-word-mute2.ts
deleted file mode 100644
index d68c12740e..0000000000
--- a/migration/1595782306083-word-mute2.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class wordMute21595782306083 implements MigrationInterface {
-    name = 'wordMute21595782306083'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TYPE "muted_note_reason_enum" AS ENUM('word', 'manual', 'spam', 'other')`);
-        await queryRunner.query(`ALTER TABLE "muted_note" ADD "reason" "muted_note_reason_enum" NOT NULL`);
-        await queryRunner.query(`CREATE INDEX "IDX_636e977ff90b23676fb5624b25" ON "muted_note" ("reason") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_636e977ff90b23676fb5624b25"`);
-        await queryRunner.query(`ALTER TABLE "muted_note" DROP COLUMN "reason"`);
-        await queryRunner.query(`DROP TYPE "muted_note_reason_enum"`);
-    }
-
-}
diff --git a/migration/1596548170836-channel.ts b/migration/1596548170836-channel.ts
deleted file mode 100644
index 4e3ebb330a..0000000000
--- a/migration/1596548170836-channel.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class channel1596548170836 implements MigrationInterface {
-    name = 'channel1596548170836'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TABLE "channel" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastNotedAt" TIMESTAMP WITH TIME ZONE, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "description" character varying(2048), "bannerId" character varying(32), "notesCount" integer NOT NULL DEFAULT 0, "usersCount" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_590f33ee6ee7d76437acf362e39" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_71cb7b435b7c0d4843317e7e16" ON "channel" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_29ef80c6f13bcea998447fce43" ON "channel" ("lastNotedAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_823bae55bd81b3be6e05cff438" ON "channel" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0f58c11241e649d2a638a8de94" ON "channel" ("notesCount") `);
-        await queryRunner.query(`CREATE INDEX "IDX_094b86cd36bb805d1aa1e8cc9a" ON "channel" ("usersCount") `);
-        await queryRunner.query(`CREATE TABLE "channel_following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, CONSTRAINT "PK_8b104be7f7415113f2a02cd5bdd" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_11e71f2511589dcc8a4d3214f9" ON "channel_following" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0e43068c3f92cab197c3d3cd86" ON "channel_following" ("followeeId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_6d8084ec9496e7334a4602707e" ON "channel_following" ("followerId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2e230dd45a10e671d781d99f3e" ON "channel_following" ("followerId", "followeeId") `);
-        await queryRunner.query(`CREATE TABLE "channel_note_pining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_44f7474496bcf2e4b741681146d" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_8125f950afd3093acb10d2db8a" ON "channel_note_pining" ("channelId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f36fed37d6d4cdcc68c803cd9c" ON "channel_note_pining" ("channelId", "noteId") `);
-        await queryRunner.query(`ALTER TABLE "note" ADD "channelId" character varying(32) DEFAULT null`);
-        await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `);
-        await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_823bae55bd81b3be6e05cff4383" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19" FOREIGN KEY ("bannerId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_f22169eb10657bded6d875ac8f9" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e" FOREIGN KEY ("followeeId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_6d8084ec9496e7334a4602707e1" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_8125f950afd3093acb10d2db8a8" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_10b19ef67d297ea9de325cd4502" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_10b19ef67d297ea9de325cd4502"`);
-        await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_8125f950afd3093acb10d2db8a8"`);
-        await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_6d8084ec9496e7334a4602707e1"`);
-        await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e"`);
-        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_f22169eb10657bded6d875ac8f9"`);
-        await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19"`);
-        await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_823bae55bd81b3be6e05cff4383"`);
-        await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
-        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "channelId"`);
-        await queryRunner.query(`DROP INDEX "IDX_f36fed37d6d4cdcc68c803cd9c"`);
-        await queryRunner.query(`DROP INDEX "IDX_8125f950afd3093acb10d2db8a"`);
-        await queryRunner.query(`DROP TABLE "channel_note_pining"`);
-        await queryRunner.query(`DROP INDEX "IDX_2e230dd45a10e671d781d99f3e"`);
-        await queryRunner.query(`DROP INDEX "IDX_6d8084ec9496e7334a4602707e"`);
-        await queryRunner.query(`DROP INDEX "IDX_0e43068c3f92cab197c3d3cd86"`);
-        await queryRunner.query(`DROP INDEX "IDX_11e71f2511589dcc8a4d3214f9"`);
-        await queryRunner.query(`DROP TABLE "channel_following"`);
-        await queryRunner.query(`DROP INDEX "IDX_094b86cd36bb805d1aa1e8cc9a"`);
-        await queryRunner.query(`DROP INDEX "IDX_0f58c11241e649d2a638a8de94"`);
-        await queryRunner.query(`DROP INDEX "IDX_823bae55bd81b3be6e05cff438"`);
-        await queryRunner.query(`DROP INDEX "IDX_29ef80c6f13bcea998447fce43"`);
-        await queryRunner.query(`DROP INDEX "IDX_71cb7b435b7c0d4843317e7e16"`);
-        await queryRunner.query(`DROP TABLE "channel"`);
-    }
-
-}
diff --git a/migration/1596786425167-channel2.ts b/migration/1596786425167-channel2.ts
deleted file mode 100644
index 0233f7ab0f..0000000000
--- a/migration/1596786425167-channel2.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class channel21596786425167 implements MigrationInterface {
-    name = 'channel21596786425167'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`);
-    }
-
-}
diff --git a/migration/1597230137744-objectStorageSetPublicRead.ts b/migration/1597230137744-objectStorageSetPublicRead.ts
deleted file mode 100644
index f040f2afdd..0000000000
--- a/migration/1597230137744-objectStorageSetPublicRead.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class objectStorageSetPublicRead1597230137744 implements MigrationInterface {
-    name = 'objectStorageSetPublicRead1597230137744'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSetPublicRead" boolean NOT NULL DEFAULT false`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSetPublicRead"`);
-    }
-
-}
diff --git a/migration/1597236229720-IncludingNotificationTypes.ts b/migration/1597236229720-IncludingNotificationTypes.ts
deleted file mode 100644
index be57824c0b..0000000000
--- a/migration/1597236229720-IncludingNotificationTypes.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class IncludingNotificationTypes1597236229720 implements MigrationInterface {
-    name = 'IncludingNotificationTypes1597236229720'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TYPE "user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`);
-        await queryRunner.query(`DROP TYPE "user_profile_includingnotificationtypes_enum"`);
-    }
-
-}
diff --git a/migration/1597385880794-add-sensitive-index.ts b/migration/1597385880794-add-sensitive-index.ts
deleted file mode 100644
index ff6251ac0a..0000000000
--- a/migration/1597385880794-add-sensitive-index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class addSensitiveIndex1597385880794 implements MigrationInterface {
-    name = 'addSensitiveIndex1597385880794'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE INDEX "IDX_a7eba67f8b3fa27271e85d2e26" ON "drive_file" ("isSensitive") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_a7eba67f8b3fa27271e85d2e26"`);
-    }
-
-}
diff --git a/migration/1597459042300-channel-unread.ts b/migration/1597459042300-channel-unread.ts
deleted file mode 100644
index a0f862114d..0000000000
--- a/migration/1597459042300-channel-unread.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class channelUnread1597459042300 implements MigrationInterface {
-    name = 'channelUnread1597459042300'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-				await queryRunner.query(`TRUNCATE TABLE "note_unread"`, undefined);
-        await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`);
-        await queryRunner.query(`ALTER TABLE "note_unread" ADD "isMentioned" boolean NOT NULL`);
-        await queryRunner.query(`ALTER TABLE "note_unread" ADD "noteChannelId" character varying(32)`);
-        await queryRunner.query(`CREATE INDEX "IDX_25b1dd384bec391b07b74b861c" ON "note_unread" ("isMentioned") `);
-        await queryRunner.query(`CREATE INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30" ON "note_unread" ("isSpecified") `);
-        await queryRunner.query(`CREATE INDEX "IDX_29e8c1d579af54d4232939f994" ON "note_unread" ("noteUserId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_6a57f051d82c6d4036c141e107" ON "note_unread" ("noteChannelId") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_6a57f051d82c6d4036c141e107"`);
-        await queryRunner.query(`DROP INDEX "IDX_29e8c1d579af54d4232939f994"`);
-        await queryRunner.query(`DROP INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30"`);
-        await queryRunner.query(`DROP INDEX "IDX_25b1dd384bec391b07b74b861c"`);
-        await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "noteChannelId"`);
-        await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "isMentioned"`);
-        await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`);
-    }
-
-}
diff --git a/migration/1597893996136-ChannelNoteIdDescIndex.ts b/migration/1597893996136-ChannelNoteIdDescIndex.ts
deleted file mode 100644
index 6cbd865fc6..0000000000
--- a/migration/1597893996136-ChannelNoteIdDescIndex.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class ChannelNoteIdDescIndex1597893996136 implements MigrationInterface {
-    name = 'ChannelNoteIdDescIndex1597893996136'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-				await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
-				await queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("channelId", "id" desc)`);
-		}
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-				await queryRunner.query(`DROP INDEX "IDX_note_on_channelId_and_id_desc"`);
-        await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `);
-    }
-
-}
diff --git a/migration/1600353287890-mutingNotificationTypes.ts b/migration/1600353287890-mutingNotificationTypes.ts
deleted file mode 100644
index 914bad8e3f..0000000000
--- a/migration/1600353287890-mutingNotificationTypes.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class mutingNotificationTypes1600353287890 implements MigrationInterface {
-    name = 'mutingNotificationTypes1600353287890'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`);
-        await queryRunner.query(`DROP TYPE "public"."user_profile_includingnotificationtypes_enum"`);
-        await queryRunner.query(`CREATE TYPE "user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutingNotificationTypes" "user_profile_mutingnotificationtypes_enum" array NOT NULL DEFAULT '{}'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutingNotificationTypes"`);
-        await queryRunner.query(`DROP TYPE "user_profile_mutingnotificationtypes_enum"`);
-        await queryRunner.query(`CREATE TYPE "public"."user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`);
-    }
-
-}
diff --git a/migration/1603094348345-refine-abuse-user-report.ts b/migration/1603094348345-refine-abuse-user-report.ts
deleted file mode 100644
index ce74a88d48..0000000000
--- a/migration/1603094348345-refine-abuse-user-report.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class refineAbuseUserReport1603094348345 implements MigrationInterface {
-    name = 'refineAbuseUserReport1603094348345'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_d049123c413e68ca52abe734203"`);
-        await queryRunner.query(`DROP INDEX "IDX_d049123c413e68ca52abe73420"`);
-        await queryRunner.query(`DROP INDEX "IDX_5cd442c3b2e74fdd99dae20243"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" RENAME COLUMN "userId" TO "targetUserId"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "assigneeId" character varying(32)`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolved" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "comment"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "comment" character varying(2048) NOT NULL DEFAULT '{}'::varchar[]`);
-        await queryRunner.query(`CREATE INDEX "IDX_2b15aaf4a0dc5be3499af7ab6a" ON "abuse_user_report" ("resolved") `);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_08b883dd5fdd6f9c4c1572b36de" FOREIGN KEY ("assigneeId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_08b883dd5fdd6f9c4c1572b36de"`);
-        await queryRunner.query(`DROP INDEX "IDX_2b15aaf4a0dc5be3499af7ab6a"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "comment"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "comment" character varying(512) NOT NULL DEFAULT '{}'::varchar[]`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolved"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "assigneeId"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" RENAME COLUMN "targetUserId" TO "userId"`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5cd442c3b2e74fdd99dae20243" ON "abuse_user_report" ("userId", "reporterId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_d049123c413e68ca52abe73420" ON "abuse_user_report" ("userId") `);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_d049123c413e68ca52abe734203" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-}
diff --git a/migration/1603095701770-refine-abuse-user-report2.ts b/migration/1603095701770-refine-abuse-user-report2.ts
deleted file mode 100644
index 18e0c05ac2..0000000000
--- a/migration/1603095701770-refine-abuse-user-report2.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class refineAbuseUserReport21603095701770 implements MigrationInterface {
-    name = 'refineAbuseUserReport21603095701770'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "targetUserHost" character varying(128)`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "reporterHost" character varying(128)`);
-        await queryRunner.query(`CREATE INDEX "IDX_4ebbf7f93cdc10e8d1ef2fc6cd" ON "abuse_user_report" ("targetUserHost") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f8d8b93740ad12c4ce8213a199" ON "abuse_user_report" ("reporterHost") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_f8d8b93740ad12c4ce8213a199"`);
-        await queryRunner.query(`DROP INDEX "IDX_4ebbf7f93cdc10e8d1ef2fc6cd"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "reporterHost"`);
-        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "targetUserHost"`);
-    }
-
-}
diff --git a/migration/1603776877564-instance-theme-color.ts b/migration/1603776877564-instance-theme-color.ts
deleted file mode 100644
index 80c9d516fc..0000000000
--- a/migration/1603776877564-instance-theme-color.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class instanceThemeColor1603776877564 implements MigrationInterface {
-    name = 'instanceThemeColor1603776877564'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "instance" ADD "themeColor" character varying(64) DEFAULT null`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "themeColor"`);
-    }
-
-}
diff --git a/migration/1603781553011-instance-favicon.ts b/migration/1603781553011-instance-favicon.ts
deleted file mode 100644
index d748c43f5e..0000000000
--- a/migration/1603781553011-instance-favicon.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class instanceFavicon1603781553011 implements MigrationInterface {
-    name = 'instanceFavicon1603781553011'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "instance" ADD "faviconUrl" character varying(256) DEFAULT null`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "faviconUrl"`);
-    }
-
-}
diff --git a/migration/1604821689616-delete-auto-watch.ts b/migration/1604821689616-delete-auto-watch.ts
deleted file mode 100644
index e47a4cf1db..0000000000
--- a/migration/1604821689616-delete-auto-watch.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class deleteAutoWatch1604821689616 implements MigrationInterface {
-    name = 'deleteAutoWatch1604821689616'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoWatch"`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoWatch" boolean NOT NULL DEFAULT false`);
-    }
-
-}
diff --git a/migration/1605408848373-clip-description.ts b/migration/1605408848373-clip-description.ts
deleted file mode 100644
index b352173e4c..0000000000
--- a/migration/1605408848373-clip-description.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class clipDescription1605408848373 implements MigrationInterface {
-    name = 'clipDescription1605408848373'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "clip" ADD "description" character varying(2048) DEFAULT null`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-
-        await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "description"`);
-    }
-
-}
diff --git a/migration/1605408971051-comments.ts b/migration/1605408971051-comments.ts
deleted file mode 100644
index c69ae29a9b..0000000000
--- a/migration/1605408971051-comments.ts
+++ /dev/null
@@ -1,434 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class comments1605408971051 implements MigrationInterface {
-    name = 'comments1605408971051'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`COMMENT ON COLUMN "log"."createdAt" IS 'The created date of the Log.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."createdAt" IS 'The created date of the DriveFolder.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."name" IS 'The name of the DriveFolder.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."userId" IS 'The owner ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."parentId" IS 'The parent folder ID. If null, it means the DriveFolder is located in root.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."createdAt" IS 'The created date of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userId" IS 'The owner ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userHost" IS 'The host of owner. It will be null if the user in local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."md5" IS 'The MD5 hash of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."name" IS 'The file name of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."type" IS 'The content type (MIME) of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."size" IS 'The file size (bytes) of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."comment" IS 'The comment of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."blurhash" IS 'The BlurHash string.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."properties" IS 'The any properties of the DriveFile. For example, it includes image width/height.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."thumbnailUrl" IS 'The URL of the thumbnail of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."webpublicUrl" IS 'The URL of the webpublic of the DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."folderId" IS 'The parent folder ID. If null, it means the DriveFile is located in root.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isSensitive" IS 'Whether the DriveFile is NSFW.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isLink" IS 'Whether the DriveFile is direct link to remote server.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."createdAt" IS 'The created date of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."updatedAt" IS 'The updated date of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."username" IS 'The username of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."usernameLower" IS 'The username (lowercased) of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."name" IS 'The name of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."followersCount" IS 'The count of followers.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."followingCount" IS 'The count of following.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."notesCount" IS 'The count of notes.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."avatarId" IS 'The ID of avatar DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."bannerId" IS 'The ID of banner DriveFile.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isSuspended" IS 'Whether the User is suspended.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isSilenced" IS 'Whether the User is silenced.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isLocked" IS 'Whether the User is locked.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isBot" IS 'Whether the User is a bot.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isCat" IS 'Whether the User is a cat.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isAdmin" IS 'Whether the User is the admin.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isModerator" IS 'Whether the User is a moderator.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."host" IS 'The host of the User. It will be null if the origin of the user is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."inbox" IS 'The inbox URL of the User. It will be null if the origin of the user is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."sharedInbox" IS 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."featured" IS 'The featured URL of the User. It will be null if the origin of the user is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."uri" IS 'The URI of the User. It will be null if the origin of the user is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS 'The native access token of the User. It will be null if the origin of the user is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."createdAt" IS 'The created date of the App.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."userId" IS 'The owner ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."secret" IS 'The secret key of the App.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."name" IS 'The name of the App.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."description" IS 'The description of the App.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."permission" IS 'The permission of the App.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."callbackUrl" IS 'The callbackUrl of the App.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."createdAt" IS 'The created date of the AccessToken.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."lastUsedAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."session" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."appId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."description" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."iconUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."createdAt" IS 'The created date of the Channel.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."userId" IS 'The owner ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."name" IS 'The name of the Channel.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."description" IS 'The description of the Channel.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."bannerId" IS 'The ID of banner Channel.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."notesCount" IS 'The count of notes.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."usersCount" IS 'The count of users.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."createdAt" IS 'The created date of the Note.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."replyId" IS 'The ID of reply target.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteId" IS 'The ID of renote target.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."userId" IS 'The ID of author.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."uri" IS 'The URI of a note. it will be null when the note is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."url" IS 'The human readable url of a note. it will be null when the note is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."channelId" IS 'The ID of source channel.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."userHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserId" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserId" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll_vote"."createdAt" IS 'The created date of the PollVote.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_reaction"."createdAt" IS 'The created date of the NoteReaction.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."createdAt" IS 'The created date of the NoteWatching.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."userId" IS 'The watcher ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteId" IS 'The target Note ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteUserId" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteUserId" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteChannelId" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."createdAt" IS 'The created date of the FollowRequest.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeId" IS 'The followee user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerId" IS 'The follower user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."requestId" IS 'id of Follow Activity.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerInbox" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerSharedInbox" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeInbox" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeSharedInbox" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group"."createdAt" IS 'The created date of the UserGroup.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group"."userId" IS 'The ID of owner.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."createdAt" IS 'The created date of the UserGroupInvitation.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userId" IS 'The user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userGroupId" IS 'The group ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."createdAt" IS 'The created date of the Notification.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."notifieeId" IS 'The ID of recipient user of the Notification.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."localDriveCapacityMb" IS 'Drive capacity of a local user (MB)'`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."remoteDriveCapacityMb" IS 'Drive capacity of a remote user (MB)'`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."maxNoteTextLength" IS 'Max allowed note text length in characters'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."createdAt" IS 'The created date of the Following.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeId" IS 'The followee user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followerId" IS 'The follower user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followerHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followerInbox" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followerSharedInbox" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeInbox" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeSharedInbox" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."caughtAt" IS 'The caught date of the Instance.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."host" IS 'The host of the Instance.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."usersCount" IS 'The count of the users of the Instance.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."notesCount" IS 'The count of the notes of the Instance.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareName" IS 'The software of the Instance.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareVersion" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."openRegistrations" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."description" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerName" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerEmail" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."iconUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."faviconUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."themeColor" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "muting"."createdAt" IS 'The created date of the Muting.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "muting"."muteeId" IS 'The mutee user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "muting"."muterId" IS 'The muter user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "blocking"."createdAt" IS 'The created date of the Blocking.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockeeId" IS 'The blockee user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockerId" IS 'The blocker user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list"."createdAt" IS 'The created date of the UserList.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list"."userId" IS 'The owner ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list"."name" IS 'The name of the UserList.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."createdAt" IS 'The created date of the UserListJoining.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userId" IS 'The user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userListId" IS 'The list ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."createdAt" IS 'The created date of the UserGroupJoining.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userId" IS 'The user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userGroupId" IS 'The group ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_favorite"."createdAt" IS 'The created date of the NoteFavorite.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."createdAt" IS 'The created date of the AbuseUserReport.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."targetUserHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."reporterHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."createdAt" IS 'The created date of the MessagingMessage.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."userId" IS 'The sender user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."groupId" IS 'The recipient group ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "signin"."createdAt" IS 'The created date of the Signin.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "auth_session"."createdAt" IS 'The created date of the AuthSession.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."createdAt" IS 'The created date of the ReversiGame.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."startedAt" IS 'The started date of the ReversiGame.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form1" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form2" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_matching"."createdAt" IS 'The created date of the ReversiMatching.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_note_pining"."createdAt" IS 'The created date of the UserNotePinings.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll"."noteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll"."noteVisibility" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll"."userId" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll"."userHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_keypair"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_publickey"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "page"."createdAt" IS 'The created date of the Page.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "page"."updatedAt" IS 'The updated date of the Page.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "page"."userId" IS 'The ID of author.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."location" IS 'The location of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."birthday" IS 'The birthday (YYYY-MM-DD) of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."description" IS 'The description (bio) of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."url" IS 'Remote URL of the user.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."email" IS 'The email address of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."password" IS 'The password hash of the User. It will be null if the origin of the user is local.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."clientData" IS 'The client-specific data of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."room" IS 'The room data of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userHost" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."id" IS 'Variable-length id given to navigator.credentials.get()'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'Variable-length public key used to verify attestations (hex-encoded).'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'The date of the last time the UserSecurityKey was successfully validated.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."name" IS 'User-defined name for this key'`);
-        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS 'Hex-encoded sha256 hash of the challenge.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS 'The date challenge was created for expiry purposes.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "moderation_log"."createdAt" IS 'The created date of the ModerationLog.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "announcement"."createdAt" IS 'The created date of the Announcement.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "announcement"."updatedAt" IS 'The updated date of the Announcement.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "announcement_read"."createdAt" IS 'The created date of the AnnouncementRead.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip"."createdAt" IS 'The created date of the Clip.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip"."userId" IS 'The owner ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip"."name" IS 'The name of the Clip.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip"."description" IS 'The description of the Clip.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip_note"."noteId" IS 'The note ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip_note"."clipId" IS 'The clip ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna"."createdAt" IS 'The created date of the Antenna.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna"."userId" IS 'The owner ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna"."name" IS 'The name of the Antenna.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."noteId" IS 'The note ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."antennaId" IS 'The antenna ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "promo_note"."noteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "promo_note"."userId" IS '[Denormalized]'`);
-        await queryRunner.query(`COMMENT ON COLUMN "promo_read"."createdAt" IS 'The created date of the PromoRead.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."noteId" IS 'The note ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."userId" IS 'The user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."reason" IS 'The reason of the MutedNote.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."createdAt" IS 'The created date of the ChannelFollowing.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followeeId" IS 'The followee channel ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followerId" IS 'The follower user ID.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel_note_pining"."createdAt" IS 'The created date of the ChannelNotePining.'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`COMMENT ON COLUMN "channel_note_pining"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followerId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followeeId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."reason" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."noteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "promo_read"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "promo_note"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "promo_note"."noteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."antennaId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."noteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "antenna"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip_note"."clipId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip_note"."noteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip"."description" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "clip"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "announcement_read"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "announcement"."updatedAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "announcement"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "moderation_log"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."id" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."room" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."clientData" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."password" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."email" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."url" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."description" IS 'The description (bio) of the User.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."birthday" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."location" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "page"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "page"."updatedAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "page"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_publickey"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_keypair"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll"."userHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll"."noteVisibility" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll"."noteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_note_pining"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_matching"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form2" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form1" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."startedAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "auth_session"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "signin"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."groupId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."reporterHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."targetUserHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_favorite"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userGroupId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userListId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_list"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockerId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockeeId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "blocking"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "muting"."muterId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "muting"."muteeId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "muting"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."themeColor" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."faviconUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."iconUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerEmail" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerName" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."description" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."openRegistrations" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareVersion" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareName" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."notesCount" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."usersCount" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."host" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "instance"."caughtAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeSharedInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followerSharedInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followerInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followerHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followerId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "following"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."maxNoteTextLength" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."remoteDriveCapacityMb" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "meta"."localDriveCapacityMb" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."notifieeId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "notification"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userGroupId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_group"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeSharedInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerSharedInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."requestId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteChannelId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteUserId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteUserId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note_reaction"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "poll_vote"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."userHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."channelId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."url" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."uri" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."replyId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "note"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."usersCount" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."notesCount" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."bannerId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."description" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "channel"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."iconUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."description" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."appId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."session" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."lastUsedAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "access_token"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."callbackUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."permission" IS 'The permission of the App.'`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."description" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."secret" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "app"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."uri" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."featured" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."sharedInbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."inbox" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."host" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isModerator" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isAdmin" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isCat" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isBot" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isLocked" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isSilenced" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isSuspended" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."bannerId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."avatarId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."notesCount" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."followingCount" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."followersCount" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."usernameLower" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."username" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."updatedAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isLink" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isSensitive" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."folderId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."webpublicUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."thumbnailUrl" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."properties" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."blurhash" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."comment" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."size" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."type" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."md5" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userHost" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."parentId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."userId" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."name" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."createdAt" IS NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "log"."createdAt" IS NULL`);
-    }
-
-}
diff --git a/migration/1605585339718-instance-pinned-pages.ts b/migration/1605585339718-instance-pinned-pages.ts
deleted file mode 100644
index f593461306..0000000000
--- a/migration/1605585339718-instance-pinned-pages.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class instancePinnedPages1605585339718 implements MigrationInterface {
-    name = 'instancePinnedPages1605585339718'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'::varchar[]`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`);
-    }
-
-}
diff --git a/migration/1605965516823-instance-images.ts b/migration/1605965516823-instance-images.ts
deleted file mode 100644
index bf8d408563..0000000000
--- a/migration/1605965516823-instance-images.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class instanceImages1605965516823 implements MigrationInterface {
-    name = 'instanceImages1605965516823'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "backgroundImageUrl" character varying(512)`);
-        await queryRunner.query(`ALTER TABLE "meta" ADD "logoImageUrl" character varying(512)`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "logoImageUrl"`);
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "backgroundImageUrl"`);
-    }
-
-}
diff --git a/migration/1606191203881-no-crawle.ts b/migration/1606191203881-no-crawle.ts
deleted file mode 100644
index accc8f8fe2..0000000000
--- a/migration/1606191203881-no-crawle.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class noCrawle1606191203881 implements MigrationInterface {
-    name = 'noCrawle1606191203881'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "noCrawle" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`);
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "noCrawle"`);
-    }
-
-}
diff --git a/migration/1607151207216-instance-pinned-clip.ts b/migration/1607151207216-instance-pinned-clip.ts
deleted file mode 100644
index 7e2c8b7de0..0000000000
--- a/migration/1607151207216-instance-pinned-clip.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class instancePinnedClip1607151207216 implements MigrationInterface {
-    name = 'instancePinnedClip1607151207216'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedClipId" character varying(32)`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedClipId"`);
-    }
-
-}
diff --git a/migration/1607353487793-isExplorable.ts b/migration/1607353487793-isExplorable.ts
deleted file mode 100644
index 034f8c3845..0000000000
--- a/migration/1607353487793-isExplorable.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class isExplorable1607353487793 implements MigrationInterface {
-    name = 'isExplorable1607353487793'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user" ADD "isExplorable" boolean NOT NULL DEFAULT true`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isExplorable" IS 'Whether the User is explorable.'`);
-        await queryRunner.query(`CREATE INDEX "IDX_d5a1b83c7cab66f167e6888188" ON "user" ("isExplorable") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_d5a1b83c7cab66f167e6888188"`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isExplorable" IS 'Whether the User is explorable.'`);
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isExplorable"`);
-    }
-
-}
diff --git a/migration/1610277136869-registry.ts b/migration/1610277136869-registry.ts
deleted file mode 100644
index 46c8113c16..0000000000
--- a/migration/1610277136869-registry.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class registry1610277136869 implements MigrationInterface {
-    name = 'registry1610277136869'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TABLE "registry_item" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "key" character varying(1024) NOT NULL, "scope" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[], "domain" character varying(512), CONSTRAINT "PK_64b3f7e6008b4d89b826cd3af95" PRIMARY KEY ("id")); COMMENT ON COLUMN "registry_item"."createdAt" IS 'The created date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."updatedAt" IS 'The updated date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "registry_item"."key" IS 'The key of the RegistryItem.'`);
-        await queryRunner.query(`CREATE INDEX "IDX_fb9d21ba0abb83223263df6bcb" ON "registry_item" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_22baca135bb8a3ea1a83d13df3" ON "registry_item" ("scope") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad" ON "registry_item" ("domain") `);
-        await queryRunner.query(`ALTER TABLE "registry_item" ADD CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "registry_item" DROP CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3"`);
-        await queryRunner.query(`DROP INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad"`);
-        await queryRunner.query(`DROP INDEX "IDX_22baca135bb8a3ea1a83d13df3"`);
-        await queryRunner.query(`DROP INDEX "IDX_fb9d21ba0abb83223263df6bcb"`);
-        await queryRunner.query(`DROP TABLE "registry_item"`);
-    }
-
-}
diff --git a/migration/1610277585759-registry2.ts b/migration/1610277585759-registry2.ts
deleted file mode 100644
index 2f2d80c486..0000000000
--- a/migration/1610277585759-registry2.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class registry21610277585759 implements MigrationInterface {
-    name = 'registry21610277585759'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "registry_item" ADD "value" jsonb NOT NULL DEFAULT '{}'`);
-        await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`);
-        await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "value"`);
-    }
-
-}
diff --git a/migration/1610283021566-registry3.ts b/migration/1610283021566-registry3.ts
deleted file mode 100644
index 61f235fb22..0000000000
--- a/migration/1610283021566-registry3.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class registry31610283021566 implements MigrationInterface {
-    name = 'registry31610283021566'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" DROP NOT NULL`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" SET NOT NULL`);
-    }
-
-}
diff --git a/migration/1611354329133-followersUri.ts b/migration/1611354329133-followersUri.ts
deleted file mode 100644
index 5114418aed..0000000000
--- a/migration/1611354329133-followersUri.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class followersUri1611354329133 implements MigrationInterface {
-    name = 'followersUri1611354329133'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user" ADD "followersUri" varchar(512) DEFAULT NULL`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."followersUri" IS 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`COMMENT ON COLUMN "user"."followersUri" IS 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'`);
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "followersUri"`);
-    }
-
-}
diff --git a/migration/1611397665007-gallery.ts b/migration/1611397665007-gallery.ts
deleted file mode 100644
index 1b64490feb..0000000000
--- a/migration/1611397665007-gallery.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class gallery1611397665007 implements MigrationInterface {
-    name = 'gallery1611397665007'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TABLE "gallery_post" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "description" character varying(2048), "userId" character varying(32) NOT NULL, "fileIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "isSensitive" boolean NOT NULL DEFAULT false, "likedCount" integer NOT NULL DEFAULT '0', "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_8e90d7b6015f2c4518881b14753" PRIMARY KEY ("id")); COMMENT ON COLUMN "gallery_post"."createdAt" IS 'The created date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."updatedAt" IS 'The updated date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."userId" IS 'The ID of author.'; COMMENT ON COLUMN "gallery_post"."isSensitive" IS 'Whether the post is sensitive.'`);
-        await queryRunner.query(`CREATE INDEX "IDX_8f1a239bd077c8864a20c62c2c" ON "gallery_post" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f631d37835adb04792e361807c" ON "gallery_post" ("updatedAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_985b836dddd8615e432d7043dd" ON "gallery_post" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3ca50563facd913c425e7a89ee" ON "gallery_post" ("fileIds") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f2d744d9a14d0dfb8b96cb7fc5" ON "gallery_post" ("isSensitive") `);
-        await queryRunner.query(`CREATE INDEX "IDX_1a165c68a49d08f11caffbd206" ON "gallery_post" ("likedCount") `);
-        await queryRunner.query(`CREATE INDEX "IDX_05cca34b985d1b8edc1d1e28df" ON "gallery_post" ("tags") `);
-        await queryRunner.query(`CREATE TABLE "gallery_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "postId" character varying(32) NOT NULL, CONSTRAINT "PK_853ab02be39b8de45cd720cc15f" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_8fd5215095473061855ceb948c" ON "gallery_like" ("userId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_df1b5f4099e99fb0bc5eae53b6" ON "gallery_like" ("userId", "postId") `);
-        await queryRunner.query(`ALTER TABLE "gallery_post" ADD CONSTRAINT "FK_985b836dddd8615e432d7043ddb" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_8fd5215095473061855ceb948cf" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-        await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_b1cb568bfe569e47b7051699fc8" FOREIGN KEY ("postId") REFERENCES "gallery_post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "gallery_like" DROP CONSTRAINT "FK_b1cb568bfe569e47b7051699fc8"`);
-        await queryRunner.query(`ALTER TABLE "gallery_like" DROP CONSTRAINT "FK_8fd5215095473061855ceb948cf"`);
-        await queryRunner.query(`ALTER TABLE "gallery_post" DROP CONSTRAINT "FK_985b836dddd8615e432d7043ddb"`);
-        await queryRunner.query(`DROP INDEX "IDX_df1b5f4099e99fb0bc5eae53b6"`);
-        await queryRunner.query(`DROP INDEX "IDX_8fd5215095473061855ceb948c"`);
-        await queryRunner.query(`DROP TABLE "gallery_like"`);
-        await queryRunner.query(`DROP INDEX "IDX_05cca34b985d1b8edc1d1e28df"`);
-        await queryRunner.query(`DROP INDEX "IDX_1a165c68a49d08f11caffbd206"`);
-        await queryRunner.query(`DROP INDEX "IDX_f2d744d9a14d0dfb8b96cb7fc5"`);
-        await queryRunner.query(`DROP INDEX "IDX_3ca50563facd913c425e7a89ee"`);
-        await queryRunner.query(`DROP INDEX "IDX_985b836dddd8615e432d7043dd"`);
-        await queryRunner.query(`DROP INDEX "IDX_f631d37835adb04792e361807c"`);
-        await queryRunner.query(`DROP INDEX "IDX_8f1a239bd077c8864a20c62c2c"`);
-        await queryRunner.query(`DROP TABLE "gallery_post"`);
-    }
-
-}
diff --git a/migration/1611547387175-objectStorageS3ForcePathStyle.ts b/migration/1611547387175-objectStorageS3ForcePathStyle.ts
deleted file mode 100644
index 1506a29007..0000000000
--- a/migration/1611547387175-objectStorageS3ForcePathStyle.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class objectStorageS3ForcePathStyle1611547387175 implements MigrationInterface {
-    name = 'objectStorageS3ForcePathStyle1611547387175'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageS3ForcePathStyle" boolean NOT NULL DEFAULT true`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageS3ForcePathStyle"`);
-    }
-
-}
diff --git a/migration/1612619156584-announcement-email.ts b/migration/1612619156584-announcement-email.ts
deleted file mode 100644
index 0371daf8fa..0000000000
--- a/migration/1612619156584-announcement-email.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class announcementEmail1612619156584 implements MigrationInterface {
-    name = 'announcementEmail1612619156584'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "receiveAnnouncementEmail" boolean NOT NULL DEFAULT true`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "receiveAnnouncementEmail"`);
-    }
-
-}
diff --git a/migration/1613155914446-emailNotificationTypes.ts b/migration/1613155914446-emailNotificationTypes.ts
deleted file mode 100644
index d6908aecfa..0000000000
--- a/migration/1613155914446-emailNotificationTypes.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class emailNotificationTypes1613155914446 implements MigrationInterface {
-    name = 'emailNotificationTypes1613155914446'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "emailNotificationTypes" jsonb NOT NULL DEFAULT '["follow","receiveFollowRequest","groupInvited"]'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "emailNotificationTypes"`);
-    }
-
-}
diff --git a/migration/1613181457597-user-lang.ts b/migration/1613181457597-user-lang.ts
deleted file mode 100644
index ac1fc88c99..0000000000
--- a/migration/1613181457597-user-lang.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class userLang1613181457597 implements MigrationInterface {
-    name = 'userLang1613181457597'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "lang" character varying(32)`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "lang"`);
-    }
-
-}
diff --git a/migration/1613503367223-use-bigint-for-driveUsage.ts b/migration/1613503367223-use-bigint-for-driveUsage.ts
deleted file mode 100644
index d04e817ec5..0000000000
--- a/migration/1613503367223-use-bigint-for-driveUsage.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class useBigintForDriveUsage1613503367223 implements MigrationInterface {
-    name = 'useBigintForDriveUsage1613503367223'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "driveUsage" TYPE bigint`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "driveUsage"`);
-        await queryRunner.query(`ALTER TABLE "instance" ADD "driveUsage" integer NOT NULL DEFAULT 0`);
-    }
-
-}
diff --git a/migration/1615965918224-chart-v2.ts b/migration/1615965918224-chart-v2.ts
deleted file mode 100644
index cacbd1945b..0000000000
--- a/migration/1615965918224-chart-v2.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class chartV21615965918224 implements MigrationInterface {
-	name = 'chartV21615965918224'
-
-	public async up(queryRunner: QueryRunner): Promise<void> {
-		await queryRunner.query(`DELETE FROM "__chart__active_users" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__drive" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__federation" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__hashtag" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__instance" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__network" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__notes" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__per_user_drive" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__per_user_following" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__per_user_notes" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__per_user_reaction" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__test" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__test_grouped" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__test_unique" WHERE "span" = 'day'`);
-		await queryRunner.query(`DELETE FROM "__chart__users" WHERE "span" = 'day'`);
-
-		await queryRunner.query(`DROP INDEX "IDX_15e91a03aeeac9dbccdf43fc06"`);
-		await queryRunner.query(`DROP INDEX "IDX_20f57cc8f142c131340ee16742"`);
-		await queryRunner.query(`DROP INDEX "IDX_c26e2c1cbb6e911e0554b27416"`);
-		await queryRunner.query(`DROP INDEX "IDX_3fa0d0f17ca72e3dc80999a032"`);
-		await queryRunner.query(`DROP INDEX "IDX_6e1df243476e20cbf86572ecc0"`);
-		await queryRunner.query(`DROP INDEX "IDX_06690fc959f1c9fdaf21928222"`);
-		await queryRunner.query(`DROP INDEX "IDX_e447064455928cf627590ef527"`);
-		await queryRunner.query(`DROP INDEX "IDX_2d416e6af791a82e338c79d480"`);
-		await queryRunner.query(`DROP INDEX "IDX_e9cd07672b37d8966cf3709283"`);
-		await queryRunner.query(`DROP INDEX "IDX_fcc181fb8283009c61cc4083ef"`);
-		await queryRunner.query(`DROP INDEX "IDX_49975586f50ed7b800fdd88fbd"`);
-		await queryRunner.query(`DROP INDEX "IDX_6d6f156ceefc6bc5f273a0e370"`);
-		await queryRunner.query(`DROP INDEX "IDX_c12f0af4a66cdd30c2287ce8aa"`);
-		await queryRunner.query(`DROP INDEX "IDX_d0a4f79af5a97b08f37b547197"`);
-		await queryRunner.query(`DROP INDEX "IDX_f5448d9633cff74208d850aabe"`);
-		await queryRunner.query(`DROP INDEX "IDX_f8dd01baeded2ffa833e0a610a"`);
-		await queryRunner.query(`DROP INDEX "IDX_08fac0eb3b11f04c200c0b40dd"`);
-		await queryRunner.query(`DROP INDEX "IDX_9ff6944f01acb756fdc92d7563"`);
-		await queryRunner.query(`DROP INDEX "IDX_e69096589f11e3baa98ddd64d0"`);
-		await queryRunner.query(`DROP INDEX "IDX_0c9a159c5082cbeef3ca6706b5"`);
-		await queryRunner.query(`DROP INDEX "IDX_924fc196c80ca24bae01dd37e4"`);
-		await queryRunner.query(`DROP INDEX "IDX_328f259961e60c4fa0bfcf55ca"`);
-		await queryRunner.query(`DROP INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53"`);
-		await queryRunner.query(`DROP INDEX "IDX_f2aeafde2ae6fbad38e857631b"`);
-		await queryRunner.query(`DROP INDEX "IDX_f92dd6d03f8d994f29987f6214"`);
-		await queryRunner.query(`DROP INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f"`);
-		await queryRunner.query(`DROP INDEX "IDX_4db3b84c7be0d3464714f3e0b1"`);
-		await queryRunner.query(`DROP INDEX "IDX_8d2cbbc8114d90d19b44d626b6"`);
-		await queryRunner.query(`DROP INDEX "IDX_046feeb12e9ef5f783f409866a"`);
-		await queryRunner.query(`DROP INDEX "IDX_f68a5ab958f9f5fa17a32ac23b"`);
-		await queryRunner.query(`DROP INDEX "IDX_65633a106bce43fc7c5c30a5c7"`);
-		await queryRunner.query(`DROP INDEX "IDX_edeb73c09c3143a81bcb34d569"`);
-		await queryRunner.query(`DROP INDEX "IDX_e316f01a6d24eb31db27f88262"`);
-		await queryRunner.query(`DROP INDEX "IDX_2be7ec6cebddc14dc11e206686"`);
-		await queryRunner.query(`DROP INDEX "IDX_a5133470f4825902e170328ca5"`);
-		await queryRunner.query(`DROP INDEX "IDX_84e661abb7bd1e51b690d4b017"`);
-		await queryRunner.query(`DROP INDEX "IDX_5c73bf61da4f6e6f15bae88ed1"`);
-		await queryRunner.query(`DROP INDEX "IDX_d70c86baedc68326be11f9c0ce"`);
-		await queryRunner.query(`DROP INDEX "IDX_66e1e1ecd2f29e57778af35b59"`);
-		await queryRunner.query(`DROP INDEX "IDX_92255988735563f0fe4aba1f05"`);
-		await queryRunner.query(`DROP INDEX "IDX_c5870993e25c3d5771f91f5003"`);
-		await queryRunner.query(`DROP INDEX "IDX_f170de677ea75ad4533de2723e"`);
-		await queryRunner.query(`DROP INDEX "IDX_7c184198ecf66a8d3ecb253ab3"`);
-		await queryRunner.query(`DROP INDEX "IDX_f091abb24193d50c653c6b77fc"`);
-		await queryRunner.query(`DROP INDEX "IDX_a770a57c70e668cc61590c9161"`);
-		await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__active_users_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_count"`);
-		await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_count"`);
-		await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__drive_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__federation_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__hashtag_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_count"`);
-		await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_count"`);
-		await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__instance_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__network_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__notes_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__per_user_drive_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__per_user_following_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__per_user_notes_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__per_user_reaction_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_grouped" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__test_grouped_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_grouped" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__test_unique_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "___foo"`);
-		await queryRunner.query(`ALTER TABLE "__chart__test" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__test_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__test" DROP COLUMN "unique"`);
-		await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "span"`);
-		await queryRunner.query(`DROP TYPE "public"."__chart__users_span_enum"`);
-		await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "unique"`);
-	}
-
-	public async down(queryRunner: QueryRunner): Promise<void> {
-		await queryRunner.query(`ALTER TABLE "__chart__users" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__users_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__users" ADD "span" "__chart__users_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__test" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__test_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__test" ADD "span" "__chart__test_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "___foo" bigint NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__test_unique_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "span" "__chart__test_unique_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_grouped" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__test_grouped_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__test_grouped" ADD "span" "__chart__test_grouped_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_reaction_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD "span" "__chart__per_user_reaction_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_notes_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "span" "__chart__per_user_notes_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_following_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD "span" "__chart__per_user_following_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_drive_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD "span" "__chart__per_user_drive_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__notes_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "span" "__chart__notes_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__network" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__network_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__network" ADD "span" "__chart__network_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__instance_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "span" "__chart__instance_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_count" bigint NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_count" bigint NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__hashtag_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "span" "__chart__hashtag_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__federation_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "span" "__chart__federation_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__drive_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "span" "__chart__drive_span_enum" NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_count" bigint NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_count" bigint NOT NULL`);
-		await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
-		await queryRunner.query(`CREATE TYPE "public"."__chart__active_users_span_enum" AS ENUM('hour', 'day')`);
-		await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "span" "__chart__active_users_span_enum" NOT NULL`);
-		await queryRunner.query(`CREATE INDEX "IDX_a770a57c70e668cc61590c9161" ON "__chart__users" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_f091abb24193d50c653c6b77fc" ON "__chart__users" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_7c184198ecf66a8d3ecb253ab3" ON "__chart__users" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_f170de677ea75ad4533de2723e" ON "__chart__test" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_c5870993e25c3d5771f91f5003" ON "__chart__test" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_92255988735563f0fe4aba1f05" ON "__chart__test" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_66e1e1ecd2f29e57778af35b59" ON "__chart__test_unique" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_d70c86baedc68326be11f9c0ce" ON "__chart__test_unique" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_5c73bf61da4f6e6f15bae88ed1" ON "__chart__test_unique" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_84e661abb7bd1e51b690d4b017" ON "__chart__test_grouped" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_a5133470f4825902e170328ca5" ON "__chart__test_grouped" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_2be7ec6cebddc14dc11e206686" ON "__chart__test_grouped" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_e316f01a6d24eb31db27f88262" ON "__chart__per_user_reaction" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_edeb73c09c3143a81bcb34d569" ON "__chart__per_user_reaction" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_65633a106bce43fc7c5c30a5c7" ON "__chart__per_user_reaction" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_f68a5ab958f9f5fa17a32ac23b" ON "__chart__per_user_notes" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_046feeb12e9ef5f783f409866a" ON "__chart__per_user_notes" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_8d2cbbc8114d90d19b44d626b6" ON "__chart__per_user_notes" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_4db3b84c7be0d3464714f3e0b1" ON "__chart__per_user_following" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f" ON "__chart__per_user_following" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_f92dd6d03f8d994f29987f6214" ON "__chart__per_user_following" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_f2aeafde2ae6fbad38e857631b" ON "__chart__per_user_drive" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53" ON "__chart__per_user_drive" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_328f259961e60c4fa0bfcf55ca" ON "__chart__per_user_drive" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_924fc196c80ca24bae01dd37e4" ON "__chart__notes" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_0c9a159c5082cbeef3ca6706b5" ON "__chart__notes" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_e69096589f11e3baa98ddd64d0" ON "__chart__notes" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_9ff6944f01acb756fdc92d7563" ON "__chart__network" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_08fac0eb3b11f04c200c0b40dd" ON "__chart__network" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_f8dd01baeded2ffa833e0a610a" ON "__chart__network" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_f5448d9633cff74208d850aabe" ON "__chart__instance" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_d0a4f79af5a97b08f37b547197" ON "__chart__instance" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_c12f0af4a66cdd30c2287ce8aa" ON "__chart__instance" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_6d6f156ceefc6bc5f273a0e370" ON "__chart__hashtag" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_49975586f50ed7b800fdd88fbd" ON "__chart__hashtag" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_fcc181fb8283009c61cc4083ef" ON "__chart__hashtag" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_e9cd07672b37d8966cf3709283" ON "__chart__federation" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_2d416e6af791a82e338c79d480" ON "__chart__federation" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_e447064455928cf627590ef527" ON "__chart__federation" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_06690fc959f1c9fdaf21928222" ON "__chart__drive" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_6e1df243476e20cbf86572ecc0" ON "__chart__drive" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_3fa0d0f17ca72e3dc80999a032" ON "__chart__drive" ("span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_c26e2c1cbb6e911e0554b27416" ON "__chart__active_users" ("date", "group", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_20f57cc8f142c131340ee16742" ON "__chart__active_users" ("date", "span") `);
-		await queryRunner.query(`CREATE INDEX "IDX_15e91a03aeeac9dbccdf43fc06" ON "__chart__active_users" ("span") `);
-	}
-
-}
diff --git a/migration/1615966519402-chart-v2-2.ts b/migration/1615966519402-chart-v2-2.ts
deleted file mode 100644
index a694f9542a..0000000000
--- a/migration/1615966519402-chart-v2-2.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class chartV221615966519402 implements MigrationInterface {
-    name = 'chartV221615966519402'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
-        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
-        await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
-        await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
-        await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "___foo" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "___foo"`);
-        await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_users"`);
-        await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_users"`);
-        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_users"`);
-        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_users"`);
-    }
-
-}
diff --git a/migration/1618637372000-user-last-active-date.ts b/migration/1618637372000-user-last-active-date.ts
deleted file mode 100644
index a66c433a3a..0000000000
--- a/migration/1618637372000-user-last-active-date.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class userLastActiveDate1618637372000 implements MigrationInterface {
-    name = 'userLastActiveDate1618637372000'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user" ADD "lastActiveDate" TIMESTAMP WITH TIME ZONE DEFAULT NULL`);
-        await queryRunner.query(`CREATE INDEX "IDX_seoignmeoprigmkpodgrjmkpormg" ON "user" ("lastActiveDate") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_seoignmeoprigmkpodgrjmkpormg"`);
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "lastActiveDate"`);
-    }
-
-}
diff --git a/migration/1618639857000-user-hide-online-status.ts b/migration/1618639857000-user-hide-online-status.ts
deleted file mode 100644
index d5d77f9837..0000000000
--- a/migration/1618639857000-user-hide-online-status.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class userHideOnlineStatus1618639857000 implements MigrationInterface {
-    name = 'userHideOnlineStatus1618639857000'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-			await queryRunner.query(`ALTER TABLE "user" ADD "hideOnlineStatus" boolean NOT NULL DEFAULT false`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hideOnlineStatus"`);
-    }
-
-}
diff --git a/migration/1619942102890-password-reset.ts b/migration/1619942102890-password-reset.ts
deleted file mode 100644
index 66854cb025..0000000000
--- a/migration/1619942102890-password-reset.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class passwordReset1619942102890 implements MigrationInterface {
-    name = 'passwordReset1619942102890'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TABLE "password_reset_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_fcf4b02eae1403a2edaf87fd074" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0b575fa9a4cfe638a925949285" ON "password_reset_request" ("token") `);
-        await queryRunner.query(`CREATE INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac" ON "password_reset_request" ("userId") `);
-        await queryRunner.query(`ALTER TABLE "password_reset_request" ADD CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "password_reset_request" DROP CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8"`);
-        await queryRunner.query(`DROP INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac"`);
-        await queryRunner.query(`DROP INDEX "IDX_0b575fa9a4cfe638a925949285"`);
-        await queryRunner.query(`DROP TABLE "password_reset_request"`);
-    }
-
-}
diff --git a/migration/1620019354680-ad.ts b/migration/1620019354680-ad.ts
deleted file mode 100644
index 27fb99f181..0000000000
--- a/migration/1620019354680-ad.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class ad1620019354680 implements MigrationInterface {
-    name = 'ad1620019354680'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TABLE "ad" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "place" character varying(32) NOT NULL, "priority" character varying(32) NOT NULL, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, CONSTRAINT "PK_0193d5ef09746e88e9ea92c634d" PRIMARY KEY ("id")); COMMENT ON COLUMN "ad"."createdAt" IS 'The created date of the Ad.'; COMMENT ON COLUMN "ad"."expiresAt" IS 'The expired date of the Ad.'`);
-        await queryRunner.query(`CREATE INDEX "IDX_1129c2ef687fc272df040bafaa" ON "ad" ("createdAt") `);
-        await queryRunner.query(`CREATE INDEX "IDX_2da24ce20ad209f1d9dc032457" ON "ad" ("expiresAt") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_2da24ce20ad209f1d9dc032457"`);
-        await queryRunner.query(`DROP INDEX "IDX_1129c2ef687fc272df040bafaa"`);
-        await queryRunner.query(`DROP TABLE "ad"`);
-    }
-
-}
diff --git a/migration/1620364649428-ad2.ts b/migration/1620364649428-ad2.ts
deleted file mode 100644
index a2d7f563c2..0000000000
--- a/migration/1620364649428-ad2.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class ad21620364649428 implements MigrationInterface {
-    name = 'ad21620364649428'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "ad" ADD "ratio" integer NOT NULL DEFAULT '1'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "ratio"`);
-    }
-
-}
diff --git a/migration/1621479946000-add-note-indexes.ts b/migration/1621479946000-add-note-indexes.ts
deleted file mode 100644
index 53d49964a7..0000000000
--- a/migration/1621479946000-add-note-indexes.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class addNoteIndexes1621479946000 implements MigrationInterface {
-    name = 'addNoteIndexes1621479946000'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE INDEX "IDX_NOTE_MENTIONS" ON "note" USING gin ("mentions")`, undefined);
-				await queryRunner.query(`CREATE INDEX "IDX_NOTE_VISIBLE_USER_IDS" ON "note" USING gin ("visibleUserIds")`, undefined);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_NOTE_MENTIONS"`, undefined);
-				await queryRunner.query(`DROP INDEX "IDX_NOTE_VISIBLE_USER_IDS"`, undefined);
-    }
-
-}
diff --git a/migration/1622679304522-user-profile-description-length.ts b/migration/1622679304522-user-profile-description-length.ts
deleted file mode 100644
index 015d1e24b7..0000000000
--- a/migration/1622679304522-user-profile-description-length.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class userProfileDescriptionLength1622679304522 implements MigrationInterface {
-	name = 'userProfileDescriptionLength1622679304522';
-
-	public async up(queryRunner: QueryRunner): Promise<void> {
-		await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(2048)`, undefined);
-	}
-
-	public async down(queryRunner: QueryRunner): Promise<void> {
-		await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(1024)`, undefined);
-	}
-}
diff --git a/migration/1622681548499-log-message-length.ts b/migration/1622681548499-log-message-length.ts
deleted file mode 100644
index ef8c33982b..0000000000
--- a/migration/1622681548499-log-message-length.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class logMessageLength1622681548499 implements MigrationInterface {
-	name = 'logMessageLength1622681548499';
-	public async up(queryRunner: QueryRunner): Promise<void> {
-		await queryRunner.query(`ALTER TABLE "log" ALTER COLUMN "message" TYPE character varying(2048)`, undefined);
-	}
-
-	public async down(queryRunner: QueryRunner): Promise<void> {
-		await queryRunner.query(`ALTER TABLE "log" ALTER COLUMN "message" TYPE character varying(1024)`, undefined);
-	}
-}
diff --git a/migration/1629004542760-chart-reindex.ts b/migration/1629004542760-chart-reindex.ts
deleted file mode 100644
index c3d09f51ba..0000000000
--- a/migration/1629004542760-chart-reindex.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class chartReindex1629004542760 implements MigrationInterface {
-    name = 'chartReindex1629004542760'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DELETE FROM "__chart__active_users" a USING "__chart__active_users" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__drive" a USING "__chart__drive" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__federation" a USING "__chart__federation" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__hashtag" a USING "__chart__hashtag" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__instance" a USING "__chart__instance" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__network" a USING "__chart__network" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__notes" a USING "__chart__notes" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__per_user_drive" a USING "__chart__per_user_drive" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__per_user_following" a USING "__chart__per_user_following" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__per_user_notes" a USING "__chart__per_user_notes" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__per_user_reaction" a USING "__chart__per_user_reaction" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__test_grouped" a USING "__chart__test_grouped" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__test_unique" a USING "__chart__test_unique" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DELETE FROM "__chart__users" a USING "__chart__users" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
-        await queryRunner.query(`DROP INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc"`);
-        await queryRunner.query(`DROP INDEX "IDX_00ed5f86db1f7efafb1978bf21"`);
-        await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`);
-        await queryRunner.query(`DROP INDEX "IDX_13565815f618a1ff53886c5b28"`);
-        await queryRunner.query(`DROP INDEX "IDX_7a170f67425e62a8fabb76c872"`);
-        await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`);
-        await queryRunner.query(`DROP INDEX "IDX_36cb699c49580d4e6c2e6159f9"`);
-        await queryRunner.query(`DROP INDEX "IDX_76e87c7bfc5d925fcbba405d84"`);
-        await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`);
-        await queryRunner.query(`DROP INDEX "IDX_07747a1038c05f532a718fe1de"`);
-        await queryRunner.query(`DROP INDEX "IDX_99a7d2faaef84a6f728d714ad6"`);
-        await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`);
-        await queryRunner.query(`DROP INDEX "IDX_6b8f34a1a64b06014b6fb66824"`);
-        await queryRunner.query(`DROP INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63"`);
-        await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`);
-        await queryRunner.query(`DROP INDEX "IDX_a1efd3e0048a5f2793a47360dc"`);
-        await queryRunner.query(`DROP INDEX "IDX_7b5da130992ec9df96712d4290"`);
-        await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`);
-        await queryRunner.query(`DROP INDEX "IDX_42eb716a37d381cdf566192b2b"`);
-        await queryRunner.query(`DROP INDEX "IDX_7036f2957151588b813185c794"`);
-        await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`);
-        await queryRunner.query(`DROP INDEX "IDX_5f86db6492274e07c1a3cdf286"`);
-        await queryRunner.query(`DROP INDEX "IDX_e496ca8096d28f6b9b509264dc"`);
-        await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`);
-        await queryRunner.query(`DROP INDEX "IDX_7af07790712aa3438ff6773f3b"`);
-        await queryRunner.query(`DROP INDEX "IDX_4b3593098b6edc9c5afe36b18b"`);
-        await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`);
-        await queryRunner.query(`DROP INDEX "IDX_84234bd1abb873f07329681c83"`);
-        await queryRunner.query(`DROP INDEX "IDX_55bf20f366979f2436de99206b"`);
-        await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`);
-        await queryRunner.query(`DROP INDEX "IDX_f7bf4c62059764c2c2bb40fdab"`);
-        await queryRunner.query(`DROP INDEX "IDX_8cf3156fd7a6b15c43459c6e3b"`);
-        await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`);
-        await queryRunner.query(`DROP INDEX "IDX_0c641990ecf47d2545df4edb75"`);
-        await queryRunner.query(`DROP INDEX "IDX_234dff3c0b56a6150b95431ab9"`);
-        await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`);
-        await queryRunner.query(`DROP INDEX "IDX_437bab3c6061d90f6bb65fd2cc"`);
-        await queryRunner.query(`DROP INDEX "IDX_bbfa573a8181018851ed0b6357"`);
-        await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`);
-        await queryRunner.query(`DROP INDEX "IDX_b070a906db04b44c67c6c2144d"`);
-        await queryRunner.query(`DROP INDEX "IDX_d41cce6aee1a50bfc062038f9b"`);
-        await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`);
-        await queryRunner.query(`DROP INDEX "IDX_845254b3eaf708ae8a6cac3026"`);
-        await queryRunner.query(`DROP INDEX "IDX_ed9b95919c672a13008e9487ee"`);
-        await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_60c5c6e7e538c09aa274ecd1cf" ON "__chart__active_users" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ceab80a6729f8e2e6f5b8a1a3d" ON "__chart__drive" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_eddfed8fb40305a04c6f941050" ON "__chart__federation" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_53a3604b939e2b479eb2cfaac8" ON "__chart__hashtag" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8111b817b9818c04d7eb8475b1" ON "__chart__instance" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2082327b2699ce924fa654afc5" ON "__chart__network" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e60c358aaced5aab8900a4af31" ON "__chart__notes" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a9a806d466b314f253a1a611c4" ON "__chart__per_user_drive" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dabbb38a51ab86ee3cab291326" ON "__chart__per_user_following" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_583a157ed0cf0ed1b5ec2a833f" ON "__chart__per_user_notes" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3b7697a96f522d0478972e6d6f" ON "__chart__per_user_reaction" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_da522b4008a9f5d7743b87ad55" ON "__chart__test_grouped" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_16effb2e888f6763673b579f80" ON "__chart__test_unique" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dab383a36f3c9db4a0c9b02cf3" ON "__chart__test" ("date") WHERE "group" IS NULL`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_66feba81e1795d176d06c0b1e6" ON "__chart__users" ("date") WHERE "group" IS NULL`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_66feba81e1795d176d06c0b1e6"`);
-        await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`);
-        await queryRunner.query(`DROP INDEX "IDX_dab383a36f3c9db4a0c9b02cf3"`);
-        await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`);
-        await queryRunner.query(`DROP INDEX "IDX_16effb2e888f6763673b579f80"`);
-        await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`);
-        await queryRunner.query(`DROP INDEX "IDX_da522b4008a9f5d7743b87ad55"`);
-        await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`);
-        await queryRunner.query(`DROP INDEX "IDX_3b7697a96f522d0478972e6d6f"`);
-        await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`);
-        await queryRunner.query(`DROP INDEX "IDX_583a157ed0cf0ed1b5ec2a833f"`);
-        await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`);
-        await queryRunner.query(`DROP INDEX "IDX_dabbb38a51ab86ee3cab291326"`);
-        await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`);
-        await queryRunner.query(`DROP INDEX "IDX_a9a806d466b314f253a1a611c4"`);
-        await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`);
-        await queryRunner.query(`DROP INDEX "IDX_e60c358aaced5aab8900a4af31"`);
-        await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`);
-        await queryRunner.query(`DROP INDEX "IDX_2082327b2699ce924fa654afc5"`);
-        await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`);
-        await queryRunner.query(`DROP INDEX "IDX_8111b817b9818c04d7eb8475b1"`);
-        await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`);
-        await queryRunner.query(`DROP INDEX "IDX_53a3604b939e2b479eb2cfaac8"`);
-        await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`);
-        await queryRunner.query(`DROP INDEX "IDX_eddfed8fb40305a04c6f941050"`);
-        await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`);
-        await queryRunner.query(`DROP INDEX "IDX_ceab80a6729f8e2e6f5b8a1a3d"`);
-        await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`);
-        await queryRunner.query(`DROP INDEX "IDX_60c5c6e7e538c09aa274ecd1cf"`);
-        await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`);
-        await queryRunner.query(`DROP INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5"`);
-        await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
-        await queryRunner.query(`DROP INDEX "IDX_c8cc87bd0f2f4487d17c651fbf"`);
-        await queryRunner.query(`DROP INDEX "IDX_754499f9b2642336433769518d"`);
-        await queryRunner.query(`DROP INDEX "IDX_315c779174fe8247ab324f036e"`);
-        await queryRunner.query(`DROP INDEX "IDX_c5d46cbfda48b1c33ed852e21b"`);
-        await queryRunner.query(`CREATE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_ed9b95919c672a13008e9487ee" ON "__chart__users" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_845254b3eaf708ae8a6cac3026" ON "__chart__users" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_d41cce6aee1a50bfc062038f9b" ON "__chart__test" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_b070a906db04b44c67c6c2144d" ON "__chart__test" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_bbfa573a8181018851ed0b6357" ON "__chart__test_unique" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_437bab3c6061d90f6bb65fd2cc" ON "__chart__test_unique" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_234dff3c0b56a6150b95431ab9" ON "__chart__test_grouped" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0c641990ecf47d2545df4edb75" ON "__chart__test_grouped" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_8cf3156fd7a6b15c43459c6e3b" ON "__chart__per_user_reaction" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f7bf4c62059764c2c2bb40fdab" ON "__chart__per_user_reaction" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_55bf20f366979f2436de99206b" ON "__chart__per_user_notes" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_84234bd1abb873f07329681c83" ON "__chart__per_user_notes" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_4b3593098b6edc9c5afe36b18b" ON "__chart__per_user_following" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7af07790712aa3438ff6773f3b" ON "__chart__per_user_following" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_e496ca8096d28f6b9b509264dc" ON "__chart__per_user_drive" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_5f86db6492274e07c1a3cdf286" ON "__chart__per_user_drive" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7036f2957151588b813185c794" ON "__chart__notes" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_42eb716a37d381cdf566192b2b" ON "__chart__notes" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7b5da130992ec9df96712d4290" ON "__chart__network" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_a1efd3e0048a5f2793a47360dc" ON "__chart__network" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63" ON "__chart__instance" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_6b8f34a1a64b06014b6fb66824" ON "__chart__instance" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_99a7d2faaef84a6f728d714ad6" ON "__chart__hashtag" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_07747a1038c05f532a718fe1de" ON "__chart__hashtag" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_76e87c7bfc5d925fcbba405d84" ON "__chart__federation" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_36cb699c49580d4e6c2e6159f9" ON "__chart__federation" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_7a170f67425e62a8fabb76c872" ON "__chart__drive" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_13565815f618a1ff53886c5b28" ON "__chart__drive" ("date") `);
-        await queryRunner.query(`CREATE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_00ed5f86db1f7efafb1978bf21" ON "__chart__active_users" ("group") `);
-        await queryRunner.query(`CREATE INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc" ON "__chart__active_users" ("date") `);
-    }
-
-}
diff --git a/migration/1629024377804-deepl-integration.ts b/migration/1629024377804-deepl-integration.ts
deleted file mode 100644
index 639f947c7d..0000000000
--- a/migration/1629024377804-deepl-integration.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class deeplIntegration1629024377804 implements MigrationInterface {
-    name = 'deeplIntegration1629024377804'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "deeplAuthKey" character varying(128)`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplAuthKey"`);
-    }
-
-}
diff --git a/migration/1629288472000-fix-channel-userId.ts b/migration/1629288472000-fix-channel-userId.ts
deleted file mode 100644
index cd8f81bb01..0000000000
--- a/migration/1629288472000-fix-channel-userId.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class fixChannelUserId1629288472000 implements MigrationInterface {
-    name = 'fixChannelUserId1629288472000'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "channel" ALTER COLUMN "userId" DROP NOT NULL;`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "channel" ALTER COLUMN "userId" SET NOT NULL;`);
-    }
-
-}
diff --git a/migration/1629512953000-user-is-deleted.ts b/migration/1629512953000-user-is-deleted.ts
deleted file mode 100644
index 10b7d1d7b7..0000000000
--- a/migration/1629512953000-user-is-deleted.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class isUserDeleted1629512953000 implements MigrationInterface {
-    name = 'isUserDeleted1629512953000'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user" ADD "isDeleted" boolean NOT NULL DEFAULT false`);
-        await queryRunner.query(`COMMENT ON COLUMN "user"."isDeleted" IS 'Whether the User is deleted.'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isDeleted"`);
-    }
-
-}
diff --git a/migration/1629778475000-deepl-integration2.ts b/migration/1629778475000-deepl-integration2.ts
deleted file mode 100644
index 67e5ea02ae..0000000000
--- a/migration/1629778475000-deepl-integration2.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class deeplIntegration21629778475000 implements MigrationInterface {
-    name = 'deeplIntegration21629778475000'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "deeplIsPro" boolean NOT NULL DEFAULT false`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplIsPro"`);
-    }
-
-}
diff --git a/migration/1633068642000-email-required-for-signup.ts b/migration/1633068642000-email-required-for-signup.ts
deleted file mode 100644
index ab7be7a0d1..0000000000
--- a/migration/1633068642000-email-required-for-signup.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class emailRequiredForSignup1633068642000 implements MigrationInterface {
-    name = 'emailRequiredForSignup1633068642000'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" ADD "emailRequiredForSignup" boolean NOT NULL DEFAULT false`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "emailRequiredForSignup"`);
-    }
-
-}
diff --git a/migration/1633071909016-user-pending.ts b/migration/1633071909016-user-pending.ts
deleted file mode 100644
index 28b556888a..0000000000
--- a/migration/1633071909016-user-pending.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class userPending1633071909016 implements MigrationInterface {
-    name = 'userPending1633071909016'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TABLE "user_pending" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(128) NOT NULL, "username" character varying(128) NOT NULL, "email" character varying(128) NOT NULL, "password" character varying(128) NOT NULL, CONSTRAINT "PK_d4c84e013c98ec02d19b8fbbafa" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4e5c4c99175638ec0761714ab0" ON "user_pending" ("code") `);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`DROP INDEX "IDX_4e5c4c99175638ec0761714ab0"`);
-        await queryRunner.query(`DROP TABLE "user_pending"`);
-    }
-
-}
diff --git a/migration/1634486652000-user-public-reactions.ts b/migration/1634486652000-user-public-reactions.ts
deleted file mode 100644
index da57a13269..0000000000
--- a/migration/1634486652000-user-public-reactions.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class userPublicReactions1634486652000 implements MigrationInterface {
-    name = 'userPublicReactions1634486652000'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-			await queryRunner.query(`ALTER TABLE "user_profile" ADD "publicReactions" boolean NOT NULL DEFAULT false`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "publicReactions"`);
-    }
-
-}
diff --git a/migration/1634902659689-delete-log.ts b/migration/1634902659689-delete-log.ts
deleted file mode 100644
index 5264d585c7..0000000000
--- a/migration/1634902659689-delete-log.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class deleteLog1634902659689 implements MigrationInterface {
-    name = 'deleteLog1634902659689'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-			await queryRunner.query(`DROP TABLE "log"`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-    }
-
-}
diff --git a/migration/1635500777168-note-thread-mute.ts b/migration/1635500777168-note-thread-mute.ts
deleted file mode 100644
index aed10d18d7..0000000000
--- a/migration/1635500777168-note-thread-mute.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class noteThreadMute1635500777168 implements MigrationInterface {
-    name = 'noteThreadMute1635500777168'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TABLE "note_thread_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "threadId" character varying(256) NOT NULL, CONSTRAINT "PK_ec5936d94d1a0369646d12a3a47" PRIMARY KEY ("id"))`);
-        await queryRunner.query(`CREATE INDEX "IDX_29c11c7deb06615076f8c95b80" ON "note_thread_muting" ("userId") `);
-        await queryRunner.query(`CREATE INDEX "IDX_c426394644267453e76f036926" ON "note_thread_muting" ("threadId") `);
-        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ae7aab18a2641d3e5f25e0c4ea" ON "note_thread_muting" ("userId", "threadId") `);
-        await queryRunner.query(`ALTER TABLE "note" ADD "threadId" character varying(256)`);
-        await queryRunner.query(`CREATE INDEX "IDX_d4ebdef929896d6dc4a3c5bb48" ON "note" ("threadId") `);
-        await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD CONSTRAINT "FK_29c11c7deb06615076f8c95b80a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP CONSTRAINT "FK_29c11c7deb06615076f8c95b80a"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_d4ebdef929896d6dc4a3c5bb48"`);
-        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "threadId"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_ae7aab18a2641d3e5f25e0c4ea"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_c426394644267453e76f036926"`);
-        await queryRunner.query(`DROP INDEX "public"."IDX_29c11c7deb06615076f8c95b80"`);
-        await queryRunner.query(`DROP TABLE "note_thread_muting"`);
-    }
-
-}
diff --git a/migration/1636197624383-ff-visibility.ts b/migration/1636197624383-ff-visibility.ts
deleted file mode 100644
index 8829aeb884..0000000000
--- a/migration/1636197624383-ff-visibility.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {MigrationInterface, QueryRunner} from "typeorm";
-
-export class ffVisibility1636197624383 implements MigrationInterface {
-    name = 'ffVisibility1636197624383'
-
-    public async up(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`);
-        await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`);
-    }
-
-    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`);
-        await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`);
-    }
-
-}
diff --git a/ormconfig.js b/ormconfig.js
deleted file mode 100644
index 8a0196947e..0000000000
--- a/ormconfig.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const config = require('./built/config').default;
-const entities = require('./built/db/postgre').entities;
-
-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'
-	}
-};
diff --git a/package.json b/package.json
index 1b0e43e1c3..66d52e578b 100644
--- a/package.json
+++ b/package.json
@@ -6,248 +6,41 @@
 		"type": "git",
 		"url": "https://github.com/misskey-dev/misskey.git"
 	},
-	"main": "./index.js",
 	"private": true,
 	"scripts": {
-		"start": "node --experimental-json-modules ./index.js",
-		"start:test": "cross-env NODE_ENV=test node --experimental-json-modules ./index.js",
+		"start": "cd packages/backend && node --experimental-json-modules ./built/index.js",
+		"start:test": "cd packages/backend && cross-env NODE_ENV=test node --experimental-json-modules ./index.js",
 		"init": "npm run migrate",
-		"ormconfig": "node ./built/ormconfig.js",
-		"migrate": "ts-node ./node_modules/typeorm/cli.js migration:run",
+		"ormconfig": "node ./packages/backend/ormconfig.js",
+		"migrate": "cd packages/backend && npx typeorm migration:run",
 		"migrateandstart": "npm run migrate && npm run start",
-		"build": "npm run build-webpack && npm run build-ts && npm run build-gulp",
-		"build-webpack": "webpack",
-		"build-ts": "tsc -p src/tsconfig.json || echo done. && tsc-alias -p src/tsconfig.json",
+		"build": "npm run build-client && npm run build-backend && npm run build-gulp",
+		"build-client": "cd packages/client && npm run build",
+		"build-backend": "cd packages/backend && npm run build",
 		"build-gulp": "gulp build",
 		"watch": "concurrently \"npm:watch-*\"",
 		"watch-webpack": "webpack --watch",
-		"watch-ts": "tsc -w -p src/tsconfig.json && tsc-alias -w -p src/tsconfig.json",
+		"watch-ts": "tsc -w -p packages/tsconfig.json && tsc-alias -w -p packages/tsconfig.json",
 		"watch-gulp": "gulp watch",
 		"clean": "gulp clean",
 		"cleanall": "gulp cleanall",
-		"lint": "tslint 'src/**/*.ts'",
+		"lint": "tslint 'packages/**/*.ts'",
 		"cy:open": "cypress open",
 		"cy:run": "cypress run",
 		"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
-		"mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
+		"mocha": "cd packages/backend && cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
 		"test": "npm run mocha",
 		"format": "gulp format"
 	},
-	"resolutions": {
-		"chokidar": "^3.3.1",
-		"lodash": "^4.17.21"
-	},
 	"dependencies": {
-		"@discordapp/twemoji": "13.1.0",
-		"@elastic/elasticsearch": "7.11.0",
-		"@koa/cors": "3.1.0",
-		"@koa/multer": "3.0.0",
-		"@koa/router": "9.0.1",
-		"@sentry/browser": "5.29.2",
-		"@sentry/tracing": "5.29.2",
-		"@sinonjs/fake-timers": "7.1.2",
-		"@syuilo/aiscript": "0.11.1",
-		"@types/bcryptjs": "2.4.2",
-		"@types/bull": "3.15.5",
-		"@types/cbor": "6.0.0",
-		"@types/dateformat": "3.0.1",
-		"@types/escape-regexp": "0.0.0",
-		"@types/glob": "7.2.0",
 		"@types/gulp": "4.0.9",
 		"@types/gulp-rename": "2.0.1",
-		"@types/is-url": "1.2.30",
-		"@types/js-yaml": "4.0.4",
-		"@types/jsdom": "16.2.13",
-		"@types/jsonld": "1.5.6",
-		"@types/katex": "0.11.1",
-		"@types/koa": "2.13.4",
-		"@types/koa-bodyparser": "4.3.3",
-		"@types/koa-cors": "0.0.2",
-		"@types/koa-favicon": "2.0.21",
-		"@types/koa-logger": "3.1.2",
-		"@types/koa-mount": "4.0.1",
-		"@types/koa-send": "4.1.3",
-		"@types/koa-views": "7.0.0",
-		"@types/koa__cors": "3.0.3",
-		"@types/koa__multer": "2.0.4",
-		"@types/koa__router": "8.0.8",
-		"@types/matter-js": "0.17.6",
-		"@types/mocha": "8.2.3",
-		"@types/node": "16.11.7",
-		"@types/node-fetch": "2.5.12",
-		"@types/nodemailer": "6.4.4",
-		"@types/nprogress": "0.2.0",
-		"@types/oauth": "0.9.1",
-		"@types/parse5": "6.0.2",
-		"@types/parsimmon": "1.10.6",
-		"@types/portscanner": "2.1.1",
-		"@types/pug": "2.0.5",
-		"@types/punycode": "2.1.0",
-		"@types/qrcode": "1.4.1",
-		"@types/random-seed": "0.3.3",
-		"@types/ratelimiter": "3.4.2",
-		"@types/redis": "2.8.32",
-		"@types/rename": "1.0.4",
-		"@types/request-stats": "3.0.0",
-		"@types/rimraf": "3.0.2",
-		"@types/seedrandom": "2.4.28",
-		"@types/sharp": "0.29.3",
-		"@types/sinonjs__fake-timers": "6.0.4",
-		"@types/speakeasy": "2.0.6",
-		"@types/throttle-debounce": "2.1.0",
-		"@types/tinycolor2": "1.4.3",
-		"@types/tmp": "0.2.2",
-		"@types/uuid": "8.3.1",
-		"@types/web-push": "3.3.2",
-		"@types/webpack": "5.28.0",
-		"@types/webpack-stream": "3.2.12",
-		"@types/websocket": "1.0.4",
-		"@types/ws": "8.2.0",
-		"@typescript-eslint/parser": "5.1.0",
-		"@vue/compiler-sfc": "3.2.21",
-		"abort-controller": "3.0.0",
-		"autobind-decorator": "2.4.0",
-		"autosize": "4.0.4",
-		"autwh": "0.1.0",
-		"aws-sdk": "2.1013.0",
-		"bcryptjs": "2.4.3",
-		"blurhash": "1.1.4",
-		"broadcast-channel": "4.5.0",
-		"bull": "4.1.0",
-		"cacheable-lookup": "6.0.4",
-		"cafy": "15.2.1",
-		"cbor": "8.1.0",
-		"chalk": "4.1.2",
-		"chart.js": "3.6.0",
-		"chartjs-adapter-date-fns": "2.0.0",
-		"chartjs-plugin-zoom": "1.1.1",
-		"cli-highlight": "2.1.11",
-		"compare-versions": "3.6.0",
-		"concurrently": "6.3.0",
-		"content-disposition": "0.5.3",
-		"crc-32": "1.2.0",
-		"css-loader": "6.5.1",
-		"cssnano": "5.0.10",
-		"date-fns": "2.25.0",
-		"dateformat": "4.5.1",
-		"deep-email-validator": "0.1.18",
-		"escape-regexp": "0.0.1",
-		"eslint": "8.2.0",
-		"eslint-plugin-vue": "8.0.3",
-		"eventemitter3": "4.0.7",
-		"feed": "4.2.2",
-		"file-type": "16.5.3",
-		"fluent-ffmpeg": "2.1.2",
-		"glob": "7.2.0",
-		"got": "11.8.2",
 		"gulp": "4.0.2",
 		"gulp-cssnano": "2.1.3",
 		"gulp-rename": "2.0.0",
 		"gulp-replace": "1.1.3",
 		"gulp-terser": "2.1.0",
-		"gulp-tslint": "8.1.4",
-		"hpagent": "0.1.2",
-		"http-signature": "1.3.5",
-		"idb-keyval": "5.1.3",
-		"insert-text-at-cursor": "0.3.0",
-		"ip-cidr": "3.0.4",
-		"is-svg": "4.3.1",
-		"js-yaml": "4.1.0",
-		"jsdom": "16.7.0",
-		"json5": "2.2.0",
-		"json5-loader": "4.0.1",
-		"jsonld": "5.2.0",
-		"jsrsasign": "8.0.20",
-		"katex": "0.13.18",
-		"koa": "2.13.4",
-		"koa-bodyparser": "4.3.0",
-		"koa-favicon": "2.1.0",
-		"koa-json-body": "5.3.0",
-		"koa-logger": "3.2.1",
-		"koa-mount": "4.0.0",
-		"koa-send": "5.0.1",
-		"koa-slow": "2.1.0",
-		"koa-views": "7.0.2",
-		"langmap": "0.0.16",
-		"matter-js": "0.17.1",
-		"mfm-js": "0.20.0",
-		"misskey-js": "0.0.8",
-		"mocha": "8.4.0",
-		"ms": "2.1.3",
-		"multer": "1.4.3",
-		"nested-property": "4.0.0",
-		"node-fetch": "2.6.1",
-		"nodemailer": "6.7.0",
-		"os-utils": "0.0.14",
-		"parse5": "6.0.1",
-		"pg": "8.7.1",
-		"photoswipe": "git://github.com/dimsemenov/photoswipe#v5-beta",
-		"portscanner": "2.2.0",
-		"postcss": "8.3.11",
-		"postcss-loader": "6.2.0",
-		"prismjs": "1.25.0",
-		"private-ip": "2.3.3",
-		"probe-image-size": "7.2.1",
-		"promise-limit": "2.7.0",
-		"pug": "3.0.2",
-		"punycode": "2.1.1",
-		"pureimage": "0.3.5",
-		"qrcode": "1.4.4",
-		"random-seed": "0.3.0",
-		"ratelimiter": "3.4.1",
-		"re2": "1.16.0",
-		"redis": "3.1.2",
-		"redis-lock": "0.1.4",
-		"reflect-metadata": "0.1.13",
-		"rename": "1.0.4",
-		"request-stats": "3.0.0",
-		"require-all": "3.0.0",
-		"rimraf": "3.0.2",
-		"rndstr": "1.0.0",
-		"s-age": "1.1.2",
-		"sass": "1.43.4",
-		"sass-loader": "12.3.0",
-		"seedrandom": "3.0.5",
-		"sharp": "0.29.2",
-		"speakeasy": "2.0.0",
-		"strict-event-emitter-types": "2.0.0",
-		"stringz": "2.1.0",
-		"style-loader": "3.3.1",
-		"summaly": "2.4.1",
-		"syslog-pro": "1.0.0",
-		"systeminformation": "5.9.9",
-		"syuilo-password-strength": "0.0.1",
-		"textarea-caret": "3.1.0",
-		"three": "0.117.1",
-		"throttle-debounce": "3.0.1",
-		"tinycolor2": "1.4.2",
-		"tmp": "0.2.1",
-		"ts-loader": "9.2.6",
-		"ts-node": "10.4.0",
-		"tsc-alias": "1.3.10",
-		"tsconfig-paths": "3.11.0",
-		"tslint": "6.1.3",
-		"tslint-sonarts": "1.9.0",
-		"twemoji-parser": "13.1.0",
-		"typeorm": "0.2.39",
-		"typescript": "4.4.4",
-		"ulid": "2.3.0",
-		"uuid": "8.3.2",
-		"v-debounce": "0.1.2",
-		"vanilla-tilt": "1.7.2",
-		"vue": "3.2.21",
-		"vue-loader": "16.7.0",
-		"vue-prism-editor": "2.0.0-alpha.2",
-		"vue-router": "4.0.5",
-		"vue-style-loader": "4.1.3",
-		"vue-svg-loader": "0.17.0-beta.2",
-		"vuedraggable": "4.0.1",
-		"web-push": "3.4.5",
-		"webpack": "5.63.0",
-		"webpack-cli": "4.9.1",
-		"websocket": "1.0.34",
-		"ws": "8.2.3",
-		"xev": "2.0.1"
+		"gulp-tslint": "8.1.4"
 	},
 	"devDependencies": {
 		"@redocly/openapi-core": "1.0.0-beta.54",
diff --git a/packages/backend/.eslintrc b/packages/backend/.eslintrc
new file mode 100644
index 0000000000..d54e20f6b6
--- /dev/null
+++ b/packages/backend/.eslintrc
@@ -0,0 +1,6 @@
+{
+	"env": {
+		"node": true,
+		"commonjs": true
+	}
+}
diff --git a/packages/backend/.mocharc.json b/packages/backend/.mocharc.json
new file mode 100644
index 0000000000..278a5b310a
--- /dev/null
+++ b/packages/backend/.mocharc.json
@@ -0,0 +1,7 @@
+{
+	"extension": ["ts","js","cjs","mjs"],
+	"require": ["ts-node/register", "tsconfig-paths/register"],
+	"slow": 1000,
+	"timeout": 35000,
+	"exit": true
+}
diff --git a/packages/backend/.npmrc b/packages/backend/.npmrc
new file mode 100644
index 0000000000..6b5f38e890
--- /dev/null
+++ b/packages/backend/.npmrc
@@ -0,0 +1,2 @@
+save-exact = true
+package-lock = false
diff --git a/packages/backend/@types/hcaptcha.d.ts b/packages/backend/@types/hcaptcha.d.ts
new file mode 100644
index 0000000000..afed587560
--- /dev/null
+++ b/packages/backend/@types/hcaptcha.d.ts
@@ -0,0 +1,11 @@
+declare module 'hcaptcha' {
+	interface IVerifyResponse {
+		success: boolean;
+		challenge_ts: string;
+		hostname: string;
+		credit?: boolean;
+		'error-codes'?: unknown[];
+	}
+
+	export function verify(secret: string, token: string): Promise<IVerifyResponse>;
+}
diff --git a/packages/backend/@types/http-signature.d.ts b/packages/backend/@types/http-signature.d.ts
new file mode 100644
index 0000000000..8d484312dc
--- /dev/null
+++ b/packages/backend/@types/http-signature.d.ts
@@ -0,0 +1,77 @@
+declare module 'http-signature' {
+	import { IncomingMessage, ClientRequest } from 'http';
+
+	interface ISignature {
+		keyId: string;
+		algorithm: string;
+		headers: string[];
+		signature: string;
+	}
+
+	interface IOptions {
+		headers?: string[];
+		algorithm?: string;
+		strict?: boolean;
+		authorizationHeaderName?: string;
+	}
+
+	interface IParseRequestOptions extends IOptions {
+		clockSkew?: number;
+	}
+
+	interface IParsedSignature {
+		scheme: string;
+		params: ISignature;
+		signingString: string;
+		algorithm: string;
+		keyId: string;
+	}
+
+	type RequestSignerConstructorOptions =
+		IRequestSignerConstructorOptionsFromProperties |
+		IRequestSignerConstructorOptionsFromFunction;
+
+	interface IRequestSignerConstructorOptionsFromProperties {
+		keyId: string;
+		key: string | Buffer;
+		algorithm?: string;
+	}
+
+	interface IRequestSignerConstructorOptionsFromFunction {
+		sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void;
+	}
+
+	class RequestSigner {
+		constructor(options: RequestSignerConstructorOptions);
+
+		public writeHeader(header: string, value: string): string;
+
+		public writeDateHeader(): string;
+
+		public writeTarget(method: string, path: string): void;
+
+		public sign(cb: (err: any, authz: string) => void): void;
+	}
+
+	interface ISignRequestOptions extends IOptions {
+		keyId: string;
+		key: string;
+		httpVersion?: string;
+	}
+
+	export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
+	export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
+
+	export function sign(request: ClientRequest, options: ISignRequestOptions): boolean;
+	export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean;
+	export function createSigner(): RequestSigner;
+	export function isSigner(obj: any): obj is RequestSigner;
+
+	export function sshKeyToPEM(key: string): string;
+	export function sshKeyFingerprint(key: string): string;
+	export function pemToRsaSSHKey(pem: string, comment: string): string;
+
+	export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
+	export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
+	export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean;
+}
diff --git a/packages/backend/@types/jsrsasign.d.ts b/packages/backend/@types/jsrsasign.d.ts
new file mode 100644
index 0000000000..bc9d746f7e
--- /dev/null
+++ b/packages/backend/@types/jsrsasign.d.ts
@@ -0,0 +1,800 @@
+// Attention: Partial Type Definition
+
+declare module 'jsrsasign' {
+	//// HELPER TYPES
+
+	/**
+	 * Attention: The value might be changed by the function.
+	 */
+	type Mutable<T> = T;
+
+	/**
+	 * Deprecated: The function might be deleted in future release.
+	 */
+	type Deprecated<T> = T;
+
+	//// COMMON TYPES
+
+	/**
+	 * byte number
+	 */
+	type ByteNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255;
+
+	/**
+	 * hexadecimal string /[0-9A-F]/
+	 */
+	type HexString = string;
+
+	/**
+	 * binary string /[01]/
+	 */
+	type BinString = string;
+
+	/**
+	 * base64 string /[A-Za-z0-9+/]=+/
+	 */
+	type Base64String = string;
+
+	/**
+	 * base64 URL encoded string /[A-Za-z0-9_-]/
+	 */
+	type Base64URLString = string;
+
+	/**
+	 * time value (ex. "151231235959Z")
+	 */
+	type TimeValue = string;
+
+	/**
+	 * OID string (ex. '1.2.3.4.567')
+	 */
+	type OID = string;
+
+	/**
+	 * OID name
+	 */
+	type OIDName = string;
+
+	/**
+	 * PEM formatted string
+	 */
+	type PEM = string;
+
+	//// ASN1 TYPES
+
+	class ASN1Object {
+		public isModified: boolean;
+
+		public hTLV: ASN1TLV;
+
+		public hT: ASN1T;
+
+		public hL: ASN1L;
+
+		public hV: ASN1V;
+
+		public getLengthHexFromValue(): HexString;
+
+		public getEncodedHex(): ASN1TLV;
+
+		public getValueHex(): ASN1V;
+
+		public getFreshValueHex(): ASN1V;
+	}
+
+	class DERAbstractStructured extends ASN1Object {
+		constructor(params?: Partial<Record<'array', ASN1Object[]>>);
+
+		public setByASN1ObjectArray(asn1ObjectArray: ASN1Object[]): void;
+
+		public appendASN1Object(asn1Object: ASN1Object): void;
+	}
+
+	class DERSequence extends DERAbstractStructured {
+		constructor(params?: Partial<Record<'array', ASN1Object[]>>);
+
+		public getFreshValueHex(): ASN1V;
+	}
+
+	//// ASN1HEX TYPES
+
+	/**
+	 * ASN.1 DER encoded data (hexadecimal string)
+	 */
+	type ASN1S = HexString;
+
+	/**
+	 * index of something
+	 */
+	type Idx<T extends { [idx: string]: unknown } | { [idx: number]: unknown }> = ASN1S extends { [idx: string]: unknown } ? string : ASN1S extends { [idx: number]: unknown } ? number : never;
+
+	/**
+	 * byte length of something
+	 */
+	type ByteLength<T extends { length: unknown }> = T['length'];
+
+	/**
+	 * ASN.1 L(length) (hexadecimal string)
+	 */
+	type ASN1L = HexString;
+
+	/**
+	 * ASN.1 T(tag) (hexadecimal string)
+	 */
+	type ASN1T = HexString;
+
+	/**
+	 * ASN.1 V(value) (hexadecimal string)
+	 */
+	type ASN1V = HexString;
+
+	/**
+	 * ASN.1 TLV (hexadecimal string)
+	 */
+	type ASN1TLV = HexString;
+
+	/**
+	 * ASN.1 object string
+	 */
+	type ASN1ObjectString = string;
+
+	/**
+	 * nth
+	 */
+	type Nth = number;
+
+	/**
+	 * ASN.1 DER encoded OID value (hexadecimal string)
+	 */
+	type ASN1OIDV = HexString;
+
+	class ASN1HEX {
+		public static getLblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1L>;
+
+		public static getL(s: ASN1S, idx: Idx<ASN1S>): ASN1L;
+
+		public static getVblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1V>;
+
+		public static getVidx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1V>;
+
+		public static getV(s: ASN1S, idx: Idx<ASN1S>): ASN1V;
+
+		public static getTLV(s: ASN1S, idx: Idx<ASN1S>): ASN1TLV;
+
+		public static getNextSiblingIdx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1ObjectString>;
+
+		public static getChildIdx(h: ASN1S, pos: Idx<ASN1S>): Idx<ASN1ObjectString>[];
+
+		public static getNthChildIdx(h: ASN1S, idx: Idx<ASN1S>, nth: Nth): Idx<ASN1ObjectString>;
+
+		public static getIdxbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): Idx<Mutable<Nth[]>>;
+
+		public static getTLVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): ASN1TLV;
+
+		// tslint:disable-next-line:bool-param-default
+		public static getVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string, removeUnusedbits?: boolean): ASN1V;
+
+		public static hextooidstr(hex: ASN1OIDV): OID;
+
+		public static dump(hexOrObj: ASN1S | ASN1Object, flags?: Record<string, unknown>, idx?: Idx<ASN1S>, indent?: string): string;
+
+		public static isASN1HEX(hex: string): hex is HexString;
+
+		public static oidname(oidDotOrHex: OID | ASN1OIDV): OIDName;
+	}
+
+	//// BIG INTEGER TYPES (PARTIAL)
+
+	class BigInteger {
+		constructor(a: null);
+
+		constructor(a: number, b: SecureRandom);
+
+		constructor(a: number, b: number, c: SecureRandom);
+
+		constructor(a: unknown);
+
+		constructor(a: string, b: number);
+
+		public am(i: number, x: number, w: number, j: number, c: number, n: number): number;
+
+		public DB: number;
+
+		public DM: number;
+
+		public DV: number;
+
+		public FV: number;
+
+		public F1: number;
+
+		public F2: number;
+
+		protected copyTo(r: Mutable<BigInteger>): void;
+
+		protected fromInt(x: number): void;
+
+		protected fromString(s: string, b: number): void;
+
+		protected clamp(): void;
+
+		public toString(b: number): string;
+
+		public negate(): BigInteger;
+
+		public abs(): BigInteger;
+
+		public compareTo(a: BigInteger): number;
+
+		public bitLength(): number;
+
+		protected dlShiftTo(n: number, r: Mutable<BigInteger>): void;
+
+		protected drShiftTo(n: number, r: Mutable<BigInteger>): void;
+
+		protected lShiftTo(n: number, r: Mutable<BigInteger>): void;
+
+		protected rShiftTo(n: number, r: Mutable<BigInteger>): void;
+
+		protected subTo(a: BigInteger, r: Mutable<BigInteger>): void;
+
+		protected multiplyTo(a: BigInteger, r: Mutable<BigInteger>): void;
+
+		protected squareTo(r: Mutable<BigInteger>): void;
+
+		protected divRemTo(m: BigInteger, q: Mutable<BigInteger>, r: Mutable<BigInteger>): void;
+
+		public mod(a: BigInteger): BigInteger;
+
+		protected invDigit(): number;
+
+		protected isEven(): boolean;
+
+		protected exp(e: number, z: Classic | Montgomery): BigInteger;
+
+		public modPowInt(e: number, m: BigInteger): BigInteger;
+
+		public static ZERO: BigInteger;
+
+		public static ONE: BigInteger;
+	}
+
+	class Classic {
+		constructor(m: BigInteger);
+
+		public convert(x: BigInteger): BigInteger;
+
+		public revert(x: BigInteger): BigInteger;
+
+		public reduce(x: Mutable<BigInteger>): void;
+
+		public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
+
+		public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
+	}
+
+	class Montgomery {
+		constructor(m: BigInteger);
+
+		public convert(x: BigInteger): BigInteger;
+
+		public revert(x: BigInteger): BigInteger;
+
+		public reduce(x: Mutable<BigInteger>): void;
+
+		public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
+
+		public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
+	}
+
+	//// KEYUTIL TYPES
+
+	type DecryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
+
+	type Decrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
+
+	type DecryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
+
+	type EncryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
+
+	type Encrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
+
+	type EncryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
+
+	type AlgList = {
+		'AES-256-CBC':  { 'proc': DecryptAES;  'eproc': EncryptAES;  keylen: 32; ivlen: 16; };
+		'AES-192-CBC':  { 'proc': DecryptAES;  'eproc': EncryptAES;  keylen: 24; ivlen: 16; };
+		'AES-128-CBC':  { 'proc': DecryptAES;  'eproc': EncryptAES;  keylen: 16; ivlen: 16; };
+		'DES-EDE3-CBC': { 'proc': Decrypt3DES; 'eproc': Encrypt3DES; keylen: 24; ivlen: 8;  };
+		'DES-CBC':      { 'proc': DecryptDES;  'eproc': EncryptDES;  keylen: 8;  ivlen: 8;  };
+	};
+
+	type AlgName = keyof AlgList;
+
+	type PEMHeadAlgName = 'RSA' | 'EC' | 'DSA';
+
+	type GetKeyRSAParam = RSAKey | {
+		n: BigInteger;
+		e: number;
+	} | Record<'n' | 'e', HexString> | Record<'n' | 'e', HexString> & Record<'d' | 'p' | 'q' | 'dp' | 'dq' | 'co', HexString | null> | {
+		n: BigInteger;
+		e: number;
+		d: BigInteger;
+	} | {
+		kty: 'RSA';
+	} & Record<'n' | 'e', Base64URLString> | {
+		kty: 'RSA';
+	} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
+		kty: 'RSA';
+	} & Record<'n' | 'e' | 'd', Base64URLString>;
+
+	type GetKeyECDSAParam = KJUR.crypto.ECDSA | {
+		curve: KJUR.crypto.CurveName;
+		xy: HexString;
+	} | {
+		curve: KJUR.crypto.CurveName;
+		d: HexString;
+	} | {
+		kty: 'EC';
+		crv: KJUR.crypto.CurveName;
+		x: Base64URLString;
+		y: Base64URLString;
+	} | {
+		kty: 'EC';
+		crv: KJUR.crypto.CurveName;
+		x: Base64URLString;
+		y: Base64URLString;
+		d: Base64URLString;
+	};
+
+	type GetKeyDSAParam = KJUR.crypto.DSA | Record<'p' | 'q' | 'g', BigInteger> & Record<'y', BigInteger | null> | Record<'p' | 'q' | 'g' | 'x', BigInteger> & Record<'y', BigInteger | null>;
+
+	type GetKeyParam = GetKeyRSAParam | GetKeyECDSAParam | GetKeyDSAParam | string;
+
+	class KEYUTIL {
+		public version: '1.0.0';
+
+		public parsePKCS5PEM(sPKCS5PEM: PEM): Partial<Record<'type' | 's', string>> & (Record<'cipher' | 'ivsalt', string> | Record<'cipher' | 'ivsalt', undefined>);
+
+		public getKeyAndUnusedIvByPasscodeAndIvsalt(algName: AlgName, passcode: string, ivsaltHex: HexString): Record<'keyhex' | 'ivhex', HexString>;
+
+		public decryptKeyB64(privateKeyB64: Base64String, sharedKeyAlgName: AlgName, sharedKeyHex: HexString, ivsaltHex: HexString): Base64String;
+
+		public getDecryptedKeyHex(sEncryptedPEM: PEM, passcode: string): HexString;
+
+		public getEncryptedPKCS5PEMFromPrvKeyHex(pemHeadAlg: PEMHeadAlgName, hPrvKey: string, passcode: string, sharedKeyAlgName?: AlgName | null, ivsaltHex?: HexString | null): PEM;
+
+		public parseHexOfEncryptedPKCS8(sHEX: HexString): {
+			ciphertext: ASN1V;
+			encryptionSchemeAlg: 'TripleDES';
+			encryptionSchemeIV: ASN1V;
+			pbkdf2Salt: ASN1V;
+			pbkdf2Iter: number;
+		};
+
+		public getPBKDF2KeyHexFromParam(info: ReturnType<this['parseHexOfEncryptedPKCS8']>, passcode: string): HexString;
+
+		private _getPlainPKCS8HexFromEncryptedPKCS8PEM(pkcs8PEM: PEM, passcode: string): HexString;
+
+		public getKeyFromEncryptedPKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
+
+		public parsePlainPrivatePKCS8Hex(pkcs8PrvHex: HexString): {
+			algparam: ASN1V | null;
+			algoid: ASN1V;
+			keyidx: Idx<ASN1V>;
+		};
+
+		public getKeyFromPlainPrivatePKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
+
+		public getKeyFromPlainPrivatePKCS8Hex(prvKeyHex: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
+
+		private _getKeyFromPublicPKCS8Hex(h: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
+
+		public parsePublicRawRSAKeyHex(pubRawRSAHex: HexString): Record<'n' | 'e', ASN1V>;
+
+		public parsePublicPKCS8Hex(pkcs8PubHex: HexString): {
+			algparam: ASN1V | Record<'p' | 'q' | 'g', ASN1V> | null;
+			algoid: ASN1V;
+			key: ASN1V;
+		};
+
+		public static getKey(param: GetKeyRSAParam): RSAKey;
+
+		public static getKey(param: GetKeyECDSAParam): KJUR.crypto.ECDSA;
+
+		public static getKey(param: GetKeyDSAParam): KJUR.crypto.DSA;
+
+		public static getKey(param: string, passcode?: string, hextype?: string): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
+
+		public static generateKeypair(alg: 'RSA', keylen: number): Record<'prvKeyObj' | 'pubKeyObj', RSAKey>;
+
+		public static generateKeypair(alg: 'EC', curve: KJUR.crypto.CurveName): Record<'prvKeyObj' | 'pubKeyObj', KJUR.crypto.ECDSA>;
+
+		public static getPEM(keyObjOrHex: RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA, formatType?: 'PKCS1PRV' | 'PKCS5PRV' | 'PKCS8PRV', passwd?: string, encAlg?: 'DES-CBC' | 'DES-EDE3-CBC' | 'AES-128-CBC' | 'AES-192-CBC' | 'AES-256-CBC', hexType?: string, ivsaltHex?: HexString): object; // To Do
+
+		public static getKeyFromCSRPEM(csrPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
+
+		public static getKeyFromCSRHex(csrHex: HexString): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
+
+		public static parseCSRHex(csrHex: HexString): Record<'p8pubkeyhex', ASN1TLV>;
+
+		public static getJWKFromKey(keyObj: RSAKey): {
+			kty: 'RSA';
+		} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
+			kty: 'RSA';
+		} & Record<'n' | 'e', Base64URLString>;
+
+		public static getJWKFromKey(keyObj: KJUR.crypto.ECDSA): {
+			kty: 'EC';
+			crv: KJUR.crypto.CurveName;
+			x: Base64URLString;
+			y: Base64URLString;
+			d: Base64URLString;
+		} | {
+			kty: 'EC';
+			crv: KJUR.crypto.CurveName;
+			x: Base64URLString;
+			y: Base64URLString;
+		};
+	}
+
+	//// KJUR NAMESPACE (PARTIAL)
+
+	namespace KJUR {
+		namespace crypto {
+			type CurveName = 'secp128r1' | 'secp160k1' | 'secp160r1' | 'secp192k1' | 'secp192r1' | 'secp224r1' | 'secp256k1' | 'secp256r1' | 'secp384r1' | 'secp521r1';
+
+			class DSA {
+				public p: BigInteger | null;
+
+				public q: BigInteger | null;
+
+				public g: BigInteger | null;
+
+				public y: BigInteger | null;
+
+				public x: BigInteger | null;
+
+				public type: 'DSA';
+
+				public isPrivate: boolean;
+
+				public isPublic: boolean;
+
+				public setPrivate(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger | null, x: BigInteger): void;
+
+				public setPrivateHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString | null, hX: HexString): void;
+
+				public setPublic(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger): void;
+
+				public setPublicHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString): void;
+
+				public signWithMessageHash(sHashHex: HexString): HexString;
+
+				public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
+
+				public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
+
+				public readPKCS5PrvKeyHex(h: HexString): void;
+
+				public readPKCS8PrvKeyHex(h: HexString): void;
+
+				public readPKCS8PubKeyHex(h: HexString): void;
+
+				public readCertPubKeyHex(h: HexString, nthPKI: number): void;
+			}
+
+			class ECDSA {
+				constructor(params?: {
+					curve?: CurveName;
+					prv?: HexString;
+					pub?: HexString;
+				});
+
+				public p: BigInteger | null;
+
+				public q: BigInteger | null;
+
+				public g: BigInteger | null;
+
+				public y: BigInteger | null;
+
+				public x: BigInteger | null;
+
+				public type: 'EC';
+
+				public isPrivate: boolean;
+
+				public isPublic: boolean;
+
+				public getBigRandom(limit: BigInteger): BigInteger;
+
+				public setNamedCurve(curveName: CurveName): void;
+
+				public setPrivateKeyHex(prvKeyHex: HexString): void;
+
+				public setPublicKeyHex(pubKeyHex: HexString): void;
+
+				public getPublicKeyXYHex(): Record<'x' | 'y', HexString>;
+
+				public getShortNISTPCurveName(): 'P-256' | 'P-384' | null;
+
+				public generateKeyPairHex(): Record<'ecprvhex' | 'ecpubhex', HexString>;
+
+				public signWithMessageHash(hashHex: HexString): HexString;
+
+				public signHex(hashHex: HexString, privHex: HexString): HexString;
+
+				public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
+
+				public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
+
+				public readPKCS5PrvKeyHex(h: HexString): void;
+
+				public readPKCS8PrvKeyHex(h: HexString): void;
+
+				public readPKCS8PubKeyHex(h: HexString): void;
+
+				public readCertPubKeyHex(h: HexString, nthPKI: number): void;
+
+				public static parseSigHex(sigHex: HexString): Record<'r' | 's', BigInteger>;
+
+				public static parseSigHexInHexRS(sigHex: HexString): Record<'r' | 's', ASN1V>;
+
+				public static asn1SigToConcatSig(asn1Sig: HexString): HexString;
+
+				public static concatSigToASN1Sig(concatSig: HexString): ASN1TLV;
+
+				public static hexRSSigToASN1Sig(hR: HexString, hS: HexString): ASN1TLV;
+
+				public static biRSSigToASN1Sig(biR: BigInteger, biS: BigInteger): ASN1TLV;
+
+				public static getName(s: CurveName | HexString): 'secp256r1' | 'secp256k1' | 'secp384r1' | null;
+			}
+
+			class Signature {
+				constructor(params?: ({
+					alg: string;
+					prov?: string;
+				} | {}) & ({
+					psssaltlen: number;
+				} | {}) & ({
+					prvkeypem: PEM;
+					prvkeypas?: never;
+				} | {}));
+
+				private _setAlgNames(): void;
+
+				private _zeroPaddingOfSignature(hex: HexString, bitLength: number): HexString;
+
+				public setAlgAndProvider(alg: string, prov: string): void;
+
+				public init(key: GetKeyParam, pass?: string): void;
+
+				public updateString(str: string): void;
+
+				public updateHex(hex: HexString): void;
+
+				public sign(): HexString;
+
+				public signString(str: string): HexString;
+
+				public signHex(hex: HexString): HexString;
+
+				public verify(hSigVal: string): boolean | 0;
+			}
+		}
+	}
+
+	//// RSAKEY TYPES
+
+	class RSAKey {
+		public n: BigInteger | null;
+
+		public e: number;
+
+		public d: BigInteger | null;
+
+		public p: BigInteger | null;
+
+		public q: BigInteger | null;
+
+		public dmp1: BigInteger | null;
+
+		public dmq1: BigInteger | null;
+
+		public coeff: BigInteger | null;
+
+		public type: 'RSA';
+
+		public isPrivate?: boolean;
+
+		public isPublic?: boolean;
+
+		//// RSA PUBLIC
+
+		protected doPublic(x: BigInteger): BigInteger;
+
+		public setPublic(N: BigInteger, E: number): void;
+
+		public setPublic(N: HexString, E: HexString): void;
+
+		public encrypt(text: string): HexString | null;
+
+		public encryptOAEP(text: string, hash?: string | ((s: string) => string), hashLen?: number): HexString | null;
+
+		//// RSA PRIVATE
+
+		protected doPrivate(x: BigInteger): BigInteger;
+
+		public setPrivate(N: BigInteger, E: number, D: BigInteger): void;
+
+		public setPrivate(N: HexString, E: HexString, D: HexString): void;
+
+		public setPrivateEx(N: HexString, E: HexString, D?: HexString | null, P?: HexString | null, Q?: HexString | null, DP?: HexString | null, DQ?: HexString | null, C?: HexString | null): void;
+
+		public generate(B: number, E: HexString): void;
+
+		public decrypt(ctext: HexString): string;
+
+		public decryptOAEP(ctext: HexString, hash?: string | ((s: string) => string), hashLen?: number): string | null;
+
+		//// RSA PEM
+
+		public getPosArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
+
+		public getHexValueArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
+
+		public readPrivateKeyFromPEMString(keyPEM: PEM): void;
+
+		public readPKCS5PrvKeyHex(h: HexString): void;
+
+		public readPKCS8PrvKeyHex(h: HexString): void;
+
+		public readPKCS5PubKeyHex(h: HexString): void;
+
+		public readPKCS8PubKeyHex(h: HexString): void;
+
+		public readCertPubKeyHex(h: HexString, nthPKI: Nth): void;
+
+		//// RSA SIGN
+
+		public sign(s: string, hashAlg: string): HexString;
+
+		public signWithMessageHash(sHashHex: HexString, hashAlg: string): HexString;
+
+		public signPSS(s: string, hashAlg: string, sLen: number): HexString;
+
+		public signWithMessageHashPSS(hHash: HexString, hashAlg: string, sLen: number): HexString;
+
+		public verify(sMsg: string, hSig: HexString): boolean | 0;
+
+		public verifyWithMessageHash(sHashHex: HexString, hSig: HexString): boolean | 0;
+
+		public verifyPSS(sMsg: string, hSig: HexString, hashAlg: string, sLen: number): boolean;
+
+		public verifyWithMessageHashPSS(hHash: HexString, hSig: HexString, hashAlg: string, sLen: number): boolean;
+
+		public static SALT_LEN_HLEN: -1;
+
+		public static SALT_LEN_MAX: -2;
+
+		public static SALT_LEN_RECOVER: -2;
+	}
+
+	/// RNG TYPES
+	class SecureRandom {
+		public nextBytes(ba: Mutable<ByteNumber[]>): void;
+	}
+
+	//// X509 TYPES
+
+	type ExtInfo = {
+		critical: boolean;
+		oid: OID;
+		vidx: Idx<ASN1V>;
+	};
+
+	type ExtAIAInfo = Record<'ocsp' | 'caissuer', string>;
+
+	type ExtCertificatePolicy = {
+		id: OIDName;
+	} & Partial<{
+		cps: string;
+	} | {
+		unotice: string;
+	}>;
+
+	class X509 {
+		public hex: HexString | null;
+
+		public version: number;
+
+		public foffset: number;
+
+		public aExtInfo: null;
+
+		public getVersion(): number;
+
+		public getSerialNumberHex(): ASN1V;
+
+		public getSignatureAlgorithmField(): OIDName;
+
+		public getIssuerHex(): ASN1TLV;
+
+		public getIssuerString(): HexString;
+
+		public getSubjectHex(): ASN1TLV;
+
+		public getSubjectString(): HexString;
+
+		public getNotBefore(): TimeValue;
+
+		public getNotAfter(): TimeValue;
+
+		public getPublicKeyHex(): ASN1TLV;
+
+		public getPublicKeyIdx(): Idx<Mutable<Nth[]>>;
+
+		public getPublicKeyContentIdx(): Idx<Mutable<Nth[]>>;
+
+		public getPublicKey(): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
+
+		public getSignatureAlgorithmName(): OIDName;
+
+		public getSignatureValueHex(): ASN1V;
+
+		public verifySignature(pubKey: GetKeyParam): boolean | 0;
+
+		public parseExt(): void;
+
+		public getExtInfo(oidOrName: OID | string): ExtInfo | undefined;
+
+		public getExtBasicConstraints(): ExtInfo | {} | {
+			cA: true;
+			pathLen?: number;
+		};
+
+		public getExtKeyUsageBin(): BinString;
+
+		public getExtKeyUsageString(): string;
+
+		public getExtSubjectKeyIdentifier(): ASN1V | undefined;
+
+		public getExtAuthorityKeyIdentifier(): {
+			kid: ASN1V;
+		} | undefined;
+
+		public getExtExtKeyUsageName(): OIDName[] | undefined;
+
+		public getExtSubjectAltName(): Deprecated<string[]>;
+
+		public getExtSubjectAltName2(): ['MAIL' | 'DNS' | 'DN' | 'URI' | 'IP', string][] | undefined;
+
+		public getExtCRLDistributionPointsURI(): string[] | undefined;
+
+		public getExtAIAInfo(): ExtAIAInfo | undefined;
+
+		public getExtCertificatePolicies(): ExtCertificatePolicy[] | undefined;
+
+		public readCertPEM(sCertPEM: PEM): void;
+
+		public readCertHex(sCertHex: HexString): void;
+
+		public getInfo(): string;
+
+		public static hex2dn(hex: HexString, idx?: Idx<HexString>): string;
+
+		public static hex2rdn(hex: HexString, idx?: Idx<HexString>): string;
+
+		public static hex2attrTypeValue(hex: HexString, idx?: Idx<HexString>): string;
+
+		public static getPublicKeyFromCertPEM(sCertPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
+
+		public static getPublicKeyInfoPropOfCertPEM(sCertPEM: PEM): {
+			algparam: ASN1V | null;
+			leyhex: ASN1V;
+			algoid: ASN1V;
+		};
+	}
+}
diff --git a/packages/backend/@types/koa-json-body.d.ts b/packages/backend/@types/koa-json-body.d.ts
new file mode 100644
index 0000000000..5aa8179c5b
--- /dev/null
+++ b/packages/backend/@types/koa-json-body.d.ts
@@ -0,0 +1,15 @@
+declare module 'koa-json-body' {
+	import { Middleware } from 'koa';
+
+	interface IKoaJsonBodyOptions {
+		strict: boolean;
+		limit: string;
+		fallback: boolean;
+	}
+
+	function koaJsonBody(opt?: IKoaJsonBodyOptions): Middleware;
+
+	namespace koaJsonBody {} // Hack
+
+	export = koaJsonBody;
+}
diff --git a/packages/backend/@types/koa-slow.d.ts b/packages/backend/@types/koa-slow.d.ts
new file mode 100644
index 0000000000..e748e2cc98
--- /dev/null
+++ b/packages/backend/@types/koa-slow.d.ts
@@ -0,0 +1,14 @@
+declare module 'koa-slow' {
+	import { Middleware } from 'koa';
+
+	interface ISlowOptions {
+		url?: RegExp;
+		delay?: number;
+	}
+
+	function slow(options?: ISlowOptions): Middleware;
+
+	namespace slow {} // Hack
+
+	export = slow;
+}
diff --git a/packages/backend/@types/langmap.d.ts b/packages/backend/@types/langmap.d.ts
new file mode 100644
index 0000000000..a0f99028ab
--- /dev/null
+++ b/packages/backend/@types/langmap.d.ts
@@ -0,0 +1,10 @@
+declare module 'langmap' {
+	type Lang = {
+		nativeName: string;
+		englishName: string;
+	};
+
+	const langmap: { [lang: string]: Lang };
+
+	export = langmap;
+}
diff --git a/packages/backend/@types/ms.d.ts b/packages/backend/@types/ms.d.ts
new file mode 100644
index 0000000000..2f0156d104
--- /dev/null
+++ b/packages/backend/@types/ms.d.ts
@@ -0,0 +1,12 @@
+declare module 'ms' {
+	interface IMSOptions {
+		long: boolean;
+	}
+
+	function ms(value: string): number;
+	function ms(value: number, options?: IMSOptions): string;
+
+	namespace ms {} // Hack
+
+	export = ms;
+}
diff --git a/packages/backend/@types/os-utils.d.ts b/packages/backend/@types/os-utils.d.ts
new file mode 100644
index 0000000000..390df17d39
--- /dev/null
+++ b/packages/backend/@types/os-utils.d.ts
@@ -0,0 +1,30 @@
+declare module 'os-utils' {
+	type FreeCommandCallback = (usedmem: number) => void;
+
+	type HarddriveCallback = (total: number, free: number, used: number) => void;
+
+	type GetProcessesCallback = (result: string) => void;
+
+	type CPUCallback = (perc: number) => void;
+
+	export function platform(): NodeJS.Platform;
+	export function cpuCount(): number;
+	export function sysUptime(): number;
+	export function processUptime(): number;
+
+	export function freemem(): number;
+	export function totalmem(): number;
+	export function freememPercentage(): number;
+	export function freeCommand(callback: FreeCommandCallback): void;
+
+	export function harddrive(callback: HarddriveCallback): void;
+
+	export function getProcesses(callback: GetProcessesCallback): void;
+	export function getProcesses(nProcess: number, callback: GetProcessesCallback): void;
+
+	export function allLoadavg(): string;
+	export function loadavg(_time?: number): number;
+
+	export function cpuFree(callback: CPUCallback): void;
+	export function cpuUsage(callback: CPUCallback): void;
+}
diff --git a/packages/backend/@types/package.json.d.ts b/packages/backend/@types/package.json.d.ts
new file mode 100644
index 0000000000..abe5fae687
--- /dev/null
+++ b/packages/backend/@types/package.json.d.ts
@@ -0,0 +1,10 @@
+declare module '*/package.json' {
+	interface IRepository {
+		type: string;
+		url: string;
+	}
+
+	export const name: string;
+	export const version: string;
+	export const repository: IRepository;
+}
diff --git a/packages/backend/@types/probe-image-size.d.ts b/packages/backend/@types/probe-image-size.d.ts
new file mode 100644
index 0000000000..665edcf2e7
--- /dev/null
+++ b/packages/backend/@types/probe-image-size.d.ts
@@ -0,0 +1,27 @@
+declare module 'probe-image-size' {
+	import { ReadStream } from 'fs';
+
+	type ProbeOptions = {
+		retries: 1;
+		timeout: 30000;
+	};
+
+	type ProbeResult = {
+		width: number;
+		height: number;
+		length?: number;
+		type: string;
+		mime: string;
+		wUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex';
+		hUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex';
+		url?: string;
+	};
+
+	function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise<ProbeResult>;
+	function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void;
+	function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void;
+
+	namespace probeImageSize {} // Hack
+
+	export = probeImageSize;
+}
diff --git a/packages/backend/assets/api-doc.png b/packages/backend/assets/api-doc.png
new file mode 100644
index 0000000000..9b07f1f398
Binary files /dev/null and b/packages/backend/assets/api-doc.png differ
diff --git a/packages/backend/assets/apple-touch-icon.png b/packages/backend/assets/apple-touch-icon.png
new file mode 100644
index 0000000000..947c513bbb
Binary files /dev/null and b/packages/backend/assets/apple-touch-icon.png differ
diff --git a/packages/backend/assets/favicon.ico b/packages/backend/assets/favicon.ico
new file mode 100644
index 0000000000..9be1ff6295
Binary files /dev/null and b/packages/backend/assets/favicon.ico differ
diff --git a/packages/backend/assets/favicon.png b/packages/backend/assets/favicon.png
new file mode 100644
index 0000000000..b4eb18a5cb
Binary files /dev/null and b/packages/backend/assets/favicon.png differ
diff --git a/packages/backend/assets/icons/192.png b/packages/backend/assets/icons/192.png
new file mode 100644
index 0000000000..606b46d87c
Binary files /dev/null and b/packages/backend/assets/icons/192.png differ
diff --git a/packages/backend/assets/icons/512.png b/packages/backend/assets/icons/512.png
new file mode 100644
index 0000000000..ba51546427
Binary files /dev/null and b/packages/backend/assets/icons/512.png differ
diff --git a/packages/backend/assets/mi-white.png b/packages/backend/assets/mi-white.png
new file mode 100644
index 0000000000..1e57da6b38
Binary files /dev/null and b/packages/backend/assets/mi-white.png differ
diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html
new file mode 100644
index 0000000000..9ee5a95c05
--- /dev/null
+++ b/packages/backend/assets/redoc.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Misskey API</title>
+		<!-- needed for adaptive design -->
+		<meta charset="utf-8"/>
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
+
+		<!--
+		ReDoc doesn't change outer page styles
+		-->
+		<style>
+			body {
+				margin: 0;
+				padding: 0;
+			}
+		</style>
+	</head>
+	<body>
+		<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
+		<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script>
+	</body>
+</html>
diff --git a/packages/backend/assets/robots.txt b/packages/backend/assets/robots.txt
new file mode 100644
index 0000000000..dc17e04e3f
--- /dev/null
+++ b/packages/backend/assets/robots.txt
@@ -0,0 +1,4 @@
+user-agent: *
+allow: /
+
+# todo: sitemap
diff --git a/packages/backend/assets/user-unknown.png b/packages/backend/assets/user-unknown.png
new file mode 100644
index 0000000000..ab29bef2b1
Binary files /dev/null and b/packages/backend/assets/user-unknown.png differ
diff --git a/packages/backend/migration/1000000000000-Init.js b/packages/backend/migration/1000000000000-Init.js
new file mode 100644
index 0000000000..19b2166003
--- /dev/null
+++ b/packages/backend/migration/1000000000000-Init.js
@@ -0,0 +1,483 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class Init1000000000000 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TYPE "log_level_enum" AS ENUM('error', 'warning', 'info', 'success', 'debug')`);
+        await queryRunner.query(`CREATE TABLE "log" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "domain" character varying(64) array NOT NULL DEFAULT '{}'::varchar[], "level" "log_level_enum" NOT NULL, "worker" character varying(8) NOT NULL, "machine" character varying(128) NOT NULL, "message" character varying(1024) NOT NULL, "data" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_350604cbdf991d5930d9e618fbd" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_8e4eb51a35d81b64dda28eed0a" ON "log" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_8cb40cfc8f3c28261e6f887b03" ON "log" ("domain") `);
+        await queryRunner.query(`CREATE INDEX "IDX_584b536b49e53ac81beb39a177" ON "log" ("level") `);
+        await queryRunner.query(`CREATE TABLE "drive_folder" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(128) NOT NULL, "userId" character varying(32), "parentId" character varying(32), CONSTRAINT "PK_7a0c089191f5ebdc214e0af808a" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_02878d441ceae15ce060b73daf" ON "drive_folder" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f4fc06e49c0171c85f1c48060d" ON "drive_folder" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_00ceffb0cdc238b3233294f08f" ON "drive_folder" ("parentId") `);
+        await queryRunner.query(`CREATE TABLE "drive_file" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32), "userHost" character varying(128), "md5" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "type" character varying(128) NOT NULL, "size" integer NOT NULL, "comment" character varying(512), "properties" jsonb NOT NULL DEFAULT '{}', "storedInternal" boolean NOT NULL, "url" character varying(512) NOT NULL, "thumbnailUrl" character varying(512), "webpublicUrl" character varying(512), "accessKey" character varying(256), "thumbnailAccessKey" character varying(256), "webpublicAccessKey" character varying(256), "uri" character varying(512), "src" character varying(512), "folderId" character varying(32), "isSensitive" boolean NOT NULL DEFAULT false, "isLink" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_43ddaaaf18c9e68029b7cbb032e" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_860fa6f6c7df5bb887249fba22" ON "drive_file" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_92779627994ac79277f070c91e" ON "drive_file" ("userHost") `);
+        await queryRunner.query(`CREATE INDEX "IDX_37bb9a1b4585f8a3beb24c62d6" ON "drive_file" ("md5") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a40b8df8c989d7db937ea27cf6" ON "drive_file" ("type") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d85a184c2540d2deba33daf642" ON "drive_file" ("accessKey") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e74022ce9a074b3866f70e0d27" ON "drive_file" ("thumbnailAccessKey") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c55b2b7c284d9fef98026fc88e" ON "drive_file" ("webpublicAccessKey") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e5848eac4940934e23dbc17581" ON "drive_file" ("uri") `);
+        await queryRunner.query(`CREATE INDEX "IDX_bb90d1956dafc4068c28aa7560" ON "drive_file" ("folderId") `);
+        await queryRunner.query(`CREATE TABLE "user" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "lastFetchedAt" TIMESTAMP WITH TIME ZONE, "username" character varying(128) NOT NULL, "usernameLower" character varying(128) NOT NULL, "name" character varying(128), "followersCount" integer NOT NULL DEFAULT 0, "followingCount" integer NOT NULL DEFAULT 0, "notesCount" integer NOT NULL DEFAULT 0, "avatarId" character varying(32), "bannerId" character varying(32), "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "avatarUrl" character varying(512), "bannerUrl" character varying(512), "avatarColor" character varying(32), "bannerColor" character varying(32), "isSuspended" boolean NOT NULL DEFAULT false, "isSilenced" boolean NOT NULL DEFAULT false, "isLocked" boolean NOT NULL DEFAULT false, "isBot" boolean NOT NULL DEFAULT false, "isCat" boolean NOT NULL DEFAULT false, "isAdmin" boolean NOT NULL DEFAULT false, "isModerator" boolean NOT NULL DEFAULT false, "isVerified" boolean NOT NULL DEFAULT false, "emojis" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "host" character varying(128), "inbox" character varying(512), "sharedInbox" character varying(512), "featured" character varying(512), "uri" character varying(512), "token" character(16), CONSTRAINT "UQ_a854e557b1b14814750c7c7b0c9" UNIQUE ("token"), CONSTRAINT "REL_58f5c71eaab331645112cf8cfa" UNIQUE ("avatarId"), CONSTRAINT "REL_afc64b53f8db3707ceb34eb28e" UNIQUE ("bannerId"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_e11e649824a45d8ed01d597fd9" ON "user" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_80ca6e6ef65fb9ef34ea8c90f4" ON "user" ("updatedAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a27b942a0d6dcff90e3ee9b5e8" ON "user" ("usernameLower") `);
+        await queryRunner.query(`CREATE INDEX "IDX_fa99d777623947a5b05f394cae" ON "user" ("tags") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3252a5df8d5bbd16b281f7799e" ON "user" ("host") `);
+        await queryRunner.query(`CREATE INDEX "IDX_be623adaa4c566baf5d29ce0c8" ON "user" ("uri") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a854e557b1b14814750c7c7b0c" ON "user" ("token") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5deb01ae162d1d70b80d064c27" ON "user" ("usernameLower", "host") `);
+        await queryRunner.query(`CREATE TABLE "app" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32), "secret" character varying(64) NOT NULL, "name" character varying(128) NOT NULL, "description" character varying(512) NOT NULL, "permission" character varying(64) array NOT NULL, "callbackUrl" character varying(512), CONSTRAINT "PK_9478629fc093d229df09e560aea" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_048a757923ed8b157e9895da53" ON "app" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3f5b0899ef90527a3462d7c2cb" ON "app" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f49922d511d666848f250663c4" ON "app" ("secret") `);
+        await queryRunner.query(`CREATE TABLE "access_token" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(128) NOT NULL, "hash" character varying(128) NOT NULL, "userId" character varying(32) NOT NULL, "appId" character varying(32) NOT NULL, CONSTRAINT "PK_f20f028607b2603deabd8182d12" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_70ba8f6af34bc924fc9e12adb8" ON "access_token" ("token") `);
+        await queryRunner.query(`CREATE INDEX "IDX_64c327441248bae40f7d92f34f" ON "access_token" ("hash") `);
+        await queryRunner.query(`CREATE INDEX "IDX_9949557d0e1b2c19e5344c171e" ON "access_token" ("userId") `);
+        await queryRunner.query(`CREATE TYPE "note_visibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`);
+        await queryRunner.query(`CREATE TABLE "note" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "replyId" character varying(32), "renoteId" character varying(32), "text" text, "name" character varying(256), "cw" character varying(512), "appId" character varying(32), "userId" character varying(32) NOT NULL, "viaMobile" boolean NOT NULL DEFAULT false, "localOnly" boolean NOT NULL DEFAULT false, "renoteCount" smallint NOT NULL DEFAULT 0, "repliesCount" smallint NOT NULL DEFAULT 0, "reactions" jsonb NOT NULL DEFAULT '{}', "visibility" "note_visibility_enum" NOT NULL, "uri" character varying(512), "score" integer NOT NULL DEFAULT 0, "fileIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "attachedFileTypes" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "mentions" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "mentionedRemoteUsers" text NOT NULL DEFAULT '[]', "emojis" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "hasPoll" boolean NOT NULL DEFAULT false, "geo" jsonb DEFAULT null, "userHost" character varying(128), "replyUserId" character varying(32), "replyUserHost" character varying(128), "renoteUserId" character varying(32), "renoteUserHost" character varying(128), CONSTRAINT "PK_96d0c172a4fba276b1bbed43058" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_17cb3553c700a4985dff5a30ff" ON "note" ("replyId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_52ccc804d7c69037d558bac4c9" ON "note" ("renoteId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_153536c67d05e9adb24e99fc2b" ON "note" ("uri") `);
+        await queryRunner.query(`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds") `);
+        await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `);
+        await queryRunner.query(`CREATE INDEX "IDX_796a8c03959361f97dc2be1d5c" ON "note" ("visibleUserIds") `);
+        await queryRunner.query(`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions") `);
+        await queryRunner.query(`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7125a826ab192eb27e11d358a5" ON "note" ("userHost") `);
+        await queryRunner.query(`CREATE TABLE "poll_vote" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "choice" integer NOT NULL, CONSTRAINT "PK_fd002d371201c472490ba89c6a0" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_0fb627e1c2f753262a74f0562d" ON "poll_vote" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_66d2bd2ee31d14bcc23069a89f" ON "poll_vote" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_aecfbd5ef60374918e63ee95fa" ON "poll_vote" ("noteId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_50bd7164c5b78f1f4a42c4d21f" ON "poll_vote" ("userId", "noteId", "choice") `);
+        await queryRunner.query(`CREATE TABLE "note_reaction" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "reaction" character varying(128) NOT NULL, CONSTRAINT "PK_767ec729b108799b587a3fcc9cf" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_13761f64257f40c5636d0ff95e" ON "note_reaction" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_45145e4953780f3cd5656f0ea6" ON "note_reaction" ("noteId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") `);
+        await queryRunner.query(`CREATE TABLE "note_watching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "noteUserId" character varying(32) NOT NULL, CONSTRAINT "PK_49286fdb23725945a74aa27d757" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_318cdf42a9cfc11f479bd802bb" ON "note_watching" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_b0134ec406e8d09a540f818288" ON "note_watching" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_03e7028ab8388a3f5e3ce2a861" ON "note_watching" ("noteId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_44499765eec6b5489d72c4253b" ON "note_watching" ("noteUserId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a42c93c69989ce1d09959df4cf" ON "note_watching" ("userId", "noteId") `);
+        await queryRunner.query(`CREATE TABLE "note_unread" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "noteUserId" character varying(32) NOT NULL, "isSpecified" boolean NOT NULL, CONSTRAINT "PK_1904eda61a784f57e6e51fa9c1f" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_56b0166d34ddae49d8ef7610bb" ON "note_unread" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e637cba4dc4410218c4251260e" ON "note_unread" ("noteId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d908433a4953cc13216cd9c274" ON "note_unread" ("userId", "noteId") `);
+        await queryRunner.query(`CREATE TABLE "notification" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "notifieeId" character varying(32) NOT NULL, "notifierId" character varying(32) NOT NULL, "type" character varying(32) NOT NULL, "isRead" boolean NOT NULL DEFAULT false, "noteId" character varying(32), "reaction" character varying(128), "choice" integer, CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON "notification" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3c601b70a1066d2c8b517094cb" ON "notification" ("notifieeId") `);
+        await queryRunner.query(`CREATE TABLE "meta" ("id" character varying(32) NOT NULL, "name" character varying(128), "description" character varying(1024), "maintainerName" character varying(128), "maintainerEmail" character varying(128), "announcements" jsonb NOT NULL DEFAULT '[]', "disableRegistration" boolean NOT NULL DEFAULT false, "disableLocalTimeline" boolean NOT NULL DEFAULT false, "disableGlobalTimeline" boolean NOT NULL DEFAULT false, "enableEmojiReaction" boolean NOT NULL DEFAULT true, "useStarForReactionFallback" boolean NOT NULL DEFAULT false, "langs" character varying(64) array NOT NULL DEFAULT '{}'::varchar[], "hiddenTags" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "blockedHosts" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "mascotImageUrl" character varying(512) DEFAULT '/assets/ai.png', "bannerUrl" character varying(512), "errorImageUrl" character varying(512) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png', "iconUrl" character varying(512), "cacheRemoteFiles" boolean NOT NULL DEFAULT true, "proxyAccount" character varying(128), "enableRecaptcha" boolean NOT NULL DEFAULT false, "recaptchaSiteKey" character varying(64), "recaptchaSecretKey" character varying(64), "localDriveCapacityMb" integer NOT NULL DEFAULT 1024, "remoteDriveCapacityMb" integer NOT NULL DEFAULT 32, "maxNoteTextLength" integer NOT NULL DEFAULT 500, "summalyProxy" character varying(128), "enableEmail" boolean NOT NULL DEFAULT false, "email" character varying(128), "smtpSecure" boolean NOT NULL DEFAULT false, "smtpHost" character varying(128), "smtpPort" integer, "smtpUser" character varying(128), "smtpPass" character varying(128), "enableServiceWorker" boolean NOT NULL DEFAULT false, "swPublicKey" character varying(128), "swPrivateKey" character varying(128), "enableTwitterIntegration" boolean NOT NULL DEFAULT false, "twitterConsumerKey" character varying(128), "twitterConsumerSecret" character varying(128), "enableGithubIntegration" boolean NOT NULL DEFAULT false, "githubClientId" character varying(128), "githubClientSecret" character varying(128), "enableDiscordIntegration" boolean NOT NULL DEFAULT false, "discordClientId" character varying(128), "discordClientSecret" character varying(128), CONSTRAINT "PK_c4c17a6c2bd7651338b60fc590b" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TABLE "following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, "followerHost" character varying(128), "followerInbox" character varying(512), "followerSharedInbox" character varying(512), "followeeHost" character varying(128), "followeeInbox" character varying(512), "followeeSharedInbox" character varying(512), CONSTRAINT "PK_c76c6e044bdf76ecf8bfb82a645" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_582f8fab771a9040a12961f3e7" ON "following" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_24e0042143a18157b234df186c" ON "following" ("followeeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6516c5a6f3c015b4eed39978be" ON "following" ("followerId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_307be5f1d1252e0388662acb96" ON "following" ("followerId", "followeeId") `);
+        await queryRunner.query(`CREATE TABLE "instance" ("id" character varying(32) NOT NULL, "caughtAt" TIMESTAMP WITH TIME ZONE NOT NULL, "host" character varying(128) NOT NULL, "system" character varying(64), "usersCount" integer NOT NULL DEFAULT 0, "notesCount" integer NOT NULL DEFAULT 0, "followingCount" integer NOT NULL DEFAULT 0, "followersCount" integer NOT NULL DEFAULT 0, "driveUsage" integer NOT NULL DEFAULT 0, "driveFiles" integer NOT NULL DEFAULT 0, "latestRequestSentAt" TIMESTAMP WITH TIME ZONE, "latestStatus" integer, "latestRequestReceivedAt" TIMESTAMP WITH TIME ZONE, "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "isNotResponding" boolean NOT NULL DEFAULT false, "isMarkedAsClosed" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_eaf60e4a0c399c9935413e06474" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_2cd3b2a6b4cf0b910b260afe08" ON "instance" ("caughtAt") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8d5afc98982185799b160e10eb" ON "instance" ("host") `);
+        await queryRunner.query(`CREATE TABLE "muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_2e92d06c8b5c602eeb27ca9ba48" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_f86d57fbca33c7a4e6897490cc" ON "muting" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_ec96b4fed9dae517e0dbbe0675" ON "muting" ("muteeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_93060675b4a79a577f31d260c6" ON "muting" ("muterId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_1eb9d9824a630321a29fd3b290" ON "muting" ("muterId", "muteeId") `);
+        await queryRunner.query(`CREATE TABLE "sw_subscription" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "endpoint" character varying(512) NOT NULL, "auth" character varying(256) NOT NULL, "publickey" character varying(128) NOT NULL, CONSTRAINT "PK_e8f763631530051b95eb6279b91" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_97754ca6f2baff9b4abb7f853d" ON "sw_subscription" ("userId") `);
+        await queryRunner.query(`CREATE TABLE "blocking" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "blockeeId" character varying(32) NOT NULL, "blockerId" character varying(32) NOT NULL, CONSTRAINT "PK_e5d9a541cc1965ee7e048ea09dd" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_b9a354f7941c1e779f3b33aea6" ON "blocking" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_2cd4a2743a99671308f5417759" ON "blocking" ("blockeeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0627125f1a8a42c9a1929edb55" ON "blocking" ("blockerId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_98a1bc5cb30dfd159de056549f" ON "blocking" ("blockerId", "blockeeId") `);
+        await queryRunner.query(`CREATE TABLE "user_list" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, CONSTRAINT "PK_87bab75775fd9b1ff822b656402" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_b7fcefbdd1c18dce86687531f9" ON "user_list" ("userId") `);
+        await queryRunner.query(`CREATE TABLE "user_list_joining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userListId" character varying(32) NOT NULL, CONSTRAINT "PK_11abb3768da1c5f8de101c9df45" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_joining" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_joining" ("userListId") `);
+        await queryRunner.query(`CREATE TABLE "hashtag" ("id" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "mentionedUserIds" character varying(32) array NOT NULL, "mentionedUsersCount" integer NOT NULL DEFAULT 0, "mentionedLocalUserIds" character varying(32) array NOT NULL, "mentionedLocalUsersCount" integer NOT NULL DEFAULT 0, "mentionedRemoteUserIds" character varying(32) array NOT NULL, "mentionedRemoteUsersCount" integer NOT NULL DEFAULT 0, "attachedUserIds" character varying(32) array NOT NULL, "attachedUsersCount" integer NOT NULL DEFAULT 0, "attachedLocalUserIds" character varying(32) array NOT NULL, "attachedLocalUsersCount" integer NOT NULL DEFAULT 0, "attachedRemoteUserIds" character varying(32) array NOT NULL, "attachedRemoteUsersCount" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_cb36eb8af8412bfa978f1165d78" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_347fec870eafea7b26c8a73bac" ON "hashtag" ("name") `);
+        await queryRunner.query(`CREATE INDEX "IDX_2710a55f826ee236ea1a62698f" ON "hashtag" ("mentionedUsersCount") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0e206cec573f1edff4a3062923" ON "hashtag" ("mentionedLocalUsersCount") `);
+        await queryRunner.query(`CREATE INDEX "IDX_4c02d38a976c3ae132228c6fce" ON "hashtag" ("mentionedRemoteUsersCount") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d57f9030cd3af7f63ffb1c267c" ON "hashtag" ("attachedUsersCount") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0c44bf4f680964145f2a68a341" ON "hashtag" ("attachedLocalUsersCount") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0b03cbcd7e6a7ce068efa8ecc2" ON "hashtag" ("attachedRemoteUsersCount") `);
+        await queryRunner.query(`CREATE TABLE "note_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_af0da35a60b9fa4463a62082b36" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_47f4b1892f5d6ba8efb3057d81" ON "note_favorite" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0f4fb9ad355f3effff221ef245" ON "note_favorite" ("userId", "noteId") `);
+        await queryRunner.query(`CREATE TABLE "abuse_user_report" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "reporterId" character varying(32) NOT NULL, "comment" character varying(512) NOT NULL, CONSTRAINT "PK_87873f5f5cc5c321a1306b2d18c" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_db2098070b2b5a523c58181f74" ON "abuse_user_report" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d049123c413e68ca52abe73420" ON "abuse_user_report" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_04cc96756f89d0b7f9473e8cdf" ON "abuse_user_report" ("reporterId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5cd442c3b2e74fdd99dae20243" ON "abuse_user_report" ("userId", "reporterId") `);
+        await queryRunner.query(`CREATE TABLE "registration_ticket" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(64) NOT NULL, CONSTRAINT "PK_f11696b6fafcf3662d4292734f8" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0ff69e8dfa9fe31bb4a4660f59" ON "registration_ticket" ("code") `);
+        await queryRunner.query(`CREATE TABLE "messaging_message" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "recipientId" character varying(32) NOT NULL, "text" character varying(4096), "isRead" boolean NOT NULL DEFAULT false, "fileId" character varying(32), CONSTRAINT "PK_db398fd79dc95d0eb8c30456eaa" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_e21cd3646e52ef9c94aaf17c2e" ON "messaging_message" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5377c307783fce2b6d352e1203" ON "messaging_message" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_cac14a4e3944454a5ce7daa514" ON "messaging_message" ("recipientId") `);
+        await queryRunner.query(`CREATE TABLE "signin" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "ip" character varying(128) NOT NULL, "headers" jsonb NOT NULL, "success" boolean NOT NULL, CONSTRAINT "PK_9e96ddc025712616fc492b3b588" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_2c308dbdc50d94dc625670055f" ON "signin" ("userId") `);
+        await queryRunner.query(`CREATE TABLE "auth_session" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(128) NOT NULL, "userId" character varying(32), "appId" character varying(32) NOT NULL, CONSTRAINT "PK_19354ed146424a728c1112a8cbf" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_62cb09e1129f6ec024ef66e183" ON "auth_session" ("token") `);
+        await queryRunner.query(`CREATE TABLE "follow_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, "requestId" character varying(128), "followerHost" character varying(128), "followerInbox" character varying(512), "followerSharedInbox" character varying(512), "followeeHost" character varying(128), "followeeInbox" character varying(512), "followeeSharedInbox" character varying(512), CONSTRAINT "PK_53a9aa3725f7a3deb150b39dbfc" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_12c01c0d1a79f77d9f6c15fadd" ON "follow_request" ("followeeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a7fd92dd6dc519e6fb435dd108" ON "follow_request" ("followerId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d54a512b822fac7ed52800f6b4" ON "follow_request" ("followerId", "followeeId") `);
+        await queryRunner.query(`CREATE TABLE "emoji" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "name" character varying(128) NOT NULL, "host" character varying(128), "url" character varying(512) NOT NULL, "uri" character varying(512), "type" character varying(64), "aliases" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_df74ce05e24999ee01ea0bc50a3" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_b37dafc86e9af007e3295c2781" ON "emoji" ("name") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5900e907bb46516ddf2871327c" ON "emoji" ("host") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4f4d35e1256c84ae3d1f0eab10" ON "emoji" ("name", "host") `);
+        await queryRunner.query(`CREATE TABLE "reversi_game" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32) NOT NULL, "user2Id" character varying(32) NOT NULL, "user1Accepted" boolean NOT NULL DEFAULT false, "user2Accepted" boolean NOT NULL DEFAULT false, "black" integer, "isStarted" boolean NOT NULL DEFAULT false, "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "surrendered" character varying(32), "logs" jsonb NOT NULL DEFAULT '[]', "map" character varying(64) array NOT NULL, "bw" character varying(32) NOT NULL, "isLlotheo" boolean NOT NULL DEFAULT false, "canPutEverywhere" boolean NOT NULL DEFAULT false, "loopedBoard" boolean NOT NULL DEFAULT false, "form1" jsonb DEFAULT null, "form2" jsonb DEFAULT null, "crc32" character varying(32), CONSTRAINT "PK_76b30eeba71b1193ad7c5311c3f" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt") `);
+        await queryRunner.query(`CREATE TABLE "reversi_matching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "parentId" character varying(32) NOT NULL, "childId" character varying(32) NOT NULL, CONSTRAINT "PK_880bd0afbab232f21c8b9d146cf" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3b25402709dd9882048c2bbade" ON "reversi_matching" ("parentId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId") `);
+        await queryRunner.query(`CREATE TABLE "user_note_pining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_a6a2dad4ae000abce2ea9d9b103" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_bfbc6f79ba4007b4ce5097f08d" ON "user_note_pining" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_410cd649884b501c02d6e72738" ON "user_note_pining" ("userId", "noteId") `);
+        await queryRunner.query(`CREATE TYPE "poll_notevisibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`);
+        await queryRunner.query(`CREATE TABLE "poll" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE, "multiple" boolean NOT NULL, "choices" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "votes" integer array NOT NULL, "noteVisibility" "poll_notevisibility_enum" NOT NULL, "userId" character varying(32) NOT NULL, "userHost" character varying(128), CONSTRAINT "REL_da851e06d0dfe2ef397d8b1bf1" UNIQUE ("noteId"), CONSTRAINT "PK_da851e06d0dfe2ef397d8b1bf1b" PRIMARY KEY ("noteId"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7fa20a12319c7f6dc3aed98c0a" ON "poll" ("userHost") `);
+        await queryRunner.query(`CREATE TABLE "user_keypair" ("userId" character varying(32) NOT NULL, "publicKey" character varying(4096) NOT NULL, "privateKey" character varying(4096) NOT NULL, CONSTRAINT "REL_f4853eb41ab722fe05f81cedeb" UNIQUE ("userId"), CONSTRAINT "PK_f4853eb41ab722fe05f81cedeb6" PRIMARY KEY ("userId"))`);
+        await queryRunner.query(`CREATE TABLE "user_publickey" ("userId" character varying(32) NOT NULL, "keyId" character varying(256) NOT NULL, "keyPem" character varying(4096) NOT NULL, CONSTRAINT "REL_10c146e4b39b443ede016f6736" UNIQUE ("userId"), CONSTRAINT "PK_10c146e4b39b443ede016f6736d" PRIMARY KEY ("userId"))`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_171e64971c780ebd23fae140bb" ON "user_publickey" ("keyId") `);
+        await queryRunner.query(`CREATE TABLE "user_profile" ("userId" character varying(32) NOT NULL, "location" character varying(128), "birthday" character(10), "description" character varying(1024), "fields" jsonb NOT NULL DEFAULT '[]', "url" character varying(512), "email" character varying(128), "emailVerifyCode" character varying(128), "emailVerified" boolean NOT NULL DEFAULT false, "twoFactorTempSecret" character varying(128), "twoFactorSecret" character varying(128), "twoFactorEnabled" boolean NOT NULL DEFAULT false, "password" character varying(128), "clientData" jsonb NOT NULL DEFAULT '{}', "autoWatch" boolean NOT NULL DEFAULT false, "autoAcceptFollowed" boolean NOT NULL DEFAULT false, "alwaysMarkNsfw" boolean NOT NULL DEFAULT false, "carefulBot" boolean NOT NULL DEFAULT false, "twitter" boolean NOT NULL DEFAULT false, "twitterAccessToken" character varying(64) DEFAULT null, "twitterAccessTokenSecret" character varying(64) DEFAULT null, "twitterUserId" character varying(64) DEFAULT null, "twitterScreenName" character varying(64) DEFAULT null, "github" boolean NOT NULL DEFAULT false, "githubAccessToken" character varying(64) DEFAULT null, "githubId" integer DEFAULT null, "githubLogin" character varying(64) DEFAULT null, "discord" boolean NOT NULL DEFAULT false, "discordAccessToken" character varying(64) DEFAULT null, "discordRefreshToken" character varying(64) DEFAULT null, "discordExpiresDate" integer DEFAULT null, "discordId" character varying(64) DEFAULT null, "discordUsername" character varying(64) DEFAULT null, "discordDiscriminator" character varying(64) DEFAULT null, "userHost" character varying(128), CONSTRAINT "REL_51cb79b5555effaf7d69ba1cff" UNIQUE ("userId"), CONSTRAINT "PK_51cb79b5555effaf7d69ba1cff9" PRIMARY KEY ("userId"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_dce530b98e454793dac5ec2f5a" ON "user_profile" ("userHost") `);
+        await queryRunner.query(`CREATE TYPE "__chart__active_users_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__active_users" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__active_users_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_317237a9f733b970604a11e314f" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__drive_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__drive" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__drive_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_totalCount" bigint NOT NULL, "___local_totalSize" bigint NOT NULL, "___local_incCount" bigint NOT NULL, "___local_incSize" bigint NOT NULL, "___local_decCount" bigint NOT NULL, "___local_decSize" bigint NOT NULL, "___remote_totalCount" bigint NOT NULL, "___remote_totalSize" bigint NOT NULL, "___remote_incCount" bigint NOT NULL, "___remote_incSize" bigint NOT NULL, "___remote_decCount" bigint NOT NULL, "___remote_decSize" bigint NOT NULL, CONSTRAINT "PK_f96bc548a765cd4b3b354221ce7" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__federation_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__federation" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__federation_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___instance_total" bigint NOT NULL, "___instance_inc" bigint NOT NULL, "___instance_dec" bigint NOT NULL, CONSTRAINT "PK_b39dcd31a0fe1a7757e348e85fd" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__hashtag_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__hashtag" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__hashtag_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_c32f1ea2b44a5d2f7881e37f8f9" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__instance_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__instance" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__instance_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___requests_failed" bigint NOT NULL, "___requests_succeeded" bigint NOT NULL, "___requests_received" bigint NOT NULL, "___notes_total" bigint NOT NULL, "___notes_inc" bigint NOT NULL, "___notes_dec" bigint NOT NULL, "___notes_diffs_normal" bigint NOT NULL, "___notes_diffs_reply" bigint NOT NULL, "___notes_diffs_renote" bigint NOT NULL, "___users_total" bigint NOT NULL, "___users_inc" bigint NOT NULL, "___users_dec" bigint NOT NULL, "___following_total" bigint NOT NULL, "___following_inc" bigint NOT NULL, "___following_dec" bigint NOT NULL, "___followers_total" bigint NOT NULL, "___followers_inc" bigint NOT NULL, "___followers_dec" bigint NOT NULL, "___drive_totalFiles" bigint NOT NULL, "___drive_totalUsage" bigint NOT NULL, "___drive_incFiles" bigint NOT NULL, "___drive_incUsage" bigint NOT NULL, "___drive_decFiles" bigint NOT NULL, "___drive_decUsage" bigint NOT NULL, CONSTRAINT "PK_1267c67c7c2d47b4903975f2c00" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__network_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__network" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__network_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___incomingRequests" bigint NOT NULL, "___outgoingRequests" bigint NOT NULL, "___totalTime" bigint NOT NULL, "___incomingBytes" bigint NOT NULL, "___outgoingBytes" bigint NOT NULL, CONSTRAINT "PK_bc4290c2e27fad14ef0c1ca93f3" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__notes_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__notes" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__notes_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_total" bigint NOT NULL, "___local_inc" bigint NOT NULL, "___local_dec" bigint NOT NULL, "___local_diffs_normal" bigint NOT NULL, "___local_diffs_reply" bigint NOT NULL, "___local_diffs_renote" bigint NOT NULL, "___remote_total" bigint NOT NULL, "___remote_inc" bigint NOT NULL, "___remote_dec" bigint NOT NULL, "___remote_diffs_normal" bigint NOT NULL, "___remote_diffs_reply" bigint NOT NULL, "___remote_diffs_renote" bigint NOT NULL, CONSTRAINT "PK_0aec823fa85c7f901bdb3863b14" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__per_user_drive_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__per_user_drive" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_drive_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___totalCount" bigint NOT NULL, "___totalSize" bigint NOT NULL, "___incCount" bigint NOT NULL, "___incSize" bigint NOT NULL, "___decCount" bigint NOT NULL, "___decSize" bigint NOT NULL, CONSTRAINT "PK_d0ef23d24d666e1a44a0cd3d208" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__per_user_following_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__per_user_following" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_following_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_followings_total" bigint NOT NULL, "___local_followings_inc" bigint NOT NULL, "___local_followings_dec" bigint NOT NULL, "___local_followers_total" bigint NOT NULL, "___local_followers_inc" bigint NOT NULL, "___local_followers_dec" bigint NOT NULL, "___remote_followings_total" bigint NOT NULL, "___remote_followings_inc" bigint NOT NULL, "___remote_followings_dec" bigint NOT NULL, "___remote_followers_total" bigint NOT NULL, "___remote_followers_inc" bigint NOT NULL, "___remote_followers_dec" bigint NOT NULL, CONSTRAINT "PK_85bb1b540363a29c2fec83bd907" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__per_user_notes_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__per_user_notes" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_notes_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___total" bigint NOT NULL, "___inc" bigint NOT NULL, "___dec" bigint NOT NULL, "___diffs_normal" bigint NOT NULL, "___diffs_reply" bigint NOT NULL, "___diffs_renote" bigint NOT NULL, CONSTRAINT "PK_334acf6e915af2f29edc11b8e50" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__per_user_reaction_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__per_user_reaction" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_reaction_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_984f54dae441e65b633e8d27a7f" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__test_grouped_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__test_grouped" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_grouped_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo_total" bigint NOT NULL, "___foo_inc" bigint NOT NULL, "___foo_dec" bigint NOT NULL, CONSTRAINT "PK_f4a2b175d308695af30d4293272" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__test_unique_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__test_unique" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_unique_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo" bigint NOT NULL, CONSTRAINT "PK_409bac9c97cc612d8500012319d" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__test_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__test" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo_total" bigint NOT NULL, "___foo_inc" bigint NOT NULL, "___foo_dec" bigint NOT NULL, CONSTRAINT "PK_b4bc31dffbd1b785276a3ecfc1e" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE TYPE "__chart__users_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`CREATE TABLE "__chart__users" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__users_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_total" bigint NOT NULL, "___local_inc" bigint NOT NULL, "___local_dec" bigint NOT NULL, "___remote_total" bigint NOT NULL, "___remote_inc" bigint NOT NULL, "___remote_dec" bigint NOT NULL, CONSTRAINT "PK_4dfcf2c78d03524b9eb2c99d328" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`ALTER TABLE "drive_folder" ADD CONSTRAINT "FK_f4fc06e49c0171c85f1c48060d2" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "drive_folder" ADD CONSTRAINT "FK_00ceffb0cdc238b3233294f08f2" FOREIGN KEY ("parentId") REFERENCES "drive_folder"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "drive_file" ADD CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "drive_file" ADD CONSTRAINT "FK_bb90d1956dafc4068c28aa7560a" FOREIGN KEY ("folderId") REFERENCES "drive_folder"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_58f5c71eaab331645112cf8cfa5" FOREIGN KEY ("avatarId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_afc64b53f8db3707ceb34eb28e2" FOREIGN KEY ("bannerId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "app" ADD CONSTRAINT "FK_3f5b0899ef90527a3462d7c2cb3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_9949557d0e1b2c19e5344c171e9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_ec5c201576192ba8904c345c5cc" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_5b87d9d19127bd5d92026017a7b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_66d2bd2ee31d14bcc23069a89f8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_aecfbd5ef60374918e63ee95fa7" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note_reaction" ADD CONSTRAINT "FK_13761f64257f40c5636d0ff95ee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note_reaction" ADD CONSTRAINT "FK_45145e4953780f3cd5656f0ea6a" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note_watching" ADD CONSTRAINT "FK_b0134ec406e8d09a540f8182888" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note_watching" ADD CONSTRAINT "FK_03e7028ab8388a3f5e3ce2a8619" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note_unread" ADD CONSTRAINT "FK_56b0166d34ddae49d8ef7610bb9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note_unread" ADD CONSTRAINT "FK_e637cba4dc4410218c4251260e4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3c601b70a1066d2c8b517094cb9" FOREIGN KEY ("notifieeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_769cb6b73a1efe22ddf733ac453" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "following" ADD CONSTRAINT "FK_24e0042143a18157b234df186c3" FOREIGN KEY ("followeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "following" ADD CONSTRAINT "FK_6516c5a6f3c015b4eed39978be5" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "muting" ADD CONSTRAINT "FK_ec96b4fed9dae517e0dbbe0675c" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "muting" ADD CONSTRAINT "FK_93060675b4a79a577f31d260c67" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "sw_subscription" ADD CONSTRAINT "FK_97754ca6f2baff9b4abb7f853dd" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "blocking" ADD CONSTRAINT "FK_2cd4a2743a99671308f5417759e" FOREIGN KEY ("blockeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "blocking" ADD CONSTRAINT "FK_0627125f1a8a42c9a1929edb552" FOREIGN KEY ("blockerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_list" ADD CONSTRAINT "FK_b7fcefbdd1c18dce86687531f99" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_list_joining" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_list_joining" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note_favorite" ADD CONSTRAINT "FK_47f4b1892f5d6ba8efb3057d81a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note_favorite" ADD CONSTRAINT "FK_0e00498f180193423c992bc4370" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_d049123c413e68ca52abe734203" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_04cc96756f89d0b7f9473e8cdf3" FOREIGN KEY ("reporterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_5377c307783fce2b6d352e1203b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_cac14a4e3944454a5ce7daa5142" FOREIGN KEY ("recipientId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "signin" ADD CONSTRAINT "FK_2c308dbdc50d94dc625670055f7" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "FK_c072b729d71697f959bde66ade0" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "FK_dbe037d4bddd17b03a1dc778dee" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "follow_request" ADD CONSTRAINT "FK_12c01c0d1a79f77d9f6c15fadd2" FOREIGN KEY ("followeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "follow_request" ADD CONSTRAINT "FK_a7fd92dd6dc519e6fb435dd108f" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_note_pining" ADD CONSTRAINT "FK_bfbc6f79ba4007b4ce5097f08d6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_note_pining" ADD CONSTRAINT "FK_68881008f7c3588ad7ecae471cf" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`);
+        await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`);
+        await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`);
+        await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`);
+        await queryRunner.query(`ALTER TABLE "user_note_pining" DROP CONSTRAINT "FK_68881008f7c3588ad7ecae471cf"`);
+        await queryRunner.query(`ALTER TABLE "user_note_pining" DROP CONSTRAINT "FK_bfbc6f79ba4007b4ce5097f08d6"`);
+        await queryRunner.query(`ALTER TABLE "reversi_matching" DROP CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066"`);
+        await queryRunner.query(`ALTER TABLE "reversi_matching" DROP CONSTRAINT "FK_3b25402709dd9882048c2bbade0"`);
+        await queryRunner.query(`ALTER TABLE "reversi_game" DROP CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6"`);
+        await queryRunner.query(`ALTER TABLE "reversi_game" DROP CONSTRAINT "FK_f7467510c60a45ce5aca6292743"`);
+        await queryRunner.query(`ALTER TABLE "follow_request" DROP CONSTRAINT "FK_a7fd92dd6dc519e6fb435dd108f"`);
+        await queryRunner.query(`ALTER TABLE "follow_request" DROP CONSTRAINT "FK_12c01c0d1a79f77d9f6c15fadd2"`);
+        await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "FK_dbe037d4bddd17b03a1dc778dee"`);
+        await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "FK_c072b729d71697f959bde66ade0"`);
+        await queryRunner.query(`ALTER TABLE "signin" DROP CONSTRAINT "FK_2c308dbdc50d94dc625670055f7"`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b"`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_cac14a4e3944454a5ce7daa5142"`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_5377c307783fce2b6d352e1203b"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_04cc96756f89d0b7f9473e8cdf3"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_d049123c413e68ca52abe734203"`);
+        await queryRunner.query(`ALTER TABLE "note_favorite" DROP CONSTRAINT "FK_0e00498f180193423c992bc4370"`);
+        await queryRunner.query(`ALTER TABLE "note_favorite" DROP CONSTRAINT "FK_47f4b1892f5d6ba8efb3057d81a"`);
+        await queryRunner.query(`ALTER TABLE "user_list_joining" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`);
+        await queryRunner.query(`ALTER TABLE "user_list_joining" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`);
+        await queryRunner.query(`ALTER TABLE "user_list" DROP CONSTRAINT "FK_b7fcefbdd1c18dce86687531f99"`);
+        await queryRunner.query(`ALTER TABLE "blocking" DROP CONSTRAINT "FK_0627125f1a8a42c9a1929edb552"`);
+        await queryRunner.query(`ALTER TABLE "blocking" DROP CONSTRAINT "FK_2cd4a2743a99671308f5417759e"`);
+        await queryRunner.query(`ALTER TABLE "sw_subscription" DROP CONSTRAINT "FK_97754ca6f2baff9b4abb7f853dd"`);
+        await queryRunner.query(`ALTER TABLE "muting" DROP CONSTRAINT "FK_93060675b4a79a577f31d260c67"`);
+        await queryRunner.query(`ALTER TABLE "muting" DROP CONSTRAINT "FK_ec96b4fed9dae517e0dbbe0675c"`);
+        await queryRunner.query(`ALTER TABLE "following" DROP CONSTRAINT "FK_6516c5a6f3c015b4eed39978be5"`);
+        await queryRunner.query(`ALTER TABLE "following" DROP CONSTRAINT "FK_24e0042143a18157b234df186c3"`);
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_769cb6b73a1efe22ddf733ac453"`);
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`);
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3c601b70a1066d2c8b517094cb9"`);
+        await queryRunner.query(`ALTER TABLE "note_unread" DROP CONSTRAINT "FK_e637cba4dc4410218c4251260e4"`);
+        await queryRunner.query(`ALTER TABLE "note_unread" DROP CONSTRAINT "FK_56b0166d34ddae49d8ef7610bb9"`);
+        await queryRunner.query(`ALTER TABLE "note_watching" DROP CONSTRAINT "FK_03e7028ab8388a3f5e3ce2a8619"`);
+        await queryRunner.query(`ALTER TABLE "note_watching" DROP CONSTRAINT "FK_b0134ec406e8d09a540f8182888"`);
+        await queryRunner.query(`ALTER TABLE "note_reaction" DROP CONSTRAINT "FK_45145e4953780f3cd5656f0ea6a"`);
+        await queryRunner.query(`ALTER TABLE "note_reaction" DROP CONSTRAINT "FK_13761f64257f40c5636d0ff95ee"`);
+        await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_aecfbd5ef60374918e63ee95fa7"`);
+        await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_66d2bd2ee31d14bcc23069a89f8"`);
+        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_5b87d9d19127bd5d92026017a7b"`);
+        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_ec5c201576192ba8904c345c5cc"`);
+        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`);
+        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_9949557d0e1b2c19e5344c171e9"`);
+        await queryRunner.query(`ALTER TABLE "app" DROP CONSTRAINT "FK_3f5b0899ef90527a3462d7c2cb3"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_afc64b53f8db3707ceb34eb28e2"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_58f5c71eaab331645112cf8cfa5"`);
+        await queryRunner.query(`ALTER TABLE "drive_file" DROP CONSTRAINT "FK_bb90d1956dafc4068c28aa7560a"`);
+        await queryRunner.query(`ALTER TABLE "drive_file" DROP CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e"`);
+        await queryRunner.query(`ALTER TABLE "drive_folder" DROP CONSTRAINT "FK_00ceffb0cdc238b3233294f08f2"`);
+        await queryRunner.query(`ALTER TABLE "drive_folder" DROP CONSTRAINT "FK_f4fc06e49c0171c85f1c48060d2"`);
+        await queryRunner.query(`DROP TABLE "__chart__users"`);
+        await queryRunner.query(`DROP TYPE "__chart__users_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__test"`);
+        await queryRunner.query(`DROP TYPE "__chart__test_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__test_unique"`);
+        await queryRunner.query(`DROP TYPE "__chart__test_unique_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__test_grouped"`);
+        await queryRunner.query(`DROP TYPE "__chart__test_grouped_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__per_user_reaction"`);
+        await queryRunner.query(`DROP TYPE "__chart__per_user_reaction_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__per_user_notes"`);
+        await queryRunner.query(`DROP TYPE "__chart__per_user_notes_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__per_user_following"`);
+        await queryRunner.query(`DROP TYPE "__chart__per_user_following_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__per_user_drive"`);
+        await queryRunner.query(`DROP TYPE "__chart__per_user_drive_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__notes"`);
+        await queryRunner.query(`DROP TYPE "__chart__notes_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__network"`);
+        await queryRunner.query(`DROP TYPE "__chart__network_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__instance"`);
+        await queryRunner.query(`DROP TYPE "__chart__instance_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__hashtag"`);
+        await queryRunner.query(`DROP TYPE "__chart__hashtag_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__federation"`);
+        await queryRunner.query(`DROP TYPE "__chart__federation_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__drive"`);
+        await queryRunner.query(`DROP TYPE "__chart__drive_span_enum"`);
+        await queryRunner.query(`DROP TABLE "__chart__active_users"`);
+        await queryRunner.query(`DROP TYPE "__chart__active_users_span_enum"`);
+        await queryRunner.query(`DROP INDEX "IDX_dce530b98e454793dac5ec2f5a"`);
+        await queryRunner.query(`DROP TABLE "user_profile"`);
+        await queryRunner.query(`DROP INDEX "IDX_171e64971c780ebd23fae140bb"`);
+        await queryRunner.query(`DROP TABLE "user_publickey"`);
+        await queryRunner.query(`DROP TABLE "user_keypair"`);
+        await queryRunner.query(`DROP INDEX "IDX_7fa20a12319c7f6dc3aed98c0a"`);
+        await queryRunner.query(`DROP INDEX "IDX_0610ebcfcfb4a18441a9bcdab2"`);
+        await queryRunner.query(`DROP TABLE "poll"`);
+        await queryRunner.query(`DROP TYPE "poll_notevisibility_enum"`);
+        await queryRunner.query(`DROP INDEX "IDX_410cd649884b501c02d6e72738"`);
+        await queryRunner.query(`DROP INDEX "IDX_bfbc6f79ba4007b4ce5097f08d"`);
+        await queryRunner.query(`DROP TABLE "user_note_pining"`);
+        await queryRunner.query(`DROP INDEX "IDX_e247b23a3c9b45f89ec1299d06"`);
+        await queryRunner.query(`DROP INDEX "IDX_3b25402709dd9882048c2bbade"`);
+        await queryRunner.query(`DROP INDEX "IDX_b604d92d6c7aec38627f6eaf16"`);
+        await queryRunner.query(`DROP TABLE "reversi_matching"`);
+        await queryRunner.query(`DROP INDEX "IDX_b46ec40746efceac604142be1c"`);
+        await queryRunner.query(`DROP TABLE "reversi_game"`);
+        await queryRunner.query(`DROP INDEX "IDX_4f4d35e1256c84ae3d1f0eab10"`);
+        await queryRunner.query(`DROP INDEX "IDX_5900e907bb46516ddf2871327c"`);
+        await queryRunner.query(`DROP INDEX "IDX_b37dafc86e9af007e3295c2781"`);
+        await queryRunner.query(`DROP TABLE "emoji"`);
+        await queryRunner.query(`DROP INDEX "IDX_d54a512b822fac7ed52800f6b4"`);
+        await queryRunner.query(`DROP INDEX "IDX_a7fd92dd6dc519e6fb435dd108"`);
+        await queryRunner.query(`DROP INDEX "IDX_12c01c0d1a79f77d9f6c15fadd"`);
+        await queryRunner.query(`DROP TABLE "follow_request"`);
+        await queryRunner.query(`DROP INDEX "IDX_62cb09e1129f6ec024ef66e183"`);
+        await queryRunner.query(`DROP TABLE "auth_session"`);
+        await queryRunner.query(`DROP INDEX "IDX_2c308dbdc50d94dc625670055f"`);
+        await queryRunner.query(`DROP TABLE "signin"`);
+        await queryRunner.query(`DROP INDEX "IDX_cac14a4e3944454a5ce7daa514"`);
+        await queryRunner.query(`DROP INDEX "IDX_5377c307783fce2b6d352e1203"`);
+        await queryRunner.query(`DROP INDEX "IDX_e21cd3646e52ef9c94aaf17c2e"`);
+        await queryRunner.query(`DROP TABLE "messaging_message"`);
+        await queryRunner.query(`DROP INDEX "IDX_0ff69e8dfa9fe31bb4a4660f59"`);
+        await queryRunner.query(`DROP TABLE "registration_ticket"`);
+        await queryRunner.query(`DROP INDEX "IDX_5cd442c3b2e74fdd99dae20243"`);
+        await queryRunner.query(`DROP INDEX "IDX_04cc96756f89d0b7f9473e8cdf"`);
+        await queryRunner.query(`DROP INDEX "IDX_d049123c413e68ca52abe73420"`);
+        await queryRunner.query(`DROP INDEX "IDX_db2098070b2b5a523c58181f74"`);
+        await queryRunner.query(`DROP TABLE "abuse_user_report"`);
+        await queryRunner.query(`DROP INDEX "IDX_0f4fb9ad355f3effff221ef245"`);
+        await queryRunner.query(`DROP INDEX "IDX_47f4b1892f5d6ba8efb3057d81"`);
+        await queryRunner.query(`DROP TABLE "note_favorite"`);
+        await queryRunner.query(`DROP INDEX "IDX_0b03cbcd7e6a7ce068efa8ecc2"`);
+        await queryRunner.query(`DROP INDEX "IDX_0c44bf4f680964145f2a68a341"`);
+        await queryRunner.query(`DROP INDEX "IDX_d57f9030cd3af7f63ffb1c267c"`);
+        await queryRunner.query(`DROP INDEX "IDX_4c02d38a976c3ae132228c6fce"`);
+        await queryRunner.query(`DROP INDEX "IDX_0e206cec573f1edff4a3062923"`);
+        await queryRunner.query(`DROP INDEX "IDX_2710a55f826ee236ea1a62698f"`);
+        await queryRunner.query(`DROP INDEX "IDX_347fec870eafea7b26c8a73bac"`);
+        await queryRunner.query(`DROP TABLE "hashtag"`);
+        await queryRunner.query(`DROP INDEX "IDX_605472305f26818cc93d1baaa7"`);
+        await queryRunner.query(`DROP INDEX "IDX_d844bfc6f3f523a05189076efa"`);
+        await queryRunner.query(`DROP TABLE "user_list_joining"`);
+        await queryRunner.query(`DROP INDEX "IDX_b7fcefbdd1c18dce86687531f9"`);
+        await queryRunner.query(`DROP TABLE "user_list"`);
+        await queryRunner.query(`DROP INDEX "IDX_98a1bc5cb30dfd159de056549f"`);
+        await queryRunner.query(`DROP INDEX "IDX_0627125f1a8a42c9a1929edb55"`);
+        await queryRunner.query(`DROP INDEX "IDX_2cd4a2743a99671308f5417759"`);
+        await queryRunner.query(`DROP INDEX "IDX_b9a354f7941c1e779f3b33aea6"`);
+        await queryRunner.query(`DROP TABLE "blocking"`);
+        await queryRunner.query(`DROP INDEX "IDX_97754ca6f2baff9b4abb7f853d"`);
+        await queryRunner.query(`DROP TABLE "sw_subscription"`);
+        await queryRunner.query(`DROP INDEX "IDX_1eb9d9824a630321a29fd3b290"`);
+        await queryRunner.query(`DROP INDEX "IDX_93060675b4a79a577f31d260c6"`);
+        await queryRunner.query(`DROP INDEX "IDX_ec96b4fed9dae517e0dbbe0675"`);
+        await queryRunner.query(`DROP INDEX "IDX_f86d57fbca33c7a4e6897490cc"`);
+        await queryRunner.query(`DROP TABLE "muting"`);
+        await queryRunner.query(`DROP INDEX "IDX_8d5afc98982185799b160e10eb"`);
+        await queryRunner.query(`DROP INDEX "IDX_2cd3b2a6b4cf0b910b260afe08"`);
+        await queryRunner.query(`DROP TABLE "instance"`);
+        await queryRunner.query(`DROP INDEX "IDX_307be5f1d1252e0388662acb96"`);
+        await queryRunner.query(`DROP INDEX "IDX_6516c5a6f3c015b4eed39978be"`);
+        await queryRunner.query(`DROP INDEX "IDX_24e0042143a18157b234df186c"`);
+        await queryRunner.query(`DROP INDEX "IDX_582f8fab771a9040a12961f3e7"`);
+        await queryRunner.query(`DROP TABLE "following"`);
+        await queryRunner.query(`DROP TABLE "meta"`);
+        await queryRunner.query(`DROP INDEX "IDX_3c601b70a1066d2c8b517094cb"`);
+        await queryRunner.query(`DROP INDEX "IDX_b11a5e627c41d4dc3170f1d370"`);
+        await queryRunner.query(`DROP TABLE "notification"`);
+        await queryRunner.query(`DROP INDEX "IDX_d908433a4953cc13216cd9c274"`);
+        await queryRunner.query(`DROP INDEX "IDX_e637cba4dc4410218c4251260e"`);
+        await queryRunner.query(`DROP INDEX "IDX_56b0166d34ddae49d8ef7610bb"`);
+        await queryRunner.query(`DROP TABLE "note_unread"`);
+        await queryRunner.query(`DROP INDEX "IDX_a42c93c69989ce1d09959df4cf"`);
+        await queryRunner.query(`DROP INDEX "IDX_44499765eec6b5489d72c4253b"`);
+        await queryRunner.query(`DROP INDEX "IDX_03e7028ab8388a3f5e3ce2a861"`);
+        await queryRunner.query(`DROP INDEX "IDX_b0134ec406e8d09a540f818288"`);
+        await queryRunner.query(`DROP INDEX "IDX_318cdf42a9cfc11f479bd802bb"`);
+        await queryRunner.query(`DROP TABLE "note_watching"`);
+        await queryRunner.query(`DROP INDEX "IDX_ad0c221b25672daf2df320a817"`);
+        await queryRunner.query(`DROP INDEX "IDX_45145e4953780f3cd5656f0ea6"`);
+        await queryRunner.query(`DROP INDEX "IDX_13761f64257f40c5636d0ff95e"`);
+        await queryRunner.query(`DROP INDEX "IDX_01f4581f114e0ebd2bbb876f0b"`);
+        await queryRunner.query(`DROP TABLE "note_reaction"`);
+        await queryRunner.query(`DROP INDEX "IDX_50bd7164c5b78f1f4a42c4d21f"`);
+        await queryRunner.query(`DROP INDEX "IDX_aecfbd5ef60374918e63ee95fa"`);
+        await queryRunner.query(`DROP INDEX "IDX_66d2bd2ee31d14bcc23069a89f"`);
+        await queryRunner.query(`DROP INDEX "IDX_0fb627e1c2f753262a74f0562d"`);
+        await queryRunner.query(`DROP TABLE "poll_vote"`);
+        await queryRunner.query(`DROP INDEX "IDX_7125a826ab192eb27e11d358a5"`);
+        await queryRunner.query(`DROP INDEX "IDX_88937d94d7443d9a99a76fa5c0"`);
+        await queryRunner.query(`DROP INDEX "IDX_54ebcb6d27222913b908d56fd8"`);
+        await queryRunner.query(`DROP INDEX "IDX_796a8c03959361f97dc2be1d5c"`);
+        await queryRunner.query(`DROP INDEX "IDX_25dfc71b0369b003a4cd434d0b"`);
+        await queryRunner.query(`DROP INDEX "IDX_51c063b6a133a9cb87145450f5"`);
+        await queryRunner.query(`DROP INDEX "IDX_153536c67d05e9adb24e99fc2b"`);
+        await queryRunner.query(`DROP INDEX "IDX_5b87d9d19127bd5d92026017a7"`);
+        await queryRunner.query(`DROP INDEX "IDX_52ccc804d7c69037d558bac4c9"`);
+        await queryRunner.query(`DROP INDEX "IDX_17cb3553c700a4985dff5a30ff"`);
+        await queryRunner.query(`DROP INDEX "IDX_e7c0567f5261063592f022e9b5"`);
+        await queryRunner.query(`DROP TABLE "note"`);
+        await queryRunner.query(`DROP TYPE "note_visibility_enum"`);
+        await queryRunner.query(`DROP INDEX "IDX_9949557d0e1b2c19e5344c171e"`);
+        await queryRunner.query(`DROP INDEX "IDX_64c327441248bae40f7d92f34f"`);
+        await queryRunner.query(`DROP INDEX "IDX_70ba8f6af34bc924fc9e12adb8"`);
+        await queryRunner.query(`DROP TABLE "access_token"`);
+        await queryRunner.query(`DROP INDEX "IDX_f49922d511d666848f250663c4"`);
+        await queryRunner.query(`DROP INDEX "IDX_3f5b0899ef90527a3462d7c2cb"`);
+        await queryRunner.query(`DROP INDEX "IDX_048a757923ed8b157e9895da53"`);
+        await queryRunner.query(`DROP TABLE "app"`);
+        await queryRunner.query(`DROP INDEX "IDX_5deb01ae162d1d70b80d064c27"`);
+        await queryRunner.query(`DROP INDEX "IDX_a854e557b1b14814750c7c7b0c"`);
+        await queryRunner.query(`DROP INDEX "IDX_be623adaa4c566baf5d29ce0c8"`);
+        await queryRunner.query(`DROP INDEX "IDX_3252a5df8d5bbd16b281f7799e"`);
+        await queryRunner.query(`DROP INDEX "IDX_fa99d777623947a5b05f394cae"`);
+        await queryRunner.query(`DROP INDEX "IDX_a27b942a0d6dcff90e3ee9b5e8"`);
+        await queryRunner.query(`DROP INDEX "IDX_80ca6e6ef65fb9ef34ea8c90f4"`);
+        await queryRunner.query(`DROP INDEX "IDX_e11e649824a45d8ed01d597fd9"`);
+        await queryRunner.query(`DROP TABLE "user"`);
+        await queryRunner.query(`DROP INDEX "IDX_bb90d1956dafc4068c28aa7560"`);
+        await queryRunner.query(`DROP INDEX "IDX_e5848eac4940934e23dbc17581"`);
+        await queryRunner.query(`DROP INDEX "IDX_c55b2b7c284d9fef98026fc88e"`);
+        await queryRunner.query(`DROP INDEX "IDX_e74022ce9a074b3866f70e0d27"`);
+        await queryRunner.query(`DROP INDEX "IDX_d85a184c2540d2deba33daf642"`);
+        await queryRunner.query(`DROP INDEX "IDX_a40b8df8c989d7db937ea27cf6"`);
+        await queryRunner.query(`DROP INDEX "IDX_37bb9a1b4585f8a3beb24c62d6"`);
+        await queryRunner.query(`DROP INDEX "IDX_92779627994ac79277f070c91e"`);
+        await queryRunner.query(`DROP INDEX "IDX_860fa6f6c7df5bb887249fba22"`);
+        await queryRunner.query(`DROP INDEX "IDX_c8dfad3b72196dd1d6b5db168a"`);
+        await queryRunner.query(`DROP TABLE "drive_file"`);
+        await queryRunner.query(`DROP INDEX "IDX_00ceffb0cdc238b3233294f08f"`);
+        await queryRunner.query(`DROP INDEX "IDX_f4fc06e49c0171c85f1c48060d"`);
+        await queryRunner.query(`DROP INDEX "IDX_02878d441ceae15ce060b73daf"`);
+        await queryRunner.query(`DROP TABLE "drive_folder"`);
+        await queryRunner.query(`DROP INDEX "IDX_584b536b49e53ac81beb39a177"`);
+        await queryRunner.query(`DROP INDEX "IDX_8cb40cfc8f3c28261e6f887b03"`);
+        await queryRunner.query(`DROP INDEX "IDX_8e4eb51a35d81b64dda28eed0a"`);
+        await queryRunner.query(`DROP TABLE "log"`);
+        await queryRunner.query(`DROP TYPE "log_level_enum"`);
+    }
+}
+exports.Init1000000000000 = Init1000000000000;
diff --git a/packages/backend/migration/1556348509290-Pages.js b/packages/backend/migration/1556348509290-Pages.js
new file mode 100644
index 0000000000..d69d172587
--- /dev/null
+++ b/packages/backend/migration/1556348509290-Pages.js
@@ -0,0 +1,29 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class Pages1556348509290 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TYPE "page_visibility_enum" AS ENUM('public', 'followers', 'specified')`);
+        await queryRunner.query(`CREATE TABLE "page" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "name" character varying(256) NOT NULL, "summary" character varying(256), "alignCenter" boolean NOT NULL, "font" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "eyeCatchingImageId" character varying(32), "content" jsonb NOT NULL DEFAULT '[]', "variables" jsonb NOT NULL DEFAULT '[]', "visibility" "page_visibility_enum" NOT NULL, "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_742f4117e065c5b6ad21b37ba1f" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_af639b066dfbca78b01a920f8a" ON "page" ("updatedAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_b82c19c08afb292de4600d99e4" ON "page" ("name") `);
+        await queryRunner.query(`CREATE INDEX "IDX_ae1d917992dd0c9d9bbdad06c4" ON "page" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_90148bbc2bf0854428786bfc15" ON "page" ("visibleUserIds") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2133ef8317e4bdb839c0dcbf13" ON "page" ("userId", "name") `);
+        await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10"`);
+        await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a"`);
+        await queryRunner.query(`DROP INDEX "IDX_2133ef8317e4bdb839c0dcbf13"`);
+        await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`);
+        await queryRunner.query(`DROP INDEX "IDX_ae1d917992dd0c9d9bbdad06c4"`);
+        await queryRunner.query(`DROP INDEX "IDX_b82c19c08afb292de4600d99e4"`);
+        await queryRunner.query(`DROP INDEX "IDX_af639b066dfbca78b01a920f8a"`);
+        await queryRunner.query(`DROP INDEX "IDX_fbb4297c927a9b85e9cefa2eb1"`);
+        await queryRunner.query(`DROP TABLE "page"`);
+        await queryRunner.query(`DROP TYPE "page_visibility_enum"`);
+    }
+}
+exports.Pages1556348509290 = Pages1556348509290;
diff --git a/packages/backend/migration/1556746559567-UserProfile.js b/packages/backend/migration/1556746559567-UserProfile.js
new file mode 100644
index 0000000000..3a082b882d
--- /dev/null
+++ b/packages/backend/migration/1556746559567-UserProfile.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class UserProfile1556746559567 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "githubId" TYPE VARCHAR(64) USING "githubId"::VARCHAR(64)`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "discordExpiresDate" TYPE VARCHAR(64) USING "discordExpiresDate"::VARCHAR(64)`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`UPDATE "user_profile" SET github = FALSE, discord = FALSE`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "githubId" TYPE INTEGER USING NULL`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "discordExpiresDate" TYPE INTEGER USING NULL`);
+    }
+}
+exports.UserProfile1556746559567 = UserProfile1556746559567;
diff --git a/packages/backend/migration/1557476068003-PinnedUsers.js b/packages/backend/migration/1557476068003-PinnedUsers.js
new file mode 100644
index 0000000000..c5e7fe748f
--- /dev/null
+++ b/packages/backend/migration/1557476068003-PinnedUsers.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class PinnedUsers1557476068003 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedUsers" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedUsers"`);
+    }
+}
+exports.PinnedUsers1557476068003 = PinnedUsers1557476068003;
diff --git a/packages/backend/migration/1557761316509-AddSomeUrls.js b/packages/backend/migration/1557761316509-AddSomeUrls.js
new file mode 100644
index 0000000000..f6a7b409c2
--- /dev/null
+++ b/packages/backend/migration/1557761316509-AddSomeUrls.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class AddSomeUrls1557761316509 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "ToSUrl" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "repositoryUrl" character varying(512) NOT NULL DEFAULT 'https://github.com/misskey-dev/misskey'`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "feedbackUrl" character varying(512) DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "feedbackUrl"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "repositoryUrl"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ToSUrl"`);
+    }
+}
+exports.AddSomeUrls1557761316509 = AddSomeUrls1557761316509;
diff --git a/packages/backend/migration/1557932705754-ObjectStorageSetting.js b/packages/backend/migration/1557932705754-ObjectStorageSetting.js
new file mode 100644
index 0000000000..d798ac5ca6
--- /dev/null
+++ b/packages/backend/migration/1557932705754-ObjectStorageSetting.js
@@ -0,0 +1,29 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ObjectStorageSetting1557932705754 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "useObjectStorage" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBucket" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePrefix" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBaseUrl" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageEndpoint" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageRegion" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageAccessKey" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSecretKey" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePort" integer`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseSSL" boolean NOT NULL DEFAULT true`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseSSL"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePort"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSecretKey"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageAccessKey"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRegion"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageEndpoint"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBaseUrl"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePrefix"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBucket"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useObjectStorage"`);
+    }
+}
+exports.ObjectStorageSetting1557932705754 = ObjectStorageSetting1557932705754;
diff --git a/packages/backend/migration/1558072954435-PageLike.js b/packages/backend/migration/1558072954435-PageLike.js
new file mode 100644
index 0000000000..7038434971
--- /dev/null
+++ b/packages/backend/migration/1558072954435-PageLike.js
@@ -0,0 +1,21 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class PageLike1558072954435 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "page_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "pageId" character varying(32) NOT NULL, CONSTRAINT "PK_813f034843af992d3ae0f43c64c" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_0e61efab7f88dbb79c9166dbb4" ON "page_like" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa" ON "page_like" ("userId", "pageId") `);
+        await queryRunner.query(`ALTER TABLE "page" ADD "likedCount" integer NOT NULL DEFAULT 0`);
+        await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_cf8782626dced3176038176a847" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_cf8782626dced3176038176a847"`);
+        await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48"`);
+        await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "likedCount"`);
+        await queryRunner.query(`DROP INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa"`);
+        await queryRunner.query(`DROP INDEX "IDX_0e61efab7f88dbb79c9166dbb4"`);
+        await queryRunner.query(`DROP TABLE "page_like"`);
+    }
+}
+exports.PageLike1558072954435 = PageLike1558072954435;
diff --git a/packages/backend/migration/1558103093633-UserGroup.js b/packages/backend/migration/1558103093633-UserGroup.js
new file mode 100644
index 0000000000..9c1e511286
--- /dev/null
+++ b/packages/backend/migration/1558103093633-UserGroup.js
@@ -0,0 +1,39 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class UserGroup1558103093633 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "user_group" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "isPrivate" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c29fba6fe013ec8724378ce7c9" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_20e30aa35180e317e133d75316" ON "user_group" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3d6b372788ab01be58853003c9" ON "user_group" ("userId") `);
+        await queryRunner.query(`CREATE TABLE "user_group_joining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_15f2425885253c5507e1599cfe7" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23" ON "user_group_joining" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_67dc758bc0566985d1b3d39986" ON "user_group_joining" ("userGroupId") `);
+        await queryRunner.query(`ALTER TABLE "messaging_message" ADD "groupId" character varying(32)`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" ADD "reads" character varying(32) array NOT NULL DEFAULT '{}'::varchar[]`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" DROP NOT NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS 'The recipient user ID.'`);
+        await queryRunner.query(`CREATE INDEX "IDX_2c4be03b446884f9e9c502135b" ON "messaging_message" ("groupId") `);
+        await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_2c4be03b446884f9e9c502135be" FOREIGN KEY ("groupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_group" ADD CONSTRAINT "FK_3d6b372788ab01be58853003c93" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_67dc758bc0566985d1b3d399865" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_67dc758bc0566985d1b3d399865"`);
+        await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231"`);
+        await queryRunner.query(`ALTER TABLE "user_group" DROP CONSTRAINT "FK_3d6b372788ab01be58853003c93"`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_2c4be03b446884f9e9c502135be"`);
+        await queryRunner.query(`DROP INDEX "IDX_2c4be03b446884f9e9c502135b"`);
+        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS ''`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" SET NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "reads"`);
+        await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "groupId"`);
+        await queryRunner.query(`DROP INDEX "IDX_67dc758bc0566985d1b3d39986"`);
+        await queryRunner.query(`DROP INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23"`);
+        await queryRunner.query(`DROP TABLE "user_group_joining"`);
+        await queryRunner.query(`DROP INDEX "IDX_3d6b372788ab01be58853003c9"`);
+        await queryRunner.query(`DROP INDEX "IDX_20e30aa35180e317e133d75316"`);
+        await queryRunner.query(`DROP TABLE "user_group"`);
+    }
+}
+exports.UserGroup1558103093633 = UserGroup1558103093633;
diff --git a/packages/backend/migration/1558257926829-UserGroupInvite.js b/packages/backend/migration/1558257926829-UserGroupInvite.js
new file mode 100644
index 0000000000..e8a575e74f
--- /dev/null
+++ b/packages/backend/migration/1558257926829-UserGroupInvite.js
@@ -0,0 +1,23 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class UserGroupInvite1558257926829 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "user_group_invite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_3893884af0d3a5f4d01e7921a97" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_1039988afa3bf991185b277fe0" ON "user_group_invite" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e10924607d058004304611a436" ON "user_group_invite" ("userGroupId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78787741f9010886796f2320a4" ON "user_group_invite" ("userId", "userGroupId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `);
+        await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_1039988afa3bf991185b277fe03" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_e10924607d058004304611a436a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_e10924607d058004304611a436a"`);
+        await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_1039988afa3bf991185b277fe03"`);
+        await queryRunner.query(`DROP INDEX "IDX_d9ecaed8c6dc43f3592c229282"`);
+        await queryRunner.query(`DROP INDEX "IDX_78787741f9010886796f2320a4"`);
+        await queryRunner.query(`DROP INDEX "IDX_e10924607d058004304611a436"`);
+        await queryRunner.query(`DROP INDEX "IDX_1039988afa3bf991185b277fe0"`);
+        await queryRunner.query(`DROP TABLE "user_group_invite"`);
+    }
+}
+exports.UserGroupInvite1558257926829 = UserGroupInvite1558257926829;
diff --git a/packages/backend/migration/1558266512381-UserListJoining.js b/packages/backend/migration/1558266512381-UserListJoining.js
new file mode 100644
index 0000000000..b31f7bd3d1
--- /dev/null
+++ b/packages/backend/migration/1558266512381-UserListJoining.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class UserListJoining1558266512381 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_joining" ("userId", "userListId") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_90f7da835e4c10aca6853621e1"`);
+    }
+}
+exports.UserListJoining1558266512381 = UserListJoining1558266512381;
diff --git a/packages/backend/migration/1561706992953-webauthn.js b/packages/backend/migration/1561706992953-webauthn.js
new file mode 100644
index 0000000000..f05054f553
--- /dev/null
+++ b/packages/backend/migration/1561706992953-webauthn.js
@@ -0,0 +1,27 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class webauthn1561706992953 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "attestation_challenge" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "challenge" character varying(64) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "registrationChallenge" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_d0ba6786e093f1bcb497572a6b5" PRIMARY KEY ("id", "userId"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_f1a461a618fa1755692d0e0d59" ON "attestation_challenge" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_47efb914aed1f72dd39a306c7b" ON "attestation_challenge" ("challenge") `);
+        await queryRunner.query(`CREATE TABLE "user_security_key" ("id" character varying NOT NULL, "userId" character varying(32) NOT NULL, "publicKey" character varying NOT NULL, "lastUsed" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(30) NOT NULL, CONSTRAINT "PK_3e508571121ab39c5f85d10c166" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_ff9ca3b5f3ee3d0681367a9b44" ON "user_security_key" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0d7718e562dcedd0aa5cf2c9f7" ON "user_security_key" ("publicKey") `);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "securityKeysAvailable" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`ALTER TABLE "attestation_challenge" ADD CONSTRAINT "FK_f1a461a618fa1755692d0e0d592" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "user_security_key" ADD CONSTRAINT "FK_ff9ca3b5f3ee3d0681367a9b447" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_security_key" DROP CONSTRAINT "FK_ff9ca3b5f3ee3d0681367a9b447"`);
+        await queryRunner.query(`ALTER TABLE "attestation_challenge" DROP CONSTRAINT "FK_f1a461a618fa1755692d0e0d592"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "securityKeysAvailable"`);
+        await queryRunner.query(`DROP INDEX "IDX_0d7718e562dcedd0aa5cf2c9f7"`);
+        await queryRunner.query(`DROP INDEX "IDX_ff9ca3b5f3ee3d0681367a9b44"`);
+        await queryRunner.query(`DROP TABLE "user_security_key"`);
+        await queryRunner.query(`DROP INDEX "IDX_47efb914aed1f72dd39a306c7b"`);
+        await queryRunner.query(`DROP INDEX "IDX_f1a461a618fa1755692d0e0d59"`);
+        await queryRunner.query(`DROP TABLE "attestation_challenge"`);
+    }
+}
+exports.webauthn1561706992953 = webauthn1561706992953;
diff --git a/packages/backend/migration/1561873850023-ChartIndexes.js b/packages/backend/migration/1561873850023-ChartIndexes.js
new file mode 100644
index 0000000000..559cb70cb3
--- /dev/null
+++ b/packages/backend/migration/1561873850023-ChartIndexes.js
@@ -0,0 +1,199 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ChartIndexes1561873850023 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc" ON "__chart__active_users" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_15e91a03aeeac9dbccdf43fc06" ON "__chart__active_users" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_00ed5f86db1f7efafb1978bf21" ON "__chart__active_users" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_20f57cc8f142c131340ee16742" ON "__chart__active_users" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_c26e2c1cbb6e911e0554b27416" ON "__chart__active_users" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_13565815f618a1ff53886c5b28" ON "__chart__drive" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3fa0d0f17ca72e3dc80999a032" ON "__chart__drive" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7a170f67425e62a8fabb76c872" ON "__chart__drive" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6e1df243476e20cbf86572ecc0" ON "__chart__drive" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_06690fc959f1c9fdaf21928222" ON "__chart__drive" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_36cb699c49580d4e6c2e6159f9" ON "__chart__federation" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e447064455928cf627590ef527" ON "__chart__federation" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_76e87c7bfc5d925fcbba405d84" ON "__chart__federation" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_2d416e6af791a82e338c79d480" ON "__chart__federation" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e9cd07672b37d8966cf3709283" ON "__chart__federation" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_07747a1038c05f532a718fe1de" ON "__chart__hashtag" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_fcc181fb8283009c61cc4083ef" ON "__chart__hashtag" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_99a7d2faaef84a6f728d714ad6" ON "__chart__hashtag" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_49975586f50ed7b800fdd88fbd" ON "__chart__hashtag" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6d6f156ceefc6bc5f273a0e370" ON "__chart__hashtag" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6b8f34a1a64b06014b6fb66824" ON "__chart__instance" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_c12f0af4a66cdd30c2287ce8aa" ON "__chart__instance" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63" ON "__chart__instance" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d0a4f79af5a97b08f37b547197" ON "__chart__instance" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f5448d9633cff74208d850aabe" ON "__chart__instance" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a1efd3e0048a5f2793a47360dc" ON "__chart__network" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f8dd01baeded2ffa833e0a610a" ON "__chart__network" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7b5da130992ec9df96712d4290" ON "__chart__network" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_08fac0eb3b11f04c200c0b40dd" ON "__chart__network" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_9ff6944f01acb756fdc92d7563" ON "__chart__network" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_42eb716a37d381cdf566192b2b" ON "__chart__notes" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e69096589f11e3baa98ddd64d0" ON "__chart__notes" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7036f2957151588b813185c794" ON "__chart__notes" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0c9a159c5082cbeef3ca6706b5" ON "__chart__notes" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_924fc196c80ca24bae01dd37e4" ON "__chart__notes" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5f86db6492274e07c1a3cdf286" ON "__chart__per_user_drive" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_328f259961e60c4fa0bfcf55ca" ON "__chart__per_user_drive" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e496ca8096d28f6b9b509264dc" ON "__chart__per_user_drive" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53" ON "__chart__per_user_drive" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f2aeafde2ae6fbad38e857631b" ON "__chart__per_user_drive" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7af07790712aa3438ff6773f3b" ON "__chart__per_user_following" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f92dd6d03f8d994f29987f6214" ON "__chart__per_user_following" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_4b3593098b6edc9c5afe36b18b" ON "__chart__per_user_following" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f" ON "__chart__per_user_following" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_4db3b84c7be0d3464714f3e0b1" ON "__chart__per_user_following" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_84234bd1abb873f07329681c83" ON "__chart__per_user_notes" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_8d2cbbc8114d90d19b44d626b6" ON "__chart__per_user_notes" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_55bf20f366979f2436de99206b" ON "__chart__per_user_notes" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_046feeb12e9ef5f783f409866a" ON "__chart__per_user_notes" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f68a5ab958f9f5fa17a32ac23b" ON "__chart__per_user_notes" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f7bf4c62059764c2c2bb40fdab" ON "__chart__per_user_reaction" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_65633a106bce43fc7c5c30a5c7" ON "__chart__per_user_reaction" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_8cf3156fd7a6b15c43459c6e3b" ON "__chart__per_user_reaction" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_edeb73c09c3143a81bcb34d569" ON "__chart__per_user_reaction" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e316f01a6d24eb31db27f88262" ON "__chart__per_user_reaction" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0c641990ecf47d2545df4edb75" ON "__chart__test_grouped" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_2be7ec6cebddc14dc11e206686" ON "__chart__test_grouped" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_234dff3c0b56a6150b95431ab9" ON "__chart__test_grouped" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a5133470f4825902e170328ca5" ON "__chart__test_grouped" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_84e661abb7bd1e51b690d4b017" ON "__chart__test_grouped" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_437bab3c6061d90f6bb65fd2cc" ON "__chart__test_unique" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5c73bf61da4f6e6f15bae88ed1" ON "__chart__test_unique" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_bbfa573a8181018851ed0b6357" ON "__chart__test_unique" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d70c86baedc68326be11f9c0ce" ON "__chart__test_unique" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_66e1e1ecd2f29e57778af35b59" ON "__chart__test_unique" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_b070a906db04b44c67c6c2144d" ON "__chart__test" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_92255988735563f0fe4aba1f05" ON "__chart__test" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d41cce6aee1a50bfc062038f9b" ON "__chart__test" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_c5870993e25c3d5771f91f5003" ON "__chart__test" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f170de677ea75ad4533de2723e" ON "__chart__test" ("span", "date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_845254b3eaf708ae8a6cac3026" ON "__chart__users" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7c184198ecf66a8d3ecb253ab3" ON "__chart__users" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_ed9b95919c672a13008e9487ee" ON "__chart__users" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f091abb24193d50c653c6b77fc" ON "__chart__users" ("span", "date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a770a57c70e668cc61590c9161" ON "__chart__users" ("span", "date", "group") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_a770a57c70e668cc61590c9161"`);
+        await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`);
+        await queryRunner.query(`DROP INDEX "IDX_f091abb24193d50c653c6b77fc"`);
+        await queryRunner.query(`DROP INDEX "IDX_ed9b95919c672a13008e9487ee"`);
+        await queryRunner.query(`DROP INDEX "IDX_7c184198ecf66a8d3ecb253ab3"`);
+        await queryRunner.query(`DROP INDEX "IDX_845254b3eaf708ae8a6cac3026"`);
+        await queryRunner.query(`DROP INDEX "IDX_f170de677ea75ad4533de2723e"`);
+        await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`);
+        await queryRunner.query(`DROP INDEX "IDX_c5870993e25c3d5771f91f5003"`);
+        await queryRunner.query(`DROP INDEX "IDX_d41cce6aee1a50bfc062038f9b"`);
+        await queryRunner.query(`DROP INDEX "IDX_92255988735563f0fe4aba1f05"`);
+        await queryRunner.query(`DROP INDEX "IDX_b070a906db04b44c67c6c2144d"`);
+        await queryRunner.query(`DROP INDEX "IDX_66e1e1ecd2f29e57778af35b59"`);
+        await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`);
+        await queryRunner.query(`DROP INDEX "IDX_d70c86baedc68326be11f9c0ce"`);
+        await queryRunner.query(`DROP INDEX "IDX_bbfa573a8181018851ed0b6357"`);
+        await queryRunner.query(`DROP INDEX "IDX_5c73bf61da4f6e6f15bae88ed1"`);
+        await queryRunner.query(`DROP INDEX "IDX_437bab3c6061d90f6bb65fd2cc"`);
+        await queryRunner.query(`DROP INDEX "IDX_84e661abb7bd1e51b690d4b017"`);
+        await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`);
+        await queryRunner.query(`DROP INDEX "IDX_a5133470f4825902e170328ca5"`);
+        await queryRunner.query(`DROP INDEX "IDX_234dff3c0b56a6150b95431ab9"`);
+        await queryRunner.query(`DROP INDEX "IDX_2be7ec6cebddc14dc11e206686"`);
+        await queryRunner.query(`DROP INDEX "IDX_0c641990ecf47d2545df4edb75"`);
+        await queryRunner.query(`DROP INDEX "IDX_e316f01a6d24eb31db27f88262"`);
+        await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`);
+        await queryRunner.query(`DROP INDEX "IDX_edeb73c09c3143a81bcb34d569"`);
+        await queryRunner.query(`DROP INDEX "IDX_8cf3156fd7a6b15c43459c6e3b"`);
+        await queryRunner.query(`DROP INDEX "IDX_65633a106bce43fc7c5c30a5c7"`);
+        await queryRunner.query(`DROP INDEX "IDX_f7bf4c62059764c2c2bb40fdab"`);
+        await queryRunner.query(`DROP INDEX "IDX_f68a5ab958f9f5fa17a32ac23b"`);
+        await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`);
+        await queryRunner.query(`DROP INDEX "IDX_046feeb12e9ef5f783f409866a"`);
+        await queryRunner.query(`DROP INDEX "IDX_55bf20f366979f2436de99206b"`);
+        await queryRunner.query(`DROP INDEX "IDX_8d2cbbc8114d90d19b44d626b6"`);
+        await queryRunner.query(`DROP INDEX "IDX_84234bd1abb873f07329681c83"`);
+        await queryRunner.query(`DROP INDEX "IDX_4db3b84c7be0d3464714f3e0b1"`);
+        await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`);
+        await queryRunner.query(`DROP INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f"`);
+        await queryRunner.query(`DROP INDEX "IDX_4b3593098b6edc9c5afe36b18b"`);
+        await queryRunner.query(`DROP INDEX "IDX_f92dd6d03f8d994f29987f6214"`);
+        await queryRunner.query(`DROP INDEX "IDX_7af07790712aa3438ff6773f3b"`);
+        await queryRunner.query(`DROP INDEX "IDX_f2aeafde2ae6fbad38e857631b"`);
+        await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`);
+        await queryRunner.query(`DROP INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53"`);
+        await queryRunner.query(`DROP INDEX "IDX_e496ca8096d28f6b9b509264dc"`);
+        await queryRunner.query(`DROP INDEX "IDX_328f259961e60c4fa0bfcf55ca"`);
+        await queryRunner.query(`DROP INDEX "IDX_5f86db6492274e07c1a3cdf286"`);
+        await queryRunner.query(`DROP INDEX "IDX_924fc196c80ca24bae01dd37e4"`);
+        await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`);
+        await queryRunner.query(`DROP INDEX "IDX_0c9a159c5082cbeef3ca6706b5"`);
+        await queryRunner.query(`DROP INDEX "IDX_7036f2957151588b813185c794"`);
+        await queryRunner.query(`DROP INDEX "IDX_e69096589f11e3baa98ddd64d0"`);
+        await queryRunner.query(`DROP INDEX "IDX_42eb716a37d381cdf566192b2b"`);
+        await queryRunner.query(`DROP INDEX "IDX_9ff6944f01acb756fdc92d7563"`);
+        await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`);
+        await queryRunner.query(`DROP INDEX "IDX_08fac0eb3b11f04c200c0b40dd"`);
+        await queryRunner.query(`DROP INDEX "IDX_7b5da130992ec9df96712d4290"`);
+        await queryRunner.query(`DROP INDEX "IDX_f8dd01baeded2ffa833e0a610a"`);
+        await queryRunner.query(`DROP INDEX "IDX_a1efd3e0048a5f2793a47360dc"`);
+        await queryRunner.query(`DROP INDEX "IDX_f5448d9633cff74208d850aabe"`);
+        await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`);
+        await queryRunner.query(`DROP INDEX "IDX_d0a4f79af5a97b08f37b547197"`);
+        await queryRunner.query(`DROP INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63"`);
+        await queryRunner.query(`DROP INDEX "IDX_c12f0af4a66cdd30c2287ce8aa"`);
+        await queryRunner.query(`DROP INDEX "IDX_6b8f34a1a64b06014b6fb66824"`);
+        await queryRunner.query(`DROP INDEX "IDX_6d6f156ceefc6bc5f273a0e370"`);
+        await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`);
+        await queryRunner.query(`DROP INDEX "IDX_49975586f50ed7b800fdd88fbd"`);
+        await queryRunner.query(`DROP INDEX "IDX_99a7d2faaef84a6f728d714ad6"`);
+        await queryRunner.query(`DROP INDEX "IDX_fcc181fb8283009c61cc4083ef"`);
+        await queryRunner.query(`DROP INDEX "IDX_07747a1038c05f532a718fe1de"`);
+        await queryRunner.query(`DROP INDEX "IDX_e9cd07672b37d8966cf3709283"`);
+        await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`);
+        await queryRunner.query(`DROP INDEX "IDX_2d416e6af791a82e338c79d480"`);
+        await queryRunner.query(`DROP INDEX "IDX_76e87c7bfc5d925fcbba405d84"`);
+        await queryRunner.query(`DROP INDEX "IDX_e447064455928cf627590ef527"`);
+        await queryRunner.query(`DROP INDEX "IDX_36cb699c49580d4e6c2e6159f9"`);
+        await queryRunner.query(`DROP INDEX "IDX_06690fc959f1c9fdaf21928222"`);
+        await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`);
+        await queryRunner.query(`DROP INDEX "IDX_6e1df243476e20cbf86572ecc0"`);
+        await queryRunner.query(`DROP INDEX "IDX_7a170f67425e62a8fabb76c872"`);
+        await queryRunner.query(`DROP INDEX "IDX_3fa0d0f17ca72e3dc80999a032"`);
+        await queryRunner.query(`DROP INDEX "IDX_13565815f618a1ff53886c5b28"`);
+        await queryRunner.query(`DROP INDEX "IDX_c26e2c1cbb6e911e0554b27416"`);
+        await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`);
+        await queryRunner.query(`DROP INDEX "IDX_20f57cc8f142c131340ee16742"`);
+        await queryRunner.query(`DROP INDEX "IDX_00ed5f86db1f7efafb1978bf21"`);
+        await queryRunner.query(`DROP INDEX "IDX_15e91a03aeeac9dbccdf43fc06"`);
+        await queryRunner.query(`DROP INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc"`);
+        await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`);
+        await queryRunner.query(`DROP INDEX "IDX_88937d94d7443d9a99a76fa5c0"`);
+        await queryRunner.query(`DROP INDEX "IDX_54ebcb6d27222913b908d56fd8"`);
+        await queryRunner.query(`DROP INDEX "IDX_796a8c03959361f97dc2be1d5c"`);
+        await queryRunner.query(`DROP INDEX "IDX_25dfc71b0369b003a4cd434d0b"`);
+        await queryRunner.query(`DROP INDEX "IDX_51c063b6a133a9cb87145450f5"`);
+        await queryRunner.query(`DROP INDEX "IDX_fa99d777623947a5b05f394cae"`);
+        await queryRunner.query(`DROP INDEX "IDX_315c779174fe8247ab324f036e"`);
+        await queryRunner.query(`DROP INDEX "IDX_c5d46cbfda48b1c33ed852e21b"`);
+        await queryRunner.query(`DROP INDEX "IDX_8cb40cfc8f3c28261e6f887b03"`);
+    }
+}
+exports.ChartIndexes1561873850023 = ChartIndexes1561873850023;
diff --git a/packages/backend/migration/1562422242907-PasswordLessLogin.js b/packages/backend/migration/1562422242907-PasswordLessLogin.js
new file mode 100644
index 0000000000..bed879ade1
--- /dev/null
+++ b/packages/backend/migration/1562422242907-PasswordLessLogin.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class PasswordLessLogin1562422242907 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "usePasswordLessLogin" boolean DEFAULT false NOT NULL`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "usePasswordLessLogin"`);
+    }
+}
+exports.PasswordLessLogin1562422242907 = PasswordLessLogin1562422242907;
diff --git a/packages/backend/migration/1562444565093-PinnedPage.js b/packages/backend/migration/1562444565093-PinnedPage.js
new file mode 100644
index 0000000000..f0aac3a4b3
--- /dev/null
+++ b/packages/backend/migration/1562444565093-PinnedPage.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class PinnedPage1562444565093 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "pinnedPageId" character varying(32)`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27" UNIQUE ("pinnedPageId")`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27" FOREIGN KEY ("pinnedPageId") REFERENCES "page"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "pinnedPageId"`);
+    }
+}
+exports.PinnedPage1562444565093 = PinnedPage1562444565093;
diff --git a/packages/backend/migration/1562448332510-PageTitleHideOption.js b/packages/backend/migration/1562448332510-PageTitleHideOption.js
new file mode 100644
index 0000000000..238b742262
--- /dev/null
+++ b/packages/backend/migration/1562448332510-PageTitleHideOption.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class PageTitleHideOption1562448332510 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "page" ADD "hideTitleWhenPinned" boolean NOT NULL DEFAULT false`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "hideTitleWhenPinned"`);
+    }
+}
+exports.PageTitleHideOption1562448332510 = PageTitleHideOption1562448332510;
diff --git a/packages/backend/migration/1562869971568-ModerationLog.js b/packages/backend/migration/1562869971568-ModerationLog.js
new file mode 100644
index 0000000000..c8953c513e
--- /dev/null
+++ b/packages/backend/migration/1562869971568-ModerationLog.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ModerationLog1562869971568 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "moderation_log" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "type" character varying(128) NOT NULL, "info" jsonb NOT NULL, CONSTRAINT "PK_d0adca6ecfd068db83e4526cc26" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_a08ad074601d204e0f69da9a95" ON "moderation_log" ("userId") `);
+        await queryRunner.query(`ALTER TABLE "moderation_log" ADD CONSTRAINT "FK_a08ad074601d204e0f69da9a954" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "moderation_log" DROP CONSTRAINT "FK_a08ad074601d204e0f69da9a954"`);
+        await queryRunner.query(`DROP INDEX "IDX_a08ad074601d204e0f69da9a95"`);
+        await queryRunner.query(`DROP TABLE "moderation_log"`);
+    }
+}
+exports.ModerationLog1562869971568 = ModerationLog1562869971568;
diff --git a/packages/backend/migration/1563757595828-UsedUsername.js b/packages/backend/migration/1563757595828-UsedUsername.js
new file mode 100644
index 0000000000..0e59fdd45b
--- /dev/null
+++ b/packages/backend/migration/1563757595828-UsedUsername.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class UsedUsername1563757595828 {
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "used_username" ("username" character varying(128) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_78fd79d2d24c6ac2f4cc9a31a5d" PRIMARY KEY ("username"))`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP TABLE "used_username"`);
+    }
+}
+exports.UsedUsername1563757595828 = UsedUsername1563757595828;
diff --git a/packages/backend/migration/1565634203341-room.js b/packages/backend/migration/1565634203341-room.js
new file mode 100644
index 0000000000..49ca5ea502
--- /dev/null
+++ b/packages/backend/migration/1565634203341-room.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class room1565634203341 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "room"`);
+    }
+}
+exports.room1565634203341 = room1565634203341;
diff --git a/packages/backend/migration/1571220798684-CustomEmojiCategory.js b/packages/backend/migration/1571220798684-CustomEmojiCategory.js
new file mode 100644
index 0000000000..c67c437dec
--- /dev/null
+++ b/packages/backend/migration/1571220798684-CustomEmojiCategory.js
@@ -0,0 +1,11 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class CustomEmojiCategory1571220798684 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "emoji" ADD "category" character varying(128)`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "category"`, undefined);
+    }
+}
+exports.CustomEmojiCategory1571220798684 = CustomEmojiCategory1571220798684;
diff --git a/packages/backend/migration/1572760203493-nodeinfo.js b/packages/backend/migration/1572760203493-nodeinfo.js
new file mode 100644
index 0000000000..a3ca5d7180
--- /dev/null
+++ b/packages/backend/migration/1572760203493-nodeinfo.js
@@ -0,0 +1,27 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class nodeinfo1572760203493 {
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "system"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "softwareName" character varying(64) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "softwareVersion" character varying(64) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "openRegistrations" boolean DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "name" character varying(256) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "description" character varying(4096) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "maintainerName" character varying(128) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "maintainerEmail" character varying(256) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "infoUpdatedAt" TIMESTAMP WITH TIME ZONE`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "infoUpdatedAt"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "maintainerEmail"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "maintainerName"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "description"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "name"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "openRegistrations"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "softwareVersion"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "softwareName"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "system" character varying(64)`, undefined);
+    }
+}
+exports.nodeinfo1572760203493 = nodeinfo1572760203493;
diff --git a/packages/backend/migration/1576269851876-TalkFederationId.js b/packages/backend/migration/1576269851876-TalkFederationId.js
new file mode 100644
index 0000000000..63a8f8a9a3
--- /dev/null
+++ b/packages/backend/migration/1576269851876-TalkFederationId.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class TalkFederationId1576269851876 {
+    constructor() {
+        this.name = 'TalkFederationId1576269851876';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "messaging_message" ADD "uri" character varying(512)`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "uri"`, undefined);
+    }
+}
+exports.TalkFederationId1576269851876 = TalkFederationId1576269851876;
diff --git a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js
new file mode 100644
index 0000000000..867ae3d6e7
--- /dev/null
+++ b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ProxyRemoteFiles1576869585998 {
+    constructor() {
+        this.name = 'ProxyRemoteFiles1576869585998';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT false`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`, undefined);
+    }
+}
+exports.ProxyRemoteFiles1576869585998 = ProxyRemoteFiles1576869585998;
diff --git a/packages/backend/migration/1579267006611-v12.js b/packages/backend/migration/1579267006611-v12.js
new file mode 100644
index 0000000000..ccc5247736
--- /dev/null
+++ b/packages/backend/migration/1579267006611-v12.js
@@ -0,0 +1,34 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v121579267006611 {
+    constructor() {
+        this.name = 'v121579267006611';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "announcement" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "text" character varying(8192) NOT NULL, "title" character varying(256) NOT NULL, "imageUrl" character varying(1024), CONSTRAINT "PK_e0ef0550174fd1099a308fd18a0" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_118ec703e596086fc4515acb39" ON "announcement" ("createdAt") `, undefined);
+        await queryRunner.query(`CREATE TABLE "announcement_read" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "announcementId" character varying(32) NOT NULL, CONSTRAINT "PK_4b90ad1f42681d97b2683890c5e" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_8288151386172b8109f7239ab2" ON "announcement_read" ("userId") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf" ON "announcement_read" ("announcementId") `, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_924fa71815cfa3941d003702a0" ON "announcement_read" ("userId", "announcementId") `, undefined);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isVerified"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "announcements"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableEmojiReaction"`, undefined);
+        await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_8288151386172b8109f7239ab28" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe" FOREIGN KEY ("announcementId") REFERENCES "announcement"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe"`, undefined);
+        await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_8288151386172b8109f7239ab28"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableEmojiReaction" boolean NOT NULL DEFAULT true`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "announcements" jsonb NOT NULL DEFAULT '[]'`, undefined);
+        await queryRunner.query(`ALTER TABLE "user" ADD "isVerified" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_924fa71815cfa3941d003702a0"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_8288151386172b8109f7239ab2"`, undefined);
+        await queryRunner.query(`DROP TABLE "announcement_read"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_118ec703e596086fc4515acb39"`, undefined);
+        await queryRunner.query(`DROP TABLE "announcement"`, undefined);
+    }
+}
+exports.v121579267006611 = v121579267006611;
diff --git a/packages/backend/migration/1579270193251-v12-2.js b/packages/backend/migration/1579270193251-v12-2.js
new file mode 100644
index 0000000000..7f67645aaf
--- /dev/null
+++ b/packages/backend/migration/1579270193251-v12-2.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v1221579270193251 {
+    constructor() {
+        this.name = 'v1221579270193251';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "announcement_read" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "announcement_read" DROP COLUMN "createdAt"`, undefined);
+    }
+}
+exports.v1221579270193251 = v1221579270193251;
diff --git a/packages/backend/migration/1579282808087-v12-3.js b/packages/backend/migration/1579282808087-v12-3.js
new file mode 100644
index 0000000000..80fa234a54
--- /dev/null
+++ b/packages/backend/migration/1579282808087-v12-3.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v1231579282808087 {
+    constructor() {
+        this.name = 'v1231579282808087';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "announcement" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "updatedAt"`, undefined);
+    }
+}
+exports.v1231579282808087 = v1231579282808087;
diff --git a/packages/backend/migration/1579544426412-v12-4.js b/packages/backend/migration/1579544426412-v12-4.js
new file mode 100644
index 0000000000..ef2b804f58
--- /dev/null
+++ b/packages/backend/migration/1579544426412-v12-4.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v1241579544426412 {
+    constructor() {
+        this.name = 'v1241579544426412';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "notification" ADD "followRequestId" character varying(32)`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_bd7fab507621e635b32cd31892c" FOREIGN KEY ("followRequestId") REFERENCES "follow_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_bd7fab507621e635b32cd31892c"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "followRequestId"`, undefined);
+    }
+}
+exports.v1241579544426412 = v1241579544426412;
diff --git a/packages/backend/migration/1579977526288-v12-5.js b/packages/backend/migration/1579977526288-v12-5.js
new file mode 100644
index 0000000000..1d2ed09664
--- /dev/null
+++ b/packages/backend/migration/1579977526288-v12-5.js
@@ -0,0 +1,54 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v1251579977526288 {
+    constructor() {
+        this.name = 'v1251579977526288';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "clip" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "isPublic" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_f0685dac8d4dd056d7255670b75" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_2b5ec6c574d6802c94c80313fb" ON "clip" ("userId") `, undefined);
+        await queryRunner.query(`CREATE TABLE "clip_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_e94cda2f40a99b57e032a1a738b" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_a012eaf5c87c65da1deb5fdbfa" ON "clip_note" ("noteId") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_ebe99317bbbe9968a0c6f579ad" ON "clip_note" ("clipId") `, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6fc0ec357d55a18646262fdfff" ON "clip_note" ("noteId", "clipId") `, undefined);
+        await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'list')`, undefined);
+        await queryRunner.query(`CREATE TABLE "antenna" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "src" "antenna_src_enum" NOT NULL, "userListId" character varying(32), "keywords" jsonb NOT NULL DEFAULT '[]', "withFile" boolean NOT NULL, "expression" character varying(2048), "notify" boolean NOT NULL, "hasNewNote" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_c170b99775e1dccca947c9f2d5f" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_6446c571a0e8d0f05f01c78909" ON "antenna" ("userId") `, undefined);
+        await queryRunner.query(`CREATE TABLE "antenna_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "antennaId" character varying(32) NOT NULL, CONSTRAINT "PK_fb28d94d0989a3872df19fd6ef8" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_bd0397be22147e17210940e125" ON "antenna_note" ("noteId") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_0d775946662d2575dfd2068a5f" ON "antenna_note" ("antennaId") `, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_335a0bf3f904406f9ef3dd51c2" ON "antenna_note" ("noteId", "antennaId") `, undefined);
+        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "geo"`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip" ADD CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_6446c571a0e8d0f05f01c789096" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_0d775946662d2575dfd2068a5f5"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_bd0397be22147e17210940e125b"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_6446c571a0e8d0f05f01c789096"`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf"`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3"`, undefined);
+        await queryRunner.query(`ALTER TABLE "clip" DROP CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2"`, undefined);
+        await queryRunner.query(`ALTER TABLE "note" ADD "geo" jsonb`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_335a0bf3f904406f9ef3dd51c2"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_0d775946662d2575dfd2068a5f"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_bd0397be22147e17210940e125"`, undefined);
+        await queryRunner.query(`DROP TABLE "antenna_note"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_6446c571a0e8d0f05f01c78909"`, undefined);
+        await queryRunner.query(`DROP TABLE "antenna"`, undefined);
+        await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_6fc0ec357d55a18646262fdfff"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_ebe99317bbbe9968a0c6f579ad"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_a012eaf5c87c65da1deb5fdbfa"`, undefined);
+        await queryRunner.query(`DROP TABLE "clip_note"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_2b5ec6c574d6802c94c80313fb"`, undefined);
+        await queryRunner.query(`DROP TABLE "clip"`, undefined);
+    }
+}
+exports.v1251579977526288 = v1251579977526288;
diff --git a/packages/backend/migration/1579993013959-v12-6.js b/packages/backend/migration/1579993013959-v12-6.js
new file mode 100644
index 0000000000..e6c3a40a34
--- /dev/null
+++ b/packages/backend/migration/1579993013959-v12-6.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v1261579993013959 {
+    constructor() {
+        this.name = 'v1261579993013959';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "hasNewNote"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" ADD "read" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON "antenna_note" ("read") `, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_9937ea48d7ae97ffb4f3f063a4"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna_note" DROP COLUMN "read"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "hasNewNote" boolean NOT NULL DEFAULT false`, undefined);
+    }
+}
+exports.v1261579993013959 = v1261579993013959;
diff --git a/packages/backend/migration/1580069531114-v12-7.js b/packages/backend/migration/1580069531114-v12-7.js
new file mode 100644
index 0000000000..7915603b8f
--- /dev/null
+++ b/packages/backend/migration/1580069531114-v12-7.js
@@ -0,0 +1,24 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v1271580069531114 {
+    constructor() {
+        this.name = 'v1271580069531114';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "caseSensitive" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined);
+        await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list')`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined);
+        await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'list')`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined);
+        await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
+        await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO  "antenna_src_enum"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "caseSensitive"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
+    }
+}
+exports.v1271580069531114 = v1271580069531114;
diff --git a/packages/backend/migration/1580148575182-v12-8.js b/packages/backend/migration/1580148575182-v12-8.js
new file mode 100644
index 0000000000..43fbc70e4b
--- /dev/null
+++ b/packages/backend/migration/1580148575182-v12-8.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v1281580148575182 {
+    constructor() {
+        this.name = 'v1281580148575182';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_ec5c201576192ba8904c345c5cc"`, undefined);
+        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "appId"`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note" ADD "appId" character varying(32)`, undefined);
+        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_ec5c201576192ba8904c345c5cc" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, undefined);
+    }
+}
+exports.v1281580148575182 = v1281580148575182;
diff --git a/packages/backend/migration/1580154400017-v12-9.js b/packages/backend/migration/1580154400017-v12-9.js
new file mode 100644
index 0000000000..f47611868f
--- /dev/null
+++ b/packages/backend/migration/1580154400017-v12-9.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v1291580154400017 {
+    constructor() {
+        this.name = 'v1291580154400017';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "withReplies" boolean NOT NULL DEFAULT false`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "withReplies"`, undefined);
+    }
+}
+exports.v1291580154400017 = v1291580154400017;
diff --git a/packages/backend/migration/1580276619901-v12-10.js b/packages/backend/migration/1580276619901-v12-10.js
new file mode 100644
index 0000000000..301d0ceb91
--- /dev/null
+++ b/packages/backend/migration/1580276619901-v12-10.js
@@ -0,0 +1,19 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v12101580276619901 {
+    constructor() {
+        this.name = 'v12101580276619901';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`TRUNCATE TABLE "notification"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined);
+        await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD "type" "notification_type_enum" NOT NULL`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined);
+        await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD "type" character varying(32) NOT NULL`, undefined);
+    }
+}
+exports.v12101580276619901 = v12101580276619901;
diff --git a/packages/backend/migration/1580331224276-v12-11.js b/packages/backend/migration/1580331224276-v12-11.js
new file mode 100644
index 0000000000..fb5124baa5
--- /dev/null
+++ b/packages/backend/migration/1580331224276-v12-11.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v12111580331224276 {
+    constructor() {
+        this.name = 'v12111580331224276';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "isMarkedAsClosed"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "isSuspended" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_34500da2e38ac393f7bb6b299c"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "isSuspended"`, undefined);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "isMarkedAsClosed" boolean NOT NULL DEFAULT false`, undefined);
+    }
+}
+exports.v12111580331224276 = v12111580331224276;
diff --git a/packages/backend/migration/1580508795118-v12-12.js b/packages/backend/migration/1580508795118-v12-12.js
new file mode 100644
index 0000000000..eb70ba3cef
--- /dev/null
+++ b/packages/backend/migration/1580508795118-v12-12.js
@@ -0,0 +1,46 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v12121580508795118 {
+    constructor() {
+        this.name = 'v12121580508795118';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitter"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterAccessToken"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterAccessTokenSecret"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterUserId"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterScreenName"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "github"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubAccessToken"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubId"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubLogin"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discord"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordAccessToken"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordRefreshToken"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordExpiresDate"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordId"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordUsername"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordDiscriminator"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "integrations" jsonb NOT NULL DEFAULT '{}'`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "integrations"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordDiscriminator" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordUsername" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordId" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordExpiresDate" character varying(64)`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordRefreshToken" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordAccessToken" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "discord" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubLogin" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubId" character varying(64)`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubAccessToken" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "github" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterScreenName" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterUserId" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterAccessTokenSecret" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterAccessToken" character varying(64) DEFAULT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitter" boolean NOT NULL DEFAULT false`, undefined);
+    }
+}
+exports.v12121580508795118 = v12121580508795118;
diff --git a/packages/backend/migration/1580543501339-v12-13.js b/packages/backend/migration/1580543501339-v12-13.js
new file mode 100644
index 0000000000..7dae7178b1
--- /dev/null
+++ b/packages/backend/migration/1580543501339-v12-13.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v12131580543501339 {
+    constructor() {
+        this.name = 'v12131580543501339';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE INDEX "IDX_NOTE_TAGS" ON "note" USING gin ("tags")`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_NOTE_TAGS"`, undefined);
+    }
+}
+exports.v12131580543501339 = v12131580543501339;
diff --git a/packages/backend/migration/1580864313253-v12-14.js b/packages/backend/migration/1580864313253-v12-14.js
new file mode 100644
index 0000000000..b1939c51b5
--- /dev/null
+++ b/packages/backend/migration/1580864313253-v12-14.js
@@ -0,0 +1,20 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class v12141580864313253 {
+    constructor() {
+        this.name = 'v12141580864313253';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "proxyAccount" TO "proxyAccountId"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(32)`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad" FOREIGN KEY ("proxyAccountId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(128)`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "proxyAccountId" TO "proxyAccount"`, undefined);
+    }
+}
+exports.v12141580864313253 = v12141580864313253;
diff --git a/packages/backend/migration/1581526429287-user-group-invitation.js b/packages/backend/migration/1581526429287-user-group-invitation.js
new file mode 100644
index 0000000000..80946c94ac
--- /dev/null
+++ b/packages/backend/migration/1581526429287-user-group-invitation.js
@@ -0,0 +1,38 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class userGroupInvitation1581526429287 {
+    constructor() {
+        this.name = 'userGroupInvitation1581526429287';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "user_group_invitation" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_160c63ec02bf23f6a5c5e8140d6" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_bfbc6305547539369fe73eb144" ON "user_group_invitation" ("userId") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_5cc8c468090e129857e9fecce5" ON "user_group_invitation" ("userGroupId") `, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e9793f65f504e5a31fbaedbf2f" ON "user_group_invitation" ("userId", "userGroupId") `, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD "userGroupInvitationId" character varying(32)`, undefined);
+        await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined);
+        await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined);
+        await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD CONSTRAINT "FK_bfbc6305547539369fe73eb144a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD CONSTRAINT "FK_5cc8c468090e129857e9fecce5a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_8fe87814e978053a53b1beb7e98" FOREIGN KEY ("userGroupInvitationId") REFERENCES "user_group_invitation"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_8fe87814e978053a53b1beb7e98"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP CONSTRAINT "FK_5cc8c468090e129857e9fecce5a"`, undefined);
+        await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP CONSTRAINT "FK_bfbc6305547539369fe73eb144a"`, undefined);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined);
+        await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined);
+        await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined);
+        await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO  "notification_type_enum"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "userGroupInvitationId"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_e9793f65f504e5a31fbaedbf2f"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_5cc8c468090e129857e9fecce5"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_bfbc6305547539369fe73eb144"`, undefined);
+        await queryRunner.query(`DROP TABLE "user_group_invitation"`, undefined);
+    }
+}
+exports.userGroupInvitation1581526429287 = userGroupInvitation1581526429287;
diff --git a/packages/backend/migration/1581695816408-user-group-antenna.js b/packages/backend/migration/1581695816408-user-group-antenna.js
new file mode 100644
index 0000000000..307b5b0579
--- /dev/null
+++ b/packages/backend/migration/1581695816408-user-group-antenna.js
@@ -0,0 +1,28 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class userGroupAntenna1581695816408 {
+    constructor() {
+        this.name = 'userGroupAntenna1581695816408';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "userGroupJoiningId" character varying(32)`, undefined);
+        await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined);
+        await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list', 'group')`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined);
+        await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb" FOREIGN KEY ("userGroupJoiningId") REFERENCES "user_group_joining"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying array NOT NULL DEFAULT '{}'`, undefined);
+        await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list')`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined);
+        await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
+        await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO  "antenna_src_enum"`, undefined);
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "userGroupJoiningId"`, undefined);
+    }
+}
+exports.userGroupAntenna1581695816408 = userGroupAntenna1581695816408;
diff --git a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js
new file mode 100644
index 0000000000..8029ef1399
--- /dev/null
+++ b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class driveUserFolderIdIndex1581708415836 {
+    constructor() {
+        this.name = 'driveUserFolderIdIndex1581708415836';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE INDEX "IDX_55720b33a61a7c806a8215b825" ON "drive_file" ("userId", "folderId", "id") `, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_55720b33a61a7c806a8215b825"`, undefined);
+    }
+}
+exports.driveUserFolderIdIndex1581708415836 = driveUserFolderIdIndex1581708415836;
diff --git a/packages/backend/migration/1581979837262-promo.js b/packages/backend/migration/1581979837262-promo.js
new file mode 100644
index 0000000000..d2c5bac17d
--- /dev/null
+++ b/packages/backend/migration/1581979837262-promo.js
@@ -0,0 +1,28 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class promo1581979837262 {
+    constructor() {
+        this.name = 'promo1581979837262';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "promo_note" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "REL_e263909ca4fe5d57f8d4230dd5" UNIQUE ("noteId"), CONSTRAINT "PK_e263909ca4fe5d57f8d4230dd5c" PRIMARY KEY ("noteId"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_83f0862e9bae44af52ced7099e" ON "promo_note" ("userId") `, undefined);
+        await queryRunner.query(`CREATE TABLE "promo_read" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_61917c1541002422b703318b7c9" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_9657d55550c3d37bfafaf7d4b0" ON "promo_read" ("userId") `, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2882b8a1a07c7d281a98b6db16" ON "promo_read" ("userId", "noteId") `, undefined);
+        await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4"`, undefined);
+        await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05"`, undefined);
+        await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_2882b8a1a07c7d281a98b6db16"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_9657d55550c3d37bfafaf7d4b0"`, undefined);
+        await queryRunner.query(`DROP TABLE "promo_read"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_83f0862e9bae44af52ced7099e"`, undefined);
+        await queryRunner.query(`DROP TABLE "promo_note"`, undefined);
+    }
+}
+exports.promo1581979837262 = promo1581979837262;
diff --git a/packages/backend/migration/1582019042083-featured-injecttion.js b/packages/backend/migration/1582019042083-featured-injecttion.js
new file mode 100644
index 0000000000..9779e2a08f
--- /dev/null
+++ b/packages/backend/migration/1582019042083-featured-injecttion.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class featuredInjecttion1582019042083 {
+    constructor() {
+        this.name = 'featuredInjecttion1582019042083';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "injectFeaturedNote" boolean NOT NULL DEFAULT true`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "injectFeaturedNote"`, undefined);
+    }
+}
+exports.featuredInjecttion1582019042083 = featuredInjecttion1582019042083;
diff --git a/packages/backend/migration/1582210532752-antenna-exclude.js b/packages/backend/migration/1582210532752-antenna-exclude.js
new file mode 100644
index 0000000000..cc095b4b59
--- /dev/null
+++ b/packages/backend/migration/1582210532752-antenna-exclude.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class antennaExclude1582210532752 {
+    constructor() {
+        this.name = 'antennaExclude1582210532752';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeKeywords" jsonb NOT NULL DEFAULT '[]'`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords"`, undefined);
+    }
+}
+exports.antennaExclude1582210532752 = antennaExclude1582210532752;
diff --git a/packages/backend/migration/1582875306439-note-reaction-length.js b/packages/backend/migration/1582875306439-note-reaction-length.js
new file mode 100644
index 0000000000..bdee1ef14f
--- /dev/null
+++ b/packages/backend/migration/1582875306439-note-reaction-length.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class noteReactionLength1582875306439 {
+    constructor() {
+        this.name = 'noteReactionLength1582875306439';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(128)`, undefined);
+    }
+}
+exports.noteReactionLength1582875306439 = noteReactionLength1582875306439;
diff --git a/packages/backend/migration/1585361548360-miauth.js b/packages/backend/migration/1585361548360-miauth.js
new file mode 100644
index 0000000000..c1a695dd27
--- /dev/null
+++ b/packages/backend/migration/1585361548360-miauth.js
@@ -0,0 +1,36 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class miauth1585361548360 {
+    constructor() {
+        this.name = 'miauth1585361548360';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "access_token" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD "session" character varying(128) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD "name" character varying(128) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD "description" character varying(512) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD "iconUrl" character varying(512) DEFAULT null`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD "permission" character varying(64) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD "fetched" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP NOT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET DEFAULT null`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_bf3a053c07d9fb5d87317c56ee" ON "access_token" ("session") `, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_bf3a053c07d9fb5d87317c56ee"`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET NOT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "fetched"`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "permission"`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "iconUrl"`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "description"`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "name"`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "session"`, undefined);
+        await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "lastUsedAt"`, undefined);
+    }
+}
+exports.miauth1585361548360 = miauth1585361548360;
diff --git a/packages/backend/migration/1585385921215-custom-notification.js b/packages/backend/migration/1585385921215-custom-notification.js
new file mode 100644
index 0000000000..7f94c741a5
--- /dev/null
+++ b/packages/backend/migration/1585385921215-custom-notification.js
@@ -0,0 +1,48 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class customNotification1585385921215 {
+    constructor() {
+        this.name = 'customNotification1585385921215';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "notification" ADD "customBody" character varying(2048)`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD "customHeader" character varying(256)`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD "customIcon" character varying(1024)`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD "appAccessTokenId" character varying(32)`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" DROP NOT NULL`, undefined);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS 'The ID of sender user of the Notification.'`, undefined);
+        await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined);
+        await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined);
+        await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71" ON "notification" ("notifierId") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_33f33cc8ef29d805a97ff4628b" ON "notification" ("type") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_080ab397c379af09b9d2169e5b" ON "notification" ("isRead") `, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c" ON "notification" ("appAccessTokenId") `, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9" FOREIGN KEY ("appAccessTokenId") REFERENCES "access_token"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_080ab397c379af09b9d2169e5b"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_33f33cc8ef29d805a97ff4628b"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71"`, undefined);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined);
+        await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined);
+        await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined);
+        await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO  "notification_type_enum"`, undefined);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS ''`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" SET NOT NULL`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "appAccessTokenId"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customIcon"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customHeader"`, undefined);
+        await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customBody"`, undefined);
+    }
+}
+exports.customNotification1585385921215 = customNotification1585385921215;
diff --git a/packages/backend/migration/1585772678853-ap-url.js b/packages/backend/migration/1585772678853-ap-url.js
new file mode 100644
index 0000000000..f7c1f87b80
--- /dev/null
+++ b/packages/backend/migration/1585772678853-ap-url.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class apUrl1585772678853 {
+    constructor() {
+        this.name = 'apUrl1585772678853';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note" ADD "url" character varying(512)`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "url"`, undefined);
+    }
+}
+exports.apUrl1585772678853 = apUrl1585772678853;
diff --git a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js
new file mode 100644
index 0000000000..9fcef0c90d
--- /dev/null
+++ b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class AddObjectStorageUseProxy1586624197029 {
+    constructor() {
+        this.name = 'AddObjectStorageUseProxy1586624197029';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseProxy" boolean NOT NULL DEFAULT true`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseProxy"`, undefined);
+    }
+}
+exports.AddObjectStorageUseProxy1586624197029 = AddObjectStorageUseProxy1586624197029;
diff --git a/packages/backend/migration/1586641139527-remote-reaction.js b/packages/backend/migration/1586641139527-remote-reaction.js
new file mode 100644
index 0000000000..a9d362474b
--- /dev/null
+++ b/packages/backend/migration/1586641139527-remote-reaction.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class remoteReaction1586641139527 {
+    constructor() {
+        this.name = 'remoteReaction1586641139527';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(260)`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined);
+    }
+}
+exports.remoteReaction1586641139527 = remoteReaction1586641139527;
diff --git a/packages/backend/migration/1586708940386-pageAiScript.js b/packages/backend/migration/1586708940386-pageAiScript.js
new file mode 100644
index 0000000000..089ff6e27a
--- /dev/null
+++ b/packages/backend/migration/1586708940386-pageAiScript.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class pageAiScript1586708940386 {
+    constructor() {
+        this.name = 'pageAiScript1586708940386';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "page" ADD "script" character varying(16384) NOT NULL DEFAULT ''`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "script"`, undefined);
+    }
+}
+exports.pageAiScript1586708940386 = pageAiScript1586708940386;
diff --git a/packages/backend/migration/1588044505511-hCaptcha.js b/packages/backend/migration/1588044505511-hCaptcha.js
new file mode 100644
index 0000000000..9b2524afba
--- /dev/null
+++ b/packages/backend/migration/1588044505511-hCaptcha.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class hCaptcha1588044505511 {
+    constructor() {
+        this.name = 'hCaptcha1588044505511';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableHcaptcha" boolean NOT NULL DEFAULT false`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "hcaptchaSiteKey" character varying(64)`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "hcaptchaSecretKey" character varying(64)`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hcaptchaSecretKey"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hcaptchaSiteKey"`, undefined);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableHcaptcha"`, undefined);
+    }
+}
+exports.hCaptcha1588044505511 = hCaptcha1588044505511;
diff --git a/packages/backend/migration/1589023282116-pubRelay.js b/packages/backend/migration/1589023282116-pubRelay.js
new file mode 100644
index 0000000000..f03384a51a
--- /dev/null
+++ b/packages/backend/migration/1589023282116-pubRelay.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class pubRelay1589023282116 {
+    constructor() {
+        this.name = 'pubRelay1589023282116';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TYPE "relay_status_enum" AS ENUM('requesting', 'accepted', 'rejected')`, undefined);
+        await queryRunner.query(`CREATE TABLE "relay" ("id" character varying(32) NOT NULL, "inbox" character varying(512) NOT NULL, "status" "relay_status_enum" NOT NULL, CONSTRAINT "PK_78ebc9cfddf4292633b7ba57aee" PRIMARY KEY ("id"))`, undefined);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab" ON "relay" ("inbox") `, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab"`, undefined);
+        await queryRunner.query(`DROP TABLE "relay"`, undefined);
+        await queryRunner.query(`DROP TYPE "relay_status_enum"`, undefined);
+    }
+}
+exports.pubRelay1589023282116 = pubRelay1589023282116;
diff --git a/packages/backend/migration/1595075960584-blurhash.js b/packages/backend/migration/1595075960584-blurhash.js
new file mode 100644
index 0000000000..3bd9c46f13
--- /dev/null
+++ b/packages/backend/migration/1595075960584-blurhash.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class blurhash1595075960584 {
+    constructor() {
+        this.name = 'blurhash1595075960584';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "drive_file" ADD "blurhash" character varying(128)`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "blurhash"`);
+    }
+}
+exports.blurhash1595075960584 = blurhash1595075960584;
diff --git a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js
new file mode 100644
index 0000000000..c2a9c97561
--- /dev/null
+++ b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js
@@ -0,0 +1,20 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class blurhashForAvatarBanner1595077605646 {
+    constructor() {
+        this.name = 'blurhashForAvatarBanner1595077605646';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarColor"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerColor"`);
+        await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
+        await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
+        await queryRunner.query(`ALTER TABLE "user" ADD "bannerColor" character varying(32)`);
+        await queryRunner.query(`ALTER TABLE "user" ADD "avatarColor" character varying(32)`);
+    }
+}
+exports.blurhashForAvatarBanner1595077605646 = blurhashForAvatarBanner1595077605646;
diff --git a/packages/backend/migration/1595676934834-instance-icon-url.js b/packages/backend/migration/1595676934834-instance-icon-url.js
new file mode 100644
index 0000000000..21ae26c55d
--- /dev/null
+++ b/packages/backend/migration/1595676934834-instance-icon-url.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class instanceIconUrl1595676934834 {
+    constructor() {
+        this.name = 'instanceIconUrl1595676934834';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" ADD "iconUrl" character varying(256) DEFAULT null`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "iconUrl"`);
+    }
+}
+exports.instanceIconUrl1595676934834 = instanceIconUrl1595676934834;
diff --git a/packages/backend/migration/1595771249699-word-mute.js b/packages/backend/migration/1595771249699-word-mute.js
new file mode 100644
index 0000000000..0dd3a55565
--- /dev/null
+++ b/packages/backend/migration/1595771249699-word-mute.js
@@ -0,0 +1,30 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class wordMute1595771249699 {
+    constructor() {
+        this.name = 'wordMute1595771249699';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "muted_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_897e2eff1c0b9b64e55ca1418a4" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_70ab9786313d78e4201d81cdb8" ON "muted_note" ("noteId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d8e07aa18c2d64e86201601aec" ON "muted_note" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a8c6bfd637d3f1d67a27c48e27" ON "muted_note" ("noteId", "userId") `);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "enableWordMute" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutedWords" jsonb NOT NULL DEFAULT '[]'`);
+        await queryRunner.query(`CREATE INDEX "IDX_3befe6f999c86aff06eb0257b4" ON "user_profile" ("enableWordMute") `);
+        await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_70ab9786313d78e4201d81cdb89" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1"`);
+        await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_70ab9786313d78e4201d81cdb89"`);
+        await queryRunner.query(`DROP INDEX "IDX_3befe6f999c86aff06eb0257b4"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutedWords"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "enableWordMute"`);
+        await queryRunner.query(`DROP INDEX "IDX_a8c6bfd637d3f1d67a27c48e27"`);
+        await queryRunner.query(`DROP INDEX "IDX_d8e07aa18c2d64e86201601aec"`);
+        await queryRunner.query(`DROP INDEX "IDX_70ab9786313d78e4201d81cdb8"`);
+        await queryRunner.query(`DROP TABLE "muted_note"`);
+    }
+}
+exports.wordMute1595771249699 = wordMute1595771249699;
diff --git a/packages/backend/migration/1595782306083-word-mute2.js b/packages/backend/migration/1595782306083-word-mute2.js
new file mode 100644
index 0000000000..29c707e952
--- /dev/null
+++ b/packages/backend/migration/1595782306083-word-mute2.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class wordMute21595782306083 {
+    constructor() {
+        this.name = 'wordMute21595782306083';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TYPE "muted_note_reason_enum" AS ENUM('word', 'manual', 'spam', 'other')`);
+        await queryRunner.query(`ALTER TABLE "muted_note" ADD "reason" "muted_note_reason_enum" NOT NULL`);
+        await queryRunner.query(`CREATE INDEX "IDX_636e977ff90b23676fb5624b25" ON "muted_note" ("reason") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_636e977ff90b23676fb5624b25"`);
+        await queryRunner.query(`ALTER TABLE "muted_note" DROP COLUMN "reason"`);
+        await queryRunner.query(`DROP TYPE "muted_note_reason_enum"`);
+    }
+}
+exports.wordMute21595782306083 = wordMute21595782306083;
diff --git a/packages/backend/migration/1596548170836-channel.js b/packages/backend/migration/1596548170836-channel.js
new file mode 100644
index 0000000000..bce812d5e3
--- /dev/null
+++ b/packages/backend/migration/1596548170836-channel.js
@@ -0,0 +1,58 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class channel1596548170836 {
+    constructor() {
+        this.name = 'channel1596548170836';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "channel" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastNotedAt" TIMESTAMP WITH TIME ZONE, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "description" character varying(2048), "bannerId" character varying(32), "notesCount" integer NOT NULL DEFAULT 0, "usersCount" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_590f33ee6ee7d76437acf362e39" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_71cb7b435b7c0d4843317e7e16" ON "channel" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_29ef80c6f13bcea998447fce43" ON "channel" ("lastNotedAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_823bae55bd81b3be6e05cff438" ON "channel" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0f58c11241e649d2a638a8de94" ON "channel" ("notesCount") `);
+        await queryRunner.query(`CREATE INDEX "IDX_094b86cd36bb805d1aa1e8cc9a" ON "channel" ("usersCount") `);
+        await queryRunner.query(`CREATE TABLE "channel_following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, CONSTRAINT "PK_8b104be7f7415113f2a02cd5bdd" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_11e71f2511589dcc8a4d3214f9" ON "channel_following" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0e43068c3f92cab197c3d3cd86" ON "channel_following" ("followeeId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6d8084ec9496e7334a4602707e" ON "channel_following" ("followerId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2e230dd45a10e671d781d99f3e" ON "channel_following" ("followerId", "followeeId") `);
+        await queryRunner.query(`CREATE TABLE "channel_note_pining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_44f7474496bcf2e4b741681146d" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_8125f950afd3093acb10d2db8a" ON "channel_note_pining" ("channelId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f36fed37d6d4cdcc68c803cd9c" ON "channel_note_pining" ("channelId", "noteId") `);
+        await queryRunner.query(`ALTER TABLE "note" ADD "channelId" character varying(32) DEFAULT null`);
+        await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `);
+        await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_823bae55bd81b3be6e05cff4383" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19" FOREIGN KEY ("bannerId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_f22169eb10657bded6d875ac8f9" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e" FOREIGN KEY ("followeeId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_6d8084ec9496e7334a4602707e1" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_8125f950afd3093acb10d2db8a8" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_10b19ef67d297ea9de325cd4502" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_10b19ef67d297ea9de325cd4502"`);
+        await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_8125f950afd3093acb10d2db8a8"`);
+        await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_6d8084ec9496e7334a4602707e1"`);
+        await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e"`);
+        await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_f22169eb10657bded6d875ac8f9"`);
+        await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19"`);
+        await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_823bae55bd81b3be6e05cff4383"`);
+        await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
+        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "channelId"`);
+        await queryRunner.query(`DROP INDEX "IDX_f36fed37d6d4cdcc68c803cd9c"`);
+        await queryRunner.query(`DROP INDEX "IDX_8125f950afd3093acb10d2db8a"`);
+        await queryRunner.query(`DROP TABLE "channel_note_pining"`);
+        await queryRunner.query(`DROP INDEX "IDX_2e230dd45a10e671d781d99f3e"`);
+        await queryRunner.query(`DROP INDEX "IDX_6d8084ec9496e7334a4602707e"`);
+        await queryRunner.query(`DROP INDEX "IDX_0e43068c3f92cab197c3d3cd86"`);
+        await queryRunner.query(`DROP INDEX "IDX_11e71f2511589dcc8a4d3214f9"`);
+        await queryRunner.query(`DROP TABLE "channel_following"`);
+        await queryRunner.query(`DROP INDEX "IDX_094b86cd36bb805d1aa1e8cc9a"`);
+        await queryRunner.query(`DROP INDEX "IDX_0f58c11241e649d2a638a8de94"`);
+        await queryRunner.query(`DROP INDEX "IDX_823bae55bd81b3be6e05cff438"`);
+        await queryRunner.query(`DROP INDEX "IDX_29ef80c6f13bcea998447fce43"`);
+        await queryRunner.query(`DROP INDEX "IDX_71cb7b435b7c0d4843317e7e16"`);
+        await queryRunner.query(`DROP TABLE "channel"`);
+    }
+}
+exports.channel1596548170836 = channel1596548170836;
diff --git a/packages/backend/migration/1596786425167-channel2.js b/packages/backend/migration/1596786425167-channel2.js
new file mode 100644
index 0000000000..160c73b06c
--- /dev/null
+++ b/packages/backend/migration/1596786425167-channel2.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class channel21596786425167 {
+    constructor() {
+        this.name = 'channel21596786425167';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`);
+    }
+}
+exports.channel21596786425167 = channel21596786425167;
diff --git a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js
new file mode 100644
index 0000000000..d208c425ec
--- /dev/null
+++ b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class objectStorageSetPublicRead1597230137744 {
+    constructor() {
+        this.name = 'objectStorageSetPublicRead1597230137744';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSetPublicRead" boolean NOT NULL DEFAULT false`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSetPublicRead"`);
+    }
+}
+exports.objectStorageSetPublicRead1597230137744 = objectStorageSetPublicRead1597230137744;
diff --git a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js
new file mode 100644
index 0000000000..0efbf2fe5a
--- /dev/null
+++ b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class IncludingNotificationTypes1597236229720 {
+    constructor() {
+        this.name = 'IncludingNotificationTypes1597236229720';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TYPE "user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`);
+        await queryRunner.query(`DROP TYPE "user_profile_includingnotificationtypes_enum"`);
+    }
+}
+exports.IncludingNotificationTypes1597236229720 = IncludingNotificationTypes1597236229720;
diff --git a/packages/backend/migration/1597385880794-add-sensitive-index.js b/packages/backend/migration/1597385880794-add-sensitive-index.js
new file mode 100644
index 0000000000..69fd394b03
--- /dev/null
+++ b/packages/backend/migration/1597385880794-add-sensitive-index.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class addSensitiveIndex1597385880794 {
+    constructor() {
+        this.name = 'addSensitiveIndex1597385880794';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE INDEX "IDX_a7eba67f8b3fa27271e85d2e26" ON "drive_file" ("isSensitive") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_a7eba67f8b3fa27271e85d2e26"`);
+    }
+}
+exports.addSensitiveIndex1597385880794 = addSensitiveIndex1597385880794;
diff --git a/packages/backend/migration/1597459042300-channel-unread.js b/packages/backend/migration/1597459042300-channel-unread.js
new file mode 100644
index 0000000000..629aa6009d
--- /dev/null
+++ b/packages/backend/migration/1597459042300-channel-unread.js
@@ -0,0 +1,27 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class channelUnread1597459042300 {
+    constructor() {
+        this.name = 'channelUnread1597459042300';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`TRUNCATE TABLE "note_unread"`, undefined);
+        await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`);
+        await queryRunner.query(`ALTER TABLE "note_unread" ADD "isMentioned" boolean NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "note_unread" ADD "noteChannelId" character varying(32)`);
+        await queryRunner.query(`CREATE INDEX "IDX_25b1dd384bec391b07b74b861c" ON "note_unread" ("isMentioned") `);
+        await queryRunner.query(`CREATE INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30" ON "note_unread" ("isSpecified") `);
+        await queryRunner.query(`CREATE INDEX "IDX_29e8c1d579af54d4232939f994" ON "note_unread" ("noteUserId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6a57f051d82c6d4036c141e107" ON "note_unread" ("noteChannelId") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_6a57f051d82c6d4036c141e107"`);
+        await queryRunner.query(`DROP INDEX "IDX_29e8c1d579af54d4232939f994"`);
+        await queryRunner.query(`DROP INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30"`);
+        await queryRunner.query(`DROP INDEX "IDX_25b1dd384bec391b07b74b861c"`);
+        await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "noteChannelId"`);
+        await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "isMentioned"`);
+        await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`);
+    }
+}
+exports.channelUnread1597459042300 = channelUnread1597459042300;
diff --git a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js
new file mode 100644
index 0000000000..770ac1887e
--- /dev/null
+++ b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ChannelNoteIdDescIndex1597893996136 {
+    constructor() {
+        this.name = 'ChannelNoteIdDescIndex1597893996136';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
+        await queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("channelId", "id" desc)`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_note_on_channelId_and_id_desc"`);
+        await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `);
+    }
+}
+exports.ChannelNoteIdDescIndex1597893996136 = ChannelNoteIdDescIndex1597893996136;
diff --git a/packages/backend/migration/1600353287890-mutingNotificationTypes.js b/packages/backend/migration/1600353287890-mutingNotificationTypes.js
new file mode 100644
index 0000000000..d39a6f2e44
--- /dev/null
+++ b/packages/backend/migration/1600353287890-mutingNotificationTypes.js
@@ -0,0 +1,20 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class mutingNotificationTypes1600353287890 {
+    constructor() {
+        this.name = 'mutingNotificationTypes1600353287890';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`);
+        await queryRunner.query(`DROP TYPE "public"."user_profile_includingnotificationtypes_enum"`);
+        await queryRunner.query(`CREATE TYPE "user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutingNotificationTypes" "user_profile_mutingnotificationtypes_enum" array NOT NULL DEFAULT '{}'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutingNotificationTypes"`);
+        await queryRunner.query(`DROP TYPE "user_profile_mutingnotificationtypes_enum"`);
+        await queryRunner.query(`CREATE TYPE "public"."user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`);
+    }
+}
+exports.mutingNotificationTypes1600353287890 = mutingNotificationTypes1600353287890;
diff --git a/packages/backend/migration/1603094348345-refine-abuse-user-report.js b/packages/backend/migration/1603094348345-refine-abuse-user-report.js
new file mode 100644
index 0000000000..b01a5d80aa
--- /dev/null
+++ b/packages/backend/migration/1603094348345-refine-abuse-user-report.js
@@ -0,0 +1,32 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class refineAbuseUserReport1603094348345 {
+    constructor() {
+        this.name = 'refineAbuseUserReport1603094348345';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_d049123c413e68ca52abe734203"`);
+        await queryRunner.query(`DROP INDEX "IDX_d049123c413e68ca52abe73420"`);
+        await queryRunner.query(`DROP INDEX "IDX_5cd442c3b2e74fdd99dae20243"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" RENAME COLUMN "userId" TO "targetUserId"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "assigneeId" character varying(32)`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolved" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "comment"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "comment" character varying(2048) NOT NULL DEFAULT '{}'::varchar[]`);
+        await queryRunner.query(`CREATE INDEX "IDX_2b15aaf4a0dc5be3499af7ab6a" ON "abuse_user_report" ("resolved") `);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_08b883dd5fdd6f9c4c1572b36de" FOREIGN KEY ("assigneeId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_08b883dd5fdd6f9c4c1572b36de"`);
+        await queryRunner.query(`DROP INDEX "IDX_2b15aaf4a0dc5be3499af7ab6a"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "comment"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "comment" character varying(512) NOT NULL DEFAULT '{}'::varchar[]`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolved"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "assigneeId"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" RENAME COLUMN "targetUserId" TO "userId"`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5cd442c3b2e74fdd99dae20243" ON "abuse_user_report" ("userId", "reporterId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d049123c413e68ca52abe73420" ON "abuse_user_report" ("userId") `);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_d049123c413e68ca52abe734203" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+}
+exports.refineAbuseUserReport1603094348345 = refineAbuseUserReport1603094348345;
diff --git a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js
new file mode 100644
index 0000000000..32b4e0bd21
--- /dev/null
+++ b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js
@@ -0,0 +1,20 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class refineAbuseUserReport21603095701770 {
+    constructor() {
+        this.name = 'refineAbuseUserReport21603095701770';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "targetUserHost" character varying(128)`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "reporterHost" character varying(128)`);
+        await queryRunner.query(`CREATE INDEX "IDX_4ebbf7f93cdc10e8d1ef2fc6cd" ON "abuse_user_report" ("targetUserHost") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f8d8b93740ad12c4ce8213a199" ON "abuse_user_report" ("reporterHost") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_f8d8b93740ad12c4ce8213a199"`);
+        await queryRunner.query(`DROP INDEX "IDX_4ebbf7f93cdc10e8d1ef2fc6cd"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "reporterHost"`);
+        await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "targetUserHost"`);
+    }
+}
+exports.refineAbuseUserReport21603095701770 = refineAbuseUserReport21603095701770;
diff --git a/packages/backend/migration/1603776877564-instance-theme-color.js b/packages/backend/migration/1603776877564-instance-theme-color.js
new file mode 100644
index 0000000000..315305f531
--- /dev/null
+++ b/packages/backend/migration/1603776877564-instance-theme-color.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class instanceThemeColor1603776877564 {
+    constructor() {
+        this.name = 'instanceThemeColor1603776877564';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" ADD "themeColor" character varying(64) DEFAULT null`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "themeColor"`);
+    }
+}
+exports.instanceThemeColor1603776877564 = instanceThemeColor1603776877564;
diff --git a/packages/backend/migration/1603781553011-instance-favicon.js b/packages/backend/migration/1603781553011-instance-favicon.js
new file mode 100644
index 0000000000..451989f91d
--- /dev/null
+++ b/packages/backend/migration/1603781553011-instance-favicon.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class instanceFavicon1603781553011 {
+    constructor() {
+        this.name = 'instanceFavicon1603781553011';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" ADD "faviconUrl" character varying(256) DEFAULT null`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "faviconUrl"`);
+    }
+}
+exports.instanceFavicon1603781553011 = instanceFavicon1603781553011;
diff --git a/packages/backend/migration/1604821689616-delete-auto-watch.js b/packages/backend/migration/1604821689616-delete-auto-watch.js
new file mode 100644
index 0000000000..9d972dd3f5
--- /dev/null
+++ b/packages/backend/migration/1604821689616-delete-auto-watch.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class deleteAutoWatch1604821689616 {
+    constructor() {
+        this.name = 'deleteAutoWatch1604821689616';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoWatch"`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoWatch" boolean NOT NULL DEFAULT false`);
+    }
+}
+exports.deleteAutoWatch1604821689616 = deleteAutoWatch1604821689616;
diff --git a/packages/backend/migration/1605408848373-clip-description.js b/packages/backend/migration/1605408848373-clip-description.js
new file mode 100644
index 0000000000..acbe2b0c8d
--- /dev/null
+++ b/packages/backend/migration/1605408848373-clip-description.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class clipDescription1605408848373 {
+    constructor() {
+        this.name = 'clipDescription1605408848373';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "clip" ADD "description" character varying(2048) DEFAULT null`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "description"`);
+    }
+}
+exports.clipDescription1605408848373 = clipDescription1605408848373;
diff --git a/packages/backend/migration/1605408971051-comments.js b/packages/backend/migration/1605408971051-comments.js
new file mode 100644
index 0000000000..6e5dacbb64
--- /dev/null
+++ b/packages/backend/migration/1605408971051-comments.js
@@ -0,0 +1,434 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class comments1605408971051 {
+    constructor() {
+        this.name = 'comments1605408971051';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`COMMENT ON COLUMN "log"."createdAt" IS 'The created date of the Log.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."createdAt" IS 'The created date of the DriveFolder.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."name" IS 'The name of the DriveFolder.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."userId" IS 'The owner ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."parentId" IS 'The parent folder ID. If null, it means the DriveFolder is located in root.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."createdAt" IS 'The created date of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userId" IS 'The owner ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userHost" IS 'The host of owner. It will be null if the user in local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."md5" IS 'The MD5 hash of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."name" IS 'The file name of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."type" IS 'The content type (MIME) of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."size" IS 'The file size (bytes) of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."comment" IS 'The comment of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."blurhash" IS 'The BlurHash string.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."properties" IS 'The any properties of the DriveFile. For example, it includes image width/height.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."thumbnailUrl" IS 'The URL of the thumbnail of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."webpublicUrl" IS 'The URL of the webpublic of the DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."folderId" IS 'The parent folder ID. If null, it means the DriveFile is located in root.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isSensitive" IS 'Whether the DriveFile is NSFW.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isLink" IS 'Whether the DriveFile is direct link to remote server.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."createdAt" IS 'The created date of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."updatedAt" IS 'The updated date of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."username" IS 'The username of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."usernameLower" IS 'The username (lowercased) of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."name" IS 'The name of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."followersCount" IS 'The count of followers.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."followingCount" IS 'The count of following.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."notesCount" IS 'The count of notes.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."avatarId" IS 'The ID of avatar DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."bannerId" IS 'The ID of banner DriveFile.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isSuspended" IS 'Whether the User is suspended.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isSilenced" IS 'Whether the User is silenced.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isLocked" IS 'Whether the User is locked.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isBot" IS 'Whether the User is a bot.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isCat" IS 'Whether the User is a cat.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isAdmin" IS 'Whether the User is the admin.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isModerator" IS 'Whether the User is a moderator.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."host" IS 'The host of the User. It will be null if the origin of the user is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."inbox" IS 'The inbox URL of the User. It will be null if the origin of the user is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."sharedInbox" IS 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."featured" IS 'The featured URL of the User. It will be null if the origin of the user is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."uri" IS 'The URI of the User. It will be null if the origin of the user is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS 'The native access token of the User. It will be null if the origin of the user is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."createdAt" IS 'The created date of the App.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."userId" IS 'The owner ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."secret" IS 'The secret key of the App.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."name" IS 'The name of the App.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."description" IS 'The description of the App.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."permission" IS 'The permission of the App.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."callbackUrl" IS 'The callbackUrl of the App.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."createdAt" IS 'The created date of the AccessToken.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."lastUsedAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."session" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."appId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."description" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."iconUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."createdAt" IS 'The created date of the Channel.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."userId" IS 'The owner ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."name" IS 'The name of the Channel.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."description" IS 'The description of the Channel.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."bannerId" IS 'The ID of banner Channel.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."notesCount" IS 'The count of notes.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."usersCount" IS 'The count of users.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."createdAt" IS 'The created date of the Note.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."replyId" IS 'The ID of reply target.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteId" IS 'The ID of renote target.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."userId" IS 'The ID of author.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."uri" IS 'The URI of a note. it will be null when the note is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."url" IS 'The human readable url of a note. it will be null when the note is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."channelId" IS 'The ID of source channel.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."userHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserId" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserId" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll_vote"."createdAt" IS 'The created date of the PollVote.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_reaction"."createdAt" IS 'The created date of the NoteReaction.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."createdAt" IS 'The created date of the NoteWatching.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."userId" IS 'The watcher ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteId" IS 'The target Note ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteUserId" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteUserId" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteChannelId" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."createdAt" IS 'The created date of the FollowRequest.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeId" IS 'The followee user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerId" IS 'The follower user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."requestId" IS 'id of Follow Activity.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerInbox" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerSharedInbox" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeInbox" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeSharedInbox" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group"."createdAt" IS 'The created date of the UserGroup.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group"."userId" IS 'The ID of owner.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."createdAt" IS 'The created date of the UserGroupInvitation.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userId" IS 'The user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userGroupId" IS 'The group ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."createdAt" IS 'The created date of the Notification.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."notifieeId" IS 'The ID of recipient user of the Notification.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."localDriveCapacityMb" IS 'Drive capacity of a local user (MB)'`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."remoteDriveCapacityMb" IS 'Drive capacity of a remote user (MB)'`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."maxNoteTextLength" IS 'Max allowed note text length in characters'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."createdAt" IS 'The created date of the Following.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeId" IS 'The followee user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followerId" IS 'The follower user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followerHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followerInbox" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followerSharedInbox" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeInbox" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeSharedInbox" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."caughtAt" IS 'The caught date of the Instance.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."host" IS 'The host of the Instance.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."usersCount" IS 'The count of the users of the Instance.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."notesCount" IS 'The count of the notes of the Instance.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareName" IS 'The software of the Instance.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareVersion" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."openRegistrations" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."description" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerName" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerEmail" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."iconUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."faviconUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."themeColor" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "muting"."createdAt" IS 'The created date of the Muting.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "muting"."muteeId" IS 'The mutee user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "muting"."muterId" IS 'The muter user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "blocking"."createdAt" IS 'The created date of the Blocking.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockeeId" IS 'The blockee user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockerId" IS 'The blocker user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list"."createdAt" IS 'The created date of the UserList.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list"."userId" IS 'The owner ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list"."name" IS 'The name of the UserList.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."createdAt" IS 'The created date of the UserListJoining.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userId" IS 'The user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userListId" IS 'The list ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."createdAt" IS 'The created date of the UserGroupJoining.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userId" IS 'The user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userGroupId" IS 'The group ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_favorite"."createdAt" IS 'The created date of the NoteFavorite.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."createdAt" IS 'The created date of the AbuseUserReport.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."targetUserHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."reporterHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."createdAt" IS 'The created date of the MessagingMessage.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."userId" IS 'The sender user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."groupId" IS 'The recipient group ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "signin"."createdAt" IS 'The created date of the Signin.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "auth_session"."createdAt" IS 'The created date of the AuthSession.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."createdAt" IS 'The created date of the ReversiGame.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."startedAt" IS 'The started date of the ReversiGame.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form1" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form2" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_matching"."createdAt" IS 'The created date of the ReversiMatching.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_note_pining"."createdAt" IS 'The created date of the UserNotePinings.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll"."noteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll"."noteVisibility" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll"."userId" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll"."userHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_keypair"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_publickey"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "page"."createdAt" IS 'The created date of the Page.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "page"."updatedAt" IS 'The updated date of the Page.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "page"."userId" IS 'The ID of author.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."location" IS 'The location of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."birthday" IS 'The birthday (YYYY-MM-DD) of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."description" IS 'The description (bio) of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."url" IS 'Remote URL of the user.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."email" IS 'The email address of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."password" IS 'The password hash of the User. It will be null if the origin of the user is local.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."clientData" IS 'The client-specific data of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."room" IS 'The room data of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userHost" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."id" IS 'Variable-length id given to navigator.credentials.get()'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'Variable-length public key used to verify attestations (hex-encoded).'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'The date of the last time the UserSecurityKey was successfully validated.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."name" IS 'User-defined name for this key'`);
+        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS 'Hex-encoded sha256 hash of the challenge.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS 'The date challenge was created for expiry purposes.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "moderation_log"."createdAt" IS 'The created date of the ModerationLog.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "announcement"."createdAt" IS 'The created date of the Announcement.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "announcement"."updatedAt" IS 'The updated date of the Announcement.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "announcement_read"."createdAt" IS 'The created date of the AnnouncementRead.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip"."createdAt" IS 'The created date of the Clip.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip"."userId" IS 'The owner ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip"."name" IS 'The name of the Clip.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip"."description" IS 'The description of the Clip.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip_note"."noteId" IS 'The note ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip_note"."clipId" IS 'The clip ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna"."createdAt" IS 'The created date of the Antenna.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna"."userId" IS 'The owner ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna"."name" IS 'The name of the Antenna.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."noteId" IS 'The note ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."antennaId" IS 'The antenna ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "promo_note"."noteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "promo_note"."userId" IS '[Denormalized]'`);
+        await queryRunner.query(`COMMENT ON COLUMN "promo_read"."createdAt" IS 'The created date of the PromoRead.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."noteId" IS 'The note ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."userId" IS 'The user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."reason" IS 'The reason of the MutedNote.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."createdAt" IS 'The created date of the ChannelFollowing.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followeeId" IS 'The followee channel ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followerId" IS 'The follower user ID.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel_note_pining"."createdAt" IS 'The created date of the ChannelNotePining.'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`COMMENT ON COLUMN "channel_note_pining"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followerId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followeeId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel_following"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."reason" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "muted_note"."noteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "promo_read"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "promo_note"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "promo_note"."noteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."antennaId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."noteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "antenna"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip_note"."clipId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip_note"."noteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip"."description" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "clip"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "announcement_read"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "announcement"."updatedAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "announcement"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "moderation_log"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."id" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."room" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."clientData" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."password" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."email" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."url" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."description" IS 'The description (bio) of the User.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."birthday" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."location" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "page"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "page"."updatedAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "page"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_publickey"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_keypair"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll"."userHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll"."noteVisibility" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll"."noteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_note_pining"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_matching"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form2" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form1" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."startedAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "auth_session"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "signin"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."groupId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."reporterHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."targetUserHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_favorite"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userGroupId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userListId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_list"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockerId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockeeId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "blocking"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "muting"."muterId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "muting"."muteeId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "muting"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."themeColor" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."faviconUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."iconUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerEmail" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerName" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."description" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."openRegistrations" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareVersion" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareName" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."notesCount" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."usersCount" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."host" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "instance"."caughtAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeSharedInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followerSharedInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followerInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followerHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followerId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."followeeId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "following"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."maxNoteTextLength" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."remoteDriveCapacityMb" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "meta"."localDriveCapacityMb" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."notifieeId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "notification"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userGroupId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_group"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeSharedInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerSharedInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."requestId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "follow_request"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteChannelId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteUserId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteUserId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_watching"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note_reaction"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "poll_vote"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."userHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."channelId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."url" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."uri" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."renoteId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."replyId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "note"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."usersCount" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."notesCount" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."bannerId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."description" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "channel"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."iconUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."description" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."appId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."session" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."lastUsedAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "access_token"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."callbackUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."permission" IS 'The permission of the App.'`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."description" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."secret" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "app"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."uri" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."featured" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."sharedInbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."inbox" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."host" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isModerator" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isAdmin" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isCat" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isBot" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isLocked" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isSilenced" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isSuspended" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."bannerId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."avatarId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."notesCount" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."followingCount" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."followersCount" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."usernameLower" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."username" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."updatedAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isLink" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isSensitive" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."folderId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."webpublicUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."thumbnailUrl" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."properties" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."blurhash" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."comment" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."size" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."type" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."md5" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userHost" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_file"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."parentId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."userId" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."name" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."createdAt" IS NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "log"."createdAt" IS NULL`);
+    }
+}
+exports.comments1605408971051 = comments1605408971051;
diff --git a/packages/backend/migration/1605585339718-instance-pinned-pages.js b/packages/backend/migration/1605585339718-instance-pinned-pages.js
new file mode 100644
index 0000000000..8d23572762
--- /dev/null
+++ b/packages/backend/migration/1605585339718-instance-pinned-pages.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class instancePinnedPages1605585339718 {
+    constructor() {
+        this.name = 'instancePinnedPages1605585339718';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'::varchar[]`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`);
+    }
+}
+exports.instancePinnedPages1605585339718 = instancePinnedPages1605585339718;
diff --git a/packages/backend/migration/1605965516823-instance-images.js b/packages/backend/migration/1605965516823-instance-images.js
new file mode 100644
index 0000000000..f078428bad
--- /dev/null
+++ b/packages/backend/migration/1605965516823-instance-images.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class instanceImages1605965516823 {
+    constructor() {
+        this.name = 'instanceImages1605965516823';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "backgroundImageUrl" character varying(512)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "logoImageUrl" character varying(512)`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "logoImageUrl"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "backgroundImageUrl"`);
+    }
+}
+exports.instanceImages1605965516823 = instanceImages1605965516823;
diff --git a/packages/backend/migration/1606191203881-no-crawle.js b/packages/backend/migration/1606191203881-no-crawle.js
new file mode 100644
index 0000000000..2c5fdaed21
--- /dev/null
+++ b/packages/backend/migration/1606191203881-no-crawle.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class noCrawle1606191203881 {
+    constructor() {
+        this.name = 'noCrawle1606191203881';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "noCrawle" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "noCrawle"`);
+    }
+}
+exports.noCrawle1606191203881 = noCrawle1606191203881;
diff --git a/packages/backend/migration/1607151207216-instance-pinned-clip.js b/packages/backend/migration/1607151207216-instance-pinned-clip.js
new file mode 100644
index 0000000000..3bd4792964
--- /dev/null
+++ b/packages/backend/migration/1607151207216-instance-pinned-clip.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class instancePinnedClip1607151207216 {
+    constructor() {
+        this.name = 'instancePinnedClip1607151207216';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedClipId" character varying(32)`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedClipId"`);
+    }
+}
+exports.instancePinnedClip1607151207216 = instancePinnedClip1607151207216;
diff --git a/packages/backend/migration/1607353487793-isExplorable.js b/packages/backend/migration/1607353487793-isExplorable.js
new file mode 100644
index 0000000000..3ddd0cb32f
--- /dev/null
+++ b/packages/backend/migration/1607353487793-isExplorable.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class isExplorable1607353487793 {
+    constructor() {
+        this.name = 'isExplorable1607353487793';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "isExplorable" boolean NOT NULL DEFAULT true`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isExplorable" IS 'Whether the User is explorable.'`);
+        await queryRunner.query(`CREATE INDEX "IDX_d5a1b83c7cab66f167e6888188" ON "user" ("isExplorable") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_d5a1b83c7cab66f167e6888188"`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isExplorable" IS 'Whether the User is explorable.'`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isExplorable"`);
+    }
+}
+exports.isExplorable1607353487793 = isExplorable1607353487793;
diff --git a/packages/backend/migration/1610277136869-registry.js b/packages/backend/migration/1610277136869-registry.js
new file mode 100644
index 0000000000..a7b5af84b6
--- /dev/null
+++ b/packages/backend/migration/1610277136869-registry.js
@@ -0,0 +1,22 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class registry1610277136869 {
+    constructor() {
+        this.name = 'registry1610277136869';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "registry_item" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "key" character varying(1024) NOT NULL, "scope" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[], "domain" character varying(512), CONSTRAINT "PK_64b3f7e6008b4d89b826cd3af95" PRIMARY KEY ("id")); COMMENT ON COLUMN "registry_item"."createdAt" IS 'The created date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."updatedAt" IS 'The updated date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "registry_item"."key" IS 'The key of the RegistryItem.'`);
+        await queryRunner.query(`CREATE INDEX "IDX_fb9d21ba0abb83223263df6bcb" ON "registry_item" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_22baca135bb8a3ea1a83d13df3" ON "registry_item" ("scope") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad" ON "registry_item" ("domain") `);
+        await queryRunner.query(`ALTER TABLE "registry_item" ADD CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "registry_item" DROP CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3"`);
+        await queryRunner.query(`DROP INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad"`);
+        await queryRunner.query(`DROP INDEX "IDX_22baca135bb8a3ea1a83d13df3"`);
+        await queryRunner.query(`DROP INDEX "IDX_fb9d21ba0abb83223263df6bcb"`);
+        await queryRunner.query(`DROP TABLE "registry_item"`);
+    }
+}
+exports.registry1610277136869 = registry1610277136869;
diff --git a/packages/backend/migration/1610277585759-registry2.js b/packages/backend/migration/1610277585759-registry2.js
new file mode 100644
index 0000000000..aa1417e186
--- /dev/null
+++ b/packages/backend/migration/1610277585759-registry2.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class registry21610277585759 {
+    constructor() {
+        this.name = 'registry21610277585759';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "registry_item" ADD "value" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`);
+        await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "value"`);
+    }
+}
+exports.registry21610277585759 = registry21610277585759;
diff --git a/packages/backend/migration/1610283021566-registry3.js b/packages/backend/migration/1610283021566-registry3.js
new file mode 100644
index 0000000000..7089cee1f2
--- /dev/null
+++ b/packages/backend/migration/1610283021566-registry3.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class registry31610283021566 {
+    constructor() {
+        this.name = 'registry31610283021566';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" DROP NOT NULL`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" SET NOT NULL`);
+    }
+}
+exports.registry31610283021566 = registry31610283021566;
diff --git a/packages/backend/migration/1611354329133-followersUri.js b/packages/backend/migration/1611354329133-followersUri.js
new file mode 100644
index 0000000000..e944da73ab
--- /dev/null
+++ b/packages/backend/migration/1611354329133-followersUri.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class followersUri1611354329133 {
+    constructor() {
+        this.name = 'followersUri1611354329133';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "followersUri" varchar(512) DEFAULT NULL`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."followersUri" IS 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`COMMENT ON COLUMN "user"."followersUri" IS 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "followersUri"`);
+    }
+}
+exports.followersUri1611354329133 = followersUri1611354329133;
diff --git a/packages/backend/migration/1611397665007-gallery.js b/packages/backend/migration/1611397665007-gallery.js
new file mode 100644
index 0000000000..9cf73cbcf3
--- /dev/null
+++ b/packages/backend/migration/1611397665007-gallery.js
@@ -0,0 +1,40 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class gallery1611397665007 {
+    constructor() {
+        this.name = 'gallery1611397665007';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "gallery_post" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "description" character varying(2048), "userId" character varying(32) NOT NULL, "fileIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "isSensitive" boolean NOT NULL DEFAULT false, "likedCount" integer NOT NULL DEFAULT '0', "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_8e90d7b6015f2c4518881b14753" PRIMARY KEY ("id")); COMMENT ON COLUMN "gallery_post"."createdAt" IS 'The created date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."updatedAt" IS 'The updated date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."userId" IS 'The ID of author.'; COMMENT ON COLUMN "gallery_post"."isSensitive" IS 'Whether the post is sensitive.'`);
+        await queryRunner.query(`CREATE INDEX "IDX_8f1a239bd077c8864a20c62c2c" ON "gallery_post" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f631d37835adb04792e361807c" ON "gallery_post" ("updatedAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_985b836dddd8615e432d7043dd" ON "gallery_post" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3ca50563facd913c425e7a89ee" ON "gallery_post" ("fileIds") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f2d744d9a14d0dfb8b96cb7fc5" ON "gallery_post" ("isSensitive") `);
+        await queryRunner.query(`CREATE INDEX "IDX_1a165c68a49d08f11caffbd206" ON "gallery_post" ("likedCount") `);
+        await queryRunner.query(`CREATE INDEX "IDX_05cca34b985d1b8edc1d1e28df" ON "gallery_post" ("tags") `);
+        await queryRunner.query(`CREATE TABLE "gallery_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "postId" character varying(32) NOT NULL, CONSTRAINT "PK_853ab02be39b8de45cd720cc15f" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_8fd5215095473061855ceb948c" ON "gallery_like" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_df1b5f4099e99fb0bc5eae53b6" ON "gallery_like" ("userId", "postId") `);
+        await queryRunner.query(`ALTER TABLE "gallery_post" ADD CONSTRAINT "FK_985b836dddd8615e432d7043ddb" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_8fd5215095473061855ceb948cf" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_b1cb568bfe569e47b7051699fc8" FOREIGN KEY ("postId") REFERENCES "gallery_post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "gallery_like" DROP CONSTRAINT "FK_b1cb568bfe569e47b7051699fc8"`);
+        await queryRunner.query(`ALTER TABLE "gallery_like" DROP CONSTRAINT "FK_8fd5215095473061855ceb948cf"`);
+        await queryRunner.query(`ALTER TABLE "gallery_post" DROP CONSTRAINT "FK_985b836dddd8615e432d7043ddb"`);
+        await queryRunner.query(`DROP INDEX "IDX_df1b5f4099e99fb0bc5eae53b6"`);
+        await queryRunner.query(`DROP INDEX "IDX_8fd5215095473061855ceb948c"`);
+        await queryRunner.query(`DROP TABLE "gallery_like"`);
+        await queryRunner.query(`DROP INDEX "IDX_05cca34b985d1b8edc1d1e28df"`);
+        await queryRunner.query(`DROP INDEX "IDX_1a165c68a49d08f11caffbd206"`);
+        await queryRunner.query(`DROP INDEX "IDX_f2d744d9a14d0dfb8b96cb7fc5"`);
+        await queryRunner.query(`DROP INDEX "IDX_3ca50563facd913c425e7a89ee"`);
+        await queryRunner.query(`DROP INDEX "IDX_985b836dddd8615e432d7043dd"`);
+        await queryRunner.query(`DROP INDEX "IDX_f631d37835adb04792e361807c"`);
+        await queryRunner.query(`DROP INDEX "IDX_8f1a239bd077c8864a20c62c2c"`);
+        await queryRunner.query(`DROP TABLE "gallery_post"`);
+    }
+}
+exports.gallery1611397665007 = gallery1611397665007;
diff --git a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js
new file mode 100644
index 0000000000..50a6189948
--- /dev/null
+++ b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class objectStorageS3ForcePathStyle1611547387175 {
+    constructor() {
+        this.name = 'objectStorageS3ForcePathStyle1611547387175';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageS3ForcePathStyle" boolean NOT NULL DEFAULT true`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageS3ForcePathStyle"`);
+    }
+}
+exports.objectStorageS3ForcePathStyle1611547387175 = objectStorageS3ForcePathStyle1611547387175;
diff --git a/packages/backend/migration/1612619156584-announcement-email.js b/packages/backend/migration/1612619156584-announcement-email.js
new file mode 100644
index 0000000000..bbd04679bb
--- /dev/null
+++ b/packages/backend/migration/1612619156584-announcement-email.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class announcementEmail1612619156584 {
+    constructor() {
+        this.name = 'announcementEmail1612619156584';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "receiveAnnouncementEmail" boolean NOT NULL DEFAULT true`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "receiveAnnouncementEmail"`);
+    }
+}
+exports.announcementEmail1612619156584 = announcementEmail1612619156584;
diff --git a/packages/backend/migration/1613155914446-emailNotificationTypes.js b/packages/backend/migration/1613155914446-emailNotificationTypes.js
new file mode 100644
index 0000000000..30fa4924b5
--- /dev/null
+++ b/packages/backend/migration/1613155914446-emailNotificationTypes.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class emailNotificationTypes1613155914446 {
+    constructor() {
+        this.name = 'emailNotificationTypes1613155914446';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "emailNotificationTypes" jsonb NOT NULL DEFAULT '["follow","receiveFollowRequest","groupInvited"]'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "emailNotificationTypes"`);
+    }
+}
+exports.emailNotificationTypes1613155914446 = emailNotificationTypes1613155914446;
diff --git a/packages/backend/migration/1613181457597-user-lang.js b/packages/backend/migration/1613181457597-user-lang.js
new file mode 100644
index 0000000000..f6be7cdab9
--- /dev/null
+++ b/packages/backend/migration/1613181457597-user-lang.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class userLang1613181457597 {
+    constructor() {
+        this.name = 'userLang1613181457597';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "lang" character varying(32)`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "lang"`);
+    }
+}
+exports.userLang1613181457597 = userLang1613181457597;
diff --git a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js
new file mode 100644
index 0000000000..91956ad786
--- /dev/null
+++ b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class useBigintForDriveUsage1613503367223 {
+    constructor() {
+        this.name = 'useBigintForDriveUsage1613503367223';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "driveUsage" TYPE bigint`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "driveUsage"`);
+        await queryRunner.query(`ALTER TABLE "instance" ADD "driveUsage" integer NOT NULL DEFAULT 0`);
+    }
+}
+exports.useBigintForDriveUsage1613503367223 = useBigintForDriveUsage1613503367223;
diff --git a/packages/backend/migration/1615965918224-chart-v2.js b/packages/backend/migration/1615965918224-chart-v2.js
new file mode 100644
index 0000000000..95f44879b9
--- /dev/null
+++ b/packages/backend/migration/1615965918224-chart-v2.js
@@ -0,0 +1,217 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class chartV21615965918224 {
+    constructor() {
+        this.name = 'chartV21615965918224';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`DELETE FROM "__chart__active_users" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__drive" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__federation" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__hashtag" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__instance" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__network" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__notes" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__per_user_drive" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__per_user_following" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__per_user_notes" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__per_user_reaction" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__test" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__test_grouped" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__test_unique" WHERE "span" = 'day'`);
+        await queryRunner.query(`DELETE FROM "__chart__users" WHERE "span" = 'day'`);
+        await queryRunner.query(`DROP INDEX "IDX_15e91a03aeeac9dbccdf43fc06"`);
+        await queryRunner.query(`DROP INDEX "IDX_20f57cc8f142c131340ee16742"`);
+        await queryRunner.query(`DROP INDEX "IDX_c26e2c1cbb6e911e0554b27416"`);
+        await queryRunner.query(`DROP INDEX "IDX_3fa0d0f17ca72e3dc80999a032"`);
+        await queryRunner.query(`DROP INDEX "IDX_6e1df243476e20cbf86572ecc0"`);
+        await queryRunner.query(`DROP INDEX "IDX_06690fc959f1c9fdaf21928222"`);
+        await queryRunner.query(`DROP INDEX "IDX_e447064455928cf627590ef527"`);
+        await queryRunner.query(`DROP INDEX "IDX_2d416e6af791a82e338c79d480"`);
+        await queryRunner.query(`DROP INDEX "IDX_e9cd07672b37d8966cf3709283"`);
+        await queryRunner.query(`DROP INDEX "IDX_fcc181fb8283009c61cc4083ef"`);
+        await queryRunner.query(`DROP INDEX "IDX_49975586f50ed7b800fdd88fbd"`);
+        await queryRunner.query(`DROP INDEX "IDX_6d6f156ceefc6bc5f273a0e370"`);
+        await queryRunner.query(`DROP INDEX "IDX_c12f0af4a66cdd30c2287ce8aa"`);
+        await queryRunner.query(`DROP INDEX "IDX_d0a4f79af5a97b08f37b547197"`);
+        await queryRunner.query(`DROP INDEX "IDX_f5448d9633cff74208d850aabe"`);
+        await queryRunner.query(`DROP INDEX "IDX_f8dd01baeded2ffa833e0a610a"`);
+        await queryRunner.query(`DROP INDEX "IDX_08fac0eb3b11f04c200c0b40dd"`);
+        await queryRunner.query(`DROP INDEX "IDX_9ff6944f01acb756fdc92d7563"`);
+        await queryRunner.query(`DROP INDEX "IDX_e69096589f11e3baa98ddd64d0"`);
+        await queryRunner.query(`DROP INDEX "IDX_0c9a159c5082cbeef3ca6706b5"`);
+        await queryRunner.query(`DROP INDEX "IDX_924fc196c80ca24bae01dd37e4"`);
+        await queryRunner.query(`DROP INDEX "IDX_328f259961e60c4fa0bfcf55ca"`);
+        await queryRunner.query(`DROP INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53"`);
+        await queryRunner.query(`DROP INDEX "IDX_f2aeafde2ae6fbad38e857631b"`);
+        await queryRunner.query(`DROP INDEX "IDX_f92dd6d03f8d994f29987f6214"`);
+        await queryRunner.query(`DROP INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f"`);
+        await queryRunner.query(`DROP INDEX "IDX_4db3b84c7be0d3464714f3e0b1"`);
+        await queryRunner.query(`DROP INDEX "IDX_8d2cbbc8114d90d19b44d626b6"`);
+        await queryRunner.query(`DROP INDEX "IDX_046feeb12e9ef5f783f409866a"`);
+        await queryRunner.query(`DROP INDEX "IDX_f68a5ab958f9f5fa17a32ac23b"`);
+        await queryRunner.query(`DROP INDEX "IDX_65633a106bce43fc7c5c30a5c7"`);
+        await queryRunner.query(`DROP INDEX "IDX_edeb73c09c3143a81bcb34d569"`);
+        await queryRunner.query(`DROP INDEX "IDX_e316f01a6d24eb31db27f88262"`);
+        await queryRunner.query(`DROP INDEX "IDX_2be7ec6cebddc14dc11e206686"`);
+        await queryRunner.query(`DROP INDEX "IDX_a5133470f4825902e170328ca5"`);
+        await queryRunner.query(`DROP INDEX "IDX_84e661abb7bd1e51b690d4b017"`);
+        await queryRunner.query(`DROP INDEX "IDX_5c73bf61da4f6e6f15bae88ed1"`);
+        await queryRunner.query(`DROP INDEX "IDX_d70c86baedc68326be11f9c0ce"`);
+        await queryRunner.query(`DROP INDEX "IDX_66e1e1ecd2f29e57778af35b59"`);
+        await queryRunner.query(`DROP INDEX "IDX_92255988735563f0fe4aba1f05"`);
+        await queryRunner.query(`DROP INDEX "IDX_c5870993e25c3d5771f91f5003"`);
+        await queryRunner.query(`DROP INDEX "IDX_f170de677ea75ad4533de2723e"`);
+        await queryRunner.query(`DROP INDEX "IDX_7c184198ecf66a8d3ecb253ab3"`);
+        await queryRunner.query(`DROP INDEX "IDX_f091abb24193d50c653c6b77fc"`);
+        await queryRunner.query(`DROP INDEX "IDX_a770a57c70e668cc61590c9161"`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__active_users_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_count"`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_count"`);
+        await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__drive_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__federation_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__hashtag_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_count"`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_count"`);
+        await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__instance_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__network_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__notes_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__per_user_drive_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__per_user_following_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__per_user_notes_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__per_user_reaction_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_grouped" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__test_grouped_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_grouped" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__test_unique_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "___foo"`);
+        await queryRunner.query(`ALTER TABLE "__chart__test" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__test_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__test" DROP COLUMN "unique"`);
+        await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "span"`);
+        await queryRunner.query(`DROP TYPE "public"."__chart__users_span_enum"`);
+        await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "unique"`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "__chart__users" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__users_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__users" ADD "span" "__chart__users_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__test" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__test_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__test" ADD "span" "__chart__test_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "___foo" bigint NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__test_unique_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "span" "__chart__test_unique_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_grouped" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__test_grouped_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_grouped" ADD "span" "__chart__test_grouped_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_reaction_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD "span" "__chart__per_user_reaction_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_notes_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "span" "__chart__per_user_notes_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_following_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD "span" "__chart__per_user_following_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_drive_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD "span" "__chart__per_user_drive_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__notes_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "span" "__chart__notes_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__network" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__network_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__network" ADD "span" "__chart__network_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__instance_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "span" "__chart__instance_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_count" bigint NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_count" bigint NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__hashtag_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "span" "__chart__hashtag_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__federation_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "span" "__chart__federation_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__drive_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "span" "__chart__drive_span_enum" NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_count" bigint NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_count" bigint NOT NULL`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique" jsonb NOT NULL DEFAULT '{}'`);
+        await queryRunner.query(`CREATE TYPE "public"."__chart__active_users_span_enum" AS ENUM('hour', 'day')`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "span" "__chart__active_users_span_enum" NOT NULL`);
+        await queryRunner.query(`CREATE INDEX "IDX_a770a57c70e668cc61590c9161" ON "__chart__users" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f091abb24193d50c653c6b77fc" ON "__chart__users" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7c184198ecf66a8d3ecb253ab3" ON "__chart__users" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f170de677ea75ad4533de2723e" ON "__chart__test" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_c5870993e25c3d5771f91f5003" ON "__chart__test" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_92255988735563f0fe4aba1f05" ON "__chart__test" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_66e1e1ecd2f29e57778af35b59" ON "__chart__test_unique" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d70c86baedc68326be11f9c0ce" ON "__chart__test_unique" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5c73bf61da4f6e6f15bae88ed1" ON "__chart__test_unique" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_84e661abb7bd1e51b690d4b017" ON "__chart__test_grouped" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a5133470f4825902e170328ca5" ON "__chart__test_grouped" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_2be7ec6cebddc14dc11e206686" ON "__chart__test_grouped" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e316f01a6d24eb31db27f88262" ON "__chart__per_user_reaction" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_edeb73c09c3143a81bcb34d569" ON "__chart__per_user_reaction" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_65633a106bce43fc7c5c30a5c7" ON "__chart__per_user_reaction" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f68a5ab958f9f5fa17a32ac23b" ON "__chart__per_user_notes" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_046feeb12e9ef5f783f409866a" ON "__chart__per_user_notes" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_8d2cbbc8114d90d19b44d626b6" ON "__chart__per_user_notes" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_4db3b84c7be0d3464714f3e0b1" ON "__chart__per_user_following" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f" ON "__chart__per_user_following" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f92dd6d03f8d994f29987f6214" ON "__chart__per_user_following" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f2aeafde2ae6fbad38e857631b" ON "__chart__per_user_drive" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53" ON "__chart__per_user_drive" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_328f259961e60c4fa0bfcf55ca" ON "__chart__per_user_drive" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_924fc196c80ca24bae01dd37e4" ON "__chart__notes" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0c9a159c5082cbeef3ca6706b5" ON "__chart__notes" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e69096589f11e3baa98ddd64d0" ON "__chart__notes" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_9ff6944f01acb756fdc92d7563" ON "__chart__network" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_08fac0eb3b11f04c200c0b40dd" ON "__chart__network" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f8dd01baeded2ffa833e0a610a" ON "__chart__network" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f5448d9633cff74208d850aabe" ON "__chart__instance" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d0a4f79af5a97b08f37b547197" ON "__chart__instance" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_c12f0af4a66cdd30c2287ce8aa" ON "__chart__instance" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6d6f156ceefc6bc5f273a0e370" ON "__chart__hashtag" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_49975586f50ed7b800fdd88fbd" ON "__chart__hashtag" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_fcc181fb8283009c61cc4083ef" ON "__chart__hashtag" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e9cd07672b37d8966cf3709283" ON "__chart__federation" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_2d416e6af791a82e338c79d480" ON "__chart__federation" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e447064455928cf627590ef527" ON "__chart__federation" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_06690fc959f1c9fdaf21928222" ON "__chart__drive" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6e1df243476e20cbf86572ecc0" ON "__chart__drive" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3fa0d0f17ca72e3dc80999a032" ON "__chart__drive" ("span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_c26e2c1cbb6e911e0554b27416" ON "__chart__active_users" ("date", "group", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_20f57cc8f142c131340ee16742" ON "__chart__active_users" ("date", "span") `);
+        await queryRunner.query(`CREATE INDEX "IDX_15e91a03aeeac9dbccdf43fc06" ON "__chart__active_users" ("span") `);
+    }
+}
+exports.chartV21615965918224 = chartV21615965918224;
diff --git a/packages/backend/migration/1615966519402-chart-v2-2.js b/packages/backend/migration/1615966519402-chart-v2-2.js
new file mode 100644
index 0000000000..85a83df1a2
--- /dev/null
+++ b/packages/backend/migration/1615966519402-chart-v2-2.js
@@ -0,0 +1,22 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class chartV221615966519402 {
+    constructor() {
+        this.name = 'chartV221615966519402';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
+        await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "___foo" character varying array NOT NULL DEFAULT '{}'::varchar[]`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "___foo"`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_users"`);
+        await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_users"`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_users"`);
+        await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_users"`);
+    }
+}
+exports.chartV221615966519402 = chartV221615966519402;
diff --git a/packages/backend/migration/1618637372000-user-last-active-date.js b/packages/backend/migration/1618637372000-user-last-active-date.js
new file mode 100644
index 0000000000..03fc011e4d
--- /dev/null
+++ b/packages/backend/migration/1618637372000-user-last-active-date.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class userLastActiveDate1618637372000 {
+    constructor() {
+        this.name = 'userLastActiveDate1618637372000';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "lastActiveDate" TIMESTAMP WITH TIME ZONE DEFAULT NULL`);
+        await queryRunner.query(`CREATE INDEX "IDX_seoignmeoprigmkpodgrjmkpormg" ON "user" ("lastActiveDate") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_seoignmeoprigmkpodgrjmkpormg"`);
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "lastActiveDate"`);
+    }
+}
+exports.userLastActiveDate1618637372000 = userLastActiveDate1618637372000;
diff --git a/packages/backend/migration/1618639857000-user-hide-online-status.js b/packages/backend/migration/1618639857000-user-hide-online-status.js
new file mode 100644
index 0000000000..0b1e341005
--- /dev/null
+++ b/packages/backend/migration/1618639857000-user-hide-online-status.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class userHideOnlineStatus1618639857000 {
+    constructor() {
+        this.name = 'userHideOnlineStatus1618639857000';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "hideOnlineStatus" boolean NOT NULL DEFAULT false`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hideOnlineStatus"`);
+    }
+}
+exports.userHideOnlineStatus1618639857000 = userHideOnlineStatus1618639857000;
diff --git a/packages/backend/migration/1619942102890-password-reset.js b/packages/backend/migration/1619942102890-password-reset.js
new file mode 100644
index 0000000000..9505be4b84
--- /dev/null
+++ b/packages/backend/migration/1619942102890-password-reset.js
@@ -0,0 +1,20 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class passwordReset1619942102890 {
+    constructor() {
+        this.name = 'passwordReset1619942102890';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "password_reset_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_fcf4b02eae1403a2edaf87fd074" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0b575fa9a4cfe638a925949285" ON "password_reset_request" ("token") `);
+        await queryRunner.query(`CREATE INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac" ON "password_reset_request" ("userId") `);
+        await queryRunner.query(`ALTER TABLE "password_reset_request" ADD CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "password_reset_request" DROP CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8"`);
+        await queryRunner.query(`DROP INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac"`);
+        await queryRunner.query(`DROP INDEX "IDX_0b575fa9a4cfe638a925949285"`);
+        await queryRunner.query(`DROP TABLE "password_reset_request"`);
+    }
+}
+exports.passwordReset1619942102890 = passwordReset1619942102890;
diff --git a/packages/backend/migration/1620019354680-ad.js b/packages/backend/migration/1620019354680-ad.js
new file mode 100644
index 0000000000..655629bc3b
--- /dev/null
+++ b/packages/backend/migration/1620019354680-ad.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ad1620019354680 {
+    constructor() {
+        this.name = 'ad1620019354680';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "ad" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "place" character varying(32) NOT NULL, "priority" character varying(32) NOT NULL, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, CONSTRAINT "PK_0193d5ef09746e88e9ea92c634d" PRIMARY KEY ("id")); COMMENT ON COLUMN "ad"."createdAt" IS 'The created date of the Ad.'; COMMENT ON COLUMN "ad"."expiresAt" IS 'The expired date of the Ad.'`);
+        await queryRunner.query(`CREATE INDEX "IDX_1129c2ef687fc272df040bafaa" ON "ad" ("createdAt") `);
+        await queryRunner.query(`CREATE INDEX "IDX_2da24ce20ad209f1d9dc032457" ON "ad" ("expiresAt") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_2da24ce20ad209f1d9dc032457"`);
+        await queryRunner.query(`DROP INDEX "IDX_1129c2ef687fc272df040bafaa"`);
+        await queryRunner.query(`DROP TABLE "ad"`);
+    }
+}
+exports.ad1620019354680 = ad1620019354680;
diff --git a/packages/backend/migration/1620364649428-ad2.js b/packages/backend/migration/1620364649428-ad2.js
new file mode 100644
index 0000000000..7f2213f6a6
--- /dev/null
+++ b/packages/backend/migration/1620364649428-ad2.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ad21620364649428 {
+    constructor() {
+        this.name = 'ad21620364649428';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "ad" ADD "ratio" integer NOT NULL DEFAULT '1'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "ratio"`);
+    }
+}
+exports.ad21620364649428 = ad21620364649428;
diff --git a/packages/backend/migration/1621479946000-add-note-indexes.js b/packages/backend/migration/1621479946000-add-note-indexes.js
new file mode 100644
index 0000000000..1bf9827f6f
--- /dev/null
+++ b/packages/backend/migration/1621479946000-add-note-indexes.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class addNoteIndexes1621479946000 {
+    constructor() {
+        this.name = 'addNoteIndexes1621479946000';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE INDEX "IDX_NOTE_MENTIONS" ON "note" USING gin ("mentions")`, undefined);
+        await queryRunner.query(`CREATE INDEX "IDX_NOTE_VISIBLE_USER_IDS" ON "note" USING gin ("visibleUserIds")`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_NOTE_MENTIONS"`, undefined);
+        await queryRunner.query(`DROP INDEX "IDX_NOTE_VISIBLE_USER_IDS"`, undefined);
+    }
+}
+exports.addNoteIndexes1621479946000 = addNoteIndexes1621479946000;
diff --git a/packages/backend/migration/1622679304522-user-profile-description-length.js b/packages/backend/migration/1622679304522-user-profile-description-length.js
new file mode 100644
index 0000000000..237870d6c6
--- /dev/null
+++ b/packages/backend/migration/1622679304522-user-profile-description-length.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class userProfileDescriptionLength1622679304522 {
+    constructor() {
+        this.name = 'userProfileDescriptionLength1622679304522';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(2048)`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(1024)`, undefined);
+    }
+}
+exports.userProfileDescriptionLength1622679304522 = userProfileDescriptionLength1622679304522;
diff --git a/packages/backend/migration/1622681548499-log-message-length.js b/packages/backend/migration/1622681548499-log-message-length.js
new file mode 100644
index 0000000000..cb2ad288ba
--- /dev/null
+++ b/packages/backend/migration/1622681548499-log-message-length.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class logMessageLength1622681548499 {
+    constructor() {
+        this.name = 'logMessageLength1622681548499';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "log" ALTER COLUMN "message" TYPE character varying(2048)`, undefined);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "log" ALTER COLUMN "message" TYPE character varying(1024)`, undefined);
+    }
+}
+exports.logMessageLength1622681548499 = logMessageLength1622681548499;
diff --git a/packages/backend/migration/1629004542760-chart-reindex.js b/packages/backend/migration/1629004542760-chart-reindex.js
new file mode 100644
index 0000000000..927ea312e0
--- /dev/null
+++ b/packages/backend/migration/1629004542760-chart-reindex.js
@@ -0,0 +1,182 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class chartReindex1629004542760 {
+    constructor() {
+        this.name = 'chartReindex1629004542760';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`DELETE FROM "__chart__active_users" a USING "__chart__active_users" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__drive" a USING "__chart__drive" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__federation" a USING "__chart__federation" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__hashtag" a USING "__chart__hashtag" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__instance" a USING "__chart__instance" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__network" a USING "__chart__network" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__notes" a USING "__chart__notes" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__per_user_drive" a USING "__chart__per_user_drive" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__per_user_following" a USING "__chart__per_user_following" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__per_user_notes" a USING "__chart__per_user_notes" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__per_user_reaction" a USING "__chart__per_user_reaction" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__test_grouped" a USING "__chart__test_grouped" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__test_unique" a USING "__chart__test_unique" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DELETE FROM "__chart__users" a USING "__chart__users" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`);
+        await queryRunner.query(`DROP INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc"`);
+        await queryRunner.query(`DROP INDEX "IDX_00ed5f86db1f7efafb1978bf21"`);
+        await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`);
+        await queryRunner.query(`DROP INDEX "IDX_13565815f618a1ff53886c5b28"`);
+        await queryRunner.query(`DROP INDEX "IDX_7a170f67425e62a8fabb76c872"`);
+        await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`);
+        await queryRunner.query(`DROP INDEX "IDX_36cb699c49580d4e6c2e6159f9"`);
+        await queryRunner.query(`DROP INDEX "IDX_76e87c7bfc5d925fcbba405d84"`);
+        await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`);
+        await queryRunner.query(`DROP INDEX "IDX_07747a1038c05f532a718fe1de"`);
+        await queryRunner.query(`DROP INDEX "IDX_99a7d2faaef84a6f728d714ad6"`);
+        await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`);
+        await queryRunner.query(`DROP INDEX "IDX_6b8f34a1a64b06014b6fb66824"`);
+        await queryRunner.query(`DROP INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63"`);
+        await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`);
+        await queryRunner.query(`DROP INDEX "IDX_a1efd3e0048a5f2793a47360dc"`);
+        await queryRunner.query(`DROP INDEX "IDX_7b5da130992ec9df96712d4290"`);
+        await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`);
+        await queryRunner.query(`DROP INDEX "IDX_42eb716a37d381cdf566192b2b"`);
+        await queryRunner.query(`DROP INDEX "IDX_7036f2957151588b813185c794"`);
+        await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`);
+        await queryRunner.query(`DROP INDEX "IDX_5f86db6492274e07c1a3cdf286"`);
+        await queryRunner.query(`DROP INDEX "IDX_e496ca8096d28f6b9b509264dc"`);
+        await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`);
+        await queryRunner.query(`DROP INDEX "IDX_7af07790712aa3438ff6773f3b"`);
+        await queryRunner.query(`DROP INDEX "IDX_4b3593098b6edc9c5afe36b18b"`);
+        await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`);
+        await queryRunner.query(`DROP INDEX "IDX_84234bd1abb873f07329681c83"`);
+        await queryRunner.query(`DROP INDEX "IDX_55bf20f366979f2436de99206b"`);
+        await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`);
+        await queryRunner.query(`DROP INDEX "IDX_f7bf4c62059764c2c2bb40fdab"`);
+        await queryRunner.query(`DROP INDEX "IDX_8cf3156fd7a6b15c43459c6e3b"`);
+        await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`);
+        await queryRunner.query(`DROP INDEX "IDX_0c641990ecf47d2545df4edb75"`);
+        await queryRunner.query(`DROP INDEX "IDX_234dff3c0b56a6150b95431ab9"`);
+        await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`);
+        await queryRunner.query(`DROP INDEX "IDX_437bab3c6061d90f6bb65fd2cc"`);
+        await queryRunner.query(`DROP INDEX "IDX_bbfa573a8181018851ed0b6357"`);
+        await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`);
+        await queryRunner.query(`DROP INDEX "IDX_b070a906db04b44c67c6c2144d"`);
+        await queryRunner.query(`DROP INDEX "IDX_d41cce6aee1a50bfc062038f9b"`);
+        await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`);
+        await queryRunner.query(`DROP INDEX "IDX_845254b3eaf708ae8a6cac3026"`);
+        await queryRunner.query(`DROP INDEX "IDX_ed9b95919c672a13008e9487ee"`);
+        await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_60c5c6e7e538c09aa274ecd1cf" ON "__chart__active_users" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ceab80a6729f8e2e6f5b8a1a3d" ON "__chart__drive" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_eddfed8fb40305a04c6f941050" ON "__chart__federation" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_53a3604b939e2b479eb2cfaac8" ON "__chart__hashtag" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8111b817b9818c04d7eb8475b1" ON "__chart__instance" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2082327b2699ce924fa654afc5" ON "__chart__network" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e60c358aaced5aab8900a4af31" ON "__chart__notes" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a9a806d466b314f253a1a611c4" ON "__chart__per_user_drive" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dabbb38a51ab86ee3cab291326" ON "__chart__per_user_following" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_583a157ed0cf0ed1b5ec2a833f" ON "__chart__per_user_notes" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3b7697a96f522d0478972e6d6f" ON "__chart__per_user_reaction" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_da522b4008a9f5d7743b87ad55" ON "__chart__test_grouped" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_16effb2e888f6763673b579f80" ON "__chart__test_unique" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dab383a36f3c9db4a0c9b02cf3" ON "__chart__test" ("date") WHERE "group" IS NULL`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_66feba81e1795d176d06c0b1e6" ON "__chart__users" ("date") WHERE "group" IS NULL`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_66feba81e1795d176d06c0b1e6"`);
+        await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`);
+        await queryRunner.query(`DROP INDEX "IDX_dab383a36f3c9db4a0c9b02cf3"`);
+        await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`);
+        await queryRunner.query(`DROP INDEX "IDX_16effb2e888f6763673b579f80"`);
+        await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`);
+        await queryRunner.query(`DROP INDEX "IDX_da522b4008a9f5d7743b87ad55"`);
+        await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`);
+        await queryRunner.query(`DROP INDEX "IDX_3b7697a96f522d0478972e6d6f"`);
+        await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`);
+        await queryRunner.query(`DROP INDEX "IDX_583a157ed0cf0ed1b5ec2a833f"`);
+        await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`);
+        await queryRunner.query(`DROP INDEX "IDX_dabbb38a51ab86ee3cab291326"`);
+        await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`);
+        await queryRunner.query(`DROP INDEX "IDX_a9a806d466b314f253a1a611c4"`);
+        await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`);
+        await queryRunner.query(`DROP INDEX "IDX_e60c358aaced5aab8900a4af31"`);
+        await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`);
+        await queryRunner.query(`DROP INDEX "IDX_2082327b2699ce924fa654afc5"`);
+        await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`);
+        await queryRunner.query(`DROP INDEX "IDX_8111b817b9818c04d7eb8475b1"`);
+        await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`);
+        await queryRunner.query(`DROP INDEX "IDX_53a3604b939e2b479eb2cfaac8"`);
+        await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`);
+        await queryRunner.query(`DROP INDEX "IDX_eddfed8fb40305a04c6f941050"`);
+        await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`);
+        await queryRunner.query(`DROP INDEX "IDX_ceab80a6729f8e2e6f5b8a1a3d"`);
+        await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`);
+        await queryRunner.query(`DROP INDEX "IDX_60c5c6e7e538c09aa274ecd1cf"`);
+        await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`);
+        await queryRunner.query(`DROP INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5"`);
+        await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`);
+        await queryRunner.query(`DROP INDEX "IDX_c8cc87bd0f2f4487d17c651fbf"`);
+        await queryRunner.query(`DROP INDEX "IDX_754499f9b2642336433769518d"`);
+        await queryRunner.query(`DROP INDEX "IDX_315c779174fe8247ab324f036e"`);
+        await queryRunner.query(`DROP INDEX "IDX_c5d46cbfda48b1c33ed852e21b"`);
+        await queryRunner.query(`CREATE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_ed9b95919c672a13008e9487ee" ON "__chart__users" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_845254b3eaf708ae8a6cac3026" ON "__chart__users" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d41cce6aee1a50bfc062038f9b" ON "__chart__test" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_b070a906db04b44c67c6c2144d" ON "__chart__test" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_bbfa573a8181018851ed0b6357" ON "__chart__test_unique" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_437bab3c6061d90f6bb65fd2cc" ON "__chart__test_unique" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_234dff3c0b56a6150b95431ab9" ON "__chart__test_grouped" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0c641990ecf47d2545df4edb75" ON "__chart__test_grouped" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_8cf3156fd7a6b15c43459c6e3b" ON "__chart__per_user_reaction" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f7bf4c62059764c2c2bb40fdab" ON "__chart__per_user_reaction" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_55bf20f366979f2436de99206b" ON "__chart__per_user_notes" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_84234bd1abb873f07329681c83" ON "__chart__per_user_notes" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_4b3593098b6edc9c5afe36b18b" ON "__chart__per_user_following" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7af07790712aa3438ff6773f3b" ON "__chart__per_user_following" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_e496ca8096d28f6b9b509264dc" ON "__chart__per_user_drive" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_5f86db6492274e07c1a3cdf286" ON "__chart__per_user_drive" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7036f2957151588b813185c794" ON "__chart__notes" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_42eb716a37d381cdf566192b2b" ON "__chart__notes" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7b5da130992ec9df96712d4290" ON "__chart__network" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_a1efd3e0048a5f2793a47360dc" ON "__chart__network" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63" ON "__chart__instance" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_6b8f34a1a64b06014b6fb66824" ON "__chart__instance" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_99a7d2faaef84a6f728d714ad6" ON "__chart__hashtag" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_07747a1038c05f532a718fe1de" ON "__chart__hashtag" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_76e87c7bfc5d925fcbba405d84" ON "__chart__federation" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_36cb699c49580d4e6c2e6159f9" ON "__chart__federation" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_7a170f67425e62a8fabb76c872" ON "__chart__drive" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_13565815f618a1ff53886c5b28" ON "__chart__drive" ("date") `);
+        await queryRunner.query(`CREATE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_00ed5f86db1f7efafb1978bf21" ON "__chart__active_users" ("group") `);
+        await queryRunner.query(`CREATE INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc" ON "__chart__active_users" ("date") `);
+    }
+}
+exports.chartReindex1629004542760 = chartReindex1629004542760;
diff --git a/packages/backend/migration/1629024377804-deepl-integration.js b/packages/backend/migration/1629024377804-deepl-integration.js
new file mode 100644
index 0000000000..a2bd3b1885
--- /dev/null
+++ b/packages/backend/migration/1629024377804-deepl-integration.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class deeplIntegration1629024377804 {
+    constructor() {
+        this.name = 'deeplIntegration1629024377804';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "deeplAuthKey" character varying(128)`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplAuthKey"`);
+    }
+}
+exports.deeplIntegration1629024377804 = deeplIntegration1629024377804;
diff --git a/packages/backend/migration/1629288472000-fix-channel-userId.js b/packages/backend/migration/1629288472000-fix-channel-userId.js
new file mode 100644
index 0000000000..d0e8baa06c
--- /dev/null
+++ b/packages/backend/migration/1629288472000-fix-channel-userId.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class fixChannelUserId1629288472000 {
+    constructor() {
+        this.name = 'fixChannelUserId1629288472000';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "channel" ALTER COLUMN "userId" DROP NOT NULL;`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "channel" ALTER COLUMN "userId" SET NOT NULL;`);
+    }
+}
+exports.fixChannelUserId1629288472000 = fixChannelUserId1629288472000;
diff --git a/packages/backend/migration/1629512953000-user-is-deleted.js b/packages/backend/migration/1629512953000-user-is-deleted.js
new file mode 100644
index 0000000000..0083907192
--- /dev/null
+++ b/packages/backend/migration/1629512953000-user-is-deleted.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class isUserDeleted1629512953000 {
+    constructor() {
+        this.name = 'isUserDeleted1629512953000';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" ADD "isDeleted" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`COMMENT ON COLUMN "user"."isDeleted" IS 'Whether the User is deleted.'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isDeleted"`);
+    }
+}
+exports.isUserDeleted1629512953000 = isUserDeleted1629512953000;
diff --git a/packages/backend/migration/1629778475000-deepl-integration2.js b/packages/backend/migration/1629778475000-deepl-integration2.js
new file mode 100644
index 0000000000..50365f4bb5
--- /dev/null
+++ b/packages/backend/migration/1629778475000-deepl-integration2.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class deeplIntegration21629778475000 {
+    constructor() {
+        this.name = 'deeplIntegration21629778475000';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "deeplIsPro" boolean NOT NULL DEFAULT false`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplIsPro"`);
+    }
+}
+exports.deeplIntegration21629778475000 = deeplIntegration21629778475000;
diff --git a/packages/backend/migration/1633068642000-email-required-for-signup.js b/packages/backend/migration/1633068642000-email-required-for-signup.js
new file mode 100644
index 0000000000..9793cac00a
--- /dev/null
+++ b/packages/backend/migration/1633068642000-email-required-for-signup.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class emailRequiredForSignup1633068642000 {
+    constructor() {
+        this.name = 'emailRequiredForSignup1633068642000';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "emailRequiredForSignup" boolean NOT NULL DEFAULT false`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "emailRequiredForSignup"`);
+    }
+}
+exports.emailRequiredForSignup1633068642000 = emailRequiredForSignup1633068642000;
diff --git a/packages/backend/migration/1633071909016-user-pending.js b/packages/backend/migration/1633071909016-user-pending.js
new file mode 100644
index 0000000000..92a5139944
--- /dev/null
+++ b/packages/backend/migration/1633071909016-user-pending.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class userPending1633071909016 {
+    constructor() {
+        this.name = 'userPending1633071909016';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "user_pending" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(128) NOT NULL, "username" character varying(128) NOT NULL, "email" character varying(128) NOT NULL, "password" character varying(128) NOT NULL, CONSTRAINT "PK_d4c84e013c98ec02d19b8fbbafa" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4e5c4c99175638ec0761714ab0" ON "user_pending" ("code") `);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`DROP INDEX "IDX_4e5c4c99175638ec0761714ab0"`);
+        await queryRunner.query(`DROP TABLE "user_pending"`);
+    }
+}
+exports.userPending1633071909016 = userPending1633071909016;
diff --git a/packages/backend/migration/1634486652000-user-public-reactions.js b/packages/backend/migration/1634486652000-user-public-reactions.js
new file mode 100644
index 0000000000..1447abe6a1
--- /dev/null
+++ b/packages/backend/migration/1634486652000-user-public-reactions.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class userPublicReactions1634486652000 {
+    constructor() {
+        this.name = 'userPublicReactions1634486652000';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "publicReactions" boolean NOT NULL DEFAULT false`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "publicReactions"`);
+    }
+}
+exports.userPublicReactions1634486652000 = userPublicReactions1634486652000;
diff --git a/packages/backend/migration/1634902659689-delete-log.js b/packages/backend/migration/1634902659689-delete-log.js
new file mode 100644
index 0000000000..b2162519f6
--- /dev/null
+++ b/packages/backend/migration/1634902659689-delete-log.js
@@ -0,0 +1,13 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class deleteLog1634902659689 {
+    constructor() {
+        this.name = 'deleteLog1634902659689';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`DROP TABLE "log"`);
+    }
+    async down(queryRunner) {
+    }
+}
+exports.deleteLog1634902659689 = deleteLog1634902659689;
diff --git a/packages/backend/migration/1635500777168-note-thread-mute.js b/packages/backend/migration/1635500777168-note-thread-mute.js
new file mode 100644
index 0000000000..1e01952748
--- /dev/null
+++ b/packages/backend/migration/1635500777168-note-thread-mute.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class noteThreadMute1635500777168 {
+    constructor() {
+        this.name = 'noteThreadMute1635500777168';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TABLE "note_thread_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "threadId" character varying(256) NOT NULL, CONSTRAINT "PK_ec5936d94d1a0369646d12a3a47" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_29c11c7deb06615076f8c95b80" ON "note_thread_muting" ("userId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_c426394644267453e76f036926" ON "note_thread_muting" ("threadId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ae7aab18a2641d3e5f25e0c4ea" ON "note_thread_muting" ("userId", "threadId") `);
+        await queryRunner.query(`ALTER TABLE "note" ADD "threadId" character varying(256)`);
+        await queryRunner.query(`CREATE INDEX "IDX_d4ebdef929896d6dc4a3c5bb48" ON "note" ("threadId") `);
+        await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD CONSTRAINT "FK_29c11c7deb06615076f8c95b80a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP CONSTRAINT "FK_29c11c7deb06615076f8c95b80a"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_d4ebdef929896d6dc4a3c5bb48"`);
+        await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "threadId"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_ae7aab18a2641d3e5f25e0c4ea"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_c426394644267453e76f036926"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_29c11c7deb06615076f8c95b80"`);
+        await queryRunner.query(`DROP TABLE "note_thread_muting"`);
+    }
+}
+exports.noteThreadMute1635500777168 = noteThreadMute1635500777168;
diff --git a/packages/backend/migration/1636197624383-ff-visibility.js b/packages/backend/migration/1636197624383-ff-visibility.js
new file mode 100644
index 0000000000..9e8d5a3dc8
--- /dev/null
+++ b/packages/backend/migration/1636197624383-ff-visibility.js
@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ffVisibility1636197624383 {
+    constructor() {
+        this.name = 'ffVisibility1636197624383';
+    }
+    async up(queryRunner) {
+        await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`);
+    }
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`);
+        await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`);
+    }
+}
+exports.ffVisibility1636197624383 = ffVisibility1636197624383;
diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js
new file mode 100644
index 0000000000..524181cb89
--- /dev/null
+++ b/packages/backend/ormconfig.js
@@ -0,0 +1,17 @@
+const config = require('./built/config').default;
+const entities = require('./built/db/postgre').entities;
+
+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/*.js'],
+	cli: {
+		migrationsDir: 'migration'
+	}
+};
diff --git a/packages/backend/package.json b/packages/backend/package.json
new file mode 100644
index 0000000000..87c5c0464a
--- /dev/null
+++ b/packages/backend/package.json
@@ -0,0 +1,199 @@
+{
+	"main": "./index.js",
+	"private": true,
+	"scripts": {
+		"init": "npm run migrate",
+		"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
+		"watch": "tsc -w -p tsconfig.json && tsc-alias -w -p tsconfig.json",
+		"lint": "tslint '**/*.ts'",
+		"mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
+		"test": "npm run mocha"
+	},
+	"resolutions": {
+		"chokidar": "^3.3.1",
+		"lodash": "^4.17.21"
+	},
+	"dependencies": {
+		"@discordapp/twemoji": "13.1.0",
+		"@elastic/elasticsearch": "7.11.0",
+		"@koa/cors": "3.1.0",
+		"@koa/multer": "3.0.0",
+		"@koa/router": "9.0.1",
+		"@sinonjs/fake-timers": "7.1.2",
+		"@syuilo/aiscript": "0.11.1",
+		"@types/bcryptjs": "2.4.2",
+		"@types/bull": "3.15.5",
+		"@types/cbor": "6.0.0",
+		"@types/dateformat": "3.0.1",
+		"@types/escape-regexp": "0.0.0",
+		"@types/glob": "7.2.0",
+		"@types/is-url": "1.2.30",
+		"@types/js-yaml": "4.0.4",
+		"@types/jsdom": "16.2.13",
+		"@types/jsonld": "1.5.6",
+		"@types/koa": "2.13.4",
+		"@types/koa-bodyparser": "4.3.3",
+		"@types/koa-cors": "0.0.2",
+		"@types/koa-favicon": "2.0.21",
+		"@types/koa-logger": "3.1.2",
+		"@types/koa-mount": "4.0.1",
+		"@types/koa-send": "4.1.3",
+		"@types/koa-views": "7.0.0",
+		"@types/koa__cors": "3.0.3",
+		"@types/koa__multer": "2.0.4",
+		"@types/koa__router": "8.0.8",
+		"@types/mocha": "8.2.3",
+		"@types/node": "16.11.7",
+		"@types/node-fetch": "2.5.12",
+		"@types/nodemailer": "6.4.4",
+		"@types/nprogress": "0.2.0",
+		"@types/oauth": "0.9.1",
+		"@types/parse5": "6.0.2",
+		"@types/parsimmon": "1.10.6",
+		"@types/portscanner": "2.1.1",
+		"@types/pug": "2.0.5",
+		"@types/punycode": "2.1.0",
+		"@types/qrcode": "1.4.1",
+		"@types/random-seed": "0.3.3",
+		"@types/ratelimiter": "3.4.2",
+		"@types/redis": "2.8.32",
+		"@types/rename": "1.0.4",
+		"@types/request-stats": "3.0.0",
+		"@types/rimraf": "3.0.2",
+		"@types/seedrandom": "2.4.28",
+		"@types/sharp": "0.29.3",
+		"@types/sinonjs__fake-timers": "6.0.4",
+		"@types/speakeasy": "2.0.6",
+		"@types/throttle-debounce": "2.1.0",
+		"@types/tinycolor2": "1.4.3",
+		"@types/tmp": "0.2.2",
+		"@types/uuid": "8.3.1",
+		"@types/web-push": "3.3.2",
+		"@types/webpack": "5.28.0",
+		"@types/webpack-stream": "3.2.12",
+		"@types/websocket": "1.0.4",
+		"@types/ws": "8.2.0",
+		"@typescript-eslint/parser": "5.1.0",
+		"abort-controller": "3.0.0",
+		"autobind-decorator": "2.4.0",
+		"autosize": "4.0.4",
+		"autwh": "0.1.0",
+		"aws-sdk": "2.1013.0",
+		"bcryptjs": "2.4.3",
+		"blurhash": "1.1.4",
+		"broadcast-channel": "4.5.0",
+		"bull": "4.1.0",
+		"cacheable-lookup": "6.0.4",
+		"cafy": "15.2.1",
+		"cbor": "8.1.0",
+		"chalk": "4.1.2",
+		"chart.js": "3.6.0",
+		"chartjs-adapter-date-fns": "2.0.0",
+		"chartjs-plugin-zoom": "1.1.1",
+		"cli-highlight": "2.1.11",
+		"compare-versions": "3.6.0",
+		"concurrently": "6.3.0",
+		"content-disposition": "0.5.3",
+		"crc-32": "1.2.0",
+		"css-loader": "6.5.1",
+		"cssnano": "5.0.10",
+		"date-fns": "2.25.0",
+		"dateformat": "4.5.1",
+		"deep-email-validator": "0.1.18",
+		"escape-regexp": "0.0.1",
+		"eslint": "8.2.0",
+		"eslint-plugin-vue": "8.0.3",
+		"eventemitter3": "4.0.7",
+		"feed": "4.2.2",
+		"file-type": "16.5.3",
+		"fluent-ffmpeg": "2.1.2",
+		"glob": "7.2.0",
+		"got": "11.8.2",
+		"hpagent": "0.1.2",
+		"http-signature": "1.3.5",
+		"idb-keyval": "5.1.3",
+		"insert-text-at-cursor": "0.3.0",
+		"ip-cidr": "3.0.4",
+		"is-svg": "4.3.1",
+		"js-yaml": "4.1.0",
+		"jsdom": "16.7.0",
+		"json5": "2.2.0",
+		"json5-loader": "4.0.1",
+		"jsonld": "5.2.0",
+		"jsrsasign": "8.0.20",
+		"koa": "2.13.4",
+		"koa-bodyparser": "4.3.0",
+		"koa-favicon": "2.1.0",
+		"koa-json-body": "5.3.0",
+		"koa-logger": "3.2.1",
+		"koa-mount": "4.0.0",
+		"koa-send": "5.0.1",
+		"koa-slow": "2.1.0",
+		"koa-views": "7.0.2",
+		"langmap": "0.0.16",
+		"mfm-js": "0.20.0",
+		"misskey-js": "0.0.8",
+		"mocha": "8.4.0",
+		"ms": "2.1.3",
+		"multer": "1.4.3",
+		"nested-property": "4.0.0",
+		"node-fetch": "2.6.1",
+		"nodemailer": "6.7.0",
+		"os-utils": "0.0.14",
+		"parse5": "6.0.1",
+		"pg": "8.7.1",
+		"portscanner": "2.2.0",
+		"prismjs": "1.25.0",
+		"private-ip": "2.3.3",
+		"probe-image-size": "7.2.1",
+		"promise-limit": "2.7.0",
+		"pug": "3.0.2",
+		"punycode": "2.1.1",
+		"pureimage": "0.3.5",
+		"qrcode": "1.4.4",
+		"random-seed": "0.3.0",
+		"ratelimiter": "3.4.1",
+		"re2": "1.16.0",
+		"redis": "3.1.2",
+		"redis-lock": "0.1.4",
+		"reflect-metadata": "0.1.13",
+		"rename": "1.0.4",
+		"request-stats": "3.0.0",
+		"require-all": "3.0.0",
+		"rimraf": "3.0.2",
+		"rndstr": "1.0.0",
+		"s-age": "1.1.2",
+		"seedrandom": "3.0.5",
+		"sharp": "0.29.2",
+		"speakeasy": "2.0.0",
+		"strict-event-emitter-types": "2.0.0",
+		"stringz": "2.1.0",
+		"style-loader": "3.3.1",
+		"summaly": "2.4.1",
+		"syslog-pro": "1.0.0",
+		"systeminformation": "5.9.9",
+		"throttle-debounce": "3.0.1",
+		"tinycolor2": "1.4.2",
+		"tmp": "0.2.1",
+		"ts-loader": "9.2.6",
+		"ts-node": "10.4.0",
+		"tsc-alias": "1.3.10",
+		"tsconfig-paths": "3.11.0",
+		"tslint": "6.1.3",
+		"tslint-sonarts": "1.9.0",
+		"twemoji-parser": "13.1.0",
+		"typeorm": "0.2.39",
+		"typescript": "4.4.4",
+		"ulid": "2.3.0",
+		"uuid": "8.3.2",
+		"web-push": "3.4.5",
+		"websocket": "1.0.34",
+		"ws": "8.2.3",
+		"xev": "2.0.1"
+	},
+	"devDependencies": {
+		"@redocly/openapi-core": "1.0.0-beta.54",
+		"@types/fluent-ffmpeg": "2.1.17",
+		"cross-env": "7.0.3"
+	}
+}
diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts
new file mode 100644
index 0000000000..1723c32dd0
--- /dev/null
+++ b/packages/backend/src/boot/index.ts
@@ -0,0 +1,79 @@
+import * as cluster from 'cluster';
+import * as chalk from 'chalk';
+import Xev from 'xev';
+
+import Logger from '@/services/logger';
+import { envOption } from '../env';
+
+// for typeorm
+import 'reflect-metadata';
+import { masterMain } from './master';
+import { workerMain } from './worker';
+
+const logger = new Logger('core', 'cyan');
+const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
+const ev = new Xev();
+
+/**
+ * Init process
+ */
+export default async function() {
+	process.title = `Misskey (${cluster.isPrimary ? 'master' : 'worker'})`;
+
+	if (cluster.isPrimary || envOption.disableClustering) {
+		await masterMain();
+
+		if (cluster.isPrimary) {
+			ev.mount();
+		}
+	}
+
+	if (cluster.isWorker || envOption.disableClustering) {
+		await workerMain();
+	}
+
+	// ユニットテスト時にMisskeyが子プロセスで起動された時のため
+	// それ以外のときは process.send は使えないので弾く
+	if (process.send) {
+		process.send('ok');
+	}
+}
+
+//#region Events
+
+// Listen new workers
+cluster.on('fork', worker => {
+	clusterLogger.debug(`Process forked: [${worker.id}]`);
+});
+
+// Listen online workers
+cluster.on('online', worker => {
+	clusterLogger.debug(`Process is now online: [${worker.id}]`);
+});
+
+// Listen for dying workers
+cluster.on('exit', worker => {
+	// Replace the dead worker,
+	// we're not sentimental
+	clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
+	cluster.fork();
+});
+
+// Display detail of unhandled promise rejection
+if (!envOption.quiet) {
+	process.on('unhandledRejection', console.dir);
+}
+
+// Display detail of uncaught exception
+process.on('uncaughtException', err => {
+	try {
+		logger.error(err);
+	} catch { }
+});
+
+// Dying away...
+process.on('exit', code => {
+	logger.info(`The process is going to exit with code ${code}`);
+});
+
+//#endregion
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
new file mode 100644
index 0000000000..eeb7017cb0
--- /dev/null
+++ b/packages/backend/src/boot/master.ts
@@ -0,0 +1,194 @@
+import * as fs from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import * as os from 'os';
+import * as cluster from 'cluster';
+import * as chalk from 'chalk';
+import * as portscanner from 'portscanner';
+import { getConnection } from 'typeorm';
+
+import Logger from '@/services/logger';
+import loadConfig from '@/config/load';
+import { Config } from '@/config/types';
+import { lessThan } from '@/prelude/array';
+import { envOption } from '../env';
+import { showMachineInfo } from '@/misc/show-machine-info';
+import { initDb } from '../db/postgre';
+
+//const _filename = fileURLToPath(import.meta.url);
+const _filename = __filename;
+const _dirname = dirname(_filename);
+
+const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
+
+const logger = new Logger('core', 'cyan');
+const bootLogger = logger.createSubLogger('boot', 'magenta', false);
+
+function greet() {
+	if (!envOption.quiet) {
+		//#region Misskey logo
+		const v = `v${meta.version}`;
+		console.log('  _____ _         _           ');
+		console.log(' |     |_|___ ___| |_ ___ _ _ ');
+		console.log(' | | | | |_ -|_ -| \'_| -_| | |');
+		console.log(' |_|_|_|_|___|___|_,_|___|_  |');
+		console.log(' ' + chalk.gray(v) + ('                        |___|\n'.substr(v.length)));
+		//#endregion
+
+		console.log(' Misskey is an open-source decentralized microblogging platform.');
+		console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
+
+		console.log('');
+		console.log(chalk`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`);
+	}
+
+	bootLogger.info('Welcome to Misskey!');
+	bootLogger.info(`Misskey v${meta.version}`, null, true);
+}
+
+function isRoot() {
+	// maybe process.getuid will be undefined under not POSIX environment (e.g. Windows)
+	return process.getuid != null && process.getuid() === 0;
+}
+
+/**
+ * Init master process
+ */
+export async function masterMain() {
+	let config!: Config;
+
+	// initialize app
+	try {
+		greet();
+		showEnvironment();
+		await showMachineInfo(bootLogger);
+		showNodejsVersion();
+		config = loadConfigBoot();
+		await connectDb();
+		await validatePort(config);
+	} catch (e) {
+		bootLogger.error('Fatal error occurred during initialization', null, true);
+		process.exit(1);
+	}
+
+	bootLogger.succ('Misskey initialized');
+
+	if (!envOption.disableClustering) {
+		await spawnWorkers(config.clusterLimit);
+	}
+
+	bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
+
+	if (!envOption.noDaemons) {
+		require('../daemons/server-stats').default();
+		require('../daemons/queue-stats').default();
+		require('../daemons/janitor').default();
+	}
+}
+
+const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
+const requiredNodejsVersion = [11, 7, 0];
+const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
+
+function showEnvironment(): void {
+	const env = process.env.NODE_ENV;
+	const logger = bootLogger.createSubLogger('env');
+	logger.info(typeof env === 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
+
+	if (env !== 'production') {
+		logger.warn('The environment is not in production mode.');
+		logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
+	}
+
+	logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
+}
+
+function showNodejsVersion(): void {
+	const nodejsLogger = bootLogger.createSubLogger('nodejs');
+
+	nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`);
+
+	if (!satisfyNodejsVersion) {
+		nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true);
+		process.exit(1);
+	}
+}
+
+function loadConfigBoot(): Config {
+	const configLogger = bootLogger.createSubLogger('config');
+	let config;
+
+	try {
+		config = loadConfig();
+	} catch (exception) {
+		if (typeof exception === 'string') {
+			configLogger.error(exception);
+			process.exit(1);
+		}
+		if (exception.code === 'ENOENT') {
+			configLogger.error('Configuration file not found', null, true);
+			process.exit(1);
+		}
+		throw exception;
+	}
+
+	configLogger.succ('Loaded');
+
+	return config;
+}
+
+async function connectDb(): Promise<void> {
+	const dbLogger = bootLogger.createSubLogger('db');
+
+	// Try to connect to DB
+	try {
+		dbLogger.info('Connecting...');
+		await initDb();
+		const v = await getConnection().query('SHOW server_version').then(x => x[0].server_version);
+		dbLogger.succ(`Connected: v${v}`);
+	} catch (e) {
+		dbLogger.error('Cannot connect', null, true);
+		dbLogger.error(e);
+		process.exit(1);
+	}
+}
+
+async function validatePort(config: Config): Promise<void> {
+	const isWellKnownPort = (port: number) => port < 1024;
+
+	async function isPortAvailable(port: number): Promise<boolean> {
+		return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
+	}
+
+	if (config.port == null || Number.isNaN(config.port)) {
+		bootLogger.error('The port is not configured. Please configure port.', null, true);
+		process.exit(1);
+	}
+
+	if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
+		bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
+		process.exit(1);
+	}
+
+	if (!await isPortAvailable(config.port)) {
+		bootLogger.error(`Port ${config.port} is already in use`, null, true);
+		process.exit(1);
+	}
+}
+
+async function spawnWorkers(limit: number = 1) {
+	const workers = Math.min(limit, os.cpus().length);
+	bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
+	await Promise.all([...Array(workers)].map(spawnWorker));
+	bootLogger.succ('All workers started');
+}
+
+function spawnWorker(): Promise<void> {
+	return new Promise(res => {
+		const worker = cluster.fork();
+		worker.on('message', message => {
+			if (message !== 'ready') return;
+			res();
+		});
+	});
+}
diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts
new file mode 100644
index 0000000000..362fa3f26b
--- /dev/null
+++ b/packages/backend/src/boot/worker.ts
@@ -0,0 +1,20 @@
+import * as cluster from 'cluster';
+import { initDb } from '../db/postgre';
+
+/**
+ * Init worker process
+ */
+export async function workerMain() {
+	await initDb();
+
+	// start server
+	await require('../server').default();
+
+	// start job queue
+	require('../queue').default();
+
+	if (cluster.isWorker) {
+		// Send a 'ready' message to parent process
+		process.send!('ready');
+	}
+}
diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts
new file mode 100644
index 0000000000..7bfdca4612
--- /dev/null
+++ b/packages/backend/src/config/index.ts
@@ -0,0 +1,3 @@
+import load from './load';
+
+export default load();
diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts
new file mode 100644
index 0000000000..1b5457cdb0
--- /dev/null
+++ b/packages/backend/src/config/load.ts
@@ -0,0 +1,61 @@
+/**
+ * Config loader
+ */
+
+import * as fs from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import * as yaml from 'js-yaml';
+import { Source, Mixin } from './types';
+
+//const _filename = fileURLToPath(import.meta.url);
+const _filename = __filename;
+const _dirname = dirname(_filename);
+
+/**
+ * Path of configuration directory
+ */
+const dir = `${_dirname}/../../../../.config`;
+
+/**
+ * Path of configuration file
+ */
+const path = process.env.NODE_ENV === 'test'
+	? `${dir}/test.yml`
+	: `${dir}/default.yml`;
+
+export default function load() {
+	const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
+	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
+
+	const mixin = {} as Mixin;
+
+	const url = tryCreateUrl(config.url);
+
+	config.url = url.origin;
+
+	config.port = config.port || parseInt(process.env.PORT || '', 10);
+
+	mixin.version = meta.version;
+	mixin.host = url.host;
+	mixin.hostname = url.hostname;
+	mixin.scheme = url.protocol.replace(/:$/, '');
+	mixin.wsScheme = mixin.scheme.replace('http', 'ws');
+	mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
+	mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`;
+	mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
+	mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
+	mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
+
+	if (!config.redis.prefix) config.redis.prefix = mixin.host;
+
+	return Object.assign(config, mixin);
+}
+
+function tryCreateUrl(url: string) {
+	try {
+		return new URL(url);
+	} catch (e) {
+		throw `url="${url}" is not a valid URL.`;
+	}
+}
diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts
new file mode 100644
index 0000000000..e3ca6c1ab6
--- /dev/null
+++ b/packages/backend/src/config/types.ts
@@ -0,0 +1,85 @@
+/**
+ * ユーザーが設定する必要のある情報
+ */
+export type Source = {
+	repository_url?: string;
+	feedback_url?: string;
+	url: string;
+	port: number;
+	https?: { [x: string]: string };
+	disableHsts?: boolean;
+	db: {
+		host: string;
+		port: number;
+		db: string;
+		user: string;
+		pass: string;
+		disableCache?: boolean;
+		extra?: { [x: string]: string };
+	};
+	redis: {
+		host: string;
+		port: number;
+		pass: string;
+		db?: number;
+		prefix?: string;
+	};
+	elasticsearch: {
+		host: string;
+		port: number;
+		ssl?: boolean;
+		user?: string;
+		pass?: string;
+		index?: string;
+	};
+
+	proxy?: string;
+	proxySmtp?: string;
+	proxyBypassHosts?: string[];
+
+	allowedPrivateNetworks?: string[];
+
+	maxFileSize?: number;
+
+	accesslog?: string;
+
+	clusterLimit?: number;
+
+	id: string;
+
+	outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual';
+
+	deliverJobConcurrency?: number;
+	inboxJobConcurrency?: number;
+	deliverJobPerSec?: number;
+	inboxJobPerSec?: number;
+	deliverJobMaxAttempts?: number;
+	inboxJobMaxAttempts?: number;
+
+	syslog: {
+		host: string;
+		port: number;
+	};
+
+	mediaProxy?: string;
+
+	signToActivityPubGet?: boolean;
+};
+
+/**
+ * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報
+ */
+export type Mixin = {
+	version: string;
+	host: string;
+	hostname: string;
+	scheme: string;
+	wsScheme: string;
+	apiUrl: string;
+	wsUrl: string;
+	authUrl: string;
+	driveUrl: string;
+	userAgent: string;
+};
+
+export type Config = Source & Mixin;
diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts
new file mode 100644
index 0000000000..43f59f1e4f
--- /dev/null
+++ b/packages/backend/src/const.ts
@@ -0,0 +1,2 @@
+export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
+export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
diff --git a/packages/backend/src/daemons/janitor.ts b/packages/backend/src/daemons/janitor.ts
new file mode 100644
index 0000000000..72568cfe18
--- /dev/null
+++ b/packages/backend/src/daemons/janitor.ts
@@ -0,0 +1,20 @@
+// TODO: 消したい
+
+const interval = 30 * 60 * 1000;
+import { AttestationChallenges } from '@/models/index';
+import { LessThan } from 'typeorm';
+
+/**
+ * Clean up database occasionally
+ */
+export default function() {
+	async function tick() {
+		await AttestationChallenges.delete({
+			createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000))
+		});
+	}
+
+	tick();
+
+	setInterval(tick, interval);
+}
diff --git a/packages/backend/src/daemons/queue-stats.ts b/packages/backend/src/daemons/queue-stats.ts
new file mode 100644
index 0000000000..77f09b18d6
--- /dev/null
+++ b/packages/backend/src/daemons/queue-stats.ts
@@ -0,0 +1,60 @@
+import Xev from 'xev';
+import { deliverQueue, inboxQueue } from '../queue/queues';
+
+const ev = new Xev();
+
+const interval = 10000;
+
+/**
+ * Report queue stats regularly
+ */
+export default function() {
+	const log = [] as any[];
+
+	ev.on('requestQueueStatsLog', x => {
+		ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50));
+	});
+
+	let activeDeliverJobs = 0;
+	let activeInboxJobs = 0;
+
+	deliverQueue.on('global:active', () => {
+		activeDeliverJobs++;
+	});
+
+	inboxQueue.on('global:active', () => {
+		activeInboxJobs++;
+	});
+
+	async function tick() {
+		const deliverJobCounts = await deliverQueue.getJobCounts();
+		const inboxJobCounts = await inboxQueue.getJobCounts();
+
+		const stats = {
+			deliver: {
+				activeSincePrevTick: activeDeliverJobs,
+				active: deliverJobCounts.active,
+				waiting: deliverJobCounts.waiting,
+				delayed: deliverJobCounts.delayed
+			},
+			inbox: {
+				activeSincePrevTick: activeInboxJobs,
+				active: inboxJobCounts.active,
+				waiting: inboxJobCounts.waiting,
+				delayed: inboxJobCounts.delayed
+			},
+		};
+
+		ev.emit('queueStats', stats);
+
+		log.unshift(stats);
+		if (log.length > 200) log.pop();
+
+		activeDeliverJobs = 0;
+		activeInboxJobs = 0;
+	}
+
+	tick();
+
+	setInterval(tick, interval);
+}
diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts
new file mode 100644
index 0000000000..8dfa946250
--- /dev/null
+++ b/packages/backend/src/daemons/server-stats.ts
@@ -0,0 +1,79 @@
+import * as si from 'systeminformation';
+import Xev from 'xev';
+import * as osUtils from 'os-utils';
+
+const ev = new Xev();
+
+const interval = 2000;
+
+const roundCpu = (num: number) => Math.round(num * 1000) / 1000;
+const round = (num: number) => Math.round(num * 10) / 10;
+
+/**
+ * Report server stats regularly
+ */
+export default function() {
+	const log = [] as any[];
+
+	ev.on('requestServerStatsLog', x => {
+		ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
+	});
+
+	async function tick() {
+		const cpu = await cpuUsage();
+		const memStats = await mem();
+		const netStats = await net();
+		const fsStats = await fs();
+
+		const stats = {
+			cpu: roundCpu(cpu),
+			mem: {
+				used: round(memStats.used - memStats.buffers - memStats.cached),
+				active: round(memStats.active),
+			},
+			net: {
+				rx: round(Math.max(0, netStats.rx_sec)),
+				tx: round(Math.max(0, netStats.tx_sec)),
+			},
+			fs: {
+				r: round(Math.max(0, fsStats.rIO_sec)),
+				w: round(Math.max(0, fsStats.wIO_sec)),
+			}
+		};
+		ev.emit('serverStats', stats);
+		log.unshift(stats);
+		if (log.length > 200) log.pop();
+	}
+
+	tick();
+
+	setInterval(tick, interval);
+}
+
+// CPU STAT
+function cpuUsage() {
+	return new Promise((res, rej) => {
+		osUtils.cpuUsage((cpuUsage: number) => {
+			res(cpuUsage);
+		});
+	});
+}
+
+// MEMORY STAT
+async function mem() {
+	const data = await si.mem();
+	return data;
+}
+
+// NETWORK STAT
+async function net() {
+	const iface = await si.networkInterfaceDefault();
+	const data = await si.networkStats(iface);
+	return data[0];
+}
+
+// FS STAT
+async function fs() {
+	const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
+	return data || { rIO_sec: 0, wIO_sec: 0 };
+}
diff --git a/packages/backend/src/db/elasticsearch.ts b/packages/backend/src/db/elasticsearch.ts
new file mode 100644
index 0000000000..c99183007a
--- /dev/null
+++ b/packages/backend/src/db/elasticsearch.ts
@@ -0,0 +1,56 @@
+import * as elasticsearch from '@elastic/elasticsearch';
+import config from '@/config/index';
+
+const index = {
+	settings: {
+		analysis: {
+			analyzer: {
+				ngram: {
+					tokenizer: 'ngram'
+				}
+			}
+		}
+	},
+	mappings: {
+		properties: {
+			text: {
+				type: 'text',
+				index: true,
+				analyzer: 'ngram',
+			},
+			userId: {
+				type: 'keyword',
+				index: true,
+			},
+			userHost: {
+				type: 'keyword',
+				index: true,
+			}
+		}
+	}
+};
+
+// Init ElasticSearch connection
+const client = config.elasticsearch ? new elasticsearch.Client({
+	node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`,
+	auth: (config.elasticsearch.user && config.elasticsearch.pass) ? {
+		username: config.elasticsearch.user,
+		password: config.elasticsearch.pass
+	} : undefined,
+	pingTimeout: 30000
+}) : null;
+
+if (client) {
+	client.indices.exists({
+		index: config.elasticsearch.index || 'misskey_note',
+	}).then(exist => {
+		if (!exist.body) {
+			client.indices.create({
+				index: config.elasticsearch.index || 'misskey_note',
+				body: index
+			});
+		}
+	});
+}
+
+export default client;
diff --git a/packages/backend/src/db/logger.ts b/packages/backend/src/db/logger.ts
new file mode 100644
index 0000000000..62f90555a0
--- /dev/null
+++ b/packages/backend/src/db/logger.ts
@@ -0,0 +1,3 @@
+import Logger from '@/services/logger';
+
+export const dbLogger = new Logger('db');
diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts
new file mode 100644
index 0000000000..f52c2ab722
--- /dev/null
+++ b/packages/backend/src/db/postgre.ts
@@ -0,0 +1,225 @@
+// https://github.com/typeorm/typeorm/issues/2400
+const types = require('pg').types;
+types.setTypeParser(20, Number);
+
+import { createConnection, Logger, getConnection } from 'typeorm';
+import config from '@/config/index';
+import { entities as charts } from '@/services/chart/entities';
+import { dbLogger } from './logger';
+import * as highlight from 'cli-highlight';
+
+import { User } from '@/models/entities/user';
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFolder } from '@/models/entities/drive-folder';
+import { AccessToken } from '@/models/entities/access-token';
+import { App } from '@/models/entities/app';
+import { PollVote } from '@/models/entities/poll-vote';
+import { Note } from '@/models/entities/note';
+import { NoteReaction } from '@/models/entities/note-reaction';
+import { NoteWatching } from '@/models/entities/note-watching';
+import { NoteThreadMuting } from '@/models/entities/note-thread-muting';
+import { NoteUnread } from '@/models/entities/note-unread';
+import { Notification } from '@/models/entities/notification';
+import { Meta } from '@/models/entities/meta';
+import { Following } from '@/models/entities/following';
+import { Instance } from '@/models/entities/instance';
+import { Muting } from '@/models/entities/muting';
+import { SwSubscription } from '@/models/entities/sw-subscription';
+import { Blocking } from '@/models/entities/blocking';
+import { UserList } from '@/models/entities/user-list';
+import { UserListJoining } from '@/models/entities/user-list-joining';
+import { UserGroup } from '@/models/entities/user-group';
+import { UserGroupJoining } from '@/models/entities/user-group-joining';
+import { UserGroupInvitation } from '@/models/entities/user-group-invitation';
+import { Hashtag } from '@/models/entities/hashtag';
+import { NoteFavorite } from '@/models/entities/note-favorite';
+import { AbuseUserReport } from '@/models/entities/abuse-user-report';
+import { RegistrationTicket } from '@/models/entities/registration-tickets';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { Signin } from '@/models/entities/signin';
+import { AuthSession } from '@/models/entities/auth-session';
+import { FollowRequest } from '@/models/entities/follow-request';
+import { Emoji } from '@/models/entities/emoji';
+import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { ReversiMatching } from '@/models/entities/games/reversi/matching';
+import { UserNotePining } from '@/models/entities/user-note-pining';
+import { Poll } from '@/models/entities/poll';
+import { UserKeypair } from '@/models/entities/user-keypair';
+import { UserPublickey } from '@/models/entities/user-publickey';
+import { UserProfile } from '@/models/entities/user-profile';
+import { UserSecurityKey } from '@/models/entities/user-security-key';
+import { AttestationChallenge } from '@/models/entities/attestation-challenge';
+import { Page } from '@/models/entities/page';
+import { PageLike } from '@/models/entities/page-like';
+import { GalleryPost } from '@/models/entities/gallery-post';
+import { GalleryLike } from '@/models/entities/gallery-like';
+import { ModerationLog } from '@/models/entities/moderation-log';
+import { UsedUsername } from '@/models/entities/used-username';
+import { Announcement } from '@/models/entities/announcement';
+import { AnnouncementRead } from '@/models/entities/announcement-read';
+import { Clip } from '@/models/entities/clip';
+import { ClipNote } from '@/models/entities/clip-note';
+import { Antenna } from '@/models/entities/antenna';
+import { AntennaNote } from '@/models/entities/antenna-note';
+import { PromoNote } from '@/models/entities/promo-note';
+import { PromoRead } from '@/models/entities/promo-read';
+import { envOption } from '../env';
+import { Relay } from '@/models/entities/relay';
+import { MutedNote } from '@/models/entities/muted-note';
+import { Channel } from '@/models/entities/channel';
+import { ChannelFollowing } from '@/models/entities/channel-following';
+import { ChannelNotePining } from '@/models/entities/channel-note-pining';
+import { RegistryItem } from '@/models/entities/registry-item';
+import { Ad } from '@/models/entities/ad';
+import { PasswordResetRequest } from '@/models/entities/password-reset-request';
+import { UserPending } from '@/models/entities/user-pending';
+
+const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
+
+class MyCustomLogger implements Logger {
+	private highlight(sql: string) {
+		return highlight.highlight(sql, {
+			language: 'sql', ignoreIllegals: true,
+		});
+	}
+
+	public logQuery(query: string, parameters?: any[]) {
+		if (envOption.verbose) {
+			sqlLogger.info(this.highlight(query));
+		}
+	}
+
+	public logQueryError(error: string, query: string, parameters?: any[]) {
+		sqlLogger.error(this.highlight(query));
+	}
+
+	public logQuerySlow(time: number, query: string, parameters?: any[]) {
+		sqlLogger.warn(this.highlight(query));
+	}
+
+	public logSchemaBuild(message: string) {
+		sqlLogger.info(message);
+	}
+
+	public log(message: string) {
+		sqlLogger.info(message);
+	}
+
+	public logMigration(message: string) {
+		sqlLogger.info(message);
+	}
+}
+
+export const entities = [
+	Announcement,
+	AnnouncementRead,
+	Meta,
+	Instance,
+	App,
+	AuthSession,
+	AccessToken,
+	User,
+	UserProfile,
+	UserKeypair,
+	UserPublickey,
+	UserList,
+	UserListJoining,
+	UserGroup,
+	UserGroupJoining,
+	UserGroupInvitation,
+	UserNotePining,
+	UserSecurityKey,
+	UsedUsername,
+	AttestationChallenge,
+	Following,
+	FollowRequest,
+	Muting,
+	Blocking,
+	Note,
+	NoteFavorite,
+	NoteReaction,
+	NoteWatching,
+	NoteThreadMuting,
+	NoteUnread,
+	Page,
+	PageLike,
+	GalleryPost,
+	GalleryLike,
+	DriveFile,
+	DriveFolder,
+	Poll,
+	PollVote,
+	Notification,
+	Emoji,
+	Hashtag,
+	SwSubscription,
+	AbuseUserReport,
+	RegistrationTicket,
+	MessagingMessage,
+	Signin,
+	ModerationLog,
+	Clip,
+	ClipNote,
+	Antenna,
+	AntennaNote,
+	PromoNote,
+	PromoRead,
+	ReversiGame,
+	ReversiMatching,
+	Relay,
+	MutedNote,
+	Channel,
+	ChannelFollowing,
+	ChannelNotePining,
+	RegistryItem,
+	Ad,
+	PasswordResetRequest,
+	UserPending,
+	...charts as any
+];
+
+export function initDb(justBorrow = false, sync = false, forceRecreate = false) {
+	if (!forceRecreate) {
+		try {
+			const conn = getConnection();
+			return Promise.resolve(conn);
+		} catch (e) {}
+	}
+
+	const log = process.env.NODE_ENV != 'production';
+
+	return createConnection({
+		type: 'postgres',
+		host: config.db.host,
+		port: config.db.port,
+		username: config.db.user,
+		password: config.db.pass,
+		database: config.db.db,
+		extra: config.db.extra,
+		synchronize: process.env.NODE_ENV === 'test' || sync,
+		dropSchema: process.env.NODE_ENV === 'test' && !justBorrow,
+		cache: !config.db.disableCache ? {
+			type: 'redis',
+			options: {
+				host: config.redis.host,
+				port: config.redis.port,
+				password: config.redis.pass,
+				prefix: `${config.redis.prefix}:query:`,
+				db: config.redis.db || 0
+			}
+		} : false,
+		logging: log,
+		logger: log ? new MyCustomLogger() : undefined,
+		entities: entities
+	});
+}
+
+export async function resetDb() {
+	const conn = await getConnection();
+	const tables = await conn.query(`SELECT relname AS "table"
+	FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
+	WHERE nspname NOT IN ('pg_catalog', 'information_schema')
+		AND C.relkind = 'r'
+		AND nspname !~ '^pg_toast';`);
+	await Promise.all(tables.map(t => t.table).map(x => conn.query(`DELETE FROM "${x}" CASCADE`)));
+}
diff --git a/packages/backend/src/db/redis.ts b/packages/backend/src/db/redis.ts
new file mode 100644
index 0000000000..7e0ee1e3ce
--- /dev/null
+++ b/packages/backend/src/db/redis.ts
@@ -0,0 +1,19 @@
+import * as redis from 'redis';
+import config from '@/config/index';
+
+export function createConnection() {
+	return redis.createClient(
+		config.redis.port,
+		config.redis.host,
+		{
+			password: config.redis.pass,
+			prefix: config.redis.prefix,
+			db: config.redis.db || 0
+		}
+	);
+}
+
+export const subsdcriber = createConnection();
+subsdcriber.subscribe(config.host);
+
+export const redisClient = createConnection();
diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts
new file mode 100644
index 0000000000..1b678edc44
--- /dev/null
+++ b/packages/backend/src/env.ts
@@ -0,0 +1,20 @@
+const envOption = {
+	onlyQueue: false,
+	onlyServer: false,
+	noDaemons: false,
+	disableClustering: false,
+	verbose: false,
+	withLogTime: false,
+	quiet: false,
+	slow: false,
+};
+
+for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
+	if (process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]) envOption[key] = true;
+}
+
+if (process.env.NODE_ENV === 'test') envOption.disableClustering = true;
+if (process.env.NODE_ENV === 'test') envOption.quiet = true;
+if (process.env.NODE_ENV === 'test') envOption.noDaemons = true;
+
+export { envOption };
diff --git a/packages/backend/src/games/reversi/core.ts b/packages/backend/src/games/reversi/core.ts
new file mode 100644
index 0000000000..9bfce9834a
--- /dev/null
+++ b/packages/backend/src/games/reversi/core.ts
@@ -0,0 +1,263 @@
+import { count, concat } from '@/prelude/array';
+
+// MISSKEY REVERSI ENGINE
+
+/**
+ * true ... 黒
+ * false ... 白
+ */
+export type Color = boolean;
+const BLACK = true;
+const WHITE = false;
+
+export type MapPixel = 'null' | 'empty';
+
+export type Options = {
+	isLlotheo: boolean;
+	canPutEverywhere: boolean;
+	loopedBoard: boolean;
+};
+
+export type Undo = {
+	/**
+	 * 色
+	 */
+	color: Color;
+
+	/**
+	 * どこに打ったか
+	 */
+	pos: number;
+
+	/**
+	 * 反転した石の位置の配列
+	 */
+	effects: number[];
+
+	/**
+	 * ターン
+	 */
+	turn: Color | null;
+};
+
+/**
+ * リバーシエンジン
+ */
+export default class Reversi {
+	public map: MapPixel[];
+	public mapWidth: number;
+	public mapHeight: number;
+	public board: (Color | null | undefined)[];
+	public turn: Color | null = BLACK;
+	public opts: Options;
+
+	public prevPos = -1;
+	public prevColor: Color | null = null;
+
+	private logs: Undo[] = [];
+
+	/**
+	 * ゲームを初期化します
+	 */
+	constructor(map: string[], opts: Options) {
+		//#region binds
+		this.put = this.put.bind(this);
+		//#endregion
+
+		//#region Options
+		this.opts = opts;
+		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
+		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
+		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
+		//#endregion
+
+		//#region Parse map data
+		this.mapWidth = map[0].length;
+		this.mapHeight = map.length;
+		const mapData = map.join('');
+
+		this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
+
+		this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
+		//#endregion
+
+		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
+		if (!this.canPutSomewhere(BLACK))
+			this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
+	}
+
+	/**
+	 * 黒石の数
+	 */
+	public get blackCount() {
+		return count(BLACK, this.board);
+	}
+
+	/**
+	 * 白石の数
+	 */
+	public get whiteCount() {
+		return count(WHITE, this.board);
+	}
+
+	public transformPosToXy(pos: number): number[] {
+		const x = pos % this.mapWidth;
+		const y = Math.floor(pos / this.mapWidth);
+		return [x, y];
+	}
+
+	public transformXyToPos(x: number, y: number): number {
+		return x + (y * this.mapWidth);
+	}
+
+	/**
+	 * 指定のマスに石を打ちます
+	 * @param color 石の色
+	 * @param pos 位置
+	 */
+	public put(color: Color, pos: number) {
+		this.prevPos = pos;
+		this.prevColor = color;
+
+		this.board[pos] = color;
+
+		// 反転させられる石を取得
+		const effects = this.effects(color, pos);
+
+		// 反転させる
+		for (const pos of effects) {
+			this.board[pos] = color;
+		}
+
+		const turn = this.turn;
+
+		this.logs.push({
+			color,
+			pos,
+			effects,
+			turn
+		});
+
+		this.calcTurn();
+	}
+
+	private calcTurn() {
+		// ターン計算
+		this.turn =
+			this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
+			this.canPutSomewhere(this.prevColor!) ? this.prevColor :
+			null;
+	}
+
+	public undo() {
+		const undo = this.logs.pop()!;
+		this.prevColor = undo.color;
+		this.prevPos = undo.pos;
+		this.board[undo.pos] = null;
+		for (const pos of undo.effects) {
+			const color = this.board[pos];
+			this.board[pos] = !color;
+		}
+		this.turn = undo.turn;
+	}
+
+	/**
+	 * 指定した位置のマップデータのマスを取得します
+	 * @param pos 位置
+	 */
+	public mapDataGet(pos: number): MapPixel {
+		const [x, y] = this.transformPosToXy(pos);
+		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
+	}
+
+	/**
+	 * 打つことができる場所を取得します
+	 */
+	public puttablePlaces(color: Color): number[] {
+		return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
+	}
+
+	/**
+	 * 打つことができる場所があるかどうかを取得します
+	 */
+	public canPutSomewhere(color: Color): boolean {
+		return this.puttablePlaces(color).length > 0;
+	}
+
+	/**
+	 * 指定のマスに石を打つことができるかどうかを取得します
+	 * @param color 自分の色
+	 * @param pos 位置
+	 */
+	public canPut(color: Color, pos: number): boolean {
+		return (
+			this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
+			this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
+			this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
+	}
+
+	/**
+	 * 指定のマスに石を置いた時の、反転させられる石を取得します
+	 * @param color 自分の色
+	 * @param initPos 位置
+	 */
+	public effects(color: Color, initPos: number): number[] {
+		const enemyColor = !color;
+
+		const diffVectors: [number, number][] = [
+			[  0,  -1], // 上
+			[ +1,  -1], // 右上
+			[ +1,   0], // 右
+			[ +1,  +1], // 右下
+			[  0,  +1], // 下
+			[ -1,  +1], // 左下
+			[ -1,   0], // 左
+			[ -1,  -1]  // 左上
+		];
+
+		const effectsInLine = ([dx, dy]: [number, number]): number[] => {
+			const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
+
+			const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
+			let [x, y] = this.transformPosToXy(initPos);
+			while (true) {
+				[x, y] = nextPos(x, y);
+
+				// 座標が指し示す位置がボード外に出たとき
+				if (this.opts.loopedBoard && this.transformXyToPos(
+					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
+					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos)
+						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
+					return found;
+				else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight)
+					return []; // 挟めないことが確定 (盤面外に到達)
+
+				const pos = this.transformXyToPos(x, y);
+				if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
+				const stone = this.board[pos];
+				if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
+				if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
+				if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
+			}
+		};
+
+		return concat(diffVectors.map(effectsInLine));
+	}
+
+	/**
+	 * ゲームが終了したか否か
+	 */
+	public get isEnded(): boolean {
+		return this.turn === null;
+	}
+
+	/**
+	 * ゲームの勝者 (null = 引き分け)
+	 */
+	public get winner(): Color | null {
+		return this.isEnded ?
+			this.blackCount == this.whiteCount ? null :
+			this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
+			undefined as never;
+	}
+}
diff --git a/packages/backend/src/games/reversi/maps.ts b/packages/backend/src/games/reversi/maps.ts
new file mode 100644
index 0000000000..dc0d1bf9d0
--- /dev/null
+++ b/packages/backend/src/games/reversi/maps.ts
@@ -0,0 +1,896 @@
+/**
+ * 組み込みマップ定義
+ *
+ * データ値:
+ * (スペース) ... マス無し
+ * - ... マス
+ * b ... 初期配置される黒石
+ * w ... 初期配置される白石
+ */
+
+export type Map = {
+	name?: string;
+	category?: string;
+	author?: string;
+	data: string[];
+};
+
+export const fourfour: Map = {
+	name: '4x4',
+	category: '4x4',
+	data: [
+		'----',
+		'-wb-',
+		'-bw-',
+		'----'
+	]
+};
+
+export const sixsix: Map = {
+	name: '6x6',
+	category: '6x6',
+	data: [
+		'------',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
+		'------'
+	]
+};
+
+export const roundedSixsix: Map = {
+	name: '6x6 rounded',
+	category: '6x6',
+	author: 'syuilo',
+	data: [
+		' ---- ',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
+		' ---- '
+	]
+};
+
+export const roundedSixsix2: Map = {
+	name: '6x6 rounded 2',
+	category: '6x6',
+	author: 'syuilo',
+	data: [
+		'  --  ',
+		' ---- ',
+		'--wb--',
+		'--bw--',
+		' ---- ',
+		'  --  '
+	]
+};
+
+export const eighteight: Map = {
+	name: '8x8',
+	category: '8x8',
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const eighteightH1: Map = {
+	name: '8x8 handicap 1',
+	category: '8x8',
+	data: [
+		'b-------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const eighteightH2: Map = {
+	name: '8x8 handicap 2',
+	category: '8x8',
+	data: [
+		'b-------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'-------b'
+	]
+};
+
+export const eighteightH3: Map = {
+	name: '8x8 handicap 3',
+	category: '8x8',
+	data: [
+		'b------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'-------b'
+	]
+};
+
+export const eighteightH4: Map = {
+	name: '8x8 handicap 4',
+	category: '8x8',
+	data: [
+		'b------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'b------b'
+	]
+};
+
+export const eighteightH28: Map = {
+	name: '8x8 handicap 28',
+	category: '8x8',
+	data: [
+		'bbbbbbbb',
+		'b------b',
+		'b------b',
+		'b--wb--b',
+		'b--bw--b',
+		'b------b',
+		'b------b',
+		'bbbbbbbb'
+	]
+};
+
+export const roundedEighteight: Map = {
+	name: '8x8 rounded',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		' ------ ',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		' ------ '
+	]
+};
+
+export const roundedEighteight2: Map = {
+	name: '8x8 rounded 2',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'  ----  ',
+		' ------ ',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		' ------ ',
+		'  ----  '
+	]
+};
+
+export const roundedEighteight3: Map = {
+	name: '8x8 rounded 3',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'   --   ',
+		'  ----  ',
+		' ------ ',
+		'---wb---',
+		'---bw---',
+		' ------ ',
+		'  ----  ',
+		'   --   '
+	]
+};
+
+export const eighteightWithNotch: Map = {
+	name: '8x8 with notch',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'---  ---',
+		'--------',
+		'--------',
+		' --wb-- ',
+		' --bw-- ',
+		'--------',
+		'--------',
+		'---  ---'
+	]
+};
+
+export const eighteightWithSomeHoles: Map = {
+	name: '8x8 with some holes',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'--- ----',
+		'----- --',
+		'-- -----',
+		'---wb---',
+		'---bw- -',
+		' -------',
+		'--- ----',
+		'--------'
+	]
+};
+
+export const circle: Map = {
+	name: 'Circle',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'   --   ',
+		' ------ ',
+		' ------ ',
+		'---wb---',
+		'---bw---',
+		' ------ ',
+		' ------ ',
+		'   --   '
+	]
+};
+
+export const smile: Map = {
+	name: 'Smile',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		' ------ ',
+		'--------',
+		'-- -- --',
+		'---wb---',
+		'-- bw --',
+		'---  ---',
+		'--------',
+		' ------ '
+	]
+};
+
+export const window: Map = {
+	name: 'Window',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'--------',
+		'-  --  -',
+		'-  --  -',
+		'---wb---',
+		'---bw---',
+		'-  --  -',
+		'-  --  -',
+		'--------'
+	]
+};
+
+export const reserved: Map = {
+	name: 'Reserved',
+	category: '8x8',
+	author: 'Aya',
+	data: [
+		'w------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'b------w'
+	]
+};
+
+export const x: Map = {
+	name: 'X',
+	category: '8x8',
+	author: 'Aya',
+	data: [
+		'w------b',
+		'-w----b-',
+		'--w--b--',
+		'---wb---',
+		'---bw---',
+		'--b--w--',
+		'-b----w-',
+		'b------w'
+	]
+};
+
+export const parallel: Map = {
+	name: 'Parallel',
+	category: '8x8',
+	author: 'Aya',
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---bb---',
+		'---ww---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const lackOfBlack: Map = {
+	name: 'Lack of Black',
+	category: '8x8',
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---w----',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const squareParty: Map = {
+	name: 'Square Party',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'--------',
+		'-wwwbbb-',
+		'-w-wb-b-',
+		'-wwwbbb-',
+		'-bbbwww-',
+		'-b-bw-w-',
+		'-bbbwww-',
+		'--------'
+	]
+};
+
+export const minesweeper: Map = {
+	name: 'Minesweeper',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'b-b--w-w',
+		'-w-wb-b-',
+		'w-b--w-b',
+		'-b-wb-w-',
+		'-w-bw-b-',
+		'b-w--b-w',
+		'-b-bw-w-',
+		'w-w--b-b'
+	]
+};
+
+export const tenthtenth: Map = {
+	name: '10x10',
+	category: '10x10',
+	data: [
+		'----------',
+		'----------',
+		'----------',
+		'----------',
+		'----wb----',
+		'----bw----',
+		'----------',
+		'----------',
+		'----------',
+		'----------'
+	]
+};
+
+export const hole: Map = {
+	name: 'The Hole',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'----------',
+		'----------',
+		'--wb--wb--',
+		'--bw--bw--',
+		'----  ----',
+		'----  ----',
+		'--wb--wb--',
+		'--bw--bw--',
+		'----------',
+		'----------'
+	]
+};
+
+export const grid: Map = {
+	name: 'Grid',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'----------',
+		'- - -- - -',
+		'----------',
+		'- - -- - -',
+		'----wb----',
+		'----bw----',
+		'- - -- - -',
+		'----------',
+		'- - -- - -',
+		'----------'
+	]
+};
+
+export const cross: Map = {
+	name: 'Cross',
+	category: '10x10',
+	author: 'Aya',
+	data: [
+		'   ----   ',
+		'   ----   ',
+		'   ----   ',
+		'----------',
+		'----wb----',
+		'----bw----',
+		'----------',
+		'   ----   ',
+		'   ----   ',
+		'   ----   '
+	]
+};
+
+export const charX: Map = {
+	name: 'Char X',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'---    ---',
+		'----  ----',
+		'----------',
+		' -------- ',
+		'  --wb--  ',
+		'  --bw--  ',
+		' -------- ',
+		'----------',
+		'----  ----',
+		'---    ---'
+	]
+};
+
+export const charY: Map = {
+	name: 'Char Y',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'---    ---',
+		'----  ----',
+		'----------',
+		' -------- ',
+		'  --wb--  ',
+		'  --bw--  ',
+		'  ------  ',
+		'  ------  ',
+		'  ------  ',
+		'  ------  '
+	]
+};
+
+export const walls: Map = {
+	name: 'Walls',
+	category: '10x10',
+	author: 'Aya',
+	data: [
+		' bbbbbbbb ',
+		'w--------w',
+		'w--------w',
+		'w--------w',
+		'w---wb---w',
+		'w---bw---w',
+		'w--------w',
+		'w--------w',
+		'w--------w',
+		' bbbbbbbb '
+	]
+};
+
+export const cpu: Map = {
+	name: 'CPU',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		' b b  b b ',
+		'w--------w',
+		' -------- ',
+		'w--------w',
+		' ---wb--- ',
+		' ---bw--- ',
+		'w--------w',
+		' -------- ',
+		'w--------w',
+		' b b  b b '
+	]
+};
+
+export const checker: Map = {
+	name: 'Checker',
+	category: '10x10',
+	author: 'Aya',
+	data: [
+		'----------',
+		'----------',
+		'----------',
+		'---wbwb---',
+		'---bwbw---',
+		'---wbwb---',
+		'---bwbw---',
+		'----------',
+		'----------',
+		'----------'
+	]
+};
+
+export const japaneseCurry: Map = {
+	name: 'Japanese curry',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'w-b-b-b-b-',
+		'-w-b-b-b-b',
+		'w-w-b-b-b-',
+		'-w-w-b-b-b',
+		'w-w-wwb-b-',
+		'-w-wbb-b-b',
+		'w-w-w-b-b-',
+		'-w-w-w-b-b',
+		'w-w-w-w-b-',
+		'-w-w-w-w-b'
+	]
+};
+
+export const mosaic: Map = {
+	name: 'Mosaic',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'- - - - - ',
+		' - - - - -',
+		'- - - - - ',
+		' - w w - -',
+		'- - b b - ',
+		' - w w - -',
+		'- - b b - ',
+		' - - - - -',
+		'- - - - - ',
+		' - - - - -',
+	]
+};
+
+export const arena: Map = {
+	name: 'Arena',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'- - -- - -',
+		' - -  - - ',
+		'- ------ -',
+		' -------- ',
+		'- --wb-- -',
+		'- --bw-- -',
+		' -------- ',
+		'- ------ -',
+		' - -  - - ',
+		'- - -- - -'
+	]
+};
+
+export const reactor: Map = {
+	name: 'Reactor',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'-w------b-',
+		'b- -  - -w',
+		'- --wb-- -',
+		'---b  w---',
+		'- b wb w -',
+		'- w bw b -',
+		'---w  b---',
+		'- --bw-- -',
+		'w- -  - -b',
+		'-b------w-'
+	]
+};
+
+export const sixeight: Map = {
+	name: '6x8',
+	category: 'Special',
+	data: [
+		'------',
+		'------',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
+		'------',
+		'------'
+	]
+};
+
+export const spark: Map = {
+	name: 'Spark',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		' -      - ',
+		'----------',
+		' -------- ',
+		' -------- ',
+		' ---wb--- ',
+		' ---bw--- ',
+		' -------- ',
+		' -------- ',
+		'----------',
+		' -      - '
+	]
+};
+
+export const islands: Map = {
+	name: 'Islands',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'--------  ',
+		'---wb---  ',
+		'---bw---  ',
+		'--------  ',
+		'  -    -  ',
+		'  -    -  ',
+		'  --------',
+		'  --------',
+		'  --------',
+		'  --------'
+	]
+};
+
+export const galaxy: Map = {
+	name: 'Galaxy',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'   ------   ',
+		'  --www---  ',
+		' ------w--- ',
+		'---bbb--w---',
+		'--b---b-w-b-',
+		'-b--wwb-w-b-',
+		'-b-w-bww--b-',
+		'-b-w-b---b--',
+		'---w--bbb---',
+		' ---w------ ',
+		'  ---www--  ',
+		'   ------   '
+	]
+};
+
+export const triangle: Map = {
+	name: 'Triangle',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'    --    ',
+		'    --    ',
+		'   ----   ',
+		'   ----   ',
+		'  --wb--  ',
+		'  --bw--  ',
+		' -------- ',
+		' -------- ',
+		'----------',
+		'----------'
+	]
+};
+
+export const iphonex: Map = {
+	name: 'iPhone X',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		' --  -- ',
+		'--------',
+		'--------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------',
+		'--------',
+		' ------ '
+	]
+};
+
+export const dealWithIt: Map = {
+	name: 'Deal with it!',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'------------',
+		'--w-b-------',
+		' --b-w------',
+		'  --w-b---- ',
+		'   -------  '
+	]
+};
+
+export const experiment: Map = {
+	name: 'Let\'s experiment',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		' ------------ ',
+		'------wb------',
+		'------bw------',
+		'--------------',
+		'    -    -    ',
+		'------  ------',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'wwwwww  bbbbbb'
+	]
+};
+
+export const bigBoard: Map = {
+	name: 'Big board',
+	category: 'Special',
+	data: [
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'-------wb-------',
+		'-------bw-------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------'
+	]
+};
+
+export const twoBoard: Map = {
+	name: 'Two board',
+	category: 'Special',
+	author: 'Aya',
+	data: [
+		'-------- --------',
+		'-------- --------',
+		'-------- --------',
+		'---wb--- ---wb---',
+		'---bw--- ---bw---',
+		'-------- --------',
+		'-------- --------',
+		'-------- --------'
+	]
+};
+
+export const test1: Map = {
+	name: 'Test1',
+	category: 'Test',
+	data: [
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------'
+	]
+};
+
+export const test2: Map = {
+	name: 'Test2',
+	category: 'Test',
+	data: [
+		'------',
+		'------',
+		'-b--w-',
+		'-w--b-',
+		'-w--b-'
+	]
+};
+
+export const test3: Map = {
+	name: 'Test3',
+	category: 'Test',
+	data: [
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'---',
+		'b--',
+	]
+};
+
+export const test4: Map = {
+	name: 'Test4',
+	category: 'Test',
+	data: [
+		'-w--b-',
+		'-w--b-',
+		'------',
+		'-w--b-',
+		'-w--b-'
+	]
+};
+
+// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)A1に打ってしまう
+export const test6: Map = {
+	name: 'Test6',
+	category: 'Test',
+	data: [
+		'--wwwww-',
+		'wwwwwwww',
+		'wbbbwbwb',
+		'wbbbbwbb',
+		'wbwbbwbb',
+		'wwbwbbbb',
+		'--wbbbbb',
+		'-wwwww--',
+	]
+};
+
+// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)G7に打ってしまう
+export const test7: Map = {
+	name: 'Test7',
+	category: 'Test',
+	data: [
+		'b--w----',
+		'b-wwww--',
+		'bwbwwwbb',
+		'wbwwwwb-',
+		'wwwwwww-',
+		'-wwbbwwb',
+		'--wwww--',
+		'--wwww--',
+	]
+};
+
+// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
+export const test8: Map = {
+	name: 'Test8',
+	category: 'Test',
+	data: [
+		'--------',
+		'-----w--',
+		'w--www--',
+		'wwwwww--',
+		'bbbbwww-',
+		'wwwwww--',
+		'--www---',
+		'--ww----',
+	]
+};
diff --git a/packages/backend/src/games/reversi/package.json b/packages/backend/src/games/reversi/package.json
new file mode 100644
index 0000000000..a4415ad141
--- /dev/null
+++ b/packages/backend/src/games/reversi/package.json
@@ -0,0 +1,18 @@
+{
+  "name": "misskey-reversi",
+  "version": "0.0.5",
+  "description": "Misskey reversi engine",
+  "keywords": [
+    "misskey"
+  ],
+  "author": "syuilo <i@syuilo.com>",
+  "license": "MIT",
+  "repository": "https://github.com/misskey-dev/misskey.git",
+  "bugs": "https://github.com/misskey-dev/misskey/issues",
+  "main": "./built/core.js",
+  "types": "./built/core.d.ts",
+  "scripts": {
+    "build": "tsc"
+  },
+  "dependencies": {}
+}
diff --git a/packages/backend/src/games/reversi/tsconfig.json b/packages/backend/src/games/reversi/tsconfig.json
new file mode 100644
index 0000000000..851fb6b7e4
--- /dev/null
+++ b/packages/backend/src/games/reversi/tsconfig.json
@@ -0,0 +1,21 @@
+{
+	"compilerOptions": {
+		"noEmitOnError": false,
+		"noImplicitAny": false,
+		"noImplicitReturns": true,
+		"noFallthroughCasesInSwitch": true,
+		"experimentalDecorators": true,
+		"declaration": true,
+		"sourceMap": false,
+		"target": "es2017",
+		"module": "commonjs",
+		"removeComments": false,
+		"noLib": false,
+		"outDir": "./built",
+		"rootDir": "./"
+	},
+	"compileOnSave": false,
+	"include": [
+		"./core.ts"
+	]
+}
diff --git a/packages/backend/src/global.d.ts b/packages/backend/src/global.d.ts
new file mode 100644
index 0000000000..7343aa1994
--- /dev/null
+++ b/packages/backend/src/global.d.ts
@@ -0,0 +1 @@
+type FIXME = any;
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
new file mode 100644
index 0000000000..5e4e377ab0
--- /dev/null
+++ b/packages/backend/src/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Misskey Entry Point!
+ */
+
+Error.stackTraceLimit = Infinity;
+
+require('events').EventEmitter.defaultMaxListeners = 128;
+
+import boot from './boot/index';
+
+boot();
diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts
new file mode 100644
index 0000000000..de6aa3d0cc
--- /dev/null
+++ b/packages/backend/src/mfm/from-html.ts
@@ -0,0 +1,209 @@
+import * as parse5 from 'parse5';
+import treeAdapter = require('parse5/lib/tree-adapters/default');
+import { URL } from 'url';
+
+const urlRegex     = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
+const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
+
+export function fromHtml(html: string, hashtagNames?: string[]): string | null {
+	if (html == null) return null;
+
+	const dom = parse5.parseFragment(html);
+
+	let text = '';
+
+	for (const n of dom.childNodes) {
+		analyze(n);
+	}
+
+	return text.trim();
+
+	function getText(node: parse5.Node): string {
+		if (treeAdapter.isTextNode(node)) return node.value;
+		if (!treeAdapter.isElementNode(node)) return '';
+		if (node.nodeName === 'br') return '\n';
+
+		if (node.childNodes) {
+			return node.childNodes.map(n => getText(n)).join('');
+		}
+
+		return '';
+	}
+
+	function appendChildren(childNodes: parse5.ChildNode[]): void {
+		if (childNodes) {
+			for (const n of childNodes) {
+				analyze(n);
+			}
+		}
+	}
+
+	function analyze(node: parse5.Node) {
+		if (treeAdapter.isTextNode(node)) {
+			text += node.value;
+			return;
+		}
+
+		// Skip comment or document type node
+		if (!treeAdapter.isElementNode(node)) return;
+
+		switch (node.nodeName) {
+			case 'br':
+				text += '\n';
+				break;
+
+			case 'a':
+			{
+				const txt = getText(node);
+				const rel = node.attrs.find(x => x.name === 'rel');
+				const href = node.attrs.find(x => x.name === 'href');
+
+				// ハッシュタグ
+				if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
+					text += txt;
+				// メンション
+				} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
+					const part = txt.split('@');
+
+					if (part.length === 2 && href) {
+						//#region ホスト名部分が省略されているので復元する
+						const acct = `${txt}@${(new URL(href.value)).hostname}`;
+						text += acct;
+						//#endregion
+					} else if (part.length === 3) {
+						text += txt;
+					}
+				// その他
+				} else {
+					const generateLink = () => {
+						if (!href && !txt) {
+							return '';
+						}
+						if (!href) {
+							return txt;
+						}
+						if (!txt || txt === href.value) {	// #6383: Missing text node
+							if (href.value.match(urlRegexFull)) {
+								return href.value;
+							} else {
+								return `<${href.value}>`;
+							}
+						}
+						if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) {
+							return `[${txt}](<${href.value}>)`;	// #6846
+						} else {
+							return `[${txt}](${href.value})`;
+						}
+					};
+
+					text += generateLink();
+				}
+				break;
+			}
+
+			case 'h1':
+			{
+				text += '【';
+				appendChildren(node.childNodes);
+				text += '】\n';
+				break;
+			}
+
+			case 'b':
+			case 'strong':
+			{
+				text += '**';
+				appendChildren(node.childNodes);
+				text += '**';
+				break;
+			}
+
+			case 'small':
+			{
+				text += '<small>';
+				appendChildren(node.childNodes);
+				text += '</small>';
+				break;
+			}
+
+			case 's':
+			case 'del':
+			{
+				text += '~~';
+				appendChildren(node.childNodes);
+				text += '~~';
+				break;
+			}
+
+			case 'i':
+			case 'em':
+			{
+				text += '<i>';
+				appendChildren(node.childNodes);
+				text += '</i>';
+				break;
+			}
+
+			// block code (<pre><code>)
+			case 'pre': {
+				if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
+					text += '\n```\n';
+					text += getText(node.childNodes[0]);
+					text += '\n```\n';
+				} else {
+					appendChildren(node.childNodes);
+				}
+				break;
+			}
+
+			// inline code (<code>)
+			case 'code': {
+				text += '`';
+				appendChildren(node.childNodes);
+				text += '`';
+				break;
+			}
+
+			case 'blockquote': {
+				const t = getText(node);
+				if (t) {
+					text += '> ';
+					text += t.split('\n').join(`\n> `);
+				}
+				break;
+			}
+
+			case 'p':
+			case 'h2':
+			case 'h3':
+			case 'h4':
+			case 'h5':
+			case 'h6':
+			{
+				text += '\n\n';
+				appendChildren(node.childNodes);
+				break;
+			}
+
+			// other block elements
+			case 'div':
+			case 'header':
+			case 'footer':
+			case 'article':
+			case 'li':
+			case 'dt':
+			case 'dd':
+			{
+				text += '\n';
+				appendChildren(node.childNodes);
+				break;
+			}
+
+			default:	// includes inline elements
+			{
+				appendChildren(node.childNodes);
+				break;
+			}
+		}
+	}
+}
diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts
new file mode 100644
index 0000000000..343c76fb3e
--- /dev/null
+++ b/packages/backend/src/mfm/to-html.ts
@@ -0,0 +1,153 @@
+import { JSDOM } from 'jsdom';
+import * as mfm from 'mfm-js';
+import config from '@/config/index';
+import { intersperse } from '@/prelude/array';
+import { IMentionedRemoteUsers } from '@/models/entities/note';
+
+export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
+	if (nodes == null) {
+		return null;
+	}
+
+	const { window } = new JSDOM('');
+
+	const doc = window.document;
+
+	function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
+		if (children) {
+			for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
+		}
+	}
+
+	const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
+		bold(node) {
+			const el = doc.createElement('b');
+			appendChildren(node.children, el);
+			return el;
+		},
+
+		small(node) {
+			const el = doc.createElement('small');
+			appendChildren(node.children, el);
+			return el;
+		},
+
+		strike(node) {
+			const el = doc.createElement('del');
+			appendChildren(node.children, el);
+			return el;
+		},
+
+		italic(node) {
+			const el = doc.createElement('i');
+			appendChildren(node.children, el);
+			return el;
+		},
+
+		fn(node) {
+			const el = doc.createElement('i');
+			appendChildren(node.children, el);
+			return el;
+		},
+
+		blockCode(node) {
+			const pre = doc.createElement('pre');
+			const inner = doc.createElement('code');
+			inner.textContent = node.props.code;
+			pre.appendChild(inner);
+			return pre;
+		},
+
+		center(node) {
+			const el = doc.createElement('div');
+			appendChildren(node.children, el);
+			return el;
+		},
+
+		emojiCode(node) {
+			return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
+		},
+
+		unicodeEmoji(node) {
+			return doc.createTextNode(node.props.emoji);
+		},
+
+		hashtag(node) {
+			const a = doc.createElement('a');
+			a.href = `${config.url}/tags/${node.props.hashtag}`;
+			a.textContent = `#${node.props.hashtag}`;
+			a.setAttribute('rel', 'tag');
+			return a;
+		},
+
+		inlineCode(node) {
+			const el = doc.createElement('code');
+			el.textContent = node.props.code;
+			return el;
+		},
+
+		mathInline(node) {
+			const el = doc.createElement('code');
+			el.textContent = node.props.formula;
+			return el;
+		},
+
+		mathBlock(node) {
+			const el = doc.createElement('code');
+			el.textContent = node.props.formula;
+			return el;
+		},
+
+		link(node) {
+			const a = doc.createElement('a');
+			a.href = node.props.url;
+			appendChildren(node.children, a);
+			return a;
+		},
+
+		mention(node) {
+			const a = doc.createElement('a');
+			const { username, host, acct } = node.props;
+			const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
+			a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${config.url}/${acct}`;
+			a.className = 'u-url mention';
+			a.textContent = acct;
+			return a;
+		},
+
+		quote(node) {
+			const el = doc.createElement('blockquote');
+			appendChildren(node.children, el);
+			return el;
+		},
+
+		text(node) {
+			const el = doc.createElement('span');
+			const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
+
+			for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
+				el.appendChild(x === 'br' ? doc.createElement('br') : x);
+			}
+
+			return el;
+		},
+
+		url(node) {
+			const a = doc.createElement('a');
+			a.href = node.props.url;
+			a.textContent = node.props.url;
+			return a;
+		},
+
+		search(node) {
+			const a = doc.createElement('a');
+			a.href = `https://www.google.com/search?q=${node.props.query}`;
+			a.textContent = node.props.content;
+			return a;
+		}
+	};
+
+	appendChildren(nodes, doc.body);
+
+	return `<p>${doc.body.innerHTML}</p>`;
+}
diff --git a/packages/backend/src/misc/antenna-cache.ts b/packages/backend/src/misc/antenna-cache.ts
new file mode 100644
index 0000000000..a23eeb45ec
--- /dev/null
+++ b/packages/backend/src/misc/antenna-cache.ts
@@ -0,0 +1,36 @@
+import { Antennas } from '@/models/index';
+import { Antenna } from '@/models/entities/antenna';
+import { subsdcriber } from '../db/redis';
+
+let antennasFetched = false;
+let antennas: Antenna[] = [];
+
+export async function getAntennas() {
+	if (!antennasFetched) {
+		antennas = await Antennas.find();
+		antennasFetched = true;
+	}
+
+	return antennas;
+}
+
+subsdcriber.on('message', async (_, data) => {
+	const obj = JSON.parse(data);
+
+	if (obj.channel === 'internal') {
+		const { type, body } = obj.message;
+		switch (type) {
+			case 'antennaCreated':
+				antennas.push(body);
+				break;
+			case 'antennaUpdated':
+				antennas[antennas.findIndex(a => a.id === body.id)] = body;
+				break;
+			case 'antennaDeleted':
+				antennas = antennas.filter(a => a.id !== body.id);
+				break;
+			default:
+				break;
+		}
+	}
+});
diff --git a/packages/backend/src/misc/api-permissions.ts b/packages/backend/src/misc/api-permissions.ts
new file mode 100644
index 0000000000..160cdf9fd6
--- /dev/null
+++ b/packages/backend/src/misc/api-permissions.ts
@@ -0,0 +1,35 @@
+export const kinds = [
+	'read:account',
+	'write:account',
+	'read:blocks',
+	'write:blocks',
+	'read:drive',
+	'write:drive',
+	'read:favorites',
+	'write:favorites',
+	'read:following',
+	'write:following',
+	'read:messaging',
+	'write:messaging',
+	'read:mutes',
+	'write:mutes',
+	'write:notes',
+	'read:notifications',
+	'write:notifications',
+	'read:reactions',
+	'write:reactions',
+	'write:votes',
+	'read:pages',
+	'write:pages',
+	'write:page-likes',
+	'read:page-likes',
+	'read:user-groups',
+	'write:user-groups',
+	'read:channels',
+	'write:channels',
+	'read:gallery',
+	'write:gallery',
+	'read:gallery-likes',
+	'write:gallery-likes',
+];
+// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).
diff --git a/packages/backend/src/misc/app-lock.ts b/packages/backend/src/misc/app-lock.ts
new file mode 100644
index 0000000000..a32b600612
--- /dev/null
+++ b/packages/backend/src/misc/app-lock.ts
@@ -0,0 +1,31 @@
+import { redisClient } from '../db/redis';
+import { promisify } from 'util';
+import * as redisLock from 'redis-lock';
+
+/**
+ * Retry delay (ms) for lock acquisition
+ */
+const retryDelay = 100;
+
+const lock: (key: string, timeout?: number) => Promise<() => void>
+	= redisClient
+	? promisify(redisLock(redisClient, retryDelay))
+	: async () => () => { };
+
+/**
+ * Get AP Object lock
+ * @param uri AP object ID
+ * @param timeout Lock timeout (ms), The timeout releases previous lock.
+ * @returns Unlock function
+ */
+export function getApLock(uri: string, timeout = 30 * 1000) {
+	return lock(`ap-object:${uri}`, timeout);
+}
+
+export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) {
+	return lock(`instance:${host}`, timeout);
+}
+
+export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) {
+	return lock(`chart-insert:${lockKey}`, timeout);
+}
diff --git a/packages/backend/src/misc/before-shutdown.ts b/packages/backend/src/misc/before-shutdown.ts
new file mode 100644
index 0000000000..33abf5fb4d
--- /dev/null
+++ b/packages/backend/src/misc/before-shutdown.ts
@@ -0,0 +1,92 @@
+// https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58
+
+'use strict';
+
+/**
+ * @callback BeforeShutdownListener
+ * @param {string} [signalOrEvent] The exit signal or event name received on the process.
+ */
+
+/**
+ * System signals the app will listen to initiate shutdown.
+ * @const {string[]}
+ */
+const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
+
+/**
+ * Time in milliseconds to wait before forcing shutdown.
+ * @const {number}
+ */
+const SHUTDOWN_TIMEOUT = 15000;
+
+/**
+ * A queue of listener callbacks to execute before shutting
+ * down the process.
+ * @type {BeforeShutdownListener[]}
+ */
+const shutdownListeners = [];
+
+/**
+ * Listen for signals and execute given `fn` function once.
+ * @param  {string[]} signals System signals to listen to.
+ * @param  {function(string)} fn Function to execute on shutdown.
+ */
+const processOnce = (signals, fn) => {
+	for (const sig of signals) {
+		process.once(sig, fn);
+	}
+};
+
+/**
+ * Sets a forced shutdown mechanism that will exit the process after `timeout` milliseconds.
+ * @param {number} timeout Time to wait before forcing shutdown (milliseconds)
+ */
+const forceExitAfter = timeout => () => {
+	setTimeout(() => {
+		// Force shutdown after timeout
+		console.warn(`Could not close resources gracefully after ${timeout}ms: forcing shutdown`);
+		return process.exit(1);
+	}, timeout).unref();
+};
+
+/**
+ * Main process shutdown handler. Will invoke every previously registered async shutdown listener
+ * in the queue and exit with a code of `0`. Any `Promise` rejections from any listener will
+ * be logged out as a warning, but won't prevent other callbacks from executing.
+ * @param {string} signalOrEvent The exit signal or event name received on the process.
+ */
+async function shutdownHandler(signalOrEvent) {
+	if (process.env.NODE_ENV === 'test') return process.exit(0);
+
+	console.warn(`Shutting down: received [${signalOrEvent}] signal`);
+
+	for (const listener of shutdownListeners) {
+		try {
+			await listener(signalOrEvent);
+		} catch (err) {
+			console.warn(`A shutdown handler failed before completing with: ${err.message || err}`);
+		}
+	}
+
+	return process.exit(0);
+}
+
+/**
+ * Registers a new shutdown listener to be invoked before exiting
+ * the main process. Listener handlers are guaranteed to be called in the order
+ * they were registered.
+ * @param {BeforeShutdownListener} listener The shutdown listener to register.
+ * @returns {BeforeShutdownListener} Echoes back the supplied `listener`.
+ */
+export function beforeShutdown(listener) {
+	shutdownListeners.push(listener);
+	return listener;
+}
+
+// Register shutdown callback that kills the process after `SHUTDOWN_TIMEOUT` milliseconds
+// This prevents custom shutdown handlers from hanging the process indefinitely
+processOnce(SHUTDOWN_SIGNALS, forceExitAfter(SHUTDOWN_TIMEOUT));
+
+// Register process shutdown callback
+// Will listen to incoming signal events and execute all registered handlers in the stack
+processOnce(SHUTDOWN_SIGNALS, shutdownHandler);
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
new file mode 100644
index 0000000000..71fbbd8a4c
--- /dev/null
+++ b/packages/backend/src/misc/cache.ts
@@ -0,0 +1,43 @@
+export class Cache<T> {
+	private cache: Map<string | null, { date: number; value: T; }>;
+	private lifetime: number;
+
+	constructor(lifetime: Cache<never>['lifetime']) {
+		this.cache = new Map();
+		this.lifetime = lifetime;
+	}
+
+	public set(key: string | null, value: T): void {
+		this.cache.set(key, {
+			date: Date.now(),
+			value
+		});
+	}
+
+	public get(key: string | null): T | undefined {
+		const cached = this.cache.get(key);
+		if (cached == null) return undefined;
+		if ((Date.now() - cached.date) > this.lifetime) {
+			this.cache.delete(key);
+			return undefined;
+		}
+		return cached.value;
+	}
+
+	public delete(key: string | null) {
+		this.cache.delete(key);
+	}
+
+	public async fetch(key: string | null, fetcher: () => Promise<T>): Promise<T> {
+		const cachedValue = this.get(key);
+		if (cachedValue !== undefined) {
+			// Cache HIT
+			return cachedValue;
+		}
+
+		// Cache MISS
+		const value = await fetcher();
+		this.set(key, value);
+		return value;
+	}
+}
diff --git a/packages/backend/src/misc/cafy-id.ts b/packages/backend/src/misc/cafy-id.ts
new file mode 100644
index 0000000000..39886611e1
--- /dev/null
+++ b/packages/backend/src/misc/cafy-id.ts
@@ -0,0 +1,32 @@
+import { Context } from 'cafy';
+
+export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> {
+	public readonly name = 'ID';
+
+	constructor(optional = false, nullable = false) {
+		super(optional, nullable);
+
+		this.push((v: any) => {
+			if (typeof v !== 'string') {
+				return new Error('must-be-an-id');
+			}
+			return true;
+		});
+	}
+
+	public getType() {
+		return super.getType('String');
+	}
+
+	public makeOptional(): ID<undefined> {
+		return new ID(true, false);
+	}
+
+	public makeNullable(): ID<null> {
+		return new ID(false, true);
+	}
+
+	public makeOptionalNullable(): ID<undefined | null> {
+		return new ID(true, true);
+	}
+}
diff --git a/packages/backend/src/misc/captcha.ts b/packages/backend/src/misc/captcha.ts
new file mode 100644
index 0000000000..f36943b589
--- /dev/null
+++ b/packages/backend/src/misc/captcha.ts
@@ -0,0 +1,56 @@
+import fetch from 'node-fetch';
+import { URLSearchParams } from 'url';
+import { getAgentByUrl } from './fetch';
+import config from '@/config/index';
+
+export async function verifyRecaptcha(secret: string, response: string) {
+	const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
+		throw `recaptcha-request-failed: ${e}`;
+	});
+
+	if (result.success !== true) {
+		const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
+		throw `recaptcha-failed: ${errorCodes}`;
+	}
+}
+
+export async function verifyHcaptcha(secret: string, response: string) {
+	const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => {
+		throw `hcaptcha-request-failed: ${e}`;
+	});
+
+	if (result.success !== true) {
+		const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
+		throw `hcaptcha-failed: ${errorCodes}`;
+	}
+}
+
+type CaptchaResponse = {
+	success: boolean;
+	'error-codes'?: string[];
+};
+
+async function getCaptchaResponse(url: string, secret: string, response: string): Promise<CaptchaResponse> {
+	const params = new URLSearchParams({
+		secret,
+		response
+	});
+
+	const res = await fetch(url, {
+		method: 'POST',
+		body: params,
+		headers: {
+			'User-Agent': config.userAgent
+		},
+		timeout: 10 * 1000,
+		agent: getAgentByUrl
+	}).catch(e => {
+		throw `${e.message || e}`;
+	});
+
+	if (!res.ok) {
+		throw `${res.status}`;
+	}
+
+	return await res.json() as CaptchaResponse;
+}
diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts
new file mode 100644
index 0000000000..e70b7429c7
--- /dev/null
+++ b/packages/backend/src/misc/check-hit-antenna.ts
@@ -0,0 +1,90 @@
+import { Antenna } from '@/models/entities/antenna';
+import { Note } from '@/models/entities/note';
+import { User } from '@/models/entities/user';
+import { UserListJoinings, UserGroupJoinings } from '@/models/index';
+import { getFullApAccount } from './convert-host';
+import * as Acct from 'misskey-js/built/acct';
+import { Packed } from './schema';
+
+/**
+ * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
+ */
+export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
+	if (note.visibility === 'specified') return false;
+
+	if (note.visibility === 'followers') {
+		if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
+		if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
+	}
+
+	if (!antenna.withReplies && note.replyId != null) return false;
+
+	if (antenna.src === 'home') {
+		if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
+		if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
+	} else if (antenna.src === 'list') {
+		const listUsers = (await UserListJoinings.find({
+			userListId: antenna.userListId!
+		})).map(x => x.userId);
+
+		if (!listUsers.includes(note.userId)) return false;
+	} else if (antenna.src === 'group') {
+		const joining = await UserGroupJoinings.findOneOrFail(antenna.userGroupJoiningId!);
+
+		const groupUsers = (await UserGroupJoinings.find({
+			userGroupId: joining.userGroupId
+		})).map(x => x.userId);
+
+		if (!groupUsers.includes(note.userId)) return false;
+	} else if (antenna.src === 'users') {
+		const accts = antenna.users.map(x => {
+			const { username, host } = Acct.parse(x);
+			return getFullApAccount(username, host).toLowerCase();
+		});
+		if (!accts.includes(getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
+	}
+
+	const keywords = antenna.keywords
+		// Clean up
+		.map(xs => xs.filter(x => x !== ''))
+		.filter(xs => xs.length > 0);
+
+	if (keywords.length > 0) {
+		if (note.text == null) return false;
+
+		const matched = keywords.some(and =>
+			and.every(keyword =>
+				antenna.caseSensitive
+					? note.text!.includes(keyword)
+					: note.text!.toLowerCase().includes(keyword.toLowerCase())
+			));
+
+		if (!matched) return false;
+	}
+
+	const excludeKeywords = antenna.excludeKeywords
+		// Clean up
+		.map(xs => xs.filter(x => x !== ''))
+		.filter(xs => xs.length > 0);
+
+	if (excludeKeywords.length > 0) {
+		if (note.text == null) return false;
+
+		const matched = excludeKeywords.some(and =>
+			and.every(keyword =>
+				antenna.caseSensitive
+					? note.text!.includes(keyword)
+					: note.text!.toLowerCase().includes(keyword.toLowerCase())
+			));
+
+		if (matched) return false;
+	}
+
+	if (antenna.withFile) {
+		if (note.fileIds && note.fileIds.length === 0) return false;
+	}
+
+	// TODO: eval expression
+
+	return true;
+}
diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts
new file mode 100644
index 0000000000..e2e871dd2b
--- /dev/null
+++ b/packages/backend/src/misc/check-word-mute.ts
@@ -0,0 +1,39 @@
+const RE2 = require('re2');
+import { Note } from '@/models/entities/note';
+import { User } from '@/models/entities/user';
+
+type NoteLike = {
+	userId: Note['userId'];
+	text: Note['text'];
+};
+
+type UserLike = {
+	id: User['id'];
+};
+
+export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: string[][]): Promise<boolean> {
+	// 自分自身
+	if (me && (note.userId === me.id)) return false;
+
+	const words = mutedWords
+		// Clean up
+		.map(xs => xs.filter(x => x !== ''))
+		.filter(xs => xs.length > 0);
+
+	if (words.length > 0) {
+		if (note.text == null) return false;
+
+		const matched = words.some(and =>
+			and.every(keyword => {
+				const regexp = keyword.match(/^\/(.+)\/(.*)$/);
+				if (regexp) {
+					return new RE2(regexp[1], regexp[2]).test(note.text!);
+				}
+				return note.text!.includes(keyword);
+			}));
+
+		if (matched) return true;
+	}
+
+	return false;
+}
diff --git a/packages/backend/src/misc/content-disposition.ts b/packages/backend/src/misc/content-disposition.ts
new file mode 100644
index 0000000000..9df7ed4688
--- /dev/null
+++ b/packages/backend/src/misc/content-disposition.ts
@@ -0,0 +1,6 @@
+const cd = require('content-disposition');
+
+export function contentDisposition(type: 'inline' | 'attachment', filename: string): string {
+	const fallback = filename.replace(/[^\w.-]/g, '_');
+	return cd(filename, { type, fallback });
+}
diff --git a/packages/backend/src/misc/convert-host.ts b/packages/backend/src/misc/convert-host.ts
new file mode 100644
index 0000000000..6e9f6ed3e9
--- /dev/null
+++ b/packages/backend/src/misc/convert-host.ts
@@ -0,0 +1,26 @@
+import { URL } from 'url';
+import config from '@/config/index';
+import { toASCII } from 'punycode/';
+
+export function getFullApAccount(username: string, host: string | null) {
+	return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`;
+}
+
+export function isSelfHost(host: string) {
+	if (host == null) return true;
+	return toPuny(config.host) === toPuny(host);
+}
+
+export function extractDbHost(uri: string) {
+	const url = new URL(uri);
+	return toPuny(url.hostname);
+}
+
+export function toPuny(host: string) {
+	return toASCII(host.toLowerCase());
+}
+
+export function toPunyNullable(host: string | null | undefined): string | null {
+	if (host == null) return null;
+	return toASCII(host.toLowerCase());
+}
diff --git a/packages/backend/src/misc/count-same-renotes.ts b/packages/backend/src/misc/count-same-renotes.ts
new file mode 100644
index 0000000000..6628761182
--- /dev/null
+++ b/packages/backend/src/misc/count-same-renotes.ts
@@ -0,0 +1,15 @@
+import { Notes } from '@/models/index';
+
+export async function countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
+	// 指定したユーザーの指定したノートのリノートがいくつあるか数える
+	const query = Notes.createQueryBuilder('note')
+		.where('note.userId = :userId', { userId })
+		.andWhere('note.renoteId = :renoteId', { renoteId });
+
+	// 指定した投稿を除く
+	if (excludeNoteId) {
+		query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
+	}
+
+	return await query.getCount();
+}
diff --git a/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts
new file mode 100644
index 0000000000..04604cf7d0
--- /dev/null
+++ b/packages/backend/src/misc/create-temp.ts
@@ -0,0 +1,10 @@
+import * as tmp from 'tmp';
+
+export function createTemp(): Promise<[string, any]> {
+	return new Promise<[string, any]>((res, rej) => {
+		tmp.file((e, path, fd, cleanup) => {
+			if (e) return rej(e);
+			res([path, cleanup]);
+		});
+	});
+}
diff --git a/packages/backend/src/misc/detect-url-mime.ts b/packages/backend/src/misc/detect-url-mime.ts
new file mode 100644
index 0000000000..274c291737
--- /dev/null
+++ b/packages/backend/src/misc/detect-url-mime.ts
@@ -0,0 +1,15 @@
+import { createTemp } from './create-temp';
+import { downloadUrl } from './download-url';
+import { detectType } from './get-file-info';
+
+export async function detectUrlMime(url: string) {
+	const [path, cleanup] = await createTemp();
+
+	try {
+		await downloadUrl(url, path);
+		const { mime } = await detectType(path);
+		return mime;
+	} finally {
+		cleanup();
+	}
+}
diff --git a/packages/backend/src/misc/download-text-file.ts b/packages/backend/src/misc/download-text-file.ts
new file mode 100644
index 0000000000..e8e23cc120
--- /dev/null
+++ b/packages/backend/src/misc/download-text-file.ts
@@ -0,0 +1,25 @@
+import * as fs from 'fs';
+import * as util from 'util';
+import Logger from '@/services/logger';
+import { createTemp } from './create-temp';
+import { downloadUrl } from './download-url';
+
+const logger = new Logger('download-text-file');
+
+export async function downloadTextFile(url: string): Promise<string> {
+	// Create temp file
+	const [path, cleanup] = await createTemp();
+
+	logger.info(`Temp file is ${path}`);
+
+	try {
+		// write content at URL to temp file
+		await downloadUrl(url, path);
+
+		const text = await util.promisify(fs.readFile)(path, 'utf8');
+
+		return text;
+	} finally {
+		cleanup();
+	}
+}
diff --git a/packages/backend/src/misc/download-url.ts b/packages/backend/src/misc/download-url.ts
new file mode 100644
index 0000000000..c96b4fd1d6
--- /dev/null
+++ b/packages/backend/src/misc/download-url.ts
@@ -0,0 +1,87 @@
+import * as fs from 'fs';
+import * as stream from 'stream';
+import * as util from 'util';
+import got, * as Got from 'got';
+import { httpAgent, httpsAgent, StatusError } from './fetch';
+import config from '@/config/index';
+import * as chalk from 'chalk';
+import Logger from '@/services/logger';
+import * as IPCIDR from 'ip-cidr';
+const PrivateIp = require('private-ip');
+
+const pipeline = util.promisify(stream.pipeline);
+
+export async function downloadUrl(url: string, path: string) {
+	const logger = new Logger('download');
+
+	logger.info(`Downloading ${chalk.cyan(url)} ...`);
+
+	const timeout = 30 * 1000;
+	const operationTimeout = 60 * 1000;
+	const maxSize = config.maxFileSize || 262144000;
+
+	const req = got.stream(url, {
+		headers: {
+			'User-Agent': config.userAgent
+		},
+		timeout: {
+			lookup: timeout,
+			connect: timeout,
+			secureConnect: timeout,
+			socket: timeout,	// read timeout
+			response: timeout,
+			send: timeout,
+			request: operationTimeout,	// whole operation timeout
+		},
+		agent: {
+			http: httpAgent,
+			https: httpsAgent,
+		},
+		http2: false,	// default
+		retry: 0,
+	}).on('response', (res: Got.Response) => {
+		if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
+			if (isPrivateIp(res.ip)) {
+				logger.warn(`Blocked address: ${res.ip}`);
+				req.destroy();
+			}
+		}
+
+		const contentLength = res.headers['content-length'];
+		if (contentLength != null) {
+			const size = Number(contentLength);
+			if (size > maxSize) {
+				logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`);
+				req.destroy();
+			}
+		}
+	}).on('downloadProgress', (progress: Got.Progress) => {
+		if (progress.transferred > maxSize) {
+			logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
+			req.destroy();
+		}
+	});
+
+	try {
+		await pipeline(req, fs.createWriteStream(path));
+	} catch (e) {
+		if (e instanceof Got.HTTPError) {
+			throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
+		} else {
+			throw e;
+		}
+	}
+
+	logger.succ(`Download finished: ${chalk.cyan(url)}`);
+}
+
+function isPrivateIp(ip: string) {
+	for (const net of config.allowedPrivateNetworks || []) {
+		const cidr = new IPCIDR(net);
+		if (cidr.contains(ip)) {
+			return false;
+		}
+	}
+
+	return PrivateIp(ip);
+}
diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts
new file mode 100644
index 0000000000..8b07fbd8f2
--- /dev/null
+++ b/packages/backend/src/misc/emoji-regex.ts
@@ -0,0 +1,3 @@
+const twemojiRegex = require('twemoji-parser/dist/lib/regex').default;
+
+export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);
diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts
new file mode 100644
index 0000000000..b29ce281b3
--- /dev/null
+++ b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts
@@ -0,0 +1,10 @@
+import * as mfm from 'mfm-js';
+import { unique } from '@/prelude/array';
+
+export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
+	const emojiNodes = mfm.extract(nodes, (node) => {
+		return (node.type === 'emojiCode' && node.props.name.length <= 100);
+	});
+
+	return unique(emojiNodes.map(x => x.props.name));
+}
diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts
new file mode 100644
index 0000000000..b0a74df219
--- /dev/null
+++ b/packages/backend/src/misc/extract-hashtags.ts
@@ -0,0 +1,9 @@
+import * as mfm from 'mfm-js';
+import { unique } from '@/prelude/array';
+
+export function extractHashtags(nodes: mfm.MfmNode[]): string[] {
+	const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag');
+	const hashtags = unique(hashtagNodes.map(x => x.props.hashtag));
+
+	return hashtags;
+}
diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts
new file mode 100644
index 0000000000..cc19b161a8
--- /dev/null
+++ b/packages/backend/src/misc/extract-mentions.ts
@@ -0,0 +1,11 @@
+// test is located in test/extract-mentions
+
+import * as mfm from 'mfm-js';
+
+export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] {
+	// TODO: 重複を削除
+	const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention');
+	const mentions = mentionNodes.map(x => x.props);
+
+	return mentions;
+}
diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts
new file mode 100644
index 0000000000..a0bcdd4d48
--- /dev/null
+++ b/packages/backend/src/misc/fetch-meta.ts
@@ -0,0 +1,35 @@
+import { Meta } from '@/models/entities/meta';
+import { getConnection } from 'typeorm';
+
+let cache: Meta;
+
+export async function fetchMeta(noCache = false): Promise<Meta> {
+	if (!noCache && cache) return cache;
+
+	return await getConnection().transaction(async transactionalEntityManager => {
+		// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
+		const meta = await transactionalEntityManager.findOne(Meta, {
+			order: {
+				id: 'DESC'
+			}
+		});
+
+		if (meta) {
+			cache = meta;
+			return meta;
+		} else {
+			const saved = await transactionalEntityManager.save(Meta, {
+				id: 'x'
+			}) as Meta;
+
+			cache = saved;
+			return saved;
+		}
+	});
+}
+
+setInterval(() => {
+	fetchMeta(true).then(meta => {
+		cache = meta;
+	});
+}, 1000 * 10);
diff --git a/packages/backend/src/misc/fetch-proxy-account.ts b/packages/backend/src/misc/fetch-proxy-account.ts
new file mode 100644
index 0000000000..e0eedea4c8
--- /dev/null
+++ b/packages/backend/src/misc/fetch-proxy-account.ts
@@ -0,0 +1,9 @@
+import { fetchMeta } from './fetch-meta';
+import { ILocalUser } from '@/models/entities/user';
+import { Users } from '@/models/index';
+
+export async function fetchProxyAccount(): Promise<ILocalUser | null> {
+	const meta = await fetchMeta();
+	if (meta.proxyAccountId == null) return null;
+	return await Users.findOneOrFail(meta.proxyAccountId) as ILocalUser;
+}
diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts
new file mode 100644
index 0000000000..f4f16a27e2
--- /dev/null
+++ b/packages/backend/src/misc/fetch.ts
@@ -0,0 +1,141 @@
+import * as http from 'http';
+import * as https from 'https';
+import CacheableLookup from 'cacheable-lookup';
+import fetch from 'node-fetch';
+import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
+import config from '@/config/index';
+import { URL } from 'url';
+
+export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) {
+	const res = await getResponse({
+		url,
+		method: 'GET',
+		headers: Object.assign({
+			'User-Agent': config.userAgent,
+			Accept: accept
+		}, headers || {}),
+		timeout
+	});
+
+	return await res.json();
+}
+
+export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record<string, string>) {
+	const res = await getResponse({
+		url,
+		method: 'GET',
+		headers: Object.assign({
+			'User-Agent': config.userAgent,
+			Accept: accept
+		}, headers || {}),
+		timeout
+	});
+
+	return await res.text();
+}
+
+export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) {
+	const timeout = args?.timeout || 10 * 1000;
+
+	const controller = new AbortController();
+	setTimeout(() => {
+		controller.abort();
+	}, timeout * 6);
+
+	const res = await fetch(args.url, {
+		method: args.method,
+		headers: args.headers,
+		body: args.body,
+		timeout,
+		size: args?.size || 10 * 1024 * 1024,
+		agent: getAgentByUrl,
+		signal: controller.signal,
+	});
+
+	if (!res.ok) {
+		throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
+	}
+
+	return res;
+}
+
+const cache = new CacheableLookup({
+	maxTtl: 3600,	// 1hours
+	errorTtl: 30,	// 30secs
+	lookup: false,	// nativeのdns.lookupにfallbackしない
+});
+
+/**
+ * Get http non-proxy agent
+ */
+const _http = new http.Agent({
+	keepAlive: true,
+	keepAliveMsecs: 30 * 1000,
+	lookup: cache.lookup,
+} as http.AgentOptions);
+
+/**
+ * Get https non-proxy agent
+ */
+const _https = new https.Agent({
+	keepAlive: true,
+	keepAliveMsecs: 30 * 1000,
+	lookup: cache.lookup,
+} as https.AgentOptions);
+
+const maxSockets = Math.max(256, config.deliverJobConcurrency || 128);
+
+/**
+ * Get http proxy or non-proxy agent
+ */
+export const httpAgent = config.proxy
+	? new HttpProxyAgent({
+		keepAlive: true,
+		keepAliveMsecs: 30 * 1000,
+		maxSockets,
+		maxFreeSockets: 256,
+		scheduling: 'lifo',
+		proxy: config.proxy
+	})
+	: _http;
+
+/**
+ * Get https proxy or non-proxy agent
+ */
+export const httpsAgent = config.proxy
+	? new HttpsProxyAgent({
+		keepAlive: true,
+		keepAliveMsecs: 30 * 1000,
+		maxSockets,
+		maxFreeSockets: 256,
+		scheduling: 'lifo',
+		proxy: config.proxy
+	})
+	: _https;
+
+/**
+ * Get agent by URL
+ * @param url URL
+ * @param bypassProxy Allways bypass proxy
+ */
+export function getAgentByUrl(url: URL, bypassProxy = false) {
+	if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) {
+		return url.protocol == 'http:' ? _http : _https;
+	} else {
+		return url.protocol == 'http:' ? httpAgent : httpsAgent;
+	}
+}
+
+export class StatusError extends Error {
+	public statusCode: number;
+	public statusMessage?: string;
+	public isClientError: boolean;
+
+	constructor(message: string, statusCode: number, statusMessage?: string) {
+		super(message);
+		this.name = 'StatusError';
+		this.statusCode = statusCode;
+		this.statusMessage = statusMessage;
+		this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
+	}
+}
diff --git a/packages/backend/src/misc/gen-avatar.ts b/packages/backend/src/misc/gen-avatar.ts
new file mode 100644
index 0000000000..f03ca9f96d
--- /dev/null
+++ b/packages/backend/src/misc/gen-avatar.ts
@@ -0,0 +1,90 @@
+/**
+ * Random avatar generator
+ */
+
+import * as p from 'pureimage';
+import * as gen from 'random-seed';
+import { WriteStream } from 'fs';
+
+const size = 256; // px
+const n = 5; // resolution
+const margin = (size / n);
+const colors = [
+	'#e57373',
+	'#F06292',
+	'#BA68C8',
+	'#9575CD',
+	'#7986CB',
+	'#64B5F6',
+	'#4FC3F7',
+	'#4DD0E1',
+	'#4DB6AC',
+	'#81C784',
+	'#8BC34A',
+	'#AFB42B',
+	'#F57F17',
+	'#FF5722',
+	'#795548',
+	'#455A64',
+];
+const bg = '#e9e9e9';
+
+const actualSize = size - (margin * 2);
+const cellSize = actualSize / n;
+const sideN = Math.floor(n / 2);
+
+/**
+ * Generate buffer of random avatar by seed
+ */
+export function genAvatar(seed: string, stream: WriteStream): Promise<void> {
+	const rand = gen.create(seed);
+	const canvas = p.make(size, size);
+	const ctx = canvas.getContext('2d');
+
+	ctx.fillStyle = bg;
+	ctx.beginPath();
+	ctx.fillRect(0, 0, size, size);
+
+	ctx.fillStyle = colors[rand(colors.length)];
+
+	// side bitmap (filled by false)
+	const side: boolean[][] = new Array(sideN);
+	for (let i = 0; i < side.length; i++) {
+		side[i] = new Array(n).fill(false);
+	}
+
+	// 1*n (filled by false)
+	const center: boolean[] = new Array(n).fill(false);
+
+	// tslint:disable-next-line:prefer-for-of
+	for (let x = 0; x < side.length; x++) {
+		for (let y = 0; y < side[x].length; y++) {
+			side[x][y] = rand(3) === 0;
+		}
+	}
+
+	for (let i = 0; i < center.length; i++) {
+		center[i] = rand(3) === 0;
+	}
+
+	// Draw
+	for (let x = 0; x < n; x++) {
+		for (let y = 0; y < n; y++) {
+			const isXCenter = x === ((n - 1) / 2);
+			if (isXCenter && !center[y]) continue;
+
+			const isLeftSide = x < ((n - 1) / 2);
+			if (isLeftSide && !side[x][y]) continue;
+
+			const isRightSide = x > ((n - 1) / 2);
+			if (isRightSide && !side[sideN - (x - sideN)][y]) continue;
+
+			const actualX = margin + (cellSize * x);
+			const actualY = margin + (cellSize * y);
+			ctx.beginPath();
+			ctx.fillRect(actualX, actualY, cellSize, cellSize);
+		}
+	}
+
+	return p.encodePNGToStream(canvas, stream);
+}
diff --git a/packages/backend/src/misc/gen-id.ts b/packages/backend/src/misc/gen-id.ts
new file mode 100644
index 0000000000..b1b542dc4b
--- /dev/null
+++ b/packages/backend/src/misc/gen-id.ts
@@ -0,0 +1,21 @@
+import { ulid } from 'ulid';
+import { genAid } from './id/aid';
+import { genMeid } from './id/meid';
+import { genMeidg } from './id/meidg';
+import { genObjectId } from './id/object-id';
+import config from '@/config/index';
+
+const metohd = config.id.toLowerCase();
+
+export function genId(date?: Date): string {
+	if (!date || (date > new Date())) date = new Date();
+
+	switch (metohd) {
+		case 'aid': return genAid(date);
+		case 'meid': return genMeid(date);
+		case 'meidg': return genMeidg(date);
+		case 'ulid': return ulid(date.getTime());
+		case 'objectid': return genObjectId(date);
+		default: throw new Error('unrecognized id generation method');
+	}
+}
diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts
new file mode 100644
index 0000000000..d4a8fa7534
--- /dev/null
+++ b/packages/backend/src/misc/gen-key-pair.ts
@@ -0,0 +1,36 @@
+import * as crypto from 'crypto';
+import * as util from 'util';
+
+const generateKeyPair = util.promisify(crypto.generateKeyPair);
+
+export async function genRsaKeyPair(modulusLength = 2048) {
+	return await generateKeyPair('rsa', {
+		modulusLength,
+		publicKeyEncoding: {
+			type: 'spki',
+			format: 'pem'
+		},
+		privateKeyEncoding: {
+			type: 'pkcs8',
+			format: 'pem',
+			cipher: undefined,
+			passphrase: undefined
+		}
+	});
+}
+
+export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'secp521r1' | 'curve25519' = 'prime256v1') {
+	return await generateKeyPair('ec', {
+		namedCurve,
+		publicKeyEncoding: {
+			type: 'spki',
+			format: 'pem'
+		},
+		privateKeyEncoding: {
+			type: 'pkcs8',
+			format: 'pem',
+			cipher: undefined,
+			passphrase: undefined
+		}
+	});
+}
diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts
new file mode 100644
index 0000000000..39ba541395
--- /dev/null
+++ b/packages/backend/src/misc/get-file-info.ts
@@ -0,0 +1,196 @@
+import * as fs from 'fs';
+import * as crypto from 'crypto';
+import * as stream from 'stream';
+import * as util from 'util';
+import * as fileType from 'file-type';
+import isSvg from 'is-svg';
+import * as probeImageSize from 'probe-image-size';
+import * as sharp from 'sharp';
+import { encode } from 'blurhash';
+
+const pipeline = util.promisify(stream.pipeline);
+
+export type FileInfo = {
+	size: number;
+	md5: string;
+	type: {
+		mime: string;
+		ext: string | null;
+	};
+	width?: number;
+	height?: number;
+	blurhash?: string;
+	warnings: string[];
+};
+
+const TYPE_OCTET_STREAM = {
+	mime: 'application/octet-stream',
+	ext: null
+};
+
+const TYPE_SVG = {
+	mime: 'image/svg+xml',
+	ext: 'svg'
+};
+
+/**
+ * Get file information
+ */
+export async function getFileInfo(path: string): Promise<FileInfo> {
+	const warnings = [] as string[];
+
+	const size = await getFileSize(path);
+	const md5 = await calcHash(path);
+
+	let type = await detectType(path);
+
+	// image dimensions
+	let width: number | undefined;
+	let height: number | undefined;
+
+	if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) {
+		const imageSize = await detectImageSize(path).catch(e => {
+			warnings.push(`detectImageSize failed: ${e}`);
+			return undefined;
+		});
+
+		// うまく判定できない画像は octet-stream にする
+		if (!imageSize) {
+			warnings.push(`cannot detect image dimensions`);
+			type = TYPE_OCTET_STREAM;
+		} else if (imageSize.wUnits === 'px') {
+			width = imageSize.width;
+			height = imageSize.height;
+
+			// 制限を超えている画像は octet-stream にする
+			if (imageSize.width > 16383 || imageSize.height > 16383) {
+				warnings.push(`image dimensions exceeds limits`);
+				type = TYPE_OCTET_STREAM;
+			}
+		} else {
+			warnings.push(`unsupported unit type: ${imageSize.wUnits}`);
+		}
+	}
+
+	let blurhash: string | undefined;
+
+	if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) {
+		blurhash = await getBlurhash(path).catch(e => {
+			warnings.push(`getBlurhash failed: ${e}`);
+			return undefined;
+		});
+	}
+
+	return {
+		size,
+		md5,
+		type,
+		width,
+		height,
+		blurhash,
+		warnings,
+	};
+}
+
+/**
+ * Detect MIME Type and extension
+ */
+export async function detectType(path: string) {
+	// Check 0 byte
+	const fileSize = await getFileSize(path);
+	if (fileSize === 0) {
+		return TYPE_OCTET_STREAM;
+	}
+
+	const type = await fileType.fromFile(path);
+
+	if (type) {
+		// XMLはSVGかもしれない
+		if (type.mime === 'application/xml' && await checkSvg(path)) {
+			return TYPE_SVG;
+		}
+
+		return {
+			mime: type.mime,
+			ext: type.ext
+		};
+	}
+
+	// 種類が不明でもSVGかもしれない
+	if (await checkSvg(path)) {
+		return TYPE_SVG;
+	}
+
+	// それでも種類が不明なら application/octet-stream にする
+	return TYPE_OCTET_STREAM;
+}
+
+/**
+ * Check the file is SVG or not
+ */
+export async function checkSvg(path: string) {
+	try {
+		const size = await getFileSize(path);
+		if (size > 1 * 1024 * 1024) return false;
+		return isSvg(fs.readFileSync(path));
+	} catch {
+		return false;
+	}
+}
+
+/**
+ * Get file size
+ */
+export async function getFileSize(path: string): Promise<number> {
+	const getStat = util.promisify(fs.stat);
+	return (await getStat(path)).size;
+}
+
+/**
+ * Calculate MD5 hash
+ */
+async function calcHash(path: string): Promise<string> {
+	const hash = crypto.createHash('md5').setEncoding('hex');
+	await pipeline(fs.createReadStream(path), hash);
+	return hash.read();
+}
+
+/**
+ * Detect dimensions of image
+ */
+async function detectImageSize(path: string): Promise<{
+	width: number;
+	height: number;
+	wUnits: string;
+	hUnits: string;
+}> {
+	const readable = fs.createReadStream(path);
+	const imageSize = await probeImageSize(readable);
+	readable.destroy();
+	return imageSize;
+}
+
+/**
+ * Calculate average color of image
+ */
+function getBlurhash(path: string): Promise<string> {
+	return new Promise((resolve, reject) => {
+		sharp(path)
+			.raw()
+			.ensureAlpha()
+			.resize(64, 64, { fit: 'inside' })
+			.toBuffer((err, buffer, { width, height }) => {
+				if (err) return reject(err);
+
+				let hash;
+
+				try {
+					hash = encode(new Uint8ClampedArray(buffer), width, height, 7, 7);
+				} catch (e) {
+					return reject(e);
+				}
+
+				resolve(hash);
+			});
+	});
+}
diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts
new file mode 100644
index 0000000000..d7273d1c5b
--- /dev/null
+++ b/packages/backend/src/misc/get-note-summary.ts
@@ -0,0 +1,54 @@
+import { Packed } from './schema';
+
+/**
+ * 投稿を表す文字列を取得します。
+ * @param {*} note (packされた)投稿
+ */
+export const getNoteSummary = (note: Packed<'Note'>): string => {
+	if (note.deletedAt) {
+		return `(❌⛔)`;
+	}
+
+	if (note.isHidden) {
+		return `(⛔)`;
+	}
+
+	let summary = '';
+
+	// 本文
+	if (note.cw != null) {
+		summary += note.cw;
+	} else {
+		summary += note.text ? note.text : '';
+	}
+
+	// ファイルが添付されているとき
+	if ((note.files || []).length != 0) {
+		summary += ` (📎${note.files!.length})`;
+	}
+
+	// 投票が添付されているとき
+	if (note.poll) {
+		summary += ` (📊)`;
+	}
+
+	// 返信のとき
+	if (note.replyId) {
+		if (note.reply) {
+			summary += `\n\nRE: ${getNoteSummary(note.reply)}`;
+		} else {
+			summary += '\n\nRE: ...';
+		}
+	}
+
+	// Renoteのとき
+	if (note.renoteId) {
+		if (note.renote) {
+			summary += `\n\nRN: ${getNoteSummary(note.renote)}`;
+		} else {
+			summary += '\n\nRN: ...';
+		}
+	}
+
+	return summary.trim();
+};
diff --git a/packages/backend/src/misc/get-reaction-emoji.ts b/packages/backend/src/misc/get-reaction-emoji.ts
new file mode 100644
index 0000000000..c2e0b98582
--- /dev/null
+++ b/packages/backend/src/misc/get-reaction-emoji.ts
@@ -0,0 +1,16 @@
+export default function(reaction: string): string {
+	switch (reaction) {
+		case 'like': return '👍';
+		case 'love': return '❤️';
+		case 'laugh': return '😆';
+		case 'hmm': return '🤔';
+		case 'surprise': return '😮';
+		case 'congrats': return '🎉';
+		case 'angry': return '💢';
+		case 'confused': return '😥';
+		case 'rip': return '😇';
+		case 'pudding': return '🍮';
+		case 'star': return '⭐';
+		default: return reaction;
+	}
+}
diff --git a/packages/backend/src/misc/hard-limits.ts b/packages/backend/src/misc/hard-limits.ts
new file mode 100644
index 0000000000..1039f7335a
--- /dev/null
+++ b/packages/backend/src/misc/hard-limits.ts
@@ -0,0 +1,14 @@
+
+// If you change DB_* values, you must also change the DB schema.
+
+/**
+ * Maximum note text length that can be stored in DB.
+ * Surrogate pairs count as one
+ */
+export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
+
+/**
+ * Maximum image description length that can be stored in DB.
+ * Surrogate pairs count as one
+ */
+export const DB_MAX_IMAGE_COMMENT_LENGTH = 512;
diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts
new file mode 100644
index 0000000000..4fa398763a
--- /dev/null
+++ b/packages/backend/src/misc/i18n.ts
@@ -0,0 +1,29 @@
+export class I18n<T extends Record<string, any>> {
+	public locale: T;
+
+	constructor(locale: T) {
+		this.locale = locale;
+
+		//#region BIND
+		this.t = this.t.bind(this);
+		//#endregion
+	}
+
+	// string にしているのは、ドット区切りでのパス指定を許可するため
+	// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
+	public t(key: string, args?: Record<string, any>): string {
+		try {
+			let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
+
+			if (args) {
+				for (const [k, v] of Object.entries(args)) {
+					str = str.replace(`{${k}}`, v);
+				}
+			}
+			return str;
+		} catch (e) {
+			console.warn(`missing localization '${key}'`);
+			return key;
+		}
+	}
+}
diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts
new file mode 100644
index 0000000000..2bcde90bff
--- /dev/null
+++ b/packages/backend/src/misc/id/aid.ts
@@ -0,0 +1,25 @@
+// AID
+// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列]
+
+import * as crypto from 'crypto';
+
+const TIME2000 = 946684800000;
+let counter = crypto.randomBytes(2).readUInt16LE(0);
+
+function getTime(time: number) {
+	time = time - TIME2000;
+	if (time < 0) time = 0;
+
+	return time.toString(36).padStart(8, '0');
+}
+
+function getNoise() {
+	return counter.toString(36).padStart(2, '0').slice(-2);
+}
+
+export function genAid(date: Date): string {
+	const t = date.getTime();
+	if (isNaN(t)) throw 'Failed to create AID: Invalid Date';
+	counter++;
+	return getTime(t) + getNoise();
+}
diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts
new file mode 100644
index 0000000000..30bbdf1698
--- /dev/null
+++ b/packages/backend/src/misc/id/meid.ts
@@ -0,0 +1,26 @@
+const CHARS = '0123456789abcdef';
+
+function getTime(time: number) {
+	if (time < 0) time = 0;
+	if (time === 0) {
+		return CHARS[0];
+	}
+
+	time += 0x800000000000;
+
+	return time.toString(16).padStart(12, CHARS[0]);
+}
+
+function getRandom() {
+	let str = '';
+
+	for (let i = 0; i < 12; i++) {
+		str += CHARS[Math.floor(Math.random() * CHARS.length)];
+	}
+
+	return str;
+}
+
+export function genMeid(date: Date): string {
+	return getTime(date.getTime()) + getRandom();
+}
diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts
new file mode 100644
index 0000000000..d4aaaea1ba
--- /dev/null
+++ b/packages/backend/src/misc/id/meidg.ts
@@ -0,0 +1,28 @@
+const CHARS = '0123456789abcdef';
+
+//  4bit Fixed hex value 'g'
+// 44bit UNIX Time ms in Hex
+// 48bit Random value in Hex
+
+function getTime(time: number) {
+	if (time < 0) time = 0;
+	if (time === 0) {
+		return CHARS[0];
+	}
+
+	return time.toString(16).padStart(11, CHARS[0]);
+}
+
+function getRandom() {
+	let str = '';
+
+	for (let i = 0; i < 12; i++) {
+		str += CHARS[Math.floor(Math.random() * CHARS.length)];
+	}
+
+	return str;
+}
+
+export function genMeidg(date: Date): string {
+	return 'g' + getTime(date.getTime()) + getRandom();
+}
diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts
new file mode 100644
index 0000000000..392ea43301
--- /dev/null
+++ b/packages/backend/src/misc/id/object-id.ts
@@ -0,0 +1,26 @@
+const CHARS = '0123456789abcdef';
+
+function getTime(time: number) {
+	if (time < 0) time = 0;
+	if (time === 0) {
+		return CHARS[0];
+	}
+
+	time = Math.floor(time / 1000);
+
+	return time.toString(16).padStart(8, CHARS[0]);
+}
+
+function getRandom() {
+	let str = '';
+
+	for (let i = 0; i < 16; i++) {
+		str += CHARS[Math.floor(Math.random() * CHARS.length)];
+	}
+
+	return str;
+}
+
+export function genObjectId(date: Date): string {
+	return getTime(date.getTime()) + getRandom();
+}
diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts
new file mode 100644
index 0000000000..2d7c6bd0c6
--- /dev/null
+++ b/packages/backend/src/misc/identifiable-error.ts
@@ -0,0 +1,13 @@
+/**
+ * ID付きエラー
+ */
+export class IdentifiableError extends Error {
+	public message: string;
+	public id: string;
+
+	constructor(id: string, message?: string) {
+		super(message);
+		this.message = message || '';
+		this.id = id;
+	}
+}
diff --git a/packages/backend/src/misc/is-blocker-user-related.ts b/packages/backend/src/misc/is-blocker-user-related.ts
new file mode 100644
index 0000000000..8c0ebfad9b
--- /dev/null
+++ b/packages/backend/src/misc/is-blocker-user-related.ts
@@ -0,0 +1,15 @@
+export function isBlockerUserRelated(note: any, blockerUserIds: Set<string>): boolean {
+	if (blockerUserIds.has(note.userId)) {
+		return true;
+	}
+
+	if (note.reply != null && blockerUserIds.has(note.reply.userId)) {
+		return true;
+	}
+
+	if (note.renote != null && blockerUserIds.has(note.renote.userId)) {
+		return true;
+	}
+
+	return false;
+}
diff --git a/packages/backend/src/misc/is-duplicate-key-value-error.ts b/packages/backend/src/misc/is-duplicate-key-value-error.ts
new file mode 100644
index 0000000000..23d8ceb1b7
--- /dev/null
+++ b/packages/backend/src/misc/is-duplicate-key-value-error.ts
@@ -0,0 +1,3 @@
+export function isDuplicateKeyValueError(e: Error): boolean {
+	return e.message.startsWith('duplicate key value');
+}
diff --git a/packages/backend/src/misc/is-muted-user-related.ts b/packages/backend/src/misc/is-muted-user-related.ts
new file mode 100644
index 0000000000..2caa743f95
--- /dev/null
+++ b/packages/backend/src/misc/is-muted-user-related.ts
@@ -0,0 +1,15 @@
+export function isMutedUserRelated(note: any, mutedUserIds: Set<string>): boolean {
+	if (mutedUserIds.has(note.userId)) {
+		return true;
+	}
+
+	if (note.reply != null && mutedUserIds.has(note.reply.userId)) {
+		return true;
+	}
+
+	if (note.renote != null && mutedUserIds.has(note.renote.userId)) {
+		return true;
+	}
+
+	return false;
+}
diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts
new file mode 100644
index 0000000000..2b57f036a2
--- /dev/null
+++ b/packages/backend/src/misc/is-quote.ts
@@ -0,0 +1,5 @@
+import { Note } from '@/models/entities/note';
+
+export default function(note: Note): boolean {
+	return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
+}
diff --git a/packages/backend/src/misc/keypair-store.ts b/packages/backend/src/misc/keypair-store.ts
new file mode 100644
index 0000000000..c018013b7b
--- /dev/null
+++ b/packages/backend/src/misc/keypair-store.ts
@@ -0,0 +1,10 @@
+import { UserKeypairs } from '@/models/index';
+import { User } from '@/models/entities/user';
+import { UserKeypair } from '@/models/entities/user-keypair';
+import { Cache } from './cache';
+
+const cache = new Cache<UserKeypair>(Infinity);
+
+export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
+	return await cache.fetch(userId, () => UserKeypairs.findOneOrFail(userId));
+}
diff --git a/packages/backend/src/misc/normalize-for-search.ts b/packages/backend/src/misc/normalize-for-search.ts
new file mode 100644
index 0000000000..200540566e
--- /dev/null
+++ b/packages/backend/src/misc/normalize-for-search.ts
@@ -0,0 +1,6 @@
+export function normalizeForSearch(tag: string): string {
+	// ref.
+	// - https://analytics-note.xyz/programming/unicode-normalization-forms/
+	// - https://maku77.github.io/js/string/normalize.html
+	return tag.normalize('NFKC').toLowerCase();
+}
diff --git a/packages/backend/src/misc/nyaize.ts b/packages/backend/src/misc/nyaize.ts
new file mode 100644
index 0000000000..500d1db2cb
--- /dev/null
+++ b/packages/backend/src/misc/nyaize.ts
@@ -0,0 +1,15 @@
+export function nyaize(text: string): string {
+	return text
+		// ja-JP
+		.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ﾅ/g, 'ﾆｬ')
+		// en-US
+		.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
+		.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
+		.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
+		// ko-KR
+		.replace(/[나-낳]/g, match => String.fromCharCode(
+			match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
+		))
+		.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
+		.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
+}
diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts
new file mode 100644
index 0000000000..f0a8bde31e
--- /dev/null
+++ b/packages/backend/src/misc/populate-emojis.ts
@@ -0,0 +1,124 @@
+import { In } from 'typeorm';
+import { Emojis } from '@/models/index';
+import { Emoji } from '@/models/entities/emoji';
+import { Note } from '@/models/entities/note';
+import { Cache } from './cache';
+import { isSelfHost, toPunyNullable } from './convert-host';
+import { decodeReaction } from './reaction-lib';
+import config from '@/config/index';
+import { query } from '@/prelude/url';
+
+const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
+
+/**
+ * 添付用絵文字情報
+ */
+type PopulatedEmoji = {
+	name: string;
+	url: string;
+};
+
+function normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
+	// クエリに使うホスト
+	let host = src === '.' ? null	// .はローカルホスト (ここがマッチするのはリアクションのみ)
+		: src === undefined ? noteUserHost	// ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
+		: isSelfHost(src) ? null	// 自ホスト指定
+		: (src || noteUserHost);	// 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
+
+	host = toPunyNullable(host);
+
+	return host;
+}
+
+function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
+	const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
+	if (!match) return { name: null, host: null };
+
+	const name = match[1];
+
+	// ホスト正規化
+	const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
+
+	return { name, host };
+}
+
+/**
+ * 添付用絵文字情報を解決する
+ * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
+ * @param noteUserHost ノートやユーザープロフィールの所有者のホスト
+ * @returns 絵文字情報, nullは未マッチを意味する
+ */
+export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
+	const { name, host } = parseEmojiStr(emojiName, noteUserHost);
+	if (name == null) return null;
+
+	const queryOrNull = async () => (await Emojis.findOne({
+		name,
+		host
+	})) || null;
+
+	const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
+
+	if (emoji == null) return null;
+
+	const isLocal = emoji.host == null;
+	const url = isLocal ? emoji.url : `${config.url}/proxy/image.png?${query({url: emoji.url})}`;
+
+	return {
+		name: emojiName,
+		url,
+	};
+}
+
+/**
+ * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される)
+ */
+export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> {
+	const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost)));
+	return emojis.filter((x): x is PopulatedEmoji => x != null);
+}
+
+export function aggregateNoteEmojis(notes: Note[]) {
+	let emojis: { name: string | null; host: string | null; }[] = [];
+	for (const note of notes) {
+		emojis = emojis.concat(note.emojis
+			.map(e => parseEmojiStr(e, note.userHost)));
+		if (note.renote) {
+			emojis = emojis.concat(note.renote.emojis
+				.map(e => parseEmojiStr(e, note.renote!.userHost)));
+			if (note.renote.user) {
+				emojis = emojis.concat(note.renote.user.emojis
+					.map(e => parseEmojiStr(e, note.renote!.userHost)));
+			}
+		}
+		const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
+		emojis = emojis.concat(customReactions);
+		if (note.user) {
+			emojis = emojis.concat(note.user.emojis
+				.map(e => parseEmojiStr(e, note.userHost)));
+		}
+	}
+	return emojis.filter(x => x.name != null) as { name: string; host: string | null; }[];
+}
+
+/**
+ * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
+ */
+export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
+	const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null);
+	const emojisQuery: any[] = [];
+	const hosts = new Set(notCachedEmojis.map(e => e.host));
+	for (const host of hosts) {
+		emojisQuery.push({
+			name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)),
+			host: host
+		});
+	}
+	const _emojis = emojisQuery.length > 0 ? await Emojis.find({
+		where: emojisQuery,
+		select: ['name', 'host', 'url']
+	}) : [];
+	for (const emoji of _emojis) {
+		cache.set(`${emoji.name} ${emoji.host}`, emoji);
+	}
+}
diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts
new file mode 100644
index 0000000000..46dedfa24b
--- /dev/null
+++ b/packages/backend/src/misc/reaction-lib.ts
@@ -0,0 +1,129 @@
+import { emojiRegex } from './emoji-regex';
+import { fetchMeta } from './fetch-meta';
+import { Emojis } from '@/models/index';
+import { toPunyNullable } from './convert-host';
+
+const legacies: Record<string, string> = {
+	'like':     '👍',
+	'love':     '❤', // ここに記述する場合は異体字セレクタを入れない
+	'laugh':    '😆',
+	'hmm':      '🤔',
+	'surprise': '😮',
+	'congrats': '🎉',
+	'angry':    '💢',
+	'confused': '😥',
+	'rip':      '😇',
+	'pudding':  '🍮',
+	'star':     '⭐',
+};
+
+export async function getFallbackReaction(): Promise<string> {
+	const meta = await fetchMeta();
+	return meta.useStarForReactionFallback ? '⭐' : '👍';
+}
+
+export function convertLegacyReactions(reactions: Record<string, number>) {
+	const _reactions = {} as Record<string, number>;
+
+	for (const reaction of Object.keys(reactions)) {
+		if (reactions[reaction] <= 0) continue;
+
+		if (Object.keys(legacies).includes(reaction)) {
+			if (_reactions[legacies[reaction]]) {
+				_reactions[legacies[reaction]] += reactions[reaction];
+			} else {
+				_reactions[legacies[reaction]] = reactions[reaction];
+			}
+		} else {
+			if (_reactions[reaction]) {
+				_reactions[reaction] += reactions[reaction];
+			} else {
+				_reactions[reaction] = reactions[reaction];
+			}
+		}
+	}
+
+	const _reactions2 = {} as Record<string, number>;
+
+	for (const reaction of Object.keys(_reactions)) {
+		_reactions2[decodeReaction(reaction).reaction] = _reactions[reaction];
+	}
+
+	return _reactions2;
+}
+
+export async function toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
+	if (reaction == null) return await getFallbackReaction();
+
+	reacterHost = toPunyNullable(reacterHost);
+
+	// 文字列タイプのリアクションを絵文字に変換
+	if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
+
+	// Unicode絵文字
+	const match = emojiRegex.exec(reaction);
+	if (match) {
+		// 合字を含む1つの絵文字
+		const unicode = match[0];
+
+		// 異体字セレクタ除去
+		return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
+	}
+
+	const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
+	if (custom) {
+		const name = custom[1];
+		const emoji = await Emojis.findOne({
+			host: reacterHost || null,
+			name,
+		});
+
+		if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
+	}
+
+	return await getFallbackReaction();
+}
+
+type DecodedReaction = {
+	/**
+	 * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.')
+	 */
+	reaction: string;
+
+	/**
+	 * name (カスタム絵文字の場合name, Emojiクエリに使う)
+	 */
+	name?: string;
+
+	/**
+	 * host (カスタム絵文字の場合host, Emojiクエリに使う)
+	 */
+	host?: string | null;
+};
+
+export function decodeReaction(str: string): DecodedReaction {
+	const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
+
+	if (custom) {
+		const name = custom[1];
+		const host = custom[2] || null;
+
+		return {
+			reaction: `:${name}@${host || '.'}:`,	// ローカル分は@以降を省略するのではなく.にする
+			name,
+			host
+		};
+	}
+
+	return {
+		reaction: str,
+		name: undefined,
+		host: undefined
+	};
+}
+
+export function convertLegacyReaction(reaction: string): string {
+	reaction = decodeReaction(reaction).reaction;
+	if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
+	return reaction;
+}
diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts
new file mode 100644
index 0000000000..02eb7f0a26
--- /dev/null
+++ b/packages/backend/src/misc/safe-for-sql.ts
@@ -0,0 +1,3 @@
+export function safeForSql(text: string): boolean {
+	return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text);
+}
diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts
new file mode 100644
index 0000000000..4131875ef7
--- /dev/null
+++ b/packages/backend/src/misc/schema.ts
@@ -0,0 +1,107 @@
+import { SimpleObj, SimpleSchema } from './simple-schema';
+import { packedUserSchema } from '@/models/repositories/user';
+import { packedNoteSchema } from '@/models/repositories/note';
+import { packedUserListSchema } from '@/models/repositories/user-list';
+import { packedAppSchema } from '@/models/repositories/app';
+import { packedMessagingMessageSchema } from '@/models/repositories/messaging-message';
+import { packedNotificationSchema } from '@/models/repositories/notification';
+import { packedDriveFileSchema } from '@/models/repositories/drive-file';
+import { packedDriveFolderSchema } from '@/models/repositories/drive-folder';
+import { packedFollowingSchema } from '@/models/repositories/following';
+import { packedMutingSchema } from '@/models/repositories/muting';
+import { packedBlockingSchema } from '@/models/repositories/blocking';
+import { packedNoteReactionSchema } from '@/models/repositories/note-reaction';
+import { packedHashtagSchema } from '@/models/repositories/hashtag';
+import { packedPageSchema } from '@/models/repositories/page';
+import { packedUserGroupSchema } from '@/models/repositories/user-group';
+import { packedNoteFavoriteSchema } from '@/models/repositories/note-favorite';
+import { packedChannelSchema } from '@/models/repositories/channel';
+import { packedAntennaSchema } from '@/models/repositories/antenna';
+import { packedClipSchema } from '@/models/repositories/clip';
+import { packedFederationInstanceSchema } from '@/models/repositories/federation-instance';
+import { packedQueueCountSchema } from '@/models/repositories/queue';
+import { packedGalleryPostSchema } from '@/models/repositories/gallery-post';
+import { packedEmojiSchema } from '@/models/repositories/emoji';
+import { packedReversiGameSchema } from '@/models/repositories/games/reversi/game';
+import { packedReversiMatchingSchema } from '@/models/repositories/games/reversi/matching';
+
+export const refs = {
+	User: packedUserSchema,
+	UserList: packedUserListSchema,
+	UserGroup: packedUserGroupSchema,
+	App: packedAppSchema,
+	MessagingMessage: packedMessagingMessageSchema,
+	Note: packedNoteSchema,
+	NoteReaction: packedNoteReactionSchema,
+	NoteFavorite: packedNoteFavoriteSchema,
+	Notification: packedNotificationSchema,
+	DriveFile: packedDriveFileSchema,
+	DriveFolder: packedDriveFolderSchema,
+	Following: packedFollowingSchema,
+	Muting: packedMutingSchema,
+	Blocking: packedBlockingSchema,
+	Hashtag: packedHashtagSchema,
+	Page: packedPageSchema,
+	Channel: packedChannelSchema,
+	QueueCount: packedQueueCountSchema,
+	Antenna: packedAntennaSchema,
+	Clip: packedClipSchema,
+	FederationInstance: packedFederationInstanceSchema,
+	GalleryPost: packedGalleryPostSchema,
+	Emoji: packedEmojiSchema,
+	ReversiGame: packedReversiGameSchema,
+	ReversiMatching: packedReversiMatchingSchema,
+};
+
+export type Packed<x extends keyof typeof refs> = ObjType<(typeof refs[x])['properties']>;
+
+export interface Schema extends SimpleSchema {
+	items?: Schema;
+	properties?: Obj;
+	ref?: keyof typeof refs;
+}
+
+type NonUndefinedPropertyNames<T extends Obj> = {
+	[K in keyof T]: T[K]['optional'] extends true ? never : K
+}[keyof T];
+
+type UndefinedPropertyNames<T extends Obj> = {
+	[K in keyof T]: T[K]['optional'] extends true ? K : never
+}[keyof T];
+
+type OnlyRequired<T extends Obj> = Pick<T, NonUndefinedPropertyNames<T>>;
+type OnlyOptional<T extends Obj> = Pick<T, UndefinedPropertyNames<T>>;
+
+export interface Obj extends SimpleObj { [key: string]: Schema; }
+
+export type ObjType<s extends Obj> =
+	{ [P in keyof OnlyOptional<s>]?: SchemaType<s[P]> } &
+	{ [P in keyof OnlyRequired<s>]: SchemaType<s[P]> };
+
+// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2
+type MyType<T extends Schema> = {
+	0: any;
+	1: SchemaType<T>;
+}[T extends Schema ? 1 : 0];
+
+type NullOrUndefined<p extends Schema, T> =
+	p['nullable'] extends true
+		?	p['optional'] extends true
+			? (T | null | undefined)
+			: (T | null)
+		: p['optional'] extends true
+			? (T | undefined)
+			: T;
+
+export type SchemaType<p extends Schema> =
+	p['type'] extends 'number' ? NullOrUndefined<p, number> :
+	p['type'] extends 'string' ? NullOrUndefined<p, string> :
+	p['type'] extends 'boolean' ? NullOrUndefined<p, boolean> :
+	p['type'] extends 'array' ? NullOrUndefined<p, MyType<NonNullable<p['items']>>[]> :
+	p['type'] extends 'object' ? (
+		p['ref'] extends keyof typeof refs
+			? NullOrUndefined<p, Packed<p['ref']>>
+			: NullOrUndefined<p, ObjType<NonNullable<p['properties']>>>
+	) :
+	p['type'] extends 'any' ? NullOrUndefined<p, any> :
+	any;
diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts
new file mode 100644
index 0000000000..76ee1225eb
--- /dev/null
+++ b/packages/backend/src/misc/secure-rndstr.ts
@@ -0,0 +1,21 @@
+import * as crypto from 'crypto';
+
+const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
+const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+export function secureRndstr(length = 32, useLU = true): string {
+	const chars = useLU ? LU_CHARS : L_CHARS;
+	const chars_len = chars.length;
+
+	let str = '';
+
+	for (let i = 0; i < length; i++) {
+		let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len);
+		if (rand === chars_len) {
+			rand = chars_len - 1;
+		}
+		str += chars.charAt(rand);
+	}
+
+	return str;
+}
diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts
new file mode 100644
index 0000000000..58747c1152
--- /dev/null
+++ b/packages/backend/src/misc/show-machine-info.ts
@@ -0,0 +1,13 @@
+import * as os from 'os';
+import * as sysUtils from 'systeminformation';
+import Logger from '@/services/logger';
+
+export async function showMachineInfo(parentLogger: Logger) {
+	const logger = parentLogger.createSubLogger('machine');
+	logger.debug(`Hostname: ${os.hostname()}`);
+	logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
+	const mem = await sysUtils.mem();
+	const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
+	const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
+	logger.debug(`CPU: ${os.cpus().length} core MEM: ${totalmem}GB (available: ${availmem}GB)`);
+}
diff --git a/packages/backend/src/misc/simple-schema.ts b/packages/backend/src/misc/simple-schema.ts
new file mode 100644
index 0000000000..abbb348e24
--- /dev/null
+++ b/packages/backend/src/misc/simple-schema.ts
@@ -0,0 +1,15 @@
+export interface SimpleSchema {
+	type: 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
+	nullable: boolean;
+	optional: boolean;
+	items?: SimpleSchema;
+	properties?: SimpleObj;
+	description?: string;
+	example?: any;
+	format?: string;
+	ref?: string;
+	enum?: string[];
+	default?: boolean | null;
+}
+
+export interface SimpleObj { [key: string]: SimpleSchema; }
diff --git a/packages/backend/src/misc/truncate.ts b/packages/backend/src/misc/truncate.ts
new file mode 100644
index 0000000000..cb120331a1
--- /dev/null
+++ b/packages/backend/src/misc/truncate.ts
@@ -0,0 +1,11 @@
+import { substring } from 'stringz';
+
+export function truncate(input: string, size: number): string;
+export function truncate(input: string | undefined, size: number): string | undefined;
+export function truncate(input: string | undefined, size: number): string | undefined {
+	if (!input) {
+		return input;
+	} else {
+		return substring(input, 0, size);
+	}
+}
diff --git a/packages/backend/src/models/entities/abuse-user-report.ts b/packages/backend/src/models/entities/abuse-user-report.ts
new file mode 100644
index 0000000000..c0cff139f6
--- /dev/null
+++ b/packages/backend/src/models/entities/abuse-user-report.ts
@@ -0,0 +1,74 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class AbuseUserReport {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the AbuseUserReport.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public targetUserId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public targetUser: User | null;
+
+	@Index()
+	@Column(id())
+	public reporterId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public reporter: User | null;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public assigneeId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public assignee: User | null;
+
+	@Index()
+	@Column('boolean', {
+		default: false
+	})
+	public resolved: boolean;
+
+	@Column('varchar', {
+		length: 2048,
+	})
+	public comment: string;
+
+	//#region Denormalized fields
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public targetUserHost: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public reporterHost: string | null;
+	//#endregion
+}
diff --git a/packages/backend/src/models/entities/access-token.ts b/packages/backend/src/models/entities/access-token.ts
new file mode 100644
index 0000000000..5f41b3c1fc
--- /dev/null
+++ b/packages/backend/src/models/entities/access-token.ts
@@ -0,0 +1,96 @@
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { id } from '../id';
+
+@Entity()
+export class AccessToken {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the AccessToken.'
+	})
+	public createdAt: Date;
+
+	@Column('timestamp with time zone', {
+		nullable: true,
+		default: null,
+	})
+	public lastUsedAt: Date | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128
+	})
+	public token: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 128,
+		nullable: true,
+		default: null
+	})
+	public session: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128
+	})
+	public hash: string;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		default: null
+	})
+	public appId: App['id'] | null;
+
+	@ManyToOne(type => App, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public app: App | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true,
+		default: null
+	})
+	public name: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true,
+		default: null
+	})
+	public description: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true,
+		default: null
+	})
+	public iconUrl: string | null;
+
+	@Column('varchar', {
+		length: 64, array: true,
+		default: '{}'
+	})
+	public permission: string[];
+
+	@Column('boolean', {
+		default: false
+	})
+	public fetched: boolean;
+}
diff --git a/packages/backend/src/models/entities/ad.ts b/packages/backend/src/models/entities/ad.ts
new file mode 100644
index 0000000000..b2fc04c4f0
--- /dev/null
+++ b/packages/backend/src/models/entities/ad.ts
@@ -0,0 +1,59 @@
+import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Ad {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Ad.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The expired date of the Ad.'
+	})
+	public expiresAt: Date;
+
+	@Column('varchar', {
+		length: 32, nullable: false
+	})
+	public place: string;
+
+	// 今は使われていないが将来的に活用される可能性はある
+	@Column('varchar', {
+		length: 32, nullable: false
+	})
+	public priority: string;
+
+	@Column('integer', {
+		default: 1, nullable: false
+	})
+	public ratio: number;
+
+	@Column('varchar', {
+		length: 1024, nullable: false
+	})
+	public url: string;
+
+	@Column('varchar', {
+		length: 1024, nullable: false
+	})
+	public imageUrl: string;
+
+	@Column('varchar', {
+		length: 8192, nullable: false
+	})
+	public memo: string;
+
+	constructor(data: Partial<Ad>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/announcement-read.ts b/packages/backend/src/models/entities/announcement-read.ts
new file mode 100644
index 0000000000..892beb826f
--- /dev/null
+++ b/packages/backend/src/models/entities/announcement-read.ts
@@ -0,0 +1,36 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Announcement } from './announcement';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'announcementId'], { unique: true })
+export class AnnouncementRead {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the AnnouncementRead.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column(id())
+	public announcementId: Announcement['id'];
+
+	@ManyToOne(type => Announcement, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public announcement: Announcement | null;
+}
diff --git a/packages/backend/src/models/entities/announcement.ts b/packages/backend/src/models/entities/announcement.ts
new file mode 100644
index 0000000000..06d379c229
--- /dev/null
+++ b/packages/backend/src/models/entities/announcement.ts
@@ -0,0 +1,43 @@
+import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Announcement {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Announcement.'
+	})
+	public createdAt: Date;
+
+	@Column('timestamp with time zone', {
+		comment: 'The updated date of the Announcement.',
+		nullable: true
+	})
+	public updatedAt: Date | null;
+
+	@Column('varchar', {
+		length: 8192, nullable: false
+	})
+	public text: string;
+
+	@Column('varchar', {
+		length: 256, nullable: false
+	})
+	public title: string;
+
+	@Column('varchar', {
+		length: 1024, nullable: true
+	})
+	public imageUrl: string | null;
+
+	constructor(data: Partial<Announcement>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/antenna-note.ts b/packages/backend/src/models/entities/antenna-note.ts
new file mode 100644
index 0000000000..9b911524ef
--- /dev/null
+++ b/packages/backend/src/models/entities/antenna-note.ts
@@ -0,0 +1,43 @@
+import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
+import { Note } from './note';
+import { Antenna } from './antenna';
+import { id } from '../id';
+
+@Entity()
+@Index(['noteId', 'antennaId'], { unique: true })
+export class AntennaNote {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The note ID.'
+	})
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The antenna ID.'
+	})
+	public antennaId: Antenna['id'];
+
+	@ManyToOne(type => Antenna, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public antenna: Antenna | null;
+
+	@Index()
+	@Column('boolean', {
+		default: false
+	})
+	public read: boolean;
+}
diff --git a/packages/backend/src/models/entities/antenna.ts b/packages/backend/src/models/entities/antenna.ts
new file mode 100644
index 0000000000..bcfe09a829
--- /dev/null
+++ b/packages/backend/src/models/entities/antenna.ts
@@ -0,0 +1,99 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { UserList } from './user-list';
+import { UserGroupJoining } from './user-group-joining';
+
+@Entity()
+export class Antenna {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Antenna.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the Antenna.'
+	})
+	public name: string;
+
+	@Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] })
+	public src: 'home' | 'all' | 'users' | 'list' | 'group';
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public userListId: UserList['id'] | null;
+
+	@ManyToOne(type => UserList, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public userList: UserList | null;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public userGroupJoiningId: UserGroupJoining['id'] | null;
+
+	@ManyToOne(type => UserGroupJoining, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public userGroupJoining: UserGroupJoining | null;
+
+	@Column('varchar', {
+		length: 1024, array: true,
+		default: '{}'
+	})
+	public users: string[];
+
+	@Column('jsonb', {
+		default: []
+	})
+	public keywords: string[][];
+
+	@Column('jsonb', {
+		default: []
+	})
+	public excludeKeywords: string[][];
+
+	@Column('boolean', {
+		default: false
+	})
+	public caseSensitive: boolean;
+
+	@Column('boolean', {
+		default: false
+	})
+	public withReplies: boolean;
+
+	@Column('boolean')
+	public withFile: boolean;
+
+	@Column('varchar', {
+		length: 2048, nullable: true,
+	})
+	public expression: string | null;
+
+	@Column('boolean')
+	public notify: boolean;
+}
diff --git a/packages/backend/src/models/entities/app.ts b/packages/backend/src/models/entities/app.ts
new file mode 100644
index 0000000000..ea87546311
--- /dev/null
+++ b/packages/backend/src/models/entities/app.ts
@@ -0,0 +1,60 @@
+import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class App {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the App.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'SET NULL',
+		nullable: true,
+	})
+	public user: User | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 64,
+		comment: 'The secret key of the App.'
+	})
+	public secret: string;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the App.'
+	})
+	public name: string;
+
+	@Column('varchar', {
+		length: 512,
+		comment: 'The description of the App.'
+	})
+	public description: string;
+
+	@Column('varchar', {
+		length: 64, array: true,
+		comment: 'The permission of the App.'
+	})
+	public permission: string[];
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The callbackUrl of the App.'
+	})
+	public callbackUrl: string | null;
+}
diff --git a/packages/backend/src/models/entities/attestation-challenge.ts b/packages/backend/src/models/entities/attestation-challenge.ts
new file mode 100644
index 0000000000..942747c02f
--- /dev/null
+++ b/packages/backend/src/models/entities/attestation-challenge.ts
@@ -0,0 +1,46 @@
+import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class AttestationChallenge {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@PrimaryColumn(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 64,
+		comment: 'Hex-encoded sha256 hash of the challenge.'
+	})
+	public challenge: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The date challenge was created for expiry purposes.'
+	})
+	public createdAt: Date;
+
+	@Column('boolean', {
+		comment:
+			'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.',
+		default: false
+	})
+	public registrationChallenge: boolean;
+
+	constructor(data: Partial<AttestationChallenge>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/auth-session.ts b/packages/backend/src/models/entities/auth-session.ts
new file mode 100644
index 0000000000..4eec27e3f6
--- /dev/null
+++ b/packages/backend/src/models/entities/auth-session.ts
@@ -0,0 +1,43 @@
+import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from './user';
+import { App } from './app';
+import { id } from '../id';
+
+@Entity()
+export class AuthSession {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the AuthSession.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('varchar', {
+		length: 128
+	})
+	public token: string;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE',
+		nullable: true
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public appId: App['id'];
+
+	@ManyToOne(type => App, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public app: App | null;
+}
diff --git a/packages/backend/src/models/entities/blocking.ts b/packages/backend/src/models/entities/blocking.ts
new file mode 100644
index 0000000000..48487cb086
--- /dev/null
+++ b/packages/backend/src/models/entities/blocking.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['blockerId', 'blockeeId'], { unique: true })
+export class Blocking {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Blocking.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The blockee user ID.'
+	})
+	public blockeeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public blockee: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The blocker user ID.'
+	})
+	public blockerId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public blocker: User | null;
+}
diff --git a/packages/backend/src/models/entities/channel-following.ts b/packages/backend/src/models/entities/channel-following.ts
new file mode 100644
index 0000000000..fca801e5ab
--- /dev/null
+++ b/packages/backend/src/models/entities/channel-following.ts
@@ -0,0 +1,43 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { Channel } from './channel';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class ChannelFollowing {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the ChannelFollowing.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The followee channel ID.'
+	})
+	public followeeId: Channel['id'];
+
+	@ManyToOne(type => Channel, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public followee: Channel | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The follower user ID.'
+	})
+	public followerId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public follower: User | null;
+}
diff --git a/packages/backend/src/models/entities/channel-note-pining.ts b/packages/backend/src/models/entities/channel-note-pining.ts
new file mode 100644
index 0000000000..26a7eb501f
--- /dev/null
+++ b/packages/backend/src/models/entities/channel-note-pining.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { Channel } from './channel';
+import { id } from '../id';
+
+@Entity()
+@Index(['channelId', 'noteId'], { unique: true })
+export class ChannelNotePining {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the ChannelNotePining.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public channelId: Channel['id'];
+
+	@ManyToOne(type => Channel, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public channel: Channel | null;
+
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+}
diff --git a/packages/backend/src/models/entities/channel.ts b/packages/backend/src/models/entities/channel.ts
new file mode 100644
index 0000000000..f2d713612d
--- /dev/null
+++ b/packages/backend/src/models/entities/channel.ts
@@ -0,0 +1,75 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { DriveFile } from './drive-file';
+
+@Entity()
+export class Channel {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Channel.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		nullable: true
+	})
+	public lastNotedAt: Date | null;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the Channel.'
+	})
+	public name: string;
+
+	@Column('varchar', {
+		length: 2048, nullable: true,
+		comment: 'The description of the Channel.'
+	})
+	public description: string | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of banner Channel.'
+	})
+	public bannerId: DriveFile['id'] | null;
+
+	@ManyToOne(type => DriveFile, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public banner: DriveFile | null;
+
+	@Index()
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of notes.'
+	})
+	public notesCount: number;
+
+	@Index()
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of users.'
+	})
+	public usersCount: number;
+}
diff --git a/packages/backend/src/models/entities/clip-note.ts b/packages/backend/src/models/entities/clip-note.ts
new file mode 100644
index 0000000000..7d96b2ef7a
--- /dev/null
+++ b/packages/backend/src/models/entities/clip-note.ts
@@ -0,0 +1,37 @@
+import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
+import { Note } from './note';
+import { Clip } from './clip';
+import { id } from '../id';
+
+@Entity()
+@Index(['noteId', 'clipId'], { unique: true })
+export class ClipNote {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The note ID.'
+	})
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The clip ID.'
+	})
+	public clipId: Clip['id'];
+
+	@ManyToOne(type => Clip, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public clip: Clip | null;
+}
diff --git a/packages/backend/src/models/entities/clip.ts b/packages/backend/src/models/entities/clip.ts
new file mode 100644
index 0000000000..66b5b8847e
--- /dev/null
+++ b/packages/backend/src/models/entities/clip.ts
@@ -0,0 +1,44 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Clip {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Clip.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the Clip.'
+	})
+	public name: string;
+
+	@Column('boolean', {
+		default: false
+	})
+	public isPublic: boolean;
+
+	@Column('varchar', {
+		length: 2048, nullable: true, default: null,
+		comment: 'The description of the Clip.'
+	})
+	public description: string | null;
+}
diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
new file mode 100644
index 0000000000..698dfac222
--- /dev/null
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -0,0 +1,164 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFolder } from './drive-folder';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'folderId', 'id'])
+export class DriveFile {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the DriveFile.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The host of owner. It will be null if the user in local.'
+	})
+	public userHost: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 32,
+		comment: 'The MD5 hash of the DriveFile.'
+	})
+	public md5: string;
+
+	@Column('varchar', {
+		length: 256,
+		comment: 'The file name of the DriveFile.'
+	})
+	public name: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 128,
+		comment: 'The content type (MIME) of the DriveFile.'
+	})
+	public type: string;
+
+	@Column('integer', {
+		comment: 'The file size (bytes) of the DriveFile.'
+	})
+	public size: number;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The comment of the DriveFile.'
+	})
+	public comment: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The BlurHash string.'
+	})
+	public blurhash: string | null;
+
+	@Column('jsonb', {
+		default: {},
+		comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
+	})
+	public properties: { width?: number; height?: number; avgColor?: string };
+
+	@Index()
+	@Column('boolean')
+	public storedInternal: boolean;
+
+	@Column('varchar', {
+		length: 512,
+		comment: 'The URL of the DriveFile.'
+	})
+	public url: string;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URL of the thumbnail of the DriveFile.'
+	})
+	public thumbnailUrl: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URL of the webpublic of the DriveFile.'
+	})
+	public webpublicUrl: string | null;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256, nullable: true,
+	})
+	public accessKey: string | null;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256, nullable: true,
+	})
+	public thumbnailAccessKey: string | null;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256, nullable: true,
+	})
+	public webpublicAccessKey: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.'
+	})
+	public uri: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+	})
+	public src: string | null;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The parent folder ID. If null, it means the DriveFile is located in root.'
+	})
+	public folderId: DriveFolder['id'] | null;
+
+	@ManyToOne(type => DriveFolder, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public folder: DriveFolder | null;
+
+	@Index()
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the DriveFile is NSFW.'
+	})
+	public isSensitive: boolean;
+
+	/**
+	 * 外部の(信頼されていない)URLへの直リンクか否か
+	 */
+	@Index()
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the DriveFile is direct link to remote server.'
+	})
+	public isLink: boolean;
+}
diff --git a/packages/backend/src/models/entities/drive-folder.ts b/packages/backend/src/models/entities/drive-folder.ts
new file mode 100644
index 0000000000..a80d075855
--- /dev/null
+++ b/packages/backend/src/models/entities/drive-folder.ts
@@ -0,0 +1,49 @@
+import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class DriveFolder {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the DriveFolder.'
+	})
+	public createdAt: Date;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the DriveFolder.'
+	})
+	public name: string;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.'
+	})
+	public parentId: DriveFolder['id'] | null;
+
+	@ManyToOne(type => DriveFolder, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public parent: DriveFolder | null;
+}
diff --git a/packages/backend/src/models/entities/emoji.ts b/packages/backend/src/models/entities/emoji.ts
new file mode 100644
index 0000000000..d6080ae099
--- /dev/null
+++ b/packages/backend/src/models/entities/emoji.ts
@@ -0,0 +1,51 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+@Index(['name', 'host'], { unique: true })
+export class Emoji {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		nullable: true
+	})
+	public updatedAt: Date | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128
+	})
+	public name: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public host: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public category: string | null;
+
+	@Column('varchar', {
+		length: 512,
+	})
+	public url: string;
+
+	@Column('varchar', {
+		length: 512, nullable: true
+	})
+	public uri: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true
+	})
+	public type: string | null;
+
+	@Column('varchar', {
+		array: true, length: 128, default: '{}'
+	})
+	public aliases: string[];
+}
diff --git a/packages/backend/src/models/entities/follow-request.ts b/packages/backend/src/models/entities/follow-request.ts
new file mode 100644
index 0000000000..22ec263962
--- /dev/null
+++ b/packages/backend/src/models/entities/follow-request.ts
@@ -0,0 +1,85 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class FollowRequest {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the FollowRequest.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The followee user ID.'
+	})
+	public followeeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public followee: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The follower user ID.'
+	})
+	public followerId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public follower: User | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'id of Follow Activity.'
+	})
+	public requestId: string | null;
+
+	//#region Denormalized fields
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerHost: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerInbox: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerSharedInbox: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeHost: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeInbox: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeSharedInbox: string | null;
+	//#endregion
+}
diff --git a/packages/backend/src/models/entities/following.ts b/packages/backend/src/models/entities/following.ts
new file mode 100644
index 0000000000..ee3286a1a1
--- /dev/null
+++ b/packages/backend/src/models/entities/following.ts
@@ -0,0 +1,80 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['followerId', 'followeeId'], { unique: true })
+export class Following {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Following.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The followee user ID.'
+	})
+	public followeeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public followee: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The follower user ID.'
+	})
+	public followerId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public follower: User | null;
+
+	//#region Denormalized fields
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerHost: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerInbox: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followerSharedInbox: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeHost: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeInbox: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public followeeSharedInbox: string | null;
+	//#endregion
+}
diff --git a/packages/backend/src/models/entities/gallery-like.ts b/packages/backend/src/models/entities/gallery-like.ts
new file mode 100644
index 0000000000..7d084a2275
--- /dev/null
+++ b/packages/backend/src/models/entities/gallery-like.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { GalleryPost } from './gallery-post';
+
+@Entity()
+@Index(['userId', 'postId'], { unique: true })
+export class GalleryLike {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public postId: GalleryPost['id'];
+
+	@ManyToOne(type => GalleryPost, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public post: GalleryPost | null;
+}
diff --git a/packages/backend/src/models/entities/gallery-post.ts b/packages/backend/src/models/entities/gallery-post.ts
new file mode 100644
index 0000000000..f59cd671f3
--- /dev/null
+++ b/packages/backend/src/models/entities/gallery-post.ts
@@ -0,0 +1,79 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { DriveFile } from './drive-file';
+
+@Entity()
+export class GalleryPost {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the GalleryPost.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The updated date of the GalleryPost.'
+	})
+	public updatedAt: Date;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public title: string;
+
+	@Column('varchar', {
+		length: 2048, nullable: true
+	})
+	public description: string | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The ID of author.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public fileIds: DriveFile['id'][];
+
+	@Index()
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the post is sensitive.'
+	})
+	public isSensitive: boolean;
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public likedCount: number;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public tags: string[];
+
+	constructor(data: Partial<GalleryPost>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/games/reversi/game.ts b/packages/backend/src/models/entities/games/reversi/game.ts
new file mode 100644
index 0000000000..9deacaf5c6
--- /dev/null
+++ b/packages/backend/src/models/entities/games/reversi/game.ts
@@ -0,0 +1,133 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from '../../user';
+import { id } from '../../../id';
+
+@Entity()
+export class ReversiGame {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the ReversiGame.'
+	})
+	public createdAt: Date;
+
+	@Column('timestamp with time zone', {
+		nullable: true,
+		comment: 'The started date of the ReversiGame.'
+	})
+	public startedAt: Date | null;
+
+	@Column(id())
+	public user1Id: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user1: User | null;
+
+	@Column(id())
+	public user2Id: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user2: User | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public user1Accepted: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public user2Accepted: boolean;
+
+	/**
+	 * どちらのプレイヤーが先行(黒)か
+	 * 1 ... user1
+	 * 2 ... user2
+	 */
+	@Column('integer', {
+		nullable: true,
+	})
+	public black: number | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isStarted: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isEnded: boolean;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public winnerId: User['id'] | null;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public surrendered: User['id'] | null;
+
+	@Column('jsonb', {
+		default: [],
+	})
+	public logs: {
+		at: Date;
+		color: boolean;
+		pos: number;
+	}[];
+
+	@Column('varchar', {
+		array: true, length: 64,
+	})
+	public map: string[];
+
+	@Column('varchar', {
+		length: 32
+	})
+	public bw: string;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isLlotheo: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public canPutEverywhere: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public loopedBoard: boolean;
+
+	@Column('jsonb', {
+		nullable: true, default: null,
+	})
+	public form1: any | null;
+
+	@Column('jsonb', {
+		nullable: true, default: null,
+	})
+	public form2: any | null;
+
+	/**
+	 * ログのposを文字列としてすべて連結したもののCRC32値
+	 */
+	@Column('varchar', {
+		length: 32, nullable: true
+	})
+	public crc32: string | null;
+}
diff --git a/packages/backend/src/models/entities/games/reversi/matching.ts b/packages/backend/src/models/entities/games/reversi/matching.ts
new file mode 100644
index 0000000000..477a29316e
--- /dev/null
+++ b/packages/backend/src/models/entities/games/reversi/matching.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from '../../user';
+import { id } from '../../../id';
+
+@Entity()
+export class ReversiMatching {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the ReversiMatching.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public parentId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public parent: User | null;
+
+	@Index()
+	@Column(id())
+	public childId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public child: User | null;
+}
diff --git a/packages/backend/src/models/entities/hashtag.ts b/packages/backend/src/models/entities/hashtag.ts
new file mode 100644
index 0000000000..842cdaa562
--- /dev/null
+++ b/packages/backend/src/models/entities/hashtag.ts
@@ -0,0 +1,87 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Hashtag {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 128
+	})
+	public name: string;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public mentionedUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public mentionedUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public mentionedLocalUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public mentionedLocalUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public mentionedRemoteUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public mentionedRemoteUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public attachedUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public attachedUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public attachedLocalUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public attachedLocalUsersCount: number;
+
+	@Column({
+		...id(),
+		array: true,
+	})
+	public attachedRemoteUserIds: User['id'][];
+
+	@Index()
+	@Column('integer', {
+		default: 0
+	})
+	public attachedRemoteUsersCount: number;
+}
diff --git a/packages/backend/src/models/entities/instance.ts b/packages/backend/src/models/entities/instance.ts
new file mode 100644
index 0000000000..7c8719e06a
--- /dev/null
+++ b/packages/backend/src/models/entities/instance.ts
@@ -0,0 +1,180 @@
+import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Instance {
+	@PrimaryColumn(id())
+	public id: string;
+
+	/**
+	 * このインスタンスを捕捉した日時
+	 */
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The caught date of the Instance.'
+	})
+	public caughtAt: Date;
+
+	/**
+	 * ホスト
+	 */
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 128,
+		comment: 'The host of the Instance.'
+	})
+	public host: string;
+
+	/**
+	 * インスタンスのユーザー数
+	 */
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of the users of the Instance.'
+	})
+	public usersCount: number;
+
+	/**
+	 * インスタンスの投稿数
+	 */
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of the notes of the Instance.'
+	})
+	public notesCount: number;
+
+	/**
+	 * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
+	 */
+	@Column('integer', {
+		default: 0,
+	})
+	public followingCount: number;
+
+	/**
+	 * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
+	 */
+	@Column('integer', {
+		default: 0,
+	})
+	public followersCount: number;
+
+	/**
+	 * ドライブ使用量
+	 */
+	@Column('bigint', {
+		default: 0,
+	})
+	public driveUsage: number;
+
+	/**
+	 * ドライブのファイル数
+	 */
+	@Column('integer', {
+		default: 0,
+	})
+	public driveFiles: number;
+
+	/**
+	 * 直近のリクエスト送信日時
+	 */
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public latestRequestSentAt: Date | null;
+
+	/**
+	 * 直近のリクエスト送信時のHTTPステータスコード
+	 */
+	@Column('integer', {
+		nullable: true,
+	})
+	public latestStatus: number | null;
+
+	/**
+	 * 直近のリクエスト受信日時
+	 */
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public latestRequestReceivedAt: Date | null;
+
+	/**
+	 * このインスタンスと最後にやり取りした日時
+	 */
+	@Column('timestamp with time zone')
+	public lastCommunicatedAt: Date;
+
+	/**
+	 * このインスタンスと不通かどうか
+	 */
+	@Column('boolean', {
+		default: false
+	})
+	public isNotResponding: boolean;
+
+	/**
+	 * このインスタンスへの配信を停止するか
+	 */
+	@Index()
+	@Column('boolean', {
+		default: false
+	})
+	public isSuspended: boolean;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+		comment: 'The software of the Instance.'
+	})
+	public softwareName: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public softwareVersion: string | null;
+
+	@Column('boolean', {
+		nullable: true, default: null,
+	})
+	public openRegistrations: boolean | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true, default: null,
+	})
+	public name: string | null;
+
+	@Column('varchar', {
+		length: 4096, nullable: true, default: null,
+	})
+	public description: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true, default: null,
+	})
+	public maintainerName: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true, default: null,
+	})
+	public maintainerEmail: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true, default: null,
+	})
+	public iconUrl: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true, default: null,
+	})
+	public faviconUrl: string | null;
+
+	@Column('varchar', {
+		length: 64, nullable: true, default: null,
+	})
+	public themeColor: string | null;
+
+	@Column('timestamp with time zone', {
+		nullable: true,
+	})
+	public infoUpdatedAt: Date | null;
+}
diff --git a/packages/backend/src/models/entities/messaging-message.ts b/packages/backend/src/models/entities/messaging-message.ts
new file mode 100644
index 0000000000..ac0764674c
--- /dev/null
+++ b/packages/backend/src/models/entities/messaging-message.ts
@@ -0,0 +1,89 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+import { UserGroup } from './user-group';
+
+@Entity()
+export class MessagingMessage {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the MessagingMessage.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The sender user ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(), nullable: true,
+		comment: 'The recipient user ID.'
+	})
+	public recipientId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public recipient: User | null;
+
+	@Index()
+	@Column({
+		...id(), nullable: true,
+		comment: 'The recipient group ID.'
+	})
+	public groupId: UserGroup['id'] | null;
+
+	@ManyToOne(type => UserGroup, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public group: UserGroup | null;
+
+	@Column('varchar', {
+		length: 4096, nullable: true
+	})
+	public text: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isRead: boolean;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+	})
+	public uri: string | null;
+
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public reads: User['id'][];
+
+	@Column({
+		...id(),
+		nullable: true,
+	})
+	public fileId: DriveFile['id'] | null;
+
+	@ManyToOne(type => DriveFile, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public file: DriveFile | null;
+}
diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts
new file mode 100644
index 0000000000..9a1a87c155
--- /dev/null
+++ b/packages/backend/src/models/entities/meta.ts
@@ -0,0 +1,423 @@
+import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { Clip } from './clip';
+
+@Entity()
+export class Meta {
+	@PrimaryColumn({
+		type: 'varchar',
+		length: 32
+	})
+	public id: string;
+
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public name: string | null;
+
+	@Column('varchar', {
+		length: 1024, nullable: true
+	})
+	public description: string | null;
+
+	/**
+	 * メンテナの名前
+	 */
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public maintainerName: string | null;
+
+	/**
+	 * メンテナの連絡先
+	 */
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public maintainerEmail: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public disableRegistration: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public disableLocalTimeline: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public disableGlobalTimeline: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public useStarForReactionFallback: boolean;
+
+	@Column('varchar', {
+		length: 64, array: true, default: '{}'
+	})
+	public langs: string[];
+
+	@Column('varchar', {
+		length: 256, array: true, default: '{}'
+	})
+	public pinnedUsers: string[];
+
+	@Column('varchar', {
+		length: 256, array: true, default: '{}'
+	})
+	public hiddenTags: string[];
+
+	@Column('varchar', {
+		length: 256, array: true, default: '{}'
+	})
+	public blockedHosts: string[];
+
+	@Column('varchar', {
+		length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'
+	})
+	public pinnedPages: string[];
+
+	@Column({
+		...id(),
+		nullable: true,
+	})
+	public pinnedClipId: Clip['id'] | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true,
+		default: '/assets/ai.png'
+	})
+	public mascotImageUrl: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public bannerUrl: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public backgroundImageUrl: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public logoImageUrl: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true,
+		default: 'https://xn--931a.moe/aiart/yubitun.png'
+	})
+	public errorImageUrl: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public iconUrl: string | null;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public cacheRemoteFiles: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public proxyRemoteFiles: boolean;
+
+	@Column({
+		...id(),
+		nullable: true,
+	})
+	public proxyAccountId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public proxyAccount: User | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public emailRequiredForSignup: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableHcaptcha: boolean;
+
+	@Column('varchar', {
+		length: 64,
+		nullable: true
+	})
+	public hcaptchaSiteKey: string | null;
+
+	@Column('varchar', {
+		length: 64,
+		nullable: true
+	})
+	public hcaptchaSecretKey: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableRecaptcha: boolean;
+
+	@Column('varchar', {
+		length: 64,
+		nullable: true
+	})
+	public recaptchaSiteKey: string | null;
+
+	@Column('varchar', {
+		length: 64,
+		nullable: true
+	})
+	public recaptchaSecretKey: string | null;
+
+	@Column('integer', {
+		default: 1024,
+		comment: 'Drive capacity of a local user (MB)'
+	})
+	public localDriveCapacityMb: number;
+
+	@Column('integer', {
+		default: 32,
+		comment: 'Drive capacity of a remote user (MB)'
+	})
+	public remoteDriveCapacityMb: number;
+
+	@Column('integer', {
+		default: 500,
+		comment: 'Max allowed note text length in characters'
+	})
+	public maxNoteTextLength: number;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public summalyProxy: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableEmail: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public email: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public smtpSecure: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public smtpHost: string | null;
+
+	@Column('integer', {
+		nullable: true
+	})
+	public smtpPort: number | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public smtpUser: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public smtpPass: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableServiceWorker: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public swPublicKey: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public swPrivateKey: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableTwitterIntegration: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public twitterConsumerKey: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public twitterConsumerSecret: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableGithubIntegration: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public githubClientId: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public githubClientSecret: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public enableDiscordIntegration: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public discordClientId: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public discordClientSecret: string | null;
+
+	@Column('varchar', {
+		length: 128,
+		nullable: true
+	})
+	public deeplAuthKey: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public deeplIsPro: boolean;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public ToSUrl: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		default: 'https://github.com/misskey-dev/misskey',
+		nullable: false
+	})
+	public repositoryUrl: string;
+
+	@Column('varchar', {
+		length: 512,
+		default: 'https://github.com/misskey-dev/misskey/issues/new',
+		nullable: true
+	})
+	public feedbackUrl: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public useObjectStorage: boolean;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public objectStorageBucket: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public objectStoragePrefix: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public objectStorageBaseUrl: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public objectStorageEndpoint: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public objectStorageRegion: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public objectStorageAccessKey: string | null;
+
+	@Column('varchar', {
+		length: 512,
+		nullable: true
+	})
+	public objectStorageSecretKey: string | null;
+
+	@Column('integer', {
+		nullable: true
+	})
+	public objectStoragePort: number | null;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public objectStorageUseSSL: boolean;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public objectStorageUseProxy: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public objectStorageSetPublicRead: boolean;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public objectStorageS3ForcePathStyle: boolean;
+}
diff --git a/packages/backend/src/models/entities/moderation-log.ts b/packages/backend/src/models/entities/moderation-log.ts
new file mode 100644
index 0000000000..33d3d683ae
--- /dev/null
+++ b/packages/backend/src/models/entities/moderation-log.ts
@@ -0,0 +1,32 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class ModerationLog {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the ModerationLog.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public type: string;
+
+	@Column('jsonb')
+	public info: Record<string, any>;
+}
diff --git a/packages/backend/src/models/entities/muted-note.ts b/packages/backend/src/models/entities/muted-note.ts
new file mode 100644
index 0000000000..521876688c
--- /dev/null
+++ b/packages/backend/src/models/entities/muted-note.ts
@@ -0,0 +1,48 @@
+import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+import { mutedNoteReasons } from '../../types';
+
+@Entity()
+@Index(['noteId', 'userId'], { unique: true })
+export class MutedNote {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The note ID.'
+	})
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The user ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	/**
+	 * ミュートされた理由。
+	 */
+	@Index()
+	@Column('enum', {
+		enum: mutedNoteReasons,
+		comment: 'The reason of the MutedNote.'
+	})
+	public reason: typeof mutedNoteReasons[number];
+}
diff --git a/packages/backend/src/models/entities/muting.ts b/packages/backend/src/models/entities/muting.ts
new file mode 100644
index 0000000000..0084213bcc
--- /dev/null
+++ b/packages/backend/src/models/entities/muting.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['muterId', 'muteeId'], { unique: true })
+export class Muting {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Muting.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The mutee user ID.'
+	})
+	public muteeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public mutee: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The muter user ID.'
+	})
+	public muterId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public muter: User | null;
+}
diff --git a/packages/backend/src/models/entities/note-favorite.ts b/packages/backend/src/models/entities/note-favorite.ts
new file mode 100644
index 0000000000..0713c3ae56
--- /dev/null
+++ b/packages/backend/src/models/entities/note-favorite.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteFavorite {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the NoteFavorite.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+}
diff --git a/packages/backend/src/models/entities/note-reaction.ts b/packages/backend/src/models/entities/note-reaction.ts
new file mode 100644
index 0000000000..674dc3639e
--- /dev/null
+++ b/packages/backend/src/models/entities/note-reaction.ts
@@ -0,0 +1,44 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteReaction {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the NoteReaction.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user?: User | null;
+
+	@Index()
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note?: Note | null;
+
+	// TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため)
+
+	@Column('varchar', {
+		length: 260
+	})
+	public reaction: string;
+}
diff --git a/packages/backend/src/models/entities/note-thread-muting.ts b/packages/backend/src/models/entities/note-thread-muting.ts
new file mode 100644
index 0000000000..b438522a4c
--- /dev/null
+++ b/packages/backend/src/models/entities/note-thread-muting.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'threadId'], { unique: true })
+export class NoteThreadMuting {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 256,
+	})
+	public threadId: string;
+}
diff --git a/packages/backend/src/models/entities/note-unread.ts b/packages/backend/src/models/entities/note-unread.ts
new file mode 100644
index 0000000000..57dda4fafd
--- /dev/null
+++ b/packages/backend/src/models/entities/note-unread.ts
@@ -0,0 +1,63 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+import { Channel } from './channel';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteUnread {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	/**
+	 * メンションか否か
+	 */
+	@Index()
+	@Column('boolean')
+	public isMentioned: boolean;
+
+	/**
+	 * ダイレクト投稿か否か
+	 */
+	@Index()
+	@Column('boolean')
+	public isSpecified: boolean;
+
+	//#region Denormalized fields
+	@Index()
+	@Column({
+		...id(),
+		comment: '[Denormalized]'
+	})
+	public noteUserId: User['id'];
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: '[Denormalized]'
+	})
+	public noteChannelId: Channel['id'] | null;
+	//#endregion
+}
diff --git a/packages/backend/src/models/entities/note-watching.ts b/packages/backend/src/models/entities/note-watching.ts
new file mode 100644
index 0000000000..741a1c0c8b
--- /dev/null
+++ b/packages/backend/src/models/entities/note-watching.ts
@@ -0,0 +1,52 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class NoteWatching {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the NoteWatching.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The watcher ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The target Note ID.'
+	})
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	//#region Denormalized fields
+	@Index()
+	@Column({
+		...id(),
+		comment: '[Denormalized]'
+	})
+	public noteUserId: Note['userId'];
+	//#endregion
+}
diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts
new file mode 100644
index 0000000000..4a5411f93d
--- /dev/null
+++ b/packages/backend/src/models/entities/note.ts
@@ -0,0 +1,247 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+import { noteVisibilities } from '../../types';
+import { Channel } from './channel';
+
+@Entity()
+@Index('IDX_NOTE_TAGS', { synchronize: false })
+@Index('IDX_NOTE_MENTIONS', { synchronize: false })
+@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
+export class Note {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Note.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of reply target.'
+	})
+	public replyId: Note['id'] | null;
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public reply: Note | null;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of renote target.'
+	})
+	public renoteId: Note['id'] | null;
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public renote: Note | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 256, nullable: true
+	})
+	public threadId: string | null;
+
+	@Column('varchar', {
+		length: 8192, nullable: true
+	})
+	public text: string | null;
+
+	@Column('varchar', {
+		length: 256, nullable: true
+	})
+	public name: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true
+	})
+	public cw: string | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The ID of author.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('boolean', {
+		default: false
+	})
+	public viaMobile: boolean;
+
+	@Column('boolean', {
+		default: false
+	})
+	public localOnly: boolean;
+
+	@Column('smallint', {
+		default: 0
+	})
+	public renoteCount: number;
+
+	@Column('smallint', {
+		default: 0
+	})
+	public repliesCount: number;
+
+	@Column('jsonb', {
+		default: {}
+	})
+	public reactions: Record<string, number>;
+
+	/**
+	 * public ... 公開
+	 * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
+	 * followers ... フォロワーのみ
+	 * specified ... visibleUserIds で指定したユーザーのみ
+	 */
+	@Column('enum', { enum: noteVisibilities })
+	public visibility: typeof noteVisibilities[number];
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URI of a note. it will be null when the note is local.'
+	})
+	public uri: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The human readable url of a note. it will be null when the note is local.'
+	})
+	public url: string | null;
+
+	@Column('integer', {
+		default: 0, select: false
+	})
+	public score: number;
+
+	@Index()
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public fileIds: DriveFile['id'][];
+
+	@Index()
+	@Column('varchar', {
+		length: 256, array: true, default: '{}'
+	})
+	public attachedFileTypes: string[];
+
+	@Index()
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public visibleUserIds: User['id'][];
+
+	@Index()
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public mentions: User['id'][];
+
+	@Column('text', {
+		default: '[]'
+	})
+	public mentionedRemoteUsers: string;
+
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public emojis: string[];
+
+	@Index()
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public tags: string[];
+
+	@Column('boolean', {
+		default: false
+	})
+	public hasPoll: boolean;
+
+	@Index()
+	@Column({
+		...id(),
+		nullable: true, default: null,
+		comment: 'The ID of source channel.'
+	})
+	public channelId: Channel['id'] | null;
+
+	@ManyToOne(type => Channel, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public channel: Channel | null;
+
+	//#region Denormalized fields
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public userHost: string | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: '[Denormalized]'
+	})
+	public replyUserId: User['id'] | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public replyUserHost: string | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: '[Denormalized]'
+	})
+	public renoteUserId: User['id'] | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public renoteUserHost: string | null;
+	//#endregion
+
+	constructor(data: Partial<Note>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
+
+export type IMentionedRemoteUsers = {
+	uri: string;
+	url?: string;
+	username: string;
+	host: string;
+}[];
diff --git a/packages/backend/src/models/entities/notification.ts b/packages/backend/src/models/entities/notification.ts
new file mode 100644
index 0000000000..47184caacc
--- /dev/null
+++ b/packages/backend/src/models/entities/notification.ts
@@ -0,0 +1,172 @@
+import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { Note } from './note';
+import { FollowRequest } from './follow-request';
+import { UserGroupInvitation } from './user-group-invitation';
+import { AccessToken } from './access-token';
+import { notificationTypes } from '@/types';
+
+@Entity()
+export class Notification {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Notification.'
+	})
+	public createdAt: Date;
+
+	/**
+	 * 通知の受信者
+	 */
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The ID of recipient user of the Notification.'
+	})
+	public notifieeId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public notifiee: User | null;
+
+	/**
+	 * 通知の送信者(initiator)
+	 */
+	@Index()
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of sender user of the Notification.'
+	})
+	public notifierId: User['id'] | null;
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public notifier: User | null;
+
+	/**
+	 * 通知の種類。
+	 * follow - フォローされた
+	 * mention - 投稿で自分が言及された
+	 * reply - (自分または自分がWatchしている)投稿が返信された
+	 * renote - (自分または自分がWatchしている)投稿がRenoteされた
+	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
+	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた
+	 * pollVote - (自分または自分がWatchしている)投稿の投票に投票された
+	 * receiveFollowRequest - フォローリクエストされた
+	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された
+	 * groupInvited - グループに招待された
+	 * app - アプリ通知
+	 */
+	@Index()
+	@Column('enum', {
+		enum: notificationTypes,
+		comment: 'The type of the Notification.'
+	})
+	public type: typeof notificationTypes[number];
+
+	/**
+	 * 通知が読まれたかどうか
+	 */
+	@Index()
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the Notification is read.'
+	})
+	public isRead: boolean;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public noteId: Note['id'] | null;
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public followRequestId: FollowRequest['id'] | null;
+
+	@ManyToOne(type => FollowRequest, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public followRequest: FollowRequest | null;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public userGroupInvitationId: UserGroupInvitation['id'] | null;
+
+	@ManyToOne(type => UserGroupInvitation, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public userGroupInvitation: UserGroupInvitation | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true
+	})
+	public reaction: string | null;
+
+	@Column('integer', {
+		nullable: true
+	})
+	public choice: number | null;
+
+	/**
+	 * アプリ通知のbody
+	 */
+	@Column('varchar', {
+		length: 2048, nullable: true
+	})
+	public customBody: string | null;
+
+	/**
+	 * アプリ通知のheader
+	 * (省略時はアプリ名で表示されることを期待)
+	 */
+	@Column('varchar', {
+		length: 256, nullable: true
+	})
+	public customHeader: string | null;
+
+	/**
+	 * アプリ通知のicon(URL)
+	 * (省略時はアプリアイコンで表示されることを期待)
+	 */
+	@Column('varchar', {
+		length: 1024, nullable: true
+	})
+	public customIcon: string | null;
+
+	/**
+	 * アプリ通知のアプリ(のトークン)
+	 */
+	@Index()
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public appAccessTokenId: AccessToken['id'] | null;
+
+	@ManyToOne(type => AccessToken, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public appAccessToken: AccessToken | null;
+}
diff --git a/packages/backend/src/models/entities/page-like.ts b/packages/backend/src/models/entities/page-like.ts
new file mode 100644
index 0000000000..ca84ece8fd
--- /dev/null
+++ b/packages/backend/src/models/entities/page-like.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { Page } from './page';
+
+@Entity()
+@Index(['userId', 'pageId'], { unique: true })
+export class PageLike {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public pageId: Page['id'];
+
+	@ManyToOne(type => Page, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public page: Page | null;
+}
diff --git a/packages/backend/src/models/entities/page.ts b/packages/backend/src/models/entities/page.ts
new file mode 100644
index 0000000000..ed0411a3d0
--- /dev/null
+++ b/packages/backend/src/models/entities/page.ts
@@ -0,0 +1,121 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+import { DriveFile } from './drive-file';
+
+@Entity()
+@Index(['userId', 'name'], { unique: true })
+export class Page {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Page.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The updated date of the Page.'
+	})
+	public updatedAt: Date;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public title: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 256,
+	})
+	public name: string;
+
+	@Column('varchar', {
+		length: 256, nullable: true
+	})
+	public summary: string | null;
+
+	@Column('boolean')
+	public alignCenter: boolean;
+
+	@Column('boolean', {
+		default: false
+	})
+	public hideTitleWhenPinned: boolean;
+
+	@Column('varchar', {
+		length: 32,
+	})
+	public font: string;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The ID of author.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+	})
+	public eyeCatchingImageId: DriveFile['id'] | null;
+
+	@ManyToOne(type => DriveFile, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public eyeCatchingImage: DriveFile | null;
+
+	@Column('jsonb', {
+		default: []
+	})
+	public content: Record<string, any>[];
+
+	@Column('jsonb', {
+		default: []
+	})
+	public variables: Record<string, any>[];
+
+	@Column('varchar', {
+		length: 16384,
+		default: ''
+	})
+	public script: string;
+
+	/**
+	 * public ... 公開
+	 * followers ... フォロワーのみ
+	 * specified ... visibleUserIds で指定したユーザーのみ
+	 */
+	@Column('enum', { enum: ['public', 'followers', 'specified'] })
+	public visibility: 'public' | 'followers' | 'specified';
+
+	@Index()
+	@Column({
+		...id(),
+		array: true, default: '{}'
+	})
+	public visibleUserIds: User['id'][];
+
+	@Column('integer', {
+		default: 0
+	})
+	public likedCount: number;
+
+	constructor(data: Partial<Page>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/password-reset-request.ts b/packages/backend/src/models/entities/password-reset-request.ts
new file mode 100644
index 0000000000..6d41d38a93
--- /dev/null
+++ b/packages/backend/src/models/entities/password-reset-request.ts
@@ -0,0 +1,30 @@
+import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { id } from '../id';
+import { User } from './user';
+
+@Entity()
+export class PasswordResetRequest {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256,
+	})
+	public token: string;
+
+	@Index()
+	@Column({
+		...id(),
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+}
diff --git a/packages/backend/src/models/entities/poll-vote.ts b/packages/backend/src/models/entities/poll-vote.ts
new file mode 100644
index 0000000000..709376f909
--- /dev/null
+++ b/packages/backend/src/models/entities/poll-vote.ts
@@ -0,0 +1,40 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { Note } from './note';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId', 'choice'], { unique: true })
+export class PollVote {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the PollVote.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column('integer')
+	public choice: number;
+}
diff --git a/packages/backend/src/models/entities/poll.ts b/packages/backend/src/models/entities/poll.ts
new file mode 100644
index 0000000000..e3bbb1c3f2
--- /dev/null
+++ b/packages/backend/src/models/entities/poll.ts
@@ -0,0 +1,72 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { id } from '../id';
+import { Note } from './note';
+import { User } from './user';
+import { noteVisibilities } from '../../types';
+
+@Entity()
+export class Poll {
+	@PrimaryColumn(id())
+	public noteId: Note['id'];
+
+	@OneToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column('timestamp with time zone', {
+		nullable: true
+	})
+	public expiresAt: Date | null;
+
+	@Column('boolean')
+	public multiple: boolean;
+
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public choices: string[];
+
+	@Column('integer', {
+		array: true,
+	})
+	public votes: number[];
+
+	//#region Denormalized fields
+	@Column('enum', {
+		enum: noteVisibilities,
+		comment: '[Denormalized]'
+	})
+	public noteVisibility: typeof noteVisibilities[number];
+
+	@Index()
+	@Column({
+		...id(),
+		comment: '[Denormalized]'
+	})
+	public userId: User['id'];
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public userHost: string | null;
+	//#endregion
+
+	constructor(data: Partial<Poll>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
+
+export type IPoll = {
+	choices: string[];
+	votes?: number[];
+	multiple: boolean;
+	expiresAt: Date | null;
+};
diff --git a/packages/backend/src/models/entities/promo-note.ts b/packages/backend/src/models/entities/promo-note.ts
new file mode 100644
index 0000000000..474f1cb235
--- /dev/null
+++ b/packages/backend/src/models/entities/promo-note.ts
@@ -0,0 +1,28 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class PromoNote {
+	@PrimaryColumn(id())
+	public noteId: Note['id'];
+
+	@OneToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Column('timestamp with time zone')
+	public expiresAt: Date;
+
+	//#region Denormalized fields
+	@Index()
+	@Column({
+		...id(),
+		comment: '[Denormalized]'
+	})
+	public userId: User['id'];
+	//#endregion
+}
diff --git a/packages/backend/src/models/entities/promo-read.ts b/packages/backend/src/models/entities/promo-read.ts
new file mode 100644
index 0000000000..2e0977b6b5
--- /dev/null
+++ b/packages/backend/src/models/entities/promo-read.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class PromoRead {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the PromoRead.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+}
diff --git a/packages/backend/src/models/entities/registration-tickets.ts b/packages/backend/src/models/entities/registration-tickets.ts
new file mode 100644
index 0000000000..d962f78a78
--- /dev/null
+++ b/packages/backend/src/models/entities/registration-tickets.ts
@@ -0,0 +1,17 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class RegistrationTicket {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 64,
+	})
+	public code: string;
+}
diff --git a/packages/backend/src/models/entities/registry-item.ts b/packages/backend/src/models/entities/registry-item.ts
new file mode 100644
index 0000000000..54d2ef2082
--- /dev/null
+++ b/packages/backend/src/models/entities/registry-item.ts
@@ -0,0 +1,58 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい
+@Entity()
+export class RegistryItem {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the RegistryItem.'
+	})
+	public createdAt: Date;
+
+	@Column('timestamp with time zone', {
+		comment: 'The updated date of the RegistryItem.'
+	})
+	public updatedAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 1024,
+		comment: 'The key of the RegistryItem.'
+	})
+	public key: string;
+
+	@Column('jsonb', {
+		default: {}, nullable: true,
+		comment: 'The value of the RegistryItem.'
+	})
+	public value: any | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 1024, array: true, default: '{}'
+	})
+	public scope: string[];
+
+	// サードパーティアプリに開放するときのためのカラム
+	@Index()
+	@Column('varchar', {
+		length: 512, nullable: true
+	})
+	public domain: string | null;
+}
diff --git a/packages/backend/src/models/entities/relay.ts b/packages/backend/src/models/entities/relay.ts
new file mode 100644
index 0000000000..4c82ccb125
--- /dev/null
+++ b/packages/backend/src/models/entities/relay.ts
@@ -0,0 +1,19 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class Relay {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 512, nullable: false,
+	})
+	public inbox: string;
+
+	@Column('enum', {
+		enum: ['requesting', 'accepted', 'rejected'],
+	})
+	public status: 'requesting' | 'accepted' | 'rejected';
+}
diff --git a/packages/backend/src/models/entities/signin.ts b/packages/backend/src/models/entities/signin.ts
new file mode 100644
index 0000000000..7e047084b1
--- /dev/null
+++ b/packages/backend/src/models/entities/signin.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class Signin {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the Signin.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public ip: string;
+
+	@Column('jsonb')
+	public headers: Record<string, any>;
+
+	@Column('boolean')
+	public success: boolean;
+}
diff --git a/packages/backend/src/models/entities/sw-subscription.ts b/packages/backend/src/models/entities/sw-subscription.ts
new file mode 100644
index 0000000000..7c3f6f0a6c
--- /dev/null
+++ b/packages/backend/src/models/entities/sw-subscription.ts
@@ -0,0 +1,37 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class SwSubscription {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 512,
+	})
+	public endpoint: string;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public auth: string;
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public publickey: string;
+}
diff --git a/packages/backend/src/models/entities/used-username.ts b/packages/backend/src/models/entities/used-username.ts
new file mode 100644
index 0000000000..eb90bef6ca
--- /dev/null
+++ b/packages/backend/src/models/entities/used-username.ts
@@ -0,0 +1,20 @@
+import { PrimaryColumn, Entity, Column } from 'typeorm';
+
+@Entity()
+export class UsedUsername {
+	@PrimaryColumn('varchar', {
+		length: 128,
+	})
+	public username: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	constructor(data: Partial<UsedUsername>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/user-group-invitation.ts b/packages/backend/src/models/entities/user-group-invitation.ts
new file mode 100644
index 0000000000..6fe8f20134
--- /dev/null
+++ b/packages/backend/src/models/entities/user-group-invitation.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { UserGroup } from './user-group';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'userGroupId'], { unique: true })
+export class UserGroupInvitation {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserGroupInvitation.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The user ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The group ID.'
+	})
+	public userGroupId: UserGroup['id'];
+
+	@ManyToOne(type => UserGroup, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public userGroup: UserGroup | null;
+}
diff --git a/packages/backend/src/models/entities/user-group-joining.ts b/packages/backend/src/models/entities/user-group-joining.ts
new file mode 100644
index 0000000000..e09c3230f1
--- /dev/null
+++ b/packages/backend/src/models/entities/user-group-joining.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { UserGroup } from './user-group';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'userGroupId'], { unique: true })
+export class UserGroupJoining {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserGroupJoining.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The user ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The group ID.'
+	})
+	public userGroupId: UserGroup['id'];
+
+	@ManyToOne(type => UserGroup, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public userGroup: UserGroup | null;
+}
diff --git a/packages/backend/src/models/entities/user-group.ts b/packages/backend/src/models/entities/user-group.ts
new file mode 100644
index 0000000000..f4bac03223
--- /dev/null
+++ b/packages/backend/src/models/entities/user-group.ts
@@ -0,0 +1,46 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserGroup {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserGroup.'
+	})
+	public createdAt: Date;
+
+	@Column('varchar', {
+		length: 256,
+	})
+	public name: string;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The ID of owner.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public isPrivate: boolean;
+
+	constructor(data: Partial<UserGroup>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/user-keypair.ts b/packages/backend/src/models/entities/user-keypair.ts
new file mode 100644
index 0000000000..603321d758
--- /dev/null
+++ b/packages/backend/src/models/entities/user-keypair.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserKeypair {
+	@PrimaryColumn(id())
+	public userId: User['id'];
+
+	@OneToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 4096,
+	})
+	public publicKey: string;
+
+	@Column('varchar', {
+		length: 4096,
+	})
+	public privateKey: string;
+
+	constructor(data: Partial<UserKeypair>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/user-list-joining.ts b/packages/backend/src/models/entities/user-list-joining.ts
new file mode 100644
index 0000000000..bb7dc40b95
--- /dev/null
+++ b/packages/backend/src/models/entities/user-list-joining.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { UserList } from './user-list';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'userListId'], { unique: true })
+export class UserListJoining {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserListJoining.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The user ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The list ID.'
+	})
+	public userListId: UserList['id'];
+
+	@ManyToOne(type => UserList, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public userList: UserList | null;
+}
diff --git a/packages/backend/src/models/entities/user-list.ts b/packages/backend/src/models/entities/user-list.ts
new file mode 100644
index 0000000000..35a83ef8c3
--- /dev/null
+++ b/packages/backend/src/models/entities/user-list.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserList {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserList.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The owner ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The name of the UserList.'
+	})
+	public name: string;
+}
diff --git a/packages/backend/src/models/entities/user-note-pining.ts b/packages/backend/src/models/entities/user-note-pining.ts
new file mode 100644
index 0000000000..04a6f8f645
--- /dev/null
+++ b/packages/backend/src/models/entities/user-note-pining.ts
@@ -0,0 +1,35 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+@Index(['userId', 'noteId'], { unique: true })
+export class UserNotePining {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the UserNotePinings.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column(id())
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+}
diff --git a/packages/backend/src/models/entities/user-pending.ts b/packages/backend/src/models/entities/user-pending.ts
new file mode 100644
index 0000000000..40482af333
--- /dev/null
+++ b/packages/backend/src/models/entities/user-pending.ts
@@ -0,0 +1,32 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class UserPending {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Column('timestamp with time zone')
+	public createdAt: Date;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 128,
+	})
+	public code: string;
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public username: string;
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public email: string;
+
+	@Column('varchar', {
+		length: 128,
+	})
+	public password: string;
+}
diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts
new file mode 100644
index 0000000000..8a8cacfd52
--- /dev/null
+++ b/packages/backend/src/models/entities/user-profile.ts
@@ -0,0 +1,215 @@
+import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+import { id } from '../id';
+import { User } from './user';
+import { Page } from './page';
+import { ffVisibility, notificationTypes } from '@/types';
+
+// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
+//       ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
+@Entity()
+export class UserProfile {
+	@PrimaryColumn(id())
+	public userId: User['id'];
+
+	@OneToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The location of the User.'
+	})
+	public location: string | null;
+
+	@Column('char', {
+		length: 10, nullable: true,
+		comment: 'The birthday (YYYY-MM-DD) of the User.'
+	})
+	public birthday: string | null;
+
+	@Column('varchar', {
+		length: 2048, nullable: true,
+		comment: 'The description (bio) of the User.'
+	})
+	public description: string | null;
+
+	@Column('jsonb', {
+		default: [],
+	})
+	public fields: {
+		name: string;
+		value: string;
+	}[];
+
+	@Column('varchar', {
+		length: 32, nullable: true,
+	})
+	public lang: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'Remote URL of the user.'
+	})
+	public url: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The email address of the User.'
+	})
+	public email: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public emailVerifyCode: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public emailVerified: boolean;
+
+	@Column('jsonb', {
+		default: ['follow', 'receiveFollowRequest', 'groupInvited']
+	})
+	public emailNotificationTypes: string[];
+
+	@Column('boolean', {
+		default: false,
+	})
+	public publicReactions: boolean;
+
+	@Column('enum', {
+		enum: ffVisibility,
+		default: 'public',
+	})
+	public ffVisibility: typeof ffVisibility[number];
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public twoFactorTempSecret: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public twoFactorSecret: string | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public twoFactorEnabled: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public securityKeysAvailable: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public usePasswordLessLogin: boolean;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The password hash of the User. It will be null if the origin of the user is local.'
+	})
+	public password: string | null;
+
+	// TODO: そのうち消す
+	@Column('jsonb', {
+		default: {},
+		comment: 'The client-specific data of the User.'
+	})
+	public clientData: Record<string, any>;
+
+	@Column('jsonb', {
+		default: {},
+		comment: 'The room data of the User.'
+	})
+	public room: Record<string, any>;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public autoAcceptFollowed: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether reject index by crawler.'
+	})
+	public noCrawle: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public alwaysMarkNsfw: boolean;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public carefulBot: boolean;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public injectFeaturedNote: boolean;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public receiveAnnouncementEmail: boolean;
+
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public pinnedPageId: Page['id'] | null;
+
+	@OneToOne(type => Page, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public pinnedPage: Page | null;
+
+	@Column('jsonb', {
+		default: {}
+	})
+	public integrations: Record<string, any>;
+
+	@Index()
+	@Column('boolean', {
+		default: false, select: false,
+	})
+	public enableWordMute: boolean;
+
+	@Column('jsonb', {
+		default: []
+	})
+	public mutedWords: string[][];
+
+	@Column('enum', {
+		enum: notificationTypes,
+		array: true,
+		default: [],
+	})
+	public mutingNotificationTypes: typeof notificationTypes[number][];
+
+	//#region Denormalized fields
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: '[Denormalized]'
+	})
+	public userHost: string | null;
+	//#endregion
+
+	constructor(data: Partial<UserProfile>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/user-publickey.ts b/packages/backend/src/models/entities/user-publickey.ts
new file mode 100644
index 0000000000..21edc3e9e2
--- /dev/null
+++ b/packages/backend/src/models/entities/user-publickey.ts
@@ -0,0 +1,34 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserPublickey {
+	@PrimaryColumn(id())
+	public userId: User['id'];
+
+	@OneToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index({ unique: true })
+	@Column('varchar', {
+		length: 256,
+	})
+	public keyId: string;
+
+	@Column('varchar', {
+		length: 4096,
+	})
+	public keyPem: string;
+
+	constructor(data: Partial<UserPublickey>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/user-security-key.ts b/packages/backend/src/models/entities/user-security-key.ts
new file mode 100644
index 0000000000..d54c728e53
--- /dev/null
+++ b/packages/backend/src/models/entities/user-security-key.ts
@@ -0,0 +1,48 @@
+import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class UserSecurityKey {
+	@PrimaryColumn('varchar', {
+		comment: 'Variable-length id given to navigator.credentials.get()'
+	})
+	public id: string;
+
+	@Index()
+	@Column(id())
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	@Index()
+	@Column('varchar', {
+		comment:
+			'Variable-length public key used to verify attestations (hex-encoded).'
+	})
+	public publicKey: string;
+
+	@Column('timestamp with time zone', {
+		comment:
+			'The date of the last time the UserSecurityKey was successfully validated.'
+	})
+	public lastUsed: Date;
+
+	@Column('varchar', {
+		comment: 'User-defined name for this key',
+		length: 30
+	})
+	public name: string;
+
+	constructor(data: Partial<UserSecurityKey>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts
new file mode 100644
index 0000000000..65aebd2d1a
--- /dev/null
+++ b/packages/backend/src/models/entities/user.ts
@@ -0,0 +1,250 @@
+import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+import { DriveFile } from './drive-file';
+import { id } from '../id';
+
+@Entity()
+@Index(['usernameLower', 'host'], { unique: true })
+export class User {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		comment: 'The created date of the User.'
+	})
+	public createdAt: Date;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		nullable: true,
+		comment: 'The updated date of the User.'
+	})
+	public updatedAt: Date | null;
+
+	@Column('timestamp with time zone', {
+		nullable: true
+	})
+	public lastFetchedAt: Date | null;
+
+	@Index()
+	@Column('timestamp with time zone', {
+		nullable: true
+	})
+	public lastActiveDate: Date | null;
+
+	@Column('boolean', {
+		default: false,
+	})
+	public hideOnlineStatus: boolean;
+
+	@Column('varchar', {
+		length: 128,
+		comment: 'The username of the User.'
+	})
+	public username: string;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, select: false,
+		comment: 'The username (lowercased) of the User.'
+	})
+	public usernameLower: string;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The name of the User.'
+	})
+	public name: string | null;
+
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of followers.'
+	})
+	public followersCount: number;
+
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of following.'
+	})
+	public followingCount: number;
+
+	@Column('integer', {
+		default: 0,
+		comment: 'The count of notes.'
+	})
+	public notesCount: number;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of avatar DriveFile.'
+	})
+	public avatarId: DriveFile['id'] | null;
+
+	@OneToOne(type => DriveFile, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public avatar: DriveFile | null;
+
+	@Column({
+		...id(),
+		nullable: true,
+		comment: 'The ID of banner DriveFile.'
+	})
+	public bannerId: DriveFile['id'] | null;
+
+	@OneToOne(type => DriveFile, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public banner: DriveFile | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public tags: string[];
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+	})
+	public avatarUrl: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+	})
+	public bannerUrl: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public avatarBlurhash: string | null;
+
+	@Column('varchar', {
+		length: 128, nullable: true,
+	})
+	public bannerBlurhash: string | null;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is suspended.'
+	})
+	public isSuspended: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is silenced.'
+	})
+	public isSilenced: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is locked.'
+	})
+	public isLocked: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is a bot.'
+	})
+	public isBot: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is a cat.'
+	})
+	public isCat: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is the admin.'
+	})
+	public isAdmin: boolean;
+
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is a moderator.'
+	})
+	public isModerator: boolean;
+
+	@Index()
+	@Column('boolean', {
+		default: true,
+		comment: 'Whether the User is explorable.'
+	})
+	public isExplorable: boolean;
+
+	// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
+	@Column('boolean', {
+		default: false,
+		comment: 'Whether the User is deleted.'
+	})
+	public isDeleted: boolean;
+
+	@Column('varchar', {
+		length: 128, array: true, default: '{}'
+	})
+	public emojis: string[];
+
+	@Index()
+	@Column('varchar', {
+		length: 128, nullable: true,
+		comment: 'The host of the User. It will be null if the origin of the user is local.'
+	})
+	public host: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The inbox URL of the User. It will be null if the origin of the user is local.'
+	})
+	public inbox: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'
+	})
+	public sharedInbox: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The featured URL of the User. It will be null if the origin of the user is local.'
+	})
+	public featured: string | null;
+
+	@Index()
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URI of the User. It will be null if the origin of the user is local.'
+	})
+	public uri: string | null;
+
+	@Column('varchar', {
+		length: 512, nullable: true,
+		comment: 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'
+	})
+	public followersUri: string | null;
+
+	@Index({ unique: true })
+	@Column('char', {
+		length: 16, nullable: true, unique: true,
+		comment: 'The native access token of the User. It will be null if the origin of the user is local.'
+	})
+	public token: string | null;
+
+	constructor(data: Partial<User>) {
+		if (data == null) return;
+
+		for (const [k, v] of Object.entries(data)) {
+			(this as any)[k] = v;
+		}
+	}
+}
+
+export interface ILocalUser extends User {
+	host: null;
+}
+
+export interface IRemoteUser extends User {
+	host: string;
+}
diff --git a/packages/backend/src/models/id.ts b/packages/backend/src/models/id.ts
new file mode 100644
index 0000000000..cdb8259073
--- /dev/null
+++ b/packages/backend/src/models/id.ts
@@ -0,0 +1,4 @@
+export const id = () => ({
+	type: 'varchar' as const,
+	length: 32
+});
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
new file mode 100644
index 0000000000..7154cca550
--- /dev/null
+++ b/packages/backend/src/models/index.ts
@@ -0,0 +1,130 @@
+import { getRepository, getCustomRepository } from 'typeorm';
+import { Announcement } from './entities/announcement';
+import { AnnouncementRead } from './entities/announcement-read';
+import { Instance } from './entities/instance';
+import { Poll } from './entities/poll';
+import { PollVote } from './entities/poll-vote';
+import { Meta } from './entities/meta';
+import { SwSubscription } from './entities/sw-subscription';
+import { NoteWatching } from './entities/note-watching';
+import { NoteThreadMuting } from './entities/note-thread-muting';
+import { NoteUnread } from './entities/note-unread';
+import { RegistrationTicket } from './entities/registration-tickets';
+import { UserRepository } from './repositories/user';
+import { NoteRepository } from './repositories/note';
+import { DriveFileRepository } from './repositories/drive-file';
+import { DriveFolderRepository } from './repositories/drive-folder';
+import { AccessToken } from './entities/access-token';
+import { UserNotePining } from './entities/user-note-pining';
+import { SigninRepository } from './repositories/signin';
+import { MessagingMessageRepository } from './repositories/messaging-message';
+import { ReversiGameRepository } from './repositories/games/reversi/game';
+import { UserListRepository } from './repositories/user-list';
+import { UserListJoining } from './entities/user-list-joining';
+import { UserGroupRepository } from './repositories/user-group';
+import { UserGroupJoining } from './entities/user-group-joining';
+import { UserGroupInvitationRepository } from './repositories/user-group-invitation';
+import { FollowRequestRepository } from './repositories/follow-request';
+import { MutingRepository } from './repositories/muting';
+import { BlockingRepository } from './repositories/blocking';
+import { NoteReactionRepository } from './repositories/note-reaction';
+import { NotificationRepository } from './repositories/notification';
+import { NoteFavoriteRepository } from './repositories/note-favorite';
+import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
+import { UserPublickey } from './entities/user-publickey';
+import { UserKeypair } from './entities/user-keypair';
+import { AppRepository } from './repositories/app';
+import { FollowingRepository } from './repositories/following';
+import { AbuseUserReportRepository } from './repositories/abuse-user-report';
+import { AuthSessionRepository } from './repositories/auth-session';
+import { UserProfile } from './entities/user-profile';
+import { AttestationChallenge } from './entities/attestation-challenge';
+import { UserSecurityKey } from './entities/user-security-key';
+import { HashtagRepository } from './repositories/hashtag';
+import { PageRepository } from './repositories/page';
+import { PageLikeRepository } from './repositories/page-like';
+import { GalleryPostRepository } from './repositories/gallery-post';
+import { GalleryLikeRepository } from './repositories/gallery-like';
+import { ModerationLogRepository } from './repositories/moderation-logs';
+import { UsedUsername } from './entities/used-username';
+import { ClipRepository } from './repositories/clip';
+import { ClipNote } from './entities/clip-note';
+import { AntennaRepository } from './repositories/antenna';
+import { AntennaNote } from './entities/antenna-note';
+import { PromoNote } from './entities/promo-note';
+import { PromoRead } from './entities/promo-read';
+import { EmojiRepository } from './repositories/emoji';
+import { RelayRepository } from './repositories/relay';
+import { ChannelRepository } from './repositories/channel';
+import { MutedNote } from './entities/muted-note';
+import { ChannelFollowing } from './entities/channel-following';
+import { ChannelNotePining } from './entities/channel-note-pining';
+import { RegistryItem } from './entities/registry-item';
+import { Ad } from './entities/ad';
+import { PasswordResetRequest } from './entities/password-reset-request';
+import { UserPending } from './entities/user-pending';
+
+export const Announcements = getRepository(Announcement);
+export const AnnouncementReads = getRepository(AnnouncementRead);
+export const Apps = getCustomRepository(AppRepository);
+export const Notes = getCustomRepository(NoteRepository);
+export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
+export const NoteWatchings = getRepository(NoteWatching);
+export const NoteThreadMutings = getRepository(NoteThreadMuting);
+export const NoteReactions = getCustomRepository(NoteReactionRepository);
+export const NoteUnreads = getRepository(NoteUnread);
+export const Polls = getRepository(Poll);
+export const PollVotes = getRepository(PollVote);
+export const Users = getCustomRepository(UserRepository);
+export const UserProfiles = getRepository(UserProfile);
+export const UserKeypairs = getRepository(UserKeypair);
+export const UserPendings = getRepository(UserPending);
+export const AttestationChallenges = getRepository(AttestationChallenge);
+export const UserSecurityKeys = getRepository(UserSecurityKey);
+export const UserPublickeys = getRepository(UserPublickey);
+export const UserLists = getCustomRepository(UserListRepository);
+export const UserListJoinings = getRepository(UserListJoining);
+export const UserGroups = getCustomRepository(UserGroupRepository);
+export const UserGroupJoinings = getRepository(UserGroupJoining);
+export const UserGroupInvitations = getCustomRepository(UserGroupInvitationRepository);
+export const UserNotePinings = getRepository(UserNotePining);
+export const UsedUsernames = getRepository(UsedUsername);
+export const Followings = getCustomRepository(FollowingRepository);
+export const FollowRequests = getCustomRepository(FollowRequestRepository);
+export const Instances = getRepository(Instance);
+export const Emojis = getCustomRepository(EmojiRepository);
+export const DriveFiles = getCustomRepository(DriveFileRepository);
+export const DriveFolders = getCustomRepository(DriveFolderRepository);
+export const Notifications = getCustomRepository(NotificationRepository);
+export const Metas = getRepository(Meta);
+export const Mutings = getCustomRepository(MutingRepository);
+export const Blockings = getCustomRepository(BlockingRepository);
+export const SwSubscriptions = getRepository(SwSubscription);
+export const Hashtags = getCustomRepository(HashtagRepository);
+export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
+export const RegistrationTickets = getRepository(RegistrationTicket);
+export const AuthSessions = getCustomRepository(AuthSessionRepository);
+export const AccessTokens = getRepository(AccessToken);
+export const Signins = getCustomRepository(SigninRepository);
+export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
+export const ReversiGames = getCustomRepository(ReversiGameRepository);
+export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
+export const Pages = getCustomRepository(PageRepository);
+export const PageLikes = getCustomRepository(PageLikeRepository);
+export const GalleryPosts = getCustomRepository(GalleryPostRepository);
+export const GalleryLikes = getCustomRepository(GalleryLikeRepository);
+export const ModerationLogs = getCustomRepository(ModerationLogRepository);
+export const Clips = getCustomRepository(ClipRepository);
+export const ClipNotes = getRepository(ClipNote);
+export const Antennas = getCustomRepository(AntennaRepository);
+export const AntennaNotes = getRepository(AntennaNote);
+export const PromoNotes = getRepository(PromoNote);
+export const PromoReads = getRepository(PromoRead);
+export const Relays = getCustomRepository(RelayRepository);
+export const MutedNotes = getRepository(MutedNote);
+export const Channels = getCustomRepository(ChannelRepository);
+export const ChannelFollowings = getRepository(ChannelFollowing);
+export const ChannelNotePinings = getRepository(ChannelNotePining);
+export const RegistryItems = getRepository(RegistryItem);
+export const Ads = getRepository(Ad);
+export const PasswordResetRequests = getRepository(PasswordResetRequest);
diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts
new file mode 100644
index 0000000000..039a9924d2
--- /dev/null
+++ b/packages/backend/src/models/repositories/abuse-user-report.ts
@@ -0,0 +1,38 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../index';
+import { AbuseUserReport } from '@/models/entities/abuse-user-report';
+import { awaitAll } from '@/prelude/await-all';
+
+@EntityRepository(AbuseUserReport)
+export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
+	public async pack(
+		src: AbuseUserReport['id'] | AbuseUserReport,
+	) {
+		const report = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: report.id,
+			createdAt: report.createdAt,
+			comment: report.comment,
+			resolved: report.resolved,
+			reporterId: report.reporterId,
+			targetUserId: report.targetUserId,
+			assigneeId: report.assigneeId,
+			reporter: Users.pack(report.reporter || report.reporterId, null, {
+				detail: true
+			}),
+			targetUser: Users.pack(report.targetUser || report.targetUserId, null, {
+				detail: true
+			}),
+			assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, {
+				detail: true
+			}) : null,
+		});
+	}
+
+	public packMany(
+		reports: any[],
+	) {
+		return Promise.all(reports.map(x => this.pack(x)));
+	}
+}
diff --git a/packages/backend/src/models/repositories/antenna.ts b/packages/backend/src/models/repositories/antenna.ts
new file mode 100644
index 0000000000..657de55581
--- /dev/null
+++ b/packages/backend/src/models/repositories/antenna.ts
@@ -0,0 +1,124 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Antenna } from '@/models/entities/antenna';
+import { Packed } from '@/misc/schema';
+import { AntennaNotes, UserGroupJoinings } from '../index';
+
+@EntityRepository(Antenna)
+export class AntennaRepository extends Repository<Antenna> {
+	public async pack(
+		src: Antenna['id'] | Antenna,
+	): Promise<Packed<'Antenna'>> {
+		const antenna = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null;
+		const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOne(antenna.userGroupJoiningId) : null;
+
+		return {
+			id: antenna.id,
+			createdAt: antenna.createdAt.toISOString(),
+			name: antenna.name,
+			keywords: antenna.keywords,
+			excludeKeywords: antenna.excludeKeywords,
+			src: antenna.src,
+			userListId: antenna.userListId,
+			userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
+			users: antenna.users,
+			caseSensitive: antenna.caseSensitive,
+			notify: antenna.notify,
+			withReplies: antenna.withReplies,
+			withFile: antenna.withFile,
+			hasUnreadNote
+		};
+	}
+}
+
+export const packedAntennaSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id'
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time'
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const
+		},
+		keywords: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				type: 'array' as const,
+				optional: false as const, nullable: false as const,
+				items: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				}
+			}
+		},
+		excludeKeywords: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				type: 'array' as const,
+				optional: false as const, nullable: false as const,
+				items: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				}
+			}
+		},
+		src: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			enum: ['home', 'all', 'users', 'list', 'group']
+		},
+		userListId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id'
+		},
+		userGroupId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id'
+		},
+		users: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const
+			}
+		},
+		caseSensitive: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+			default: false
+		},
+		notify: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const
+		},
+		withReplies: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+			default: false
+		},
+		withFile: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const
+		},
+		hasUnreadNote: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+			default: false
+		}
+	},
+};
diff --git a/packages/backend/src/models/repositories/app.ts b/packages/backend/src/models/repositories/app.ts
new file mode 100644
index 0000000000..0226edad11
--- /dev/null
+++ b/packages/backend/src/models/repositories/app.ts
@@ -0,0 +1,75 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { App } from '@/models/entities/app';
+import { AccessTokens } from '../index';
+import { Packed } from '@/misc/schema';
+import { User } from '../entities/user';
+
+@EntityRepository(App)
+export class AppRepository extends Repository<App> {
+	public async pack(
+		src: App['id'] | App,
+		me?: { id: User['id'] } | null | undefined,
+		options?: {
+			detail?: boolean,
+			includeSecret?: boolean,
+			includeProfileImageIds?: boolean
+		}
+	): Promise<Packed<'App'>> {
+		const opts = Object.assign({
+			detail: false,
+			includeSecret: false,
+			includeProfileImageIds: false
+		}, options);
+
+		const app = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: app.id,
+			name: app.name,
+			callbackUrl: app.callbackUrl,
+			permission: app.permission,
+			...(opts.includeSecret ? { secret: app.secret } : {}),
+			...(me ? {
+				isAuthorized: await AccessTokens.count({
+					appId: app.id,
+					userId: me,
+				}).then(count => count > 0)
+			} : {})
+		};
+	}
+}
+
+export const packedAppSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const
+		},
+		callbackUrl: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const
+		},
+		permission: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const
+			}
+		},
+		secret: {
+			type: 'string' as const,
+			optional: true as const, nullable: false as const
+		},
+		isAuthorized: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const
+		}
+	}
+};
diff --git a/packages/backend/src/models/repositories/auth-session.ts b/packages/backend/src/models/repositories/auth-session.ts
new file mode 100644
index 0000000000..c8f4c10f2a
--- /dev/null
+++ b/packages/backend/src/models/repositories/auth-session.ts
@@ -0,0 +1,21 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Apps } from '../index';
+import { AuthSession } from '@/models/entities/auth-session';
+import { awaitAll } from '@/prelude/await-all';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(AuthSession)
+export class AuthSessionRepository extends Repository<AuthSession> {
+	public async pack(
+		src: AuthSession['id'] | AuthSession,
+		me?: { id: User['id'] } | null | undefined
+	) {
+		const session = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: session.id,
+			app: Apps.pack(session.appId, me),
+			token: session.token
+		});
+	}
+}
diff --git a/packages/backend/src/models/repositories/blocking.ts b/packages/backend/src/models/repositories/blocking.ts
new file mode 100644
index 0000000000..ac60c9a4ce
--- /dev/null
+++ b/packages/backend/src/models/repositories/blocking.ts
@@ -0,0 +1,60 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../index';
+import { Blocking } from '@/models/entities/blocking';
+import { awaitAll } from '@/prelude/await-all';
+import { Packed } from '@/misc/schema';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(Blocking)
+export class BlockingRepository extends Repository<Blocking> {
+	public async pack(
+		src: Blocking['id'] | Blocking,
+		me?: { id: User['id'] } | null | undefined
+	): Promise<Packed<'Blocking'>> {
+		const blocking = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: blocking.id,
+			createdAt: blocking.createdAt.toISOString(),
+			blockeeId: blocking.blockeeId,
+			blockee: Users.pack(blocking.blockeeId, me, {
+				detail: true
+			})
+		});
+	}
+
+	public packMany(
+		blockings: any[],
+		me: { id: User['id'] }
+	) {
+		return Promise.all(blockings.map(x => this.pack(x, me)));
+	}
+}
+
+export const packedBlockingSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		blockeeId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		blockee: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/channel.ts b/packages/backend/src/models/repositories/channel.ts
new file mode 100644
index 0000000000..5c7d095473
--- /dev/null
+++ b/packages/backend/src/models/repositories/channel.ts
@@ -0,0 +1,95 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Channel } from '@/models/entities/channel';
+import { Packed } from '@/misc/schema';
+import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(Channel)
+export class ChannelRepository extends Repository<Channel> {
+	public async pack(
+		src: Channel['id'] | Channel,
+		me?: { id: User['id'] } | null | undefined,
+	): Promise<Packed<'Channel'>> {
+		const channel = typeof src === 'object' ? src : await this.findOneOrFail(src);
+		const meId = me ? me.id : null;
+
+		const banner = channel.bannerId ? await DriveFiles.findOne(channel.bannerId) : null;
+
+		const hasUnreadNote = meId ? (await NoteUnreads.findOne({ noteChannelId: channel.id, userId: meId })) != null : undefined;
+
+		const following = meId ? await ChannelFollowings.findOne({
+			followerId: meId,
+			followeeId: channel.id,
+		}) : null;
+
+		return {
+			id: channel.id,
+			createdAt: channel.createdAt.toISOString(),
+			lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null,
+			name: channel.name,
+			description: channel.description,
+			userId: channel.userId,
+			bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
+			usersCount: channel.usersCount,
+			notesCount: channel.notesCount,
+
+			...(me ? {
+				isFollowing: following != null,
+				hasUnreadNote,
+			} : {})
+		};
+	}
+}
+
+export const packedChannelSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		lastNotedAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		description: {
+			type: 'string' as const,
+			nullable: true as const, optional: false as const,
+		},
+		bannerUrl: {
+			type: 'string' as const,
+			format: 'url',
+			nullable: true as const, optional: false as const,
+		},
+		notesCount: {
+			type: 'number' as const,
+			nullable: false as const, optional: false as const,
+		},
+		usersCount: {
+			type: 'number' as const,
+			nullable: false as const, optional: false as const,
+		},
+		isFollowing: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const,
+		},
+		userId: {
+			type: 'string' as const,
+			nullable: true as const, optional: false as const,
+			format: 'id',
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/clip.ts b/packages/backend/src/models/repositories/clip.ts
new file mode 100644
index 0000000000..7892811d48
--- /dev/null
+++ b/packages/backend/src/models/repositories/clip.ts
@@ -0,0 +1,70 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Clip } from '@/models/entities/clip';
+import { Packed } from '@/misc/schema';
+import { Users } from '../index';
+import { awaitAll } from '@/prelude/await-all';
+
+@EntityRepository(Clip)
+export class ClipRepository extends Repository<Clip> {
+	public async pack(
+		src: Clip['id'] | Clip,
+	): Promise<Packed<'Clip'>> {
+		const clip = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: clip.id,
+			createdAt: clip.createdAt.toISOString(),
+			userId: clip.userId,
+			user: Users.pack(clip.user || clip.userId),
+			name: clip.name,
+			description: clip.description,
+			isPublic: clip.isPublic,
+		});
+	}
+
+	public packMany(
+		clips: Clip[],
+	) {
+		return Promise.all(clips.map(x => this.pack(x)));
+	}
+}
+
+export const packedClipSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		userId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		user: {
+			type: 'object' as const,
+			ref: 'User' as const,
+			optional: false as const, nullable: false as const,
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		description: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		isPublic: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts
new file mode 100644
index 0000000000..ddf9a46afd
--- /dev/null
+++ b/packages/backend/src/models/repositories/drive-file.ts
@@ -0,0 +1,249 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { DriveFile } from '@/models/entities/drive-file';
+import { Users, DriveFolders } from '../index';
+import { User } from '@/models/entities/user';
+import { toPuny } from '@/misc/convert-host';
+import { awaitAll } from '@/prelude/await-all';
+import { Packed } from '@/misc/schema';
+import config from '@/config/index';
+import { query, appendQuery } from '@/prelude/url';
+import { Meta } from '@/models/entities/meta';
+import { fetchMeta } from '@/misc/fetch-meta';
+
+type PackOptions = {
+	detail?: boolean,
+	self?: boolean,
+	withUser?: boolean,
+};
+
+@EntityRepository(DriveFile)
+export class DriveFileRepository extends Repository<DriveFile> {
+	public validateFileName(name: string): boolean {
+		return (
+			(name.trim().length > 0) &&
+			(name.length <= 200) &&
+			(name.indexOf('\\') === -1) &&
+			(name.indexOf('/') === -1) &&
+			(name.indexOf('..') === -1)
+		);
+	}
+
+	public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
+		// リモートかつメディアプロキシ
+		if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
+			return appendQuery(config.mediaProxy, query({
+				url: file.uri,
+				thumbnail: thumbnail ? '1' : undefined
+			}));
+		}
+
+		// リモートかつ期限切れはローカルプロキシを試みる
+		if (file.uri != null && file.isLink && meta && meta.proxyRemoteFiles) {
+			const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey;
+
+			if (key && !key.match('/')) {	// 古いものはここにオブジェクトストレージキーが入ってるので除外
+				return `${config.url}/files/${key}`;
+			}
+		}
+
+		const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type);
+
+		return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
+	}
+
+	public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> {
+		const id = typeof user === 'object' ? user.id : user;
+
+		const { sum } = await this
+			.createQueryBuilder('file')
+			.where('file.userId = :id', { id: id })
+			.andWhere('file.isLink = FALSE')
+			.select('SUM(file.size)', 'sum')
+			.getRawOne();
+
+		return parseInt(sum, 10) || 0;
+	}
+
+	public async calcDriveUsageOfHost(host: string): Promise<number> {
+		const { sum } = await this
+			.createQueryBuilder('file')
+			.where('file.userHost = :host', { host: toPuny(host) })
+			.andWhere('file.isLink = FALSE')
+			.select('SUM(file.size)', 'sum')
+			.getRawOne();
+
+		return parseInt(sum, 10) || 0;
+	}
+
+	public async calcDriveUsageOfLocal(): Promise<number> {
+		const { sum } = await this
+			.createQueryBuilder('file')
+			.where('file.userHost IS NULL')
+			.andWhere('file.isLink = FALSE')
+			.select('SUM(file.size)', 'sum')
+			.getRawOne();
+
+		return parseInt(sum, 10) || 0;
+	}
+
+	public async calcDriveUsageOfRemote(): Promise<number> {
+		const { sum } = await this
+			.createQueryBuilder('file')
+			.where('file.userHost IS NOT NULL')
+			.andWhere('file.isLink = FALSE')
+			.select('SUM(file.size)', 'sum')
+			.getRawOne();
+
+		return parseInt(sum, 10) || 0;
+	}
+
+	public async pack(src: DriveFile['id'], options?: PackOptions): Promise<Packed<'DriveFile'> | null>;
+	public async pack(src: DriveFile, options?: PackOptions): Promise<Packed<'DriveFile'>>;
+	public async pack(
+		src: DriveFile['id'] | DriveFile,
+		options?: PackOptions
+	): Promise<Packed<'DriveFile'> | null> {
+		const opts = Object.assign({
+			detail: false,
+			self: false
+		}, options);
+
+		const file = typeof src === 'object' ? src : await this.findOne(src);
+		if (file == null) return null;
+
+		const meta = await fetchMeta();
+
+		return await awaitAll({
+			id: file.id,
+			createdAt: file.createdAt.toISOString(),
+			name: file.name,
+			type: file.type,
+			md5: file.md5,
+			size: file.size,
+			isSensitive: file.isSensitive,
+			blurhash: file.blurhash,
+			properties: file.properties,
+			url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
+			thumbnailUrl: this.getPublicUrl(file, true, meta),
+			comment: file.comment,
+			folderId: file.folderId,
+			folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
+				detail: true
+			}) : null,
+			userId: opts.withUser ? file.userId : null,
+			user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null
+		});
+	}
+
+	public async packMany(
+		files: (DriveFile['id'] | DriveFile)[],
+		options?: PackOptions
+	) {
+		const items = await Promise.all(files.map(f => this.pack(f, options)));
+		return items.filter(x => x != null);
+	}
+}
+
+export const packedDriveFileSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			example: 'lenna.jpg'
+		},
+		type: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			example: 'image/jpeg'
+		},
+		md5: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'md5',
+			example: '15eca7fba0480996e2245f5185bf39f2'
+		},
+		size: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+			example: 51469
+		},
+		isSensitive: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		blurhash: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const
+		},
+		properties: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				width: {
+					type: 'number' as const,
+					optional: true as const, nullable: false as const,
+					example: 1280
+				},
+				height: {
+					type: 'number' as const,
+					optional: true as const, nullable: false as const,
+					example: 720
+				},
+				avgColor: {
+					type: 'string' as const,
+					optional: true as const, nullable: false as const,
+					example: 'rgb(40,65,87)'
+				}
+			}
+		},
+		url: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'url',
+		},
+		thumbnailUrl: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'url',
+		},
+		comment: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const
+		},
+		folderId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		folder: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			ref: 'DriveFolder' as const,
+		},
+		userId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		user: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			ref: 'User' as const,
+		}
+	},
+};
diff --git a/packages/backend/src/models/repositories/drive-folder.ts b/packages/backend/src/models/repositories/drive-folder.ts
new file mode 100644
index 0000000000..8ef6f01b5d
--- /dev/null
+++ b/packages/backend/src/models/repositories/drive-folder.ts
@@ -0,0 +1,91 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { DriveFolders, DriveFiles } from '../index';
+import { DriveFolder } from '@/models/entities/drive-folder';
+import { awaitAll } from '@/prelude/await-all';
+import { Packed } from '@/misc/schema';
+
+@EntityRepository(DriveFolder)
+export class DriveFolderRepository extends Repository<DriveFolder> {
+	public validateFolderName(name: string): boolean {
+		return (
+			(name.trim().length > 0) &&
+			(name.length <= 200)
+		);
+	}
+
+	public async pack(
+		src: DriveFolder['id'] | DriveFolder,
+		options?: {
+			detail: boolean
+		}
+	): Promise<Packed<'DriveFolder'>> {
+		const opts = Object.assign({
+			detail: false
+		}, options);
+
+		const folder = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: folder.id,
+			createdAt: folder.createdAt.toISOString(),
+			name: folder.name,
+			parentId: folder.parentId,
+
+			...(opts.detail ? {
+				foldersCount: DriveFolders.count({
+					parentId: folder.id
+				}),
+				filesCount: DriveFiles.count({
+					folderId: folder.id
+				}),
+
+				...(folder.parentId ? {
+					parent: this.pack(folder.parentId, {
+						detail: true
+					})
+				} : {})
+			} : {})
+		});
+	}
+}
+
+export const packedDriveFolderSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		foldersCount: {
+			type: 'number' as const,
+			optional: true as const, nullable: false as const,
+		},
+		filesCount: {
+			type: 'number' as const,
+			optional: true as const, nullable: false as const,
+		},
+		parentId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		parent: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			ref: 'DriveFolder' as const,
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/emoji.ts b/packages/backend/src/models/repositories/emoji.ts
new file mode 100644
index 0000000000..7985c27aba
--- /dev/null
+++ b/packages/backend/src/models/repositories/emoji.ts
@@ -0,0 +1,65 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Emoji } from '@/models/entities/emoji';
+import { Packed } from '@/misc/schema';
+
+@EntityRepository(Emoji)
+export class EmojiRepository extends Repository<Emoji> {
+	public async pack(
+		src: Emoji['id'] | Emoji,
+	): Promise<Packed<'Emoji'>> {
+		const emoji = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: emoji.id,
+			aliases: emoji.aliases,
+			name: emoji.name,
+			category: emoji.category,
+			host: emoji.host,
+			url: emoji.url,
+		};
+	}
+
+	public packMany(
+		emojis: any[],
+	) {
+		return Promise.all(emojis.map(x => this.pack(x)));
+	}
+}
+
+export const packedEmojiSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		aliases: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id',
+			},
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		category: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		host: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		url: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/federation-instance.ts b/packages/backend/src/models/repositories/federation-instance.ts
new file mode 100644
index 0000000000..4b70971ecf
--- /dev/null
+++ b/packages/backend/src/models/repositories/federation-instance.ts
@@ -0,0 +1,106 @@
+import config from '@/config/index';
+
+export const packedFederationInstanceSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id'
+		},
+		caughtAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time'
+		},
+		host: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			example: 'misskey.example.com'
+		},
+		usersCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		notesCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		followingCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		followersCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		driveUsage: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		driveFiles: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		latestRequestSentAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'date-time'
+		},
+		lastCommunicatedAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time'
+		},
+		isNotResponding: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const
+		},
+		isSuspended: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const
+		},
+		softwareName: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			example: 'misskey'
+		},
+		softwareVersion: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			example: config.version
+		},
+		openRegistrations: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: true as const,
+			example: true
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const
+		},
+		description: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const
+		},
+		maintainerName: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const
+		},
+		maintainerEmail: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const
+		},
+		iconUrl: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'url'
+		},
+		infoUpdatedAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'date-time'
+		}
+	}
+};
diff --git a/packages/backend/src/models/repositories/follow-request.ts b/packages/backend/src/models/repositories/follow-request.ts
new file mode 100644
index 0000000000..d6ee58e235
--- /dev/null
+++ b/packages/backend/src/models/repositories/follow-request.ts
@@ -0,0 +1,20 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { FollowRequest } from '@/models/entities/follow-request';
+import { Users } from '../index';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(FollowRequest)
+export class FollowRequestRepository extends Repository<FollowRequest> {
+	public async pack(
+		src: FollowRequest['id'] | FollowRequest,
+		me?: { id: User['id'] } | null | undefined
+	) {
+		const request = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: request.id,
+			follower: await Users.pack(request.followerId, me),
+			followee: await Users.pack(request.followeeId, me),
+		};
+	}
+}
diff --git a/packages/backend/src/models/repositories/following.ts b/packages/backend/src/models/repositories/following.ts
new file mode 100644
index 0000000000..b1f716069f
--- /dev/null
+++ b/packages/backend/src/models/repositories/following.ts
@@ -0,0 +1,124 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../index';
+import { Following } from '@/models/entities/following';
+import { awaitAll } from '@/prelude/await-all';
+import { Packed } from '@/misc/schema';
+import { User } from '@/models/entities/user';
+
+type LocalFollowerFollowing = Following & {
+	followerHost: null;
+	followerInbox: null;
+	followerSharedInbox: null;
+};
+
+type RemoteFollowerFollowing = Following & {
+	followerHost: string;
+	followerInbox: string;
+	followerSharedInbox: string;
+};
+
+type LocalFolloweeFollowing = Following & {
+	followeeHost: null;
+	followeeInbox: null;
+	followeeSharedInbox: null;
+};
+
+type RemoteFolloweeFollowing = Following & {
+	followeeHost: string;
+	followeeInbox: string;
+	followeeSharedInbox: string;
+};
+
+@EntityRepository(Following)
+export class FollowingRepository extends Repository<Following> {
+	public isLocalFollower(following: Following): following is LocalFollowerFollowing {
+		return following.followerHost == null;
+	}
+
+	public isRemoteFollower(following: Following): following is RemoteFollowerFollowing {
+		return following.followerHost != null;
+	}
+
+	public isLocalFollowee(following: Following): following is LocalFolloweeFollowing {
+		return following.followeeHost == null;
+	}
+
+	public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing {
+		return following.followeeHost != null;
+	}
+
+	public async pack(
+		src: Following['id'] | Following,
+		me?: { id: User['id'] } | null | undefined,
+		opts?: {
+			populateFollowee?: boolean;
+			populateFollower?: boolean;
+		}
+	): Promise<Packed<'Following'>> {
+		const following = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		if (opts == null) opts = {};
+
+		return await awaitAll({
+			id: following.id,
+			createdAt: following.createdAt.toISOString(),
+			followeeId: following.followeeId,
+			followerId: following.followerId,
+			followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
+				detail: true
+			}) : undefined,
+			follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
+				detail: true
+			}) : undefined,
+		});
+	}
+
+	public packMany(
+		followings: any[],
+		me?: { id: User['id'] } | null | undefined,
+		opts?: {
+			populateFollowee?: boolean;
+			populateFollower?: boolean;
+		}
+	) {
+		return Promise.all(followings.map(x => this.pack(x, me, opts)));
+	}
+}
+
+export const packedFollowingSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		followeeId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		followee: {
+			type: 'object' as const,
+			optional: true as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+		followerId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		follower: {
+			type: 'object' as const,
+			optional: true as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/gallery-like.ts b/packages/backend/src/models/repositories/gallery-like.ts
new file mode 100644
index 0000000000..79123e5eec
--- /dev/null
+++ b/packages/backend/src/models/repositories/gallery-like.ts
@@ -0,0 +1,25 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { GalleryLike } from '@/models/entities/gallery-like';
+import { GalleryPosts } from '../index';
+
+@EntityRepository(GalleryLike)
+export class GalleryLikeRepository extends Repository<GalleryLike> {
+	public async pack(
+		src: GalleryLike['id'] | GalleryLike,
+		me?: any
+	) {
+		const like = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: like.id,
+			post: await GalleryPosts.pack(like.post || like.postId, me),
+		};
+	}
+
+	public packMany(
+		likes: any[],
+		me: any
+	) {
+		return Promise.all(likes.map(x => this.pack(x, me)));
+	}
+}
diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts
new file mode 100644
index 0000000000..4f666ff252
--- /dev/null
+++ b/packages/backend/src/models/repositories/gallery-post.ts
@@ -0,0 +1,111 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { GalleryPost } from '@/models/entities/gallery-post';
+import { Packed } from '@/misc/schema';
+import { Users, DriveFiles, GalleryLikes } from '../index';
+import { awaitAll } from '@/prelude/await-all';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(GalleryPost)
+export class GalleryPostRepository extends Repository<GalleryPost> {
+	public async pack(
+		src: GalleryPost['id'] | GalleryPost,
+		me?: { id: User['id'] } | null | undefined,
+	): Promise<Packed<'GalleryPost'>> {
+		const meId = me ? me.id : null;
+		const post = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: post.id,
+			createdAt: post.createdAt.toISOString(),
+			updatedAt: post.updatedAt.toISOString(),
+			userId: post.userId,
+			user: Users.pack(post.user || post.userId, me),
+			title: post.title,
+			description: post.description,
+			fileIds: post.fileIds,
+			files: DriveFiles.packMany(post.fileIds),
+			tags: post.tags.length > 0 ? post.tags : undefined,
+			isSensitive: post.isSensitive,
+			likedCount: post.likedCount,
+			isLiked: meId ? await GalleryLikes.findOne({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
+		});
+	}
+
+	public packMany(
+		posts: GalleryPost[],
+		me?: { id: User['id'] } | null | undefined,
+	) {
+		return Promise.all(posts.map(x => this.pack(x, me)));
+	}
+}
+
+export const packedGalleryPostSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		updatedAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		title: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		description: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		userId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		user: {
+			type: 'object' as const,
+			ref: 'User' as const,
+			optional: false as const, nullable: false as const,
+		},
+		fileIds: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			}
+		},
+		files: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				ref: 'DriveFile' as const,
+			}
+		},
+		tags: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			}
+		},
+		isSensitive: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/games/reversi/game.ts b/packages/backend/src/models/repositories/games/reversi/game.ts
new file mode 100644
index 0000000000..9adb386fa9
--- /dev/null
+++ b/packages/backend/src/models/repositories/games/reversi/game.ts
@@ -0,0 +1,191 @@
+import { User } from '@/models/entities/user';
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../../../index';
+import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { Packed } from '@/misc/schema';
+
+@EntityRepository(ReversiGame)
+export class ReversiGameRepository extends Repository<ReversiGame> {
+	public async pack(
+		src: ReversiGame['id'] | ReversiGame,
+		me?: { id: User['id'] } | null | undefined,
+		options?: {
+			detail?: boolean
+		}
+	): Promise<Packed<'ReversiGame'>> {
+		const opts = Object.assign({
+			detail: true
+		}, options);
+
+		const game = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: game.id,
+			createdAt: game.createdAt.toISOString(),
+			startedAt: game.startedAt && game.startedAt.toISOString(),
+			isStarted: game.isStarted,
+			isEnded: game.isEnded,
+			form1: game.form1,
+			form2: game.form2,
+			user1Accepted: game.user1Accepted,
+			user2Accepted: game.user2Accepted,
+			user1Id: game.user1Id,
+			user2Id: game.user2Id,
+			user1: await Users.pack(game.user1Id, me),
+			user2: await Users.pack(game.user2Id, me),
+			winnerId: game.winnerId,
+			winner: game.winnerId ? await Users.pack(game.winnerId, me) : null,
+			surrendered: game.surrendered,
+			black: game.black,
+			bw: game.bw,
+			isLlotheo: game.isLlotheo,
+			canPutEverywhere: game.canPutEverywhere,
+			loopedBoard: game.loopedBoard,
+			...(opts.detail ? {
+				logs: game.logs.map(log => ({
+					at: log.at.toISOString(),
+					color: log.color,
+					pos: log.pos
+				})),
+				map: game.map,
+			} : {})
+		};
+	}
+}
+
+export const packedReversiGameSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		startedAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'date-time',
+		},
+		isStarted: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		isEnded: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		form1: {
+			type: 'any' as const,
+			optional: false as const, nullable: true as const,
+		},
+		form2: {
+			type: 'any' as const,
+			optional: false as const, nullable: true as const,
+		},
+		user1Accepted: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		user2Accepted: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		user1Id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		user2Id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		user1: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+		user2: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+		winnerId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		winner: {
+			type: 'object' as const,
+			optional: false as const, nullable: true as const,
+			ref: 'User' as const,
+		},
+		surrendered: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		black: {
+			type: 'number' as const,
+			optional: false as const, nullable: true as const,
+		},
+		bw: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		isLlotheo: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		canPutEverywhere: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		loopedBoard: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		logs: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'object' as const,
+				optional: true as const, nullable: false as const,
+				properties: {
+					at: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const,
+						format: 'date-time',
+					},
+					color: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const,
+					},
+					pos: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+					},
+				}
+			}
+		},
+		map: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			}
+		}
+	}
+};
diff --git a/packages/backend/src/models/repositories/games/reversi/matching.ts b/packages/backend/src/models/repositories/games/reversi/matching.ts
new file mode 100644
index 0000000000..b4515800df
--- /dev/null
+++ b/packages/backend/src/models/repositories/games/reversi/matching.ts
@@ -0,0 +1,69 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { ReversiMatching } from '@/models/entities/games/reversi/matching';
+import { Users } from '../../../index';
+import { awaitAll } from '@/prelude/await-all';
+import { User } from '@/models/entities/user';
+import { Packed } from '@/misc/schema';
+
+@EntityRepository(ReversiMatching)
+export class ReversiMatchingRepository extends Repository<ReversiMatching> {
+	public async pack(
+		src: ReversiMatching['id'] | ReversiMatching,
+		me: { id: User['id'] }
+	): Promise<Packed<'ReversiMatching'>> {
+		const matching = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: matching.id,
+			createdAt: matching.createdAt.toISOString(),
+			parentId: matching.parentId,
+			parent: Users.pack(matching.parentId, me, {
+				detail: true
+			}),
+			childId: matching.childId,
+			child: Users.pack(matching.childId, me, {
+				detail: true
+			})
+		});
+	}
+}
+
+export const packedReversiMatchingSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		parentId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		parent: {
+			type: 'object' as const,
+			optional: false as const, nullable: true as const,
+			ref: 'User' as const,
+		},
+		childId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		child: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/hashtag.ts b/packages/backend/src/models/repositories/hashtag.ts
new file mode 100644
index 0000000000..d52f6ba7c6
--- /dev/null
+++ b/packages/backend/src/models/repositories/hashtag.ts
@@ -0,0 +1,62 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Hashtag } from '@/models/entities/hashtag';
+import { Packed } from '@/misc/schema';
+
+@EntityRepository(Hashtag)
+export class HashtagRepository extends Repository<Hashtag> {
+	public async pack(
+		src: Hashtag,
+	): Promise<Packed<'Hashtag'>> {
+		return {
+			tag: src.name,
+			mentionedUsersCount: src.mentionedUsersCount,
+			mentionedLocalUsersCount: src.mentionedLocalUsersCount,
+			mentionedRemoteUsersCount: src.mentionedRemoteUsersCount,
+			attachedUsersCount: src.attachedUsersCount,
+			attachedLocalUsersCount: src.attachedLocalUsersCount,
+			attachedRemoteUsersCount: src.attachedRemoteUsersCount,
+		};
+	}
+
+	public packMany(
+		hashtags: Hashtag[],
+	) {
+		return Promise.all(hashtags.map(x => this.pack(x)));
+	}
+}
+
+export const packedHashtagSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		tag: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			example: 'misskey',
+		},
+		mentionedUsersCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		mentionedLocalUsersCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		mentionedRemoteUsersCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		attachedUsersCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		attachedLocalUsersCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		attachedRemoteUsersCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/messaging-message.ts b/packages/backend/src/models/repositories/messaging-message.ts
new file mode 100644
index 0000000000..abdff63689
--- /dev/null
+++ b/packages/backend/src/models/repositories/messaging-message.ts
@@ -0,0 +1,119 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { Users, DriveFiles, UserGroups } from '../index';
+import { Packed } from '@/misc/schema';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(MessagingMessage)
+export class MessagingMessageRepository extends Repository<MessagingMessage> {
+	public validateText(text: string): boolean {
+		return text.trim().length <= 1000 && text.trim() != '';
+	}
+
+	public async pack(
+		src: MessagingMessage['id'] | MessagingMessage,
+		me?: { id: User['id'] } | null | undefined,
+		options?: {
+			populateRecipient?: boolean,
+			populateGroup?: boolean,
+		}
+	): Promise<Packed<'MessagingMessage'>> {
+		const opts = options || {
+			populateRecipient: true,
+			populateGroup: true,
+		};
+
+		const message = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: message.id,
+			createdAt: message.createdAt.toISOString(),
+			text: message.text,
+			userId: message.userId,
+			user: await Users.pack(message.user || message.userId, me),
+			recipientId: message.recipientId,
+			recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined,
+			groupId: message.groupId,
+			group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined,
+			fileId: message.fileId,
+			file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
+			isRead: message.isRead,
+			reads: message.reads,
+		};
+	}
+}
+
+export const packedMessagingMessageSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		userId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		user: {
+			type: 'object' as const,
+			ref: 'User' as const,
+			optional: true as const, nullable: false as const,
+		},
+		text: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		fileId: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+			format: 'id',
+		},
+		file: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			ref: 'DriveFile' as const,
+		},
+		recipientId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+		},
+		recipient: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			ref: 'User' as const,
+		},
+		groupId: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+			format: 'id',
+		},
+		group: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			ref: 'UserGroup' as const,
+		},
+		isRead: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const,
+		},
+		reads: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			}
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/moderation-logs.ts b/packages/backend/src/models/repositories/moderation-logs.ts
new file mode 100644
index 0000000000..c7df3afdc9
--- /dev/null
+++ b/packages/backend/src/models/repositories/moderation-logs.ts
@@ -0,0 +1,30 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../index';
+import { ModerationLog } from '@/models/entities/moderation-log';
+import { awaitAll } from '@/prelude/await-all';
+
+@EntityRepository(ModerationLog)
+export class ModerationLogRepository extends Repository<ModerationLog> {
+	public async pack(
+		src: ModerationLog['id'] | ModerationLog,
+	) {
+		const log = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: log.id,
+			createdAt: log.createdAt,
+			type: log.type,
+			info: log.info,
+			userId: log.userId,
+			user: Users.pack(log.user || log.userId, null, {
+				detail: true
+			}),
+		});
+	}
+
+	public packMany(
+		reports: any[],
+	) {
+		return Promise.all(reports.map(x => this.pack(x)));
+	}
+}
diff --git a/packages/backend/src/models/repositories/muting.ts b/packages/backend/src/models/repositories/muting.ts
new file mode 100644
index 0000000000..869afd3c4e
--- /dev/null
+++ b/packages/backend/src/models/repositories/muting.ts
@@ -0,0 +1,60 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '../index';
+import { Muting } from '@/models/entities/muting';
+import { awaitAll } from '@/prelude/await-all';
+import { Packed } from '@/misc/schema';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(Muting)
+export class MutingRepository extends Repository<Muting> {
+	public async pack(
+		src: Muting['id'] | Muting,
+		me?: { id: User['id'] } | null | undefined
+	): Promise<Packed<'Muting'>> {
+		const muting = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return await awaitAll({
+			id: muting.id,
+			createdAt: muting.createdAt.toISOString(),
+			muteeId: muting.muteeId,
+			mutee: Users.pack(muting.muteeId, me, {
+				detail: true
+			})
+		});
+	}
+
+	public packMany(
+		mutings: any[],
+		me: { id: User['id'] }
+	) {
+		return Promise.all(mutings.map(x => this.pack(x, me)));
+	}
+}
+
+export const packedMutingSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		muteeId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		mutee: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/note-favorite.ts b/packages/backend/src/models/repositories/note-favorite.ts
new file mode 100644
index 0000000000..47586a9116
--- /dev/null
+++ b/packages/backend/src/models/repositories/note-favorite.ts
@@ -0,0 +1,56 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { NoteFavorite } from '@/models/entities/note-favorite';
+import { Notes } from '../index';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(NoteFavorite)
+export class NoteFavoriteRepository extends Repository<NoteFavorite> {
+	public async pack(
+		src: NoteFavorite['id'] | NoteFavorite,
+		me?: { id: User['id'] } | null | undefined
+	) {
+		const favorite = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: favorite.id,
+			createdAt: favorite.createdAt,
+			noteId: favorite.noteId,
+			note: await Notes.pack(favorite.note || favorite.noteId, me),
+		};
+	}
+
+	public packMany(
+		favorites: any[],
+		me: { id: User['id'] }
+	) {
+		return Promise.all(favorites.map(x => this.pack(x, me)));
+	}
+}
+
+export const packedNoteFavoriteSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		note: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note' as const,
+		},
+		noteId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts
new file mode 100644
index 0000000000..5d86065526
--- /dev/null
+++ b/packages/backend/src/models/repositories/note-reaction.ts
@@ -0,0 +1,60 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { NoteReaction } from '@/models/entities/note-reaction';
+import { Notes, Users } from '../index';
+import { Packed } from '@/misc/schema';
+import { convertLegacyReaction } from '@/misc/reaction-lib';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(NoteReaction)
+export class NoteReactionRepository extends Repository<NoteReaction> {
+	public async pack(
+		src: NoteReaction['id'] | NoteReaction,
+		me?: { id: User['id'] } | null | undefined,
+		options?: {
+			withNote: boolean;
+		},
+	): Promise<Packed<'NoteReaction'>> {
+		const opts = Object.assign({
+			withNote: false,
+		}, options);
+
+		const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: reaction.id,
+			createdAt: reaction.createdAt.toISOString(),
+			user: await Users.pack(reaction.userId, me),
+			type: convertLegacyReaction(reaction.reaction),
+			...(opts.withNote ? {
+				note: await Notes.pack(reaction.noteId, me),
+			} : {})
+		};
+	}
+}
+
+export const packedNoteReactionSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		user: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User' as const,
+		},
+		type: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
new file mode 100644
index 0000000000..0f00c34c9c
--- /dev/null
+++ b/packages/backend/src/models/repositories/note.ts
@@ -0,0 +1,512 @@
+import { EntityRepository, Repository, In } from 'typeorm';
+import * as mfm from 'mfm-js';
+import { Note } from '@/models/entities/note';
+import { User } from '@/models/entities/user';
+import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '../index';
+import { Packed } from '@/misc/schema';
+import { nyaize } from '@/misc/nyaize';
+import { awaitAll } from '@/prelude/await-all';
+import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib';
+import { NoteReaction } from '@/models/entities/note-reaction';
+import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis';
+
+@EntityRepository(Note)
+export class NoteRepository extends Repository<Note> {
+	public validateCw(x: string) {
+		return x.trim().length <= 100;
+	}
+
+	public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
+		// visibility が specified かつ自分が指定されていなかったら非表示
+		if (note.visibility === 'specified') {
+			if (meId == null) {
+				return false;
+			} else if (meId === note.userId) {
+				return true;
+			} else {
+				// 指定されているかどうか
+				const specified = note.visibleUserIds.some((id: any) => meId === id);
+
+				if (specified) {
+					return true;
+				} else {
+					return false;
+				}
+			}
+		}
+
+		// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
+		if (note.visibility === 'followers') {
+			if (meId == null) {
+				return false;
+			} else if (meId === note.userId) {
+				return true;
+			} else if (note.reply && (meId === note.reply.userId)) {
+				// 自分の投稿に対するリプライ
+				return true;
+			} else if (note.mentions && note.mentions.some(id => meId === id)) {
+				// 自分へのメンション
+				return true;
+			} else {
+				// フォロワーかどうか
+				const following = await Followings.findOne({
+					followeeId: note.userId,
+					followerId: meId
+				});
+
+				if (following == null) {
+					return false;
+				} else {
+					return true;
+				}
+			}
+		}
+
+		return true;
+	}
+
+	private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
+		// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
+		let hide = false;
+
+		// visibility が specified かつ自分が指定されていなかったら非表示
+		if (packedNote.visibility === 'specified') {
+			if (meId == null) {
+				hide = true;
+			} else if (meId === packedNote.userId) {
+				hide = false;
+			} else {
+				// 指定されているかどうか
+				const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
+
+				if (specified) {
+					hide = false;
+				} else {
+					hide = true;
+				}
+			}
+		}
+
+		// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
+		if (packedNote.visibility === 'followers') {
+			if (meId == null) {
+				hide = true;
+			} else if (meId === packedNote.userId) {
+				hide = false;
+			} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
+				// 自分の投稿に対するリプライ
+				hide = false;
+			} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
+				// 自分へのメンション
+				hide = false;
+			} else {
+				// フォロワーかどうか
+				const following = await Followings.findOne({
+					followeeId: packedNote.userId,
+					followerId: meId
+				});
+
+				if (following == null) {
+					hide = true;
+				} else {
+					hide = false;
+				}
+			}
+		}
+
+		if (hide) {
+			packedNote.visibleUserIds = undefined;
+			packedNote.fileIds = [];
+			packedNote.files = [];
+			packedNote.text = null;
+			packedNote.poll = undefined;
+			packedNote.cw = null;
+			packedNote.isHidden = true;
+		}
+	}
+
+	public async pack(
+		src: Note['id'] | Note,
+		me?: { id: User['id'] } | null | undefined,
+		options?: {
+			detail?: boolean;
+			skipHide?: boolean;
+			_hint_?: {
+				myReactions: Map<Note['id'], NoteReaction | null>;
+			};
+		}
+	): Promise<Packed<'Note'>> {
+		const opts = Object.assign({
+			detail: true,
+			skipHide: false
+		}, options);
+
+		const meId = me ? me.id : null;
+		const note = typeof src === 'object' ? src : await this.findOneOrFail(src);
+		const host = note.userHost;
+
+		async function populatePoll() {
+			const poll = await Polls.findOneOrFail(note.id);
+			const choices = poll.choices.map(c => ({
+				text: c,
+				votes: poll.votes[poll.choices.indexOf(c)],
+				isVoted: false
+			}));
+
+			if (poll.multiple) {
+				const votes = await PollVotes.find({
+					userId: meId!,
+					noteId: note.id
+				});
+
+				const myChoices = votes.map(v => v.choice);
+				for (const myChoice of myChoices) {
+					choices[myChoice].isVoted = true;
+				}
+			} else {
+				const vote = await PollVotes.findOne({
+					userId: meId!,
+					noteId: note.id
+				});
+
+				if (vote) {
+					choices[vote.choice].isVoted = true;
+				}
+			}
+
+			return {
+				multiple: poll.multiple,
+				expiresAt: poll.expiresAt,
+				choices
+			};
+		}
+
+		async function populateMyReaction() {
+			if (options?._hint_?.myReactions) {
+				const reaction = options._hint_.myReactions.get(note.id);
+				if (reaction) {
+					return convertLegacyReaction(reaction.reaction);
+				} else if (reaction === null) {
+					return undefined;
+				}
+				// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
+			}
+
+			const reaction = await NoteReactions.findOne({
+				userId: meId!,
+				noteId: note.id,
+			});
+
+			if (reaction) {
+				return convertLegacyReaction(reaction.reaction);
+			}
+
+			return undefined;
+		}
+
+		let text = note.text;
+
+		if (note.name && (note.url || note.uri)) {
+			text = `【${note.name}】\n${(note.text || '').trim()}\n\n${note.url || note.uri}`;
+		}
+
+		const channel = note.channelId
+			? note.channel
+				? note.channel
+				: await Channels.findOne(note.channelId)
+			: null;
+
+		const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
+
+		const packed = await awaitAll({
+			id: note.id,
+			createdAt: note.createdAt.toISOString(),
+			userId: note.userId,
+			user: Users.pack(note.user || note.userId, me, {
+				detail: false,
+			}),
+			text: text,
+			cw: note.cw,
+			visibility: note.visibility,
+			localOnly: note.localOnly || undefined,
+			visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
+			viaMobile: note.viaMobile || undefined,
+			renoteCount: note.renoteCount,
+			repliesCount: note.repliesCount,
+			reactions: convertLegacyReactions(note.reactions),
+			tags: note.tags.length > 0 ? note.tags : undefined,
+			emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host),
+			fileIds: note.fileIds,
+			files: DriveFiles.packMany(note.fileIds),
+			replyId: note.replyId,
+			renoteId: note.renoteId,
+			channelId: note.channelId || undefined,
+			channel: channel ? {
+				id: channel.id,
+				name: channel.name,
+			} : undefined,
+			mentions: note.mentions.length > 0 ? note.mentions : undefined,
+			uri: note.uri || undefined,
+			url: note.url || undefined,
+
+			...(opts.detail ? {
+				reply: note.replyId ? this.pack(note.reply || note.replyId, me, {
+					detail: false,
+					_hint_: options?._hint_
+				}) : undefined,
+
+				renote: note.renoteId ? this.pack(note.renote || note.renoteId, me, {
+					detail: true,
+					_hint_: options?._hint_
+				}) : undefined,
+
+				poll: note.hasPoll ? populatePoll() : undefined,
+
+				...(meId ? {
+					myReaction: populateMyReaction()
+				} : {})
+			} : {})
+		});
+
+		if (packed.user.isCat && packed.text) {
+			const tokens = packed.text ? mfm.parse(packed.text) : [];
+			mfm.inspect(tokens, node => {
+				if (node.type === 'text') {
+					// TODO: quoteなtextはskip
+					node.props.text = nyaize(node.props.text);
+				}
+			});
+			packed.text = mfm.toString(tokens);
+		}
+
+		if (!opts.skipHide) {
+			await this.hideNote(packed, meId);
+		}
+
+		return packed;
+	}
+
+	public async packMany(
+		notes: Note[],
+		me?: { id: User['id'] } | null | undefined,
+		options?: {
+			detail?: boolean;
+			skipHide?: boolean;
+		}
+	) {
+		if (notes.length === 0) return [];
+
+		const meId = me ? me.id : null;
+		const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
+		if (meId) {
+			const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
+			const targets = [...notes.map(n => n.id), ...renoteIds];
+			const myReactions = await NoteReactions.find({
+				userId: meId,
+				noteId: In(targets),
+			});
+
+			for (const target of targets) {
+				myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
+			}
+		}
+
+		await prefetchEmojis(aggregateNoteEmojis(notes));
+
+		return await Promise.all(notes.map(n => this.pack(n, me, {
+			...options,
+			_hint_: {
+				myReactions: myReactionsMap
+			}
+		})));
+	}
+}
+
+export const packedNoteSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		text: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		cw: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+		},
+		userId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		user: {
+			type: 'object' as const,
+			ref: 'User' as const,
+			optional: false as const, nullable: false as const,
+		},
+		replyId: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		renoteId: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		reply: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			ref: 'Note' as const,
+		},
+		renote: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			ref: 'Note' as const,
+		},
+		viaMobile: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const,
+		},
+		isHidden: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const,
+		},
+		visibility: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		mentions: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			}
+		},
+		visibleUserIds: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			}
+		},
+		fileIds: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			}
+		},
+		files: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				ref: 'DriveFile' as const,
+			}
+		},
+		tags: {
+			type: 'array' as const,
+			optional: true as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			}
+		},
+		poll: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+		},
+		channelId: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		channel: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+			items: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				properties: {
+					id: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const,
+					},
+					name: {
+						type: 'string' as const,
+						optional: false as const, nullable: true as const,
+					},
+				},
+			},
+		},
+		localOnly: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const,
+		},
+		emojis: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				properties: {
+					name: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const,
+					},
+					url: {
+						type: 'string' as const,
+						optional: false as const, nullable: true as const,
+					},
+				},
+			},
+		},
+		reactions: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+		},
+		renoteCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		repliesCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		uri: {
+			type: 'string' as const,
+			optional: true as const, nullable: false as const,
+		},
+		url: {
+			type: 'string' as const,
+			optional: true as const, nullable: false as const,
+		},
+
+		myReaction: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/notification.ts b/packages/backend/src/models/repositories/notification.ts
new file mode 100644
index 0000000000..d1cf9b087e
--- /dev/null
+++ b/packages/backend/src/models/repositories/notification.ts
@@ -0,0 +1,175 @@
+import { EntityRepository, In, Repository } from 'typeorm';
+import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index';
+import { Notification } from '@/models/entities/notification';
+import { awaitAll } from '@/prelude/await-all';
+import { Packed } from '@/misc/schema';
+import { Note } from '@/models/entities/note';
+import { NoteReaction } from '@/models/entities/note-reaction';
+import { User } from '@/models/entities/user';
+import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis';
+import { notificationTypes } from '@/types';
+
+@EntityRepository(Notification)
+export class NotificationRepository extends Repository<Notification> {
+	public async pack(
+		src: Notification['id'] | Notification,
+		options: {
+			_hintForEachNotes_?: {
+				myReactions: Map<Note['id'], NoteReaction | null>;
+			};
+		}
+	): Promise<Packed<'Notification'>> {
+		const notification = typeof src === 'object' ? src : await this.findOneOrFail(src);
+		const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null;
+
+		return await awaitAll({
+			id: notification.id,
+			createdAt: notification.createdAt.toISOString(),
+			type: notification.type,
+			isRead: notification.isRead,
+			userId: notification.notifierId,
+			user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null,
+			...(notification.type === 'mention' ? {
+				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
+					detail: true,
+					_hint_: options._hintForEachNotes_
+				}),
+			} : {}),
+			...(notification.type === 'reply' ? {
+				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
+					detail: true,
+					_hint_: options._hintForEachNotes_
+				}),
+			} : {}),
+			...(notification.type === 'renote' ? {
+				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
+					detail: true,
+					_hint_: options._hintForEachNotes_
+				}),
+			} : {}),
+			...(notification.type === 'quote' ? {
+				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
+					detail: true,
+					_hint_: options._hintForEachNotes_
+				}),
+			} : {}),
+			...(notification.type === 'reaction' ? {
+				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
+					detail: true,
+					_hint_: options._hintForEachNotes_
+				}),
+				reaction: notification.reaction
+			} : {}),
+			...(notification.type === 'pollVote' ? {
+				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
+					detail: true,
+					_hint_: options._hintForEachNotes_
+				}),
+				choice: notification.choice
+			} : {}),
+			...(notification.type === 'groupInvited' ? {
+				invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!),
+			} : {}),
+			...(notification.type === 'app' ? {
+				body: notification.customBody,
+				header: notification.customHeader || token?.name,
+				icon: notification.customIcon || token?.iconUrl,
+			} : {}),
+		});
+	}
+
+	public async packMany(
+		notifications: Notification[],
+		meId: User['id']
+	) {
+		if (notifications.length === 0) return [];
+
+		const notes = notifications.filter(x => x.note != null).map(x => x.note!);
+		const noteIds = notes.map(n => n.id);
+		const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
+		const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
+		const targets = [...noteIds, ...renoteIds];
+		const myReactions = await NoteReactions.find({
+			userId: meId,
+			noteId: In(targets),
+		});
+
+		for (const target of targets) {
+			myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
+		}
+
+		await prefetchEmojis(aggregateNoteEmojis(notes));
+
+		return await Promise.all(notifications.map(x => this.pack(x, {
+			_hintForEachNotes_: {
+				myReactions: myReactionsMap
+			}
+		})));
+	}
+}
+
+export const packedNotificationSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		isRead: {
+			type: 'boolean' as const,
+			optional: false as const, nullable: false as const,
+		},
+		type: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			enum: [...notificationTypes],
+		},
+		user: {
+			type: 'object' as const,
+			ref: 'User' as const,
+			optional: true as const, nullable: true as const,
+		},
+		userId: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+			format: 'id',
+		},
+		note: {
+			type: 'object' as const,
+			ref: 'Note' as const,
+			optional: true as const, nullable: true as const,
+		},
+		reaction: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+		},
+		choice: {
+			type: 'number' as const,
+			optional: true as const, nullable: true as const,
+		},
+		invitation: {
+			type: 'object' as const,
+			optional: true as const, nullable: true as const,
+		},
+		body: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+		},
+		header: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+		},
+		icon: {
+			type: 'string' as const,
+			optional: true as const, nullable: true as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/page-like.ts b/packages/backend/src/models/repositories/page-like.ts
new file mode 100644
index 0000000000..28f34254d9
--- /dev/null
+++ b/packages/backend/src/models/repositories/page-like.ts
@@ -0,0 +1,26 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { PageLike } from '@/models/entities/page-like';
+import { Pages } from '../index';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(PageLike)
+export class PageLikeRepository extends Repository<PageLike> {
+	public async pack(
+		src: PageLike['id'] | PageLike,
+		me?: { id: User['id'] } | null | undefined
+	) {
+		const like = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: like.id,
+			page: await Pages.pack(like.page || like.pageId, me),
+		};
+	}
+
+	public packMany(
+		likes: any[],
+		me: { id: User['id'] }
+	) {
+		return Promise.all(likes.map(x => this.pack(x, me)));
+	}
+}
diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts
new file mode 100644
index 0000000000..3a3642d7ec
--- /dev/null
+++ b/packages/backend/src/models/repositories/page.ts
@@ -0,0 +1,142 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Page } from '@/models/entities/page';
+import { Packed } from '@/misc/schema';
+import { Users, DriveFiles, PageLikes } from '../index';
+import { awaitAll } from '@/prelude/await-all';
+import { DriveFile } from '@/models/entities/drive-file';
+import { User } from '@/models/entities/user';
+
+@EntityRepository(Page)
+export class PageRepository extends Repository<Page> {
+	public async pack(
+		src: Page['id'] | Page,
+		me?: { id: User['id'] } | null | undefined,
+	): Promise<Packed<'Page'>> {
+		const meId = me ? me.id : null;
+		const page = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		const attachedFiles: Promise<DriveFile | undefined>[] = [];
+		const collectFile = (xs: any[]) => {
+			for (const x of xs) {
+				if (x.type === 'image') {
+					attachedFiles.push(DriveFiles.findOne({
+						id: x.fileId,
+						userId: page.userId
+					}));
+				}
+				if (x.children) {
+					collectFile(x.children);
+				}
+			}
+		};
+		collectFile(page.content);
+
+		// 後方互換性のため
+		let migrated = false;
+		const migrate = (xs: any[]) => {
+			for (const x of xs) {
+				if (x.type === 'input') {
+					if (x.inputType === 'text') {
+						x.type = 'textInput';
+					}
+					if (x.inputType === 'number') {
+						x.type = 'numberInput';
+						if (x.default) x.default = parseInt(x.default, 10);
+					}
+					migrated = true;
+				}
+				if (x.children) {
+					migrate(x.children);
+				}
+			}
+		};
+		migrate(page.content);
+		if (migrated) {
+			this.update(page.id, {
+				content: page.content
+			});
+		}
+
+		return await awaitAll({
+			id: page.id,
+			createdAt: page.createdAt.toISOString(),
+			updatedAt: page.updatedAt.toISOString(),
+			userId: page.userId,
+			user: Users.pack(page.user || page.userId, me), // { detail: true } すると無限ループするので注意
+			content: page.content,
+			variables: page.variables,
+			title: page.title,
+			name: page.name,
+			summary: page.summary,
+			hideTitleWhenPinned: page.hideTitleWhenPinned,
+			alignCenter: page.alignCenter,
+			font: page.font,
+			script: page.script,
+			eyeCatchingImageId: page.eyeCatchingImageId,
+			eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
+			attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
+			likedCount: page.likedCount,
+			isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
+		});
+	}
+
+	public packMany(
+		pages: Page[],
+		me?: { id: User['id'] } | null | undefined,
+	) {
+		return Promise.all(pages.map(x => this.pack(x, me)));
+	}
+}
+
+export const packedPageSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		updatedAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		title: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		summary: {
+			type: 'string' as const,
+			optional: false as const, nullable: true as const,
+		},
+		content: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+		},
+		variables: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+		},
+		userId: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+		},
+		user: {
+			type: 'object' as const,
+			ref: 'User' as const,
+			optional: false as const, nullable: false as const,
+		},
+	}
+};
diff --git a/packages/backend/src/models/repositories/queue.ts b/packages/backend/src/models/repositories/queue.ts
new file mode 100644
index 0000000000..161751ddc8
--- /dev/null
+++ b/packages/backend/src/models/repositories/queue.ts
@@ -0,0 +1,30 @@
+export const packedQueueCountSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		waiting: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		active: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		completed: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		failed: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		delayed: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		},
+		paused: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const
+		}
+	}
+};
diff --git a/packages/backend/src/models/repositories/relay.ts b/packages/backend/src/models/repositories/relay.ts
new file mode 100644
index 0000000000..72ead899f1
--- /dev/null
+++ b/packages/backend/src/models/repositories/relay.ts
@@ -0,0 +1,6 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Relay } from '@/models/entities/relay';
+
+@EntityRepository(Relay)
+export class RelayRepository extends Repository<Relay> {
+}
diff --git a/packages/backend/src/models/repositories/signin.ts b/packages/backend/src/models/repositories/signin.ts
new file mode 100644
index 0000000000..f375f9b5c0
--- /dev/null
+++ b/packages/backend/src/models/repositories/signin.ts
@@ -0,0 +1,11 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Signin } from '@/models/entities/signin';
+
+@EntityRepository(Signin)
+export class SigninRepository extends Repository<Signin> {
+	public async pack(
+		src: Signin,
+	) {
+		return src;
+	}
+}
diff --git a/packages/backend/src/models/repositories/user-group-invitation.ts b/packages/backend/src/models/repositories/user-group-invitation.ts
new file mode 100644
index 0000000000..638603d6ea
--- /dev/null
+++ b/packages/backend/src/models/repositories/user-group-invitation.ts
@@ -0,0 +1,23 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { UserGroupInvitation } from '@/models/entities/user-group-invitation';
+import { UserGroups } from '../index';
+
+@EntityRepository(UserGroupInvitation)
+export class UserGroupInvitationRepository extends Repository<UserGroupInvitation> {
+	public async pack(
+		src: UserGroupInvitation['id'] | UserGroupInvitation,
+	) {
+		const invitation = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		return {
+			id: invitation.id,
+			group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId),
+		};
+	}
+
+	public packMany(
+		invitations: any[],
+	) {
+		return Promise.all(invitations.map(x => this.pack(x)));
+	}
+}
diff --git a/packages/backend/src/models/repositories/user-group.ts b/packages/backend/src/models/repositories/user-group.ts
new file mode 100644
index 0000000000..b38a2fb50d
--- /dev/null
+++ b/packages/backend/src/models/repositories/user-group.ts
@@ -0,0 +1,61 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { UserGroup } from '@/models/entities/user-group';
+import { UserGroupJoinings } from '../index';
+import { Packed } from '@/misc/schema';
+
+@EntityRepository(UserGroup)
+export class UserGroupRepository extends Repository<UserGroup> {
+	public async pack(
+		src: UserGroup['id'] | UserGroup,
+	): Promise<Packed<'UserGroup'>> {
+		const userGroup = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		const users = await UserGroupJoinings.find({
+			userGroupId: userGroup.id
+		});
+
+		return {
+			id: userGroup.id,
+			createdAt: userGroup.createdAt.toISOString(),
+			name: userGroup.name,
+			ownerId: userGroup.userId,
+			userIds: users.map(x => x.userId)
+		};
+	}
+}
+
+export const packedUserGroupSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		ownerId: {
+			type: 'string' as const,
+			nullable: false as const, optional: false as const,
+			format: 'id',
+		},
+		userIds: {
+			type: 'array' as const,
+			nullable: false as const, optional: true as const,
+			items: {
+				type: 'string' as const,
+				nullable: false as const, optional: false as const,
+				format: 'id',
+			}
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/user-list.ts b/packages/backend/src/models/repositories/user-list.ts
new file mode 100644
index 0000000000..331c278e6f
--- /dev/null
+++ b/packages/backend/src/models/repositories/user-list.ts
@@ -0,0 +1,55 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { UserList } from '@/models/entities/user-list';
+import { UserListJoinings } from '../index';
+import { Packed } from '@/misc/schema';
+
+@EntityRepository(UserList)
+export class UserListRepository extends Repository<UserList> {
+	public async pack(
+		src: UserList['id'] | UserList,
+	): Promise<Packed<'UserList'>> {
+		const userList = typeof src === 'object' ? src : await this.findOneOrFail(src);
+
+		const users = await UserListJoinings.find({
+			userListId: userList.id
+		});
+
+		return {
+			id: userList.id,
+			createdAt: userList.createdAt.toISOString(),
+			name: userList.name,
+			userIds: users.map(x => x.userId)
+		};
+	}
+}
+
+export const packedUserListSchema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		createdAt: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+			format: 'date-time',
+		},
+		name: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		},
+		userIds: {
+			type: 'array' as const,
+			nullable: false as const, optional: true as const,
+			items: {
+				type: 'string' as const,
+				nullable: false as const, optional: false as const,
+				format: 'id',
+			}
+		},
+	},
+};
diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts
new file mode 100644
index 0000000000..fc0860970c
--- /dev/null
+++ b/packages/backend/src/models/repositories/user.ts
@@ -0,0 +1,659 @@
+import $ from 'cafy';
+import { EntityRepository, Repository, In, Not } from 'typeorm';
+import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
+import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '../index';
+import config from '@/config/index';
+import { Packed } from '@/misc/schema';
+import { awaitAll } from '@/prelude/await-all';
+import { populateEmojis } from '@/misc/populate-emojis';
+import { getAntennas } from '@/misc/antenna-cache';
+import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const';
+
+@EntityRepository(User)
+export class UserRepository extends Repository<User> {
+	public async getRelation(me: User['id'], target: User['id']) {
+		const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
+			Followings.findOne({
+				followerId: me,
+				followeeId: target
+			}),
+			Followings.findOne({
+				followerId: target,
+				followeeId: me
+			}),
+			FollowRequests.findOne({
+				followerId: me,
+				followeeId: target
+			}),
+			FollowRequests.findOne({
+				followerId: target,
+				followeeId: me
+			}),
+			Blockings.findOne({
+				blockerId: me,
+				blockeeId: target
+			}),
+			Blockings.findOne({
+				blockerId: target,
+				blockeeId: me
+			}),
+			Mutings.findOne({
+				muterId: me,
+				muteeId: target
+			})
+		]);
+
+		return {
+			id: target,
+			isFollowing: following1 != null,
+			hasPendingFollowRequestFromYou: followReq1 != null,
+			hasPendingFollowRequestToYou: followReq2 != null,
+			isFollowed: following2 != null,
+			isBlocking: toBlocking != null,
+			isBlocked: fromBlocked != null,
+			isMuted: mute != null
+		};
+	}
+
+	public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
+		const mute = await Mutings.find({
+			muterId: userId
+		});
+
+		const joinings = await UserGroupJoinings.find({ userId: userId });
+
+		const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
+			.where(`message.groupId = :groupId`, { groupId: j.userGroupId })
+			.andWhere('message.userId != :userId', { userId: userId })
+			.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
+			.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
+			.getOne().then(x => x != null)));
+
+		const [withUser, withGroups] = await Promise.all([
+			MessagingMessages.count({
+				where: {
+					recipientId: userId,
+					isRead: false,
+					...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}),
+				},
+				take: 1
+			}).then(count => count > 0),
+			groupQs
+		]);
+
+		return withUser || withGroups.some(x => x);
+	}
+
+	public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
+		const reads = await AnnouncementReads.find({
+			userId: userId
+		});
+
+		const count = await Announcements.count(reads.length > 0 ? {
+			id: Not(In(reads.map(read => read.announcementId)))
+		} : {});
+
+		return count > 0;
+	}
+
+	public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
+		const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
+
+		const unread = myAntennas.length > 0 ? await AntennaNotes.findOne({
+			antennaId: In(myAntennas.map(x => x.id)),
+			read: false
+		}) : null;
+
+		return unread != null;
+	}
+
+	public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
+		const channels = await ChannelFollowings.find({ followerId: userId });
+
+		const unread = channels.length > 0 ? await NoteUnreads.findOne({
+			userId: userId,
+			noteChannelId: In(channels.map(x => x.followeeId)),
+		}) : null;
+
+		return unread != null;
+	}
+
+	public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
+		const mute = await Mutings.find({
+			muterId: userId
+		});
+		const mutedUserIds = mute.map(m => m.muteeId);
+
+		const count = await Notifications.count({
+			where: {
+				notifieeId: userId,
+				...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
+				isRead: false
+			},
+			take: 1
+		});
+
+		return count > 0;
+	}
+
+	public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
+		const count = await FollowRequests.count({
+			followeeId: userId
+		});
+
+		return count > 0;
+	}
+
+	public getOnlineStatus(user: User): string {
+		if (user.hideOnlineStatus) return 'unknown';
+		if (user.lastActiveDate == null) return 'unknown';
+		const elapsed = Date.now() - user.lastActiveDate.getTime();
+		return (
+			elapsed < USER_ONLINE_THRESHOLD ? 'online' :
+			elapsed < USER_ACTIVE_THRESHOLD ? 'active' :
+			'offline'
+		);
+	}
+
+	public getAvatarUrl(user: User): string {
+		if (user.avatarUrl) {
+			return user.avatarUrl;
+		} else {
+			return `${config.url}/random-avatar/${user.id}`;
+		}
+	}
+
+	public async pack(
+		src: User['id'] | User,
+		me?: { id: User['id'] } | null | undefined,
+		options?: {
+			detail?: boolean,
+			includeSecrets?: boolean,
+		}
+	): Promise<Packed<'User'>> {
+		const opts = Object.assign({
+			detail: false,
+			includeSecrets: false
+		}, options);
+
+		const user = typeof src === 'object' ? src : await this.findOneOrFail(src);
+		const meId = me ? me.id : null;
+
+		const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
+		const pins = opts.detail ? await UserNotePinings.createQueryBuilder('pin')
+			.where('pin.userId = :userId', { userId: user.id })
+			.innerJoinAndSelect('pin.note', 'note')
+			.orderBy('pin.id', 'DESC')
+			.getMany() : [];
+		const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null;
+
+		const followingCount = profile == null ? null :
+			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount :
+			(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followingCount :
+			null;
+
+		const followersCount = profile == null ? null :
+			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount :
+			(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followersCount :
+			null;
+
+		const falsy = opts.detail ? false : undefined;
+
+		const packed = {
+			id: user.id,
+			name: user.name,
+			username: user.username,
+			host: user.host,
+			avatarUrl: this.getAvatarUrl(user),
+			avatarBlurhash: user.avatarBlurhash,
+			avatarColor: null, // 後方互換性のため
+			isAdmin: user.isAdmin || falsy,
+			isModerator: user.isModerator || falsy,
+			isBot: user.isBot || falsy,
+			isCat: user.isCat || falsy,
+			instance: user.host ? Instances.findOne({ host: user.host }).then(instance => instance ? {
+				name: instance.name,
+				softwareName: instance.softwareName,
+				softwareVersion: instance.softwareVersion,
+				iconUrl: instance.iconUrl,
+				faviconUrl: instance.faviconUrl,
+				themeColor: instance.themeColor,
+			} : undefined) : undefined,
+			emojis: populateEmojis(user.emojis, user.host),
+			onlineStatus: this.getOnlineStatus(user),
+
+			...(opts.detail ? {
+				url: profile!.url,
+				uri: user.uri,
+				createdAt: user.createdAt.toISOString(),
+				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
+				lastFetchedAt: user.lastFetchedAt?.toISOString(),
+				bannerUrl: user.bannerUrl,
+				bannerBlurhash: user.bannerBlurhash,
+				bannerColor: null, // 後方互換性のため
+				isLocked: user.isLocked,
+				isModerator: user.isModerator || falsy,
+				isSilenced: user.isSilenced || falsy,
+				isSuspended: user.isSuspended || falsy,
+				description: profile!.description,
+				location: profile!.location,
+				birthday: profile!.birthday,
+				lang: profile!.lang,
+				fields: profile!.fields,
+				followersCount: followersCount || 0,
+				followingCount: followingCount || 0,
+				notesCount: user.notesCount,
+				pinnedNoteIds: pins.map(pin => pin.noteId),
+				pinnedNotes: Notes.packMany(pins.map(pin => pin.note!), me, {
+					detail: true
+				}),
+				pinnedPageId: profile!.pinnedPageId,
+				pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null,
+				publicReactions: profile!.publicReactions,
+				ffVisibility: profile!.ffVisibility,
+				twoFactorEnabled: profile!.twoFactorEnabled,
+				usePasswordLessLogin: profile!.usePasswordLessLogin,
+				securityKeys: profile!.twoFactorEnabled
+					? UserSecurityKeys.count({
+						userId: user.id
+					}).then(result => result >= 1)
+					: false,
+			} : {}),
+
+			...(opts.detail && meId === user.id ? {
+				avatarId: user.avatarId,
+				bannerId: user.bannerId,
+				injectFeaturedNote: profile!.injectFeaturedNote,
+				receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
+				alwaysMarkNsfw: profile!.alwaysMarkNsfw,
+				carefulBot: profile!.carefulBot,
+				autoAcceptFollowed: profile!.autoAcceptFollowed,
+				noCrawle: profile!.noCrawle,
+				isExplorable: user.isExplorable,
+				isDeleted: user.isDeleted,
+				hideOnlineStatus: user.hideOnlineStatus,
+				hasUnreadSpecifiedNotes: NoteUnreads.count({
+					where: { userId: user.id, isSpecified: true },
+					take: 1
+				}).then(count => count > 0),
+				hasUnreadMentions: NoteUnreads.count({
+					where: { userId: user.id, isMentioned: true },
+					take: 1
+				}).then(count => count > 0),
+				hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
+				hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
+				hasUnreadChannel: this.getHasUnreadChannel(user.id),
+				hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
+				hasUnreadNotification: this.getHasUnreadNotification(user.id),
+				hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
+				integrations: profile!.integrations,
+				mutedWords: profile!.mutedWords,
+				mutingNotificationTypes: profile!.mutingNotificationTypes,
+				emailNotificationTypes: profile!.emailNotificationTypes,
+			} : {}),
+
+			...(opts.includeSecrets ? {
+				email: profile!.email,
+				emailVerified: profile!.emailVerified,
+				securityKeysList: profile!.twoFactorEnabled
+					? UserSecurityKeys.find({
+						where: {
+							userId: user.id
+						},
+						select: ['id', 'name', 'lastUsed']
+					})
+					: []
+			} : {}),
+
+			...(relation ? {
+				isFollowing: relation.isFollowing,
+				isFollowed: relation.isFollowed,
+				hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
+				hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
+				isBlocking: relation.isBlocking,
+				isBlocked: relation.isBlocked,
+				isMuted: relation.isMuted,
+			} : {})
+		};
+
+		return await awaitAll(packed);
+	}
+
+	public packMany(
+		users: (User['id'] | User)[],
+		me?: { id: User['id'] } | null | undefined,
+		options?: {
+			detail?: boolean,
+			includeSecrets?: boolean,
+		}
+	) {
+		return Promise.all(users.map(u => this.pack(u, me, options)));
+	}
+
+	public isLocalUser(user: User): user is ILocalUser;
+	public isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
+	public isLocalUser(user: User | { host: User['host'] }): boolean {
+		return user.host == null;
+	}
+
+	public isRemoteUser(user: User): user is IRemoteUser;
+	public isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
+	public isRemoteUser(user: User | { host: User['host'] }): boolean {
+		return !this.isLocalUser(user);
+	}
+
+	//#region Validators
+	public validateLocalUsername = $.str.match(/^\w{1,20}$/);
+	public validatePassword = $.str.min(1);
+	public validateName = $.str.min(1).max(50);
+	public validateDescription = $.str.min(1).max(500);
+	public validateLocation = $.str.min(1).max(50);
+	public validateBirthday = $.str.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/);
+	//#endregion
+}
+
+export const packedUserSchema = {
+	type: 'object' as const,
+	nullable: false as const, optional: false as const,
+	properties: {
+		id: {
+			type: 'string' as const,
+			nullable: false as const, optional: false as const,
+			format: 'id',
+			example: 'xxxxxxxxxx',
+		},
+		name: {
+			type: 'string' as const,
+			nullable: true as const, optional: false as const,
+			example: '藍'
+		},
+		username: {
+			type: 'string' as const,
+			nullable: false as const, optional: false as const,
+			example: 'ai'
+		},
+		host: {
+			type: 'string' as const,
+			nullable: true as const, optional: false as const,
+			example: 'misskey.example.com'
+		},
+		avatarUrl: {
+			type: 'string' as const,
+			format: 'url',
+			nullable: true as const, optional: false as const,
+		},
+		avatarBlurhash: {
+			type: 'any' as const,
+			nullable: true as const, optional: false as const,
+		},
+		avatarColor: {
+			type: 'any' as const,
+			nullable: true as const, optional: false as const,
+			default: null
+		},
+		isAdmin: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+			default: false
+		},
+		isModerator: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+			default: false
+		},
+		isBot: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		isCat: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		emojis: {
+			type: 'array' as const,
+			nullable: false as const, optional: false as const,
+			items: {
+				type: 'object' as const,
+				nullable: false as const, optional: false as const,
+				properties: {
+					name: {
+						type: 'string' as const,
+						nullable: false as const, optional: false as const
+					},
+					url: {
+						type: 'string' as const,
+						nullable: false as const, optional: false as const,
+						format: 'url'
+					},
+				}
+			}
+		},
+		url: {
+			type: 'string' as const,
+			format: 'url',
+			nullable: true as const, optional: true as const,
+		},
+		createdAt: {
+			type: 'string' as const,
+			nullable: false as const, optional: true as const,
+			format: 'date-time',
+		},
+		updatedAt: {
+			type: 'string' as const,
+			nullable: true as const, optional: true as const,
+			format: 'date-time',
+		},
+		bannerUrl: {
+			type: 'string' as const,
+			format: 'url',
+			nullable: true as const, optional: true as const,
+		},
+		bannerBlurhash: {
+			type: 'any' as const,
+			nullable: true as const, optional: true as const,
+		},
+		bannerColor: {
+			type: 'any' as const,
+			nullable: true as const, optional: true as const,
+			default: null
+		},
+		isLocked: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		isSuspended: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+			example: false
+		},
+		description: {
+			type: 'string' as const,
+			nullable: true as const, optional: true as const,
+			example: 'Hi masters, I am Ai!'
+		},
+		location: {
+			type: 'string' as const,
+			nullable: true as const, optional: true as const,
+		},
+		birthday: {
+			type: 'string' as const,
+			nullable: true as const, optional: true as const,
+			example: '2018-03-12'
+		},
+		fields: {
+			type: 'array' as const,
+			nullable: false as const, optional: true as const,
+			items: {
+				type: 'object' as const,
+				nullable: false as const, optional: false as const,
+				properties: {
+					name: {
+						type: 'string' as const,
+						nullable: false as const, optional: false as const
+					},
+					value: {
+						type: 'string' as const,
+						nullable: false as const, optional: false as const
+					}
+				},
+				maxLength: 4
+			}
+		},
+		followersCount: {
+			type: 'number' as const,
+			nullable: false as const, optional: true as const,
+		},
+		followingCount: {
+			type: 'number' as const,
+			nullable: false as const, optional: true as const,
+		},
+		notesCount: {
+			type: 'number' as const,
+			nullable: false as const, optional: true as const,
+		},
+		pinnedNoteIds: {
+			type: 'array' as const,
+			nullable: false as const, optional: true as const,
+			items: {
+				type: 'string' as const,
+				nullable: false as const, optional: false as const,
+				format: 'id',
+			}
+		},
+		pinnedNotes: {
+			type: 'array' as const,
+			nullable: false as const, optional: true as const,
+			items: {
+				type: 'object' as const,
+				nullable: false as const, optional: false as const,
+				ref: 'Note' as const,
+			}
+		},
+		pinnedPageId: {
+			type: 'string' as const,
+			nullable: true as const, optional: true as const
+		},
+		pinnedPage: {
+			type: 'object' as const,
+			nullable: true as const, optional: true as const,
+			ref: 'Page' as const,
+		},
+		twoFactorEnabled: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+			default: false
+		},
+		usePasswordLessLogin: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+			default: false
+		},
+		securityKeys: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+			default: false
+		},
+		avatarId: {
+			type: 'string' as const,
+			nullable: true as const, optional: true as const,
+			format: 'id'
+		},
+		bannerId: {
+			type: 'string' as const,
+			nullable: true as const, optional: true as const,
+			format: 'id'
+		},
+		autoWatch: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const
+		},
+		injectFeaturedNote: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const
+		},
+		alwaysMarkNsfw: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const
+		},
+		carefulBot: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const
+		},
+		autoAcceptFollowed: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const
+		},
+		hasUnreadSpecifiedNotes: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		hasUnreadMentions: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		hasUnreadAnnouncement: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		hasUnreadAntenna: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		hasUnreadChannel: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		hasUnreadMessagingMessage: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		hasUnreadNotification: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		hasPendingReceivedFollowRequest: {
+			type: 'boolean' as const,
+			nullable: false as const, optional: true as const,
+		},
+		integrations: {
+			type: 'object' as const,
+			nullable: false as const, optional: true as const
+		},
+		mutedWords: {
+			type: 'array' as const,
+			nullable: false as const, optional: true as const
+		},
+		mutingNotificationTypes: {
+			type: 'array' as const,
+			nullable: false as const, optional: true as const
+		},
+		isFollowing: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const
+		},
+		hasPendingFollowRequestFromYou: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const
+		},
+		hasPendingFollowRequestToYou: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const
+		},
+		isFollowed: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const
+		},
+		isBlocking: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const
+		},
+		isBlocked: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const
+		},
+		isMuted: {
+			type: 'boolean' as const,
+			optional: true as const, nullable: false as const
+		}
+	},
+};
diff --git a/packages/backend/src/prelude/README.md b/packages/backend/src/prelude/README.md
new file mode 100644
index 0000000000..bb728cfb1b
--- /dev/null
+++ b/packages/backend/src/prelude/README.md
@@ -0,0 +1,3 @@
+# Prelude
+このディレクトリのコードはJavaScriptの表現能力を補うためのコードです。
+Misskey固有の処理とは独立したコードの集まりですが、Misskeyのコードを読みやすくすることを目的としています。
diff --git a/packages/backend/src/prelude/array.ts b/packages/backend/src/prelude/array.ts
new file mode 100644
index 0000000000..d63f0475d0
--- /dev/null
+++ b/packages/backend/src/prelude/array.ts
@@ -0,0 +1,138 @@
+import { EndoRelation, Predicate } from './relation';
+
+/**
+ * Count the number of elements that satisfy the predicate
+ */
+
+export function countIf<T>(f: Predicate<T>, xs: T[]): number {
+	return xs.filter(f).length;
+}
+
+/**
+ * Count the number of elements that is equal to the element
+ */
+export function count<T>(a: T, xs: T[]): number {
+	return countIf(x => x === a, xs);
+}
+
+/**
+ * Concatenate an array of arrays
+ */
+export function concat<T>(xss: T[][]): T[] {
+	return ([] as T[]).concat(...xss);
+}
+
+/**
+ * Intersperse the element between the elements of the array
+ * @param sep The element to be interspersed
+ */
+export function intersperse<T>(sep: T, xs: T[]): T[] {
+	return concat(xs.map(x => [sep, x])).slice(1);
+}
+
+/**
+ * Returns the array of elements that is not equal to the element
+ */
+export function erase<T>(a: T, xs: T[]): T[] {
+	return xs.filter(x => x !== a);
+}
+
+/**
+ * Finds the array of all elements in the first array not contained in the second array.
+ * The order of result values are determined by the first array.
+ */
+export function difference<T>(xs: T[], ys: T[]): T[] {
+	return xs.filter(x => !ys.includes(x));
+}
+
+/**
+ * Remove all but the first element from every group of equivalent elements
+ */
+export function unique<T>(xs: T[]): T[] {
+	return [...new Set(xs)];
+}
+
+export function sum(xs: number[]): number {
+	return xs.reduce((a, b) => a + b, 0);
+}
+
+export function maximum(xs: number[]): number {
+	return Math.max(...xs);
+}
+
+/**
+ * Splits an array based on the equivalence relation.
+ * The concatenation of the result is equal to the argument.
+ */
+export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
+	const groups = [] as T[][];
+	for (const x of xs) {
+		if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
+			groups[groups.length - 1].push(x);
+		} else {
+			groups.push([x]);
+		}
+	}
+	return groups;
+}
+
+/**
+ * Splits an array based on the equivalence relation induced by the function.
+ * The concatenation of the result is equal to the argument.
+ */
+export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
+	return groupBy((a, b) => f(a) === f(b), xs);
+}
+
+export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
+	return collections.reduce((obj: Record<string, T[]>, item: T) => {
+		const key = keySelector(item);
+		if (!obj.hasOwnProperty(key)) {
+			obj[key] = [];
+		}
+
+		obj[key].push(item);
+
+		return obj;
+	}, {});
+}
+
+/**
+ * Compare two arrays by lexicographical order
+ */
+export function lessThan(xs: number[], ys: number[]): boolean {
+	for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
+		if (xs[i] < ys[i]) return true;
+		if (xs[i] > ys[i]) return false;
+	}
+	return xs.length < ys.length;
+}
+
+/**
+ * Returns the longest prefix of elements that satisfy the predicate
+ */
+export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
+	const ys = [];
+	for (const x of xs) {
+		if (f(x)) {
+			ys.push(x);
+		} else {
+			break;
+		}
+	}
+	return ys;
+}
+
+export function cumulativeSum(xs: number[]): number[] {
+	const ys = Array.from(xs); // deep copy
+	for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
+	return ys;
+}
+
+export function toArray<T>(x: T | T[] | undefined): T[] {
+	return Array.isArray(x) ? x : x != null ? [x] : [];
+}
+
+export function toSingle<T>(x: T | T[] | undefined): T | undefined {
+	return Array.isArray(x) ? x[0] : x;
+}
diff --git a/packages/backend/src/prelude/await-all.ts b/packages/backend/src/prelude/await-all.ts
new file mode 100644
index 0000000000..24795f3ae5
--- /dev/null
+++ b/packages/backend/src/prelude/await-all.ts
@@ -0,0 +1,23 @@
+type Await<T> = T extends Promise<infer U> ? U : T;
+
+type AwaitAll<T> = {
+	[P in keyof T]: Await<T[P]>;
+};
+
+export async function awaitAll<T>(obj: T): Promise<AwaitAll<T>> {
+	const target = {} as any;
+	const keys = Object.keys(obj);
+	const values = Object.values(obj);
+
+	const resolvedValues = await Promise.all(values.map(value =>
+		(!value || !value.constructor || value.constructor.name !== 'Object')
+			? value
+			: awaitAll(value)
+	));
+
+	for (let i = 0; i < keys.length; i++) {
+		target[keys[i]] = resolvedValues[i];
+	}
+
+	return target;
+}
diff --git a/packages/backend/src/prelude/math.ts b/packages/backend/src/prelude/math.ts
new file mode 100644
index 0000000000..07b94bec30
--- /dev/null
+++ b/packages/backend/src/prelude/math.ts
@@ -0,0 +1,3 @@
+export function gcd(a: number, b: number): number {
+	return b === 0 ? a : gcd(b, a % b);
+}
diff --git a/packages/backend/src/prelude/maybe.ts b/packages/backend/src/prelude/maybe.ts
new file mode 100644
index 0000000000..0b4b543ca5
--- /dev/null
+++ b/packages/backend/src/prelude/maybe.ts
@@ -0,0 +1,20 @@
+export interface IMaybe<T> {
+	isJust(): this is IJust<T>;
+}
+
+export interface IJust<T> extends IMaybe<T> {
+	get(): T;
+}
+
+export function just<T>(value: T): IJust<T> {
+	return {
+		isJust: () => true,
+		get: () => value
+	};
+}
+
+export function nothing<T>(): IMaybe<T> {
+	return {
+		isJust: () => false,
+	};
+}
diff --git a/packages/backend/src/prelude/relation.ts b/packages/backend/src/prelude/relation.ts
new file mode 100644
index 0000000000..1f4703f52f
--- /dev/null
+++ b/packages/backend/src/prelude/relation.ts
@@ -0,0 +1,5 @@
+export type Predicate<T> = (a: T) => boolean;
+
+export type Relation<T, U> = (a: T, b: U) => boolean;
+
+export type EndoRelation<T> = Relation<T, T>;
diff --git a/packages/backend/src/prelude/string.ts b/packages/backend/src/prelude/string.ts
new file mode 100644
index 0000000000..b907e0a2e1
--- /dev/null
+++ b/packages/backend/src/prelude/string.ts
@@ -0,0 +1,15 @@
+export function concat(xs: string[]): string {
+	return xs.join('');
+}
+
+export function capitalize(s: string): string {
+	return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
+}
+
+export function toUpperCase(s: string): string {
+	return s.toUpperCase();
+}
+
+export function toLowerCase(s: string): string {
+	return s.toLowerCase();
+}
diff --git a/packages/backend/src/prelude/symbol.ts b/packages/backend/src/prelude/symbol.ts
new file mode 100644
index 0000000000..51e12f7450
--- /dev/null
+++ b/packages/backend/src/prelude/symbol.ts
@@ -0,0 +1 @@
+export const fallback = Symbol('fallback');
diff --git a/packages/backend/src/prelude/time.ts b/packages/backend/src/prelude/time.ts
new file mode 100644
index 0000000000..34e8b6b17c
--- /dev/null
+++ b/packages/backend/src/prelude/time.ts
@@ -0,0 +1,39 @@
+const dateTimeIntervals = {
+	'day': 86400000,
+	'hour': 3600000,
+	'ms': 1,
+};
+
+export function dateUTC(time: number[]): Date {
+	const d = time.length === 2 ? Date.UTC(time[0], time[1])
+					: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
+					: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
+					: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
+					: time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
+					: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
+					: null;
+
+	if (!d) throw 'wrong number of arguments';
+
+	return new Date(d);
+}
+
+export function isTimeSame(a: Date, b: Date): boolean {
+	return a.getTime() === b.getTime();
+}
+
+export function isTimeBefore(a: Date, b: Date): boolean {
+	return (a.getTime() - b.getTime()) < 0;
+}
+
+export function isTimeAfter(a: Date, b: Date): boolean {
+	return (a.getTime() - b.getTime()) > 0;
+}
+
+export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
+	return new Date(x.getTime() + (value * dateTimeIntervals[span]));
+}
+
+export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
+	return new Date(x.getTime() - (value * dateTimeIntervals[span]));
+}
diff --git a/packages/backend/src/prelude/url.ts b/packages/backend/src/prelude/url.ts
new file mode 100644
index 0000000000..c7f2b7c1e7
--- /dev/null
+++ b/packages/backend/src/prelude/url.ts
@@ -0,0 +1,13 @@
+export function query(obj: {}): string {
+	const params = Object.entries(obj)
+		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
+		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
+
+	return Object.entries(params)
+		.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
+		.join('&');
+}
+
+export function appendQuery(url: string, query: string): string {
+	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
+}
diff --git a/packages/backend/src/prelude/xml.ts b/packages/backend/src/prelude/xml.ts
new file mode 100644
index 0000000000..0773f75d47
--- /dev/null
+++ b/packages/backend/src/prelude/xml.ts
@@ -0,0 +1,41 @@
+const map: Record<string, string> = {
+	'&': '&amp;',
+	'<': '&lt;',
+	'>': '&gt;',
+	'"': '&quot;',
+	'\'': '&apos;'
+};
+
+const beginingOfCDATA = '<![CDATA[';
+const endOfCDATA = ']]>';
+
+export function escapeValue(x: string): string {
+	let insideOfCDATA = false;
+	let builder = '';
+	for (
+		let i = 0;
+		i < x.length;
+	) {
+		if (insideOfCDATA) {
+			if (x.slice(i, i + beginingOfCDATA.length) === beginingOfCDATA) {
+				insideOfCDATA = true;
+				i += beginingOfCDATA.length;
+			} else {
+				builder += x[i++];
+			}
+		} else {
+			if (x.slice(i, i + endOfCDATA.length) === endOfCDATA) {
+				insideOfCDATA = false;
+				i += endOfCDATA.length;
+			} else {
+				const b = x[i++];
+				builder += map[b] || b;
+			}
+		}
+	}
+	return builder;
+}
+
+export function escapeAttribute(x: string): string {
+	return Object.entries(map).reduce((a, [k, v]) => a.replace(k, v), x);
+}
diff --git a/packages/backend/src/queue/get-job-info.ts b/packages/backend/src/queue/get-job-info.ts
new file mode 100644
index 0000000000..f601ae62d0
--- /dev/null
+++ b/packages/backend/src/queue/get-job-info.ts
@@ -0,0 +1,15 @@
+import * as Bull from 'bull';
+
+export function getJobInfo(job: Bull.Job, increment = false) {
+	const age = Date.now() - job.timestamp;
+
+	const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
+		: age > 10000 ? `${Math.floor(age / 1000)}s`
+		: `${age}ms`;
+
+	// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
+	const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
+	const maxAttempts = job.opts ? job.opts.attempts : 0;
+
+	return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
+}
diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts
new file mode 100644
index 0000000000..37eb809604
--- /dev/null
+++ b/packages/backend/src/queue/index.ts
@@ -0,0 +1,255 @@
+import * as httpSignature from 'http-signature';
+
+import config from '@/config/index';
+import { envOption } from '../env';
+
+import processDeliver from './processors/deliver';
+import processInbox from './processors/inbox';
+import processDb from './processors/db/index';
+import procesObjectStorage from './processors/object-storage/index';
+import { queueLogger } from './logger';
+import { DriveFile } from '@/models/entities/drive-file';
+import { getJobInfo } from './get-job-info';
+import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
+import { ThinUser } from './types';
+import { IActivity } from '@/remote/activitypub/type';
+
+function renderError(e: Error): any {
+	return {
+		stack: e?.stack,
+		message: e?.message,
+		name: e?.name
+	};
+}
+
+const systemLogger = queueLogger.createSubLogger('system');
+const deliverLogger = queueLogger.createSubLogger('deliver');
+const inboxLogger = queueLogger.createSubLogger('inbox');
+const dbLogger = queueLogger.createSubLogger('db');
+const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
+
+systemQueue
+	.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
+	.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
+	.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
+	.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
+	.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
+	.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
+
+deliverQueue
+	.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
+	.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
+	.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
+	.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
+	.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
+	.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
+
+inboxQueue
+	.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
+	.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
+	.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
+	.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
+	.on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
+	.on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
+
+dbQueue
+	.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
+	.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
+	.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
+	.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
+	.on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) }))
+	.on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`));
+
+objectStorageQueue
+	.on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`))
+	.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
+	.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
+	.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
+	.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
+	.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
+
+export function deliver(user: ThinUser, content: unknown, to: string | null) {
+	if (content == null) return null;
+	if (to == null) return null;
+
+	const data = {
+		user: {
+			id: user.id
+		},
+		content,
+		to
+	};
+
+	return deliverQueue.add(data, {
+		attempts: config.deliverJobMaxAttempts || 12,
+		timeout: 1 * 60 * 1000,	// 1min
+		backoff: {
+			type: 'apBackoff'
+		},
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
+	const data = {
+		activity: activity,
+		signature
+	};
+
+	return inboxQueue.add(data, {
+		attempts: config.inboxJobMaxAttempts || 8,
+		timeout: 5 * 60 * 1000,	// 5min
+		backoff: {
+			type: 'apBackoff'
+		},
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createDeleteDriveFilesJob(user: ThinUser) {
+	return dbQueue.add('deleteDriveFiles', {
+		user: user
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createExportNotesJob(user: ThinUser) {
+	return dbQueue.add('exportNotes', {
+		user: user
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createExportFollowingJob(user: ThinUser) {
+	return dbQueue.add('exportFollowing', {
+		user: user
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createExportMuteJob(user: ThinUser) {
+	return dbQueue.add('exportMute', {
+		user: user
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createExportBlockingJob(user: ThinUser) {
+	return dbQueue.add('exportBlocking', {
+		user: user
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createExportUserListsJob(user: ThinUser) {
+	return dbQueue.add('exportUserLists', {
+		user: user
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) {
+	return dbQueue.add('importFollowing', {
+		user: user,
+		fileId: fileId
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) {
+	return dbQueue.add('importMuting', {
+		user: user,
+		fileId: fileId
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) {
+	return dbQueue.add('importBlocking', {
+		user: user,
+		fileId: fileId
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
+	return dbQueue.add('importUserLists', {
+		user: user,
+		fileId: fileId
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
+	return dbQueue.add('deleteAccount', {
+		user: user,
+		soft: opts.soft
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createDeleteObjectStorageFileJob(key: string) {
+	return objectStorageQueue.add('deleteFile', {
+		key: key
+	}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export function createCleanRemoteFilesJob() {
+	return objectStorageQueue.add('cleanRemoteFiles', {}, {
+		removeOnComplete: true,
+		removeOnFail: true
+	});
+}
+
+export default function() {
+	if (envOption.onlyServer) return;
+
+	deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
+	inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
+	processDb(dbQueue);
+	procesObjectStorage(objectStorageQueue);
+
+	systemQueue.add('resyncCharts', {
+	}, {
+		repeat: { cron: '0 0 * * *' }
+	});
+}
+
+export function destroy() {
+	deliverQueue.once('cleaned', (jobs, status) => {
+		deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
+	});
+	deliverQueue.clean(0, 'delayed');
+
+	inboxQueue.once('cleaned', (jobs, status) => {
+		inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
+	});
+	inboxQueue.clean(0, 'delayed');
+}
diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts
new file mode 100644
index 0000000000..31102a3ed2
--- /dev/null
+++ b/packages/backend/src/queue/initialize.ts
@@ -0,0 +1,33 @@
+import * as Bull from 'bull';
+import config from '@/config/index';
+
+export function initialize<T>(name: string, limitPerSec = -1) {
+	return new Bull<T>(name, {
+		redis: {
+			port: config.redis.port,
+			host: config.redis.host,
+			password: config.redis.pass,
+			db: config.redis.db || 0,
+		},
+		prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
+		limiter: limitPerSec > 0 ? {
+			max: limitPerSec,
+			duration: 1000
+		} : undefined,
+		settings: {
+			backoffStrategies: {
+				apBackoff
+			}
+		}
+	});
+}
+
+// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
+function apBackoff(attemptsMade: number, err: Error) {
+	const baseDelay = 60 * 1000;	// 1min
+	const maxBackoff = 8 * 60 * 60 * 1000;	// 8hours
+	let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
+	backoff = Math.min(backoff, maxBackoff);
+	backoff += Math.round(backoff * Math.random() * 0.2);
+	return backoff;
+}
diff --git a/packages/backend/src/queue/logger.ts b/packages/backend/src/queue/logger.ts
new file mode 100644
index 0000000000..f789b9d079
--- /dev/null
+++ b/packages/backend/src/queue/logger.ts
@@ -0,0 +1,3 @@
+import Logger from '@/services/logger';
+
+export const queueLogger = new Logger('queue', 'orange');
diff --git a/packages/backend/src/queue/processors/db/delete-account.ts b/packages/backend/src/queue/processors/db/delete-account.ts
new file mode 100644
index 0000000000..e54f38e35e
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/delete-account.ts
@@ -0,0 +1,94 @@
+import * as Bull from 'bull';
+import { queueLogger } from '../../logger';
+import { DriveFiles, Notes, UserProfiles, Users } from '@/models/index';
+import { DbUserDeleteJobData } from '@/queue/types';
+import { Note } from '@/models/entities/note';
+import { DriveFile } from '@/models/entities/drive-file';
+import { MoreThan } from 'typeorm';
+import { deleteFileSync } from '@/services/drive/delete-file';
+import { sendEmail } from '@/services/send-email';
+
+const logger = queueLogger.createSubLogger('delete-account');
+
+export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
+	logger.info(`Deleting account of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		return;
+	}
+
+	{ // Delete notes
+		let cursor: Note['id'] | null = null;
+
+		while (true) {
+			const notes = await Notes.find({
+				where: {
+					userId: user.id,
+					...(cursor ? { id: MoreThan(cursor) } : {})
+				},
+				take: 100,
+				order: {
+					id: 1
+				}
+			});
+
+			if (notes.length === 0) {
+				break;
+			}
+
+			cursor = notes[notes.length - 1].id;
+
+			await Notes.delete(notes.map(note => note.id));
+		}
+
+		logger.succ(`All of notes deleted`);
+	}
+
+	{ // Delete files
+		let cursor: DriveFile['id'] | null = null;
+
+		while (true) {
+			const files = await DriveFiles.find({
+				where: {
+					userId: user.id,
+					...(cursor ? { id: MoreThan(cursor) } : {})
+				},
+				take: 10,
+				order: {
+					id: 1
+				}
+			});
+
+			if (files.length === 0) {
+				break;
+			}
+
+			cursor = files[files.length - 1].id;
+
+			for (const file of files) {
+				await deleteFileSync(file);
+			}
+		}
+
+		logger.succ(`All of files deleted`);
+	}
+
+	{ // Send email notification
+		const profile = await UserProfiles.findOneOrFail(user.id);
+		if (profile.email && profile.emailVerified) {
+			sendEmail(profile.email, 'Account deleted',
+				`Your account has been deleted.`,
+				`Your account has been deleted.`);
+		}
+	}
+
+	// soft指定されている場合は物理削除しない
+	if (job.data.soft) {
+		// nop
+	} else {
+		await Users.delete(job.data.user.id);
+	}
+
+	return 'Account deleted';
+}
diff --git a/packages/backend/src/queue/processors/db/delete-drive-files.ts b/packages/backend/src/queue/processors/db/delete-drive-files.ts
new file mode 100644
index 0000000000..8a28468b0d
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/delete-drive-files.ts
@@ -0,0 +1,56 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import { deleteFileSync } from '@/services/drive/delete-file';
+import { Users, DriveFiles } from '@/models/index';
+import { MoreThan } from 'typeorm';
+import { DbUserJobData } from '@/queue/types';
+
+const logger = queueLogger.createSubLogger('delete-drive-files');
+
+export async function deleteDriveFiles(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
+	logger.info(`Deleting drive files of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	let deletedCount = 0;
+	let cursor: any = null;
+
+	while (true) {
+		const files = await DriveFiles.find({
+			where: {
+				userId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
+			}
+		});
+
+		if (files.length === 0) {
+			job.progress(100);
+			break;
+		}
+
+		cursor = files[files.length - 1].id;
+
+		for (const file of files) {
+			await deleteFileSync(file);
+			deletedCount++;
+		}
+
+		const total = await DriveFiles.count({
+			userId: user.id,
+		});
+
+		job.progress(deletedCount / total);
+	}
+
+	logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
+	done();
+}
diff --git a/packages/backend/src/queue/processors/db/export-blocking.ts b/packages/backend/src/queue/processors/db/export-blocking.ts
new file mode 100644
index 0000000000..8b8aa259d4
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/export-blocking.ts
@@ -0,0 +1,94 @@
+import * as Bull from 'bull';
+import * as tmp from 'tmp';
+import * as fs from 'fs';
+
+import { queueLogger } from '../../logger';
+import addFile from '@/services/drive/add-file';
+import * as dateFormat from 'dateformat';
+import { getFullApAccount } from '@/misc/convert-host';
+import { Users, Blockings } from '@/models/index';
+import { MoreThan } from 'typeorm';
+import { DbUserJobData } from '@/queue/types';
+
+const logger = queueLogger.createSubLogger('export-blocking');
+
+export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
+	logger.info(`Exporting blocking of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	// Create temp file
+	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
+		tmp.file((e, path, fd, cleanup) => {
+			if (e) return rej(e);
+			res([path, cleanup]);
+		});
+	});
+
+	logger.info(`Temp file is ${path}`);
+
+	const stream = fs.createWriteStream(path, { flags: 'a' });
+
+	let exportedCount = 0;
+	let cursor: any = null;
+
+	while (true) {
+		const blockings = await Blockings.find({
+			where: {
+				blockerId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
+			}
+		});
+
+		if (blockings.length === 0) {
+			job.progress(100);
+			break;
+		}
+
+		cursor = blockings[blockings.length - 1].id;
+
+		for (const block of blockings) {
+			const u = await Users.findOne({ id: block.blockeeId });
+			if (u == null) {
+				exportedCount++; continue;
+			}
+
+			const content = getFullApAccount(u.username, u.host);
+			await new Promise<void>((res, rej) => {
+				stream.write(content + '\n', err => {
+					if (err) {
+						logger.error(err);
+						rej(err);
+					} else {
+						res();
+					}
+				});
+			});
+			exportedCount++;
+		}
+
+		const total = await Blockings.count({
+			blockerId: user.id,
+		});
+
+		job.progress(exportedCount / total);
+	}
+
+	stream.end();
+	logger.succ(`Exported to: ${path}`);
+
+	const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
+	const driveFile = await addFile(user, path, fileName, null, null, true);
+
+	logger.succ(`Exported to: ${driveFile.id}`);
+	cleanup();
+	done();
+}
diff --git a/packages/backend/src/queue/processors/db/export-following.ts b/packages/backend/src/queue/processors/db/export-following.ts
new file mode 100644
index 0000000000..a0ecf5f560
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/export-following.ts
@@ -0,0 +1,94 @@
+import * as Bull from 'bull';
+import * as tmp from 'tmp';
+import * as fs from 'fs';
+
+import { queueLogger } from '../../logger';
+import addFile from '@/services/drive/add-file';
+import * as dateFormat from 'dateformat';
+import { getFullApAccount } from '@/misc/convert-host';
+import { Users, Followings } from '@/models/index';
+import { MoreThan } from 'typeorm';
+import { DbUserJobData } from '@/queue/types';
+
+const logger = queueLogger.createSubLogger('export-following');
+
+export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
+	logger.info(`Exporting following of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	// Create temp file
+	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
+		tmp.file((e, path, fd, cleanup) => {
+			if (e) return rej(e);
+			res([path, cleanup]);
+		});
+	});
+
+	logger.info(`Temp file is ${path}`);
+
+	const stream = fs.createWriteStream(path, { flags: 'a' });
+
+	let exportedCount = 0;
+	let cursor: any = null;
+
+	while (true) {
+		const followings = await Followings.find({
+			where: {
+				followerId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
+			}
+		});
+
+		if (followings.length === 0) {
+			job.progress(100);
+			break;
+		}
+
+		cursor = followings[followings.length - 1].id;
+
+		for (const following of followings) {
+			const u = await Users.findOne({ id: following.followeeId });
+			if (u == null) {
+				exportedCount++; continue;
+			}
+
+			const content = getFullApAccount(u.username, u.host);
+			await new Promise<void>((res, rej) => {
+				stream.write(content + '\n', err => {
+					if (err) {
+						logger.error(err);
+						rej(err);
+					} else {
+						res();
+					}
+				});
+			});
+			exportedCount++;
+		}
+
+		const total = await Followings.count({
+			followerId: user.id,
+		});
+
+		job.progress(exportedCount / total);
+	}
+
+	stream.end();
+	logger.succ(`Exported to: ${path}`);
+
+	const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
+	const driveFile = await addFile(user, path, fileName, null, null, true);
+
+	logger.succ(`Exported to: ${driveFile.id}`);
+	cleanup();
+	done();
+}
diff --git a/packages/backend/src/queue/processors/db/export-mute.ts b/packages/backend/src/queue/processors/db/export-mute.ts
new file mode 100644
index 0000000000..d5976f7d56
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/export-mute.ts
@@ -0,0 +1,94 @@
+import * as Bull from 'bull';
+import * as tmp from 'tmp';
+import * as fs from 'fs';
+
+import { queueLogger } from '../../logger';
+import addFile from '@/services/drive/add-file';
+import * as dateFormat from 'dateformat';
+import { getFullApAccount } from '@/misc/convert-host';
+import { Users, Mutings } from '@/models/index';
+import { MoreThan } from 'typeorm';
+import { DbUserJobData } from '@/queue/types';
+
+const logger = queueLogger.createSubLogger('export-mute');
+
+export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
+	logger.info(`Exporting mute of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	// Create temp file
+	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
+		tmp.file((e, path, fd, cleanup) => {
+			if (e) return rej(e);
+			res([path, cleanup]);
+		});
+	});
+
+	logger.info(`Temp file is ${path}`);
+
+	const stream = fs.createWriteStream(path, { flags: 'a' });
+
+	let exportedCount = 0;
+	let cursor: any = null;
+
+	while (true) {
+		const mutes = await Mutings.find({
+			where: {
+				muterId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
+			}
+		});
+
+		if (mutes.length === 0) {
+			job.progress(100);
+			break;
+		}
+
+		cursor = mutes[mutes.length - 1].id;
+
+		for (const mute of mutes) {
+			const u = await Users.findOne({ id: mute.muteeId });
+			if (u == null) {
+				exportedCount++; continue;
+			}
+
+			const content = getFullApAccount(u.username, u.host);
+			await new Promise<void>((res, rej) => {
+				stream.write(content + '\n', err => {
+					if (err) {
+						logger.error(err);
+						rej(err);
+					} else {
+						res();
+					}
+				});
+			});
+			exportedCount++;
+		}
+
+		const total = await Mutings.count({
+			muterId: user.id,
+		});
+
+		job.progress(exportedCount / total);
+	}
+
+	stream.end();
+	logger.succ(`Exported to: ${path}`);
+
+	const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
+	const driveFile = await addFile(user, path, fileName, null, null, true);
+
+	logger.succ(`Exported to: ${driveFile.id}`);
+	cleanup();
+	done();
+}
diff --git a/packages/backend/src/queue/processors/db/export-notes.ts b/packages/backend/src/queue/processors/db/export-notes.ts
new file mode 100644
index 0000000000..49850aa706
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/export-notes.ts
@@ -0,0 +1,133 @@
+import * as Bull from 'bull';
+import * as tmp from 'tmp';
+import * as fs from 'fs';
+
+import { queueLogger } from '../../logger';
+import addFile from '@/services/drive/add-file';
+import * as dateFormat from 'dateformat';
+import { Users, Notes, Polls } from '@/models/index';
+import { MoreThan } from 'typeorm';
+import { Note } from '@/models/entities/note';
+import { Poll } from '@/models/entities/poll';
+import { DbUserJobData } from '@/queue/types';
+
+const logger = queueLogger.createSubLogger('export-notes');
+
+export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
+	logger.info(`Exporting notes of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	// Create temp file
+	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
+		tmp.file((e, path, fd, cleanup) => {
+			if (e) return rej(e);
+			res([path, cleanup]);
+		});
+	});
+
+	logger.info(`Temp file is ${path}`);
+
+	const stream = fs.createWriteStream(path, { flags: 'a' });
+
+	await new Promise<void>((res, rej) => {
+		stream.write('[', err => {
+			if (err) {
+				logger.error(err);
+				rej(err);
+			} else {
+				res();
+			}
+		});
+	});
+
+	let exportedNotesCount = 0;
+	let cursor: any = null;
+
+	while (true) {
+		const notes = await Notes.find({
+			where: {
+				userId: user.id,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 100,
+			order: {
+				id: 1
+			}
+		});
+
+		if (notes.length === 0) {
+			job.progress(100);
+			break;
+		}
+
+		cursor = notes[notes.length - 1].id;
+
+		for (const note of notes) {
+			let poll: Poll | undefined;
+			if (note.hasPoll) {
+				poll = await Polls.findOneOrFail({ noteId: note.id });
+			}
+			const content = JSON.stringify(serialize(note, poll));
+			await new Promise<void>((res, rej) => {
+				stream.write(exportedNotesCount === 0 ? content : ',\n' + content, err => {
+					if (err) {
+						logger.error(err);
+						rej(err);
+					} else {
+						res();
+					}
+				});
+			});
+			exportedNotesCount++;
+		}
+
+		const total = await Notes.count({
+			userId: user.id,
+		});
+
+		job.progress(exportedNotesCount / total);
+	}
+
+	await new Promise<void>((res, rej) => {
+		stream.write(']', err => {
+			if (err) {
+				logger.error(err);
+				rej(err);
+			} else {
+				res();
+			}
+		});
+	});
+
+	stream.end();
+	logger.succ(`Exported to: ${path}`);
+
+	const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
+	const driveFile = await addFile(user, path, fileName, null, null, true);
+
+	logger.succ(`Exported to: ${driveFile.id}`);
+	cleanup();
+	done();
+}
+
+function serialize(note: Note, poll: Poll | null = null): any {
+	return {
+		id: note.id,
+		text: note.text,
+		createdAt: note.createdAt,
+		fileIds: note.fileIds,
+		replyId: note.replyId,
+		renoteId: note.renoteId,
+		poll: poll,
+		cw: note.cw,
+		viaMobile: note.viaMobile,
+		visibility: note.visibility,
+		visibleUserIds: note.visibleUserIds,
+		localOnly: note.localOnly
+	};
+}
diff --git a/packages/backend/src/queue/processors/db/export-user-lists.ts b/packages/backend/src/queue/processors/db/export-user-lists.ts
new file mode 100644
index 0000000000..8a86c4df5d
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/export-user-lists.ts
@@ -0,0 +1,71 @@
+import * as Bull from 'bull';
+import * as tmp from 'tmp';
+import * as fs from 'fs';
+
+import { queueLogger } from '../../logger';
+import addFile from '@/services/drive/add-file';
+import * as dateFormat from 'dateformat';
+import { getFullApAccount } from '@/misc/convert-host';
+import { Users, UserLists, UserListJoinings } from '@/models/index';
+import { In } from 'typeorm';
+import { DbUserJobData } from '@/queue/types';
+
+const logger = queueLogger.createSubLogger('export-user-lists');
+
+export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
+	logger.info(`Exporting user lists of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	const lists = await UserLists.find({
+		userId: user.id
+	});
+
+	// Create temp file
+	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
+		tmp.file((e, path, fd, cleanup) => {
+			if (e) return rej(e);
+			res([path, cleanup]);
+		});
+	});
+
+	logger.info(`Temp file is ${path}`);
+
+	const stream = fs.createWriteStream(path, { flags: 'a' });
+
+	for (const list of lists) {
+		const joinings = await UserListJoinings.find({ userListId: list.id });
+		const users = await Users.find({
+			id: In(joinings.map(j => j.userId))
+		});
+
+		for (const u of users) {
+			const acct = getFullApAccount(u.username, u.host);
+			const content = `${list.name},${acct}`;
+			await new Promise<void>((res, rej) => {
+				stream.write(content + '\n', err => {
+					if (err) {
+						logger.error(err);
+						rej(err);
+					} else {
+						res();
+					}
+				});
+			});
+		}
+	}
+
+	stream.end();
+	logger.succ(`Exported to: ${path}`);
+
+	const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
+	const driveFile = await addFile(user, path, fileName, null, null, true);
+
+	logger.succ(`Exported to: ${driveFile.id}`);
+	cleanup();
+	done();
+}
diff --git a/packages/backend/src/queue/processors/db/import-blocking.ts b/packages/backend/src/queue/processors/db/import-blocking.ts
new file mode 100644
index 0000000000..2e77107034
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/import-blocking.ts
@@ -0,0 +1,74 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import * as Acct from 'misskey-js/built/acct';
+import { resolveUser } from '@/remote/resolve-user';
+import { downloadTextFile } from '@/misc/download-text-file';
+import { isSelfHost, toPuny } from '@/misc/convert-host';
+import { Users, DriveFiles, Blockings } from '@/models/index';
+import { DbUserImportJobData } from '@/queue/types';
+import block from '@/services/blocking/create';
+
+const logger = queueLogger.createSubLogger('import-blocking');
+
+export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
+	logger.info(`Importing blocking of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	const file = await DriveFiles.findOne({
+		id: job.data.fileId
+	});
+	if (file == null) {
+		done();
+		return;
+	}
+
+	const csv = await downloadTextFile(file.url);
+
+	let linenum = 0;
+
+	for (const line of csv.trim().split('\n')) {
+		linenum++;
+
+		try {
+			const acct = line.split(',')[0].trim();
+			const { username, host } = Acct.parse(acct);
+
+			let target = isSelfHost(host!) ? await Users.findOne({
+				host: null,
+				usernameLower: username.toLowerCase()
+			}) : await Users.findOne({
+				host: toPuny(host!),
+				usernameLower: username.toLowerCase()
+			});
+
+			if (host == null && target == null) continue;
+
+			if (target == null) {
+				target = await resolveUser(username, host);
+			}
+
+			if (target == null) {
+				throw `cannot resolve user: @${username}@${host}`;
+			}
+
+			// skip myself
+			if (target.id === job.data.user.id) continue;
+
+			logger.info(`Block[${linenum}] ${target.id} ...`);
+
+			await block(user, target);
+		} catch (e) {
+			logger.warn(`Error in line:${linenum} ${e}`);
+		}
+	}
+
+	logger.succ('Imported');
+	done();
+}
+
diff --git a/packages/backend/src/queue/processors/db/import-following.ts b/packages/backend/src/queue/processors/db/import-following.ts
new file mode 100644
index 0000000000..2bd079e4bc
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/import-following.ts
@@ -0,0 +1,73 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import follow from '@/services/following/create';
+import * as Acct from 'misskey-js/built/acct';
+import { resolveUser } from '@/remote/resolve-user';
+import { downloadTextFile } from '@/misc/download-text-file';
+import { isSelfHost, toPuny } from '@/misc/convert-host';
+import { Users, DriveFiles } from '@/models/index';
+import { DbUserImportJobData } from '@/queue/types';
+
+const logger = queueLogger.createSubLogger('import-following');
+
+export async function importFollowing(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
+	logger.info(`Importing following of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	const file = await DriveFiles.findOne({
+		id: job.data.fileId
+	});
+	if (file == null) {
+		done();
+		return;
+	}
+
+	const csv = await downloadTextFile(file.url);
+
+	let linenum = 0;
+
+	for (const line of csv.trim().split('\n')) {
+		linenum++;
+
+		try {
+			const acct = line.split(',')[0].trim();
+			const { username, host } = Acct.parse(acct);
+
+			let target = isSelfHost(host!) ? await Users.findOne({
+				host: null,
+				usernameLower: username.toLowerCase()
+			}) : await Users.findOne({
+				host: toPuny(host!),
+				usernameLower: username.toLowerCase()
+			});
+
+			if (host == null && target == null) continue;
+
+			if (target == null) {
+				target = await resolveUser(username, host);
+			}
+
+			if (target == null) {
+				throw `cannot resolve user: @${username}@${host}`;
+			}
+
+			// skip myself
+			if (target.id === job.data.user.id) continue;
+
+			logger.info(`Follow[${linenum}] ${target.id} ...`);
+
+			follow(user, target);
+		} catch (e) {
+			logger.warn(`Error in line:${linenum} ${e}`);
+		}
+	}
+
+	logger.succ('Imported');
+	done();
+}
diff --git a/packages/backend/src/queue/processors/db/import-muting.ts b/packages/backend/src/queue/processors/db/import-muting.ts
new file mode 100644
index 0000000000..8060980625
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/import-muting.ts
@@ -0,0 +1,83 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import * as Acct from 'misskey-js/built/acct';
+import { resolveUser } from '@/remote/resolve-user';
+import { downloadTextFile } from '@/misc/download-text-file';
+import { isSelfHost, toPuny } from '@/misc/convert-host';
+import { Users, DriveFiles, Mutings } from '@/models/index';
+import { DbUserImportJobData } from '@/queue/types';
+import { User } from '@/models/entities/user';
+import { genId } from '@/misc/gen-id';
+
+const logger = queueLogger.createSubLogger('import-muting');
+
+export async function importMuting(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
+	logger.info(`Importing muting of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	const file = await DriveFiles.findOne({
+		id: job.data.fileId
+	});
+	if (file == null) {
+		done();
+		return;
+	}
+
+	const csv = await downloadTextFile(file.url);
+
+	let linenum = 0;
+
+	for (const line of csv.trim().split('\n')) {
+		linenum++;
+
+		try {
+			const acct = line.split(',')[0].trim();
+			const { username, host } = Acct.parse(acct);
+
+			let target = isSelfHost(host!) ? await Users.findOne({
+				host: null,
+				usernameLower: username.toLowerCase()
+			}) : await Users.findOne({
+				host: toPuny(host!),
+				usernameLower: username.toLowerCase()
+			});
+
+			if (host == null && target == null) continue;
+
+			if (target == null) {
+				target = await resolveUser(username, host);
+			}
+
+			if (target == null) {
+				throw `cannot resolve user: @${username}@${host}`;
+			}
+
+			// skip myself
+			if (target.id === job.data.user.id) continue;
+
+			logger.info(`Mute[${linenum}] ${target.id} ...`);
+
+			await mute(user, target);
+		} catch (e) {
+			logger.warn(`Error in line:${linenum} ${e}`);
+		}
+	}
+
+	logger.succ('Imported');
+	done();
+}
+
+async function mute(user: User, target: User) {
+	await Mutings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		muterId: user.id,
+		muteeId: target.id,
+	});
+}
diff --git a/packages/backend/src/queue/processors/db/import-user-lists.ts b/packages/backend/src/queue/processors/db/import-user-lists.ts
new file mode 100644
index 0000000000..46b728b387
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/import-user-lists.ts
@@ -0,0 +1,80 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import * as Acct from 'misskey-js/built/acct';
+import { resolveUser } from '@/remote/resolve-user';
+import { pushUserToUserList } from '@/services/user-list/push';
+import { downloadTextFile } from '@/misc/download-text-file';
+import { isSelfHost, toPuny } from '@/misc/convert-host';
+import { DriveFiles, Users, UserLists, UserListJoinings } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { DbUserImportJobData } from '@/queue/types';
+
+const logger = queueLogger.createSubLogger('import-user-lists');
+
+export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
+	logger.info(`Importing user lists of ${job.data.user.id} ...`);
+
+	const user = await Users.findOne(job.data.user.id);
+	if (user == null) {
+		done();
+		return;
+	}
+
+	const file = await DriveFiles.findOne({
+		id: job.data.fileId
+	});
+	if (file == null) {
+		done();
+		return;
+	}
+
+	const csv = await downloadTextFile(file.url);
+
+	let linenum = 0;
+
+	for (const line of csv.trim().split('\n')) {
+		linenum++;
+
+		try {
+			const listName = line.split(',')[0].trim();
+			const { username, host } = Acct.parse(line.split(',')[1].trim());
+
+			let list = await UserLists.findOne({
+				userId: user.id,
+				name: listName
+			});
+
+			if (list == null) {
+				list = await UserLists.save({
+					id: genId(),
+					createdAt: new Date(),
+					userId: user.id,
+					name: listName,
+					userIds: []
+				});
+			}
+
+			let target = isSelfHost(host!) ? await Users.findOne({
+				host: null,
+				usernameLower: username.toLowerCase()
+			}) : await Users.findOne({
+				host: toPuny(host!),
+				usernameLower: username.toLowerCase()
+			});
+
+			if (target == null) {
+				target = await resolveUser(username, host);
+			}
+
+			if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
+
+			pushUserToUserList(target, list);
+		} catch (e) {
+			logger.warn(`Error in line:${linenum} ${e}`);
+		}
+	}
+
+	logger.succ('Imported');
+	done();
+}
diff --git a/packages/backend/src/queue/processors/db/index.ts b/packages/backend/src/queue/processors/db/index.ts
new file mode 100644
index 0000000000..97087642b7
--- /dev/null
+++ b/packages/backend/src/queue/processors/db/index.ts
@@ -0,0 +1,33 @@
+import * as Bull from 'bull';
+import { DbJobData } from '@/queue/types';
+import { deleteDriveFiles } from './delete-drive-files';
+import { exportNotes } from './export-notes';
+import { exportFollowing } from './export-following';
+import { exportMute } from './export-mute';
+import { exportBlocking } from './export-blocking';
+import { exportUserLists } from './export-user-lists';
+import { importFollowing } from './import-following';
+import { importUserLists } from './import-user-lists';
+import { deleteAccount } from './delete-account';
+import { importMuting } from './import-muting';
+import { importBlocking } from './import-blocking';
+
+const jobs = {
+	deleteDriveFiles,
+	exportNotes,
+	exportFollowing,
+	exportMute,
+	exportBlocking,
+	exportUserLists,
+	importFollowing,
+	importMuting,
+	importBlocking,
+	importUserLists,
+	deleteAccount,
+} as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>;
+
+export default function(dbQueue: Bull.Queue<DbJobData>) {
+	for (const [k, v] of Object.entries(jobs)) {
+		dbQueue.process(k, v);
+	}
+}
diff --git a/packages/backend/src/queue/processors/deliver.ts b/packages/backend/src/queue/processors/deliver.ts
new file mode 100644
index 0000000000..3c61896a2f
--- /dev/null
+++ b/packages/backend/src/queue/processors/deliver.ts
@@ -0,0 +1,94 @@
+import { URL } from 'url';
+import * as Bull from 'bull';
+import request from '@/remote/activitypub/request';
+import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc';
+import Logger from '@/services/logger';
+import { Instances } from '@/models/index';
+import { instanceChart } from '@/services/chart/index';
+import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { toPuny } from '@/misc/convert-host';
+import { Cache } from '@/misc/cache';
+import { Instance } from '@/models/entities/instance';
+import { DeliverJobData } from '../types';
+import { StatusError } from '@/misc/fetch';
+
+const logger = new Logger('deliver');
+
+let latest: string | null = null;
+
+const suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
+
+export default async (job: Bull.Job<DeliverJobData>) => {
+	const { host } = new URL(job.data.to);
+
+	// ブロックしてたら中断
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(toPuny(host))) {
+		return 'skip (blocked)';
+	}
+
+	// isSuspendedなら中断
+	let suspendedHosts = suspendedHostsCache.get(null);
+	if (suspendedHosts == null) {
+		suspendedHosts = await Instances.find({
+			where: {
+				isSuspended: true
+			},
+		});
+		suspendedHostsCache.set(null, suspendedHosts);
+	}
+	if (suspendedHosts.map(x => x.host).includes(toPuny(host))) {
+		return 'skip (suspended)';
+	}
+
+	try {
+		if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
+			logger.debug(`delivering ${latest}`);
+		}
+
+		await request(job.data.user, job.data.to, job.data.content);
+
+		// Update stats
+		registerOrFetchInstanceDoc(host).then(i => {
+			Instances.update(i.id, {
+				latestRequestSentAt: new Date(),
+				latestStatus: 200,
+				lastCommunicatedAt: new Date(),
+				isNotResponding: false
+			});
+
+			fetchInstanceMetadata(i);
+
+			instanceChart.requestSent(i.host, true);
+		});
+
+		return 'Success';
+	} catch (res) {
+		// Update stats
+		registerOrFetchInstanceDoc(host).then(i => {
+			Instances.update(i.id, {
+				latestRequestSentAt: new Date(),
+				latestStatus: res instanceof StatusError ? res.statusCode : null,
+				isNotResponding: true
+			});
+
+			instanceChart.requestSent(i.host, false);
+		});
+
+		if (res instanceof StatusError) {
+			// 4xx
+			if (res.isClientError) {
+				// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
+				// 何回再送しても成功することはないということなのでエラーにはしないでおく
+				return `${res.statusCode} ${res.statusMessage}`;
+			}
+
+			// 5xx etc.
+			throw `${res.statusCode} ${res.statusMessage}`;
+		} else {
+			// DNS error, socket error, timeout ...
+			throw res;
+		}
+	}
+};
diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts
new file mode 100644
index 0000000000..4032ce8653
--- /dev/null
+++ b/packages/backend/src/queue/processors/inbox.ts
@@ -0,0 +1,149 @@
+import { URL } from 'url';
+import * as Bull from 'bull';
+import * as httpSignature from 'http-signature';
+import perform from '@/remote/activitypub/perform';
+import Logger from '@/services/logger';
+import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc';
+import { Instances } from '@/models/index';
+import { instanceChart } from '@/services/chart/index';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { toPuny, extractDbHost } from '@/misc/convert-host';
+import { getApId } from '@/remote/activitypub/type';
+import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
+import { InboxJobData } from '../types';
+import DbResolver from '@/remote/activitypub/db-resolver';
+import { resolvePerson } from '@/remote/activitypub/models/person';
+import { LdSignature } from '@/remote/activitypub/misc/ld-signature';
+import { StatusError } from '@/misc/fetch';
+
+const logger = new Logger('inbox');
+
+// ユーザーのinboxにアクティビティが届いた時の処理
+export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
+	const signature = job.data.signature;	// HTTP-signature
+	const activity = job.data.activity;
+
+	//#region Log
+	const info = Object.assign({}, activity) as any;
+	delete info['@context'];
+	logger.debug(JSON.stringify(info, null, 2));
+	//#endregion
+
+	const host = toPuny(new URL(signature.keyId).hostname);
+
+	// ブロックしてたら中断
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(host)) {
+		return `Blocked request: ${host}`;
+	}
+
+	const keyIdLower = signature.keyId.toLowerCase();
+	if (keyIdLower.startsWith('acct:')) {
+		return `Old keyId is no longer supported. ${keyIdLower}`;
+	}
+
+	// TDOO: キャッシュ
+	const dbResolver = new DbResolver();
+
+	// HTTP-Signature keyIdを元にDBから取得
+	let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
+
+	// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
+	if (authUser == null) {
+		try {
+			authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
+		} catch (e) {
+			// 対象が4xxならスキップ
+			if (e instanceof StatusError && e.isClientError) {
+				return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
+			}
+			throw `Error in actor ${activity.actor} - ${e.statusCode || e}`;
+		}
+	}
+
+	// それでもわからなければ終了
+	if (authUser == null) {
+		return `skip: failed to resolve user`;
+	}
+
+	// publicKey がなくても終了
+	if (authUser.key == null) {
+		return `skip: failed to resolve user publicKey`;
+	}
+
+	// HTTP-Signatureの検証
+	const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
+
+	// また、signatureのsignerは、activity.actorと一致する必要がある
+	if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
+		// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
+		if (activity.signature) {
+			if (activity.signature.type !== 'RsaSignature2017') {
+				return `skip: unsupported LD-signature type ${activity.signature.type}`;
+			}
+
+			// activity.signature.creator: https://example.oom/users/user#main-key
+			// みたいになっててUserを引っ張れば公開キーも入ることを期待する
+			if (activity.signature.creator) {
+				const candicate = activity.signature.creator.replace(/#.*/, '');
+				await resolvePerson(candicate).catch(() => null);
+			}
+
+			// keyIdからLD-Signatureのユーザーを取得
+			authUser = await dbResolver.getAuthUserFromKeyId(activity.signature.creator);
+			if (authUser == null) {
+				return `skip: LD-Signatureのユーザーが取得できませんでした`;
+			}
+
+			if (authUser.key == null) {
+				return `skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした`;
+			}
+
+			// LD-Signature検証
+			const ldSignature = new LdSignature();
+			const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
+			if (!verified) {
+				return `skip: LD-Signatureの検証に失敗しました`;
+			}
+
+			// もう一度actorチェック
+			if (authUser.user.uri !== activity.actor) {
+				return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
+			}
+
+			// ブロックしてたら中断
+			const ldHost = extractDbHost(authUser.user.uri);
+			if (meta.blockedHosts.includes(ldHost)) {
+				return `Blocked request: ${ldHost}`;
+			}
+		} else {
+			return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
+		}
+	}
+
+	// activity.idがあればホストが署名者のホストであることを確認する
+	if (typeof activity.id === 'string') {
+		const signerHost = extractDbHost(authUser.user.uri!);
+		const activityIdHost = extractDbHost(activity.id);
+		if (signerHost !== activityIdHost) {
+			return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
+		}
+	}
+
+	// Update stats
+	registerOrFetchInstanceDoc(authUser.user.host).then(i => {
+		Instances.update(i.id, {
+			latestRequestReceivedAt: new Date(),
+			lastCommunicatedAt: new Date(),
+			isNotResponding: false
+		});
+
+		fetchInstanceMetadata(i);
+
+		instanceChart.requestReceived(i.host);
+	});
+
+	// アクティビティを処理
+	await perform(authUser.user, activity);
+	return `ok`;
+};
diff --git a/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts b/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts
new file mode 100644
index 0000000000..3b2e4ea939
--- /dev/null
+++ b/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts
@@ -0,0 +1,50 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import { deleteFileSync } from '@/services/drive/delete-file';
+import { DriveFiles } from '@/models/index';
+import { MoreThan, Not, IsNull } from 'typeorm';
+
+const logger = queueLogger.createSubLogger('clean-remote-files');
+
+export default async function cleanRemoteFiles(job: Bull.Job<{}>, done: any): Promise<void> {
+	logger.info(`Deleting cached remote files...`);
+
+	let deletedCount = 0;
+	let cursor: any = null;
+
+	while (true) {
+		const files = await DriveFiles.find({
+			where: {
+				userHost: Not(IsNull()),
+				isLink: false,
+				...(cursor ? { id: MoreThan(cursor) } : {})
+			},
+			take: 8,
+			order: {
+				id: 1
+			}
+		});
+
+		if (files.length === 0) {
+			job.progress(100);
+			break;
+		}
+
+		cursor = files[files.length - 1].id;
+
+		await Promise.all(files.map(file => deleteFileSync(file, true)));
+
+		deletedCount += 8;
+
+		const total = await DriveFiles.count({
+			userHost: Not(IsNull()),
+			isLink: false,
+		});
+
+		job.progress(deletedCount / total);
+	}
+
+	logger.succ(`All cahced remote files has been deleted.`);
+	done();
+}
diff --git a/packages/backend/src/queue/processors/object-storage/delete-file.ts b/packages/backend/src/queue/processors/object-storage/delete-file.ts
new file mode 100644
index 0000000000..ed22968a27
--- /dev/null
+++ b/packages/backend/src/queue/processors/object-storage/delete-file.ts
@@ -0,0 +1,11 @@
+import { ObjectStorageFileJobData } from '@/queue/types';
+import * as Bull from 'bull';
+import { deleteObjectStorageFile } from '@/services/drive/delete-file';
+
+export default async (job: Bull.Job<ObjectStorageFileJobData>) => {
+	const key: string = job.data.key;
+
+	await deleteObjectStorageFile(key);
+
+	return 'Success';
+};
diff --git a/packages/backend/src/queue/processors/object-storage/index.ts b/packages/backend/src/queue/processors/object-storage/index.ts
new file mode 100644
index 0000000000..0d9570e179
--- /dev/null
+++ b/packages/backend/src/queue/processors/object-storage/index.ts
@@ -0,0 +1,15 @@
+import * as Bull from 'bull';
+import { ObjectStorageJobData } from '@/queue/types';
+import deleteFile from './delete-file';
+import cleanRemoteFiles from './clean-remote-files';
+
+const jobs = {
+	deleteFile,
+	cleanRemoteFiles,
+} as Record<string, Bull.ProcessCallbackFunction<ObjectStorageJobData> | Bull.ProcessPromiseFunction<ObjectStorageJobData>>;
+
+export default function(q: Bull.Queue) {
+	for (const [k, v] of Object.entries(jobs)) {
+		q.process(k, 16, v);
+	}
+}
diff --git a/packages/backend/src/queue/processors/system/index.ts b/packages/backend/src/queue/processors/system/index.ts
new file mode 100644
index 0000000000..52b7868105
--- /dev/null
+++ b/packages/backend/src/queue/processors/system/index.ts
@@ -0,0 +1,12 @@
+import * as Bull from 'bull';
+import { resyncCharts } from './resync-charts';
+
+const jobs = {
+	resyncCharts,
+} as Record<string, Bull.ProcessCallbackFunction<{}> | Bull.ProcessPromiseFunction<{}>>;
+
+export default function(dbQueue: Bull.Queue<{}>) {
+	for (const [k, v] of Object.entries(jobs)) {
+		dbQueue.process(k, v);
+	}
+}
diff --git a/packages/backend/src/queue/processors/system/resync-charts.ts b/packages/backend/src/queue/processors/system/resync-charts.ts
new file mode 100644
index 0000000000..b36b024cfb
--- /dev/null
+++ b/packages/backend/src/queue/processors/system/resync-charts.ts
@@ -0,0 +1,21 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import { driveChart, notesChart, usersChart } from '@/services/chart/index';
+
+const logger = queueLogger.createSubLogger('resync-charts');
+
+export default async function resyncCharts(job: Bull.Job<{}>, done: any): Promise<void> {
+	logger.info(`Resync charts...`);
+
+	// TODO: ユーザーごとのチャートも更新する
+	// TODO: インスタンスごとのチャートも更新する
+	await Promise.all([
+		driveChart.resync(),
+		notesChart.resync(),
+		usersChart.resync(),
+	]);
+
+	logger.succ(`All charts successfully resynced.`);
+	done();
+}
diff --git a/packages/backend/src/queue/queues.ts b/packages/backend/src/queue/queues.ts
new file mode 100644
index 0000000000..a66a7ca451
--- /dev/null
+++ b/packages/backend/src/queue/queues.ts
@@ -0,0 +1,9 @@
+import config from '@/config/index';
+import { initialize as initializeQueue } from './initialize';
+import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
+
+export const systemQueue = initializeQueue<{}>('system');
+export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
+export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
+export const dbQueue = initializeQueue<DbJobData>('db');
+export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage');
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
new file mode 100644
index 0000000000..39cab29966
--- /dev/null
+++ b/packages/backend/src/queue/types.ts
@@ -0,0 +1,44 @@
+import { DriveFile } from '@/models/entities/drive-file';
+import { User } from '@/models/entities/user';
+import { IActivity } from '@/remote/activitypub/type';
+import * as httpSignature from 'http-signature';
+
+export type DeliverJobData = {
+	/** Actor */
+	user: ThinUser;
+	/** Activity */
+	content: unknown;
+	/** inbox URL to deliver */
+	to: string;
+};
+
+export type InboxJobData = {
+	activity: IActivity;
+	signature: httpSignature.IParsedSignature;
+};
+
+export type DbJobData = DbUserJobData | DbUserImportJobData | DbUserDeleteJobData;
+
+export type DbUserJobData = {
+	user: ThinUser;
+};
+
+export type DbUserDeleteJobData = {
+	user: ThinUser;
+	soft?: boolean;
+};
+
+export type DbUserImportJobData = {
+	user: ThinUser;
+	fileId: DriveFile['id'];
+};
+
+export type ObjectStorageJobData = ObjectStorageFileJobData | {};
+
+export type ObjectStorageFileJobData = {
+	key: string;
+};
+
+export type ThinUser = {
+	id: User['id'];
+};
diff --git a/packages/backend/src/remote/activitypub/ap-request.ts b/packages/backend/src/remote/activitypub/ap-request.ts
new file mode 100644
index 0000000000..76a3857140
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/ap-request.ts
@@ -0,0 +1,104 @@
+import * as crypto from 'crypto';
+import { URL } from 'url';
+
+type Request = {
+	url: string;
+	method: string;
+	headers: Record<string, string>;
+};
+
+type PrivateKey = {
+	privateKeyPem: string;
+	keyId: string;
+};
+
+export function createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }) {
+	const u = new URL(args.url);
+	const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
+
+	const request: Request = {
+		url: u.href,
+		method: 'POST',
+		headers:  objectAssignWithLcKey({
+			'Date': new Date().toUTCString(),
+			'Host': u.hostname,
+			'Content-Type': 'application/activity+json',
+			'Digest': digestHeader,
+		}, args.additionalHeaders),
+	};
+
+	const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
+
+	return {
+		request,
+		signingString: result.signingString,
+		signature: result.signature,
+		signatureHeader: result.signatureHeader,
+	};
+}
+
+export function createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }) {
+	const u = new URL(args.url);
+
+	const request: Request = {
+		url: u.href,
+		method: 'GET',
+		headers:  objectAssignWithLcKey({
+			'Accept': 'application/activity+json, application/ld+json',
+			'Date': new Date().toUTCString(),
+			'Host': new URL(args.url).hostname,
+		}, args.additionalHeaders),
+	};
+
+	const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
+
+	return {
+		request,
+		signingString: result.signingString,
+		signature: result.signature,
+		signatureHeader: result.signatureHeader,
+	};
+}
+
+function signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]) {
+	const signingString = genSigningString(request, includeHeaders);
+	const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
+	const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
+
+	request.headers = objectAssignWithLcKey(request.headers, {
+		Signature: signatureHeader
+	});
+
+	return {
+		request,
+		signingString,
+		signature,
+		signatureHeader,
+	};
+}
+
+function genSigningString(request: Request, includeHeaders: string[]) {
+	request.headers = lcObjectKey(request.headers);
+
+	const results: string[] = [];
+
+	for (const key of includeHeaders.map(x => x.toLowerCase())) {
+		if (key === '(request-target)') {
+			results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`);
+		} else {
+			results.push(`${key}: ${request.headers[key]}`);
+		}
+	}
+
+	return results.join('\n');
+}
+
+function lcObjectKey(src: Record<string, string>) {
+	const dst: Record<string, string> = {};
+	for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
+	return dst;
+}
+
+function objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>) {
+	return Object.assign(lcObjectKey(a), lcObjectKey(b));
+}
diff --git a/packages/backend/src/remote/activitypub/audience.ts b/packages/backend/src/remote/activitypub/audience.ts
new file mode 100644
index 0000000000..3d2dab1459
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/audience.ts
@@ -0,0 +1,92 @@
+import { ApObject, getApIds } from './type';
+import Resolver from './resolver';
+import { resolvePerson } from './models/person';
+import { unique, concat } from '@/prelude/array';
+import * as promiseLimit from 'promise-limit';
+import { User, IRemoteUser } from '@/models/entities/user';
+
+type Visibility = 'public' | 'home' | 'followers' | 'specified';
+
+type AudienceInfo = {
+	visibility: Visibility,
+	mentionedUsers: User[],
+	visibleUsers: User[],
+};
+
+export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
+	const toGroups = groupingAudience(getApIds(to), actor);
+	const ccGroups = groupingAudience(getApIds(cc), actor);
+
+	const others = unique(concat([toGroups.other, ccGroups.other]));
+
+	const limit = promiseLimit<User | null>(2);
+	const mentionedUsers = (await Promise.all(
+		others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null)))
+	)).filter((x): x is User => x != null);
+
+	if (toGroups.public.length > 0) {
+		return {
+			visibility: 'public',
+			mentionedUsers,
+			visibleUsers: []
+		};
+	}
+
+	if (ccGroups.public.length > 0) {
+		return {
+			visibility: 'home',
+			mentionedUsers,
+			visibleUsers: []
+		};
+	}
+
+	if (toGroups.followers.length > 0) {
+		return {
+			visibility: 'followers',
+			mentionedUsers,
+			visibleUsers: []
+		};
+	}
+
+	return {
+		visibility: 'specified',
+		mentionedUsers,
+		visibleUsers: mentionedUsers
+	};
+}
+
+function groupingAudience(ids: string[], actor: IRemoteUser) {
+	const groups = {
+		public: [] as string[],
+		followers: [] as string[],
+		other: [] as string[],
+	};
+
+	for (const id of ids) {
+		if (isPublic(id)) {
+			groups.public.push(id);
+		} else if (isFollowers(id, actor)) {
+			groups.followers.push(id);
+		} else {
+			groups.other.push(id);
+		}
+	}
+
+	groups.other = unique(groups.other);
+
+	return groups;
+}
+
+function isPublic(id: string) {
+	return [
+		'https://www.w3.org/ns/activitystreams#Public',
+		'as#Public',
+		'Public',
+	].includes(id);
+}
+
+function isFollowers(id: string, actor: IRemoteUser) {
+	return (
+		id === (actor.followersUri || `${actor.uri}/followers`)
+	);
+}
diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts
new file mode 100644
index 0000000000..289b6f0ee8
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/db-resolver.ts
@@ -0,0 +1,140 @@
+import config from '@/config/index';
+import { Note } from '@/models/entities/note';
+import { User, IRemoteUser } from '@/models/entities/user';
+import { UserPublickey } from '@/models/entities/user-publickey';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index';
+import { IObject, getApId } from './type';
+import { resolvePerson } from './models/person';
+import escapeRegexp = require('escape-regexp');
+
+export default class DbResolver {
+	constructor() {
+	}
+
+	/**
+	 * AP Note => Misskey Note in DB
+	 */
+	public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
+		const parsed = this.parseUri(value);
+
+		if (parsed.id) {
+			return (await Notes.findOne({
+				id: parsed.id
+			})) || null;
+		}
+
+		if (parsed.uri) {
+			return (await Notes.findOne({
+				uri: parsed.uri
+			})) || null;
+		}
+
+		return null;
+	}
+
+	public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> {
+		const parsed = this.parseUri(value);
+
+		if (parsed.id) {
+			return (await MessagingMessages.findOne({
+				id: parsed.id
+			})) || null;
+		}
+
+		if (parsed.uri) {
+			return (await MessagingMessages.findOne({
+				uri: parsed.uri
+			})) || null;
+		}
+
+		return null;
+	}
+
+	/**
+	 * AP Person => Misskey User in DB
+	 */
+	public async getUserFromApId(value: string | IObject): Promise<User | null> {
+		const parsed = this.parseUri(value);
+
+		if (parsed.id) {
+			return (await Users.findOne({
+				id: parsed.id
+			})) || null;
+		}
+
+		if (parsed.uri) {
+			return (await Users.findOne({
+				uri: parsed.uri
+			})) || null;
+		}
+
+		return null;
+	}
+
+	/**
+	 * AP KeyId => Misskey User and Key
+	 */
+	public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> {
+		const key = await UserPublickeys.findOne({
+			keyId
+		});
+
+		if (key == null) return null;
+
+		const user = await Users.findOne(key.userId) as IRemoteUser;
+
+		return {
+			user,
+			key
+		};
+	}
+
+	/**
+	 * AP Actor id => Misskey User and Key
+	 */
+	public async getAuthUserFromApId(uri: string): Promise<AuthUser | null> {
+		const user = await resolvePerson(uri) as IRemoteUser;
+
+		if (user == null) return null;
+
+		const key = await UserPublickeys.findOne(user.id);
+
+		return {
+			user,
+			key
+		};
+	}
+
+	public parseUri(value: string | IObject): UriParseResult {
+		const uri = getApId(value);
+
+		const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)');
+		const matchLocal = uri.match(localRegex);
+
+		if (matchLocal) {
+			return {
+				type: matchLocal[1],
+				id: matchLocal[2]
+			};
+		} else {
+			return {
+				uri
+			};
+		}
+	}
+}
+
+export type AuthUser = {
+	user: IRemoteUser;
+	key?: UserPublickey;
+};
+
+type UriParseResult = {
+	/** id in DB (local object only) */
+	id?: string;
+	/** uri in DB (remote object only) */
+	uri?: string;
+	/** hint of type (local object only, ex: notes, users) */
+	type?: string
+};
diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts
new file mode 100644
index 0000000000..d37f97a447
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/deliver-manager.ts
@@ -0,0 +1,131 @@
+import { Users, Followings } from '@/models/index';
+import { ILocalUser, IRemoteUser, User } from '@/models/entities/user';
+import { deliver } from '@/queue/index';
+
+//#region types
+interface IRecipe {
+	type: string;
+}
+
+interface IFollowersRecipe extends IRecipe {
+	type: 'Followers';
+}
+
+interface IDirectRecipe extends IRecipe {
+	type: 'Direct';
+	to: IRemoteUser;
+}
+
+const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
+	recipe.type === 'Followers';
+
+const isDirect = (recipe: any): recipe is IDirectRecipe =>
+	recipe.type === 'Direct';
+//#endregion
+
+export default class DeliverManager {
+	private actor: { id: User['id']; host: null; };
+	private activity: any;
+	private recipes: IRecipe[] = [];
+
+	/**
+	 * Constructor
+	 * @param actor Actor
+	 * @param activity Activity to deliver
+	 */
+	constructor(actor: { id: User['id']; host: null; }, activity: any) {
+		this.actor = actor;
+		this.activity = activity;
+	}
+
+	/**
+	 * Add recipe for followers deliver
+	 */
+	public addFollowersRecipe() {
+		const deliver = {
+			type: 'Followers'
+		} as IFollowersRecipe;
+
+		this.addRecipe(deliver);
+	}
+
+	/**
+	 * Add recipe for direct deliver
+	 * @param to To
+	 */
+	public addDirectRecipe(to: IRemoteUser) {
+		const recipe = {
+			type: 'Direct',
+			to
+		} as IDirectRecipe;
+
+		this.addRecipe(recipe);
+	}
+
+	/**
+	 * Add recipe
+	 * @param recipe Recipe
+	 */
+	public addRecipe(recipe: IRecipe) {
+		this.recipes.push(recipe);
+	}
+
+	/**
+	 * Execute delivers
+	 */
+	public async execute() {
+		if (!Users.isLocalUser(this.actor)) return;
+
+		const inboxes = new Set<string>();
+
+		// build inbox list
+		for (const recipe of this.recipes) {
+			if (isFollowers(recipe)) {
+				// followers deliver
+				const followers = await Followings.find({
+					followeeId: this.actor.id
+				});
+
+				for (const following of followers) {
+					if (Followings.isRemoteFollower(following)) {
+						const inbox = following.followerSharedInbox || following.followerInbox;
+						inboxes.add(inbox);
+					}
+				}
+			} else if (isDirect(recipe)) {
+				// direct deliver
+				const inbox = recipe.to.inbox;
+				if (inbox) inboxes.add(inbox);
+			}
+		}
+
+		// deliver
+		for (const inbox of inboxes) {
+			deliver(this.actor, this.activity, inbox);
+		}
+	}
+}
+
+//#region Utilities
+/**
+ * Deliver activity to followers
+ * @param activity Activity
+ * @param from Followee
+ */
+export async function deliverToFollowers(actor: ILocalUser, activity: any) {
+	const manager = new DeliverManager(actor, activity);
+	manager.addFollowersRecipe();
+	await manager.execute();
+}
+
+/**
+ * Deliver activity to user
+ * @param activity Activity
+ * @param to Target user
+ */
+export async function deliverToUser(actor: ILocalUser, activity: any, to: IRemoteUser) {
+	const manager = new DeliverManager(actor, activity);
+	manager.addDirectRecipe(to);
+	await manager.execute();
+}
+//#endregion
diff --git a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts
new file mode 100644
index 0000000000..1afb733ab5
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts
@@ -0,0 +1,29 @@
+import { IRemoteUser } from '@/models/entities/user';
+import accept from '@/services/following/requests/accept';
+import { IFollow } from '../../type';
+import DbResolver from '../../db-resolver';
+import { relayAccepted } from '@/services/relay';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
+	// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
+
+	const dbResolver = new DbResolver();
+	const follower = await dbResolver.getUserFromApId(activity.actor);
+
+	if (follower == null) {
+		return `skip: follower not found`;
+	}
+
+	if (follower.host != null) {
+		return `skip: follower is not a local user`;
+	}
+
+	// relay
+	const match = activity.id?.match(/follow-relay\/(\w+)/);
+	if (match) {
+		return await relayAccepted(match[1]);
+	}
+
+	await accept(actor, follower);
+	return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/accept/index.ts b/packages/backend/src/remote/activitypub/kernel/accept/index.ts
new file mode 100644
index 0000000000..5c6f81b2e3
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/accept/index.ts
@@ -0,0 +1,24 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import acceptFollow from './follow';
+import { IAccept, isFollow, getApType } from '../../type';
+import { apLogger } from '../../logger';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
+	const uri = activity.id || activity;
+
+	logger.info(`Accept: ${uri}`);
+
+	const resolver = new Resolver();
+
+	const object = await resolver.resolve(activity.object).catch(e => {
+		logger.error(`Resolution failed: ${e}`);
+		throw e;
+	});
+
+	if (isFollow(object)) return await acceptFollow(actor, object);
+
+	return `skip: Unknown Accept type: ${getApType(object)}`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/add/index.ts b/packages/backend/src/remote/activitypub/kernel/add/index.ts
new file mode 100644
index 0000000000..b33be0cc85
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/add/index.ts
@@ -0,0 +1,23 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { IAdd } from '../../type';
+import { resolveNote } from '../../models/note';
+import { addPinned } from '@/services/i/pin';
+
+export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
+	if ('actor' in activity && actor.uri !== activity.actor) {
+		throw new Error('invalid actor');
+	}
+
+	if (activity.target == null) {
+		throw new Error('target is null');
+	}
+
+	if (activity.target === actor.featured) {
+		const note = await resolveNote(activity.object);
+		if (note == null) throw new Error('note not found');
+		await addPinned(actor, note.id);
+		return;
+	}
+
+	throw new Error(`unknown target: ${activity.target}`);
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/announce/index.ts b/packages/backend/src/remote/activitypub/kernel/announce/index.ts
new file mode 100644
index 0000000000..581357e577
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/announce/index.ts
@@ -0,0 +1,19 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import announceNote from './note';
+import { IAnnounce, getApId } from '../../type';
+import { apLogger } from '../../logger';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
+	const uri = getApId(activity);
+
+	logger.info(`Announce: ${uri}`);
+
+	const resolver = new Resolver();
+
+	const targetUri = getApId(activity.object);
+
+	announceNote(resolver, actor, activity, targetUri);
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts
new file mode 100644
index 0000000000..5230867f24
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/announce/note.ts
@@ -0,0 +1,67 @@
+import Resolver from '../../resolver';
+import post from '@/services/note/create';
+import { IRemoteUser } from '@/models/entities/user';
+import { IAnnounce, getApId } from '../../type';
+import { fetchNote, resolveNote } from '../../models/note';
+import { apLogger } from '../../logger';
+import { extractDbHost } from '@/misc/convert-host';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { getApLock } from '@/misc/app-lock';
+import { parseAudience } from '../../audience';
+import { StatusError } from '@/misc/fetch';
+
+const logger = apLogger;
+
+/**
+ * アナウンスアクティビティを捌きます
+ */
+export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
+	const uri = getApId(activity);
+
+	// アナウンサーが凍結されていたらスキップ
+	if (actor.isSuspended) {
+		return;
+	}
+
+	// アナウンス先をブロックしてたら中断
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(extractDbHost(uri))) return;
+
+	const unlock = await getApLock(uri);
+
+	try {
+		// 既に同じURIを持つものが登録されていないかチェック
+		const exist = await fetchNote(uri);
+		if (exist) {
+			return;
+		}
+
+		// Announce対象をresolve
+		let renote;
+		try {
+			renote = await resolveNote(targetUri);
+		} catch (e) {
+			// 対象が4xxならスキップ
+			if (e instanceof StatusError && e.isClientError) {
+				logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
+				return;
+			}
+			logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`);
+			throw e;
+		}
+
+		logger.info(`Creating the (Re)Note: ${uri}`);
+
+		const activityAudience = await parseAudience(actor, activity.to, activity.cc);
+
+		await post(actor, {
+			createdAt: activity.published ? new Date(activity.published) : null,
+			renote,
+			visibility: activityAudience.visibility,
+			visibleUsers: activityAudience.visibleUsers,
+			uri
+		});
+	} finally {
+		unlock();
+	}
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/block/index.ts b/packages/backend/src/remote/activitypub/kernel/block/index.ts
new file mode 100644
index 0000000000..4fd1e07b9b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/block/index.ts
@@ -0,0 +1,22 @@
+import { IBlock } from '../../type';
+import block from '@/services/blocking/create';
+import { IRemoteUser } from '@/models/entities/user';
+import DbResolver from '../../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
+	// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
+
+	const dbResolver = new DbResolver();
+	const blockee = await dbResolver.getUserFromApId(activity.object);
+
+	if (blockee == null) {
+		return `skip: blockee not found`;
+	}
+
+	if (blockee.host != null) {
+		return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`;
+	}
+
+	await block(actor, blockee);
+	return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/create/index.ts b/packages/backend/src/remote/activitypub/kernel/create/index.ts
new file mode 100644
index 0000000000..ce039a363b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/create/index.ts
@@ -0,0 +1,43 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import createNote from './note';
+import { ICreate, getApId, isPost, getApType } from '../../type';
+import { apLogger } from '../../logger';
+import { toArray, concat, unique } from '@/prelude/array';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
+	const uri = getApId(activity);
+
+	logger.info(`Create: ${uri}`);
+
+	// copy audiences between activity <=> object.
+	if (typeof activity.object === 'object') {
+		const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
+		const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)]));
+
+		activity.to = to;
+		activity.cc = cc;
+		activity.object.to = to;
+		activity.object.cc = cc;
+	}
+
+	// If there is no attributedTo, use Activity actor.
+	if (typeof activity.object === 'object' && !activity.object.attributedTo) {
+		activity.object.attributedTo = activity.actor;
+	}
+
+	const resolver = new Resolver();
+
+	const object = await resolver.resolve(activity.object).catch(e => {
+		logger.error(`Resolution failed: ${e}`);
+		throw e;
+	});
+
+	if (isPost(object)) {
+		createNote(resolver, actor, object, false, activity);
+	} else {
+		logger.warn(`Unknown type: ${getApType(object)}`);
+	}
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts
new file mode 100644
index 0000000000..14e311e4cd
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/create/note.ts
@@ -0,0 +1,44 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import { createNote, fetchNote } from '../../models/note';
+import { getApId, IObject, ICreate } from '../../type';
+import { getApLock } from '@/misc/app-lock';
+import { extractDbHost } from '@/misc/convert-host';
+import { StatusError } from '@/misc/fetch';
+
+/**
+ * 投稿作成アクティビティを捌きます
+ */
+export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
+	const uri = getApId(note);
+
+	if (typeof note === 'object') {
+		if (actor.uri !== note.attributedTo) {
+			return `skip: actor.uri !== note.attributedTo`;
+		}
+
+		if (typeof note.id === 'string') {
+			if (extractDbHost(actor.uri) !== extractDbHost(note.id)) {
+				return `skip: host in actor.uri !== note.id`;
+			}
+		}
+	}
+
+	const unlock = await getApLock(uri);
+
+	try {
+		const exist = await fetchNote(note);
+		if (exist) return 'skip: note exists';
+
+		await createNote(note, resolver, silent);
+		return 'ok';
+	} catch (e) {
+		if (e instanceof StatusError && e.isClientError) {
+			return `skip ${e.statusCode}`;
+		} else {
+			throw e;
+		}
+	} finally {
+		unlock();
+	}
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts
new file mode 100644
index 0000000000..502f8d5ab5
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts
@@ -0,0 +1,26 @@
+import { apLogger } from '../../logger';
+import { createDeleteAccountJob } from '@/queue';
+import { IRemoteUser } from '@/models/entities/user';
+import { Users } from '@/models/index';
+
+const logger = apLogger;
+
+export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> {
+	logger.info(`Deleting the Actor: ${uri}`);
+
+	if (actor.uri !== uri) {
+		return `skip: delete actor ${actor.uri} !== ${uri}`;
+	}
+
+	if (actor.isDeleted) {
+		logger.info(`skip: already deleted`);
+	}
+
+	const job = await createDeleteAccountJob(actor);
+
+	await Users.update(actor.id, {
+		isDeleted: true,
+	});
+
+	return `ok: queued ${job.name} ${job.id}`;
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts
new file mode 100644
index 0000000000..86a452de76
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/delete/index.ts
@@ -0,0 +1,49 @@
+import deleteNote from './note';
+import { IRemoteUser } from '@/models/entities/user';
+import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type';
+import { toSingle } from '@/prelude/array';
+import { deleteActor } from './actor';
+
+/**
+ * 削除アクティビティを捌きます
+ */
+export default async (actor: IRemoteUser, activity: IDelete): Promise<string> => {
+	if ('actor' in activity && actor.uri !== activity.actor) {
+		throw new Error('invalid actor');
+	}
+
+	// 削除対象objectのtype
+	let formarType: string | undefined;
+
+	if (typeof activity.object === 'string') {
+		// typeが不明だけど、どうせ消えてるのでremote resolveしない
+		formarType = undefined;
+	} else {
+		const object = activity.object as IObject;
+		if (isTombstone(object)) {
+			formarType = toSingle(object.formerType);
+		} else {
+			formarType = toSingle(object.type);
+		}
+	}
+
+	const uri = getApId(activity.object);
+
+	// type不明でもactorとobjectが同じならばそれはPersonに違いない
+	if (!formarType && actor.uri === uri) {
+		formarType = 'Person';
+	}
+
+	// それでもなかったらおそらくNote
+	if (!formarType) {
+		formarType = 'Note';
+	}
+
+	if (validPost.includes(formarType)) {
+		return await deleteNote(actor, uri);
+	} else if (validActor.includes(formarType)) {
+		return await deleteActor(actor, uri);
+	} else {
+		return `Unknown type ${formarType}`;
+	}
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts
new file mode 100644
index 0000000000..3875a33d13
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts
@@ -0,0 +1,41 @@
+import { IRemoteUser } from '@/models/entities/user';
+import deleteNode from '@/services/note/delete';
+import { apLogger } from '../../logger';
+import DbResolver from '../../db-resolver';
+import { getApLock } from '@/misc/app-lock';
+import { deleteMessage } from '@/services/messages/delete';
+
+const logger = apLogger;
+
+export default async function(actor: IRemoteUser, uri: string): Promise<string> {
+	logger.info(`Deleting the Note: ${uri}`);
+
+	const unlock = await getApLock(uri);
+
+	try {
+		const dbResolver = new DbResolver();
+		const note = await dbResolver.getNoteFromApId(uri);
+
+		if (note == null) {
+			const message = await dbResolver.getMessageFromApId(uri);
+			if (message == null) return 'message not found';
+
+			if (message.userId !== actor.id) {
+				return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
+			}
+
+			await deleteMessage(message);
+
+			return 'ok: message deleted';
+		}
+
+		if (note.userId !== actor.id) {
+			return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
+		}
+
+		await deleteNode(actor, note);
+		return 'ok: note deleted';
+	} finally {
+		unlock();
+	}
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts
new file mode 100644
index 0000000000..7abfd694cd
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/flag/index.ts
@@ -0,0 +1,30 @@
+import { IRemoteUser } from '@/models/entities/user';
+import config from '@/config/index';
+import { IFlag, getApIds } from '../../type';
+import { AbuseUserReports, Users } from '@/models/index';
+import { In } from 'typeorm';
+import { genId } from '@/misc/gen-id';
+
+export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => {
+	// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
+	// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
+	const uris = getApIds(activity.object);
+
+	const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop());
+	const users = await Users.find({
+		id: In(userIds)
+	});
+	if (users.length < 1) return `skip`;
+
+	await AbuseUserReports.insert({
+		id: genId(),
+		createdAt: new Date(),
+		targetUserId: users[0].id,
+		targetUserHost: users[0].host,
+		reporterId: actor.id,
+		reporterHost: actor.host,
+		comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`
+	});
+
+	return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/follow.ts b/packages/backend/src/remote/activitypub/kernel/follow.ts
new file mode 100644
index 0000000000..3183207afa
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/follow.ts
@@ -0,0 +1,20 @@
+import { IRemoteUser } from '@/models/entities/user';
+import follow from '@/services/following/create';
+import { IFollow } from '../type';
+import DbResolver from '../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
+	const dbResolver = new DbResolver();
+	const followee = await dbResolver.getUserFromApId(activity.object);
+
+	if (followee == null) {
+		return `skip: followee not found`;
+	}
+
+	if (followee.host != null) {
+		return `skip: フォローしようとしているユーザーはローカルユーザーではありません`;
+	}
+
+	await follow(actor, followee, activity.id);
+	return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts
new file mode 100644
index 0000000000..20df28eec6
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/index.ts
@@ -0,0 +1,71 @@
+import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type';
+import { IRemoteUser } from '@/models/entities/user';
+import create from './create/index';
+import performDeleteActivity from './delete/index';
+import performUpdateActivity from './update/index';
+import { performReadActivity } from './read';
+import follow from './follow';
+import undo from './undo/index';
+import like from './like';
+import announce from './announce/index';
+import accept from './accept/index';
+import reject from './reject/index';
+import add from './add/index';
+import remove from './remove/index';
+import block from './block/index';
+import flag from './flag/index';
+import { apLogger } from '../logger';
+import Resolver from '../resolver';
+import { toArray } from '@/prelude/array';
+
+export async function performActivity(actor: IRemoteUser, activity: IObject) {
+	if (isCollectionOrOrderedCollection(activity)) {
+		const resolver = new Resolver();
+		for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
+			const act = await resolver.resolve(item);
+			try {
+				await performOneActivity(actor, act);
+			} catch (e) {
+				apLogger.error(e);
+			}
+		}
+	} else {
+		await performOneActivity(actor, activity);
+	}
+}
+
+async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise<void> {
+	if (actor.isSuspended) return;
+
+	if (isCreate(activity)) {
+		await create(actor, activity);
+	} else if (isDelete(activity)) {
+		await performDeleteActivity(actor, activity);
+	} else if (isUpdate(activity)) {
+		await performUpdateActivity(actor, activity);
+	} else if (isRead(activity)) {
+		await performReadActivity(actor, activity);
+	} else if (isFollow(activity)) {
+		await follow(actor, activity);
+	} else if (isAccept(activity)) {
+		await accept(actor, activity);
+	} else if (isReject(activity)) {
+		await reject(actor, activity);
+	} else if (isAdd(activity)) {
+		await add(actor, activity).catch(err => apLogger.error(err));
+	} else if (isRemove(activity)) {
+		await remove(actor, activity).catch(err => apLogger.error(err));
+	} else if (isAnnounce(activity)) {
+		await announce(actor, activity);
+	} else if (isLike(activity)) {
+		await like(actor, activity);
+	} else if (isUndo(activity)) {
+		await undo(actor, activity);
+	} else if (isBlock(activity)) {
+		await block(actor, activity);
+	} else if (isFlag(activity)) {
+		await flag(actor, activity);
+	} else {
+		apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
+	}
+}
diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts
new file mode 100644
index 0000000000..58d5aefefc
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/like.ts
@@ -0,0 +1,21 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { ILike, getApId } from '../type';
+import create from '@/services/note/reaction/create';
+import { fetchNote, extractEmojis } from '../models/note';
+
+export default async (actor: IRemoteUser, activity: ILike) => {
+	const targetUri = getApId(activity.object);
+
+	const note = await fetchNote(targetUri);
+	if (!note) return `skip: target note not found ${targetUri}`;
+
+	await extractEmojis(activity.tag || [], actor.host).catch(() => null);
+
+	return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
+		if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
+			return 'skip: already reacted';
+		} else {
+			throw e;
+		}
+	}).then(() => 'ok');
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/move/index.ts b/packages/backend/src/remote/activitypub/kernel/move/index.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts
new file mode 100644
index 0000000000..11a1731869
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/read.ts
@@ -0,0 +1,27 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { IRead, getApId } from '../type';
+import { isSelfHost, extractDbHost } from '@/misc/convert-host';
+import { MessagingMessages } from '@/models/index';
+import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message';
+
+export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
+	const id = await getApId(activity.object);
+
+	if (!isSelfHost(extractDbHost(id))) {
+		return `skip: Read to foreign host (${id})`;
+	}
+
+	const messageId = id.split('/').pop();
+
+	const message = await MessagingMessages.findOne(messageId);
+	if (message == null) {
+		return `skip: message not found`;
+	}
+
+	if (actor.id != message.recipientId) {
+		return `skip: actor is not a message recipient`;
+	}
+
+	await readUserMessagingMessage(message.recipientId!, message.userId, [message.id]);
+	return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts
new file mode 100644
index 0000000000..356547440f
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts
@@ -0,0 +1,29 @@
+import { IRemoteUser } from '@/models/entities/user';
+import reject from '@/services/following/requests/reject';
+import { IFollow } from '../../type';
+import DbResolver from '../../db-resolver';
+import { relayRejected } from '@/services/relay';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
+	// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
+
+	const dbResolver = new DbResolver();
+	const follower = await dbResolver.getUserFromApId(activity.actor);
+
+	if (follower == null) {
+		return `skip: follower not found`;
+	}
+
+	if (follower.host != null) {
+		return `skip: follower is not a local user`;
+	}
+
+	// relay
+	const match = activity.id?.match(/follow-relay\/(\w+)/);
+	if (match) {
+		return await relayRejected(match[1]);
+	}
+
+	await reject(actor, follower);
+	return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/reject/index.ts b/packages/backend/src/remote/activitypub/kernel/reject/index.ts
new file mode 100644
index 0000000000..d0de9c329b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/reject/index.ts
@@ -0,0 +1,24 @@
+import Resolver from '../../resolver';
+import { IRemoteUser } from '@/models/entities/user';
+import rejectFollow from './follow';
+import { IReject, isFollow, getApType } from '../../type';
+import { apLogger } from '../../logger';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: IReject): Promise<string> => {
+	const uri = activity.id || activity;
+
+	logger.info(`Reject: ${uri}`);
+
+	const resolver = new Resolver();
+
+	const object = await resolver.resolve(activity.object).catch(e => {
+		logger.error(`Resolution failed: ${e}`);
+		throw e;
+	});
+
+	if (isFollow(object)) return await rejectFollow(actor, object);
+
+	return `skip: Unknown Reject type: ${getApType(object)}`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/remove/index.ts b/packages/backend/src/remote/activitypub/kernel/remove/index.ts
new file mode 100644
index 0000000000..d59953e653
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/remove/index.ts
@@ -0,0 +1,23 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { IRemove } from '../../type';
+import { resolveNote } from '../../models/note';
+import { removePinned } from '@/services/i/pin';
+
+export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
+	if ('actor' in activity && actor.uri !== activity.actor) {
+		throw new Error('invalid actor');
+	}
+
+	if (activity.target == null) {
+		throw new Error('target is null');
+	}
+
+	if (activity.target === actor.featured) {
+		const note = await resolveNote(activity.object);
+		if (note == null) throw new Error('note not found');
+		await removePinned(actor, note.id);
+		return;
+	}
+
+	throw new Error(`unknown target: ${activity.target}`);
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts
new file mode 100644
index 0000000000..7bb9d7fcad
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts
@@ -0,0 +1,17 @@
+import { Notes } from '@/models/index';
+import { IRemoteUser } from '@/models/entities/user';
+import { IAnnounce, getApId } from '../../type';
+import deleteNote from '@/services/note/delete';
+
+export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<string> => {
+	const uri = getApId(activity);
+
+	const note = await Notes.findOne({
+		uri
+	});
+
+	if (!note) return 'skip: no such Announce';
+
+	await deleteNote(actor, note);
+	return 'ok: deleted';
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/block.ts b/packages/backend/src/remote/activitypub/kernel/undo/block.ts
new file mode 100644
index 0000000000..61940486be
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/block.ts
@@ -0,0 +1,20 @@
+import { IBlock } from '../../type';
+import unblock from '@/services/blocking/delete';
+import { IRemoteUser } from '@/models/entities/user';
+import DbResolver from '../../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
+	const dbResolver = new DbResolver();
+	const blockee = await dbResolver.getUserFromApId(activity.object);
+
+	if (blockee == null) {
+		return `skip: blockee not found`;
+	}
+
+	if (blockee.host != null) {
+		return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`;
+	}
+
+	await unblock(actor, blockee);
+	return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts
new file mode 100644
index 0000000000..d85c7e4a71
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts
@@ -0,0 +1,41 @@
+import unfollow from '@/services/following/delete';
+import cancelRequest from '@/services/following/requests/cancel';
+import { IFollow } from '../../type';
+import { IRemoteUser } from '@/models/entities/user';
+import { FollowRequests, Followings } from '@/models/index';
+import DbResolver from '../../db-resolver';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
+	const dbResolver = new DbResolver();
+
+	const followee = await dbResolver.getUserFromApId(activity.object);
+	if (followee == null) {
+		return `skip: followee not found`;
+	}
+
+	if (followee.host != null) {
+		return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`;
+	}
+
+	const req = await FollowRequests.findOne({
+		followerId: actor.id,
+		followeeId: followee.id
+	});
+
+	const following = await Followings.findOne({
+		followerId: actor.id,
+		followeeId: followee.id
+	});
+
+	if (req) {
+		await cancelRequest(followee, actor);
+		return `ok: follow request canceled`;
+	}
+
+	if (following) {
+		await unfollow(actor, followee);
+		return `ok: unfollowed`;
+	}
+
+	return `skip: リクエストもフォローもされていない`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts
new file mode 100644
index 0000000000..14b1add152
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/index.ts
@@ -0,0 +1,34 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type';
+import unfollow from './follow';
+import unblock from './block';
+import undoLike from './like';
+import { undoAnnounce } from './announce';
+import Resolver from '../../resolver';
+import { apLogger } from '../../logger';
+
+const logger = apLogger;
+
+export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
+	if ('actor' in activity && actor.uri !== activity.actor) {
+		throw new Error('invalid actor');
+	}
+
+	const uri = activity.id || activity;
+
+	logger.info(`Undo: ${uri}`);
+
+	const resolver = new Resolver();
+
+	const object = await resolver.resolve(activity.object).catch(e => {
+		logger.error(`Resolution failed: ${e}`);
+		throw e;
+	});
+
+	if (isFollow(object)) return await unfollow(actor, object);
+	if (isBlock(object)) return await unblock(actor, object);
+	if (isLike(object)) return await undoLike(actor, object);
+	if (isAnnounce(object)) return await undoAnnounce(actor, object);
+
+	return `skip: unknown object type ${getApType(object)}`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/undo/like.ts b/packages/backend/src/remote/activitypub/kernel/undo/like.ts
new file mode 100644
index 0000000000..107d3053e3
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/undo/like.ts
@@ -0,0 +1,21 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { ILike, getApId } from '../../type';
+import deleteReaction from '@/services/note/reaction/delete';
+import { fetchNote } from '../../models/note';
+
+/**
+ * Process Undo.Like activity
+ */
+export default async (actor: IRemoteUser, activity: ILike) => {
+	const targetUri = getApId(activity.object);
+
+	const note = await fetchNote(targetUri);
+	if (!note) return `skip: target note not found ${targetUri}`;
+
+	await deleteReaction(actor, note).catch(e => {
+		if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') return;
+		throw e;
+	});
+
+	return `ok`;
+};
diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts
new file mode 100644
index 0000000000..52bfc5002e
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/kernel/update/index.ts
@@ -0,0 +1,34 @@
+import { IRemoteUser } from '@/models/entities/user';
+import { getApType, IUpdate, isActor } from '../../type';
+import { apLogger } from '../../logger';
+import { updateQuestion } from '../../models/question';
+import Resolver from '../../resolver';
+import { updatePerson } from '../../models/person';
+
+/**
+ * Updateアクティビティを捌きます
+ */
+export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> => {
+	if ('actor' in activity && actor.uri !== activity.actor) {
+		return `skip: invalid actor`;
+	}
+
+	apLogger.debug('Update');
+
+	const resolver = new Resolver();
+
+	const object = await resolver.resolve(activity.object).catch(e => {
+		apLogger.error(`Resolution failed: ${e}`);
+		throw e;
+	});
+
+	if (isActor(object)) {
+		await updatePerson(actor.uri!, resolver, object);
+		return `ok: Person updated`;
+	} else if (getApType(object) === 'Question') {
+		await updateQuestion(object).catch(e => console.log(e));
+		return `ok: Question updated`;
+	} else {
+		return `skip: Unknown type: ${getApType(object)}`;
+	}
+};
diff --git a/packages/backend/src/remote/activitypub/logger.ts b/packages/backend/src/remote/activitypub/logger.ts
new file mode 100644
index 0000000000..e13add01db
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/logger.ts
@@ -0,0 +1,3 @@
+import { remoteLogger } from '../logger';
+
+export const apLogger = remoteLogger.createSubLogger('ap', 'magenta');
diff --git a/packages/backend/src/remote/activitypub/misc/contexts.ts b/packages/backend/src/remote/activitypub/misc/contexts.ts
new file mode 100644
index 0000000000..1426ba15f5
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/misc/contexts.ts
@@ -0,0 +1,526 @@
+/* tslint:disable:quotemark indent */
+const id_v1 = {
+  "@context": {
+    "id": "@id",
+    "type": "@type",
+
+    "cred": "https://w3id.org/credentials#",
+    "dc": "http://purl.org/dc/terms/",
+    "identity": "https://w3id.org/identity#",
+    "perm": "https://w3id.org/permissions#",
+    "ps": "https://w3id.org/payswarm#",
+    "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+    "sec": "https://w3id.org/security#",
+    "schema": "http://schema.org/",
+    "xsd": "http://www.w3.org/2001/XMLSchema#",
+
+    "Group": "https://www.w3.org/ns/activitystreams#Group",
+
+    "claim": {"@id": "cred:claim", "@type": "@id"},
+    "credential": {"@id": "cred:credential", "@type": "@id"},
+    "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"},
+    "issuer": {"@id": "cred:issuer", "@type": "@id"},
+    "recipient": {"@id": "cred:recipient", "@type": "@id"},
+    "Credential": "cred:Credential",
+    "CryptographicKeyCredential": "cred:CryptographicKeyCredential",
+
+    "about": {"@id": "schema:about", "@type": "@id"},
+    "address": {"@id": "schema:address", "@type": "@id"},
+    "addressCountry": "schema:addressCountry",
+    "addressLocality": "schema:addressLocality",
+    "addressRegion": "schema:addressRegion",
+    "comment": "rdfs:comment",
+    "created": {"@id": "dc:created", "@type": "xsd:dateTime"},
+    "creator": {"@id": "dc:creator", "@type": "@id"},
+    "description": "schema:description",
+    "email": "schema:email",
+    "familyName": "schema:familyName",
+    "givenName": "schema:givenName",
+    "image": {"@id": "schema:image", "@type": "@id"},
+    "label": "rdfs:label",
+    "name": "schema:name",
+    "postalCode": "schema:postalCode",
+    "streetAddress": "schema:streetAddress",
+    "title": "dc:title",
+    "url": {"@id": "schema:url", "@type": "@id"},
+    "Person": "schema:Person",
+    "PostalAddress": "schema:PostalAddress",
+    "Organization": "schema:Organization",
+
+    "identityService": {"@id": "identity:identityService", "@type": "@id"},
+    "idp": {"@id": "identity:idp", "@type": "@id"},
+    "Identity": "identity:Identity",
+
+    "paymentProcessor": "ps:processor",
+    "preferences": {"@id": "ps:preferences", "@type": "@vocab"},
+
+    "cipherAlgorithm": "sec:cipherAlgorithm",
+    "cipherData": "sec:cipherData",
+    "cipherKey": "sec:cipherKey",
+    "digestAlgorithm": "sec:digestAlgorithm",
+    "digestValue": "sec:digestValue",
+    "domain": "sec:domain",
+    "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
+    "initializationVector": "sec:initializationVector",
+    "member": {"@id": "schema:member", "@type": "@id"},
+    "memberOf": {"@id": "schema:memberOf", "@type": "@id"},
+    "nonce": "sec:nonce",
+    "normalizationAlgorithm": "sec:normalizationAlgorithm",
+    "owner": {"@id": "sec:owner", "@type": "@id"},
+    "password": "sec:password",
+    "privateKey": {"@id": "sec:privateKey", "@type": "@id"},
+    "privateKeyPem": "sec:privateKeyPem",
+    "publicKey": {"@id": "sec:publicKey", "@type": "@id"},
+    "publicKeyPem": "sec:publicKeyPem",
+    "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"},
+    "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
+    "signature": "sec:signature",
+    "signatureAlgorithm": "sec:signatureAlgorithm",
+    "signatureValue": "sec:signatureValue",
+    "CryptographicKey": "sec:Key",
+    "EncryptedMessage": "sec:EncryptedMessage",
+    "GraphSignature2012": "sec:GraphSignature2012",
+    "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
+
+    "accessControl": {"@id": "perm:accessControl", "@type": "@id"},
+    "writePermission": {"@id": "perm:writePermission", "@type": "@id"}
+  }
+};
+
+const security_v1 = {
+  "@context": {
+    "id": "@id",
+    "type": "@type",
+
+    "dc": "http://purl.org/dc/terms/",
+    "sec": "https://w3id.org/security#",
+    "xsd": "http://www.w3.org/2001/XMLSchema#",
+
+    "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016",
+    "Ed25519Signature2018": "sec:Ed25519Signature2018",
+    "EncryptedMessage": "sec:EncryptedMessage",
+    "GraphSignature2012": "sec:GraphSignature2012",
+    "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
+    "LinkedDataSignature2016": "sec:LinkedDataSignature2016",
+    "CryptographicKey": "sec:Key",
+
+    "authenticationTag": "sec:authenticationTag",
+    "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm",
+    "cipherAlgorithm": "sec:cipherAlgorithm",
+    "cipherData": "sec:cipherData",
+    "cipherKey": "sec:cipherKey",
+    "created": {"@id": "dc:created", "@type": "xsd:dateTime"},
+    "creator": {"@id": "dc:creator", "@type": "@id"},
+    "digestAlgorithm": "sec:digestAlgorithm",
+    "digestValue": "sec:digestValue",
+    "domain": "sec:domain",
+    "encryptionKey": "sec:encryptionKey",
+    "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
+    "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
+    "initializationVector": "sec:initializationVector",
+    "iterationCount": "sec:iterationCount",
+    "nonce": "sec:nonce",
+    "normalizationAlgorithm": "sec:normalizationAlgorithm",
+    "owner": {"@id": "sec:owner", "@type": "@id"},
+    "password": "sec:password",
+    "privateKey": {"@id": "sec:privateKey", "@type": "@id"},
+    "privateKeyPem": "sec:privateKeyPem",
+    "publicKey": {"@id": "sec:publicKey", "@type": "@id"},
+    "publicKeyBase58": "sec:publicKeyBase58",
+    "publicKeyPem": "sec:publicKeyPem",
+    "publicKeyWif": "sec:publicKeyWif",
+    "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"},
+    "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
+    "salt": "sec:salt",
+    "signature": "sec:signature",
+    "signatureAlgorithm": "sec:signingAlgorithm",
+    "signatureValue": "sec:signatureValue"
+  }
+};
+
+const activitystreams = {
+  "@context": {
+    "@vocab": "_:",
+    "xsd": "http://www.w3.org/2001/XMLSchema#",
+    "as": "https://www.w3.org/ns/activitystreams#",
+    "ldp": "http://www.w3.org/ns/ldp#",
+    "vcard": "http://www.w3.org/2006/vcard/ns#",
+    "id": "@id",
+    "type": "@type",
+    "Accept": "as:Accept",
+    "Activity": "as:Activity",
+    "IntransitiveActivity": "as:IntransitiveActivity",
+    "Add": "as:Add",
+    "Announce": "as:Announce",
+    "Application": "as:Application",
+    "Arrive": "as:Arrive",
+    "Article": "as:Article",
+    "Audio": "as:Audio",
+    "Block": "as:Block",
+    "Collection": "as:Collection",
+    "CollectionPage": "as:CollectionPage",
+    "Relationship": "as:Relationship",
+    "Create": "as:Create",
+    "Delete": "as:Delete",
+    "Dislike": "as:Dislike",
+    "Document": "as:Document",
+    "Event": "as:Event",
+    "Follow": "as:Follow",
+    "Flag": "as:Flag",
+    "Group": "as:Group",
+    "Ignore": "as:Ignore",
+    "Image": "as:Image",
+    "Invite": "as:Invite",
+    "Join": "as:Join",
+    "Leave": "as:Leave",
+    "Like": "as:Like",
+    "Link": "as:Link",
+    "Mention": "as:Mention",
+    "Note": "as:Note",
+    "Object": "as:Object",
+    "Offer": "as:Offer",
+    "OrderedCollection": "as:OrderedCollection",
+    "OrderedCollectionPage": "as:OrderedCollectionPage",
+    "Organization": "as:Organization",
+    "Page": "as:Page",
+    "Person": "as:Person",
+    "Place": "as:Place",
+    "Profile": "as:Profile",
+    "Question": "as:Question",
+    "Reject": "as:Reject",
+    "Remove": "as:Remove",
+    "Service": "as:Service",
+    "TentativeAccept": "as:TentativeAccept",
+    "TentativeReject": "as:TentativeReject",
+    "Tombstone": "as:Tombstone",
+    "Undo": "as:Undo",
+    "Update": "as:Update",
+    "Video": "as:Video",
+    "View": "as:View",
+    "Listen": "as:Listen",
+    "Read": "as:Read",
+    "Move": "as:Move",
+    "Travel": "as:Travel",
+    "IsFollowing": "as:IsFollowing",
+    "IsFollowedBy": "as:IsFollowedBy",
+    "IsContact": "as:IsContact",
+    "IsMember": "as:IsMember",
+    "subject": {
+      "@id": "as:subject",
+      "@type": "@id"
+    },
+    "relationship": {
+      "@id": "as:relationship",
+      "@type": "@id"
+    },
+    "actor": {
+      "@id": "as:actor",
+      "@type": "@id"
+    },
+    "attributedTo": {
+      "@id": "as:attributedTo",
+      "@type": "@id"
+    },
+    "attachment": {
+      "@id": "as:attachment",
+      "@type": "@id"
+    },
+    "bcc": {
+      "@id": "as:bcc",
+      "@type": "@id"
+    },
+    "bto": {
+      "@id": "as:bto",
+      "@type": "@id"
+    },
+    "cc": {
+      "@id": "as:cc",
+      "@type": "@id"
+    },
+    "context": {
+      "@id": "as:context",
+      "@type": "@id"
+    },
+    "current": {
+      "@id": "as:current",
+      "@type": "@id"
+    },
+    "first": {
+      "@id": "as:first",
+      "@type": "@id"
+    },
+    "generator": {
+      "@id": "as:generator",
+      "@type": "@id"
+    },
+    "icon": {
+      "@id": "as:icon",
+      "@type": "@id"
+    },
+    "image": {
+      "@id": "as:image",
+      "@type": "@id"
+    },
+    "inReplyTo": {
+      "@id": "as:inReplyTo",
+      "@type": "@id"
+    },
+    "items": {
+      "@id": "as:items",
+      "@type": "@id"
+    },
+    "instrument": {
+      "@id": "as:instrument",
+      "@type": "@id"
+    },
+    "orderedItems": {
+      "@id": "as:items",
+      "@type": "@id",
+      "@container": "@list"
+    },
+    "last": {
+      "@id": "as:last",
+      "@type": "@id"
+    },
+    "location": {
+      "@id": "as:location",
+      "@type": "@id"
+    },
+    "next": {
+      "@id": "as:next",
+      "@type": "@id"
+    },
+    "object": {
+      "@id": "as:object",
+      "@type": "@id"
+    },
+    "oneOf": {
+      "@id": "as:oneOf",
+      "@type": "@id"
+    },
+    "anyOf": {
+      "@id": "as:anyOf",
+      "@type": "@id"
+    },
+    "closed": {
+      "@id": "as:closed",
+      "@type": "xsd:dateTime"
+    },
+    "origin": {
+      "@id": "as:origin",
+      "@type": "@id"
+    },
+    "accuracy": {
+      "@id": "as:accuracy",
+      "@type": "xsd:float"
+    },
+    "prev": {
+      "@id": "as:prev",
+      "@type": "@id"
+    },
+    "preview": {
+      "@id": "as:preview",
+      "@type": "@id"
+    },
+    "replies": {
+      "@id": "as:replies",
+      "@type": "@id"
+    },
+    "result": {
+      "@id": "as:result",
+      "@type": "@id"
+    },
+    "audience": {
+      "@id": "as:audience",
+      "@type": "@id"
+    },
+    "partOf": {
+      "@id": "as:partOf",
+      "@type": "@id"
+    },
+    "tag": {
+      "@id": "as:tag",
+      "@type": "@id"
+    },
+    "target": {
+      "@id": "as:target",
+      "@type": "@id"
+    },
+    "to": {
+      "@id": "as:to",
+      "@type": "@id"
+    },
+    "url": {
+      "@id": "as:url",
+      "@type": "@id"
+    },
+    "altitude": {
+      "@id": "as:altitude",
+      "@type": "xsd:float"
+    },
+    "content": "as:content",
+    "contentMap": {
+      "@id": "as:content",
+      "@container": "@language"
+    },
+    "name": "as:name",
+    "nameMap": {
+      "@id": "as:name",
+      "@container": "@language"
+    },
+    "duration": {
+      "@id": "as:duration",
+      "@type": "xsd:duration"
+    },
+    "endTime": {
+      "@id": "as:endTime",
+      "@type": "xsd:dateTime"
+    },
+    "height": {
+      "@id": "as:height",
+      "@type": "xsd:nonNegativeInteger"
+    },
+    "href": {
+      "@id": "as:href",
+      "@type": "@id"
+    },
+    "hreflang": "as:hreflang",
+    "latitude": {
+      "@id": "as:latitude",
+      "@type": "xsd:float"
+    },
+    "longitude": {
+      "@id": "as:longitude",
+      "@type": "xsd:float"
+    },
+    "mediaType": "as:mediaType",
+    "published": {
+      "@id": "as:published",
+      "@type": "xsd:dateTime"
+    },
+    "radius": {
+      "@id": "as:radius",
+      "@type": "xsd:float"
+    },
+    "rel": "as:rel",
+    "startIndex": {
+      "@id": "as:startIndex",
+      "@type": "xsd:nonNegativeInteger"
+    },
+    "startTime": {
+      "@id": "as:startTime",
+      "@type": "xsd:dateTime"
+    },
+    "summary": "as:summary",
+    "summaryMap": {
+      "@id": "as:summary",
+      "@container": "@language"
+    },
+    "totalItems": {
+      "@id": "as:totalItems",
+      "@type": "xsd:nonNegativeInteger"
+    },
+    "units": "as:units",
+    "updated": {
+      "@id": "as:updated",
+      "@type": "xsd:dateTime"
+    },
+    "width": {
+      "@id": "as:width",
+      "@type": "xsd:nonNegativeInteger"
+    },
+    "describes": {
+      "@id": "as:describes",
+      "@type": "@id"
+    },
+    "formerType": {
+      "@id": "as:formerType",
+      "@type": "@id"
+    },
+    "deleted": {
+      "@id": "as:deleted",
+      "@type": "xsd:dateTime"
+    },
+    "inbox": {
+      "@id": "ldp:inbox",
+      "@type": "@id"
+    },
+    "outbox": {
+      "@id": "as:outbox",
+      "@type": "@id"
+    },
+    "following": {
+      "@id": "as:following",
+      "@type": "@id"
+    },
+    "followers": {
+      "@id": "as:followers",
+      "@type": "@id"
+    },
+    "streams": {
+      "@id": "as:streams",
+      "@type": "@id"
+    },
+    "preferredUsername": "as:preferredUsername",
+    "endpoints": {
+      "@id": "as:endpoints",
+      "@type": "@id"
+    },
+    "uploadMedia": {
+      "@id": "as:uploadMedia",
+      "@type": "@id"
+    },
+    "proxyUrl": {
+      "@id": "as:proxyUrl",
+      "@type": "@id"
+    },
+    "liked": {
+      "@id": "as:liked",
+      "@type": "@id"
+    },
+    "oauthAuthorizationEndpoint": {
+      "@id": "as:oauthAuthorizationEndpoint",
+      "@type": "@id"
+    },
+    "oauthTokenEndpoint": {
+      "@id": "as:oauthTokenEndpoint",
+      "@type": "@id"
+    },
+    "provideClientKey": {
+      "@id": "as:provideClientKey",
+      "@type": "@id"
+    },
+    "signClientKey": {
+      "@id": "as:signClientKey",
+      "@type": "@id"
+    },
+    "sharedInbox": {
+      "@id": "as:sharedInbox",
+      "@type": "@id"
+    },
+    "Public": {
+      "@id": "as:Public",
+      "@type": "@id"
+    },
+    "source": "as:source",
+    "likes": {
+      "@id": "as:likes",
+      "@type": "@id"
+    },
+    "shares": {
+      "@id": "as:shares",
+      "@type": "@id"
+    },
+    "alsoKnownAs": {
+      "@id": "as:alsoKnownAs",
+      "@type": "@id"
+    }
+  }
+};
+
+export const CONTEXTS: Record<string, any> = {
+  "https://w3id.org/identity/v1": id_v1,
+  "https://w3id.org/security/v1": security_v1,
+  "https://www.w3.org/ns/activitystreams": activitystreams,
+};
diff --git a/packages/backend/src/remote/activitypub/misc/get-note-html.ts b/packages/backend/src/remote/activitypub/misc/get-note-html.ts
new file mode 100644
index 0000000000..043335a5be
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/misc/get-note-html.ts
@@ -0,0 +1,10 @@
+import * as mfm from 'mfm-js';
+import { Note } from '@/models/entities/note';
+import { toHtml } from '../../../mfm/to-html';
+
+export default function(note: Note) {
+	let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null;
+	if (html == null) html = '<p>.</p>';
+
+	return html;
+}
diff --git a/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts b/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts
new file mode 100644
index 0000000000..5cca04df21
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts
@@ -0,0 +1,9 @@
+import { IObject } from '../type';
+import { extractApHashtagObjects } from '../models/tag';
+import { fromHtml } from '../../../mfm/from-html';
+
+export function htmlToMfm(html: string, tag?: IObject | IObject[]) {
+	const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
+
+	return fromHtml(html, hashtagNames);
+}
diff --git a/packages/backend/src/remote/activitypub/misc/ld-signature.ts b/packages/backend/src/remote/activitypub/misc/ld-signature.ts
new file mode 100644
index 0000000000..dec07ea81b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/misc/ld-signature.ts
@@ -0,0 +1,134 @@
+import * as crypto from 'crypto';
+import * as jsonld from 'jsonld';
+import { CONTEXTS } from './contexts';
+import fetch from 'node-fetch';
+import { httpAgent, httpsAgent } from '@/misc/fetch';
+
+// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
+
+export class LdSignature {
+	public debug = false;
+	public preLoad = true;
+	public loderTimeout = 10 * 1000;
+
+	constructor() {
+	}
+
+	public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
+		const options = {
+			type: 'RsaSignature2017',
+			creator,
+			domain,
+			nonce: crypto.randomBytes(16).toString('hex'),
+			created: (created || new Date()).toISOString()
+		} as {
+			type: string;
+			creator: string;
+			domain: string;
+			nonce: string;
+			created: string;
+		};
+
+		if (!domain) {
+			delete options.domain;
+		}
+
+		const toBeSigned = await this.createVerifyData(data, options);
+
+		const signer = crypto.createSign('sha256');
+		signer.update(toBeSigned);
+		signer.end();
+
+		const signature = signer.sign(privateKey);
+
+		return {
+			...data,
+			signature: {
+				...options,
+				signatureValue: signature.toString('base64')
+			}
+		};
+	}
+
+	public async verifyRsaSignature2017(data: any, publicKey: string): Promise<boolean> {
+		const toBeSigned = await this.createVerifyData(data, data.signature);
+		const verifier = crypto.createVerify('sha256');
+		verifier.update(toBeSigned);
+		return verifier.verify(publicKey, data.signature.signatureValue, 'base64');
+	}
+
+	public async createVerifyData(data: any, options: any) {
+		const transformedOptions = {
+			...options,
+			'@context': 'https://w3id.org/identity/v1'
+		};
+		delete transformedOptions['type'];
+		delete transformedOptions['id'];
+		delete transformedOptions['signatureValue'];
+		const canonizedOptions = await this.normalize(transformedOptions);
+		const optionsHash = this.sha256(canonizedOptions);
+		const transformedData = { ...data };
+		delete transformedData['signature'];
+		const cannonidedData = await this.normalize(transformedData);
+		if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
+		const documentHash = this.sha256(cannonidedData);
+		const verifyData = `${optionsHash}${documentHash}`;
+		return verifyData;
+	}
+
+	public async normalize(data: any) {
+		const customLoader = this.getLoader();
+		return await jsonld.normalize(data, {
+			documentLoader: customLoader
+		});
+	}
+
+	private getLoader() {
+		return async (url: string): Promise<any> => {
+			if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`;
+
+			if (this.preLoad) {
+				if (url in CONTEXTS) {
+					if (this.debug) console.debug(`HIT: ${url}`);
+					return {
+						contextUrl: null,
+						document: CONTEXTS[url],
+						documentUrl: url
+					};
+				}
+			}
+
+			if (this.debug) console.debug(`MISS: ${url}`);
+			const document = await this.fetchDocument(url);
+			return {
+				contextUrl: null,
+				document: document,
+				documentUrl: url
+			};
+		};
+	}
+
+	private async fetchDocument(url: string) {
+		const json = await fetch(url, {
+			headers: {
+				Accept: 'application/ld+json, application/json',
+			},
+			timeout: this.loderTimeout,
+			agent: u => u.protocol == 'http:' ? httpAgent : httpsAgent,
+		}).then(res => {
+			if (!res.ok) {
+				throw `${res.status} ${res.statusText}`;
+			} else {
+				return res.json();
+			}
+		});
+
+		return json;
+	}
+
+	public sha256(data: string): string {
+		const hash = crypto.createHash('sha256');
+		hash.update(data);
+		return hash.digest('hex');
+	}
+}
diff --git a/packages/backend/src/remote/activitypub/models/icon.ts b/packages/backend/src/remote/activitypub/models/icon.ts
new file mode 100644
index 0000000000..50794a937d
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/models/icon.ts
@@ -0,0 +1,5 @@
+export type IIcon = {
+	type: string;
+	mediaType?: string;
+	url?: string;
+};
diff --git a/packages/backend/src/remote/activitypub/models/identifier.ts b/packages/backend/src/remote/activitypub/models/identifier.ts
new file mode 100644
index 0000000000..f6c3bb8c88
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/models/identifier.ts
@@ -0,0 +1,5 @@
+export type IIdentifier = {
+	type: string;
+	name: string;
+	value: string;
+};
diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts
new file mode 100644
index 0000000000..d0a96e4313
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/models/image.ts
@@ -0,0 +1,62 @@
+import uploadFromUrl from '@/services/drive/upload-from-url';
+import { IRemoteUser } from '@/models/entities/user';
+import Resolver from '../resolver';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { apLogger } from '../logger';
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFiles } from '@/models/index';
+import { truncate } from '@/misc/truncate';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
+
+const logger = apLogger;
+
+/**
+ * Imageを作成します。
+ */
+export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
+	// 投稿者が凍結されていたらスキップ
+	if (actor.isSuspended) {
+		throw new Error('actor has been suspended');
+	}
+
+	const image = await new Resolver().resolve(value) as any;
+
+	if (image.url == null) {
+		throw new Error('invalid image: url not privided');
+	}
+
+	logger.info(`Creating the Image: ${image.url}`);
+
+	const instance = await fetchMeta();
+	const cache = instance.cacheRemoteFiles;
+
+	let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH));
+
+	if (file.isLink) {
+		// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
+		// URLを更新する
+		if (file.url !== image.url) {
+			await DriveFiles.update({ id: file.id }, {
+				url: image.url,
+				uri: image.url
+			});
+
+			file = await DriveFiles.findOneOrFail(file.id);
+		}
+	}
+
+	return file;
+}
+
+/**
+ * Imageを解決します。
+ *
+ * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
+ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ */
+export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
+	// TODO
+
+	// リモートサーバーからフェッチしてきて登録
+	return await createImage(actor, value);
+}
diff --git a/packages/backend/src/remote/activitypub/models/mention.ts b/packages/backend/src/remote/activitypub/models/mention.ts
new file mode 100644
index 0000000000..ade9c90806
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/models/mention.ts
@@ -0,0 +1,24 @@
+import { toArray, unique } from '@/prelude/array';
+import { IObject, isMention, IApMention } from '../type';
+import { resolvePerson } from './person';
+import * as promiseLimit from 'promise-limit';
+import Resolver from '../resolver';
+import { User } from '@/models/entities/user';
+
+export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
+	const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
+
+	const resolver = new Resolver();
+
+	const limit = promiseLimit<User | null>(2);
+	const mentionedUsers = (await Promise.all(
+		hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
+	)).filter((x): x is User => x != null);
+
+	return mentionedUsers;
+}
+
+export function extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
+	if (tags == null) return [];
+	return toArray(tags).filter(isMention);
+}
diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts
new file mode 100644
index 0000000000..492dc05248
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/models/note.ts
@@ -0,0 +1,356 @@
+import * as promiseLimit from 'promise-limit';
+
+import config from '@/config/index';
+import Resolver from '../resolver';
+import post from '@/services/note/create';
+import { resolvePerson, updatePerson } from './person';
+import { resolveImage } from './image';
+import { IRemoteUser } from '@/models/entities/user';
+import { htmlToMfm } from '../misc/html-to-mfm';
+import { extractApHashtags } from './tag';
+import { unique, toArray, toSingle } from '@/prelude/array';
+import { extractPollFromQuestion } from './question';
+import vote from '@/services/note/polls/vote';
+import { apLogger } from '../logger';
+import { DriveFile } from '@/models/entities/drive-file';
+import { deliverQuestionUpdate } from '@/services/note/polls/update';
+import { extractDbHost, toPuny } from '@/misc/convert-host';
+import { Emojis, Polls, MessagingMessages } from '@/models/index';
+import { Note } from '@/models/entities/note';
+import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type';
+import { Emoji } from '@/models/entities/emoji';
+import { genId } from '@/misc/gen-id';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { getApLock } from '@/misc/app-lock';
+import { createMessage } from '@/services/messages/create';
+import { parseAudience } from '../audience';
+import { extractApMentions } from './mention';
+import DbResolver from '../db-resolver';
+import { StatusError } from '@/misc/fetch';
+
+const logger = apLogger;
+
+export function validateNote(object: any, uri: string) {
+	const expectHost = extractDbHost(uri);
+
+	if (object == null) {
+		return new Error('invalid Note: object is null');
+	}
+
+	if (!validPost.includes(getApType(object))) {
+		return new Error(`invalid Note: invalid object type ${getApType(object)}`);
+	}
+
+	if (object.id && extractDbHost(object.id) !== expectHost) {
+		return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractDbHost(object.id)}`);
+	}
+
+	if (object.attributedTo && extractDbHost(getOneApId(object.attributedTo)) !== expectHost) {
+		return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractDbHost(object.attributedTo)}`);
+	}
+
+	return null;
+}
+
+/**
+ * Noteをフェッチします。
+ *
+ * Misskeyに対象のNoteが登録されていればそれを返します。
+ */
+export async function fetchNote(object: string | IObject): Promise<Note | null> {
+	const dbResolver = new DbResolver();
+	return await dbResolver.getNoteFromApId(object);
+}
+
+/**
+ * Noteを作成します。
+ */
+export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
+	if (resolver == null) resolver = new Resolver();
+
+	const object: any = await resolver.resolve(value);
+
+	const entryUri = getApId(value);
+	const err = validateNote(object, entryUri);
+	if (err) {
+		logger.error(`${err.message}`, {
+			resolver: {
+				history: resolver.getHistory()
+			},
+			value: value,
+			object: object
+		});
+		throw new Error('invalid note');
+	}
+
+	const note: IPost = object;
+
+	logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
+
+	logger.info(`Creating the Note: ${note.id}`);
+
+	// 投稿者をフェッチ
+	const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser;
+
+	// 投稿者が凍結されていたらスキップ
+	if (actor.isSuspended) {
+		throw new Error('actor has been suspended');
+	}
+
+	const noteAudience = await parseAudience(actor, note.to, note.cc);
+	let visibility = noteAudience.visibility;
+	const visibleUsers = noteAudience.visibleUsers;
+
+	// Audience (to, cc) が指定されてなかった場合
+	if (visibility === 'specified' && visibleUsers.length === 0) {
+		if (typeof value === 'string') {	// 入力がstringならばresolverでGETが発生している
+			// こちらから匿名GET出来たものならばpublic
+			visibility = 'public';
+		}
+	}
+
+	let isTalk = note._misskey_talk && visibility === 'specified';
+
+	const apMentions = await extractApMentions(note.tag);
+	const apHashtags = await extractApHashtags(note.tag);
+
+	// 添付ファイル
+	// TODO: attachmentは必ずしもImageではない
+	// TODO: attachmentは必ずしも配列ではない
+	// Noteがsensitiveなら添付もsensitiveにする
+	const limit = promiseLimit(2);
+
+	note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
+	const files = note.attachment
+		.map(attach => attach.sensitive = note.sensitive)
+		? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>)))
+			.filter(image => image != null)
+		: [];
+
+	// リプライ
+	const reply: Note | null = note.inReplyTo
+		? await resolveNote(note.inReplyTo, resolver).then(x => {
+			if (x == null) {
+				logger.warn(`Specified inReplyTo, but nout found`);
+				throw new Error('inReplyTo not found');
+			} else {
+				return x;
+			}
+		}).catch(async e => {
+			// トークだったらinReplyToのエラーは無視
+			const uri = getApId(note.inReplyTo);
+			if (uri.startsWith(config.url + '/')) {
+				const id = uri.split('/').pop();
+				const talk = await MessagingMessages.findOne(id);
+				if (talk) {
+					isTalk = true;
+					return null;
+				}
+			}
+
+			logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
+			throw e;
+		})
+		: null;
+
+	// 引用
+	let quote: Note | undefined | null;
+
+	if (note._misskey_quote || note.quoteUrl) {
+		const tryResolveNote = async (uri: string): Promise<{
+			status: 'ok';
+			res: Note | null;
+		} | {
+			status: 'permerror' | 'temperror';
+		}> => {
+			if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
+			try {
+				const res = await resolveNote(uri);
+				if (res) {
+					return {
+						status: 'ok',
+						res
+					};
+				} else {
+					return {
+						status: 'permerror'
+					};
+				}
+			} catch (e) {
+				return {
+					status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror'
+				};
+			}
+		};
+
+		const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
+		const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
+
+		quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
+		if (!quote) {
+			if (results.some(x => x.status === 'temperror')) {
+				throw 'quote resolve failed';
+			}
+		}
+	}
+
+	const cw = note.summary === '' ? null : note.summary;
+
+	// テキストのパース
+	const text = note._misskey_content || (note.content ? htmlToMfm(note.content, note.tag) : null);
+
+	// vote
+	if (reply && reply.hasPoll) {
+		const poll = await Polls.findOneOrFail(reply.id);
+
+		const tryCreateVote = async (name: string, index: number): Promise<null> => {
+			if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
+				logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
+			} else if (index >= 0) {
+				logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
+				await vote(actor, reply, index);
+
+				// リモートフォロワーにUpdate配信
+				deliverQuestionUpdate(reply.id);
+			}
+			return null;
+		};
+
+		if (note.name) {
+			return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
+		}
+	}
+
+	const emojis = await extractEmojis(note.tag || [], actor.host).catch(e => {
+		logger.info(`extractEmojis: ${e}`);
+		return [] as Emoji[];
+	});
+
+	const apEmojis = emojis.map(emoji => emoji.name);
+
+	const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined);
+
+	// ユーザーの情報が古かったらついでに更新しておく
+	if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
+		if (actor.uri) updatePerson(actor.uri);
+	}
+
+	if (isTalk) {
+		for (const recipient of visibleUsers) {
+			await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id);
+			return null;
+		}
+	}
+
+	return await post(actor, {
+		createdAt: note.published ? new Date(note.published) : null,
+		files,
+		reply,
+		renote: quote,
+		name: note.name,
+		cw,
+		text,
+		viaMobile: false,
+		localOnly: false,
+		visibility,
+		visibleUsers,
+		apMentions,
+		apHashtags,
+		apEmojis,
+		poll,
+		uri: note.id,
+		url: getOneApHrefNullable(note.url),
+	}, silent);
+}
+
+/**
+ * Noteを解決します。
+ *
+ * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ
+ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ */
+export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
+	const uri = typeof value === 'string' ? value : value.id;
+	if (uri == null) throw new Error('missing uri');
+
+	// ブロックしてたら中断
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 };
+
+	const unlock = await getApLock(uri);
+
+	try {
+		//#region このサーバーに既に登録されていたらそれを返す
+		const exist = await fetchNote(uri);
+
+		if (exist) {
+			return exist;
+		}
+		//#endregion
+
+		if (uri.startsWith(config.url)) {
+			throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
+		}
+
+		// リモートサーバーからフェッチしてきて登録
+		// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
+		// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
+		return await createNote(uri, resolver, true);
+	} finally {
+		unlock();
+	}
+}
+
+export async function extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
+	host = toPuny(host);
+
+	if (!tags) return [];
+
+	const eomjiTags = toArray(tags).filter(isEmoji);
+
+	return await Promise.all(eomjiTags.map(async tag => {
+		const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
+		tag.icon = toSingle(tag.icon);
+
+		const exists = await Emojis.findOne({
+			host,
+			name
+		});
+
+		if (exists) {
+			if ((tag.updated != null && exists.updatedAt == null)
+				|| (tag.id != null && exists.uri == null)
+				|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
+				|| (tag.icon!.url !== exists.url)
+			) {
+				await Emojis.update({
+					host,
+					name,
+				}, {
+					uri: tag.id,
+					url: tag.icon!.url,
+					updatedAt: new Date(),
+				});
+
+				return await Emojis.findOne({
+					host,
+					name
+				}) as Emoji;
+			}
+
+			return exists;
+		}
+
+		logger.info(`register emoji host=${host}, name=${name}`);
+
+		return await Emojis.save({
+			id: genId(),
+			host,
+			name,
+			uri: tag.id,
+			url: tag.icon!.url,
+			updatedAt: new Date(),
+			aliases: []
+		} as Partial<Emoji>);
+	}));
+}
diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
new file mode 100644
index 0000000000..eb8c00a10b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -0,0 +1,494 @@
+import { URL } from 'url';
+import * as promiseLimit from 'promise-limit';
+
+import $, { Context } from 'cafy';
+import config from '@/config/index';
+import Resolver from '../resolver';
+import { resolveImage } from './image';
+import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type';
+import { fromHtml } from '../../../mfm/from-html';
+import { htmlToMfm } from '../misc/html-to-mfm';
+import { resolveNote, extractEmojis } from './note';
+import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc';
+import { extractApHashtags } from './tag';
+import { apLogger } from '../logger';
+import { Note } from '@/models/entities/note';
+import { updateUsertags } from '@/services/update-hashtag';
+import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index';
+import { User, IRemoteUser } from '@/models/entities/user';
+import { Emoji } from '@/models/entities/emoji';
+import { UserNotePining } from '@/models/entities/user-note-pining';
+import { genId } from '@/misc/gen-id';
+import { instanceChart, usersChart } from '@/services/chart/index';
+import { UserPublickey } from '@/models/entities/user-publickey';
+import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
+import { toPuny } from '@/misc/convert-host';
+import { UserProfile } from '@/models/entities/user-profile';
+import { getConnection } from 'typeorm';
+import { toArray } from '@/prelude/array';
+import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+import { truncate } from '@/misc/truncate';
+import { StatusError } from '@/misc/fetch';
+
+const logger = apLogger;
+
+const nameLength = 128;
+const summaryLength = 2048;
+
+/**
+ * Validate and convert to actor object
+ * @param x Fetched object
+ * @param uri Fetch target URI
+ */
+function validateActor(x: IObject, uri: string): IActor {
+	const expectHost = toPuny(new URL(uri).hostname);
+
+	if (x == null) {
+		throw new Error('invalid Actor: object is null');
+	}
+
+	if (!isActor(x)) {
+		throw new Error(`invalid Actor type '${x.type}'`);
+	}
+
+	const validate = (name: string, value: any, validater: Context) => {
+		const e = validater.test(value);
+		if (e) throw new Error(`invalid Actor: ${name} ${e.message}`);
+	};
+
+	validate('id', x.id, $.str.min(1));
+	validate('inbox', x.inbox, $.str.min(1));
+	validate('preferredUsername', x.preferredUsername, $.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/));
+
+	// These fields are only informational, and some AP software allows these
+	// fields to be very long. If they are too long, we cut them off. This way
+	// we can at least see these users and their activities.
+	validate('name', truncate(x.name, nameLength), $.optional.nullable.str);
+	validate('summary', truncate(x.summary, summaryLength), $.optional.nullable.str);
+
+	const idHost = toPuny(new URL(x.id!).hostname);
+	if (idHost !== expectHost) {
+		throw new Error('invalid Actor: id has different host');
+	}
+
+	if (x.publicKey) {
+		if (typeof x.publicKey.id !== 'string') {
+			throw new Error('invalid Actor: publicKey.id is not a string');
+		}
+
+		const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname);
+		if (publicKeyIdHost !== expectHost) {
+			throw new Error('invalid Actor: publicKey.id has different host');
+		}
+	}
+
+	return x;
+}
+
+/**
+ * Personをフェッチします。
+ *
+ * Misskeyに対象のPersonが登録されていればそれを返します。
+ */
+export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
+	if (typeof uri !== 'string') throw new Error('uri is not string');
+
+	// URIがこのサーバーを指しているならデータベースからフェッチ
+	if (uri.startsWith(config.url + '/')) {
+		const id = uri.split('/').pop();
+		return await Users.findOne(id).then(x => x || null);
+	}
+
+	//#region このサーバーに既に登録されていたらそれを返す
+	const exist = await Users.findOne({ uri });
+
+	if (exist) {
+		return exist;
+	}
+	//#endregion
+
+	return null;
+}
+
+/**
+ * Personを作成します。
+ */
+export async function createPerson(uri: string, resolver?: Resolver): Promise<User> {
+	if (typeof uri !== 'string') throw new Error('uri is not string');
+
+	if (uri.startsWith(config.url)) {
+		throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
+	}
+
+	if (resolver == null) resolver = new Resolver();
+
+	const object = await resolver.resolve(uri) as any;
+
+	const person = validateActor(object, uri);
+
+	logger.info(`Creating the Person: ${person.id}`);
+
+	const host = toPuny(new URL(object.id).hostname);
+
+	const { fields } = analyzeAttachments(person.attachment || []);
+
+	const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
+
+	const isBot = getApType(object) === 'Service';
+
+	const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
+
+	// Create user
+	let user: IRemoteUser;
+	try {
+		// Start transaction
+		await getConnection().transaction(async transactionalEntityManager => {
+			user = await transactionalEntityManager.save(new User({
+				id: genId(),
+				avatarId: null,
+				bannerId: null,
+				createdAt: new Date(),
+				lastFetchedAt: new Date(),
+				name: truncate(person.name, nameLength),
+				isLocked: !!person.manuallyApprovesFollowers,
+				isExplorable: !!person.discoverable,
+				username: person.preferredUsername,
+				usernameLower: person.preferredUsername!.toLowerCase(),
+				host,
+				inbox: person.inbox,
+				sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
+				followersUri: person.followers ? getApId(person.followers) : undefined,
+				featured: person.featured ? getApId(person.featured) : undefined,
+				uri: person.id,
+				tags,
+				isBot,
+				isCat: (person as any).isCat === true
+			})) as IRemoteUser;
+
+			await transactionalEntityManager.save(new UserProfile({
+				userId: user.id,
+				description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
+				url: getOneApHrefNullable(person.url),
+				fields,
+				birthday: bday ? bday[0] : null,
+				location: person['vcard:Address'] || null,
+				userHost: host
+			}));
+
+			if (person.publicKey) {
+				await transactionalEntityManager.save(new UserPublickey({
+					userId: user.id,
+					keyId: person.publicKey.id,
+					keyPem: person.publicKey.publicKeyPem
+				}));
+			}
+		});
+	} catch (e) {
+		// duplicate key error
+		if (isDuplicateKeyValueError(e)) {
+			// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
+			const u = await Users.findOne({
+				uri: person.id
+			});
+
+			if (u) {
+				user = u as IRemoteUser;
+			} else {
+				throw new Error('already registered');
+			}
+		} else {
+			logger.error(e);
+			throw e;
+		}
+	}
+
+	// Register host
+	registerOrFetchInstanceDoc(host).then(i => {
+		Instances.increment({ id: i.id }, 'usersCount', 1);
+		instanceChart.newUser(i.host);
+		fetchInstanceMetadata(i);
+	});
+
+	usersChart.update(user!, true);
+
+	// ハッシュタグ更新
+	updateUsertags(user!, tags);
+
+	//#region アバターとヘッダー画像をフェッチ
+	const [avatar, banner] = await Promise.all([
+		person.icon,
+		person.image
+	].map(img =>
+		img == null
+			? Promise.resolve(null)
+			: resolveImage(user!, img).catch(() => null)
+	));
+
+	const avatarId = avatar ? avatar.id : null;
+	const bannerId = banner ? banner.id : null;
+	const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar, true) : null;
+	const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
+	const avatarBlurhash = avatar ? avatar.blurhash : null;
+	const bannerBlurhash = banner ? banner.blurhash : null;
+
+	await Users.update(user!.id, {
+		avatarId,
+		bannerId,
+		avatarUrl,
+		bannerUrl,
+		avatarBlurhash,
+		bannerBlurhash
+	});
+
+	user!.avatarId = avatarId;
+	user!.bannerId = bannerId;
+	user!.avatarUrl = avatarUrl;
+	user!.bannerUrl = bannerUrl;
+	user!.avatarBlurhash = avatarBlurhash;
+	user!.bannerBlurhash = bannerBlurhash;
+	//#endregion
+
+	//#region カスタム絵文字取得
+	const emojis = await extractEmojis(person.tag || [], host).catch(e => {
+		logger.info(`extractEmojis: ${e}`);
+		return [] as Emoji[];
+	});
+
+	const emojiNames = emojis.map(emoji => emoji.name);
+
+	await Users.update(user!.id, {
+		emojis: emojiNames
+	});
+	//#endregion
+
+	await updateFeatured(user!.id).catch(err => logger.error(err));
+
+	return user!;
+}
+
+/**
+ * Personの情報を更新します。
+ * Misskeyに対象のPersonが登録されていなければ無視します。
+ * @param uri URI of Person
+ * @param resolver Resolver
+ * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
+ */
+export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: object): Promise<void> {
+	if (typeof uri !== 'string') throw new Error('uri is not string');
+
+	// URIがこのサーバーを指しているならスキップ
+	if (uri.startsWith(config.url + '/')) {
+		return;
+	}
+
+	//#region このサーバーに既に登録されているか
+	const exist = await Users.findOne({ uri }) as IRemoteUser;
+
+	if (exist == null) {
+		return;
+	}
+	//#endregion
+
+	if (resolver == null) resolver = new Resolver();
+
+	const object = hint || await resolver.resolve(uri) as any;
+
+	const person = validateActor(object, uri);
+
+	logger.info(`Updating the Person: ${person.id}`);
+
+	// アバターとヘッダー画像をフェッチ
+	const [avatar, banner] = await Promise.all([
+		person.icon,
+		person.image
+	].map(img =>
+		img == null
+			? Promise.resolve(null)
+			: resolveImage(exist, img).catch(() => null)
+	));
+
+	// カスタム絵文字取得
+	const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => {
+		logger.info(`extractEmojis: ${e}`);
+		return [] as Emoji[];
+	});
+
+	const emojiNames = emojis.map(emoji => emoji.name);
+
+	const { fields } = analyzeAttachments(person.attachment || []);
+
+	const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
+
+	const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
+
+	const updates = {
+		lastFetchedAt: new Date(),
+		inbox: person.inbox,
+		sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
+		followersUri: person.followers ? getApId(person.followers) : undefined,
+		featured: person.featured,
+		emojis: emojiNames,
+		name: truncate(person.name, nameLength),
+		tags,
+		isBot: getApType(object) === 'Service',
+		isCat: (person as any).isCat === true,
+		isLocked: !!person.manuallyApprovesFollowers,
+		isExplorable: !!person.discoverable,
+	} as Partial<User>;
+
+	if (avatar) {
+		updates.avatarId = avatar.id;
+		updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
+		updates.avatarBlurhash = avatar.blurhash;
+	}
+
+	if (banner) {
+		updates.bannerId = banner.id;
+		updates.bannerUrl = DriveFiles.getPublicUrl(banner);
+		updates.bannerBlurhash = banner.blurhash;
+	}
+
+	// Update user
+	await Users.update(exist.id, updates);
+
+	if (person.publicKey) {
+		await UserPublickeys.update({ userId: exist.id }, {
+			keyId: person.publicKey.id,
+			keyPem: person.publicKey.publicKeyPem
+		});
+	}
+
+	await UserProfiles.update({ userId: exist.id }, {
+		url: getOneApHrefNullable(person.url),
+		fields,
+		description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
+		birthday: bday ? bday[0] : null,
+		location: person['vcard:Address'] || null,
+	});
+
+	// ハッシュタグ更新
+	updateUsertags(exist, tags);
+
+	// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
+	await Followings.update({
+		followerId: exist.id
+	}, {
+		followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
+	});
+
+	await updateFeatured(exist.id).catch(err => logger.error(err));
+}
+
+/**
+ * Personを解決します。
+ *
+ * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
+ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ */
+export async function resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
+	if (typeof uri !== 'string') throw new Error('uri is not string');
+
+	//#region このサーバーに既に登録されていたらそれを返す
+	const exist = await fetchPerson(uri);
+
+	if (exist) {
+		return exist;
+	}
+	//#endregion
+
+	// リモートサーバーからフェッチしてきて登録
+	if (resolver == null) resolver = new Resolver();
+	return await createPerson(uri, resolver);
+}
+
+const services: {
+		[x: string]: (id: string, username: string) => any
+	} = {
+	'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
+	'misskey:authentication:github': (id, login) => ({ id, login }),
+	'misskey:authentication:discord': (id, name) => $discord(id, name)
+};
+
+const $discord = (id: string, name: string) => {
+	if (typeof name !== 'string')
+		name = 'unknown#0000';
+	const [username, discriminator] = name.split('#');
+	return { id, username, discriminator };
+};
+
+function addService(target: { [x: string]: any }, source: IApPropertyValue) {
+	const service = services[source.name];
+
+	if (typeof source.value !== 'string')
+		source.value = 'unknown';
+
+	const [id, username] = source.value.split('@');
+
+	if (service)
+		target[source.name.split(':')[2]] = service(id, username);
+}
+
+export function analyzeAttachments(attachments: IObject | IObject[] | undefined) {
+	const fields: {
+		name: string,
+		value: string
+	}[] = [];
+	const services: { [x: string]: any } = {};
+
+	if (Array.isArray(attachments)) {
+		for (const attachment of attachments.filter(isPropertyValue)) {
+			if (isPropertyValue(attachment.identifier)) {
+				addService(services, attachment.identifier);
+			} else {
+				fields.push({
+					name: attachment.name,
+					value: fromHtml(attachment.value)
+				});
+			}
+		}
+	}
+
+	return { fields, services };
+}
+
+export async function updateFeatured(userId: User['id']) {
+	const user = await Users.findOneOrFail(userId);
+	if (!Users.isRemoteUser(user)) return;
+	if (!user.featured) return;
+
+	logger.info(`Updating the featured: ${user.uri}`);
+
+	const resolver = new Resolver();
+
+	// Resolve to (Ordered)Collection Object
+	const collection = await resolver.resolveCollection(user.featured);
+	if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`);
+
+	// Resolve to Object(may be Note) arrays
+	const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
+	const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x)));
+
+	// Resolve and regist Notes
+	const limit = promiseLimit<Note | null>(2);
+	const featuredNotes = await Promise.all(items
+		.filter(item => getApType(item) === 'Note')	// TODO: Noteでなくてもいいかも
+		.slice(0, 5)
+		.map(item => limit(() => resolveNote(item, resolver))));
+
+	await getConnection().transaction(async transactionalEntityManager => {
+		await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
+
+		// とりあえずidを別の時間で生成して順番を維持
+		let td = 0;
+		for (const note of featuredNotes.filter(note => note != null)) {
+			td -= 1000;
+			transactionalEntityManager.insert(UserNotePining, {
+				id: genId(new Date(Date.now() + td)),
+				createdAt: new Date(),
+				userId: user.id,
+				noteId: note!.id
+			});
+		}
+	});
+}
diff --git a/packages/backend/src/remote/activitypub/models/question.ts b/packages/backend/src/remote/activitypub/models/question.ts
new file mode 100644
index 0000000000..79f93c3a30
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/models/question.ts
@@ -0,0 +1,83 @@
+import config from '@/config/index';
+import Resolver from '../resolver';
+import { IObject, IQuestion, isQuestion,  } from '../type';
+import { apLogger } from '../logger';
+import { Notes, Polls } from '@/models/index';
+import { IPoll } from '@/models/entities/poll';
+
+export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
+	if (resolver == null) resolver = new Resolver();
+
+	const question = await resolver.resolve(source);
+
+	if (!isQuestion(question)) {
+		throw new Error('invalid type');
+	}
+
+	const multiple = !question.oneOf;
+	const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
+
+	if (multiple && !question.anyOf) {
+		throw new Error('invalid question');
+	}
+
+	const choices = question[multiple ? 'anyOf' : 'oneOf']!
+		.map((x, i) => x.name!);
+
+	const votes = question[multiple ? 'anyOf' : 'oneOf']!
+		.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
+
+	return {
+		choices,
+		votes,
+		multiple,
+		expiresAt
+	};
+}
+
+/**
+ * Update votes of Question
+ * @param uri URI of AP Question object
+ * @returns true if updated
+ */
+export async function updateQuestion(value: any) {
+	const uri = typeof value === 'string' ? value : value.id;
+
+	// URIがこのサーバーを指しているならスキップ
+	if (uri.startsWith(config.url + '/')) throw new Error('uri points local');
+
+	//#region このサーバーに既に登録されているか
+	const note = await Notes.findOne({ uri });
+	if (note == null) throw new Error('Question is not registed');
+
+	const poll = await Polls.findOne({ noteId: note.id });
+	if (poll == null) throw new Error('Question is not registed');
+	//#endregion
+
+	// resolve new Question object
+	const resolver = new Resolver();
+	const question = await resolver.resolve(value) as IQuestion;
+	apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
+
+	if (question.type !== 'Question') throw new Error('object is not a Question');
+
+	const apChoices = question.oneOf || question.anyOf;
+
+	let changed = false;
+
+	for (const choice of poll.choices) {
+		const oldCount = poll.votes[poll.choices.indexOf(choice)];
+		const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
+
+		if (oldCount != newCount) {
+			changed = true;
+			poll.votes[poll.choices.indexOf(choice)] = newCount;
+		}
+	}
+
+	await Polls.update({ noteId: note.id }, {
+		votes: poll.votes
+	});
+
+	return changed;
+}
diff --git a/packages/backend/src/remote/activitypub/models/tag.ts b/packages/backend/src/remote/activitypub/models/tag.ts
new file mode 100644
index 0000000000..fbc6b9b428
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/models/tag.ts
@@ -0,0 +1,18 @@
+import { toArray } from '@/prelude/array';
+import { IObject, isHashtag, IApHashtag } from '../type';
+
+export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
+	if (tags == null) return [];
+
+	const hashtags = extractApHashtagObjects(tags);
+
+	return hashtags.map(tag => {
+		const m = tag.name.match(/^#(.+)/);
+		return m ? m[1] : null;
+	}).filter((x): x is string => x != null);
+}
+
+export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
+	if (tags == null) return [];
+	return toArray(tags).filter(isHashtag);
+}
diff --git a/packages/backend/src/remote/activitypub/perform.ts b/packages/backend/src/remote/activitypub/perform.ts
new file mode 100644
index 0000000000..01f0e3676e
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/perform.ts
@@ -0,0 +1,7 @@
+import { IObject } from './type';
+import { IRemoteUser } from '@/models/entities/user';
+import { performActivity } from './kernel/index';
+
+export default async (actor: IRemoteUser, activity: IObject): Promise<void> => {
+	await performActivity(actor, activity);
+};
diff --git a/packages/backend/src/remote/activitypub/renderer/accept.ts b/packages/backend/src/remote/activitypub/renderer/accept.ts
new file mode 100644
index 0000000000..f1e61f4c6a
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/accept.ts
@@ -0,0 +1,8 @@
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+
+export default (object: any, user: { id: User['id']; host: null }) => ({
+	type: 'Accept',
+	actor: `${config.url}/users/${user.id}`,
+	object
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/add.ts b/packages/backend/src/remote/activitypub/renderer/add.ts
new file mode 100644
index 0000000000..21414a9380
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/add.ts
@@ -0,0 +1,9 @@
+import config from '@/config/index';
+import { ILocalUser } from '@/models/entities/user';
+
+export default (user: ILocalUser, target: any, object: any) => ({
+	type: 'Add',
+	actor: `${config.url}/users/${user.id}`,
+	target,
+	object
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/announce.ts b/packages/backend/src/remote/activitypub/renderer/announce.ts
new file mode 100644
index 0000000000..7bf90922be
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/announce.ts
@@ -0,0 +1,29 @@
+import config from '@/config/index';
+import { Note } from '@/models/entities/note';
+
+export default (object: any, note: Note) => {
+	const attributedTo = `${config.url}/users/${note.userId}`;
+
+	let to: string[] = [];
+	let cc: string[] = [];
+
+	if (note.visibility === 'public') {
+		to = ['https://www.w3.org/ns/activitystreams#Public'];
+		cc = [`${attributedTo}/followers`];
+	} else if (note.visibility === 'home') {
+		to = [`${attributedTo}/followers`];
+		cc = ['https://www.w3.org/ns/activitystreams#Public'];
+	} else {
+		return null;
+	}
+
+	return {
+		id: `${config.url}/notes/${note.id}/activity`,
+		actor: `${config.url}/users/${note.userId}`,
+		type: 'Announce',
+		published: note.createdAt.toISOString(),
+		to,
+		cc,
+		object
+	};
+};
diff --git a/packages/backend/src/remote/activitypub/renderer/block.ts b/packages/backend/src/remote/activitypub/renderer/block.ts
new file mode 100644
index 0000000000..bb3d74295a
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/block.ts
@@ -0,0 +1,8 @@
+import config from '@/config/index';
+import { ILocalUser, IRemoteUser } from '@/models/entities/user';
+
+export default (blocker: ILocalUser, blockee: IRemoteUser) => ({
+	type: 'Block',
+	actor: `${config.url}/users/${blocker.id}`,
+	object: blockee.uri
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/create.ts b/packages/backend/src/remote/activitypub/renderer/create.ts
new file mode 100644
index 0000000000..04aa993a91
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/create.ts
@@ -0,0 +1,17 @@
+import config from '@/config/index';
+import { Note } from '@/models/entities/note';
+
+export default (object: any, note: Note) => {
+	const activity = {
+		id: `${config.url}/notes/${note.id}/activity`,
+		actor: `${config.url}/users/${note.userId}`,
+		type: 'Create',
+		published: note.createdAt.toISOString(),
+		object
+	} as any;
+
+	if (object.to) activity.to = object.to;
+	if (object.cc) activity.cc = object.cc;
+
+	return activity;
+};
diff --git a/packages/backend/src/remote/activitypub/renderer/delete.ts b/packages/backend/src/remote/activitypub/renderer/delete.ts
new file mode 100644
index 0000000000..176a6f7e27
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/delete.ts
@@ -0,0 +1,9 @@
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+
+export default (object: any, user: { id: User['id']; host: null }) => ({
+	type: 'Delete',
+	actor: `${config.url}/users/${user.id}`,
+	object,
+	published: new Date().toISOString(),
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/document.ts b/packages/backend/src/remote/activitypub/renderer/document.ts
new file mode 100644
index 0000000000..a9d86dea15
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/document.ts
@@ -0,0 +1,9 @@
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFiles } from '@/models/index';
+
+export default (file: DriveFile) => ({
+	type: 'Document',
+	mediaType: file.type,
+	url: DriveFiles.getPublicUrl(file),
+	name: file.comment,
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/emoji.ts b/packages/backend/src/remote/activitypub/renderer/emoji.ts
new file mode 100644
index 0000000000..ca514c56b5
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/emoji.ts
@@ -0,0 +1,14 @@
+import config from '@/config/index';
+import { Emoji } from '@/models/entities/emoji';
+
+export default (emoji: Emoji) => ({
+	id: `${config.url}/emojis/${emoji.name}`,
+	type: 'Emoji',
+	name: `:${emoji.name}:`,
+	updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString,
+	icon: {
+		type: 'Image',
+		mediaType: emoji.type || 'image/png',
+		url: emoji.url
+	}
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/follow-relay.ts b/packages/backend/src/remote/activitypub/renderer/follow-relay.ts
new file mode 100644
index 0000000000..984c3c7639
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/follow-relay.ts
@@ -0,0 +1,14 @@
+import config from '@/config/index';
+import { Relay } from '@/models/entities/relay';
+import { ILocalUser } from '@/models/entities/user';
+
+export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) {
+	const follow = {
+		id: `${config.url}/activities/follow-relay/${relay.id}`,
+		type: 'Follow',
+		actor: `${config.url}/users/${relayActor.id}`,
+		object: 'https://www.w3.org/ns/activitystreams#Public'
+	};
+
+	return follow;
+}
diff --git a/packages/backend/src/remote/activitypub/renderer/follow-user.ts b/packages/backend/src/remote/activitypub/renderer/follow-user.ts
new file mode 100644
index 0000000000..e3dde7f7fe
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/follow-user.ts
@@ -0,0 +1,12 @@
+import config from '@/config/index';
+import { Users } from '@/models/index';
+import { User } from '@/models/entities/user';
+
+/**
+ * Convert (local|remote)(Follower|Followee)ID to URL
+ * @param id Follower|Followee ID
+ */
+export default async function renderFollowUser(id: User['id']): Promise<any> {
+	const user = await Users.findOneOrFail(id);
+	return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri;
+}
diff --git a/packages/backend/src/remote/activitypub/renderer/follow.ts b/packages/backend/src/remote/activitypub/renderer/follow.ts
new file mode 100644
index 0000000000..c8a7946799
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/follow.ts
@@ -0,0 +1,15 @@
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+import { Users } from '@/models/index';
+
+export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => {
+	const follow = {
+		type: 'Follow',
+		actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
+		object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri
+	} as any;
+
+	if (requestId) follow.id = requestId;
+
+	return follow;
+};
diff --git a/packages/backend/src/remote/activitypub/renderer/hashtag.ts b/packages/backend/src/remote/activitypub/renderer/hashtag.ts
new file mode 100644
index 0000000000..290c74c7fe
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/hashtag.ts
@@ -0,0 +1,7 @@
+import config from '@/config/index';
+
+export default (tag: string) => ({
+	type: 'Hashtag',
+	href: `${config.url}/tags/${encodeURIComponent(tag)}`,
+	name: `#${tag}`
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/image.ts b/packages/backend/src/remote/activitypub/renderer/image.ts
new file mode 100644
index 0000000000..0cb3d6ed65
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/image.ts
@@ -0,0 +1,9 @@
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFiles } from '@/models/index';
+
+export default (file: DriveFile) => ({
+	type: 'Image',
+	url: DriveFiles.getPublicUrl(file),
+	sensitive: file.isSensitive,
+	name: file.comment
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts
new file mode 100644
index 0000000000..f6ec6583d0
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/index.ts
@@ -0,0 +1,59 @@
+import config from '@/config/index';
+import { v4 as uuid } from 'uuid';
+import { IActivity } from '../type';
+import { LdSignature } from '../misc/ld-signature';
+import { getUserKeypair } from '@/misc/keypair-store';
+import { User } from '@/models/entities/user';
+
+export const renderActivity = (x: any): IActivity | null => {
+	if (x == null) return null;
+
+	if (x !== null && typeof x === 'object' && x.id == null) {
+		x.id = `${config.url}/${uuid()}`;
+	}
+
+	return Object.assign({
+		'@context': [
+			'https://www.w3.org/ns/activitystreams',
+			'https://w3id.org/security/v1',
+			{
+				// as non-standards
+				manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
+				sensitive: 'as:sensitive',
+				Hashtag: 'as:Hashtag',
+				quoteUrl: 'as:quoteUrl',
+				// Mastodon
+				toot: 'http://joinmastodon.org/ns#',
+				Emoji: 'toot:Emoji',
+				featured: 'toot:featured',
+				discoverable: 'toot:discoverable',
+				// schema
+				schema: 'http://schema.org#',
+				PropertyValue: 'schema:PropertyValue',
+				value: 'schema:value',
+				// Misskey
+				misskey: `${config.url}/ns#`,
+				'_misskey_content': 'misskey:_misskey_content',
+				'_misskey_quote': 'misskey:_misskey_quote',
+				'_misskey_reaction': 'misskey:_misskey_reaction',
+				'_misskey_votes': 'misskey:_misskey_votes',
+				'_misskey_talk': 'misskey:_misskey_talk',
+				'isCat': 'misskey:isCat',
+				// vcard
+				vcard: 'http://www.w3.org/2006/vcard/ns#',
+			}
+		]
+	}, x);
+};
+
+export const attachLdSignature = async (activity: any, user: { id: User['id']; host: null; }): Promise<IActivity | null> => {
+	if (activity == null) return null;
+
+	const keypair = await getUserKeypair(user.id);
+
+	const ldSignature = new LdSignature();
+	ldSignature.debug = false;
+	activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`);
+
+	return activity;
+};
diff --git a/packages/backend/src/remote/activitypub/renderer/key.ts b/packages/backend/src/remote/activitypub/renderer/key.ts
new file mode 100644
index 0000000000..992f98d79a
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/key.ts
@@ -0,0 +1,14 @@
+import config from '@/config/index';
+import { ILocalUser } from '@/models/entities/user';
+import { UserKeypair } from '@/models/entities/user-keypair';
+import { createPublicKey } from 'crypto';
+
+export default (user: ILocalUser, key: UserKeypair, postfix?: string) => ({
+	id: `${config.url}/users/${user.id}${postfix || '/publickey'}`,
+	type: 'Key',
+	owner: `${config.url}/users/${user.id}`,
+	publicKeyPem: createPublicKey(key.publicKey).export({
+		type: 'spki',
+		format: 'pem'
+	})
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/like.ts b/packages/backend/src/remote/activitypub/renderer/like.ts
new file mode 100644
index 0000000000..a7e79a176f
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/like.ts
@@ -0,0 +1,30 @@
+import config from '@/config/index';
+import { NoteReaction } from '@/models/entities/note-reaction';
+import { Note } from '@/models/entities/note';
+import { Emojis } from '@/models/index';
+import renderEmoji from './emoji';
+
+export const renderLike = async (noteReaction: NoteReaction, note: Note) => {
+	const reaction = noteReaction.reaction;
+
+	const object =  {
+		type: 'Like',
+		id: `${config.url}/likes/${noteReaction.id}`,
+		actor: `${config.url}/users/${noteReaction.userId}`,
+		object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
+		content: reaction,
+		_misskey_reaction: reaction
+	} as any;
+
+	if (reaction.startsWith(':')) {
+		const name = reaction.replace(/:/g, '');
+		const emoji = await Emojis.findOne({
+			name,
+			host: null
+		});
+
+		if (emoji) object.tag = [ renderEmoji(emoji) ];
+	}
+
+	return object;
+};
diff --git a/packages/backend/src/remote/activitypub/renderer/mention.ts b/packages/backend/src/remote/activitypub/renderer/mention.ts
new file mode 100644
index 0000000000..06d2d33e59
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/mention.ts
@@ -0,0 +1,9 @@
+import config from '@/config/index';
+import { User, ILocalUser } from '@/models/entities/user';
+import { Users } from '@/models/index';
+
+export default (mention: User) => ({
+	type: 'Mention',
+	href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${(mention as ILocalUser).id}`,
+	name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts
new file mode 100644
index 0000000000..84a1786784
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/note.ts
@@ -0,0 +1,168 @@
+import renderDocument from './document';
+import renderHashtag from './hashtag';
+import renderMention from './mention';
+import renderEmoji from './emoji';
+import config from '@/config/index';
+import toHtml from '../misc/get-note-html';
+import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index';
+import { In } from 'typeorm';
+import { Emoji } from '@/models/entities/emoji';
+import { Poll } from '@/models/entities/poll';
+
+export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<any> {
+	const getPromisedFiles = async (ids: string[]) => {
+		if (!ids || ids.length === 0) return [];
+		const items = await DriveFiles.find({ id: In(ids) });
+		return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
+	};
+
+	let inReplyTo;
+	let inReplyToNote: Note | undefined;
+
+	if (note.replyId) {
+		inReplyToNote = await Notes.findOne(note.replyId);
+
+		if (inReplyToNote != null) {
+			const inReplyToUser = await Users.findOne(inReplyToNote.userId);
+
+			if (inReplyToUser != null) {
+				if (inReplyToNote.uri) {
+					inReplyTo = inReplyToNote.uri;
+				} else {
+					if (dive) {
+						inReplyTo = await renderNote(inReplyToNote, false);
+					} else {
+						inReplyTo = `${config.url}/notes/${inReplyToNote.id}`;
+					}
+				}
+			}
+		}
+	} else {
+		inReplyTo = null;
+	}
+
+	let quote;
+
+	if (note.renoteId) {
+		const renote = await Notes.findOne(note.renoteId);
+
+		if (renote) {
+			quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`;
+		}
+	}
+
+	const user = await Users.findOneOrFail(note.userId);
+
+	const attributedTo = `${config.url}/users/${user.id}`;
+
+	const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
+
+	let to: string[] = [];
+	let cc: string[] = [];
+
+	if (note.visibility === 'public') {
+		to = ['https://www.w3.org/ns/activitystreams#Public'];
+		cc = [`${attributedTo}/followers`].concat(mentions);
+	} else if (note.visibility === 'home') {
+		to = [`${attributedTo}/followers`];
+		cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions);
+	} else if (note.visibility === 'followers') {
+		to = [`${attributedTo}/followers`];
+		cc = mentions;
+	} else {
+		to = mentions;
+	}
+
+	const mentionedUsers = note.mentions.length > 0 ? await Users.find({
+		id: In(note.mentions)
+	}) : [];
+
+	const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
+	const mentionTags = mentionedUsers.map(u => renderMention(u));
+
+	const files = await getPromisedFiles(note.fileIds);
+
+	const text = note.text;
+	let poll: Poll | undefined;
+
+	if (note.hasPoll) {
+		poll = await Polls.findOne({ noteId: note.id });
+	}
+
+	let apText = text;
+	if (apText == null) apText = '';
+
+	if (quote) {
+		apText += `\n\nRE: ${quote}`;
+	}
+
+	const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
+
+	const content = toHtml(Object.assign({}, note, {
+		text: apText
+	}));
+
+	const emojis = await getEmojis(note.emojis);
+	const apemojis = emojis.map(emoji => renderEmoji(emoji));
+
+	const tag = [
+		...hashtagTags,
+		...mentionTags,
+		...apemojis,
+	];
+
+	const asPoll = poll ? {
+		type: 'Question',
+		content: toHtml(Object.assign({}, note, {
+			text: text
+		})),
+		[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
+		[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
+			type: 'Note',
+			name: text,
+			replies: {
+				type: 'Collection',
+				totalItems: poll!.votes[i]
+			}
+		}))
+	} : {};
+
+	const asTalk = isTalk ? {
+		_misskey_talk: true
+	} : {};
+
+	return {
+		id: `${config.url}/notes/${note.id}`,
+		type: 'Note',
+		attributedTo,
+		summary,
+		content,
+		_misskey_content: text,
+		_misskey_quote: quote,
+		quoteUrl: quote,
+		published: note.createdAt.toISOString(),
+		to,
+		cc,
+		inReplyTo,
+		attachment: files.map(renderDocument),
+		sensitive: note.cw != null || files.some(file => file.isSensitive),
+		tag,
+		...asPoll,
+		...asTalk
+	};
+}
+
+export async function getEmojis(names: string[]): Promise<Emoji[]> {
+	if (names == null || names.length === 0) return [];
+
+	const emojis = await Promise.all(
+		names.map(name => Emojis.findOne({
+			name,
+			host: null
+		}))
+	);
+
+	return emojis.filter(emoji => emoji != null) as Emoji[];
+}
diff --git a/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts b/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts
new file mode 100644
index 0000000000..2433358646
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts
@@ -0,0 +1,23 @@
+/**
+ * Render OrderedCollectionPage
+ * @param id URL of self
+ * @param totalItems Number of total items
+ * @param orderedItems Items
+ * @param partOf URL of base
+ * @param prev URL of prev page (optional)
+ * @param next URL of next page (optional)
+ */
+export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
+	const page = {
+		id,
+		partOf,
+		type: 'OrderedCollectionPage',
+		totalItems,
+		orderedItems
+	} as any;
+
+	if (prev) page.prev = prev;
+	if (next) page.next = next;
+
+	return page;
+}
diff --git a/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts b/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts
new file mode 100644
index 0000000000..68870a0ecd
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts
@@ -0,0 +1,21 @@
+/**
+ * Render OrderedCollection
+ * @param id URL of self
+ * @param totalItems Total number of items
+ * @param first URL of first page (optional)
+ * @param last URL of last page (optional)
+ * @param orderedItems attached objects (optional)
+ */
+export default function(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: object) {
+	const page: any = {
+		id,
+		type: 'OrderedCollection',
+		totalItems,
+	};
+
+	if (first) page.first = first;
+	if (last) page.last = last;
+	if (orderedItems) page.orderedItems = orderedItems;
+
+	return page;
+}
diff --git a/packages/backend/src/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts
new file mode 100644
index 0000000000..f2a283a870
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/person.ts
@@ -0,0 +1,89 @@
+import { URL } from 'url';
+import * as mfm from 'mfm-js';
+import renderImage from './image';
+import renderKey from './key';
+import config from '@/config/index';
+import { ILocalUser } from '@/models/entities/user';
+import { toHtml } from '../../../mfm/to-html';
+import { getEmojis } from './note';
+import renderEmoji from './emoji';
+import { IIdentifier } from '../models/identifier';
+import renderHashtag from './hashtag';
+import { DriveFiles, UserProfiles } from '@/models/index';
+import { getUserKeypair } from '@/misc/keypair-store';
+
+export async function renderPerson(user: ILocalUser) {
+	const id = `${config.url}/users/${user.id}`;
+	const isSystem = !!user.username.match(/\./);
+
+	const [avatar, banner, profile] = await Promise.all([
+		user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined),
+		user.bannerId ? DriveFiles.findOne(user.bannerId) : Promise.resolve(undefined),
+		UserProfiles.findOneOrFail(user.id)
+	]);
+
+	const attachment: {
+		type: 'PropertyValue',
+		name: string,
+		value: string,
+		identifier?: IIdentifier
+	}[] = [];
+
+	if (profile.fields) {
+		for (const field of profile.fields) {
+			attachment.push({
+				type: 'PropertyValue',
+				name: field.name,
+				value: (field.value != null && field.value.match(/^https?:/))
+					? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
+					: field.value
+			});
+		}
+	}
+
+	const emojis = await getEmojis(user.emojis);
+	const apemojis = emojis.map(emoji => renderEmoji(emoji));
+
+	const hashtagTags = (user.tags || []).map(tag => renderHashtag(tag));
+
+	const tag = [
+		...apemojis,
+		...hashtagTags,
+	];
+
+	const keypair = await getUserKeypair(user.id);
+
+	const person = {
+		type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
+		id,
+		inbox: `${id}/inbox`,
+		outbox: `${id}/outbox`,
+		followers: `${id}/followers`,
+		following: `${id}/following`,
+		featured: `${id}/collections/featured`,
+		sharedInbox: `${config.url}/inbox`,
+		endpoints: { sharedInbox: `${config.url}/inbox` },
+		url: `${config.url}/@${user.username}`,
+		preferredUsername: user.username,
+		name: user.name,
+		summary: profile.description ? toHtml(mfm.parse(profile.description)) : null,
+		icon: avatar ? renderImage(avatar) : null,
+		image: banner ? renderImage(banner) : null,
+		tag,
+		manuallyApprovesFollowers: user.isLocked,
+		discoverable: !!user.isExplorable,
+		publicKey: renderKey(user, keypair, `#main-key`),
+		isCat: user.isCat,
+		attachment: attachment.length ? attachment : undefined
+	} as any;
+
+	if (profile?.birthday) {
+		person['vcard:bday'] = profile.birthday;
+	}
+
+	if (profile?.location) {
+		person['vcard:Address'] = profile.location;
+	}
+
+	return person;
+}
diff --git a/packages/backend/src/remote/activitypub/renderer/question.ts b/packages/backend/src/remote/activitypub/renderer/question.ts
new file mode 100644
index 0000000000..246d599bab
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/question.ts
@@ -0,0 +1,23 @@
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { Poll } from '@/models/entities/poll';
+
+export default async function renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) {
+	const question = {
+		type: 'Question',
+		id: `${config.url}/questions/${note.id}`,
+		actor: `${config.url}/users/${user.id}`,
+		content:  note.text || '',
+		[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
+			name: text,
+			_misskey_votes: poll.votes[i],
+			replies: {
+				type: 'Collection',
+				totalItems: poll.votes[i]
+			}
+		}))
+	};
+
+	return question;
+}
diff --git a/packages/backend/src/remote/activitypub/renderer/read.ts b/packages/backend/src/remote/activitypub/renderer/read.ts
new file mode 100644
index 0000000000..95357f64d3
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/read.ts
@@ -0,0 +1,9 @@
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+
+export const renderReadActivity = (user: { id: User['id'] }, message: MessagingMessage) => ({
+	type: 'Read',
+	actor: `${config.url}/users/${user.id}`,
+	object: message.uri
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/reject.ts b/packages/backend/src/remote/activitypub/renderer/reject.ts
new file mode 100644
index 0000000000..42beffecf2
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/reject.ts
@@ -0,0 +1,8 @@
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+
+export default (object: any, user: { id: User['id'] }) => ({
+	type: 'Reject',
+	actor: `${config.url}/users/${user.id}`,
+	object
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/remove.ts b/packages/backend/src/remote/activitypub/renderer/remove.ts
new file mode 100644
index 0000000000..79d60edbaa
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/remove.ts
@@ -0,0 +1,9 @@
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+
+export default (user: { id: User['id'] }, target: any, object: any) => ({
+	type: 'Remove',
+	actor: `${config.url}/users/${user.id}`,
+	target,
+	object
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/tombstone.ts b/packages/backend/src/remote/activitypub/renderer/tombstone.ts
new file mode 100644
index 0000000000..553406b93b
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/tombstone.ts
@@ -0,0 +1,4 @@
+export default (id: string) => ({
+	id,
+	type: 'Tombstone'
+});
diff --git a/packages/backend/src/remote/activitypub/renderer/undo.ts b/packages/backend/src/remote/activitypub/renderer/undo.ts
new file mode 100644
index 0000000000..14115b788d
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/undo.ts
@@ -0,0 +1,13 @@
+import config from '@/config/index';
+import { ILocalUser, User } from '@/models/entities/user';
+
+export default (object: any, user: { id: User['id'] }) => {
+	if (object == null) return null;
+
+	return {
+		type: 'Undo',
+		actor: `${config.url}/users/${user.id}`,
+		object,
+		published: new Date().toISOString(),
+	};
+};
diff --git a/packages/backend/src/remote/activitypub/renderer/update.ts b/packages/backend/src/remote/activitypub/renderer/update.ts
new file mode 100644
index 0000000000..8bb415d117
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/update.ts
@@ -0,0 +1,15 @@
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+
+export default (object: any, user: { id: User['id'] }) => {
+	const activity = {
+		id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`,
+		actor: `${config.url}/users/${user.id}`,
+		type: 'Update',
+		to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
+		object,
+		published: new Date().toISOString(),
+	} as any;
+
+	return activity;
+};
diff --git a/packages/backend/src/remote/activitypub/renderer/vote.ts b/packages/backend/src/remote/activitypub/renderer/vote.ts
new file mode 100644
index 0000000000..ff038070f7
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/renderer/vote.ts
@@ -0,0 +1,23 @@
+import config from '@/config/index';
+import { Note } from '@/models/entities/note';
+import { IRemoteUser, User } from '@/models/entities/user';
+import { PollVote } from '@/models/entities/poll-vote';
+import { Poll } from '@/models/entities/poll';
+
+export default async function renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> {
+	return {
+		id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`,
+		actor: `${config.url}/users/${user.id}`,
+		type: 'Create',
+		to: [pollOwner.uri],
+		published: new Date().toISOString(),
+		object: {
+			id: `${config.url}/users/${user.id}#votes/${vote.id}`,
+			type: 'Note',
+			attributedTo: `${config.url}/users/${user.id}`,
+			to: [pollOwner.uri],
+			inReplyTo: note.uri,
+			name: poll.choices[vote.choice]
+		}
+	};
+}
diff --git a/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts
new file mode 100644
index 0000000000..d6ced630c1
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/request.ts
@@ -0,0 +1,58 @@
+import config from '@/config/index';
+import { getUserKeypair } from '@/misc/keypair-store';
+import { User } from '@/models/entities/user';
+import { getResponse } from '../../misc/fetch';
+import { createSignedPost, createSignedGet } from './ap-request';
+
+export default async (user: { id: User['id'] }, url: string, object: any) => {
+	const body = JSON.stringify(object);
+
+	const keypair = await getUserKeypair(user.id);
+
+	const req = createSignedPost({
+		key: {
+			privateKeyPem: keypair.privateKey,
+			keyId: `${config.url}/users/${user.id}#main-key`
+		},
+		url,
+		body,
+		additionalHeaders: {
+			'User-Agent': config.userAgent,
+		}
+	});
+
+	await getResponse({
+		url,
+		method: req.request.method,
+		headers: req.request.headers,
+		body,
+	});
+};
+
+/**
+ * Get AP object with http-signature
+ * @param user http-signature user
+ * @param url URL to fetch
+ */
+export async function signedGet(url: string, user: { id: User['id'] }) {
+	const keypair = await getUserKeypair(user.id);
+
+	const req = createSignedGet({
+		key: {
+			privateKeyPem: keypair.privateKey,
+			keyId: `${config.url}/users/${user.id}#main-key`
+		},
+		url,
+		additionalHeaders: {
+			'User-Agent': config.userAgent,
+		}
+	});
+
+	const res = await getResponse({
+		url,
+		method: req.request.method,
+		headers: req.request.headers
+	});
+
+	return await res.json();
+}
diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts
new file mode 100644
index 0000000000..f392a65e3a
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/resolver.ts
@@ -0,0 +1,73 @@
+import config from '@/config/index';
+import { getJson } from '@/misc/fetch';
+import { ILocalUser } from '@/models/entities/user';
+import { getInstanceActor } from '@/services/instance-actor';
+import { signedGet } from './request';
+import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { extractDbHost } from '@/misc/convert-host';
+
+export default class Resolver {
+	private history: Set<string>;
+	private user?: ILocalUser;
+
+	constructor() {
+		this.history = new Set();
+	}
+
+	public getHistory(): string[] {
+		return Array.from(this.history);
+	}
+
+	public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
+		const collection = typeof value === 'string'
+			? await this.resolve(value)
+			: value;
+
+		if (isCollectionOrOrderedCollection(collection)) {
+			return collection;
+		} else {
+			throw new Error(`unrecognized collection type: ${collection.type}`);
+		}
+	}
+
+	public async resolve(value: string | IObject): Promise<IObject> {
+		if (value == null) {
+			throw new Error('resolvee is null (or undefined)');
+		}
+
+		if (typeof value !== 'string') {
+			return value;
+		}
+
+		if (this.history.has(value)) {
+			throw new Error('cannot resolve already resolved one');
+		}
+
+		this.history.add(value);
+
+		const meta = await fetchMeta();
+		const host = extractDbHost(value);
+		if (meta.blockedHosts.includes(host)) {
+			throw new Error('Instance is blocked');
+		}
+
+		if (config.signToActivityPubGet && !this.user) {
+			this.user = await getInstanceActor();
+		}
+
+		const object = this.user
+			? await signedGet(value, this.user)
+			: await getJson(value, 'application/activity+json, application/ld+json');
+
+		if (object == null || (
+			Array.isArray(object['@context']) ?
+				!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
+				object['@context'] !== 'https://www.w3.org/ns/activitystreams'
+		)) {
+			throw new Error('invalid response');
+		}
+
+		return object;
+	}
+}
diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts
new file mode 100644
index 0000000000..2051d2624d
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/type.ts
@@ -0,0 +1,289 @@
+export type obj = { [x: string]: any };
+export type ApObject = IObject | string | (IObject | string)[];
+
+export interface IObject {
+	'@context': string | obj | obj[];
+	type: string | string[];
+	id?: string;
+	summary?: string;
+	published?: string;
+	cc?: ApObject;
+	to?: ApObject;
+	attributedTo: ApObject;
+	attachment?: any[];
+	inReplyTo?: any;
+	replies?: ICollection;
+	content?: string;
+	name?: string;
+	startTime?: Date;
+	endTime?: Date;
+	icon?: any;
+	image?: any;
+	url?: ApObject;
+	href?: string;
+	tag?: IObject | IObject[];
+	sensitive?: boolean;
+}
+
+/**
+ * Get array of ActivityStreams Objects id
+ */
+export function getApIds(value: ApObject | undefined): string[] {
+	if (value == null) return [];
+	const array = Array.isArray(value) ? value : [value];
+	return array.map(x => getApId(x));
+}
+
+/**
+ * Get first ActivityStreams Object id
+ */
+export function getOneApId(value: ApObject): string {
+	const firstOne = Array.isArray(value) ? value[0] : value;
+	return getApId(firstOne);
+}
+
+/**
+ * Get ActivityStreams Object id
+ */
+export function getApId(value: string | IObject): string {
+	if (typeof value === 'string') return value;
+	if (typeof value.id === 'string') return value.id;
+	throw new Error(`cannot detemine id`);
+}
+
+/**
+ * Get ActivityStreams Object type
+ */
+export function getApType(value: IObject): string {
+	if (typeof value.type === 'string') return value.type;
+	if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
+	throw new Error(`cannot detect type`);
+}
+
+export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
+	const firstOne = Array.isArray(value) ? value[0] : value;
+	return getApHrefNullable(firstOne);
+}
+
+export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
+	if (typeof value === 'string') return value;
+	if (typeof value?.href === 'string') return value.href;
+	return undefined;
+}
+
+export interface IActivity extends IObject {
+	//type: 'Activity';
+	actor: IObject | string;
+	object: IObject | string;
+	target?: IObject | string;
+	/** LD-Signature */
+	signature?: {
+		type: string;
+		created: Date;
+		creator: string;
+		domain?: string;
+		nonce?: string;
+		signatureValue: string;
+	};
+}
+
+export interface ICollection extends IObject {
+	type: 'Collection';
+	totalItems: number;
+	items: ApObject;
+}
+
+export interface IOrderedCollection extends IObject {
+	type: 'OrderedCollection';
+	totalItems: number;
+	orderedItems: ApObject;
+}
+
+export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
+
+export const isPost = (object: IObject): object is IPost =>
+	validPost.includes(getApType(object));
+
+export interface IPost extends IObject {
+	type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
+	_misskey_content?: string;
+	_misskey_quote?: string;
+	quoteUrl?: string;
+	_misskey_talk: boolean;
+}
+
+export interface IQuestion extends IObject {
+	type: 'Note' | 'Question';
+	_misskey_content?: string;
+	_misskey_quote?: string;
+	quoteUrl?: string;
+	oneOf?: IQuestionChoice[];
+	anyOf?: IQuestionChoice[];
+	endTime?: Date;
+	closed?: Date;
+}
+
+export const isQuestion = (object: IObject): object is IQuestion =>
+	getApType(object) === 'Note' || getApType(object) === 'Question';
+
+interface IQuestionChoice {
+	name?: string;
+	replies?: ICollection;
+	_misskey_votes?: number;
+}
+export interface ITombstone extends IObject {
+	type: 'Tombstone';
+	formerType?: string;
+	deleted?: Date;
+}
+
+export const isTombstone = (object: IObject): object is ITombstone =>
+	getApType(object) === 'Tombstone';
+
+export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application'];
+
+export const isActor = (object: IObject): object is IActor =>
+	validActor.includes(getApType(object));
+
+export interface IActor extends IObject {
+	type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application';
+	name?: string;
+	preferredUsername?: string;
+	manuallyApprovesFollowers?: boolean;
+	discoverable?: boolean;
+	inbox: string;
+	sharedInbox?: string;	// 後方互換性のため
+	publicKey?: {
+		id: string;
+		publicKeyPem: string;
+	};
+	followers?: string | ICollection | IOrderedCollection;
+	following?: string | ICollection | IOrderedCollection;
+	featured?: string | IOrderedCollection;
+	outbox: string | IOrderedCollection;
+	endpoints?: {
+		sharedInbox?: string;
+	};
+	'vcard:bday'?: string;
+	'vcard:Address'?: string;
+}
+
+export const isCollection = (object: IObject): object is ICollection =>
+	getApType(object) === 'Collection';
+
+export const isOrderedCollection = (object: IObject): object is IOrderedCollection =>
+	getApType(object) === 'OrderedCollection';
+
+export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
+	isCollection(object) || isOrderedCollection(object);
+
+export interface IApPropertyValue extends IObject {
+	type: 'PropertyValue';
+	identifier: IApPropertyValue;
+	name: string;
+	value: string;
+}
+
+export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
+	object &&
+	getApType(object) === 'PropertyValue' &&
+	typeof object.name === 'string' &&
+	typeof (object as any).value === 'string';
+
+export interface IApMention extends IObject {
+	type: 'Mention';
+	href: string;
+}
+
+export const isMention = (object: IObject): object is IApMention=>
+	getApType(object) === 'Mention' &&
+	typeof object.href === 'string';
+
+export interface IApHashtag extends IObject {
+	type: 'Hashtag';
+	name: string;
+}
+
+export const isHashtag = (object: IObject): object is IApHashtag =>
+	getApType(object) === 'Hashtag' &&
+	typeof object.name === 'string';
+
+export interface IApEmoji extends IObject {
+	type: 'Emoji';
+	updated: Date;
+}
+
+export const isEmoji = (object: IObject): object is IApEmoji =>
+	getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
+
+export interface ICreate extends IActivity {
+	type: 'Create';
+}
+
+export interface IDelete extends IActivity {
+	type: 'Delete';
+}
+
+export interface IUpdate extends IActivity {
+	type: 'Update';
+}
+
+export interface IRead extends IActivity {
+	type: 'Read';
+}
+
+export interface IUndo extends IActivity {
+	type: 'Undo';
+}
+
+export interface IFollow extends IActivity {
+	type: 'Follow';
+}
+
+export interface IAccept extends IActivity {
+	type: 'Accept';
+}
+
+export interface IReject extends IActivity {
+	type: 'Reject';
+}
+
+export interface IAdd extends IActivity {
+	type: 'Add';
+}
+
+export interface IRemove extends IActivity {
+	type: 'Remove';
+}
+
+export interface ILike extends IActivity {
+	type: 'Like' | 'EmojiReaction' | 'EmojiReact';
+	_misskey_reaction?: string;
+}
+
+export interface IAnnounce extends IActivity {
+	type: 'Announce';
+}
+
+export interface IBlock extends IActivity {
+	type: 'Block';
+}
+
+export interface IFlag extends IActivity {
+	type: 'Flag';
+}
+
+export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
+export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
+export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
+export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read';
+export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo';
+export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow';
+export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept';
+export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
+export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
+export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
+export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact';
+export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
+export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
+export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
diff --git a/packages/backend/src/remote/logger.ts b/packages/backend/src/remote/logger.ts
new file mode 100644
index 0000000000..9ffad4d716
--- /dev/null
+++ b/packages/backend/src/remote/logger.ts
@@ -0,0 +1,3 @@
+import Logger from '@/services/logger';
+
+export const remoteLogger = new Logger('remote', 'cyan');
diff --git a/packages/backend/src/remote/resolve-user.ts b/packages/backend/src/remote/resolve-user.ts
new file mode 100644
index 0000000000..a12396abc8
--- /dev/null
+++ b/packages/backend/src/remote/resolve-user.ts
@@ -0,0 +1,110 @@
+import { URL } from 'url';
+import webFinger from './webfinger';
+import config from '@/config/index';
+import { createPerson, updatePerson } from './activitypub/models/person';
+import { remoteLogger } from './logger';
+import * as chalk from 'chalk';
+import { User, IRemoteUser } from '@/models/entities/user';
+import { Users } from '@/models/index';
+import { toPuny } from '@/misc/convert-host';
+
+const logger = remoteLogger.createSubLogger('resolve-user');
+
+export async function resolveUser(username: string, host: string | null, option?: any, resync = false): Promise<User> {
+	const usernameLower = username.toLowerCase();
+
+	if (host == null) {
+		logger.info(`return local user: ${usernameLower}`);
+		return await Users.findOne({ usernameLower, host: null }).then(u => {
+			if (u == null) {
+				throw new Error('user not found');
+			} else {
+				return u;
+			}
+		});
+	}
+
+	host = toPuny(host);
+
+	if (config.host == host) {
+		logger.info(`return local user: ${usernameLower}`);
+		return await Users.findOne({ usernameLower, host: null }).then(u => {
+			if (u == null) {
+				throw new Error('user not found');
+			} else {
+				return u;
+			}
+		});
+	}
+
+	const user = await Users.findOne({ usernameLower, host }, option) as IRemoteUser;
+
+	const acctLower = `${usernameLower}@${host}`;
+
+	if (user == null) {
+		const self = await resolveSelf(acctLower);
+
+		logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
+		return await createPerson(self.href);
+	}
+
+	// resyncオプション OR ユーザー情報が古い場合は、WebFilgerからやりなおして返す
+	if (resync || user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
+		// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
+		await Users.update(user.id, {
+			lastFetchedAt: new Date(),
+		});
+
+		logger.info(`try resync: ${acctLower}`);
+		const self = await resolveSelf(acctLower);
+
+		if (user.uri !== self.href) {
+			// if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping.
+			logger.info(`uri missmatch: ${acctLower}`);
+			logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
+
+			// validate uri
+			const uri = new URL(self.href);
+			if (uri.hostname !== host) {
+				throw new Error(`Invalid uri`);
+			}
+
+			await Users.update({
+				usernameLower,
+				host: host
+			}, {
+				uri: self.href
+			});
+		} else {
+			logger.info(`uri is fine: ${acctLower}`);
+		}
+
+		await updatePerson(self.href);
+
+		logger.info(`return resynced remote user: ${acctLower}`);
+		return await Users.findOne({ uri: self.href }).then(u => {
+			if (u == null) {
+				throw new Error('user not found');
+			} else {
+				return u;
+			}
+		});
+	}
+
+	logger.info(`return existing remote user: ${acctLower}`);
+	return user;
+}
+
+async function resolveSelf(acctLower: string) {
+	logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
+	const finger = await webFinger(acctLower).catch(e => {
+		logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`);
+		throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
+	});
+	const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self');
+	if (!self) {
+		logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`);
+		throw new Error('self link not found');
+	}
+	return self;
+}
diff --git a/packages/backend/src/remote/webfinger.ts b/packages/backend/src/remote/webfinger.ts
new file mode 100644
index 0000000000..f63fd03628
--- /dev/null
+++ b/packages/backend/src/remote/webfinger.ts
@@ -0,0 +1,34 @@
+import { URL } from 'url';
+import { getJson } from '@/misc/fetch';
+import { query as urlQuery } from '@/prelude/url';
+
+type ILink = {
+	href: string;
+	rel?: string;
+};
+
+type IWebFinger = {
+	links: ILink[];
+	subject: string;
+};
+
+export default async function(query: string): Promise<IWebFinger> {
+	const url = genUrl(query);
+
+	return await getJson(url, 'application/jrd+json, application/json');
+}
+
+function genUrl(query: string) {
+	if (query.match(/^https?:\/\//)) {
+		const u = new URL(query);
+		return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
+	}
+
+	const m = query.match(/^([^@]+)@(.*)/);
+	if (m) {
+		const hostname = m[2];
+		return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` });
+	}
+
+	throw new Error(`Invalid query (${query})`);
+}
diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
new file mode 100644
index 0000000000..eabe681136
--- /dev/null
+++ b/packages/backend/src/server/activitypub.ts
@@ -0,0 +1,227 @@
+import * as Router from '@koa/router';
+import * as json from 'koa-json-body';
+import * as httpSignature from 'http-signature';
+
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderNote from '@/remote/activitypub/renderer/note';
+import renderKey from '@/remote/activitypub/renderer/key';
+import { renderPerson } from '@/remote/activitypub/renderer/person';
+import renderEmoji from '@/remote/activitypub/renderer/emoji';
+import Outbox, { packActivity } from './activitypub/outbox';
+import Followers from './activitypub/followers';
+import Following from './activitypub/following';
+import Featured from './activitypub/featured';
+import { inbox as processInbox } from '@/queue/index';
+import { isSelfHost } from '@/misc/convert-host';
+import { Notes, Users, Emojis, NoteReactions } from '@/models/index';
+import { ILocalUser, User } from '@/models/entities/user';
+import { In } from 'typeorm';
+import { renderLike } from '@/remote/activitypub/renderer/like';
+import { getUserKeypair } from '@/misc/keypair-store';
+
+// Init router
+const router = new Router();
+
+//#region Routing
+
+function inbox(ctx: Router.RouterContext) {
+	let signature;
+
+	try {
+		signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
+	} catch (e) {
+		ctx.status = 401;
+		return;
+	}
+
+	processInbox(ctx.request.body, signature);
+
+	ctx.status = 202;
+}
+
+const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
+const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
+
+function isActivityPubReq(ctx: Router.RouterContext) {
+	ctx.response.vary('Accept');
+	const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON);
+	return typeof accepted === 'string' && !accepted.match(/html/);
+}
+
+export function setResponseType(ctx: Router.RouterContext) {
+	const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON);
+	if (accept === LD_JSON) {
+		ctx.response.type = LD_JSON;
+	} else {
+		ctx.response.type = ACTIVITY_JSON;
+	}
+}
+
+// inbox
+router.post('/inbox', json(), inbox);
+router.post('/users/:user/inbox', json(), inbox);
+
+// note
+router.get('/notes/:note', async (ctx, next) => {
+	if (!isActivityPubReq(ctx)) return await next();
+
+	const note = await Notes.findOne({
+		id: ctx.params.note,
+		visibility: In(['public', 'home']),
+		localOnly: false
+	});
+
+	if (note == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	// リモートだったらリダイレクト
+	if (note.userHost != null) {
+		if (note.uri == null || isSelfHost(note.userHost)) {
+			ctx.status = 500;
+			return;
+		}
+		ctx.redirect(note.uri);
+		return;
+	}
+
+	ctx.body = renderActivity(await renderNote(note, false));
+	ctx.set('Cache-Control', 'public, max-age=180');
+	setResponseType(ctx);
+});
+
+// note activity
+router.get('/notes/:note/activity', async ctx => {
+	const note = await Notes.findOne({
+		id: ctx.params.note,
+		userHost: null,
+		visibility: In(['public', 'home']),
+		localOnly: false
+	});
+
+	if (note == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.body = renderActivity(await packActivity(note));
+	ctx.set('Cache-Control', 'public, max-age=180');
+	setResponseType(ctx);
+});
+
+// outbox
+router.get('/users/:user/outbox', Outbox);
+
+// followers
+router.get('/users/:user/followers', Followers);
+
+// following
+router.get('/users/:user/following', Following);
+
+// featured
+router.get('/users/:user/collections/featured', Featured);
+
+// publickey
+router.get('/users/:user/publickey', async ctx => {
+	const userId = ctx.params.user;
+
+	const user = await Users.findOne({
+		id: userId,
+		host: null
+	});
+
+	if (user == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const keypair = await getUserKeypair(user.id);
+
+	if (Users.isLocalUser(user)) {
+		ctx.body = renderActivity(renderKey(user, keypair));
+		ctx.set('Cache-Control', 'public, max-age=180');
+		setResponseType(ctx);
+	} else {
+		ctx.status = 400;
+	}
+});
+
+// user
+async function userInfo(ctx: Router.RouterContext, user: User | undefined) {
+	if (user == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.body = renderActivity(await renderPerson(user as ILocalUser));
+	ctx.set('Cache-Control', 'public, max-age=180');
+	setResponseType(ctx);
+}
+
+router.get('/users/:user', async (ctx, next) => {
+	if (!isActivityPubReq(ctx)) return await next();
+
+	const userId = ctx.params.user;
+
+	const user = await Users.findOne({
+		id: userId,
+		host: null,
+		isSuspended: false
+	});
+
+	await userInfo(ctx, user);
+});
+
+router.get('/@:user', async (ctx, next) => {
+	if (!isActivityPubReq(ctx)) return await next();
+
+	const user = await Users.findOne({
+		usernameLower: ctx.params.user.toLowerCase(),
+		host: null,
+		isSuspended: false
+	});
+
+	await userInfo(ctx, user);
+});
+//#endregion
+
+// emoji
+router.get('/emojis/:emoji', async ctx => {
+	const emoji = await Emojis.findOne({
+		host: null,
+		name: ctx.params.emoji
+	});
+
+	if (emoji == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.body = renderActivity(await renderEmoji(emoji));
+	ctx.set('Cache-Control', 'public, max-age=180');
+	setResponseType(ctx);
+});
+
+// like
+router.get('/likes/:like', async ctx => {
+	const reaction = await NoteReactions.findOne(ctx.params.like);
+
+	if (reaction == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const note = await Notes.findOne(reaction.noteId);
+
+	if (note == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.body = renderActivity(await renderLike(reaction, note));
+	ctx.set('Cache-Control', 'public, max-age=180');
+	setResponseType(ctx);
+});
+
+export default router;
diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts
new file mode 100644
index 0000000000..1598cc680f
--- /dev/null
+++ b/packages/backend/src/server/activitypub/featured.ts
@@ -0,0 +1,41 @@
+import * as Router from '@koa/router';
+import config from '@/config/index';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection';
+import { setResponseType } from '../activitypub';
+import renderNote from '@/remote/activitypub/renderer/note';
+import { Users, Notes, UserNotePinings } from '@/models/index';
+
+export default async (ctx: Router.RouterContext) => {
+	const userId = ctx.params.user;
+
+	// Verify user
+	const user = await Users.findOne({
+		id: userId,
+		host: null
+	});
+
+	if (user == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const pinings = await UserNotePinings.find({
+		where: { userId: user.id },
+		order: { id: 'DESC' }
+	});
+
+	const pinnedNotes = await Promise.all(pinings.map(pining =>
+		Notes.findOneOrFail(pining.noteId)));
+
+	const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
+
+	const rendered = renderOrderedCollection(
+		`${config.url}/users/${userId}/collections/featured`,
+		renderedNotes.length, undefined, undefined, renderedNotes
+	);
+
+	ctx.body = renderActivity(rendered);
+	ctx.set('Cache-Control', 'public, max-age=180');
+	setResponseType(ctx);
+};
diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts
new file mode 100644
index 0000000000..baf2d23460
--- /dev/null
+++ b/packages/backend/src/server/activitypub/followers.ts
@@ -0,0 +1,102 @@
+import * as Router from '@koa/router';
+import config from '@/config/index';
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import * as url from '@/prelude/url';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection';
+import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page';
+import renderFollowUser from '@/remote/activitypub/renderer/follow-user';
+import { setResponseType } from '../activitypub';
+import { Users, Followings, UserProfiles } from '@/models/index';
+import { LessThan } from 'typeorm';
+
+export default async (ctx: Router.RouterContext) => {
+	const userId = ctx.params.user;
+
+	// Get 'cursor' parameter
+	const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);
+
+	// Get 'page' parameter
+	const pageErr = !$.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
+	const page: boolean = ctx.request.query.page === 'true';
+
+	// Validate parameters
+	if (cursorErr || pageErr) {
+		ctx.status = 400;
+		return;
+	}
+
+	// Verify user
+	const user = await Users.findOne({
+		id: userId,
+		host: null
+	});
+
+	if (user == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	//#region Check ff visibility
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	if (profile.ffVisibility === 'private') {
+		ctx.status = 403;
+		ctx.set('Cache-Control', 'public, max-age=30');
+		return;
+	} else if (profile.ffVisibility === 'followers') {
+		ctx.status = 403;
+		ctx.set('Cache-Control', 'public, max-age=30');
+		return;
+	}
+	//#endregion
+
+	const limit = 10;
+	const partOf = `${config.url}/users/${userId}/followers`;
+
+	if (page) {
+		const query = {
+			followeeId: user.id
+		} as any;
+
+		// カーソルが指定されている場合
+		if (cursor) {
+			query.id = LessThan(cursor);
+		}
+
+		// Get followers
+		const followings = await Followings.find({
+			where: query,
+			take: limit + 1,
+			order: { id: -1 }
+		});
+
+		// 「次のページ」があるかどうか
+		const inStock = followings.length === limit + 1;
+		if (inStock) followings.pop();
+
+		const renderedFollowers = await Promise.all(followings.map(following => renderFollowUser(following.followerId)));
+		const rendered = renderOrderedCollectionPage(
+			`${partOf}?${url.query({
+				page: 'true',
+				cursor
+			})}`,
+			user.followersCount, renderedFollowers, partOf,
+			undefined,
+			inStock ? `${partOf}?${url.query({
+				page: 'true',
+				cursor: followings[followings.length - 1].id
+			})}` : undefined
+		);
+
+		ctx.body = renderActivity(rendered);
+		setResponseType(ctx);
+	} else {
+		// index page
+		const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
+		ctx.body = renderActivity(rendered);
+		ctx.set('Cache-Control', 'public, max-age=180');
+		setResponseType(ctx);
+	}
+};
diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts
new file mode 100644
index 0000000000..b9eb806c3c
--- /dev/null
+++ b/packages/backend/src/server/activitypub/following.ts
@@ -0,0 +1,103 @@
+import * as Router from '@koa/router';
+import config from '@/config/index';
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import * as url from '@/prelude/url';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection';
+import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page';
+import renderFollowUser from '@/remote/activitypub/renderer/follow-user';
+import { setResponseType } from '../activitypub';
+import { Users, Followings, UserProfiles } from '@/models/index';
+import { LessThan, FindConditions } from 'typeorm';
+import { Following } from '@/models/entities/following';
+
+export default async (ctx: Router.RouterContext) => {
+	const userId = ctx.params.user;
+
+	// Get 'cursor' parameter
+	const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);
+
+	// Get 'page' parameter
+	const pageErr = !$.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
+	const page: boolean = ctx.request.query.page === 'true';
+
+	// Validate parameters
+	if (cursorErr || pageErr) {
+		ctx.status = 400;
+		return;
+	}
+
+	// Verify user
+	const user = await Users.findOne({
+		id: userId,
+		host: null
+	});
+
+	if (user == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	//#region Check ff visibility
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	if (profile.ffVisibility === 'private') {
+		ctx.status = 403;
+		ctx.set('Cache-Control', 'public, max-age=30');
+		return;
+	} else if (profile.ffVisibility === 'followers') {
+		ctx.status = 403;
+		ctx.set('Cache-Control', 'public, max-age=30');
+		return;
+	}
+	//#endregion
+
+	const limit = 10;
+	const partOf = `${config.url}/users/${userId}/following`;
+
+	if (page) {
+		const query = {
+			followerId: user.id
+		} as FindConditions<Following>;
+
+		// カーソルが指定されている場合
+		if (cursor) {
+			query.id = LessThan(cursor);
+		}
+
+		// Get followings
+		const followings = await Followings.find({
+			where: query,
+			take: limit + 1,
+			order: { id: -1 }
+		});
+
+		// 「次のページ」があるかどうか
+		const inStock = followings.length === limit + 1;
+		if (inStock) followings.pop();
+
+		const renderedFollowees = await Promise.all(followings.map(following => renderFollowUser(following.followeeId)));
+		const rendered = renderOrderedCollectionPage(
+			`${partOf}?${url.query({
+				page: 'true',
+				cursor
+			})}`,
+			user.followingCount, renderedFollowees, partOf,
+			undefined,
+			inStock ? `${partOf}?${url.query({
+				page: 'true',
+				cursor: followings[followings.length - 1].id
+			})}` : undefined
+		);
+
+		ctx.body = renderActivity(rendered);
+		setResponseType(ctx);
+	} else {
+		// index page
+		const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
+		ctx.body = renderActivity(rendered);
+		ctx.set('Cache-Control', 'public, max-age=180');
+		setResponseType(ctx);
+	}
+};
diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts
new file mode 100644
index 0000000000..df528e8b5a
--- /dev/null
+++ b/packages/backend/src/server/activitypub/outbox.ts
@@ -0,0 +1,108 @@
+import * as Router from '@koa/router';
+import config from '@/config/index';
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection';
+import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page';
+import { setResponseType } from '../activitypub';
+import renderNote from '@/remote/activitypub/renderer/note';
+import renderCreate from '@/remote/activitypub/renderer/create';
+import renderAnnounce from '@/remote/activitypub/renderer/announce';
+import { countIf } from '@/prelude/array';
+import * as url from '@/prelude/url';
+import { Users, Notes } from '@/models/index';
+import { makePaginationQuery } from '../api/common/make-pagination-query';
+import { Brackets } from 'typeorm';
+import { Note } from '@/models/entities/note';
+
+export default async (ctx: Router.RouterContext) => {
+	const userId = ctx.params.user;
+
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $.optional.type(ID).get(ctx.request.query.since_id);
+
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $.optional.type(ID).get(ctx.request.query.until_id);
+
+	// Get 'page' parameter
+	const pageErr = !$.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
+	const page: boolean = ctx.request.query.page === 'true';
+
+	// Validate parameters
+	if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) {
+		ctx.status = 400;
+		return;
+	}
+
+	// Verify user
+	const user = await Users.findOne({
+		id: userId,
+		host: null
+	});
+
+	if (user == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const limit = 20;
+	const partOf = `${config.url}/users/${userId}/outbox`;
+
+	if (page) {
+		const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId)
+			.andWhere('note.userId = :userId', { userId: user.id })
+			.andWhere(new Brackets(qb => { qb
+				.where(`note.visibility = 'public'`)
+				.orWhere(`note.visibility = 'home'`);
+			}))
+			.andWhere('note.localOnly = FALSE');
+
+		const notes = await query.take(limit).getMany();
+
+		if (sinceId) notes.reverse();
+
+		const activities = await Promise.all(notes.map(note => packActivity(note)));
+		const rendered = renderOrderedCollectionPage(
+			`${partOf}?${url.query({
+				page: 'true',
+				since_id: sinceId,
+				until_id: untilId
+			})}`,
+			user.notesCount, activities, partOf,
+			notes.length ? `${partOf}?${url.query({
+				page: 'true',
+				since_id: notes[0].id
+			})}` : undefined,
+			notes.length ? `${partOf}?${url.query({
+				page: 'true',
+				until_id: notes[notes.length - 1].id
+			})}` : undefined
+		);
+
+		ctx.body = renderActivity(rendered);
+		setResponseType(ctx);
+	} else {
+		// index page
+		const rendered = renderOrderedCollection(partOf, user.notesCount,
+			`${partOf}?page=true`,
+			`${partOf}?page=true&since_id=000000000000000000000000`
+		);
+		ctx.body = renderActivity(rendered);
+		ctx.set('Cache-Control', 'public, max-age=180');
+		setResponseType(ctx);
+	}
+};
+
+/**
+ * Pack Create<Note> or Announce Activity
+ * @param note Note
+ */
+export async function packActivity(note: Note): Promise<any> {
+	if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
+		const renote = await Notes.findOneOrFail(note.renoteId);
+		return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note);
+	}
+
+	return renderCreate(await renderNote(note, false), note);
+}
diff --git a/packages/backend/src/server/api/2fa.ts b/packages/backend/src/server/api/2fa.ts
new file mode 100644
index 0000000000..117446383d
--- /dev/null
+++ b/packages/backend/src/server/api/2fa.ts
@@ -0,0 +1,422 @@
+import * as crypto from 'crypto';
+import config from '@/config/index';
+import * as jsrsasign from 'jsrsasign';
+
+const ECC_PRELUDE = Buffer.from([0x04]);
+const NULL_BYTE = Buffer.from([0]);
+const PEM_PRELUDE = Buffer.from(
+	'3059301306072a8648ce3d020106082a8648ce3d030107034200',
+	'hex'
+);
+
+// Android Safetynet attestations are signed with this cert:
+const GSR2 = `-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
+MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
+v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
+eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
+tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
+C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
+zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
+mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
+V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
+bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
+3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
+J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
+291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
+ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
+AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
+TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
+-----END CERTIFICATE-----\n`;
+
+function base64URLDecode(source: string) {
+	return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64');
+}
+
+function getCertSubject(certificate: string) {
+	const subjectCert = new jsrsasign.X509();
+	subjectCert.readCertPEM(certificate);
+
+	const subjectString = subjectCert.getSubjectString();
+	const subjectFields = subjectString.slice(1).split('/');
+
+	const fields = {} as Record<string, string>;
+	for (const field of subjectFields) {
+		const eqIndex = field.indexOf('=');
+		fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1);
+	}
+
+	return fields;
+}
+
+function verifyCertificateChain(certificates: string[]) {
+	let valid = true;
+
+	for (let i = 0; i < certificates.length; i++) {
+		const Cert = certificates[i];
+		const certificate = new jsrsasign.X509();
+		certificate.readCertPEM(Cert);
+
+		const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1];
+
+		const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
+		const algorithm = certificate.getSignatureAlgorithmField();
+		const signatureHex = certificate.getSignatureValueHex();
+
+		// Verify against CA
+		const Signature = new jsrsasign.KJUR.crypto.Signature({alg: algorithm});
+		Signature.init(CACert);
+		Signature.updateHex(certStruct);
+		valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate
+	}
+
+	return valid;
+}
+
+function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') {
+	if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) {
+		pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91);
+		type = 'PUBLIC KEY';
+	}
+	const cert = pemBuffer.toString('base64');
+
+	const keyParts = [];
+	const max = Math.ceil(cert.length / 64);
+	let start = 0;
+	for (let i = 0; i < max; i++) {
+		keyParts.push(cert.substring(start, start + 64));
+		start += 64;
+	}
+
+	return (
+		`-----BEGIN ${type}-----\n` +
+		keyParts.join('\n') +
+		`\n-----END ${type}-----\n`
+	);
+}
+
+export function hash(data: Buffer) {
+	return crypto
+		.createHash('sha256')
+		.update(data)
+		.digest();
+}
+
+export function verifyLogin({
+	publicKey,
+	authenticatorData,
+	clientDataJSON,
+	clientData,
+	signature,
+	challenge
+}: {
+	publicKey: Buffer,
+	authenticatorData: Buffer,
+	clientDataJSON: Buffer,
+	clientData: any,
+	signature: Buffer,
+	challenge: string
+}) {
+	if (clientData.type != 'webauthn.get') {
+		throw new Error('type is not webauthn.get');
+	}
+
+	if (hash(clientData.challenge).toString('hex') != challenge) {
+		throw new Error('challenge mismatch');
+	}
+	if (clientData.origin != config.scheme + '://' + config.host) {
+		throw new Error('origin mismatch');
+	}
+
+	const verificationData = Buffer.concat(
+		[authenticatorData, hash(clientDataJSON)],
+		32 + authenticatorData.length
+	);
+
+	return crypto
+		.createVerify('SHA256')
+		.update(verificationData)
+		.verify(PEMString(publicKey), signature);
+}
+
+export const procedures = {
+	none: {
+		verify({publicKey}: {publicKey: Map<number, Buffer>}) {
+			const negTwo = publicKey.get(-2);
+
+			if (!negTwo || negTwo.length != 32) {
+				throw new Error('invalid or no -2 key given');
+			}
+			const negThree = publicKey.get(-3);
+			if (!negThree || negThree.length != 32) {
+				throw new Error('invalid or no -3 key given');
+			}
+
+			const publicKeyU2F = Buffer.concat(
+				[ECC_PRELUDE, negTwo, negThree],
+				1 + 32 + 32
+			);
+
+			return {
+				publicKey: publicKeyU2F,
+				valid: true
+			};
+		}
+	},
+	'android-key': {
+		verify({
+			attStmt,
+			authenticatorData,
+			clientDataHash,
+			publicKey,
+			rpIdHash,
+			credentialId
+		}: {
+			attStmt: any,
+			authenticatorData: Buffer,
+			clientDataHash: Buffer,
+			publicKey: Map<number, any>;
+			rpIdHash: Buffer,
+			credentialId: Buffer,
+		}) {
+			if (attStmt.alg != -7) {
+				throw new Error('alg mismatch');
+			}
+
+			const verificationData = Buffer.concat([
+				authenticatorData,
+				clientDataHash
+			]);
+
+			const attCert: Buffer = attStmt.x5c[0];
+
+			const negTwo = publicKey.get(-2);
+
+			if (!negTwo || negTwo.length != 32) {
+				throw new Error('invalid or no -2 key given');
+			}
+			const negThree = publicKey.get(-3);
+			if (!negThree || negThree.length != 32) {
+				throw new Error('invalid or no -3 key given');
+			}
+
+			const publicKeyData = Buffer.concat(
+				[ECC_PRELUDE, negTwo, negThree],
+				1 + 32 + 32
+			);
+
+			if (!attCert.equals(publicKeyData)) {
+				throw new Error('public key mismatch');
+			}
+
+			const isValid = crypto
+				.createVerify('SHA256')
+				.update(verificationData)
+				.verify(PEMString(attCert), attStmt.sig);
+
+			// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
+
+			return {
+				valid: isValid,
+				publicKey: publicKeyData
+			};
+		}
+	},
+	// what a stupid attestation
+	'android-safetynet': {
+		verify({
+			attStmt,
+			authenticatorData,
+			clientDataHash,
+			publicKey,
+			rpIdHash,
+			credentialId
+		}: {
+			attStmt: any,
+			authenticatorData: Buffer,
+			clientDataHash: Buffer,
+			publicKey: Map<number, any>;
+			rpIdHash: Buffer,
+			credentialId: Buffer,
+		}) {
+			const verificationData = hash(
+				Buffer.concat([authenticatorData, clientDataHash])
+			);
+
+			const jwsParts = attStmt.response.toString('utf-8').split('.');
+
+			const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
+			const response = JSON.parse(
+				base64URLDecode(jwsParts[1]).toString('utf-8')
+			);
+			const signature = jwsParts[2];
+
+			if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
+				throw new Error('invalid nonce');
+			}
+
+			const certificateChain = header.x5c
+				.map((key: any) => PEMString(key))
+				.concat([GSR2]);
+
+			if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') {
+				throw new Error('invalid common name');
+			}
+
+			if (!verifyCertificateChain(certificateChain)) {
+				throw new Error('Invalid certificate chain!');
+			}
+
+			const signatureBase = Buffer.from(
+				jwsParts[0] + '.' + jwsParts[1],
+				'utf-8'
+			);
+
+			const valid = crypto
+				.createVerify('sha256')
+				.update(signatureBase)
+				.verify(certificateChain[0], base64URLDecode(signature));
+
+			const negTwo = publicKey.get(-2);
+
+			if (!negTwo || negTwo.length != 32) {
+				throw new Error('invalid or no -2 key given');
+			}
+			const negThree = publicKey.get(-3);
+			if (!negThree || negThree.length != 32) {
+				throw new Error('invalid or no -3 key given');
+			}
+
+			const publicKeyData = Buffer.concat(
+				[ECC_PRELUDE, negTwo, negThree],
+				1 + 32 + 32
+			);
+			return {
+				valid,
+				publicKey: publicKeyData
+			};
+		}
+	},
+	packed: {
+		verify({
+			attStmt,
+			authenticatorData,
+			clientDataHash,
+			publicKey,
+			rpIdHash,
+			credentialId
+		}: {
+			attStmt: any,
+			authenticatorData: Buffer,
+			clientDataHash: Buffer,
+			publicKey: Map<number, any>;
+			rpIdHash: Buffer,
+			credentialId: Buffer,
+		}) {
+			const verificationData = Buffer.concat([
+				authenticatorData,
+				clientDataHash
+			]);
+
+			if (attStmt.x5c) {
+				const attCert = attStmt.x5c[0];
+
+				const validSignature = crypto
+					.createVerify('SHA256')
+					.update(verificationData)
+					.verify(PEMString(attCert), attStmt.sig);
+
+				const negTwo = publicKey.get(-2);
+
+				if (!negTwo || negTwo.length != 32) {
+					throw new Error('invalid or no -2 key given');
+				}
+				const negThree = publicKey.get(-3);
+				if (!negThree || negThree.length != 32) {
+					throw new Error('invalid or no -3 key given');
+				}
+
+				const publicKeyData = Buffer.concat(
+					[ECC_PRELUDE, negTwo, negThree],
+					1 + 32 + 32
+				);
+
+				return {
+					valid: validSignature,
+					publicKey: publicKeyData
+				};
+			} else if (attStmt.ecdaaKeyId) {
+				// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
+				throw new Error('ECDAA-Verify is not supported');
+			} else {
+				if (attStmt.alg != -7) throw new Error('alg mismatch');
+
+				throw new Error('self attestation is not supported');
+			}
+		}
+	},
+
+	'fido-u2f': {
+		verify({
+			attStmt,
+			authenticatorData,
+			clientDataHash,
+			publicKey,
+			rpIdHash,
+			credentialId
+		}: {
+			attStmt: any,
+			authenticatorData: Buffer,
+			clientDataHash: Buffer,
+			publicKey: Map<number, any>,
+			rpIdHash: Buffer,
+			credentialId: Buffer
+		}) {
+			const x5c: Buffer[] = attStmt.x5c;
+			if (x5c.length != 1) {
+				throw new Error('x5c length does not match expectation');
+			}
+
+			const attCert = x5c[0];
+
+			// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
+
+			const negTwo: Buffer = publicKey.get(-2);
+
+			if (!negTwo || negTwo.length != 32) {
+				throw new Error('invalid or no -2 key given');
+			}
+			const negThree: Buffer = publicKey.get(-3);
+			if (!negThree || negThree.length != 32) {
+				throw new Error('invalid or no -3 key given');
+			}
+
+			const publicKeyU2F = Buffer.concat(
+				[ECC_PRELUDE, negTwo, negThree],
+				1 + 32 + 32
+			);
+
+			const verificationData = Buffer.concat([
+				NULL_BYTE,
+				rpIdHash,
+				clientDataHash,
+				credentialId,
+				publicKeyU2F
+			]);
+
+			const validSignature = crypto
+				.createVerify('SHA256')
+				.update(verificationData)
+				.verify(PEMString(attCert), attStmt.sig);
+
+			return {
+				valid: validSignature,
+				publicKey: publicKeyU2F
+			};
+		}
+	}
+};
diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts
new file mode 100644
index 0000000000..cbace8917e
--- /dev/null
+++ b/packages/backend/src/server/api/api-handler.ts
@@ -0,0 +1,51 @@
+import * as Koa from 'koa';
+
+import { IEndpoint } from './endpoints';
+import authenticate, { AuthenticationError } from './authenticate';
+import call from './call';
+import { ApiError } from './error';
+
+export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
+	const body = ctx.request.body;
+
+	const reply = (x?: any, y?: ApiError) => {
+		if (x == null) {
+			ctx.status = 204;
+		} else if (typeof x === 'number' && y) {
+			ctx.status = x;
+			ctx.body = {
+				error: {
+					message: y!.message,
+					code: y!.code,
+					id: y!.id,
+					kind: y!.kind,
+					...(y!.info ? { info: y!.info } : {})
+				}
+			};
+		} else {
+			// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
+			ctx.body = typeof x === 'string' ? JSON.stringify(x) : x;
+		}
+		res();
+	};
+
+	// Authentication
+	authenticate(body['i']).then(([user, app]) => {
+		// API invoking
+		call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => {
+			reply(res);
+		}).catch((e: ApiError) => {
+			reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
+		});
+	}).catch(e => {
+		if (e instanceof AuthenticationError) {
+			reply(403, new ApiError({
+				message: 'Authentication failed. Please ensure your token is correct.',
+				code: 'AUTHENTICATION_FAILED',
+				id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14'
+			}));
+		} else {
+			reply(500, new ApiError());
+		}
+	});
+});
diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts
new file mode 100644
index 0000000000..b8e216edc4
--- /dev/null
+++ b/packages/backend/src/server/api/authenticate.ts
@@ -0,0 +1,62 @@
+import isNativeToken from './common/is-native-token';
+import { User } from '@/models/entities/user';
+import { Users, AccessTokens, Apps } from '@/models/index';
+import { AccessToken } from '@/models/entities/access-token';
+
+export class AuthenticationError extends Error {
+	constructor(message: string) {
+		super(message);
+		this.name = 'AuthenticationError';
+	}
+}
+
+export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => {
+	if (token == null) {
+		return [null, null];
+	}
+
+	if (isNativeToken(token)) {
+		// Fetch user
+		const user = await Users
+			.findOne({ token });
+
+		if (user == null) {
+			throw new AuthenticationError('user not found');
+		}
+
+		return [user, null];
+	} else {
+		const accessToken = await AccessTokens.findOne({
+			where: [{
+				hash: token.toLowerCase() // app
+			}, {
+				token: token // miauth
+			}],
+		});
+
+		if (accessToken == null) {
+			throw new AuthenticationError('invalid signature');
+		}
+
+		AccessTokens.update(accessToken.id, {
+			lastUsedAt: new Date(),
+		});
+
+		const user = await Users
+			.findOne({
+				id: accessToken.userId // findOne(accessToken.userId) のように書かないのは後方互換性のため
+			});
+
+		if (accessToken.appId) {
+			const app = await Apps
+				.findOneOrFail(accessToken.appId);
+
+			return [user, {
+				id: accessToken.id,
+				permission: app.permission
+			} as AccessToken];
+		} else {
+			return [user, accessToken];
+		}
+	}
+};
diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts
new file mode 100644
index 0000000000..bd86ffdc35
--- /dev/null
+++ b/packages/backend/src/server/api/call.ts
@@ -0,0 +1,109 @@
+import { performance } from 'perf_hooks';
+import limiter from './limiter';
+import { User } from '@/models/entities/user';
+import endpoints from './endpoints';
+import { ApiError } from './error';
+import { apiLogger } from './logger';
+import { AccessToken } from '@/models/entities/access-token';
+
+const accessDenied = {
+	message: 'Access denied.',
+	code: 'ACCESS_DENIED',
+	id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e'
+};
+
+export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => {
+	const isSecure = user != null && token == null;
+
+	const ep = endpoints.find(e => e.name === endpoint);
+
+	if (ep == null) {
+		throw new ApiError({
+			message: 'No such endpoint.',
+			code: 'NO_SUCH_ENDPOINT',
+			id: 'f8080b67-5f9c-4eb7-8c18-7f1eeae8f709',
+			httpStatusCode: 404
+		});
+	}
+
+	if (ep.meta.secure && !isSecure) {
+		throw new ApiError(accessDenied);
+	}
+
+	if (ep.meta.requireCredential && user == null) {
+		throw new ApiError({
+			message: 'Credential required.',
+			code: 'CREDENTIAL_REQUIRED',
+			id: '1384574d-a912-4b81-8601-c7b1c4085df1',
+			httpStatusCode: 401
+		});
+	}
+
+	if (ep.meta.requireCredential && user!.isSuspended) {
+		throw new ApiError({
+			message: 'Your account has been suspended.',
+			code: 'YOUR_ACCOUNT_SUSPENDED',
+			id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
+			httpStatusCode: 403
+		});
+	}
+
+	if (ep.meta.requireAdmin && !user!.isAdmin) {
+		throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
+	}
+
+	if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
+		throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
+	}
+
+	if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
+		throw new ApiError({
+			message: 'Your app does not have the necessary permissions to use this endpoint.',
+			code: 'PERMISSION_DENIED',
+			id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
+		});
+	}
+
+	if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
+		// Rate limit
+		await limiter(ep, user!).catch(e => {
+			throw new ApiError({
+				message: 'Rate limit exceeded. Please try again later.',
+				code: 'RATE_LIMIT_EXCEEDED',
+				id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+				httpStatusCode: 429
+			});
+		});
+	}
+
+	// API invoking
+	const before = performance.now();
+	return await ep.exec(data, user, token, file).catch((e: Error) => {
+		if (e instanceof ApiError) {
+			throw e;
+		} else {
+			apiLogger.error(`Internal error occurred in ${ep.name}: ${e?.message}`, {
+				ep: ep.name,
+				ps: data,
+				e: {
+					message: e?.message,
+					code: e?.name,
+					stack: e?.stack
+				}
+			});
+			throw new ApiError(null, {
+				e: {
+					message: e?.message,
+					code: e?.name,
+					stack: e?.stack
+				}
+			});
+		}
+	}).finally(() => {
+		const after = performance.now();
+		const time = after - before;
+		if (time > 1000) {
+			apiLogger.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`);
+		}
+	});
+};
diff --git a/packages/backend/src/server/api/common/generate-block-query.ts b/packages/backend/src/server/api/common/generate-block-query.ts
new file mode 100644
index 0000000000..4fd6184738
--- /dev/null
+++ b/packages/backend/src/server/api/common/generate-block-query.ts
@@ -0,0 +1,42 @@
+import { User } from '@/models/entities/user';
+import { Blockings } from '@/models/index';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+// ここでいうBlockedは被Blockedの意
+export function generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
+	const blockingQuery = Blockings.createQueryBuilder('blocking')
+		.select('blocking.blockerId')
+		.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
+
+	// 投稿の作者にブロックされていない かつ
+	// 投稿の返信先の作者にブロックされていない かつ
+	// 投稿の引用元の作者にブロックされていない
+	q
+		.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.replyUserId IS NULL`)
+			.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
+		}))
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.renoteUserId IS NULL`)
+			.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
+		}));
+
+	q.setParameters(blockingQuery.getParameters());
+}
+
+export function generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
+	const blockingQuery = Blockings.createQueryBuilder('blocking')
+		.select('blocking.blockeeId')
+		.where('blocking.blockerId = :blockerId', { blockerId: me.id });
+
+	const blockedQuery = Blockings.createQueryBuilder('blocking')
+		.select('blocking.blockerId')
+		.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
+
+	q.andWhere(`user.id NOT IN (${ blockingQuery.getQuery() })`);
+	q.setParameters(blockingQuery.getParameters());
+
+	q.andWhere(`user.id NOT IN (${ blockedQuery.getQuery() })`);
+	q.setParameters(blockedQuery.getParameters());
+}
diff --git a/packages/backend/src/server/api/common/generate-channel-query.ts b/packages/backend/src/server/api/common/generate-channel-query.ts
new file mode 100644
index 0000000000..80a0acf7f9
--- /dev/null
+++ b/packages/backend/src/server/api/common/generate-channel-query.ts
@@ -0,0 +1,24 @@
+import { User } from '@/models/entities/user';
+import { ChannelFollowings } from '@/models/index';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+export function generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
+	if (me == null) {
+		q.andWhere('note.channelId IS NULL');
+	} else {
+		q.leftJoinAndSelect('note.channel', 'channel');
+
+		const channelFollowingQuery = ChannelFollowings.createQueryBuilder('channelFollowing')
+			.select('channelFollowing.followeeId')
+			.where('channelFollowing.followerId = :followerId', { followerId: me.id });
+
+		q.andWhere(new Brackets(qb => { qb
+			// チャンネルのノートではない
+			.where('note.channelId IS NULL')
+			// または自分がフォローしているチャンネルのノート
+			.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
+		}));
+
+		q.setParameters(channelFollowingQuery.getParameters());
+	}
+}
diff --git a/packages/backend/src/server/api/common/generate-muted-note-query.ts b/packages/backend/src/server/api/common/generate-muted-note-query.ts
new file mode 100644
index 0000000000..0737842613
--- /dev/null
+++ b/packages/backend/src/server/api/common/generate-muted-note-query.ts
@@ -0,0 +1,13 @@
+import { User } from '@/models/entities/user';
+import { MutedNotes } from '@/models/index';
+import { SelectQueryBuilder } from 'typeorm';
+
+export function generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
+	const mutedQuery = MutedNotes.createQueryBuilder('muted')
+		.select('muted.noteId')
+		.where('muted.userId = :userId', { userId: me.id });
+
+	q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
+
+	q.setParameters(mutedQuery.getParameters());
+}
diff --git a/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts b/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts
new file mode 100644
index 0000000000..7e2cbd498b
--- /dev/null
+++ b/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts
@@ -0,0 +1,17 @@
+import { User } from '@/models/entities/user';
+import { NoteThreadMutings } from '@/models/index';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+export function generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
+	const mutedQuery = NoteThreadMutings.createQueryBuilder('threadMuted')
+		.select('threadMuted.threadId')
+		.where('threadMuted.userId = :userId', { userId: me.id });
+
+	q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
+	q.andWhere(new Brackets(qb => { qb
+		.where(`note.threadId IS NULL`)
+		.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
+	}));
+
+	q.setParameters(mutedQuery.getParameters());
+}
diff --git a/packages/backend/src/server/api/common/generate-muted-user-query.ts b/packages/backend/src/server/api/common/generate-muted-user-query.ts
new file mode 100644
index 0000000000..7e200b87ef
--- /dev/null
+++ b/packages/backend/src/server/api/common/generate-muted-user-query.ts
@@ -0,0 +1,40 @@
+import { User } from '@/models/entities/user';
+import { Mutings } from '@/models/index';
+import { SelectQueryBuilder, Brackets } from 'typeorm';
+
+export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) {
+	const mutingQuery = Mutings.createQueryBuilder('muting')
+		.select('muting.muteeId')
+		.where('muting.muterId = :muterId', { muterId: me.id });
+
+	if (exclude) {
+		mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
+	}
+
+	// 投稿の作者をミュートしていない かつ
+	// 投稿の返信先の作者をミュートしていない かつ
+	// 投稿の引用元の作者をミュートしていない
+	q
+		.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.replyUserId IS NULL`)
+			.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
+		}))
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.renoteUserId IS NULL`)
+			.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
+		}));
+
+	q.setParameters(mutingQuery.getParameters());
+}
+
+export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
+	const mutingQuery = Mutings.createQueryBuilder('muting')
+		.select('muting.muteeId')
+		.where('muting.muterId = :muterId', { muterId: me.id });
+
+	q
+		.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
+
+	q.setParameters(mutingQuery.getParameters());
+}
diff --git a/packages/backend/src/server/api/common/generate-native-user-token.ts b/packages/backend/src/server/api/common/generate-native-user-token.ts
new file mode 100644
index 0000000000..1f791c57ce
--- /dev/null
+++ b/packages/backend/src/server/api/common/generate-native-user-token.ts
@@ -0,0 +1,3 @@
+import { secureRndstr } from '@/misc/secure-rndstr';
+
+export default () => secureRndstr(16, true);
diff --git a/packages/backend/src/server/api/common/generate-replies-query.ts b/packages/backend/src/server/api/common/generate-replies-query.ts
new file mode 100644
index 0000000000..fbc41b2c25
--- /dev/null
+++ b/packages/backend/src/server/api/common/generate-replies-query.ts
@@ -0,0 +1,27 @@
+import { User } from '@/models/entities/user';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
+	if (me == null) {
+		q.andWhere(new Brackets(qb => { qb
+			.where(`note.replyId IS NULL`) // 返信ではない
+			.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
+				.where(`note.replyId IS NOT NULL`)
+				.andWhere('note.replyUserId = note.userId');
+			}));
+		}));
+	} else {
+		q.andWhere(new Brackets(qb => { qb
+			.where(`note.replyId IS NULL`) // 返信ではない
+			.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
+			.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
+				.where(`note.replyId IS NOT NULL`)
+				.andWhere('note.userId = :meId', { meId: me.id });
+			}))
+			.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
+				.where(`note.replyId IS NOT NULL`)
+				.andWhere('note.replyUserId = note.userId');
+			}));
+		}));
+	}
+}
diff --git a/packages/backend/src/server/api/common/generate-visibility-query.ts b/packages/backend/src/server/api/common/generate-visibility-query.ts
new file mode 100644
index 0000000000..813e8b6c09
--- /dev/null
+++ b/packages/backend/src/server/api/common/generate-visibility-query.ts
@@ -0,0 +1,40 @@
+import { User } from '@/models/entities/user';
+import { Followings } from '@/models/index';
+import { Brackets, SelectQueryBuilder } from 'typeorm';
+
+export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
+	if (me == null) {
+		q.andWhere(new Brackets(qb => { qb
+			.where(`note.visibility = 'public'`)
+			.orWhere(`note.visibility = 'home'`);
+		}));
+	} else {
+		const followingQuery = Followings.createQueryBuilder('following')
+			.select('following.followeeId')
+			.where('following.followerId = :followerId', { followerId: me.id });
+
+		q.andWhere(new Brackets(qb => { qb
+			// 公開投稿である
+			.where(new Brackets(qb => { qb
+				.where(`note.visibility = 'public'`)
+				.orWhere(`note.visibility = 'home'`);
+			}))
+			// または 自分自身
+			.orWhere('note.userId = :userId1', { userId1: me.id })
+			// または 自分宛て
+			.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`)
+			.orWhere(new Brackets(qb => { qb
+				// または フォロワー宛ての投稿であり、
+				.where('note.visibility = \'followers\'')
+				.andWhere(new Brackets(qb => { qb
+					// 自分がフォロワーである
+					.where(`note.userId IN (${ followingQuery.getQuery() })`)
+					// または 自分の投稿へのリプライ
+					.orWhere('note.replyUserId = :userId3', { userId3: me.id });
+				}));
+			}));
+		}));
+
+		q.setParameters(followingQuery.getParameters());
+	}
+}
diff --git a/packages/backend/src/server/api/common/getters.ts b/packages/backend/src/server/api/common/getters.ts
new file mode 100644
index 0000000000..4b2ee8f1da
--- /dev/null
+++ b/packages/backend/src/server/api/common/getters.ts
@@ -0,0 +1,56 @@
+import { IdentifiableError } from '@/misc/identifiable-error';
+import { User } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { Notes, Users } from '@/models/index';
+
+/**
+ * Get note for API processing
+ */
+export async function getNote(noteId: Note['id']) {
+	const note = await Notes.findOne(noteId);
+
+	if (note == null) {
+		throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
+	}
+
+	return note;
+}
+
+/**
+ * Get user for API processing
+ */
+export async function getUser(userId: User['id']) {
+	const user = await Users.findOne(userId);
+
+	if (user == null) {
+		throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.');
+	}
+
+	return user;
+}
+
+/**
+ * Get remote user for API processing
+ */
+export async function getRemoteUser(userId: User['id']) {
+	const user = await getUser(userId);
+
+	if (!Users.isRemoteUser(user)) {
+		throw new Error('user is not a remote user');
+	}
+
+	return user;
+}
+
+/**
+ * Get local user for API processing
+ */
+export async function getLocalUser(userId: User['id']) {
+	const user = await getUser(userId);
+
+	if (!Users.isLocalUser(user)) {
+		throw new Error('user is not a local user');
+	}
+
+	return user;
+}
diff --git a/packages/backend/src/server/api/common/inject-featured.ts b/packages/backend/src/server/api/common/inject-featured.ts
new file mode 100644
index 0000000000..1dc13c83ef
--- /dev/null
+++ b/packages/backend/src/server/api/common/inject-featured.ts
@@ -0,0 +1,56 @@
+import rndstr from 'rndstr';
+import { Note } from '@/models/entities/note';
+import { User } from '@/models/entities/user';
+import { Notes, UserProfiles, NoteReactions } from '@/models/index';
+import { generateMutedUserQuery } from './generate-muted-user-query';
+import { generateBlockedUserQuery } from './generate-block-query';
+
+// TODO: リアクション、Renote、返信などをしたノートは除外する
+
+export async function injectFeatured(timeline: Note[], user?: User | null) {
+	if (timeline.length < 5) return;
+
+	if (user) {
+		const profile = await UserProfiles.findOneOrFail(user.id);
+		if (!profile.injectFeaturedNote) return;
+	}
+
+	const max = 30;
+	const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
+
+	const query = Notes.createQueryBuilder('note')
+		.addSelect('note.score')
+		.where('note.userHost IS NULL')
+		.andWhere(`note.score > 0`)
+		.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
+		.andWhere(`note.visibility = 'public'`)
+		.innerJoinAndSelect('note.user', 'user');
+
+	if (user) {
+		query.andWhere('note.userId != :userId', { userId: user.id });
+
+		generateMutedUserQuery(query, user);
+		generateBlockedUserQuery(query, user);
+
+		const reactionQuery = NoteReactions.createQueryBuilder('reaction')
+			.select('reaction.noteId')
+			.where('reaction.userId = :userId', { userId: user.id });
+
+		query.andWhere(`note.id NOT IN (${ reactionQuery.getQuery() })`);
+	}
+
+	const notes = await query
+		.orderBy('note.score', 'DESC')
+		.take(max)
+		.getMany();
+
+	if (notes.length === 0) return;
+
+	// Pick random one
+	const featured = notes[Math.floor(Math.random() * notes.length)];
+
+	(featured as any)._featuredId_ = rndstr('a-z0-9', 8);
+
+	// Inject featured
+	timeline.splice(3, 0, featured);
+}
diff --git a/packages/backend/src/server/api/common/inject-promo.ts b/packages/backend/src/server/api/common/inject-promo.ts
new file mode 100644
index 0000000000..87767a65bf
--- /dev/null
+++ b/packages/backend/src/server/api/common/inject-promo.ts
@@ -0,0 +1,34 @@
+import rndstr from 'rndstr';
+import { Note } from '@/models/entities/note';
+import { User } from '@/models/entities/user';
+import { PromoReads, PromoNotes, Notes, Users } from '@/models/index';
+
+export async function injectPromo(timeline: Note[], user?: User | null) {
+	if (timeline.length < 5) return;
+
+	// TODO: readやexpireフィルタはクエリ側でやる
+
+	const reads = user ? await PromoReads.find({
+		userId: user.id
+	}) : [];
+
+	let promos = await PromoNotes.find();
+
+	promos = promos.filter(n => n.expiresAt.getTime() > Date.now());
+	promos = promos.filter(n => !reads.map(r => r.noteId).includes(n.noteId));
+
+	if (promos.length === 0) return;
+
+	// Pick random promo
+	const promo = promos[Math.floor(Math.random() * promos.length)];
+
+	const note = await Notes.findOneOrFail(promo.noteId);
+
+	// Join
+	note.user = await Users.findOneOrFail(note.userId);
+
+	(note as any)._prId_ = rndstr('a-z0-9', 8);
+
+	// Inject promo
+	timeline.splice(3, 0, note);
+}
diff --git a/packages/backend/src/server/api/common/is-native-token.ts b/packages/backend/src/server/api/common/is-native-token.ts
new file mode 100644
index 0000000000..2833c570c8
--- /dev/null
+++ b/packages/backend/src/server/api/common/is-native-token.ts
@@ -0,0 +1 @@
+export default (token: string) => token.length === 16;
diff --git a/packages/backend/src/server/api/common/make-pagination-query.ts b/packages/backend/src/server/api/common/make-pagination-query.ts
new file mode 100644
index 0000000000..51c11e5dff
--- /dev/null
+++ b/packages/backend/src/server/api/common/make-pagination-query.ts
@@ -0,0 +1,28 @@
+import { SelectQueryBuilder } from 'typeorm';
+
+export function makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number) {
+	if (sinceId && untilId) {
+		q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
+		q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
+		q.orderBy(`${q.alias}.id`, 'DESC');
+	} else if (sinceId) {
+		q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
+		q.orderBy(`${q.alias}.id`, 'ASC');
+	} else if (untilId) {
+		q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
+		q.orderBy(`${q.alias}.id`, 'DESC');
+	} else if (sinceDate && untilDate) {
+		q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
+		q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
+		q.orderBy(`${q.alias}.createdAt`, 'DESC');
+	} else if (sinceDate) {
+		q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
+		q.orderBy(`${q.alias}.createdAt`, 'ASC');
+	} else if (untilDate) {
+		q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
+		q.orderBy(`${q.alias}.createdAt`, 'DESC');
+	} else {
+		q.orderBy(`${q.alias}.id`, 'DESC');
+	}
+	return q;
+}
diff --git a/packages/backend/src/server/api/common/read-messaging-message.ts b/packages/backend/src/server/api/common/read-messaging-message.ts
new file mode 100644
index 0000000000..33f41b2770
--- /dev/null
+++ b/packages/backend/src/server/api/common/read-messaging-message.ts
@@ -0,0 +1,122 @@
+import { publishMainStream, publishGroupMessagingStream } from '@/services/stream';
+import { publishMessagingStream } from '@/services/stream';
+import { publishMessagingIndexStream } from '@/services/stream';
+import { User, IRemoteUser } from '@/models/entities/user';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index';
+import { In } from 'typeorm';
+import { IdentifiableError } from '@/misc/identifiable-error';
+import { UserGroup } from '@/models/entities/user-group';
+import { toArray } from '@/prelude/array';
+import { renderReadActivity } from '@/remote/activitypub/renderer/read';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { deliver } from '@/queue/index';
+import orderedCollection from '@/remote/activitypub/renderer/ordered-collection';
+
+/**
+ * Mark messages as read
+ */
+export async function readUserMessagingMessage(
+	userId: User['id'],
+	otherpartyId: User['id'],
+	messageIds: MessagingMessage['id'][]
+) {
+	if (messageIds.length === 0) return;
+
+	const messages = await MessagingMessages.find({
+		id: In(messageIds)
+	});
+
+	for (const message of messages) {
+		if (message.recipientId !== userId) {
+			throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).');
+		}
+	}
+
+	// Update documents
+	await MessagingMessages.update({
+		id: In(messageIds),
+		userId: otherpartyId,
+		recipientId: userId,
+		isRead: false
+	}, {
+		isRead: true
+	});
+
+	// Publish event
+	publishMessagingStream(otherpartyId, userId, 'read', messageIds);
+	publishMessagingIndexStream(userId, 'read', messageIds);
+
+	if (!await Users.getHasUnreadMessagingMessage(userId)) {
+		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
+		publishMainStream(userId, 'readAllMessagingMessages');
+	}
+}
+
+/**
+ * Mark messages as read
+ */
+export async function readGroupMessagingMessage(
+	userId: User['id'],
+	groupId: UserGroup['id'],
+	messageIds: MessagingMessage['id'][]
+) {
+	if (messageIds.length === 0) return;
+
+	// check joined
+	const joining = await UserGroupJoinings.findOne({
+		userId: userId,
+		userGroupId: groupId
+	});
+
+	if (joining == null) {
+		throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).');
+	}
+
+	const messages = await MessagingMessages.find({
+		id: In(messageIds)
+	});
+
+	const reads: MessagingMessage['id'][] = [];
+
+	for (const message of messages) {
+		if (message.userId === userId) continue;
+		if (message.reads.includes(userId)) continue;
+
+		// Update document
+		await MessagingMessages.createQueryBuilder().update()
+			.set({
+				reads: (() => `array_append("reads", '${joining.userId}')`) as any
+			})
+			.where('id = :id', { id: message.id })
+			.execute();
+
+		reads.push(message.id);
+	}
+
+	// Publish event
+	publishGroupMessagingStream(groupId, 'read', {
+		ids: reads,
+		userId: userId
+	});
+	publishMessagingIndexStream(userId, 'read', reads);
+
+	if (!await Users.getHasUnreadMessagingMessage(userId)) {
+		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
+		publishMainStream(userId, 'readAllMessagingMessages');
+	}
+}
+
+export async function deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) {
+	messages = toArray(messages).filter(x => x.uri);
+	const contents = messages.map(x => renderReadActivity(user, x));
+
+	if (contents.length > 1) {
+		const collection = orderedCollection(null, contents.length, undefined, undefined, contents);
+		deliver(user, renderActivity(collection), recipient.inbox);
+	} else {
+		for (const content of contents) {
+			deliver(user, renderActivity(content), recipient.inbox);
+		}
+	}
+}
diff --git a/packages/backend/src/server/api/common/read-notification.ts b/packages/backend/src/server/api/common/read-notification.ts
new file mode 100644
index 0000000000..a4406c9eeb
--- /dev/null
+++ b/packages/backend/src/server/api/common/read-notification.ts
@@ -0,0 +1,43 @@
+import { publishMainStream } from '@/services/stream';
+import { User } from '@/models/entities/user';
+import { Notification } from '@/models/entities/notification';
+import { Notifications, Users } from '@/models/index';
+import { In } from 'typeorm';
+
+export async function readNotification(
+	userId: User['id'],
+	notificationIds: Notification['id'][]
+) {
+	// Update documents
+	await Notifications.update({
+		id: In(notificationIds),
+		isRead: false
+	}, {
+		isRead: true
+	});
+
+	post(userId);
+}
+
+export async function readNotificationByQuery(
+	userId: User['id'],
+	query: Record<string, any>
+) {
+	// Update documents
+	await Notifications.update({
+		...query,
+		notifieeId: userId,
+		isRead: false
+	}, {
+		isRead: true
+	});
+
+	post(userId);
+}
+
+async function post(userId: User['id']) {
+	if (!await Users.getHasUnreadNotification(userId)) {
+		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
+		publishMainStream(userId, 'readAllNotifications');
+	}
+}
diff --git a/packages/backend/src/server/api/common/signin.ts b/packages/backend/src/server/api/common/signin.ts
new file mode 100644
index 0000000000..4c7aacf1cd
--- /dev/null
+++ b/packages/backend/src/server/api/common/signin.ts
@@ -0,0 +1,44 @@
+import * as Koa from 'koa';
+
+import config from '@/config/index';
+import { ILocalUser } from '@/models/entities/user';
+import { Signins } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { publishMainStream } from '@/services/stream';
+
+export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
+	if (redirect) {
+		//#region Cookie
+		ctx.cookies.set('igi', user.token, {
+			path: '/',
+			// SEE: https://github.com/koajs/koa/issues/974
+			// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
+			secure: config.url.startsWith('https'),
+			httpOnly: false
+		});
+		//#endregion
+
+		ctx.redirect(config.url);
+	} else {
+		ctx.body = {
+			id: user.id,
+			i: user.token
+		};
+		ctx.status = 200;
+	}
+
+	(async () => {
+		// Append signin history
+		const record = await Signins.save({
+			id: genId(),
+			createdAt: new Date(),
+			userId: user.id,
+			ip: ctx.ip,
+			headers: ctx.headers,
+			success: true
+		});
+
+		// Publish signin event
+		publishMainStream(user.id, 'signin', await Signins.pack(record));
+	})();
+}
diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts
new file mode 100644
index 0000000000..2ba0d8e479
--- /dev/null
+++ b/packages/backend/src/server/api/common/signup.ts
@@ -0,0 +1,113 @@
+import * as bcrypt from 'bcryptjs';
+import { generateKeyPair } from 'crypto';
+import generateUserToken from './generate-native-user-token';
+import { User } from '@/models/entities/user';
+import { Users, UsedUsernames } from '@/models/index';
+import { UserProfile } from '@/models/entities/user-profile';
+import { getConnection } from 'typeorm';
+import { genId } from '@/misc/gen-id';
+import { toPunyNullable } from '@/misc/convert-host';
+import { UserKeypair } from '@/models/entities/user-keypair';
+import { usersChart } from '@/services/chart/index';
+import { UsedUsername } from '@/models/entities/used-username';
+
+export async function signup(opts: {
+	username: User['username'];
+	password?: string | null;
+	passwordHash?: UserProfile['password'] | null;
+	host?: string | null;
+}) {
+	const { username, password, passwordHash, host } = opts;
+	let hash = passwordHash;
+
+	// Validate username
+	if (!Users.validateLocalUsername.ok(username)) {
+		throw new Error('INVALID_USERNAME');
+	}
+
+	if (password != null && passwordHash == null) {
+		// Validate password
+		if (!Users.validatePassword.ok(password)) {
+			throw new Error('INVALID_PASSWORD');
+		}
+
+		// Generate hash of password
+		const salt = await bcrypt.genSalt(8);
+		hash = await bcrypt.hash(password, salt);
+	}
+
+	// Generate secret
+	const secret = generateUserToken();
+
+	// Check username duplication
+	if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
+		throw new Error('DUPLICATED_USERNAME');
+	}
+
+	// Check deleted username duplication
+	if (await UsedUsernames.findOne({ username: username.toLowerCase() })) {
+		throw new Error('USED_USERNAME');
+	}
+
+	const keyPair = await new Promise<string[]>((res, rej) =>
+		generateKeyPair('rsa', {
+			modulusLength: 4096,
+			publicKeyEncoding: {
+				type: 'spki',
+				format: 'pem'
+			},
+			privateKeyEncoding: {
+				type: 'pkcs8',
+				format: 'pem',
+				cipher: undefined,
+				passphrase: undefined
+			}
+		} as any, (err, publicKey, privateKey) =>
+			err ? rej(err) : res([publicKey, privateKey])
+		));
+
+	let account!: User;
+
+	// Start transaction
+	await getConnection().transaction(async transactionalEntityManager => {
+		const exist = await transactionalEntityManager.findOne(User, {
+			usernameLower: username.toLowerCase(),
+			host: null
+		});
+
+		if (exist) throw new Error(' the username is already used');
+
+		account = await transactionalEntityManager.save(new User({
+			id: genId(),
+			createdAt: new Date(),
+			username: username,
+			usernameLower: username.toLowerCase(),
+			host: toPunyNullable(host),
+			token: secret,
+			isAdmin: (await Users.count({
+				host: null,
+			})) === 0,
+		}));
+
+		await transactionalEntityManager.save(new UserKeypair({
+			publicKey: keyPair[0],
+			privateKey: keyPair[1],
+			userId: account.id
+		}));
+
+		await transactionalEntityManager.save(new UserProfile({
+			userId: account.id,
+			autoAcceptFollowed: true,
+			password: hash,
+		}));
+
+		await transactionalEntityManager.save(new UsedUsername({
+			createdAt: new Date(),
+			username: username.toLowerCase(),
+		}));
+	});
+
+	usersChart.update(account, true);
+
+	return { account, secret };
+}
diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts
new file mode 100644
index 0000000000..4bd8f95e31
--- /dev/null
+++ b/packages/backend/src/server/api/define.ts
@@ -0,0 +1,87 @@
+import * as fs from 'fs';
+import { ILocalUser } from '@/models/entities/user';
+import { IEndpointMeta } from './endpoints';
+import { ApiError } from './error';
+import { SchemaType } from '@/misc/schema';
+import { AccessToken } from '@/models/entities/access-token';
+
+type NonOptional<T> = T extends undefined ? never : T;
+
+type SimpleUserInfo = {
+	id: ILocalUser['id'];
+	host: ILocalUser['host'];
+	username: ILocalUser['username'];
+	uri: ILocalUser['uri'];
+	inbox: ILocalUser['inbox'];
+	sharedInbox: ILocalUser['sharedInbox'];
+	isAdmin: ILocalUser['isAdmin'];
+	isModerator: ILocalUser['isModerator'];
+	isSilenced: ILocalUser['isSilenced'];
+};
+
+type Params<T extends IEndpointMeta> = {
+	[P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function
+		? ReturnType<NonNullable<T['params']>[P]['transform']>
+		: NonNullable<T['params']>[P]['default'] extends null | number | string
+			? NonOptional<ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0]>
+			: ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0];
+};
+
+export type Response = Record<string, any> | void;
+
+type executor<T extends IEndpointMeta> =
+	(params: Params<T>, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any, cleanup?: Function) =>
+		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
+
+export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>)
+		: (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => Promise<any> {
+	return (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => {
+		function cleanup() {
+			fs.unlink(file.path, () => {});
+		}
+
+		if (meta.requireFile && file == null) return Promise.reject(new ApiError({
+			message: 'File required.',
+			code: 'FILE_REQUIRED',
+			id: '4267801e-70d1-416a-b011-4ee502885d8b',
+		}));
+
+		const [ps, pserr] = getParams(meta, params);
+		if (pserr) {
+			if (file) cleanup();
+			return Promise.reject(pserr);
+		}
+
+		return cb(ps, user, token, file, cleanup);
+	};
+}
+
+function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, ApiError | null] {
+	if (defs.params == null) return [params, null];
+
+	const x: any = {};
+	let err: ApiError | null = null;
+	Object.entries(defs.params).some(([k, def]) => {
+		const [v, e] = def.validator.get(params[k]);
+		if (e) {
+			err = new ApiError({
+				message: 'Invalid param.',
+				code: 'INVALID_PARAM',
+				id: '3d81ceae-475f-4600-b2a8-2bc116157532',
+			}, {
+				param: k,
+				reason: e.message
+			});
+			return true;
+		} else {
+			if (v === undefined && def.hasOwnProperty('default')) {
+				x[k] = def.default;
+			} else {
+				x[k] = v;
+			}
+			if (def.transform) x[k] = def.transform(x[k]);
+			return false;
+		}
+	});
+	return [x, err];
+}
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
new file mode 100644
index 0000000000..6d9d2b0782
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -0,0 +1,124 @@
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import { Context } from 'cafy';
+import * as path from 'path';
+import * as glob from 'glob';
+import { SimpleSchema } from '@/misc/simple-schema';
+
+//const _filename = fileURLToPath(import.meta.url);
+const _filename = __filename;
+const _dirname = dirname(_filename);
+
+export type Param = {
+	validator: Context<any>;
+	transform?: any;
+	default?: any;
+	deprecated?: boolean;
+	ref?: string;
+};
+
+export interface IEndpointMeta {
+	stability?: string; //'deprecated' | 'experimental' | 'stable';
+
+	tags?: string[];
+
+	params?: {
+		[key: string]: Param;
+	};
+
+	errors?: {
+		[key: string]: {
+			message: string;
+			code: string;
+			id: string;
+		};
+	};
+
+	res?: SimpleSchema;
+
+	/**
+	 * このエンドポイントにリクエストするのにユーザー情報が必須か否か
+	 * 省略した場合は false として解釈されます。
+	 */
+	requireCredential?: boolean;
+
+	/**
+	 * 管理者のみ使えるエンドポイントか否か
+	 */
+	requireAdmin?: boolean;
+
+	/**
+	 * 管理者またはモデレーターのみ使えるエンドポイントか否か
+	 */
+	requireModerator?: boolean;
+
+	/**
+	 * エンドポイントのリミテーションに関するやつ
+	 * 省略した場合はリミテーションは無いものとして解釈されます。
+	 * また、withCredential が false の場合はリミテーションを行うことはできません。
+	 */
+	limit?: {
+
+		/**
+		 * 複数のエンドポイントでリミットを共有したい場合に指定するキー
+		 */
+		key?: string;
+
+		/**
+		 * リミットを適用する期間(ms)
+		 * このプロパティを設定する場合、max プロパティも設定する必要があります。
+		 */
+		duration?: number;
+
+		/**
+		 * durationで指定した期間内にいくつまでリクエストできるのか
+		 * このプロパティを設定する場合、duration プロパティも設定する必要があります。
+		 */
+		max?: number;
+
+		/**
+		 * 最低でもどれくらいの間隔を開けてリクエストしなければならないか(ms)
+		 */
+		minInterval?: number;
+	};
+
+	/**
+	 * ファイルの添付を必要とするか否か
+	 * 省略した場合は false として解釈されます。
+	 */
+	requireFile?: boolean;
+
+	/**
+	 * サードパーティアプリからはリクエストすることができないか否か
+	 * 省略した場合は false として解釈されます。
+	 */
+	secure?: boolean;
+
+	/**
+	 * エンドポイントの種類
+	 * パーミッションの実現に利用されます。
+	 */
+	kind?: string;
+}
+
+export interface IEndpoint {
+	name: string;
+	exec: any;
+	meta: IEndpointMeta;
+}
+
+const files = glob.sync('**/*.js', {
+	cwd: path.resolve(_dirname + '/endpoints/')
+});
+
+const endpoints: IEndpoint[] = files.map(f => {
+	const ep = require(`./endpoints/${f}`);
+
+	return {
+		name: f.replace('.js', ''),
+		exec: ep.default,
+		meta: ep.meta || {}
+	};
+});
+
+export default endpoints;
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
new file mode 100644
index 0000000000..403eb24191
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -0,0 +1,134 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { AbuseUserReports } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		state: {
+			validator: $.optional.nullable.str,
+			default: null,
+		},
+
+		reporterOrigin: {
+			validator: $.optional.str.or([
+				'combined',
+				'local',
+				'remote',
+			]),
+			default: 'combined'
+		},
+
+		targetUserOrigin: {
+			validator: $.optional.str.or([
+				'combined',
+				'local',
+				'remote',
+			]),
+			default: 'combined'
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					nullable: false as const, optional: false as const,
+					format: 'id',
+					example: 'xxxxxxxxxx',
+				},
+				createdAt: {
+					type: 'string' as const,
+					nullable: false as const, optional: false as const,
+					format: 'date-time',
+				},
+				comment: {
+					type: 'string' as const,
+					nullable: false as const, optional: false as const,
+				},
+				resolved: {
+					type: 'boolean' as const,
+					nullable: false as const, optional: false as const,
+					example: false
+				},
+				reporterId: {
+					type: 'string' as const,
+					nullable: false as const, optional: false as const,
+					format: 'id',
+				},
+				targetUserId: {
+					type: 'string' as const,
+					nullable: false as const, optional: false as const,
+					format: 'id',
+				},
+				assigneeId: {
+					type: 'string' as const,
+					nullable: true as const, optional: false as const,
+					format: 'id',
+				},
+				reporter: {
+					type: 'object' as const,
+					nullable: false as const, optional: false as const,
+					ref: 'User'
+				},
+				targetUser: {
+					type: 'object' as const,
+					nullable: false as const, optional: false as const,
+					ref: 'User'
+				},
+				assignee: {
+					type: 'object' as const,
+					nullable: true as const, optional: true as const,
+					ref: 'User'
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId);
+
+	switch (ps.state) {
+		case 'resolved': query.andWhere('report.resolved = TRUE'); break;
+		case 'unresolved': query.andWhere('report.resolved = FALSE'); break;
+	}
+
+	switch (ps.reporterOrigin) {
+		case 'local': query.andWhere('report.reporterHost IS NULL'); break;
+		case 'remote': query.andWhere('report.reporterHost IS NOT NULL'); break;
+	}
+
+	switch (ps.targetUserOrigin) {
+		case 'local': query.andWhere('report.targetUserHost IS NULL'); break;
+		case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break;
+	}
+
+	const reports = await query.take(ps.limit!).getMany();
+
+	return await AbuseUserReports.packMany(reports);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
new file mode 100644
index 0000000000..fa15e84f77
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -0,0 +1,51 @@
+import define from '../../../define';
+import { Users } from '@/models/index';
+import { signup } from '../../../common/signup';
+
+export const meta = {
+	tags: ['admin'],
+
+	params: {
+		username: {
+			validator: Users.validateLocalUsername,
+		},
+
+		password: {
+			validator: Users.validatePassword,
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User',
+		properties: {
+			token: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, _me) => {
+	const me = _me ? await Users.findOneOrFail(_me.id) : null;
+	const noUsers = (await Users.count({
+		host: null,
+	})) === 0;
+	if (!noUsers && !me?.isAdmin) throw new Error('access denied');
+
+	const { account, secret } = await signup({
+		username: ps.username,
+		password: ps.password,
+	});
+
+	const res = await Users.pack(account, account, {
+		detail: true,
+		includeSecrets: true
+	});
+
+	(res as any).token = secret;
+
+	return res;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
new file mode 100644
index 0000000000..4e8a559805
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
@@ -0,0 +1,58 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Users } from '@/models/index';
+import { doPostSuspend } from '@/services/suspend-user';
+import { publishUserEvent } from '@/services/stream';
+import { createDeleteAccountJob } from '@/queue';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	if (user.isAdmin) {
+		throw new Error('cannot suspend admin');
+	}
+
+	if (user.isModerator) {
+		throw new Error('cannot suspend moderator');
+	}
+
+	if (Users.isLocalUser(user)) {
+		// 物理削除する前にDelete activityを送信する
+		await doPostSuspend(user).catch(e => {});
+
+		createDeleteAccountJob(user, {
+			soft: false
+		});
+	} else {
+		createDeleteAccountJob(user, {
+			soft: true // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
+		});
+	}
+
+	await Users.update(user.id, {
+		isDeleted: true,
+	});
+
+	if (Users.isLocalUser(user)) {
+		// Terminate streaming
+		publishUserEvent(user.id, 'terminate', {});
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
new file mode 100644
index 0000000000..27c7b5d318
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Ads } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		url: {
+			validator: $.str.min(1)
+		},
+		memo: {
+			validator: $.str
+		},
+		place: {
+			validator: $.str
+		},
+		priority: {
+			validator: $.str
+		},
+		ratio: {
+			validator: $.num.int().min(0)
+		},
+		expiresAt: {
+			validator: $.num.int()
+		},
+		imageUrl: {
+			validator: $.str.min(1)
+		}
+	},
+};
+
+export default define(meta, async (ps) => {
+	await Ads.insert({
+		id: genId(),
+		createdAt: new Date(),
+		expiresAt: new Date(ps.expiresAt),
+		url: ps.url,
+		imageUrl: ps.imageUrl,
+		priority: ps.priority,
+		ratio: ps.ratio,
+		place: ps.place,
+		memo: ps.memo,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts
new file mode 100644
index 0000000000..91934e1aab
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts
@@ -0,0 +1,34 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Ads } from '@/models/index';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		id: {
+			validator: $.type(ID)
+		}
+	},
+
+	errors: {
+		noSuchAd: {
+			message: 'No such ad.',
+			code: 'NO_SUCH_AD',
+			id: 'ccac9863-3a03-416e-b899-8a64041118b1'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const ad = await Ads.findOne(ps.id);
+
+	if (ad == null) throw new ApiError(meta.errors.noSuchAd);
+
+	await Ads.delete(ad.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts
new file mode 100644
index 0000000000..000aaaba9d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts
@@ -0,0 +1,36 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { Ads } from '@/models/index';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+};
+
+export default define(meta, async (ps) => {
+	const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId)
+		.andWhere('ad.expiresAt > :now', { now: new Date() });
+
+	const ads = await query.take(ps.limit!).getMany();
+
+	return ads;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
new file mode 100644
index 0000000000..36c87895c2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
@@ -0,0 +1,63 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Ads } from '@/models/index';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		id: {
+			validator: $.type(ID)
+		},
+		memo: {
+			validator: $.str
+		},
+		url: {
+			validator: $.str.min(1)
+		},
+		imageUrl: {
+			validator: $.str.min(1)
+		},
+		place: {
+			validator: $.str
+		},
+		priority: {
+			validator: $.str
+		},
+		ratio: {
+			validator: $.num.int().min(0)
+		},
+		expiresAt: {
+			validator: $.num.int()
+		},
+	},
+
+	errors: {
+		noSuchAd: {
+			message: 'No such ad.',
+			code: 'NO_SUCH_AD',
+			id: 'b7aa1727-1354-47bc-a182-3a9c3973d300'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const ad = await Ads.findOne(ps.id);
+
+	if (ad == null) throw new ApiError(meta.errors.noSuchAd);
+
+	await Ads.update(ad.id, {
+		url: ps.url,
+		place: ps.place,
+		priority: ps.priority,
+		ratio: ps.ratio,
+		memo: ps.memo,
+		imageUrl: ps.imageUrl,
+		expiresAt: new Date(ps.expiresAt),
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
new file mode 100644
index 0000000000..f1c07745f9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -0,0 +1,71 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Announcements } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		title: {
+			validator: $.str.min(1)
+		},
+		text: {
+			validator: $.str.min(1)
+		},
+		imageUrl: {
+			validator: $.nullable.str.min(1)
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			id: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id',
+				example: 'xxxxxxxxxx',
+			},
+			createdAt: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'date-time',
+			},
+			updatedAt: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+				format: 'date-time',
+			},
+			title: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			text: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			imageUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const announcement = await Announcements.save({
+		id: genId(),
+		createdAt: new Date(),
+		updatedAt: null,
+		title: ps.title,
+		text: ps.text,
+		imageUrl: ps.imageUrl,
+	});
+
+	return announcement;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts
new file mode 100644
index 0000000000..7dbc05b4c9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts
@@ -0,0 +1,34 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Announcements } from '@/models/index';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		id: {
+			validator: $.type(ID)
+		}
+	},
+
+	errors: {
+		noSuchAnnouncement: {
+			message: 'No such announcement.',
+			code: 'NO_SUCH_ANNOUNCEMENT',
+			id: 'ecad8040-a276-4e85-bda9-015a708d291e'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const announcement = await Announcements.findOne(ps.id);
+
+	if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
+
+	await Announcements.delete(announcement.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
new file mode 100644
index 0000000000..4039bcd88f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts
@@ -0,0 +1,84 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { Announcements, AnnouncementReads } from '@/models/index';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id',
+					example: 'xxxxxxxxxx',
+				},
+				createdAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'date-time',
+				},
+				updatedAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+					format: 'date-time',
+				},
+				text: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				},
+				title: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				},
+				imageUrl: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+				},
+				reads: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
+
+	const announcements = await query.take(ps.limit!).getMany();
+
+	for (const announcement of announcements) {
+		(announcement as any).reads = await AnnouncementReads.count({
+			announcementId: announcement.id
+		});
+	}
+
+	return announcements;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
new file mode 100644
index 0000000000..343f37d626
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts
@@ -0,0 +1,48 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Announcements } from '@/models/index';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		id: {
+			validator: $.type(ID)
+		},
+		title: {
+			validator: $.str.min(1)
+		},
+		text: {
+			validator: $.str.min(1)
+		},
+		imageUrl: {
+			validator: $.nullable.str.min(1)
+		}
+	},
+
+	errors: {
+		noSuchAnnouncement: {
+			message: 'No such announcement.',
+			code: 'NO_SUCH_ANNOUNCEMENT',
+			id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const announcement = await Announcements.findOne(ps.id);
+
+	if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
+
+	await Announcements.update(announcement.id, {
+		updatedAt: new Date(),
+		title: ps.title,
+		text: ps.text,
+		imageUrl: ps.imageUrl,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
new file mode 100644
index 0000000000..988ab29558
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
@@ -0,0 +1,28 @@
+import $ from 'cafy';
+import define from '../../define';
+import { deleteFile } from '@/services/drive/delete-file';
+import { DriveFiles } from '@/models/index';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const files = await DriveFiles.find({
+		userId: ps.userId
+	});
+
+	for (const file of files) {
+		deleteFile(file);
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-logs.ts b/packages/backend/src/server/api/endpoints/admin/delete-logs.ts
new file mode 100644
index 0000000000..9d37ceb434
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/delete-logs.ts
@@ -0,0 +1,13 @@
+import define from '../../define';
+import { Logs } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+};
+
+export default define(meta, async (ps) => {
+	await Logs.clear();	// TRUNCATE
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
new file mode 100644
index 0000000000..76a6acff59
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
@@ -0,0 +1,13 @@
+import define from '../../../define';
+import { createCleanRemoteFilesJob } from '@/queue/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+};
+
+export default define(meta, async (ps, me) => {
+	createCleanRemoteFilesJob();
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
new file mode 100644
index 0000000000..8497478da9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
@@ -0,0 +1,21 @@
+import { IsNull } from 'typeorm';
+import define from '../../../define';
+import { deleteFile } from '@/services/drive/delete-file';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+};
+
+export default define(meta, async (ps, me) => {
+	const files = await DriveFiles.find({
+		userId: IsNull()
+	});
+
+	for (const file of files) {
+		deleteFile(file);
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts
new file mode 100644
index 0000000000..fe1c799805
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts
@@ -0,0 +1,81 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { DriveFiles } from '@/models/index';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: false as const,
+	requireModerator: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		type: {
+			validator: $.optional.nullable.str.match(/^[a-zA-Z0-9\/\-*]+$/)
+		},
+
+		origin: {
+			validator: $.optional.str.or([
+				'combined',
+				'local',
+				'remote',
+			]),
+			default: 'local'
+		},
+
+		hostname: {
+			validator: $.optional.nullable.str,
+			default: null
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'DriveFile'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
+
+	if (ps.origin === 'local') {
+		query.andWhere('file.userHost IS NULL');
+	} else if (ps.origin === 'remote') {
+		query.andWhere('file.userHost IS NOT NULL');
+	}
+
+	if (ps.hostname) {
+		query.andWhere('file.userHost = :hostname', { hostname: ps.hostname });
+	}
+
+	if (ps.type) {
+		if (ps.type.endsWith('/*')) {
+			query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
+		} else {
+			query.andWhere('file.type = :type', { type: ps.type });
+		}
+	}
+
+	const files = await query.take(ps.limit!).getMany();
+
+	return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
new file mode 100644
index 0000000000..270b89c4fa
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -0,0 +1,180 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		fileId: {
+			validator: $.optional.type(ID),
+		},
+
+		url: {
+			validator: $.optional.str,
+		},
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'caf3ca38-c6e5-472e-a30c-b05377dcc240'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			id: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id',
+				example: 'xxxxxxxxxx',
+			},
+			createdAt: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'date-time',
+			},
+			userId: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+				format: 'id',
+				example: 'xxxxxxxxxx',
+			},
+			userHost: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			md5: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'md5',
+				example: '15eca7fba0480996e2245f5185bf39f2'
+			},
+			name: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				example: 'lenna.jpg'
+			},
+			type: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				example: 'image/jpeg'
+			},
+			size: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+				example: 51469
+			},
+			comment: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			blurhash: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			properties: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				properties: {
+					width: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+						example: 1280
+					},
+					height: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+						example: 720
+					},
+					avgColor: {
+						type: 'string' as const,
+						optional: true as const, nullable: false as const,
+						example: 'rgb(40,65,87)'
+					}
+				}
+			},
+			storedInternal: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: true as const,
+				example: true
+			},
+			url: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+				format: 'url',
+			},
+			thumbnailUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+				format: 'url',
+			},
+			webpublicUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+				format: 'url',
+			},
+			accessKey: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			thumbnailAccessKey: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			webpublicAccessKey: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			uri: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			src: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			folderId: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+				format: 'id',
+				example: 'xxxxxxxxxx',
+			},
+			isSensitive: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const,
+			},
+			isLink: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const file = ps.fileId ? await DriveFiles.findOne(ps.fileId) : await DriveFiles.findOne({
+		where: [{
+			url: ps.url
+		}, {
+			thumbnailUrl: ps.url
+		}, {
+			webpublicUrl: ps.url
+		}]
+	});
+
+	if (file == null) {
+		throw new ApiError(meta.errors.noSuchFile);
+	}
+
+	return file;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
new file mode 100644
index 0000000000..1af81fe46d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -0,0 +1,64 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Emojis, DriveFiles } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { getConnection } from 'typeorm';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+import { ApiError } from '../../../error';
+import { ID } from '@/misc/cafy-id';
+import rndstr from 'rndstr';
+import { publishBroadcastStream } from '@/services/stream';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		fileId: {
+			validator: $.type(ID)
+		},
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'MO_SUCH_FILE',
+			id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const file = await DriveFiles.findOne(ps.fileId);
+
+	if (file == null) throw new ApiError(meta.errors.noSuchFile);
+
+	const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
+
+	const emoji = await Emojis.save({
+		id: genId(),
+		updatedAt: new Date(),
+		name: name,
+		category: null,
+		host: null,
+		aliases: [],
+		url: file.url,
+		type: file.type,
+	});
+
+	await getConnection().queryResultCache!.remove(['meta_emojis']);
+
+	publishBroadcastStream('emojiAdded', {
+		emoji: await Emojis.pack(emoji.id)
+	});
+
+	insertModerationLog(me, 'addEmoji', {
+		emojiId: emoji.id
+	});
+
+	return {
+		id: emoji.id
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
new file mode 100644
index 0000000000..4c8ab99f7c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -0,0 +1,81 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Emojis } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { getConnection } from 'typeorm';
+import { ApiError } from '../../../error';
+import { DriveFile } from '@/models/entities/drive-file';
+import { ID } from '@/misc/cafy-id';
+import uploadFromUrl from '@/services/drive/upload-from-url';
+import { publishBroadcastStream } from '@/services/stream';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		emojiId: {
+			validator: $.type(ID)
+		},
+	},
+
+	errors: {
+		noSuchEmoji: {
+			message: 'No such emoji.',
+			code: 'NO_SUCH_EMOJI',
+			id: 'e2785b66-dca3-4087-9cac-b93c541cc425'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			id: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id',
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const emoji = await Emojis.findOne(ps.emojiId);
+
+	if (emoji == null) {
+		throw new ApiError(meta.errors.noSuchEmoji);
+	}
+
+	let driveFile: DriveFile;
+
+	try {
+		// Create file
+		driveFile = await uploadFromUrl(emoji.url, null, null, null, false, true);
+	} catch (e) {
+		throw new ApiError();
+	}
+
+	const copied = await Emojis.insert({
+		id: genId(),
+		updatedAt: new Date(),
+		name: emoji.name,
+		host: null,
+		aliases: [],
+		url: driveFile.url,
+		type: driveFile.type,
+		fileId: driveFile.id,
+	}).then(x => Emojis.findOneOrFail(x.identifiers[0]));
+
+	await getConnection().queryResultCache!.remove(['meta_emojis']);
+
+	publishBroadcastStream('emojiAdded', {
+		emoji: await Emojis.pack(copied.id)
+	});
+
+	return {
+		id: copied.id
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
new file mode 100644
index 0000000000..3c8ca22170
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -0,0 +1,99 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Emojis } from '@/models/index';
+import { toPuny } from '@/misc/convert-host';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		query: {
+			validator: $.optional.nullable.str,
+			default: null
+		},
+
+		host: {
+			validator: $.optional.nullable.str,
+			default: null
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id',
+				},
+				aliases: {
+					type: 'array' as const,
+					optional: false as const, nullable: false as const,
+					items: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const
+					}
+				},
+				name: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				},
+				category: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+				},
+				host: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+				},
+				url: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId);
+
+	if (ps.host == null) {
+		q.andWhere(`emoji.host IS NOT NULL`);
+	} else {
+		q.andWhere(`emoji.host = :host`, { host: toPuny(ps.host) });
+	}
+
+	if (ps.query) {
+		q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' });
+	}
+
+	const emojis = await q
+		.orderBy('emoji.id', 'DESC')
+		.take(ps.limit!)
+		.getMany();
+
+	return Emojis.packMany(emojis);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
new file mode 100644
index 0000000000..cb1e79e0fe
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -0,0 +1,98 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Emojis } from '@/models/index';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+import { ID } from '@/misc/cafy-id';
+import { Emoji } from '@/models/entities/emoji';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		query: {
+			validator: $.optional.nullable.str,
+			default: null
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id',
+				},
+				aliases: {
+					type: 'array' as const,
+					optional: false as const, nullable: false as const,
+					items: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const
+					}
+				},
+				name: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				},
+				category: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+				},
+				host: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+				},
+				url: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId)
+		.andWhere(`emoji.host IS NULL`);
+
+	let emojis: Emoji[];
+
+	if (ps.query) {
+		//q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` });
+		//const emojis = await q.take(ps.limit!).getMany();
+
+		emojis = await q.getMany();
+
+		emojis = emojis.filter(emoji =>
+			emoji.name.includes(ps.query!) ||
+			emoji.aliases.some(a => a.includes(ps.query!)) ||
+			emoji.category?.includes(ps.query!));
+
+		emojis.splice(ps.limit! + 1);
+	} else {
+		emojis = await q.take(ps.limit!).getMany();
+	}
+
+	return Emojis.packMany(emojis);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove.ts
new file mode 100644
index 0000000000..259950e362
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove.ts
@@ -0,0 +1,42 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Emojis } from '@/models/index';
+import { getConnection } from 'typeorm';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		id: {
+			validator: $.type(ID)
+		}
+	},
+
+	errors: {
+		noSuchEmoji: {
+			message: 'No such emoji.',
+			code: 'NO_SUCH_EMOJI',
+			id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const emoji = await Emojis.findOne(ps.id);
+
+	if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
+
+	await Emojis.delete(emoji.id);
+
+	await getConnection().queryResultCache!.remove(['meta_emojis']);
+
+	insertModerationLog(me, 'removeEmoji', {
+		emoji: emoji
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
new file mode 100644
index 0000000000..3fd547d7e5
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -0,0 +1,54 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { Emojis } from '@/models/index';
+import { getConnection } from 'typeorm';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		id: {
+			validator: $.type(ID)
+		},
+
+		name: {
+			validator: $.str
+		},
+
+		category: {
+			validator: $.optional.nullable.str
+		},
+
+		aliases: {
+			validator: $.arr($.str)
+		}
+	},
+
+	errors: {
+		noSuchEmoji: {
+			message: 'No such emoji.',
+			code: 'NO_SUCH_EMOJI',
+			id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8'
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const emoji = await Emojis.findOne(ps.id);
+
+	if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
+
+	await Emojis.update(emoji.id, {
+		updatedAt: new Date(),
+		name: ps.name,
+		category: ps.category,
+		aliases: ps.aliases,
+	});
+
+	await getConnection().queryResultCache!.remove(['meta_emojis']);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
new file mode 100644
index 0000000000..82540c5447
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
@@ -0,0 +1,27 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { deleteFile } from '@/services/drive/delete-file';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		host: {
+			validator: $.str
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const files = await DriveFiles.find({
+		userHost: ps.host
+	});
+
+	for (const file of files) {
+		deleteFile(file);
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
new file mode 100644
index 0000000000..65a6947ba0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
@@ -0,0 +1,28 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Instances } from '@/models/index';
+import { toPuny } from '@/misc/convert-host';
+import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		host: {
+			validator: $.str
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const instance = await Instances.findOne({ host: toPuny(ps.host) });
+
+	if (instance == null) {
+		throw new Error('instance not found');
+	}
+
+	fetchInstanceMetadata(instance, true);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
new file mode 100644
index 0000000000..7935eaa631
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
@@ -0,0 +1,32 @@
+import $ from 'cafy';
+import define from '../../../define';
+import deleteFollowing from '@/services/following/delete';
+import { Followings, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		host: {
+			validator: $.str
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const followings = await Followings.find({
+		followerHost: ps.host
+	});
+
+	const pairs = await Promise.all(followings.map(f => Promise.all([
+		Users.findOneOrFail(f.followerId),
+		Users.findOneOrFail(f.followeeId)
+	])));
+
+	for (const pair of pairs) {
+		deleteFollowing(pair[0], pair[1]);
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
new file mode 100644
index 0000000000..34eab27c78
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -0,0 +1,33 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Instances } from '@/models/index';
+import { toPuny } from '@/misc/convert-host';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		host: {
+			validator: $.str
+		},
+
+		isSuspended: {
+			validator: $.bool
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const instance = await Instances.findOne({ host: toPuny(ps.host) });
+
+	if (instance == null) {
+		throw new Error('instance not found');
+	}
+
+	Instances.update({ host: toPuny(ps.host) }, {
+		isSuspended: ps.isSuspended
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
new file mode 100644
index 0000000000..f2b06d0ef2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
@@ -0,0 +1,26 @@
+import define from '../../define';
+import { getConnection } from 'typeorm';
+
+export const meta = {
+	requireCredential: true as const,
+	requireModerator: true,
+
+	tags: ['admin'],
+
+	params: {
+	},
+};
+
+export default define(meta, async () => {
+	const stats = await
+		getConnection().query(`SELECT * FROM pg_indexes;`)
+		.then(recs => {
+			const res = [] as { tablename: string; indexname: string; }[];
+			for (const rec of recs) {
+				res.push(rec);
+			}
+			return res;
+		});
+
+	return stats;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
new file mode 100644
index 0000000000..bce813232b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
@@ -0,0 +1,45 @@
+import define from '../../define';
+import { getConnection } from 'typeorm';
+
+export const meta = {
+	requireCredential: true as const,
+	requireModerator: true,
+
+	tags: ['admin'],
+
+	params: {
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		example: {
+			migrations: {
+				count: 66,
+				size: 32768
+			},
+		}
+	}
+};
+
+export default define(meta, async () => {
+	const sizes = await
+		getConnection().query(`
+			SELECT relname AS "table", reltuples as "count", pg_total_relation_size(C.oid) AS "size"
+			FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
+			WHERE nspname NOT IN ('pg_catalog', 'information_schema')
+				AND C.relkind <> 'i'
+				AND nspname !~ '^pg_toast';`)
+		.then(recs => {
+			const res = {} as Record<string, { count: number; size: number; }>;
+			for (const rec of recs) {
+				res[rec.table] = {
+					count: parseInt(rec.count, 10),
+					size: parseInt(rec.size, 10),
+				};
+			}
+			return res;
+		});
+
+	return sizes;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/invite.ts b/packages/backend/src/server/api/endpoints/admin/invite.ts
new file mode 100644
index 0000000000..2c69eec535
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/invite.ts
@@ -0,0 +1,44 @@
+import rndstr from 'rndstr';
+import define from '../../define';
+import { RegistrationTickets } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			code: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				example: '2ERUA5VR',
+				maxLength: 8,
+				minLength: 8
+			}
+		}
+	}
+};
+
+export default define(meta, async () => {
+	const code = rndstr({
+		length: 8,
+		chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns)
+	});
+
+	await RegistrationTickets.insert({
+		id: genId(),
+		createdAt: new Date(),
+		code,
+	});
+
+	return {
+		code,
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts
new file mode 100644
index 0000000000..2b87fc217f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts
@@ -0,0 +1,33 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireAdmin: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps) => {
+	const user = await Users.findOne(ps.userId as string);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	if (user.isAdmin) {
+		throw new Error('cannot mark as moderator if admin user');
+	}
+
+	await Users.update(user.id, {
+		isModerator: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts
new file mode 100644
index 0000000000..cbb0625224
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts
@@ -0,0 +1,29 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireAdmin: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps) => {
+	const user = await Users.findOne(ps.userId as string);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	await Users.update(user.id, {
+		isModerator: false
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
new file mode 100644
index 0000000000..3bdaaad4d9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
@@ -0,0 +1,57 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getNote } from '../../../common/getters';
+import { PromoNotes } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+
+		expiresAt: {
+			validator: $.num.int()
+		},
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'ee449fbe-af2a-453b-9cae-cf2fe7c895fc'
+		},
+
+		alreadyPromoted: {
+			message: 'The note has already promoted.',
+			code: 'ALREADY_PROMOTED',
+			id: 'ae427aa2-7a41-484f-a18c-2c1104051604'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const exist = await PromoNotes.findOne(note.id);
+
+	if (exist != null) {
+		throw new ApiError(meta.errors.alreadyPromoted);
+	}
+
+	await PromoNotes.insert({
+		noteId: note.id,
+		createdAt: new Date(),
+		expiresAt: new Date(ps.expiresAt),
+		userId: note.userId,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
new file mode 100644
index 0000000000..fedb7065ab
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts
@@ -0,0 +1,18 @@
+import define from '../../../define';
+import { destroy } from '@/queue/index';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {}
+};
+
+export default define(meta, async (ps, me) => {
+	destroy();
+
+	insertModerationLog(me, 'clearQueue');
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
new file mode 100644
index 0000000000..cd7b640983
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -0,0 +1,55 @@
+import { deliverQueue } from '@/queue/queues';
+import { URL } from 'url';
+import define from '../../../define';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				anyOf: [
+					{
+						type: 'string' as const,
+					},
+					{
+						type: 'number' as const,
+					}
+				]
+			}
+		},
+		example: [[
+			'example.com',
+			12
+		]]
+	}
+};
+
+export default define(meta, async (ps) => {
+	const jobs = await deliverQueue.getJobs(['delayed']);
+
+	const res = [] as [string, number][];
+
+	for (const job of jobs) {
+		const host = new URL(job.data.to).host;
+		if (res.find(x => x[0] === host)) {
+			res.find(x => x[0] === host)![1]++;
+		} else {
+			res.push([host, 1]);
+		}
+	}
+
+	res.sort((a, b) => b[1] - a[1]);
+
+	return res;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
new file mode 100644
index 0000000000..1925906c28
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -0,0 +1,55 @@
+import { URL } from 'url';
+import define from '../../../define';
+import { inboxQueue } from '@/queue/queues';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				anyOf: [
+					{
+						type: 'string' as const,
+					},
+					{
+						type: 'number' as const,
+					}
+				]
+			}
+		},
+		example: [[
+			'example.com',
+			12
+		]]
+	}
+};
+
+export default define(meta, async (ps) => {
+	const jobs = await inboxQueue.getJobs(['delayed']);
+
+	const res = [] as [string, number][];
+
+	for (const job of jobs) {
+		const host = new URL(job.data.signature.keyId).host;
+		if (res.find(x => x[0] === host)) {
+			res.find(x => x[0] === host)![1]++;
+		} else {
+			res.push([host, 1]);
+		}
+	}
+
+	res.sort((a, b) => b[1] - a[1]);
+
+	return res;
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
new file mode 100644
index 0000000000..c426e5f39b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts
@@ -0,0 +1,81 @@
+import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues';
+import $ from 'cafy';
+import define from '../../../define';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		domain: {
+			validator: $.str.or(['deliver', 'inbox', 'db', 'objectStorage']),
+		},
+
+		state: {
+			validator: $.str.or(['active', 'waiting', 'delayed']),
+		},
+
+		limit: {
+			validator: $.optional.num,
+			default: 50
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				data: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const
+				},
+				attempts: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const
+				},
+				maxAttempts: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const
+				},
+				timestamp: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const queue =
+		ps.domain === 'deliver' ? deliverQueue :
+		ps.domain === 'inbox' ? inboxQueue :
+		ps.domain === 'db' ? dbQueue :
+		ps.domain === 'objectStorage' ? objectStorageQueue :
+		null as never;
+
+	const jobs = await queue.getJobs([ps.state], 0, ps.limit!);
+
+	return jobs.map(job => {
+		const data = job.data;
+		delete data.content;
+		delete data.user;
+		return {
+			id: job.id,
+			data,
+			attempts: job.attemptsMade,
+			maxAttempts: job.opts ? job.opts.attempts : 0,
+			timestamp: job.timestamp,
+		};
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
new file mode 100644
index 0000000000..38f18459dd
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
@@ -0,0 +1,44 @@
+import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues';
+import define from '../../../define';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			deliver: {
+				ref: 'QueueCount'
+			},
+			inbox: {
+				ref: 'QueueCount'
+			},
+			db: {
+				ref: 'QueueCount'
+			},
+			objectStorage: {
+				ref: 'QueueCount'
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const deliverJobCounts = await deliverQueue.getJobCounts();
+	const inboxJobCounts = await inboxQueue.getJobCounts();
+	const dbJobCounts = await dbQueue.getJobCounts();
+	const objectStorageJobCounts = await objectStorageQueue.getJobCounts();
+
+	return {
+		deliver: deliverJobCounts,
+		inbox: inboxJobCounts,
+		db: dbJobCounts,
+		objectStorage: objectStorageJobCounts,
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
new file mode 100644
index 0000000000..567035fd3a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
@@ -0,0 +1,63 @@
+import { URL } from 'url';
+import $ from 'cafy';
+import define from '../../../define';
+import { addRelay } from '@/services/relay';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true as const,
+
+	params: {
+		inbox: {
+			validator: $.str
+		},
+	},
+
+	errors: {
+		invalidUrl: {
+			message: 'Invalid URL',
+			code: 'INVALID_URL',
+			id: 'fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			id: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			},
+			inbox: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'url'
+			},
+			status: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				default: 'requesting',
+				enum: [
+					'requesting',
+					'accepted',
+					'rejected'
+				]
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	try {
+		if (new URL(ps.inbox).protocol !== 'https:') throw 'https only';
+	} catch {
+		throw new ApiError(meta.errors.invalidUrl);
+	}
+
+	return await addRelay(ps.inbox);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts
new file mode 100644
index 0000000000..031ebe85d0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/relays/list.ts
@@ -0,0 +1,47 @@
+import define from '../../../define';
+import { listRelay } from '@/services/relay';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true as const,
+
+	params: {
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				inbox: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'url'
+				},
+				status: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					default: 'requesting',
+					enum: [
+						'requesting',
+						'accepted',
+						'rejected'
+					]
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	return await listRelay();
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
new file mode 100644
index 0000000000..c1c50f5dc0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
@@ -0,0 +1,20 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { removeRelay } from '@/services/relay';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true as const,
+
+	params: {
+		inbox: {
+			validator: $.str
+		},
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	return await removeRelay(ps.inbox);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
new file mode 100644
index 0000000000..0fc2c6a868
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
@@ -0,0 +1,59 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import * as bcrypt from 'bcryptjs';
+import rndstr from 'rndstr';
+import { Users, UserProfiles } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			password: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				minLength: 8,
+				maxLength: 8
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const user = await Users.findOne(ps.userId as string);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	if (user.isAdmin) {
+		throw new Error('cannot reset password of admin');
+	}
+
+	const passwd = rndstr('a-zA-Z0-9', 8);
+
+	// Generate hash of password
+	const hash = bcrypt.hashSync(passwd);
+
+	await UserProfiles.update({
+		userId: user.id
+	}, {
+		password: hash
+	});
+
+	return {
+		password: passwd
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
new file mode 100644
index 0000000000..7b71f8e000
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -0,0 +1,30 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { AbuseUserReports } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		reportId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const report = await AbuseUserReports.findOne(ps.reportId);
+
+	if (report == null) {
+		throw new Error('report not found');
+	}
+
+	await AbuseUserReports.update(report.id, {
+		resolved: true,
+		assigneeId: me.id,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/resync-chart.ts b/packages/backend/src/server/api/endpoints/admin/resync-chart.ts
new file mode 100644
index 0000000000..e01dfce1b6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/resync-chart.ts
@@ -0,0 +1,21 @@
+import define from '../../define';
+import { driveChart, notesChart, usersChart } from '@/services/chart/index';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+};
+
+export default define(meta, async (ps, me) => {
+	insertModerationLog(me, 'chartResync');
+
+	driveChart.resync();
+	notesChart.resync();
+	usersChart.resync();
+
+	// TODO: ユーザーごとのチャートもキューに入れて更新する
+	// TODO: インスタンスごとのチャートもキューに入れて更新する
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts
new file mode 100644
index 0000000000..6f67b78542
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/send-email.ts
@@ -0,0 +1,26 @@
+import $ from 'cafy';
+import define from '../../define';
+import { sendEmail } from '@/services/send-email';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		to: {
+			validator: $.str,
+		},
+		subject: {
+			validator: $.str,
+		},
+		text: {
+			validator: $.str,
+		},
+	}
+};
+
+export default define(meta, async (ps) => {
+	await sendEmail(ps.to, ps.subject, ps.text, ps.text);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts
new file mode 100644
index 0000000000..bb2d35e397
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts
@@ -0,0 +1,119 @@
+import * as os from 'os';
+import * as si from 'systeminformation';
+import { getConnection } from 'typeorm';
+import define from '../../define';
+import { redisClient } from '../../../../db/redis';
+
+export const meta = {
+	requireCredential: true as const,
+	requireModerator: true,
+
+	tags: ['admin', 'meta'],
+
+	params: {
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			machine: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			os: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				example: 'linux'
+			},
+			node: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			psql: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			cpu: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				properties: {
+					model: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const,
+					},
+					cores: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+					}
+				}
+			},
+			mem: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				properties: {
+					total: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+						format: 'bytes',
+					}
+				}
+			},
+			fs: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				properties: {
+					total: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+						format: 'bytes',
+					},
+					used: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+						format: 'bytes',
+					}
+				}
+			},
+			net: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				properties: {
+					interface: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const,
+						example: 'eth0'
+					}
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async () => {
+	const memStats = await si.mem();
+	const fsStats = await si.fsSize();
+	const netInterface = await si.networkInterfaceDefault();
+
+	return {
+		machine: os.hostname(),
+		os: os.platform(),
+		node: process.version,
+		psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version),
+		redis: redisClient.server_info.redis_version,
+		cpu: {
+			model: os.cpus()[0].model,
+			cores: os.cpus().length
+		},
+		mem: {
+			total: memStats.total
+		},
+		fs: {
+			total: fsStats[0].size,
+			used: fsStats[0].used,
+		},
+		net: {
+			interface: netInterface
+		}
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
new file mode 100644
index 0000000000..e9509568d0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
@@ -0,0 +1,74 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ModerationLogs } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				createdAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'date-time'
+				},
+				type: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				},
+				info: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const
+				},
+				userId: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				user: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const query = makePaginationQuery(ModerationLogs.createQueryBuilder('report'), ps.sinceId, ps.untilId);
+
+	const reports = await query.take(ps.limit!).getMany();
+
+	return await ModerationLogs.packMany(reports);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
new file mode 100644
index 0000000000..963c123255
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -0,0 +1,177 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		nullable: false as const, optional: false as const,
+		properties: {
+			id: {
+				type: 'string' as const,
+				nullable: false as const, optional: false as const,
+				format: 'id'
+			},
+			createdAt: {
+				type: 'string' as const,
+				nullable: false as const, optional: false as const,
+				format: 'date-time'
+			},
+			updatedAt: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const,
+				format: 'date-time'
+			},
+			lastFetchedAt: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const
+			},
+			username: {
+				type: 'string' as const,
+				nullable: false as const, optional: false as const
+			},
+			name: {
+				type: 'string' as const,
+				nullable: false as const, optional: false as const
+			},
+			folowersCount: {
+				type: 'number' as const,
+				nullable: false as const, optional: false as const
+			},
+			followingCount: {
+				type: 'number' as const,
+				nullable: false as const, optional: false as const
+			},
+			notesCount: {
+				type: 'number' as const,
+				nullable: false as const, optional: false as const
+			},
+			avatarId: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const
+			},
+			bannerId: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const
+			},
+			tags: {
+				type: 'array' as const,
+				nullable: false as const, optional: false as const,
+				items: {
+					type: 'string' as const,
+					nullable: false as const, optional: false as const
+				}
+			},
+			avatarUrl: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const,
+				format: 'url'
+			},
+			bannerUrl: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const,
+				format: 'url'
+			},
+			avatarBlurhash: {
+				type: 'any' as const,
+				nullable: true as const, optional: false as const,
+				default: null
+			},
+			bannerBlurhash: {
+				type: 'any' as const,
+				nullable: true as const, optional: false as const,
+				default: null
+			},
+			isSuspended: {
+				type: 'boolean' as const,
+				nullable: false as const, optional: false as const
+			},
+			isSilenced: {
+				type: 'boolean' as const,
+				nullable: false as const, optional: false as const
+			},
+			isLocked: {
+				type: 'boolean' as const,
+				nullable: false as const, optional: false as const,
+			},
+			isBot: {
+				type: 'boolean' as const,
+				nullable: false as const, optional: false as const
+			},
+			isCat: {
+				type: 'boolean' as const,
+				nullable: false as const, optional: false as const
+			},
+			isAdmin: {
+				type: 'boolean' as const,
+				nullable: false as const, optional: false as const
+			},
+			isModerator: {
+				type: 'boolean' as const,
+				nullable: false as const, optional: false as const
+			},
+			emojis: {
+				type: 'array' as const,
+				nullable: false as const, optional: false as const,
+				items: {
+					type: 'string' as const,
+					nullable: false as const, optional: false as const
+				}
+			},
+			host: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const
+			},
+			inbox: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const
+			},
+			sharedInbox: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const
+			},
+			featured: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const
+			},
+			uri: {
+				type: 'string' as const,
+				nullable: true as const, optional: false as const
+			},
+			token: {
+				type: 'string' as const,
+				nullable: false as const, optional: false as const,
+				default: '<MASKED>'
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId as string);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	if ((me.isModerator && !me.isAdmin) && user.isAdmin) {
+		throw new Error('cannot show info of admin');
+	}
+
+	return {
+		...user,
+		token: user.token != null ? '<MASKED>' : user.token,
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
new file mode 100644
index 0000000000..20b63e7be6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -0,0 +1,119 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		},
+
+		sort: {
+			validator: $.optional.str.or([
+				'+follower',
+				'-follower',
+				'+createdAt',
+				'-createdAt',
+				'+updatedAt',
+				'-updatedAt',
+			]),
+		},
+
+		state: {
+			validator: $.optional.str.or([
+				'all',
+				'available',
+				'admin',
+				'moderator',
+				'adminOrModerator',
+				'silenced',
+				'suspended',
+			]),
+			default: 'all'
+		},
+
+		origin: {
+			validator: $.optional.str.or([
+				'combined',
+				'local',
+				'remote',
+			]),
+			default: 'local'
+		},
+
+		username: {
+			validator: $.optional.str,
+			default: null
+		},
+
+		hostname: {
+			validator: $.optional.str,
+			default: null
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		nullable: false as const, optional: false as const,
+		items: {
+			type: 'object' as const,
+			nullable: false as const, optional: false as const,
+			ref: 'User'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const query = Users.createQueryBuilder('user');
+
+	switch (ps.state) {
+		case 'available': query.where('user.isSuspended = FALSE'); break;
+		case 'admin': query.where('user.isAdmin = TRUE'); break;
+		case 'moderator': query.where('user.isModerator = TRUE'); break;
+		case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
+		case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
+		case 'silenced': query.where('user.isSilenced = TRUE'); break;
+		case 'suspended': query.where('user.isSuspended = TRUE'); break;
+	}
+
+	switch (ps.origin) {
+		case 'local': query.andWhere('user.host IS NULL'); break;
+		case 'remote': query.andWhere('user.host IS NOT NULL'); break;
+	}
+
+	if (ps.username) {
+		query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' });
+	}
+
+	if (ps.hostname) {
+		query.andWhere('user.host like :hostname', { hostname: '%' + ps.hostname.toLowerCase() + '%' });
+	}
+
+	switch (ps.sort) {
+		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+		case '+updatedAt': query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); break;
+		case '-updatedAt': query.orderBy('user.updatedAt', 'ASC', 'NULLS FIRST'); break;
+		default: query.orderBy('user.id', 'ASC'); break;
+	}
+
+	query.take(ps.limit!);
+	query.skip(ps.offset);
+
+	const users = await query.getMany();
+
+	return await Users.packMany(users, me, { detail: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
new file mode 100644
index 0000000000..9bfed2310a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
@@ -0,0 +1,38 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Users } from '@/models/index';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId as string);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	if (user.isAdmin) {
+		throw new Error('cannot silence admin');
+	}
+
+	await Users.update(user.id, {
+		isSilenced: true
+	});
+
+	insertModerationLog(me, 'silence', {
+		targetId: user.id,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
new file mode 100644
index 0000000000..364f258ce8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
@@ -0,0 +1,84 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import deleteFollowing from '@/services/following/delete';
+import { Users, Followings, Notifications } from '@/models/index';
+import { User } from '@/models/entities/user';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+import { doPostSuspend } from '@/services/suspend-user';
+import { publishUserEvent } from '@/services/stream';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId as string);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	if (user.isAdmin) {
+		throw new Error('cannot suspend admin');
+	}
+
+	if (user.isModerator) {
+		throw new Error('cannot suspend moderator');
+	}
+
+	await Users.update(user.id, {
+		isSuspended: true
+	});
+
+	insertModerationLog(me, 'suspend', {
+		targetId: user.id,
+	});
+
+	// Terminate streaming
+	if (Users.isLocalUser(user)) {
+		publishUserEvent(user.id, 'terminate', {});
+	}
+
+	(async () => {
+		await doPostSuspend(user).catch(e => {});
+		await unFollowAll(user).catch(e => {});
+		await readAllNotify(user).catch(e => {});
+	})();
+});
+
+async function unFollowAll(follower: User) {
+	const followings = await Followings.find({
+		followerId: follower.id
+	});
+
+	for (const following of followings) {
+		const followee = await Users.findOne({
+			id: following.followeeId
+		});
+
+		if (followee == null) {
+			throw `Cant find followee ${following.followeeId}`;
+		}
+
+		await deleteFollowing(follower, followee, true);
+	}
+}
+
+async function readAllNotify(notifier: User) {
+	await Notifications.update({
+		notifierId: notifier.id,
+		isRead: false,
+	}, {
+		isRead: true
+	});
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
new file mode 100644
index 0000000000..9994fbf462
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
@@ -0,0 +1,34 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Users } from '@/models/index';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId as string);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	await Users.update(user.id, {
+		isSilenced: false
+	});
+
+	insertModerationLog(me, 'unsilence', {
+		targetId: user.id,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
new file mode 100644
index 0000000000..ab4c2d3dfe
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts
@@ -0,0 +1,37 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Users } from '@/models/index';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+import { doPostUnsuspend } from '@/services/unsuspend-user';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId as string);
+
+	if (user == null) {
+		throw new Error('user not found');
+	}
+
+	await Users.update(user.id, {
+		isSuspended: false
+	});
+
+	insertModerationLog(me, 'unsuspend', {
+		targetId: user.id,
+	});
+
+	doPostUnsuspend(user);
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
new file mode 100644
index 0000000000..55447098dc
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -0,0 +1,608 @@
+import $ from 'cafy';
+import define from '../../define';
+import { getConnection } from 'typeorm';
+import { Meta } from '@/models/entities/meta';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireAdmin: true,
+
+	params: {
+		disableRegistration: {
+			validator: $.optional.nullable.bool,
+		},
+
+		disableLocalTimeline: {
+			validator: $.optional.nullable.bool,
+		},
+
+		disableGlobalTimeline: {
+			validator: $.optional.nullable.bool,
+		},
+
+		useStarForReactionFallback: {
+			validator: $.optional.nullable.bool,
+		},
+
+		pinnedUsers: {
+			validator: $.optional.nullable.arr($.str),
+		},
+
+		hiddenTags: {
+			validator: $.optional.nullable.arr($.str),
+		},
+
+		blockedHosts: {
+			validator: $.optional.nullable.arr($.str),
+		},
+
+		mascotImageUrl: {
+			validator: $.optional.nullable.str,
+		},
+
+		bannerUrl: {
+			validator: $.optional.nullable.str,
+		},
+
+		errorImageUrl: {
+			validator: $.optional.nullable.str,
+		},
+
+		iconUrl: {
+			validator: $.optional.nullable.str,
+		},
+
+		backgroundImageUrl: {
+			validator: $.optional.nullable.str,
+		},
+
+		logoImageUrl: {
+			validator: $.optional.nullable.str,
+		},
+
+		name: {
+			validator: $.optional.nullable.str,
+		},
+
+		description: {
+			validator: $.optional.nullable.str,
+		},
+
+		maxNoteTextLength: {
+			validator: $.optional.num.min(0).max(DB_MAX_NOTE_TEXT_LENGTH),
+		},
+
+		localDriveCapacityMb: {
+			validator: $.optional.num.min(0),
+		},
+
+		remoteDriveCapacityMb: {
+			validator: $.optional.num.min(0),
+		},
+
+		cacheRemoteFiles: {
+			validator: $.optional.bool,
+		},
+
+		proxyRemoteFiles: {
+			validator: $.optional.bool,
+		},
+
+		emailRequiredForSignup: {
+			validator: $.optional.bool,
+		},
+
+		enableHcaptcha: {
+			validator: $.optional.bool,
+		},
+
+		hcaptchaSiteKey: {
+			validator: $.optional.nullable.str,
+		},
+
+		hcaptchaSecretKey: {
+			validator: $.optional.nullable.str,
+		},
+
+		enableRecaptcha: {
+			validator: $.optional.bool,
+		},
+
+		recaptchaSiteKey: {
+			validator: $.optional.nullable.str,
+		},
+
+		recaptchaSecretKey: {
+			validator: $.optional.nullable.str,
+		},
+
+		proxyAccountId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		maintainerName: {
+			validator: $.optional.nullable.str,
+		},
+
+		maintainerEmail: {
+			validator: $.optional.nullable.str,
+		},
+
+		pinnedPages: {
+			validator: $.optional.arr($.str),
+		},
+
+		pinnedClipId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		langs: {
+			validator: $.optional.arr($.str),
+		},
+
+		summalyProxy: {
+			validator: $.optional.nullable.str,
+		},
+
+		deeplAuthKey: {
+			validator: $.optional.nullable.str,
+		},
+
+		deeplIsPro: {
+			validator: $.optional.bool,
+		},
+
+		enableTwitterIntegration: {
+			validator: $.optional.bool,
+		},
+
+		twitterConsumerKey: {
+			validator: $.optional.nullable.str,
+		},
+
+		twitterConsumerSecret: {
+			validator: $.optional.nullable.str,
+		},
+
+		enableGithubIntegration: {
+			validator: $.optional.bool,
+		},
+
+		githubClientId: {
+			validator: $.optional.nullable.str,
+		},
+
+		githubClientSecret: {
+			validator: $.optional.nullable.str,
+		},
+
+		enableDiscordIntegration: {
+			validator: $.optional.bool,
+		},
+
+		discordClientId: {
+			validator: $.optional.nullable.str,
+		},
+
+		discordClientSecret: {
+			validator: $.optional.nullable.str,
+		},
+
+		enableEmail: {
+			validator: $.optional.bool,
+		},
+
+		email: {
+			validator: $.optional.nullable.str,
+		},
+
+		smtpSecure: {
+			validator: $.optional.bool,
+		},
+
+		smtpHost: {
+			validator: $.optional.nullable.str,
+		},
+
+		smtpPort: {
+			validator: $.optional.nullable.num,
+		},
+
+		smtpUser: {
+			validator: $.optional.nullable.str,
+		},
+
+		smtpPass: {
+			validator: $.optional.nullable.str,
+		},
+
+		enableServiceWorker: {
+			validator: $.optional.bool,
+		},
+
+		swPublicKey: {
+			validator: $.optional.nullable.str,
+		},
+
+		swPrivateKey: {
+			validator: $.optional.nullable.str,
+		},
+
+		tosUrl: {
+			validator: $.optional.nullable.str,
+		},
+
+		repositoryUrl: {
+			validator: $.optional.str,
+		},
+
+		feedbackUrl: {
+			validator: $.optional.str,
+		},
+
+		useObjectStorage: {
+			validator: $.optional.bool
+		},
+
+		objectStorageBaseUrl: {
+			validator: $.optional.nullable.str
+		},
+
+		objectStorageBucket: {
+			validator: $.optional.nullable.str
+		},
+
+		objectStoragePrefix: {
+			validator: $.optional.nullable.str
+		},
+
+		objectStorageEndpoint: {
+			validator: $.optional.nullable.str
+		},
+
+		objectStorageRegion: {
+			validator: $.optional.nullable.str
+		},
+
+		objectStoragePort: {
+			validator: $.optional.nullable.num
+		},
+
+		objectStorageAccessKey: {
+			validator: $.optional.nullable.str
+		},
+
+		objectStorageSecretKey: {
+			validator: $.optional.nullable.str
+		},
+
+		objectStorageUseSSL: {
+			validator: $.optional.bool
+		},
+
+		objectStorageUseProxy: {
+			validator: $.optional.bool
+		},
+
+		objectStorageSetPublicRead: {
+			validator: $.optional.bool
+		},
+
+		objectStorageS3ForcePathStyle: {
+			validator: $.optional.bool
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const set = {} as Partial<Meta>;
+
+	if (typeof ps.disableRegistration === 'boolean') {
+		set.disableRegistration = ps.disableRegistration;
+	}
+
+	if (typeof ps.disableLocalTimeline === 'boolean') {
+		set.disableLocalTimeline = ps.disableLocalTimeline;
+	}
+
+	if (typeof ps.disableGlobalTimeline === 'boolean') {
+		set.disableGlobalTimeline = ps.disableGlobalTimeline;
+	}
+
+	if (typeof ps.useStarForReactionFallback === 'boolean') {
+		set.useStarForReactionFallback = ps.useStarForReactionFallback;
+	}
+
+	if (Array.isArray(ps.pinnedUsers)) {
+		set.pinnedUsers = ps.pinnedUsers.filter(Boolean);
+	}
+
+	if (Array.isArray(ps.hiddenTags)) {
+		set.hiddenTags = ps.hiddenTags.filter(Boolean);
+	}
+
+	if (Array.isArray(ps.blockedHosts)) {
+		set.blockedHosts = ps.blockedHosts.filter(Boolean);
+	}
+
+	if (ps.mascotImageUrl !== undefined) {
+		set.mascotImageUrl = ps.mascotImageUrl;
+	}
+
+	if (ps.bannerUrl !== undefined) {
+		set.bannerUrl = ps.bannerUrl;
+	}
+
+	if (ps.iconUrl !== undefined) {
+		set.iconUrl = ps.iconUrl;
+	}
+
+	if (ps.backgroundImageUrl !== undefined) {
+		set.backgroundImageUrl = ps.backgroundImageUrl;
+	}
+
+	if (ps.logoImageUrl !== undefined) {
+		set.logoImageUrl = ps.logoImageUrl;
+	}
+
+	if (ps.name !== undefined) {
+		set.name = ps.name;
+	}
+
+	if (ps.description !== undefined) {
+		set.description = ps.description;
+	}
+
+	if (ps.maxNoteTextLength) {
+		set.maxNoteTextLength = ps.maxNoteTextLength;
+	}
+
+	if (ps.localDriveCapacityMb !== undefined) {
+		set.localDriveCapacityMb = ps.localDriveCapacityMb;
+	}
+
+	if (ps.remoteDriveCapacityMb !== undefined) {
+		set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
+	}
+
+	if (ps.cacheRemoteFiles !== undefined) {
+		set.cacheRemoteFiles = ps.cacheRemoteFiles;
+	}
+
+	if (ps.proxyRemoteFiles !== undefined) {
+		set.proxyRemoteFiles = ps.proxyRemoteFiles;
+	}
+
+	if (ps.emailRequiredForSignup !== undefined) {
+		set.emailRequiredForSignup = ps.emailRequiredForSignup;
+	}
+
+	if (ps.enableHcaptcha !== undefined) {
+		set.enableHcaptcha = ps.enableHcaptcha;
+	}
+
+	if (ps.hcaptchaSiteKey !== undefined) {
+		set.hcaptchaSiteKey = ps.hcaptchaSiteKey;
+	}
+
+	if (ps.hcaptchaSecretKey !== undefined) {
+		set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
+	}
+
+	if (ps.enableRecaptcha !== undefined) {
+		set.enableRecaptcha = ps.enableRecaptcha;
+	}
+
+	if (ps.recaptchaSiteKey !== undefined) {
+		set.recaptchaSiteKey = ps.recaptchaSiteKey;
+	}
+
+	if (ps.recaptchaSecretKey !== undefined) {
+		set.recaptchaSecretKey = ps.recaptchaSecretKey;
+	}
+
+	if (ps.proxyAccountId !== undefined) {
+		set.proxyAccountId = ps.proxyAccountId;
+	}
+
+	if (ps.maintainerName !== undefined) {
+		set.maintainerName = ps.maintainerName;
+	}
+
+	if (ps.maintainerEmail !== undefined) {
+		set.maintainerEmail = ps.maintainerEmail;
+	}
+
+	if (Array.isArray(ps.langs)) {
+		set.langs = ps.langs.filter(Boolean);
+	}
+
+	if (Array.isArray(ps.pinnedPages)) {
+		set.pinnedPages = ps.pinnedPages.filter(Boolean);
+	}
+
+	if (ps.pinnedClipId !== undefined) {
+		set.pinnedClipId = ps.pinnedClipId;
+	}
+
+	if (ps.summalyProxy !== undefined) {
+		set.summalyProxy = ps.summalyProxy;
+	}
+
+	if (ps.enableTwitterIntegration !== undefined) {
+		set.enableTwitterIntegration = ps.enableTwitterIntegration;
+	}
+
+	if (ps.twitterConsumerKey !== undefined) {
+		set.twitterConsumerKey = ps.twitterConsumerKey;
+	}
+
+	if (ps.twitterConsumerSecret !== undefined) {
+		set.twitterConsumerSecret = ps.twitterConsumerSecret;
+	}
+
+	if (ps.enableGithubIntegration !== undefined) {
+		set.enableGithubIntegration = ps.enableGithubIntegration;
+	}
+
+	if (ps.githubClientId !== undefined) {
+		set.githubClientId = ps.githubClientId;
+	}
+
+	if (ps.githubClientSecret !== undefined) {
+		set.githubClientSecret = ps.githubClientSecret;
+	}
+
+	if (ps.enableDiscordIntegration !== undefined) {
+		set.enableDiscordIntegration = ps.enableDiscordIntegration;
+	}
+
+	if (ps.discordClientId !== undefined) {
+		set.discordClientId = ps.discordClientId;
+	}
+
+	if (ps.discordClientSecret !== undefined) {
+		set.discordClientSecret = ps.discordClientSecret;
+	}
+
+	if (ps.enableEmail !== undefined) {
+		set.enableEmail = ps.enableEmail;
+	}
+
+	if (ps.email !== undefined) {
+		set.email = ps.email;
+	}
+
+	if (ps.smtpSecure !== undefined) {
+		set.smtpSecure = ps.smtpSecure;
+	}
+
+	if (ps.smtpHost !== undefined) {
+		set.smtpHost = ps.smtpHost;
+	}
+
+	if (ps.smtpPort !== undefined) {
+		set.smtpPort = ps.smtpPort;
+	}
+
+	if (ps.smtpUser !== undefined) {
+		set.smtpUser = ps.smtpUser;
+	}
+
+	if (ps.smtpPass !== undefined) {
+		set.smtpPass = ps.smtpPass;
+	}
+
+	if (ps.errorImageUrl !== undefined) {
+		set.errorImageUrl = ps.errorImageUrl;
+	}
+
+	if (ps.enableServiceWorker !== undefined) {
+		set.enableServiceWorker = ps.enableServiceWorker;
+	}
+
+	if (ps.swPublicKey !== undefined) {
+		set.swPublicKey = ps.swPublicKey;
+	}
+
+	if (ps.swPrivateKey !== undefined) {
+		set.swPrivateKey = ps.swPrivateKey;
+	}
+
+	if (ps.tosUrl !== undefined) {
+		set.ToSUrl = ps.tosUrl;
+	}
+
+	if (ps.repositoryUrl !== undefined) {
+		set.repositoryUrl = ps.repositoryUrl;
+	}
+
+	if (ps.feedbackUrl !== undefined) {
+		set.feedbackUrl = ps.feedbackUrl;
+	}
+
+	if (ps.useObjectStorage !== undefined) {
+		set.useObjectStorage = ps.useObjectStorage;
+	}
+
+	if (ps.objectStorageBaseUrl !== undefined) {
+		set.objectStorageBaseUrl = ps.objectStorageBaseUrl;
+	}
+
+	if (ps.objectStorageBucket !== undefined) {
+		set.objectStorageBucket = ps.objectStorageBucket;
+	}
+
+	if (ps.objectStoragePrefix !== undefined) {
+		set.objectStoragePrefix = ps.objectStoragePrefix;
+	}
+
+	if (ps.objectStorageEndpoint !== undefined) {
+		set.objectStorageEndpoint = ps.objectStorageEndpoint;
+	}
+
+	if (ps.objectStorageRegion !== undefined) {
+		set.objectStorageRegion = ps.objectStorageRegion;
+	}
+
+	if (ps.objectStoragePort !== undefined) {
+		set.objectStoragePort = ps.objectStoragePort;
+	}
+
+	if (ps.objectStorageAccessKey !== undefined) {
+		set.objectStorageAccessKey = ps.objectStorageAccessKey;
+	}
+
+	if (ps.objectStorageSecretKey !== undefined) {
+		set.objectStorageSecretKey = ps.objectStorageSecretKey;
+	}
+
+	if (ps.objectStorageUseSSL !== undefined) {
+		set.objectStorageUseSSL = ps.objectStorageUseSSL;
+	}
+
+	if (ps.objectStorageUseProxy !== undefined) {
+		set.objectStorageUseProxy = ps.objectStorageUseProxy;
+	}
+
+	if (ps.objectStorageSetPublicRead !== undefined) {
+		set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead;
+	}
+
+	if (ps.objectStorageS3ForcePathStyle !== undefined) {
+		set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
+	}
+
+	if (ps.deeplAuthKey !== undefined) {
+		if (ps.deeplAuthKey === '') {
+			set.deeplAuthKey = null;
+		} else {
+			set.deeplAuthKey = ps.deeplAuthKey;
+		}
+	}
+
+	if (ps.deeplIsPro !== undefined) {
+		set.deeplIsPro = ps.deeplIsPro;
+	}
+
+	await getConnection().transaction(async transactionalEntityManager => {
+		const meta = await transactionalEntityManager.findOne(Meta, {
+			order: {
+				id: 'DESC'
+			}
+		});
+
+		if (meta) {
+			await transactionalEntityManager.update(Meta, meta.id, set);
+		} else {
+			await transactionalEntityManager.save(Meta, set);
+		}
+	});
+
+	insertModerationLog(me, 'updateMeta');
+});
diff --git a/packages/backend/src/server/api/endpoints/admin/vacuum.ts b/packages/backend/src/server/api/endpoints/admin/vacuum.ts
new file mode 100644
index 0000000000..9a80d88c44
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/vacuum.ts
@@ -0,0 +1,36 @@
+import $ from 'cafy';
+import define from '../../define';
+import { getConnection } from 'typeorm';
+import { insertModerationLog } from '@/services/insert-moderation-log';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true as const,
+	requireModerator: true,
+
+	params: {
+		full: {
+			validator: $.bool,
+		},
+		analyze: {
+			validator: $.bool,
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const params: string[] = [];
+
+	if (ps.full) {
+		params.push('FULL');
+	}
+
+	if (ps.analyze) {
+		params.push('ANALYZE');
+	}
+
+	getConnection().query('VACUUM ' + params.join(' '));
+
+	insertModerationLog(me, 'vacuum', ps);
+});
diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts
new file mode 100644
index 0000000000..a67737b2ff
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/announcements.ts
@@ -0,0 +1,92 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../define';
+import { Announcements, AnnouncementReads } from '@/models/index';
+import { makePaginationQuery } from '../common/make-pagination-query';
+
+export const meta = {
+	tags: ['meta'],
+
+	requireCredential: false as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		withUnreads: {
+			validator: $.optional.boolean,
+			default: false
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id',
+					example: 'xxxxxxxxxx',
+				},
+				createdAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'date-time',
+				},
+				updatedAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+					format: 'date-time',
+				},
+				text: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				},
+				title: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				},
+				imageUrl: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+				},
+				isRead: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const,
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
+
+	const announcements = await query.take(ps.limit!).getMany();
+
+	if (user) {
+		const reads = (await AnnouncementReads.find({
+			userId: user.id
+		})).map(x => x.announcementId);
+
+		for (const announcement of announcements) {
+			(announcement as any).isRead = reads.includes(announcement.id);
+		}
+	}
+
+	return ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements;
+});
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
new file mode 100644
index 0000000000..4bdae8cc33
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -0,0 +1,127 @@
+import $ from 'cafy';
+import define from '../../define';
+import { genId } from '@/misc/gen-id';
+import { Antennas, UserLists, UserGroupJoinings } from '@/models/index';
+import { ID } from '@/misc/cafy-id';
+import { ApiError } from '../../error';
+import { publishInternalEvent } from '@/services/stream';
+
+export const meta = {
+	tags: ['antennas'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		name: {
+			validator: $.str.range(1, 100)
+		},
+
+		src: {
+			validator: $.str.or(['home', 'all', 'users', 'list', 'group'])
+		},
+
+		userListId: {
+			validator: $.nullable.optional.type(ID),
+		},
+
+		userGroupId: {
+			validator: $.nullable.optional.type(ID),
+		},
+
+		keywords: {
+			validator: $.arr($.arr($.str))
+		},
+
+		excludeKeywords: {
+			validator: $.arr($.arr($.str))
+		},
+
+		users: {
+			validator: $.arr($.str)
+		},
+
+		caseSensitive: {
+			validator: $.bool
+		},
+
+		withReplies: {
+			validator: $.bool
+		},
+
+		withFile: {
+			validator: $.bool
+		},
+
+		notify: {
+			validator: $.bool
+		}
+	},
+
+	errors: {
+		noSuchUserList: {
+			message: 'No such user list.',
+			code: 'NO_SUCH_USER_LIST',
+			id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f'
+		},
+
+		noSuchUserGroup: {
+			message: 'No such user group.',
+			code: 'NO_SUCH_USER_GROUP',
+			id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Antenna'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	let userList;
+	let userGroupJoining;
+
+	if (ps.src === 'list' && ps.userListId) {
+		userList = await UserLists.findOne({
+			id: ps.userListId,
+			userId: user.id,
+		});
+
+		if (userList == null) {
+			throw new ApiError(meta.errors.noSuchUserList);
+		}
+	} else if (ps.src === 'group' && ps.userGroupId) {
+		userGroupJoining = await UserGroupJoinings.findOne({
+			userGroupId: ps.userGroupId,
+			userId: user.id,
+		});
+
+		if (userGroupJoining == null) {
+			throw new ApiError(meta.errors.noSuchUserGroup);
+		}
+	}
+
+	const antenna = await Antennas.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		name: ps.name,
+		src: ps.src,
+		userListId: userList ? userList.id : null,
+		userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
+		keywords: ps.keywords,
+		excludeKeywords: ps.excludeKeywords,
+		users: ps.users,
+		caseSensitive: ps.caseSensitive,
+		withReplies: ps.withReplies,
+		withFile: ps.withFile,
+		notify: ps.notify,
+	}).then(x => Antennas.findOneOrFail(x.identifiers[0]));
+
+	publishInternalEvent('antennaCreated', antenna);
+
+	return await Antennas.pack(antenna);
+});
diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts
new file mode 100644
index 0000000000..1cd136183e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/antennas/delete.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Antennas } from '@/models/index';
+import { publishInternalEvent } from '@/services/stream';
+
+export const meta = {
+	tags: ['antennas'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		antennaId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchAntenna: {
+			message: 'No such antenna.',
+			code: 'NO_SUCH_ANTENNA',
+			id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const antenna = await Antennas.findOne({
+		id: ps.antennaId,
+		userId: user.id
+	});
+
+	if (antenna == null) {
+		throw new ApiError(meta.errors.noSuchAntenna);
+	}
+
+	await Antennas.delete(antenna.id);
+
+	publishInternalEvent('antennaDeleted', antenna);
+});
diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts
new file mode 100644
index 0000000000..8baae8435b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/antennas/list.ts
@@ -0,0 +1,28 @@
+import define from '../../define';
+import { Antennas } from '@/models/index';
+
+export const meta = {
+	tags: ['antennas', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:account',
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Antenna'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const antennas = await Antennas.find({
+		userId: me.id,
+	});
+
+	return await Promise.all(antennas.map(x => Antennas.pack(x)));
+});
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
new file mode 100644
index 0000000000..1759e95b4c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -0,0 +1,93 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import readNote from '@/services/note/read';
+import { Antennas, Notes, AntennaNotes } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { ApiError } from '../../error';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['antennas', 'account', 'notes'],
+
+	requireCredential: true as const,
+
+	kind: 'read:account',
+
+	params: {
+		antennaId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchAntenna: {
+			message: 'No such antenna.',
+			code: 'NO_SUCH_ANTENNA',
+			id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe'
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const antenna = await Antennas.findOne({
+		id: ps.antennaId,
+		userId: user.id
+	});
+
+	if (antenna == null) {
+		throw new ApiError(meta.errors.noSuchAntenna);
+	}
+
+	const antennaQuery = AntennaNotes.createQueryBuilder('joining')
+		.select('joining.noteId')
+		.where('joining.antennaId = :antennaId', { antennaId: antenna.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.id IN (${ antennaQuery.getQuery() })`)
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
+		.setParameters(antennaQuery.getParameters());
+
+	generateVisibilityQuery(query, user);
+	generateMutedUserQuery(query, user);
+	generateBlockedUserQuery(query, user);
+
+	const notes = await query
+		.take(ps.limit!)
+		.getMany();
+
+	if (notes.length > 0) {
+		readNote(user.id, notes);
+	}
+
+	return await Notes.packMany(notes, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts
new file mode 100644
index 0000000000..3cdf4dcb61
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/antennas/show.ts
@@ -0,0 +1,47 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Antennas } from '@/models/index';
+
+export const meta = {
+	tags: ['antennas', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:account',
+
+	params: {
+		antennaId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchAntenna: {
+			message: 'No such antenna.',
+			code: 'NO_SUCH_ANTENNA',
+			id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Antenna'
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the antenna
+	const antenna = await Antennas.findOne({
+		id: ps.antennaId,
+		userId: me.id,
+	});
+
+	if (antenna == null) {
+		throw new ApiError(meta.errors.noSuchAntenna);
+	}
+
+	return await Antennas.pack(antenna);
+});
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
new file mode 100644
index 0000000000..d69b4feee6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -0,0 +1,143 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Antennas, UserLists, UserGroupJoinings } from '@/models/index';
+import { publishInternalEvent } from '@/services/stream';
+
+export const meta = {
+	tags: ['antennas'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		antennaId: {
+			validator: $.type(ID),
+		},
+
+		name: {
+			validator: $.str.range(1, 100)
+		},
+
+		src: {
+			validator: $.str.or(['home', 'all', 'users', 'list', 'group'])
+		},
+
+		userListId: {
+			validator: $.nullable.optional.type(ID),
+		},
+
+		userGroupId: {
+			validator: $.nullable.optional.type(ID),
+		},
+
+		keywords: {
+			validator: $.arr($.arr($.str))
+		},
+
+		excludeKeywords: {
+			validator: $.arr($.arr($.str))
+		},
+
+		users: {
+			validator: $.arr($.str)
+		},
+
+		caseSensitive: {
+			validator: $.bool
+		},
+
+		withReplies: {
+			validator: $.bool
+		},
+
+		withFile: {
+			validator: $.bool
+		},
+
+		notify: {
+			validator: $.bool
+		}
+	},
+
+	errors: {
+		noSuchAntenna: {
+			message: 'No such antenna.',
+			code: 'NO_SUCH_ANTENNA',
+			id: '10c673ac-8852-48eb-aa1f-f5b67f069290'
+		},
+
+		noSuchUserList: {
+			message: 'No such user list.',
+			code: 'NO_SUCH_USER_LIST',
+			id: '1c6b35c9-943e-48c2-81e4-2844989407f7'
+		},
+
+		noSuchUserGroup: {
+			message: 'No such user group.',
+			code: 'NO_SUCH_USER_GROUP',
+			id: '109ed789-b6eb-456e-b8a9-6059d567d385'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Antenna'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch the antenna
+	const antenna = await Antennas.findOne({
+		id: ps.antennaId,
+		userId: user.id
+	});
+
+	if (antenna == null) {
+		throw new ApiError(meta.errors.noSuchAntenna);
+	}
+
+	let userList;
+	let userGroupJoining;
+
+	if (ps.src === 'list' && ps.userListId) {
+		userList = await UserLists.findOne({
+			id: ps.userListId,
+			userId: user.id,
+		});
+
+		if (userList == null) {
+			throw new ApiError(meta.errors.noSuchUserList);
+		}
+	} else if (ps.src === 'group' && ps.userGroupId) {
+		userGroupJoining = await UserGroupJoinings.findOne({
+			userGroupId: ps.userGroupId,
+			userId: user.id,
+		});
+
+		if (userGroupJoining == null) {
+			throw new ApiError(meta.errors.noSuchUserGroup);
+		}
+	}
+
+	await Antennas.update(antenna.id, {
+		name: ps.name,
+		src: ps.src,
+		userListId: userList ? userList.id : null,
+		userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
+		keywords: ps.keywords,
+		excludeKeywords: ps.excludeKeywords,
+		users: ps.users,
+		caseSensitive: ps.caseSensitive,
+		withReplies: ps.withReplies,
+		withFile: ps.withFile,
+		notify: ps.notify,
+	});
+
+	publishInternalEvent('antennaUpdated', await Antennas.findOneOrFail(antenna.id));
+
+	return await Antennas.pack(antenna.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts
new file mode 100644
index 0000000000..78919f43b0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/ap/get.ts
@@ -0,0 +1,36 @@
+import $ from 'cafy';
+import define from '../../define';
+import Resolver from '@/remote/activitypub/resolver';
+import { ApiError } from '../../error';
+import * as ms from 'ms';
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: true as const,
+
+	limit: {
+		duration: ms('1hour'),
+		max: 30
+	},
+
+	params: {
+		uri: {
+			validator: $.str,
+		},
+	},
+
+	errors: {
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+	}
+};
+
+export default define(meta, async (ps) => {
+	const resolver = new Resolver();
+	const object = await resolver.resolve(ps.uri);
+	return object;
+});
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
new file mode 100644
index 0000000000..2280d93724
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -0,0 +1,190 @@
+import $ from 'cafy';
+import define from '../../define';
+import config from '@/config/index';
+import { createPerson } from '@/remote/activitypub/models/person';
+import { createNote } from '@/remote/activitypub/models/note';
+import Resolver from '@/remote/activitypub/resolver';
+import { ApiError } from '../../error';
+import { extractDbHost } from '@/misc/convert-host';
+import { Users, Notes } from '@/models/index';
+import { Note } from '@/models/entities/note';
+import { User } from '@/models/entities/user';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { isActor, isPost, getApId } from '@/remote/activitypub/type';
+import * as ms from 'ms';
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: true as const,
+
+	limit: {
+		duration: ms('1hour'),
+		max: 30
+	},
+
+	params: {
+		uri: {
+			validator: $.str,
+		},
+	},
+
+	errors: {
+		noSuchObject: {
+			message: 'No such object.',
+			code: 'NO_SUCH_OBJECT',
+			id: 'dc94d745-1262-4e63-a17d-fecaa57efc82'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			type: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				enum: ['User', 'Note']
+			},
+			object: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	const object = await fetchAny(ps.uri);
+	if (object) {
+		return object;
+	} else {
+		throw new ApiError(meta.errors.noSuchObject);
+	}
+});
+
+/***
+ * URIからUserかNoteを解決する
+ */
+async function fetchAny(uri: string) {
+	// URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ
+	if (uri.startsWith(config.url + '/')) {
+		const parts = uri.split('/');
+		const id = parts.pop();
+		const type = parts.pop();
+
+		if (type === 'notes') {
+			const note = await Notes.findOne(id);
+
+			if (note) {
+				return {
+					type: 'Note',
+					object: await Notes.pack(note, null, { detail: true })
+				};
+			}
+		} else if (type === 'users') {
+			const user = await Users.findOne(id);
+
+			if (user) {
+				return {
+					type: 'User',
+					object: await Users.pack(user, null, { detail: true })
+				};
+			}
+		}
+	}
+
+	// ブロックしてたら中断
+	const meta = await fetchMeta();
+	if (meta.blockedHosts.includes(extractDbHost(uri))) return null;
+
+	// URI(AP Object id)としてDB検索
+	{
+		const [user, note] = await Promise.all([
+			Users.findOne({ uri: uri }),
+			Notes.findOne({ uri: uri })
+		]);
+
+		const packed = await mergePack(user, note);
+		if (packed !== null) return packed;
+	}
+
+	// リモートから一旦オブジェクトフェッチ
+	const resolver = new Resolver();
+	const object = await resolver.resolve(uri) as any;
+
+	// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
+	// これはDBに存在する可能性があるため再度DB検索
+	if (uri !== object.id) {
+		if (object.id.startsWith(config.url + '/')) {
+			const parts = object.id.split('/');
+			const id = parts.pop();
+			const type = parts.pop();
+
+			if (type === 'notes') {
+				const note = await Notes.findOne(id);
+
+				if (note) {
+					return {
+						type: 'Note',
+						object: await Notes.pack(note, null, { detail: true })
+					};
+				}
+			} else if (type === 'users') {
+				const user = await Users.findOne(id);
+
+				if (user) {
+					return {
+						type: 'User',
+						object: await Users.pack(user, null, { detail: true })
+					};
+				}
+			}
+		}
+
+		const [user, note] = await Promise.all([
+			Users.findOne({ uri: object.id }),
+			Notes.findOne({ uri: object.id })
+		]);
+
+		const packed = await mergePack(user, note);
+		if (packed !== null) return packed;
+	}
+
+	// それでもみつからなければ新規であるため登録
+	if (isActor(object)) {
+		const user = await createPerson(getApId(object));
+		return {
+			type: 'User',
+			object: await Users.pack(user, null, { detail: true })
+		};
+	}
+
+	if (isPost(object)) {
+		const note = await createNote(getApId(object), undefined, true);
+		return {
+			type: 'Note',
+			object: await Notes.pack(note!, null, { detail: true })
+		};
+	}
+
+	return null;
+}
+
+async function mergePack(user: User | null | undefined, note: Note | null | undefined) {
+	if (user != null) {
+		return {
+			type: 'User',
+			object: await Users.pack(user, null, { detail: true })
+		};
+	}
+
+	if (note != null) {
+		return {
+			type: 'Note',
+			object: await Notes.pack(note, null, { detail: true })
+		};
+	}
+
+	return null;
+}
diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts
new file mode 100644
index 0000000000..c2ce943dcc
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/app/create.ts
@@ -0,0 +1,63 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Apps } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { unique } from '@/prelude/array';
+import { secureRndstr } from '@/misc/secure-rndstr';
+
+export const meta = {
+	tags: ['app'],
+
+	requireCredential: false as const,
+
+	params: {
+		name: {
+			validator: $.str,
+		},
+
+		description: {
+			validator: $.str,
+		},
+
+		permission: {
+			validator: $.arr($.str).unique(),
+		},
+
+		// TODO: Check it is valid url
+		callbackUrl: {
+			validator: $.optional.nullable.str,
+			default: null,
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'App',
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	// Generate secret
+	const secret = secureRndstr(32, true);
+
+	// for backward compatibility
+	const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
+
+	// Create account
+	const app = await Apps.save({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user ? user.id : null,
+		name: ps.name,
+		description: ps.description,
+		permission,
+		callbackUrl: ps.callbackUrl,
+		secret: secret
+	});
+
+	return await Apps.pack(app, null, {
+		detail: true,
+		includeSecret: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts
new file mode 100644
index 0000000000..27f12eb44f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/app/show.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Apps } from '@/models/index';
+
+export const meta = {
+	tags: ['app'],
+
+	params: {
+		appId: {
+			validator: $.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'App',
+	},
+
+	errors: {
+		noSuchApp: {
+			message: 'No such app.',
+			code: 'NO_SUCH_APP',
+			id: 'dce83913-2dc6-4093-8a7b-71dbb11718a3'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'App'
+	}
+};
+
+export default define(meta, async (ps, user, token) => {
+	const isSecure = user != null && token == null;
+
+	// Lookup app
+	const ap = await Apps.findOne(ps.appId);
+
+	if (ap == null) {
+		throw new ApiError(meta.errors.noSuchApp);
+	}
+
+	return await Apps.pack(ap, user, {
+		detail: true,
+		includeSecret: isSecure && (ap.userId === user!.id)
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts
new file mode 100644
index 0000000000..1d1d8ac227
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/auth/accept.ts
@@ -0,0 +1,76 @@
+import * as crypto from 'crypto';
+import $ from 'cafy';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { AuthSessions, AccessTokens, Apps } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { secureRndstr } from '@/misc/secure-rndstr';
+
+export const meta = {
+	tags: ['auth'],
+
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		token: {
+			validator: $.str
+		}
+	},
+
+	errors: {
+		noSuchSession: {
+			message: 'No such session.',
+			code: 'NO_SUCH_SESSION',
+			id: '9c72d8de-391a-43c1-9d06-08d29efde8df'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch token
+	const session = await AuthSessions
+		.findOne({ token: ps.token });
+
+	if (session == null) {
+		throw new ApiError(meta.errors.noSuchSession);
+	}
+
+	// Generate access token
+	const accessToken = secureRndstr(32, true);
+
+	// Fetch exist access token
+	const exist = await AccessTokens.findOne({
+		appId: session.appId,
+		userId: user.id,
+	});
+
+	if (exist == null) {
+		// Lookup app
+		const app = await Apps.findOneOrFail(session.appId);
+
+		// Generate Hash
+		const sha256 = crypto.createHash('sha256');
+		sha256.update(accessToken + app.secret);
+		const hash = sha256.digest('hex');
+
+		const now = new Date();
+
+		// Insert access token doc
+		await AccessTokens.insert({
+			id: genId(),
+			createdAt: now,
+			lastUsedAt: now,
+			appId: session.appId,
+			userId: user.id,
+			token: accessToken,
+			hash: hash
+		});
+	}
+
+	// Update session
+	await AuthSessions.update(session.id, {
+		userId: user.id
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
new file mode 100644
index 0000000000..859cf52ed3
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts
@@ -0,0 +1,70 @@
+import { v4 as uuid } from 'uuid';
+import $ from 'cafy';
+import config from '@/config/index';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { Apps, AuthSessions } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['auth'],
+
+	requireCredential: false as const,
+
+	params: {
+		appSecret: {
+			validator: $.str,
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			token: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			url: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'url',
+			},
+		}
+	},
+
+	errors: {
+		noSuchApp: {
+			message: 'No such app.',
+			code: 'NO_SUCH_APP',
+			id: '92f93e63-428e-4f2f-a5a4-39e1407fe998'
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	// Lookup app
+	const app = await Apps.findOne({
+		secret: ps.appSecret
+	});
+
+	if (app == null) {
+		throw new ApiError(meta.errors.noSuchApp);
+	}
+
+	// Generate token
+	const token = uuid();
+
+	// Create session token document
+	const doc = await AuthSessions.save({
+		id: genId(),
+		createdAt: new Date(),
+		appId: app.id,
+		token: token
+	});
+
+	return {
+		token: doc.token,
+		url: `${config.authUrl}/${doc.token}`
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts
new file mode 100644
index 0000000000..23f1a56a37
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/auth/session/show.ts
@@ -0,0 +1,58 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { AuthSessions } from '@/models/index';
+
+export const meta = {
+	tags: ['auth'],
+
+	requireCredential: false as const,
+
+	params: {
+		token: {
+			validator: $.str,
+		}
+	},
+
+	errors: {
+		noSuchSession: {
+			message: 'No such session.',
+			code: 'NO_SUCH_SESSION',
+			id: 'bd72c97d-eba7-4adb-a467-f171b8847250'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			id: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			},
+			app: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				ref: 'App'
+			},
+			token: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Lookup session
+	const session = await AuthSessions.findOne({
+		token: ps.token
+	});
+
+	if (session == null) {
+		throw new ApiError(meta.errors.noSuchSession);
+	}
+
+	return await AuthSessions.pack(session, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts
new file mode 100644
index 0000000000..72201cb207
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts
@@ -0,0 +1,98 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { Apps, AuthSessions, AccessTokens, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['auth'],
+
+	requireCredential: false as const,
+
+	params: {
+		appSecret: {
+			validator: $.str,
+		},
+
+		token: {
+			validator: $.str,
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			accessToken: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+
+			user: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				ref: 'User',
+			},
+		}
+	},
+
+	errors: {
+		noSuchApp: {
+			message: 'No such app.',
+			code: 'NO_SUCH_APP',
+			id: 'fcab192a-2c5a-43b7-8ad8-9b7054d8d40d'
+		},
+
+		noSuchSession: {
+			message: 'No such session.',
+			code: 'NO_SUCH_SESSION',
+			id: '5b5a1503-8bc8-4bd0-8054-dc189e8cdcb3'
+		},
+
+		pendingSession: {
+			message: 'This session is not completed yet.',
+			code: 'PENDING_SESSION',
+			id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e'
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	// Lookup app
+	const app = await Apps.findOne({
+		secret: ps.appSecret
+	});
+
+	if (app == null) {
+		throw new ApiError(meta.errors.noSuchApp);
+	}
+
+	// Fetch token
+	const session = await AuthSessions.findOne({
+		token: ps.token,
+		appId: app.id
+	});
+
+	if (session == null) {
+		throw new ApiError(meta.errors.noSuchSession);
+	}
+
+	if (session.userId == null) {
+		throw new ApiError(meta.errors.pendingSession);
+	}
+
+	// Lookup access token
+	const accessToken = await AccessTokens.findOneOrFail({
+		appId: app.id,
+		userId: session.userId
+	});
+
+	// Delete session
+	AuthSessions.delete(session.id);
+
+	return {
+		accessToken: accessToken.token,
+		user: await Users.pack(session.userId, null, {
+			detail: true
+		})
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts
new file mode 100644
index 0000000000..2953252394
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/blocking/create.ts
@@ -0,0 +1,89 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import * as ms from 'ms';
+import create from '@/services/blocking/create';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { Blockings, NoteWatchings, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['account'],
+
+	limit: {
+		duration: ms('1hour'),
+		max: 100
+	},
+
+	requireCredential: true as const,
+
+	kind: 'write:blocks',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e'
+		},
+
+		blockeeIsYourself: {
+			message: 'Blockee is yourself.',
+			code: 'BLOCKEE_IS_YOURSELF',
+			id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6'
+		},
+
+		alreadyBlocking: {
+			message: 'You are already blocking that user.',
+			code: 'ALREADY_BLOCKING',
+			id: '787fed64-acb9-464a-82eb-afbd745b9614'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const blocker = await Users.findOneOrFail(user.id);
+
+	// 自分自身
+	if (user.id === ps.userId) {
+		throw new ApiError(meta.errors.blockeeIsYourself);
+	}
+
+	// Get blockee
+	const blockee = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Check if already blocking
+	const exist = await Blockings.findOne({
+		blockerId: blocker.id,
+		blockeeId: blockee.id
+	});
+
+	if (exist != null) {
+		throw new ApiError(meta.errors.alreadyBlocking);
+	}
+
+	await create(blocker, blockee);
+
+	NoteWatchings.delete({
+		userId: blocker.id,
+		noteUserId: blockee.id
+	});
+
+	return await Users.pack(blockee.id, blocker, {
+		detail: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts
new file mode 100644
index 0000000000..a66e46fdf0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts
@@ -0,0 +1,85 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import * as ms from 'ms';
+import deleteBlocking from '@/services/blocking/delete';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { Blockings, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['account'],
+
+	limit: {
+		duration: ms('1hour'),
+		max: 100
+	},
+
+	requireCredential: true as const,
+
+	kind: 'write:blocks',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '8621d8bf-c358-4303-a066-5ea78610eb3f'
+		},
+
+		blockeeIsYourself: {
+			message: 'Blockee is yourself.',
+			code: 'BLOCKEE_IS_YOURSELF',
+			id: '06f6fac6-524b-473c-a354-e97a40ae6eac'
+		},
+
+		notBlocking: {
+			message: 'You are not blocking that user.',
+			code: 'NOT_BLOCKING',
+			id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User',
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const blocker = await Users.findOneOrFail(user.id);
+
+	// Check if the blockee is yourself
+	if (user.id === ps.userId) {
+		throw new ApiError(meta.errors.blockeeIsYourself);
+	}
+
+	// Get blockee
+	const blockee = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Check not blocking
+	const exist = await Blockings.findOne({
+		blockerId: blocker.id,
+		blockeeId: blockee.id
+	});
+
+	if (exist == null) {
+		throw new ApiError(meta.errors.notBlocking);
+	}
+
+	// Delete blocking
+	await deleteBlocking(blocker, blockee);
+
+	return await Users.pack(blockee.id, blocker, {
+		detail: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts
new file mode 100644
index 0000000000..fe25fdaba1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/blocking/list.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Blockings } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:blocks',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 30
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Blocking',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Blockings.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
+		.andWhere(`blocking.blockerId = :meId`, { meId: me.id });
+
+	const blockings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Blockings.packMany(blockings, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts
new file mode 100644
index 0000000000..0cedfd6c6a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/create.ts
@@ -0,0 +1,68 @@
+import $ from 'cafy';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Channels, DriveFiles } from '@/models/index';
+import { Channel } from '@/models/entities/channel';
+import { genId } from '@/misc/gen-id';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['channels'],
+
+	requireCredential: true as const,
+
+	kind: 'write:channels',
+
+	params: {
+		name: {
+			validator: $.str.range(1, 128)
+		},
+
+		description: {
+			validator: $.nullable.optional.str.range(1, 2048)
+		},
+
+		bannerId: {
+			validator: $.nullable.optional.type(ID),
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Channel',
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	let banner = null;
+	if (ps.bannerId != null) {
+		banner = await DriveFiles.findOne({
+			id: ps.bannerId,
+			userId: user.id
+		});
+
+		if (banner == null) {
+			throw new ApiError(meta.errors.noSuchFile);
+		}
+	}
+
+	const channel = await Channels.save({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		name: ps.name,
+		description: ps.description || null,
+		bannerId: banner ? banner.id : null,
+	} as Channel);
+
+	return await Channels.pack(channel, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts
new file mode 100644
index 0000000000..dc1f49f960
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/featured.ts
@@ -0,0 +1,28 @@
+import define from '../../define';
+import { Channels } from '@/models/index';
+
+export const meta = {
+	tags: ['channels'],
+
+	requireCredential: false as const,
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Channel',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = Channels.createQueryBuilder('channel')
+		.where('channel.lastNotedAt IS NOT NULL')
+		.orderBy('channel.lastNotedAt', 'DESC');
+
+	const channels = await query.take(10).getMany();
+
+	return await Promise.all(channels.map(x => Channels.pack(x, me)));
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts
new file mode 100644
index 0000000000..d4664e6996
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/follow.ts
@@ -0,0 +1,48 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Channels, ChannelFollowings } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { publishUserEvent } from '@/services/stream';
+
+export const meta = {
+	tags: ['channels'],
+
+	requireCredential: true as const,
+
+	kind: 'write:channels',
+
+	params: {
+		channelId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchChannel: {
+			message: 'No such channel.',
+			code: 'NO_SUCH_CHANNEL',
+			id: 'c0031718-d573-4e85-928e-10039f1fbb68'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const channel = await Channels.findOne({
+		id: ps.channelId,
+	});
+
+	if (channel == null) {
+		throw new ApiError(meta.errors.noSuchChannel);
+	}
+
+	await ChannelFollowings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		followerId: user.id,
+		followeeId: channel.id,
+	});
+
+	publishUserEvent(user.id, 'followChannel', channel);
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts
new file mode 100644
index 0000000000..be239a01d6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/followed.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Channels, ChannelFollowings } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['channels', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:channels',
+
+	params: {
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 5
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Channel',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(ChannelFollowings.createQueryBuilder(), ps.sinceId, ps.untilId)
+		.andWhere({ followerId: me.id });
+
+	const followings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Promise.all(followings.map(x => Channels.pack(x.followeeId, me)));
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts
new file mode 100644
index 0000000000..4a2e9db17b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/owned.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Channels } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['channels', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:channels',
+
+	params: {
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 5
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Channel',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Channels.createQueryBuilder(), ps.sinceId, ps.untilId)
+		.andWhere({ userId: me.id });
+
+	const channels = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Promise.all(channels.map(x => Channels.pack(x, me)));
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/pin-note.ts b/packages/backend/src/server/api/endpoints/channels/pin-note.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts
new file mode 100644
index 0000000000..803ce6363d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/show.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Channels } from '@/models/index';
+
+export const meta = {
+	tags: ['channels'],
+
+	requireCredential: false as const,
+
+	params: {
+		channelId: {
+			validator: $.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Channel',
+	},
+
+	errors: {
+		noSuchChannel: {
+			message: 'No such channel.',
+			code: 'NO_SUCH_CHANNEL',
+			id: '6f6c314b-7486-4897-8966-c04a66a02923'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const channel = await Channels.findOne({
+		id: ps.channelId,
+	});
+
+	if (channel == null) {
+		throw new ApiError(meta.errors.noSuchChannel);
+	}
+
+	return await Channels.pack(channel, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
new file mode 100644
index 0000000000..0ed057a11e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -0,0 +1,85 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Notes, Channels } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { activeUsersChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['notes', 'channels'],
+
+	requireCredential: false as const,
+
+	params: {
+		channelId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		sinceDate: {
+			validator: $.optional.num,
+		},
+
+		untilDate: {
+			validator: $.optional.num,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		noSuchChannel: {
+			message: 'No such channel.',
+			code: 'NO_SUCH_CHANNEL',
+			id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const channel = await Channels.findOne({
+		id: ps.channelId,
+	});
+
+	if (channel == null) {
+		throw new ApiError(meta.errors.noSuchChannel);
+	}
+
+	//#region Construct query
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere('note.channelId = :channelId', { channelId: channel.id })
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
+		.leftJoinAndSelect('note.channel', 'channel');
+	//#endregion
+
+	const timeline = await query.take(ps.limit!).getMany();
+
+	if (user) activeUsersChart.update(user);
+
+	return await Notes.packMany(timeline, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts
new file mode 100644
index 0000000000..700f8e93ba
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Channels, ChannelFollowings } from '@/models/index';
+import { publishUserEvent } from '@/services/stream';
+
+export const meta = {
+	tags: ['channels'],
+
+	requireCredential: true as const,
+
+	kind: 'write:channels',
+
+	params: {
+		channelId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchChannel: {
+			message: 'No such channel.',
+			code: 'NO_SUCH_CHANNEL',
+			id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const channel = await Channels.findOne({
+		id: ps.channelId,
+	});
+
+	if (channel == null) {
+		throw new ApiError(meta.errors.noSuchChannel);
+	}
+
+	await ChannelFollowings.delete({
+		followerId: user.id,
+		followeeId: channel.id,
+	});
+
+	publishUserEvent(user.id, 'unfollowChannel', channel);
+});
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
new file mode 100644
index 0000000000..9b447bd04b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -0,0 +1,94 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Channels, DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['channels'],
+
+	requireCredential: true as const,
+
+	kind: 'write:channels',
+
+	params: {
+		channelId: {
+			validator: $.type(ID),
+		},
+
+		name: {
+			validator: $.optional.str.range(1, 128)
+		},
+
+		description: {
+			validator: $.nullable.optional.str.range(1, 2048)
+		},
+
+		bannerId: {
+			validator: $.nullable.optional.type(ID),
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Channel',
+	},
+
+	errors: {
+		noSuchChannel: {
+			message: 'No such channel.',
+			code: 'NO_SUCH_CHANNEL',
+			id: 'f9c5467f-d492-4c3c-9a8d-a70dacc86512'
+		},
+
+		accessDenied: {
+			message: 'You do not have edit privilege of the channel.',
+			code: 'ACCESS_DENIED',
+			id: '1fb7cb09-d46a-4fdf-b8df-057788cce513'
+		},
+
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const channel = await Channels.findOne({
+		id: ps.channelId,
+	});
+
+	if (channel == null) {
+		throw new ApiError(meta.errors.noSuchChannel);
+	}
+
+	if (channel.userId !== me.id) {
+		throw new ApiError(meta.errors.accessDenied);
+	}
+
+	// tslint:disable-next-line:no-unnecessary-initializer
+	let banner = undefined;
+	if (ps.bannerId != null) {
+		banner = await DriveFiles.findOne({
+			id: ps.bannerId,
+			userId: me.id
+		});
+
+		if (banner == null) {
+			throw new ApiError(meta.errors.noSuchFile);
+		}
+	} else if (ps.bannerId === null) {
+		banner = null;
+	}
+
+	await Channels.update(channel.id, {
+		...(ps.name !== undefined ? { name: ps.name } : {}),
+		...(ps.description !== undefined ? { description: ps.description } : {}),
+		...(banner ? { bannerId: banner.id } : {}),
+	});
+
+	return await Channels.pack(channel.id, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts
new file mode 100644
index 0000000000..c4878f7d61
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts
@@ -0,0 +1,30 @@
+import $ from 'cafy';
+import define from '../../define';
+import { convertLog } from '@/services/chart/core';
+import { activeUsersChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'users'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+	},
+
+	res: convertLog(activeUsersChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await activeUsersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts
new file mode 100644
index 0000000000..07bff82cf4
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/drive.ts
@@ -0,0 +1,30 @@
+import $ from 'cafy';
+import define from '../../define';
+import { convertLog } from '@/services/chart/core';
+import { driveChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'drive'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+	},
+
+	res: convertLog(driveChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await driveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts
new file mode 100644
index 0000000000..9575f9a7b7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/federation.ts
@@ -0,0 +1,30 @@
+import $ from 'cafy';
+import define from '../../define';
+import { convertLog } from '@/services/chart/core';
+import { federationChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+	},
+
+	res: convertLog(federationChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await federationChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/hashtag.ts b/packages/backend/src/server/api/endpoints/charts/hashtag.ts
new file mode 100644
index 0000000000..53dc61e51e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/hashtag.ts
@@ -0,0 +1,34 @@
+import $ from 'cafy';
+import define from '../../define';
+import { convertLog } from '@/services/chart/core';
+import { hashtagChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'hashtags'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+
+		tag: {
+			validator: $.str,
+		},
+	},
+
+	res: convertLog(hashtagChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await hashtagChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.tag);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts
new file mode 100644
index 0000000000..1835023188
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/instance.ts
@@ -0,0 +1,34 @@
+import $ from 'cafy';
+import define from '../../define';
+import { convertLog } from '@/services/chart/core';
+import { instanceChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+
+		host: {
+			validator: $.str,
+		}
+	},
+
+	res: convertLog(instanceChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await instanceChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.host);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/network.ts b/packages/backend/src/server/api/endpoints/charts/network.ts
new file mode 100644
index 0000000000..b524df93be
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/network.ts
@@ -0,0 +1,30 @@
+import $ from 'cafy';
+import define from '../../define';
+import { convertLog } from '@/services/chart/core';
+import { networkChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+	},
+
+	res: convertLog(networkChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await networkChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts
new file mode 100644
index 0000000000..676f302939
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/notes.ts
@@ -0,0 +1,30 @@
+import $ from 'cafy';
+import define from '../../define';
+import { convertLog } from '@/services/chart/core';
+import { notesChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'notes'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+	},
+
+	res: convertLog(notesChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await notesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts
new file mode 100644
index 0000000000..f2770e2df8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts
@@ -0,0 +1,35 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { convertLog } from '@/services/chart/core';
+import { perUserDriveChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'drive', 'users'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	res: convertLog(perUserDriveChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await perUserDriveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts
new file mode 100644
index 0000000000..8c97b63e89
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts
@@ -0,0 +1,35 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { convertLog } from '@/services/chart/core';
+import { perUserFollowingChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'users', 'following'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	res: convertLog(perUserFollowingChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await perUserFollowingChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts
new file mode 100644
index 0000000000..0d5f5a8b6a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts
@@ -0,0 +1,35 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { convertLog } from '@/services/chart/core';
+import { perUserNotesChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'users', 'notes'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	res: convertLog(perUserNotesChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await perUserNotesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
new file mode 100644
index 0000000000..3cabe40d56
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
@@ -0,0 +1,35 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ID } from '@/misc/cafy-id';
+import { convertLog } from '@/services/chart/core';
+import { perUserReactionsChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'users', 'reactions'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	res: convertLog(perUserReactionsChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await perUserReactionsChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
+});
diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts
new file mode 100644
index 0000000000..deac89b59d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/charts/users.ts
@@ -0,0 +1,30 @@
+import $ from 'cafy';
+import define from '../../define';
+import { convertLog } from '@/services/chart/core';
+import { usersChart } from '@/services/chart/index';
+
+export const meta = {
+	tags: ['charts', 'users'],
+
+	params: {
+		span: {
+			validator: $.str.or(['day', 'hour']),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 500),
+			default: 30,
+		},
+
+		offset: {
+			validator: $.optional.nullable.num,
+			default: null,
+		},
+	},
+
+	res: convertLog(usersChart.schema),
+};
+
+export default define(meta, async (ps) => {
+	return await usersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
+});
diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts
new file mode 100644
index 0000000000..79d7b8adde
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts
@@ -0,0 +1,76 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ClipNotes, Clips } from '@/models/index';
+import { ApiError } from '../../error';
+import { genId } from '@/misc/gen-id';
+import { getNote } from '../../common/getters';
+
+export const meta = {
+	tags: ['account', 'notes', 'clips'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		clipId: {
+			validator: $.type(ID),
+		},
+
+		noteId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf'
+		},
+
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b'
+		},
+
+		alreadyClipped: {
+			message: 'The note has already been clipped.',
+			code: 'ALREADY_CLIPPED',
+			id: '734806c4-542c-463a-9311-15c512803965'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+		userId: user.id
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const exist = await ClipNotes.findOne({
+		noteId: note.id,
+		clipId: clip.id
+	});
+
+	if (exist != null) {
+		throw new ApiError(meta.errors.alreadyClipped);
+	}
+
+	await ClipNotes.insert({
+		id: genId(),
+		noteId: note.id,
+		clipId: clip.id
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts
new file mode 100644
index 0000000000..02d2773709
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/create.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import define from '../../define';
+import { genId } from '@/misc/gen-id';
+import { Clips } from '@/models/index';
+
+export const meta = {
+	tags: ['clips'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		name: {
+			validator: $.str.range(1, 100)
+		},
+
+		isPublic: {
+			validator: $.optional.bool
+		},
+
+		description: {
+			validator: $.optional.nullable.str.range(1, 2048)
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Clip'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const clip = await Clips.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		name: ps.name,
+		isPublic: ps.isPublic,
+		description: ps.description,
+	}).then(x => Clips.findOneOrFail(x.identifiers[0]));
+
+	return await Clips.pack(clip);
+});
diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts
new file mode 100644
index 0000000000..ca489af3bf
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Clips } from '@/models/index';
+
+export const meta = {
+	tags: ['clips'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		clipId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: '70ca08ba-6865-4630-b6fb-8494759aa754'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+		userId: user.id
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	await Clips.delete(clip.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts
new file mode 100644
index 0000000000..1f6db9b979
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/list.ts
@@ -0,0 +1,28 @@
+import define from '../../define';
+import { Clips } from '@/models/index';
+
+export const meta = {
+	tags: ['clips', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:account',
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Clip'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const clips = await Clips.find({
+		userId: me.id,
+	});
+
+	return await Promise.all(clips.map(x => Clips.pack(x)));
+});
diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts
new file mode 100644
index 0000000000..5a9fed52fa
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/notes.ts
@@ -0,0 +1,93 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ClipNotes, Clips, Notes } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { ApiError } from '../../error';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['account', 'notes', 'clips'],
+
+	requireCredential: false as const,
+
+	kind: 'read:account',
+
+	params: {
+		clipId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00'
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	if (!clip.isPublic && (user == null || (clip.userId !== user.id))) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	const clipQuery = ClipNotes.createQueryBuilder('joining')
+		.select('joining.noteId')
+		.where('joining.clipId = :clipId', { clipId: clip.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.id IN (${ clipQuery.getQuery() })`)
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
+		.setParameters(clipQuery.getParameters());
+
+	if (user) {
+		generateVisibilityQuery(query, user);
+		generateMutedUserQuery(query, user);
+		generateBlockedUserQuery(query, user);
+	}
+
+	const notes = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Notes.packMany(notes, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts
new file mode 100644
index 0000000000..8f245cd18e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/show.ts
@@ -0,0 +1,50 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Clips } from '@/models/index';
+
+export const meta = {
+	tags: ['clips', 'account'],
+
+	requireCredential: false as const,
+
+	kind: 'read:account',
+
+	params: {
+		clipId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Clip'
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the clip
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	if (!clip.isPublic && (me == null || (clip.userId !== me.id))) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	return await Clips.pack(clip);
+});
diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts
new file mode 100644
index 0000000000..7f645560bb
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/clips/update.ts
@@ -0,0 +1,65 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Clips } from '@/models/index';
+
+export const meta = {
+	tags: ['clips'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		clipId: {
+			validator: $.type(ID),
+		},
+
+		name: {
+			validator: $.str.range(1, 100),
+		},
+
+		isPublic: {
+			validator: $.optional.bool
+		},
+
+		description: {
+			validator: $.optional.nullable.str.range(1, 2048)
+		}
+	},
+
+	errors: {
+		noSuchClip: {
+			message: 'No such clip.',
+			code: 'NO_SUCH_CLIP',
+			id: 'b4d92d70-b216-46fa-9a3f-a8c811699257'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Clip'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch the clip
+	const clip = await Clips.findOne({
+		id: ps.clipId,
+		userId: user.id
+	});
+
+	if (clip == null) {
+		throw new ApiError(meta.errors.noSuchClip);
+	}
+
+	await Clips.update(clip.id, {
+		name: ps.name,
+		description: ps.description,
+		isPublic: ps.isPublic,
+	});
+
+	return await Clips.pack(clip.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
new file mode 100644
index 0000000000..2974ccfab9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -0,0 +1,38 @@
+import define from '../define';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['drive', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			capacity: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+			usage: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const instance = await fetchMeta(true);
+
+	// Calculate drive usage
+	const usage = await DriveFiles.calcDriveUsageOf(user.id);
+
+	return {
+		capacity: 1024 * 1024 * instance.localDriveCapacityMb,
+		usage: usage
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts
new file mode 100644
index 0000000000..95435e1e43
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files.ts
@@ -0,0 +1,70 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { DriveFiles } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		folderId: {
+			validator: $.optional.nullable.type(ID),
+			default: null,
+		},
+
+		type: {
+			validator: $.optional.nullable.str.match(/^[a-zA-Z\/\-*]+$/)
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'DriveFile',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
+		.andWhere('file.userId = :userId', { userId: user.id });
+
+	if (ps.folderId) {
+		query.andWhere('file.folderId = :folderId', { folderId: ps.folderId });
+	} else {
+		query.andWhere('file.folderId IS NULL');
+	}
+
+	if (ps.type) {
+		if (ps.type.endsWith('/*')) {
+			query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
+		} else {
+			query.andWhere('file.type = :type', { type: ps.type });
+		}
+	}
+
+	const files = await query.take(ps.limit!).getMany();
+
+	return await DriveFiles.packMany(files, { detail: false, self: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
new file mode 100644
index 0000000000..eec7d7877e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
@@ -0,0 +1,57 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { DriveFiles, Notes } from '@/models/index';
+
+export const meta = {
+	tags: ['drive', 'notes'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		fileId: {
+			validator: $.type(ID),
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'c118ece3-2e4b-4296-99d1-51756e32d232',
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch file
+	const file = await DriveFiles.findOne({
+		id: ps.fileId,
+		userId: user.id,
+	});
+
+	if (file == null) {
+		throw new ApiError(meta.errors.noSuchFile);
+	}
+
+	const notes = await Notes.createQueryBuilder('note')
+		.where(':file = ANY(note.fileIds)', { file: file.id })
+		.getMany();
+
+	return await Notes.packMany(notes, user, {
+		detail: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
new file mode 100644
index 0000000000..2c36078421
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
@@ -0,0 +1,31 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		md5: {
+			validator: $.str,
+		}
+	},
+
+	res: {
+		type: 'boolean' as const,
+		optional: false as const, nullable: false as const,
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const file = await DriveFiles.findOne({
+		md5: ps.md5,
+		userId: user.id,
+	});
+
+	return file != null;
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
new file mode 100644
index 0000000000..2abc104e6c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -0,0 +1,89 @@
+import * as ms from 'ms';
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import create from '@/services/drive/add-file';
+import define from '../../../define';
+import { apiLogger } from '../../../logger';
+import { ApiError } from '../../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	limit: {
+		duration: ms('1hour'),
+		max: 120
+	},
+
+	requireFile: true,
+
+	kind: 'write:drive',
+
+	params: {
+		folderId: {
+			validator: $.optional.nullable.type(ID),
+			default: null,
+		},
+
+		name: {
+			validator: $.optional.nullable.str,
+			default: null,
+		},
+
+		isSensitive: {
+			validator: $.optional.either($.bool, $.str),
+			default: false,
+			transform: (v: any): boolean => v === true || v === 'true',
+		},
+
+		force: {
+			validator: $.optional.either($.bool, $.str),
+			default: false,
+			transform: (v: any): boolean => v === true || v === 'true',
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'DriveFile',
+	},
+
+	errors: {
+		invalidFileName: {
+			message: 'Invalid file name.',
+			code: 'INVALID_FILE_NAME',
+			id: 'f449b209-0c60-4e51-84d5-29486263bfd4'
+		}
+	}
+};
+
+export default define(meta, async (ps, user, _, file, cleanup) => {
+	// Get 'name' parameter
+	let name = ps.name || file.originalname;
+	if (name !== undefined && name !== null) {
+		name = name.trim();
+		if (name.length === 0) {
+			name = null;
+		} else if (name === 'blob') {
+			name = null;
+		} else if (!DriveFiles.validateFileName(name)) {
+			throw new ApiError(meta.errors.invalidFileName);
+		}
+	} else {
+		name = null;
+	}
+
+	try {
+		// Create file
+		const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
+		return await DriveFiles.pack(driveFile, { self: true });
+	} catch (e) {
+		apiLogger.error(e);
+		throw new ApiError();
+	} finally {
+		cleanup!();
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
new file mode 100644
index 0000000000..038325694d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
@@ -0,0 +1,53 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { deleteFile } from '@/services/drive/delete-file';
+import { publishDriveStream } from '@/services/stream';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'write:drive',
+
+	params: {
+		fileId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: '908939ec-e52b-4458-b395-1025195cea58'
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '5eb8d909-2540-4970-90b8-dd6f86088121'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const file = await DriveFiles.findOne(ps.fileId);
+
+	if (file == null) {
+		throw new ApiError(meta.errors.noSuchFile);
+	}
+
+	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
+		throw new ApiError(meta.errors.accessDenied);
+	}
+
+	// Delete
+	await deleteFile(file);
+
+	// Publish fileDeleted event
+	publishDriveStream(user.id, 'fileDeleted', file.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts
new file mode 100644
index 0000000000..5fea7bbbb0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts
@@ -0,0 +1,36 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		md5: {
+			validator: $.str,
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'DriveFile',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const files = await DriveFiles.find({
+		md5: ps.md5,
+		userId: user.id,
+	});
+
+	return await DriveFiles.packMany(files, { self: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts
new file mode 100644
index 0000000000..dd419f4c04
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	tags: ['drive'],
+
+	kind: 'read:drive',
+
+	params: {
+		name: {
+			validator: $.str
+		},
+
+		folderId: {
+			validator: $.optional.nullable.type(ID),
+			default: null,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'DriveFile',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const files = await DriveFiles.find({
+		name: ps.name,
+		userId: user.id,
+		folderId: ps.folderId
+	});
+
+	return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true })));
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts
new file mode 100644
index 0000000000..a96ebaa123
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts
@@ -0,0 +1,84 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		fileId: {
+			validator: $.optional.type(ID),
+		},
+
+		url: {
+			validator: $.optional.str,
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'DriveFile',
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: '067bc436-2718-4795-b0fb-ecbe43949e31'
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '25b73c73-68b1-41d0-bad1-381cfdf6579f'
+		},
+
+		fileIdOrUrlRequired: {
+			message: 'fileId or url required.',
+			code: 'INVALID_PARAM',
+			id: '89674805-722c-440c-8d88-5641830dc3e4'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	let file: DriveFile | undefined;
+
+	if (ps.fileId) {
+		file = await DriveFiles.findOne(ps.fileId);
+	} else if (ps.url) {
+		file = await DriveFiles.findOne({
+			where: [{
+				url: ps.url
+			}, {
+				webpublicUrl: ps.url
+			}, {
+				thumbnailUrl: ps.url
+			}],
+		});
+	} else {
+		throw new ApiError(meta.errors.fileIdOrUrlRequired);
+	}
+
+	if (file == null) {
+		throw new ApiError(meta.errors.noSuchFile);
+	}
+
+	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
+		throw new ApiError(meta.errors.accessDenied);
+	}
+
+	return await DriveFiles.pack(file, {
+		detail: true,
+		withUser: true,
+		self: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts
new file mode 100644
index 0000000000..f277a9c3dc
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts
@@ -0,0 +1,116 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { publishDriveStream } from '@/services/stream';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { DriveFiles, DriveFolders } from '@/models/index';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'write:drive',
+
+	params: {
+		fileId: {
+			validator: $.type(ID),
+		},
+
+		folderId: {
+			validator: $.optional.nullable.type(ID),
+			default: undefined as any,
+		},
+
+		name: {
+			validator: $.optional.str.pipe(DriveFiles.validateFileName),
+			default: undefined as any,
+		},
+
+		isSensitive: {
+			validator: $.optional.bool,
+			default: undefined as any,
+		},
+
+		comment: {
+			validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
+			default: undefined as any,
+		}
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'e7778c7e-3af9-49cd-9690-6dbc3e6c972d'
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '01a53b27-82fc-445b-a0c1-b558465a8ed2'
+		},
+
+		noSuchFolder: {
+			message: 'No such folder.',
+			code: 'NO_SUCH_FOLDER',
+			id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'DriveFile'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const file = await DriveFiles.findOne(ps.fileId);
+
+	if (file == null) {
+		throw new ApiError(meta.errors.noSuchFile);
+	}
+
+	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
+		throw new ApiError(meta.errors.accessDenied);
+	}
+
+	if (ps.name) file.name = ps.name;
+
+	if (ps.comment !== undefined) file.comment = ps.comment;
+
+	if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
+
+	if (ps.folderId !== undefined) {
+		if (ps.folderId === null) {
+			file.folderId = null;
+		} else {
+			const folder = await DriveFolders.findOne({
+				id: ps.folderId,
+				userId: user.id
+			});
+
+			if (folder == null) {
+				throw new ApiError(meta.errors.noSuchFolder);
+			}
+
+			file.folderId = folder.id;
+		}
+	}
+
+	await DriveFiles.update(file.id, {
+		name: file.name,
+		comment: file.comment,
+		folderId: file.folderId,
+		isSensitive: file.isSensitive
+	});
+
+	const fileObj = await DriveFiles.pack(file, { self: true });
+
+	// Publish fileUpdated event
+	publishDriveStream(user.id, 'fileUpdated', fileObj);
+
+	return fileObj;
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
new file mode 100644
index 0000000000..9f10a42d24
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -0,0 +1,64 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import * as ms from 'ms';
+import uploadFromUrl from '@/services/drive/upload-from-url';
+import define from '../../../define';
+import { DriveFiles } from '@/models/index';
+import { publishMainStream } from '@/services/stream';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
+
+export const meta = {
+	tags: ['drive'],
+
+	limit: {
+		duration: ms('1hour'),
+		max: 60
+	},
+
+	requireCredential: true as const,
+
+	kind: 'write:drive',
+
+	params: {
+		url: {
+			// TODO: Validate this url
+			validator: $.str,
+		},
+
+		folderId: {
+			validator: $.optional.nullable.type(ID),
+			default: null,
+		},
+
+		isSensitive: {
+			validator: $.optional.bool,
+			default: false,
+		},
+
+		comment: {
+			validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
+			default: null,
+		},
+
+		marker: {
+			validator: $.optional.nullable.str,
+			default: null,
+		},
+
+		force: {
+			validator: $.optional.bool,
+			default: false,
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force, false, ps.comment).then(file => {
+		DriveFiles.pack(file, { self: true }).then(packedFile => {
+			publishMainStream(user.id, 'urlUploadFinished', {
+				marker: ps.marker,
+				file: packedFile
+			});
+		});
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts
new file mode 100644
index 0000000000..6f16878b13
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/folders.ts
@@ -0,0 +1,58 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { DriveFolders } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		folderId: {
+			validator: $.optional.nullable.type(ID),
+			default: null,
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'DriveFolder',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId)
+		.andWhere('folder.userId = :userId', { userId: user.id });
+
+	if (ps.folderId) {
+		query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId });
+	} else {
+		query.andWhere('folder.parentId IS NULL');
+	}
+
+	const folders = await query.take(ps.limit!).getMany();
+
+	return await Promise.all(folders.map(folder => DriveFolders.pack(folder)));
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts
new file mode 100644
index 0000000000..80f96bd641
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts
@@ -0,0 +1,72 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { publishDriveStream } from '@/services/stream';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { DriveFolders } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'write:drive',
+
+	params: {
+		name: {
+			validator: $.optional.str.pipe(DriveFolders.validateFolderName),
+			default: 'Untitled',
+		},
+
+		parentId: {
+			validator: $.optional.nullable.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchFolder: {
+			message: 'No such folder.',
+			code: 'NO_SUCH_FOLDER',
+			id: '53326628-a00d-40a6-a3cd-8975105c0f95'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'DriveFolder'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// If the parent folder is specified
+	let parent = null;
+	if (ps.parentId) {
+		// Fetch parent folder
+		parent = await DriveFolders.findOne({
+			id: ps.parentId,
+			userId: user.id
+		});
+
+		if (parent == null) {
+			throw new ApiError(meta.errors.noSuchFolder);
+		}
+	}
+
+	// Create folder
+	const folder = await DriveFolders.insert({
+		id: genId(),
+		createdAt: new Date(),
+		name: ps.name,
+		parentId: parent !== null ? parent.id : null,
+		userId: user.id
+	}).then(x => DriveFolders.findOneOrFail(x.identifiers[0]));
+
+	const folderObj = await DriveFolders.pack(folder);
+
+	// Publish folderCreated event
+	publishDriveStream(user.id, 'folderCreated', folderObj);
+
+	return folderObj;
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts
new file mode 100644
index 0000000000..38b4aef103
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { publishDriveStream } from '@/services/stream';
+import { ApiError } from '../../../error';
+import { DriveFolders, DriveFiles } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'write:drive',
+
+	params: {
+		folderId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchFolder: {
+			message: 'No such folder.',
+			code: 'NO_SUCH_FOLDER',
+			id: '1069098f-c281-440f-b085-f9932edbe091'
+		},
+
+		hasChildFilesOrFolders: {
+			message: 'This folder has child files or folders.',
+			code: 'HAS_CHILD_FILES_OR_FOLDERS',
+			id: 'b0fc8a17-963c-405d-bfbc-859a487295e1'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Get folder
+	const folder = await DriveFolders.findOne({
+		id: ps.folderId,
+		userId: user.id
+	});
+
+	if (folder == null) {
+		throw new ApiError(meta.errors.noSuchFolder);
+	}
+
+	const [childFoldersCount, childFilesCount] = await Promise.all([
+		DriveFolders.count({ parentId: folder.id }),
+		DriveFiles.count({ folderId: folder.id })
+	]);
+
+	if (childFoldersCount !== 0 || childFilesCount !== 0) {
+		throw new ApiError(meta.errors.hasChildFilesOrFolders);
+	}
+
+	await DriveFolders.delete(folder.id);
+
+	// Publish folderCreated event
+	publishDriveStream(user.id, 'folderDeleted', folder.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts
new file mode 100644
index 0000000000..a6c5a49988
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/folders/find.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { DriveFolders } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		name: {
+			validator: $.str
+		},
+
+		parentId: {
+			validator: $.optional.nullable.type(ID),
+			default: null,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'DriveFolder',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const folders = await DriveFolders.find({
+		name: ps.name,
+		userId: user.id,
+		parentId: ps.parentId
+	});
+
+	return await Promise.all(folders.map(folder => DriveFolders.pack(folder)));
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts
new file mode 100644
index 0000000000..e907a24f05
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/folders/show.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { DriveFolders } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		folderId: {
+			validator: $.type(ID),
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'DriveFolder',
+	},
+
+	errors: {
+		noSuchFolder: {
+			message: 'No such folder.',
+			code: 'NO_SUCH_FOLDER',
+			id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Get folder
+	const folder = await DriveFolders.findOne({
+		id: ps.folderId,
+		userId: user.id
+	});
+
+	if (folder == null) {
+		throw new ApiError(meta.errors.noSuchFolder);
+	}
+
+	return await DriveFolders.pack(folder, {
+		detail: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
new file mode 100644
index 0000000000..612252e6df
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -0,0 +1,123 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { publishDriveStream } from '@/services/stream';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { DriveFolders } from '@/models/index';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'write:drive',
+
+	params: {
+		folderId: {
+			validator: $.type(ID),
+		},
+
+		name: {
+			validator: $.optional.str.pipe(DriveFolders.validateFolderName),
+		},
+
+		parentId: {
+			validator: $.optional.nullable.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchFolder: {
+			message: 'No such folder.',
+			code: 'NO_SUCH_FOLDER',
+			id: 'f7974dac-2c0d-4a27-926e-23583b28e98e'
+		},
+
+		noSuchParentFolder: {
+			message: 'No such parent folder.',
+			code: 'NO_SUCH_PARENT_FOLDER',
+			id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1'
+		},
+
+		recursiveNesting: {
+			message: 'It can not be structured like nesting folders recursively.',
+			code: 'NO_SUCH_PARENT_FOLDER',
+			id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'DriveFolder'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch folder
+	const folder = await DriveFolders.findOne({
+		id: ps.folderId,
+		userId: user.id
+	});
+
+	if (folder == null) {
+		throw new ApiError(meta.errors.noSuchFolder);
+	}
+
+	if (ps.name) folder.name = ps.name;
+
+	if (ps.parentId !== undefined) {
+		if (ps.parentId === folder.id) {
+			throw new ApiError(meta.errors.recursiveNesting);
+		} else if (ps.parentId === null) {
+			folder.parentId = null;
+		} else {
+			// Get parent folder
+			const parent = await DriveFolders.findOne({
+				id: ps.parentId,
+				userId: user.id
+			});
+
+			if (parent == null) {
+				throw new ApiError(meta.errors.noSuchParentFolder);
+			}
+
+			// Check if the circular reference will occur
+			async function checkCircle(folderId: any): Promise<boolean> {
+				// Fetch folder
+				const folder2 = await DriveFolders.findOne({
+					id: folderId
+				});
+
+				if (folder2!.id === folder!.id) {
+					return true;
+				} else if (folder2!.parentId) {
+					return await checkCircle(folder2!.parentId);
+				} else {
+					return false;
+				}
+			}
+
+			if (parent.parentId !== null) {
+				if (await checkCircle(parent.parentId)) {
+					throw new ApiError(meta.errors.recursiveNesting);
+				}
+			}
+
+			folder.parentId = parent.id;
+		}
+	}
+
+	// Update
+	DriveFolders.update(folder.id, {
+		name: folder.name,
+		parentId: folder.parentId
+	});
+
+	const folderObj = await DriveFolders.pack(folder);
+
+	// Publish folderUpdated event
+	publishDriveStream(user.id, 'folderUpdated', folderObj);
+
+	return folderObj;
+});
diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts
new file mode 100644
index 0000000000..141e02f748
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/drive/stream.ts
@@ -0,0 +1,59 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { DriveFiles } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['drive'],
+
+	requireCredential: true as const,
+
+	kind: 'read:drive',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		type: {
+			validator: $.optional.str.match(/^[a-zA-Z\/\-*]+$/)
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'DriveFile',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
+		.andWhere('file.userId = :userId', { userId: user.id });
+
+	if (ps.type) {
+		if (ps.type.endsWith('/*')) {
+			query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
+		} else {
+			query.andWhere('file.type = :type', { type: ps.type });
+		}
+	}
+
+	const files = await query.take(ps.limit!).getMany();
+
+	return await DriveFiles.packMany(files, { detail: false, self: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts
new file mode 100644
index 0000000000..f6fccd59b0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/email-address/available.ts
@@ -0,0 +1,34 @@
+import $ from 'cafy';
+import define from '../../define';
+import { validateEmailForAccount } from '@/services/validate-email-for-account';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		emailAddress: {
+			validator: $.str
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			available: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const,
+			},
+			reason: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+			},
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	return await validateEmailForAccount(ps.emailAddress);
+});
diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts
new file mode 100644
index 0000000000..1a04d8bee8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/endpoint.ts
@@ -0,0 +1,26 @@
+import $ from 'cafy';
+import define from '../define';
+import endpoints from '../endpoints';
+
+export const meta = {
+	requireCredential: false as const,
+
+	tags: ['meta'],
+
+	params: {
+		endpoint: {
+			validator: $.str,
+		}
+	},
+};
+
+export default define(meta, async (ps) => {
+	const ep = endpoints.find(x => x.name === ps.endpoint);
+	if (ep == null) return null;
+	return {
+		params: Object.entries(ep.meta.params || {}).map(([k, v]) => ({
+			name: k,
+			type: v.validator.name === 'ID' ? 'String' : v.validator.name
+		}))
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts
new file mode 100644
index 0000000000..f7b9757d8d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/endpoints.ts
@@ -0,0 +1,30 @@
+import define from '../define';
+import endpoints from '../endpoints';
+
+export const meta = {
+	requireCredential: false as const,
+
+	tags: ['meta'],
+
+	params: {
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const
+		},
+		example: [
+			'admin/abuse-user-reports',
+			'admin/accounts/create',
+			'admin/announcements/create',
+			'...'
+		]
+	}
+};
+
+export default define(meta, async () => {
+	return endpoints.map(x => x.name);
+});
diff --git a/packages/backend/src/server/api/endpoints/federation/dns.ts b/packages/backend/src/server/api/endpoints/federation/dns.ts
new file mode 100644
index 0000000000..7ba566301a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/federation/dns.ts
@@ -0,0 +1,43 @@
+import { promises as dns } from 'dns';
+import $ from 'cafy';
+import define from '../../define';
+import { Instances } from '@/models/index';
+import { toPuny } from '@/misc/convert-host';
+
+const resolver = new dns.Resolver();
+resolver.setServers(['1.1.1.1']);
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: false as const,
+
+	params: {
+		host: {
+			validator: $.str
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const instance = await Instances.findOneOrFail({ host: toPuny(ps.host) });
+
+	const [
+		resolved4,
+		resolved6,
+		resolvedCname,
+		resolvedTxt,
+	] = await Promise.all([
+		resolver.resolve4(instance.host).catch(() => []),
+		resolver.resolve6(instance.host).catch(() => []),
+		resolver.resolveCname(instance.host).catch(() => []),
+		resolver.resolveTxt(instance.host).catch(() => []),
+	]);
+
+	return {
+		a: resolved4,
+		aaaa: resolved6,
+		cname: resolvedCname,
+		txt: resolvedTxt,
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts
new file mode 100644
index 0000000000..655e7b7b9a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/federation/followers.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Followings } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: false as const,
+
+	params: {
+		host: {
+			validator: $.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Following',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+		.andWhere(`following.followeeHost = :host`, { host: ps.host });
+
+	const followings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Followings.packMany(followings, me, { populateFollowee: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts
new file mode 100644
index 0000000000..5b283581a6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/federation/following.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Followings } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: false as const,
+
+	params: {
+		host: {
+			validator: $.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Following',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+		.andWhere(`following.followerHost = :host`, { host: ps.host });
+
+	const followings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Followings.packMany(followings, me, { populateFollowee: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
new file mode 100644
index 0000000000..cf5e44ebd5
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -0,0 +1,149 @@
+import $ from 'cafy';
+import config from '@/config/index';
+import define from '../../define';
+import { Instances } from '@/models/index';
+import { fetchMeta } from '@/misc/fetch-meta';
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: false as const,
+
+	params: {
+		host: {
+			validator: $.optional.nullable.str,
+		},
+
+		blocked: {
+			validator: $.optional.nullable.bool,
+		},
+
+		notResponding: {
+			validator: $.optional.nullable.bool,
+		},
+
+		suspended: {
+			validator: $.optional.nullable.bool,
+		},
+
+		federating: {
+			validator: $.optional.nullable.bool,
+		},
+
+		subscribing: {
+			validator: $.optional.nullable.bool,
+		},
+
+		publishing: {
+			validator: $.optional.nullable.bool,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 30
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		},
+
+		sort: {
+			validator: $.optional.str,
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'FederationInstance'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const query = Instances.createQueryBuilder('instance');
+
+	switch (ps.sort) {
+		case '+pubSub': query.orderBy('instance.followingCount', 'DESC').orderBy('instance.followersCount', 'DESC'); break;
+		case '-pubSub': query.orderBy('instance.followingCount', 'ASC').orderBy('instance.followersCount', 'ASC'); break;
+		case '+notes': query.orderBy('instance.notesCount', 'DESC'); break;
+		case '-notes': query.orderBy('instance.notesCount', 'ASC'); break;
+		case '+users': query.orderBy('instance.usersCount', 'DESC'); break;
+		case '-users': query.orderBy('instance.usersCount', 'ASC'); break;
+		case '+following': query.orderBy('instance.followingCount', 'DESC'); break;
+		case '-following': query.orderBy('instance.followingCount', 'ASC'); break;
+		case '+followers': query.orderBy('instance.followersCount', 'DESC'); break;
+		case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
+		case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
+		case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
+		case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break;
+		case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break;
+		case '+driveUsage': query.orderBy('instance.driveUsage', 'DESC'); break;
+		case '-driveUsage': query.orderBy('instance.driveUsage', 'ASC'); break;
+		case '+driveFiles': query.orderBy('instance.driveFiles', 'DESC'); break;
+		case '-driveFiles': query.orderBy('instance.driveFiles', 'ASC'); break;
+
+		default: query.orderBy('instance.id', 'DESC'); break;
+	}
+
+	if (typeof ps.blocked === 'boolean') {
+		const meta = await fetchMeta(true);
+		if (ps.blocked) {
+			query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts });
+		} else {
+			query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts });
+		}
+	}
+
+	if (typeof ps.notResponding === 'boolean') {
+		if (ps.notResponding) {
+			query.andWhere('instance.isNotResponding = TRUE');
+		} else {
+			query.andWhere('instance.isNotResponding = FALSE');
+		}
+	}
+
+	if (typeof ps.suspended === 'boolean') {
+		if (ps.suspended) {
+			query.andWhere('instance.isSuspended = TRUE');
+		} else {
+			query.andWhere('instance.isSuspended = FALSE');
+		}
+	}
+
+	if (typeof ps.federating === 'boolean') {
+		if (ps.federating) {
+			query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))');
+		} else {
+			query.andWhere('((instance.followingCount = 0) AND (instance.followersCount = 0))');
+		}
+	}
+
+	if (typeof ps.subscribing === 'boolean') {
+		if (ps.subscribing) {
+			query.andWhere('instance.followersCount > 0');
+		} else {
+			query.andWhere('instance.followersCount = 0');
+		}
+	}
+
+	if (typeof ps.publishing === 'boolean') {
+		if (ps.publishing) {
+			query.andWhere('instance.followingCount > 0');
+		} else {
+			query.andWhere('instance.followingCount = 0');
+		}
+	}
+
+	if (ps.host) {
+		query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' });
+	}
+
+	const instances = await query.take(ps.limit!).skip(ps.offset).getMany();
+
+	return instances;
+});
diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts
new file mode 100644
index 0000000000..f8352aefb3
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts
@@ -0,0 +1,29 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Instances } from '@/models/index';
+import { toPuny } from '@/misc/convert-host';
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: false as const,
+
+	params: {
+		host: {
+			validator: $.str
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'FederationInstance'
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const instance = await Instances
+		.findOne({ host: toPuny(ps.host) });
+
+	return instance;
+});
diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
new file mode 100644
index 0000000000..580c3cb3d9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
@@ -0,0 +1,22 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { getRemoteUser } from '../../common/getters';
+import { updatePerson } from '@/remote/activitypub/models/person';
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: true as const,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps) => {
+	const user = await getRemoteUser(ps.userId);
+	await updatePerson(user.uri!);
+});
diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts
new file mode 100644
index 0000000000..0e35df3e1c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/federation/users.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Users } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['federation'],
+
+	requireCredential: false as const,
+
+	params: {
+		host: {
+			validator: $.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Users.createQueryBuilder('user'), ps.sinceId, ps.untilId)
+		.andWhere(`user.host = :host`, { host: ps.host });
+
+	const users = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Users.packMany(users, me, { detail: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts
new file mode 100644
index 0000000000..ba9ca1092d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/create.ts
@@ -0,0 +1,100 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import * as ms from 'ms';
+import create from '@/services/following/create';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { Followings, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['following', 'users'],
+
+	limit: {
+		duration: ms('1hour'),
+		max: 100
+	},
+
+	requireCredential: true as const,
+
+	kind: 'write:following',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'
+		},
+
+		followeeIsYourself: {
+			message: 'Followee is yourself.',
+			code: 'FOLLOWEE_IS_YOURSELF',
+			id: '26fbe7bb-a331-4857-af17-205b426669a9'
+		},
+
+		alreadyFollowing: {
+			message: 'You are already following that user.',
+			code: 'ALREADY_FOLLOWING',
+			id: '35387507-38c7-4cb9-9197-300b93783fa0'
+		},
+
+		blocking: {
+			message: 'You are blocking that user.',
+			code: 'BLOCKING',
+			id: '4e2206ec-aa4f-4960-b865-6c23ac38e2d9'
+		},
+
+		blocked: {
+			message: 'You are blocked by that user.',
+			code: 'BLOCKED',
+			id: 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const follower = user;
+
+	// 自分自身
+	if (user.id === ps.userId) {
+		throw new ApiError(meta.errors.followeeIsYourself);
+	}
+
+	// Get followee
+	const followee = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Check if already following
+	const exist = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
+	});
+
+	if (exist != null) {
+		throw new ApiError(meta.errors.alreadyFollowing);
+	}
+
+	try {
+		await create(follower, followee);
+	} catch (e) {
+		if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);
+		if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked);
+		throw e;
+	}
+
+	return await Users.pack(followee.id, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts
new file mode 100644
index 0000000000..0b0158b86e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/delete.ts
@@ -0,0 +1,82 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import * as ms from 'ms';
+import deleteFollowing from '@/services/following/delete';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { Followings, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['following', 'users'],
+
+	limit: {
+		duration: ms('1hour'),
+		max: 100
+	},
+
+	requireCredential: true as const,
+
+	kind: 'write:following',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8'
+		},
+
+		followeeIsYourself: {
+			message: 'Followee is yourself.',
+			code: 'FOLLOWEE_IS_YOURSELF',
+			id: 'd9e400b9-36b0-4808-b1d8-79e707f1296c'
+		},
+
+		notFollowing: {
+			message: 'You are not following that user.',
+			code: 'NOT_FOLLOWING',
+			id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const follower = user;
+
+	// Check if the followee is yourself
+	if (user.id === ps.userId) {
+		throw new ApiError(meta.errors.followeeIsYourself);
+	}
+
+	// Get followee
+	const followee = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Check not following
+	const exist = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
+	});
+
+	if (exist == null) {
+		throw new ApiError(meta.errors.notFollowing);
+	}
+
+	await deleteFollowing(follower, followee);
+
+	return await Users.pack(followee.id, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts
new file mode 100644
index 0000000000..af39ea1d90
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/requests/accept.ts
@@ -0,0 +1,48 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import acceptFollowRequest from '@/services/following/requests/accept';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+
+export const meta = {
+	tags: ['following', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:following',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '66ce1645-d66c-46bb-8b79-96739af885bd'
+		},
+		noFollowRequest: {
+			message: 'No follow request.',
+			code: 'NO_FOLLOW_REQUEST',
+			id: 'bcde4f8b-0913-4614-8881-614e522fb041'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch follower
+	const follower = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	await acceptFollowRequest(user, follower).catch(e => {
+		if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest);
+		throw e;
+	});
+
+	return;
+});
diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts
new file mode 100644
index 0000000000..b69c9d2fe1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts
@@ -0,0 +1,58 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import cancelFollowRequest from '@/services/following/requests/cancel';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['following', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:following',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '4e68c551-fc4c-4e46-bb41-7d4a37bf9dab'
+		},
+
+		followRequestNotFound: {
+			message: 'Follow request not found.',
+			code: 'FOLLOW_REQUEST_NOT_FOUND',
+			id: '089b125b-d338-482a-9a09-e2622ac9f8d4'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch followee
+	const followee = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	try {
+		await cancelFollowRequest(followee, user);
+	} catch (e) {
+		if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound);
+		throw e;
+	}
+
+	return await Users.pack(followee.id, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts
new file mode 100644
index 0000000000..84440ccac7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts
@@ -0,0 +1,44 @@
+import define from '../../../define';
+import { FollowRequests } from '@/models/index';
+
+export const meta = {
+	tags: ['following', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:following',
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				follower: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				},
+				followee: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const reqs = await FollowRequests.find({
+		followeeId: user.id
+	});
+
+	return await Promise.all(reqs.map(req => FollowRequests.pack(req)));
+});
diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts
new file mode 100644
index 0000000000..620324361f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import rejectFollowRequest from '@/services/following/requests/reject';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+
+export const meta = {
+	tags: ['following', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:following',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch follower
+	const follower = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	await rejectFollowRequest(user, follower);
+
+	return;
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts
new file mode 100644
index 0000000000..30ef8cedec
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts
@@ -0,0 +1,29 @@
+import define from '../../define';
+import { GalleryPosts } from '@/models/index';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: false as const,
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'GalleryPost',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = GalleryPosts.createQueryBuilder('post')
+		.andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) })
+		.andWhere('post.likedCount > 0')
+		.orderBy('post.likedCount', 'DESC');
+
+	const posts = await query.take(10).getMany();
+
+	return await GalleryPosts.packMany(posts, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts
new file mode 100644
index 0000000000..18449b9654
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts
@@ -0,0 +1,28 @@
+import define from '../../define';
+import { GalleryPosts } from '@/models/index';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: false as const,
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'GalleryPost',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = GalleryPosts.createQueryBuilder('post')
+		.andWhere('post.likedCount > 0')
+		.orderBy('post.likedCount', 'DESC');
+
+	const posts = await query.take(10).getMany();
+
+	return await GalleryPosts.packMany(posts, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts
new file mode 100644
index 0000000000..53d3236d2d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { GalleryPosts } from '@/models/index';
+
+export const meta = {
+	tags: ['gallery'],
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'GalleryPost',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
+		.innerJoinAndSelect('post.user', 'user');
+
+	const posts = await query.take(ps.limit!).getMany();
+
+	return await GalleryPosts.packMany(posts, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
new file mode 100644
index 0000000000..38b487e6ea
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
@@ -0,0 +1,77 @@
+import $ from 'cafy';
+import * as ms from 'ms';
+import define from '../../../define';
+import { ID } from '../../../../../misc/cafy-id';
+import { DriveFiles, GalleryPosts } from '@/models/index';
+import { genId } from '../../../../../misc/gen-id';
+import { GalleryPost } from '@/models/entities/gallery-post';
+import { ApiError } from '../../../error';
+import { DriveFile } from '@/models/entities/drive-file';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'write:gallery',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300
+	},
+
+	params: {
+		title: {
+			validator: $.str.min(1),
+		},
+
+		description: {
+			validator: $.optional.nullable.str,
+		},
+
+		fileIds: {
+			validator: $.arr($.type(ID)).unique().range(1, 32),
+		},
+
+		isSensitive: {
+			validator: $.optional.bool,
+			default: false,
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'GalleryPost',
+	},
+
+	errors: {
+
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const files = (await Promise.all(ps.fileIds.map(fileId =>
+		DriveFiles.findOne({
+			id: fileId,
+			userId: user.id
+		})
+	))).filter((file): file is DriveFile => file != null);
+
+	if (files.length === 0) {
+		throw new Error();
+	}
+
+	const post = await GalleryPosts.insert(new GalleryPost({
+		id: genId(),
+		createdAt: new Date(),
+		updatedAt: new Date(),
+		title: ps.title,
+		description: ps.description,
+		userId: user.id,
+		isSensitive: ps.isSensitive,
+		fileIds: files.map(file => file.id)
+	})).then(x => GalleryPosts.findOneOrFail(x.identifiers[0]));
+
+	return await GalleryPosts.pack(post, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
new file mode 100644
index 0000000000..e5b7c07f2f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { GalleryPosts } from '@/models/index';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'write:gallery',
+
+	params: {
+		postId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchPost: {
+			message: 'No such post.',
+			code: 'NO_SUCH_POST',
+			id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const post = await GalleryPosts.findOne({
+		id: ps.postId,
+		userId: user.id,
+	});
+
+	if (post == null) {
+		throw new ApiError(meta.errors.noSuchPost);
+	}
+
+	await GalleryPosts.delete(post.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
new file mode 100644
index 0000000000..81a25c0ad1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
@@ -0,0 +1,71 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { GalleryPosts, GalleryLikes } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'write:gallery-likes',
+
+	params: {
+		postId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchPost: {
+			message: 'No such post.',
+			code: 'NO_SUCH_POST',
+			id: '56c06af3-1287-442f-9701-c93f7c4a62ff'
+		},
+
+		yourPost: {
+			message: 'You cannot like your post.',
+			code: 'YOUR_POST',
+			id: 'f78f1511-5ebc-4478-a888-1198d752da68'
+		},
+
+		alreadyLiked: {
+			message: 'The post has already been liked.',
+			code: 'ALREADY_LIKED',
+			id: '40e9ed56-a59c-473a-bf3f-f289c54fb5a7'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const post = await GalleryPosts.findOne(ps.postId);
+	if (post == null) {
+		throw new ApiError(meta.errors.noSuchPost);
+	}
+
+	if (post.userId === user.id) {
+		throw new ApiError(meta.errors.yourPost);
+	}
+
+	// if already liked
+	const exist = await GalleryLikes.findOne({
+		postId: post.id,
+		userId: user.id
+	});
+
+	if (exist != null) {
+		throw new ApiError(meta.errors.alreadyLiked);
+	}
+
+	// Create like
+	await GalleryLikes.insert({
+		id: genId(),
+		createdAt: new Date(),
+		postId: post.id,
+		userId: user.id
+	});
+
+	GalleryPosts.increment({ id: post.id }, 'likedCount', 1);
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts
new file mode 100644
index 0000000000..93852a5f8d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { GalleryPosts } from '@/models/index';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: false as const,
+
+	params: {
+		postId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchPost: {
+			message: 'No such post.',
+			code: 'NO_SUCH_POST',
+			id: '1137bf14-c5b0-4604-85bb-5b5371b1cd45'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'GalleryPost'
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const post = await GalleryPosts.findOne({
+		id: ps.postId,
+	});
+
+	if (post == null) {
+		throw new ApiError(meta.errors.noSuchPost);
+	}
+
+	return await GalleryPosts.pack(post, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
new file mode 100644
index 0000000000..0347cdf79e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
@@ -0,0 +1,54 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { GalleryPosts, GalleryLikes } from '@/models/index';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'write:gallery-likes',
+
+	params: {
+		postId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchPost: {
+			message: 'No such post.',
+			code: 'NO_SUCH_POST',
+			id: 'c32e6dd0-b555-4413-925e-b3757d19ed84'
+		},
+
+		notLiked: {
+			message: 'You have not liked that post.',
+			code: 'NOT_LIKED',
+			id: 'e3e8e06e-be37-41f7-a5b4-87a8250288f0'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const post = await GalleryPosts.findOne(ps.postId);
+	if (post == null) {
+		throw new ApiError(meta.errors.noSuchPost);
+	}
+
+	const exist = await GalleryLikes.findOne({
+		postId: post.id,
+		userId: user.id
+	});
+
+	if (exist == null) {
+		throw new ApiError(meta.errors.notLiked);
+	}
+
+	// Delete like
+	await GalleryLikes.delete(exist.id);
+
+	GalleryPosts.decrement({ id: post.id }, 'likedCount', 1);
+});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
new file mode 100644
index 0000000000..54eea130d3
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
@@ -0,0 +1,82 @@
+import $ from 'cafy';
+import * as ms from 'ms';
+import define from '../../../define';
+import { ID } from '../../../../../misc/cafy-id';
+import { DriveFiles, GalleryPosts } from '@/models/index';
+import { GalleryPost } from '@/models/entities/gallery-post';
+import { ApiError } from '../../../error';
+import { DriveFile } from '@/models/entities/drive-file';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'write:gallery',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300
+	},
+
+	params: {
+		postId: {
+			validator: $.type(ID),
+		},
+
+		title: {
+			validator: $.str.min(1),
+		},
+
+		description: {
+			validator: $.optional.nullable.str,
+		},
+
+		fileIds: {
+			validator: $.arr($.type(ID)).unique().range(1, 32),
+		},
+
+		isSensitive: {
+			validator: $.optional.bool,
+			default: false,
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'GalleryPost',
+	},
+
+	errors: {
+
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const files = (await Promise.all(ps.fileIds.map(fileId =>
+		DriveFiles.findOne({
+			id: fileId,
+			userId: user.id
+		})
+	))).filter((file): file is DriveFile => file != null);
+
+	if (files.length === 0) {
+		throw new Error();
+	}
+
+	await GalleryPosts.update({
+		id: ps.postId,
+		userId: user.id,
+	}, {
+		updatedAt: new Date(),
+		title: ps.title,
+		description: ps.description,
+		isSensitive: ps.isSensitive,
+		fileIds: files.map(file => file.id)
+	});
+
+	const post = await GalleryPosts.findOneOrFail(ps.postId);
+
+	return await GalleryPosts.pack(post, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/games.ts b/packages/backend/src/server/api/endpoints/games/reversi/games.ts
new file mode 100644
index 0000000000..4db9ecb69f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/games/reversi/games.ts
@@ -0,0 +1,156 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ReversiGames } from '@/models/index';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+import { Brackets } from 'typeorm';
+
+export const meta = {
+	tags: ['games'],
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		my: {
+			validator: $.optional.bool,
+			default: false
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				createdAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'date-time'
+				},
+				startedAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'date-time'
+				},
+				isStarted: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const
+				},
+				isEnded: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const
+				},
+				form1: {
+					type: 'any' as const,
+					optional: false as const, nullable: true as const
+				},
+				form2: {
+					type: 'any' as const,
+					optional: false as const, nullable: true as const
+				},
+				user1Accepted: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const,
+					default: false
+				},
+				user2Accepted: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const,
+					default: false
+				},
+				user1Id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				user2Id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				user1: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				},
+				user2: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				},
+				winnerId: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+					format: 'id'
+				},
+				winner: {
+					type: 'object' as const,
+					optional: false as const, nullable: true as const,
+					ref: 'User'
+				},
+				surrendered: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+					format: 'id'
+				},
+				black: {
+					type: 'number' as const,
+					optional: false as const, nullable: true as const
+				},
+				bw: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				},
+				isLlotheo: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const,
+				},
+				canPutEverywhere: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const
+				},
+				loopedBoard: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId)
+		.andWhere('game.isStarted = TRUE');
+
+	if (ps.my && user) {
+		query.andWhere(new Brackets(qb => { qb
+			.where('game.user1Id = :userId', { userId: user.id })
+			.orWhere('game.user2Id = :userId', { userId: user.id });
+		}));
+	}
+
+	// Fetch games
+	const games = await query.take(ps.limit!).getMany();
+
+	return await Promise.all(games.map((g) => ReversiGames.pack(g, user, {
+		detail: false
+	})));
+});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/games/show.ts b/packages/backend/src/server/api/endpoints/games/reversi/games/show.ts
new file mode 100644
index 0000000000..93afffdb1f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/games/reversi/games/show.ts
@@ -0,0 +1,168 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import Reversi from '../../../../../../games/reversi/core';
+import define from '../../../../define';
+import { ApiError } from '../../../../error';
+import { ReversiGames } from '@/models/index';
+
+export const meta = {
+	tags: ['games'],
+
+	params: {
+		gameId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchGame: {
+			message: 'No such game.',
+			code: 'NO_SUCH_GAME',
+			id: 'f13a03db-fae1-46c9-87f3-43c8165419e1'
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				createdAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'date-time'
+				},
+				startedAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'date-time'
+				},
+				isStarted: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const
+				},
+				isEnded: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const
+				},
+				form1: {
+					type: 'any' as const,
+					optional: false as const, nullable: true as const
+				},
+				form2: {
+					type: 'any' as const,
+					optional: false as const, nullable: true as const
+				},
+				user1Accepted: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const,
+					default: false
+				},
+				user2Accepted: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const,
+					default: false
+				},
+				user1Id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				user2Id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				user1: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				},
+				user2: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				},
+				winnerId: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+					format: 'id'
+				},
+				winner: {
+					type: 'object' as const,
+					optional: false as const, nullable: true as const,
+					ref: 'User'
+				},
+				surrendered: {
+					type: 'string' as const,
+					optional: false as const, nullable: true as const,
+					format: 'id'
+				},
+				black: {
+					type: 'number' as const,
+					optional: false as const, nullable: true as const
+				},
+				bw: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				},
+				isLlotheo: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const,
+				},
+				canPutEverywhere: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const
+				},
+				loopedBoard: {
+					type: 'boolean' as const,
+					optional: false as const, nullable: false as const
+				},
+				board: {
+					type: 'array' as const,
+					optional: false as const, nullable: false as const,
+					items: {
+						type: 'any' as const,
+						optional: false as const, nullable: false as const
+					}
+				},
+				turn: {
+					type: 'any' as const,
+					optional: false as const, nullable: false as const
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const game = await ReversiGames.findOne(ps.gameId);
+
+	if (game == null) {
+		throw new ApiError(meta.errors.noSuchGame);
+	}
+
+	const o = new Reversi(game.map, {
+		isLlotheo: game.isLlotheo,
+		canPutEverywhere: game.canPutEverywhere,
+		loopedBoard: game.loopedBoard
+	});
+
+	for (const log of game.logs) {
+		o.put(log.color, log.pos);
+	}
+
+	const packed = await ReversiGames.pack(game, user);
+
+	return Object.assign({
+		board: o.board,
+		turn: o.turn
+	}, packed);
+});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/games/surrender.ts b/packages/backend/src/server/api/endpoints/games/reversi/games/surrender.ts
new file mode 100644
index 0000000000..00d58b19e3
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/games/reversi/games/surrender.ts
@@ -0,0 +1,67 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { publishReversiGameStream } from '@/services/stream';
+import define from '../../../../define';
+import { ApiError } from '../../../../error';
+import { ReversiGames } from '@/models/index';
+
+export const meta = {
+	tags: ['games'],
+
+	requireCredential: true as const,
+
+	params: {
+		gameId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchGame: {
+			message: 'No such game.',
+			code: 'NO_SUCH_GAME',
+			id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df'
+		},
+
+		alreadyEnded: {
+			message: 'That game has already ended.',
+			code: 'ALREADY_ENDED',
+			id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d'
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '6e04164b-a992-4c93-8489-2123069973e1'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const game = await ReversiGames.findOne(ps.gameId);
+
+	if (game == null) {
+		throw new ApiError(meta.errors.noSuchGame);
+	}
+
+	if (game.isEnded) {
+		throw new ApiError(meta.errors.alreadyEnded);
+	}
+
+	if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) {
+		throw new ApiError(meta.errors.accessDenied);
+	}
+
+	const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
+
+	await ReversiGames.update(game.id, {
+		surrendered: user.id,
+		isEnded: true,
+		winnerId: winnerId
+	});
+
+	publishReversiGameStream(game.id, 'ended', {
+		winnerId: winnerId,
+		game: await ReversiGames.pack(game.id, user)
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/invitations.ts b/packages/backend/src/server/api/endpoints/games/reversi/invitations.ts
new file mode 100644
index 0000000000..c8629377b2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/games/reversi/invitations.ts
@@ -0,0 +1,58 @@
+import define from '../../../define';
+import { ReversiMatchings } from '@/models/index';
+
+export const meta = {
+	tags: ['games'],
+
+	requireCredential: true as const,
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				createdAt: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'date-time'
+				},
+				parentId: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				parent: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				},
+				childId: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				child: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'User'
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Find session
+	const invitations = await ReversiMatchings.find({
+		childId: user.id
+	});
+
+	return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user)));
+});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/match.ts b/packages/backend/src/server/api/endpoints/games/reversi/match.ts
new file mode 100644
index 0000000000..5ceb16c7d7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/games/reversi/match.ts
@@ -0,0 +1,108 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { publishMainStream, publishReversiStream } from '@/services/stream';
+import { eighteight } from '../../../../../games/reversi/maps';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+import { genId } from '@/misc/gen-id';
+import { ReversiMatchings, ReversiGames } from '@/models/index';
+import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { ReversiMatching } from '@/models/entities/games/reversi/matching';
+
+export const meta = {
+	tags: ['games'],
+
+	requireCredential: true as const,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '0b4f0559-b484-4e31-9581-3f73cee89b28'
+		},
+
+		isYourself: {
+			message: 'Target user is yourself.',
+			code: 'TARGET_IS_YOURSELF',
+			id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Myself
+	if (ps.userId === user.id) {
+		throw new ApiError(meta.errors.isYourself);
+	}
+
+	// Find session
+	const exist = await ReversiMatchings.findOne({
+		parentId: ps.userId,
+		childId: user.id
+	});
+
+	if (exist) {
+		// Destroy session
+		ReversiMatchings.delete(exist.id);
+
+		// Create game
+		const game = await ReversiGames.save({
+			id: genId(),
+			createdAt: new Date(),
+			user1Id: exist.parentId,
+			user2Id: user.id,
+			user1Accepted: false,
+			user2Accepted: false,
+			isStarted: false,
+			isEnded: false,
+			logs: [],
+			map: eighteight.data,
+			bw: 'random',
+			isLlotheo: false
+		} as Partial<ReversiGame>);
+
+		publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId }));
+
+		const other = await ReversiMatchings.count({
+			childId: user.id
+		});
+
+		if (other == 0) {
+			publishMainStream(user.id, 'reversiNoInvites');
+		}
+
+		return await ReversiGames.pack(game, user);
+	} else {
+		// Fetch child
+		const child = await getUser(ps.userId).catch(e => {
+			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+			throw e;
+		});
+
+		// 以前のセッションはすべて削除しておく
+		await ReversiMatchings.delete({
+			parentId: user.id
+		});
+
+		// セッションを作成
+		const matching = await ReversiMatchings.save({
+			id: genId(),
+			createdAt: new Date(),
+			parentId: user.id,
+			childId: child.id
+		} as ReversiMatching);
+
+		const packed = await ReversiMatchings.pack(matching, child);
+		publishReversiStream(child.id, 'invited', packed);
+		publishMainStream(child.id, 'reversiInvited', packed);
+
+		return;
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/games/reversi/match/cancel.ts b/packages/backend/src/server/api/endpoints/games/reversi/match/cancel.ts
new file mode 100644
index 0000000000..e4a138bb87
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/games/reversi/match/cancel.ts
@@ -0,0 +1,14 @@
+import define from '../../../../define';
+import { ReversiMatchings } from '@/models/index';
+
+export const meta = {
+	tags: ['games'],
+
+	requireCredential: true as const
+};
+
+export default define(meta, async (ps, user) => {
+	await ReversiMatchings.delete({
+		parentId: user.id
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts
new file mode 100644
index 0000000000..5c80d588d3
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts
@@ -0,0 +1,23 @@
+import { USER_ONLINE_THRESHOLD } from '@/const';
+import { Users } from '@/models/index';
+import { MoreThan } from 'typeorm';
+import define from '../define';
+
+export const meta = {
+	tags: ['meta'],
+
+	requireCredential: false as const,
+
+	params: {
+	}
+};
+
+export default define(meta, async () => {
+	const count = await Users.count({
+		lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD))
+	});
+
+	return {
+		count
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts
new file mode 100644
index 0000000000..821016a50c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts
@@ -0,0 +1,95 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Hashtags } from '@/models/index';
+
+export const meta = {
+	tags: ['hashtags'],
+
+	requireCredential: false as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		attachedToUserOnly: {
+			validator: $.optional.bool,
+			default: false
+		},
+
+		attachedToLocalUserOnly: {
+			validator: $.optional.bool,
+			default: false
+		},
+
+		attachedToRemoteUserOnly: {
+			validator: $.optional.bool,
+			default: false
+		},
+
+		sort: {
+			validator: $.str.or([
+				'+mentionedUsers',
+				'-mentionedUsers',
+				'+mentionedLocalUsers',
+				'-mentionedLocalUsers',
+				'+mentionedRemoteUsers',
+				'-mentionedRemoteUsers',
+				'+attachedUsers',
+				'-attachedUsers',
+				'+attachedLocalUsers',
+				'-attachedLocalUsers',
+				'+attachedRemoteUsers',
+				'-attachedRemoteUsers',
+			]),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Hashtag',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = Hashtags.createQueryBuilder('tag');
+
+	if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0');
+	if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0');
+	if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0');
+
+	switch (ps.sort) {
+		case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break;
+		case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break;
+		case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break;
+		case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break;
+		case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break;
+		case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break;
+		case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break;
+		case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break;
+		case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break;
+		case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break;
+		case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break;
+		case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break;
+	}
+
+	query.select([
+		'tag.name',
+		'tag.mentionedUsersCount',
+		'tag.mentionedLocalUsersCount',
+		'tag.mentionedRemoteUsersCount',
+		'tag.attachedUsersCount',
+		'tag.attachedLocalUsersCount',
+		'tag.attachedRemoteUsersCount',
+	]);
+
+	const tags = await query.take(ps.limit!).getMany();
+
+	return Hashtags.packMany(tags);
+});
diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts
new file mode 100644
index 0000000000..fd0cac3983
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts
@@ -0,0 +1,46 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Hashtags } from '@/models/index';
+
+export const meta = {
+	tags: ['hashtags'],
+
+	requireCredential: false as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		query: {
+			validator: $.str,
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0,
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		}
+	},
+};
+
+export default define(meta, async (ps) => {
+	const hashtags = await Hashtags.createQueryBuilder('tag')
+		.where('tag.name like :q', { q: ps.query.toLowerCase() + '%' })
+		.orderBy('tag.count', 'DESC')
+		.groupBy('tag.id')
+		.take(ps.limit!)
+		.skip(ps.offset)
+		.getMany();
+
+	return hashtags.map(tag => tag.name);
+});
diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts
new file mode 100644
index 0000000000..f22edbfffd
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Hashtags } from '@/models/index';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+
+export const meta = {
+	tags: ['hashtags'],
+
+	requireCredential: false as const,
+
+	params: {
+		tag: {
+			validator: $.str,
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Hashtag',
+	},
+
+	errors: {
+		noSuchHashtag: {
+			message: 'No such hashtag.',
+			code: 'NO_SUCH_HASHTAG',
+			id: '110ee688-193e-4a3a-9ecf-c167b2e6981e'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const hashtag = await Hashtags.findOne({ name: normalizeForSearch(ps.tag) });
+	if (hashtag == null) {
+		throw new ApiError(meta.errors.noSuchHashtag);
+	}
+
+	return await Hashtags.pack(hashtag);
+});
diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
new file mode 100644
index 0000000000..3d67241ab6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
@@ -0,0 +1,146 @@
+import { Brackets } from 'typeorm';
+import define from '../../define';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Notes } from '@/models/index';
+import { Note } from '@/models/entities/note';
+import { safeForSql } from '@/misc/safe-for-sql';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+
+/*
+トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前～今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
+ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
+
+..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する
+*/
+
+const rangeA = 1000 * 60 * 60; // 60分
+//const rangeB = 1000 * 60 * 120; // 2時間
+//const coefficient = 1.25; // 「n倍」の部分
+//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
+
+const max = 5;
+
+export const meta = {
+	tags: ['hashtags'],
+
+	requireCredential: false as const,
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				tag: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+				},
+				chart: {
+					type: 'array' as const,
+					optional: false as const, nullable: false as const,
+					items: {
+						type: 'number' as const,
+						optional: false as const, nullable: false as const,
+					}
+				},
+				usersCount: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async () => {
+	const instance = await fetchMeta(true);
+	const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
+
+	const now = new Date(); // 5分単位で丸めた現在日時
+	now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0);
+
+	const tagNotes = await Notes.createQueryBuilder('note')
+		.where(`note.createdAt > :date`, { date: new Date(now.getTime() - rangeA) })
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.visibility = 'public'`)
+			.orWhere(`note.visibility = 'home'`);
+		}))
+		.andWhere(`note.tags != '{}'`)
+		.select(['note.tags', 'note.userId'])
+		.cache(60000) // 1 min
+		.getMany();
+
+	if (tagNotes.length === 0) {
+		return [];
+	}
+
+	const tags: {
+		name: string;
+		users: Note['userId'][];
+	}[] = [];
+
+	for (const note of tagNotes) {
+		for (const tag of note.tags) {
+			if (hiddenTags.includes(tag)) continue;
+
+			const x = tags.find(x => x.name === tag);
+			if (x) {
+				if (!x.users.includes(note.userId)) {
+					x.users.push(note.userId);
+				}
+			} else {
+				tags.push({
+					name: tag,
+					users: [note.userId]
+				});
+			}
+		}
+	}
+
+	// タグを人気順に並べ替え
+	const hots = tags
+		.sort((a, b) => b.users.length - a.users.length)
+		.map(tag => tag.name)
+		.slice(0, max);
+
+	//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
+	const countPromises: Promise<number[]>[] = [];
+
+	const range = 20;
+
+	// 10分
+	const interval = 1000 * 60 * 10;
+
+	for (let i = 0; i < range; i++) {
+		countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
+			.select('count(distinct note.userId)')
+			.where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`)
+			.andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) })
+			.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) })
+			.cache(60000) // 1 min
+			.getRawOne()
+			.then(x => parseInt(x.count, 10))
+		)));
+	}
+
+	const countsLog = await Promise.all(countPromises);
+	//#endregion
+
+	const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
+		.select('count(distinct note.userId)')
+		.where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`)
+		.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) })
+		.cache(60000 * 60) // 60 min
+		.getRawOne()
+		.then(x => parseInt(x.count, 10))
+	));
+
+	const stats = hots.map((tag, i) => ({
+		tag,
+		chart: countsLog.map(counts => counts[i]),
+		usersCount: totalCounts[i]
+	}));
+
+	return stats;
+});
diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts
new file mode 100644
index 0000000000..8c8cd1510b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts
@@ -0,0 +1,89 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Users } from '@/models/index';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+
+export const meta = {
+	requireCredential: false as const,
+
+	tags: ['hashtags', 'users'],
+
+	params: {
+		tag: {
+			validator: $.str,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sort: {
+			validator: $.str.or([
+				'+follower',
+				'-follower',
+				'+createdAt',
+				'-createdAt',
+				'+updatedAt',
+				'-updatedAt',
+			]),
+		},
+
+		state: {
+			validator: $.optional.str.or([
+				'all',
+				'alive'
+			]),
+			default: 'all'
+		},
+
+		origin: {
+			validator: $.optional.str.or([
+				'combined',
+				'local',
+				'remote',
+			]),
+			default: 'local'
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = Users.createQueryBuilder('user')
+		.where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) });
+
+	const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));
+
+	if (ps.state === 'alive') {
+		query.andWhere('user.updatedAt > :date', { date: recent });
+	}
+
+	if (ps.origin === 'local') {
+		query.andWhere('user.host IS NULL');
+	} else if (ps.origin === 'remote') {
+		query.andWhere('user.host IS NOT NULL');
+	}
+
+	switch (ps.sort) {
+		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+		case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break;
+		case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
+	}
+
+	const users = await query.take(ps.limit!).getMany();
+
+	return await Users.packMany(users, me, { detail: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts
new file mode 100644
index 0000000000..0568a962d8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i.ts
@@ -0,0 +1,26 @@
+import define from '../define';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	params: {},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User',
+	},
+};
+
+export default define(meta, async (ps, user, token) => {
+	const isSecure = token == null;
+
+	// ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す
+	return await Users.pack(user.id, user, {
+		detail: true,
+		includeSecrets: isSecure
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
new file mode 100644
index 0000000000..2bd2128cce
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -0,0 +1,41 @@
+import $ from 'cafy';
+import * as speakeasy from 'speakeasy';
+import define from '../../../define';
+import { UserProfiles } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		token: {
+			validator: $.str
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const token = ps.token.replace(/\s/g, '');
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	if (profile.twoFactorTempSecret == null) {
+		throw new Error('二段階認証の設定が開始されていません');
+	}
+
+	const verified = (speakeasy as any).totp.verify({
+		secret: profile.twoFactorTempSecret,
+		encoding: 'base32',
+		token: token
+	});
+
+	if (!verified) {
+		throw new Error('not verified');
+	}
+
+	await UserProfiles.update(user.id, {
+		twoFactorSecret: profile.twoFactorTempSecret,
+		twoFactorEnabled: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
new file mode 100644
index 0000000000..b4d3af235a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -0,0 +1,150 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import { promisify } from 'util';
+import * as cbor from 'cbor';
+import define from '../../../define';
+import {
+	UserProfiles,
+	UserSecurityKeys,
+	AttestationChallenges,
+	Users
+} from '@/models/index';
+import config from '@/config/index';
+import { procedures, hash } from '../../../2fa';
+import { publishMainStream } from '@/services/stream';
+
+const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		clientDataJSON: {
+			validator: $.str
+		},
+		attestationObject: {
+			validator: $.str
+		},
+		password: {
+			validator: $.str
+		},
+		challengeId: {
+			validator: $.str
+		},
+		name: {
+			validator: $.str
+		}
+	}
+};
+
+const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8'));
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(ps.password, profile.password!);
+
+	if (!same) {
+		throw new Error('incorrect password');
+	}
+
+	if (!profile.twoFactorEnabled) {
+		throw new Error('2fa not enabled');
+	}
+
+	const clientData = JSON.parse(ps.clientDataJSON);
+
+	if (clientData.type != 'webauthn.create') {
+		throw new Error('not a creation attestation');
+	}
+	if (clientData.origin != config.scheme + '://' + config.host) {
+		throw new Error('origin mismatch');
+	}
+
+	const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
+
+	const attestation = await cborDecodeFirst(ps.attestationObject);
+
+	const rpIdHash = attestation.authData.slice(0, 32);
+	if (!rpIdHashReal.equals(rpIdHash)) {
+		throw new Error('rpIdHash mismatch');
+	}
+
+	const flags = attestation.authData[32];
+
+	// tslint:disable-next-line:no-bitwise
+	if (!(flags & 1)) {
+		throw new Error('user not present');
+	}
+
+	const authData = Buffer.from(attestation.authData);
+	const credentialIdLength = authData.readUInt16BE(53);
+	const credentialId = authData.slice(55, 55 + credentialIdLength);
+	const publicKeyData = authData.slice(55 + credentialIdLength);
+	const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
+	if (publicKey.get(3) != -7) {
+		throw new Error('alg mismatch');
+	}
+
+	if (!(procedures as any)[attestation.fmt]) {
+		throw new Error('unsupported fmt');
+	}
+
+	const verificationData = (procedures as any)[attestation.fmt].verify({
+		attStmt: attestation.attStmt,
+		authenticatorData: authData,
+		clientDataHash: clientDataJSONHash,
+		credentialId,
+		publicKey,
+		rpIdHash
+	});
+	if (!verificationData.valid) throw new Error('signature invalid');
+
+	const attestationChallenge = await AttestationChallenges.findOne({
+		userId: user.id,
+		id: ps.challengeId,
+		registrationChallenge: true,
+		challenge: hash(clientData.challenge).toString('hex')
+	});
+
+	if (!attestationChallenge) {
+		throw new Error('non-existent challenge');
+	}
+
+	await AttestationChallenges.delete({
+		userId: user.id,
+		id: ps.challengeId
+	});
+
+	// Expired challenge (> 5min old)
+	if (
+		new Date().getTime() - attestationChallenge.createdAt.getTime() >=
+		5 * 60 * 1000
+	) {
+		throw new Error('expired challenge');
+	}
+
+	const credentialIdString = credentialId.toString('hex');
+
+	await UserSecurityKeys.save({
+		userId: user.id,
+		id: credentialIdString,
+		lastUsed: new Date(),
+		name: ps.name,
+		publicKey: verificationData.publicKey.toString('hex')
+	});
+
+	// Publish meUpdated event
+	publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, {
+		detail: true,
+		includeSecrets: true
+	}));
+
+	return {
+		id: credentialIdString,
+		name: ps.name
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
new file mode 100644
index 0000000000..064828b638
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -0,0 +1,21 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { UserProfiles } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		value: {
+			validator: $.boolean
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	await UserProfiles.update(user.id, {
+		usePasswordLessLogin: ps.value
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
new file mode 100644
index 0000000000..1b385a10ee
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -0,0 +1,59 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import define from '../../../define';
+import { UserProfiles, AttestationChallenges } from '@/models/index';
+import { promisify } from 'util';
+import * as crypto from 'crypto';
+import { genId } from '@/misc/gen-id';
+import { hash } from '../../../2fa';
+
+const randomBytes = promisify(crypto.randomBytes);
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		password: {
+			validator: $.str
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(ps.password, profile.password!);
+
+	if (!same) {
+		throw new Error('incorrect password');
+	}
+
+	if (!profile.twoFactorEnabled) {
+		throw new Error('2fa not enabled');
+	}
+
+	// 32 byte challenge
+	const entropy = await randomBytes(32);
+	const challenge = entropy.toString('base64')
+		.replace(/=/g, '')
+		.replace(/\+/g, '-')
+		.replace(/\//g, '_');
+
+	const challengeId = genId();
+
+	await AttestationChallenges.save({
+		userId: user.id,
+		id: challengeId,
+		challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
+		createdAt: new Date(),
+		registrationChallenge: true
+	});
+
+	return {
+		challengeId,
+		challenge
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
new file mode 100644
index 0000000000..b03b98188a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -0,0 +1,54 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import * as speakeasy from 'speakeasy';
+import * as QRCode from 'qrcode';
+import config from '@/config/index';
+import define from '../../../define';
+import { UserProfiles } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		password: {
+			validator: $.str
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(ps.password, profile.password!);
+
+	if (!same) {
+		throw new Error('incorrect password');
+	}
+
+	// Generate user's secret key
+	const secret = speakeasy.generateSecret({
+		length: 32
+	});
+
+	await UserProfiles.update(user.id, {
+		twoFactorTempSecret: secret.base32
+	});
+
+	// Get the data URL of the authenticator URL
+	const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({
+		secret: secret.base32,
+		encoding: 'base32',
+		label: user.username,
+		issuer: config.host
+	}));
+
+	return {
+		qr: dataUrl,
+		secret: secret.base32,
+		label: user.username,
+		issuer: config.host
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
new file mode 100644
index 0000000000..dea56301ab
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import define from '../../../define';
+import { UserProfiles, UserSecurityKeys, Users } from '@/models/index';
+import { publishMainStream } from '@/services/stream';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		password: {
+			validator: $.str
+		},
+		credentialId: {
+			validator: $.str
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(ps.password, profile.password!);
+
+	if (!same) {
+		throw new Error('incorrect password');
+	}
+
+	// Make sure we only delete the user's own creds
+	await UserSecurityKeys.delete({
+		userId: user.id,
+		id: ps.credentialId
+	});
+
+	// Publish meUpdated event
+	publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, {
+		detail: true,
+		includeSecrets: true
+	}));
+
+	return {};
+});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
new file mode 100644
index 0000000000..af53033daa
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -0,0 +1,32 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import define from '../../../define';
+import { UserProfiles } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		password: {
+			validator: $.str
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(ps.password, profile.password!);
+
+	if (!same) {
+		throw new Error('incorrect password');
+	}
+
+	await UserProfiles.update(user.id, {
+		twoFactorSecret: null,
+		twoFactorEnabled: false
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
new file mode 100644
index 0000000000..994528e5c9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import define from '../../define';
+import { AccessTokens } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		sort: {
+			validator: $.optional.str.or([
+				'+createdAt',
+				'-createdAt',
+				'+lastUsedAt',
+				'-lastUsedAt',
+			]),
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = AccessTokens.createQueryBuilder('token')
+		.where('token.userId = :userId', { userId: user.id });
+
+	switch (ps.sort) {
+		case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break;
+		case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break;
+		case '+lastUsedAt': query.orderBy('token.lastUsedAt', 'DESC'); break;
+		case '-lastUsedAt': query.orderBy('token.lastUsedAt', 'ASC'); break;
+		default: query.orderBy('token.id', 'ASC'); break;
+	}
+
+	const tokens = await query.getMany();
+
+	return await Promise.all(tokens.map(token => ({
+		id: token.id,
+		name: token.name,
+		createdAt: token.createdAt,
+		lastUsedAt: token.lastUsedAt,
+		permission: token.permission,
+	})));
+});
diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts
new file mode 100644
index 0000000000..042fcd14e8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts
@@ -0,0 +1,44 @@
+import $ from 'cafy';
+import define from '../../define';
+import { AccessTokens, Apps } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0,
+		},
+
+		sort: {
+			validator: $.optional.str.or('desc|asc'),
+			default: 'desc',
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Get tokens
+	const tokens = await AccessTokens.find({
+		where: {
+			userId: user.id
+		},
+		take: ps.limit!,
+		skip: ps.offset,
+		order: {
+			id: ps.sort == 'asc' ? 1 : -1
+		}
+	});
+
+	return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, {
+		detail: true
+	})));
+});
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
new file mode 100644
index 0000000000..7ea5f8c488
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -0,0 +1,39 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import define from '../../define';
+import { UserProfiles } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		currentPassword: {
+			validator: $.str
+		},
+
+		newPassword: {
+			validator: $.str
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(ps.currentPassword, profile.password!);
+
+	if (!same) {
+		throw new Error('incorrect password');
+	}
+
+	// Generate hash of password
+	const salt = await bcrypt.genSalt(8);
+	const hash = await bcrypt.hash(ps.newPassword, salt);
+
+	await UserProfiles.update(user.id, {
+		password: hash
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
new file mode 100644
index 0000000000..10e5adf64a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -0,0 +1,48 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import define from '../../define';
+import { UserProfiles, Users } from '@/models/index';
+import { doPostSuspend } from '@/services/suspend-user';
+import { publishUserEvent } from '@/services/stream';
+import { createDeleteAccountJob } from '@/queue';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		password: {
+			validator: $.str
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+	const userDetailed = await Users.findOneOrFail(user.id);
+	if (userDetailed.isDeleted) {
+		return;
+	}
+
+	// Compare password
+	const same = await bcrypt.compare(ps.password, profile.password!);
+
+	if (!same) {
+		throw new Error('incorrect password');
+	}
+
+	// 物理削除する前にDelete activityを送信する
+	await doPostSuspend(user).catch(e => {});
+
+	createDeleteAccountJob(user, {
+		soft: false
+	});
+
+	await Users.update(user.id, {
+		isDeleted: true,
+	});
+
+	// Terminate streaming
+	publishUserEvent(user.id, 'terminate', {});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts
new file mode 100644
index 0000000000..e4797da0c1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/export-blocking.ts
@@ -0,0 +1,16 @@
+import define from '../../define';
+import { createExportBlockingJob } from '@/queue/index';
+import * as ms from 'ms';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+	limit: {
+		duration: ms('1hour'),
+		max: 1,
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	createExportBlockingJob(user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts
new file mode 100644
index 0000000000..b0f154cda8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/export-following.ts
@@ -0,0 +1,16 @@
+import define from '../../define';
+import { createExportFollowingJob } from '@/queue/index';
+import * as ms from 'ms';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+	limit: {
+		duration: ms('1hour'),
+		max: 1,
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	createExportFollowingJob(user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts
new file mode 100644
index 0000000000..46d547fa53
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/export-mute.ts
@@ -0,0 +1,16 @@
+import define from '../../define';
+import { createExportMuteJob } from '@/queue/index';
+import * as ms from 'ms';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+	limit: {
+		duration: ms('1hour'),
+		max: 1,
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	createExportMuteJob(user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts
new file mode 100644
index 0000000000..441bf16896
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/export-notes.ts
@@ -0,0 +1,16 @@
+import define from '../../define';
+import { createExportNotesJob } from '@/queue/index';
+import * as ms from 'ms';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+	limit: {
+		duration: ms('1day'),
+		max: 1,
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	createExportNotesJob(user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts
new file mode 100644
index 0000000000..24043a862a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts
@@ -0,0 +1,16 @@
+import define from '../../define';
+import { createExportUserListsJob } from '@/queue/index';
+import * as ms from 'ms';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+	limit: {
+		duration: ms('1min'),
+		max: 1,
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	createExportUserListsJob(user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts
new file mode 100644
index 0000000000..b79d68ae73
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/favorites.ts
@@ -0,0 +1,50 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { NoteFavorites } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['account', 'notes', 'favorites'],
+
+	requireCredential: true as const,
+
+	kind: 'read:favorites',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'NoteFavorite',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId)
+		.andWhere(`favorite.userId = :meId`, { meId: user.id })
+		.leftJoinAndSelect('favorite.note', 'note');
+
+	const favorites = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await NoteFavorites.packMany(favorites, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts
new file mode 100644
index 0000000000..7a2935a5ec
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts
@@ -0,0 +1,57 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { GalleryLikes } from '@/models/index';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['account', 'gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'read:gallery-likes',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			id: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			},
+			page: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				ref: 'GalleryPost'
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(GalleryLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
+		.andWhere(`like.userId = :meId`, { meId: user.id })
+		.leftJoinAndSelect('like.post', 'post');
+
+	const likes = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await GalleryLikes.packMany(likes, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts
new file mode 100644
index 0000000000..21bb8759fc
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { GalleryPosts } from '@/models/index';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['account', 'gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'read:gallery',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'GalleryPost'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
+		.andWhere(`post.userId = :meId`, { meId: user.id });
+
+	const posts = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await GalleryPosts.packMany(posts, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts
new file mode 100644
index 0000000000..6b9be98582
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts
@@ -0,0 +1,33 @@
+import define from '../../define';
+import { MutedNotes } from '@/models/index';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:account',
+
+	params: {
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			count: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	return {
+		count: await MutedNotes.count({
+			userId: user.id,
+			reason: 'word'
+		})
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
new file mode 100644
index 0000000000..d44d0b6077
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { createImportBlockingJob } from '@/queue/index';
+import * as ms from 'ms';
+import { ApiError } from '../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+
+	limit: {
+		duration: ms('1hour'),
+		max: 1,
+	},
+
+	params: {
+		fileId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'ebb53e5f-6574-9c0c-0b92-7ca6def56d7e'
+		},
+
+		unexpectedFileType: {
+			message: 'We need csv file.',
+			code: 'UNEXPECTED_FILE_TYPE',
+			id: 'b6fab7d6-d945-d67c-dfdb-32da1cd12cfe'
+		},
+
+		tooBigFile: {
+			message: 'That file is too big.',
+			code: 'TOO_BIG_FILE',
+			id: 'b7fbf0b1-aeef-3b21-29ef-fadd4cb72ccf'
+		},
+
+		emptyFile: {
+			message: 'That file is empty.',
+			code: 'EMPTY_FILE',
+			id: '6f3a4dcc-f060-a707-4950-806fbdbe60d6'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const file = await DriveFiles.findOne(ps.fileId);
+
+	if (file == null) throw new ApiError(meta.errors.noSuchFile);
+	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+	if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+
+	createImportBlockingJob(user, file.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
new file mode 100644
index 0000000000..b3de397661
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -0,0 +1,59 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { createImportFollowingJob } from '@/queue/index';
+import * as ms from 'ms';
+import { ApiError } from '../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+	limit: {
+		duration: ms('1hour'),
+		max: 1,
+	},
+
+	params: {
+		fileId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'b98644cf-a5ac-4277-a502-0b8054a709a3'
+		},
+
+		unexpectedFileType: {
+			message: 'We need csv file.',
+			code: 'UNEXPECTED_FILE_TYPE',
+			id: '660f3599-bce0-4f95-9dde-311fd841c183'
+		},
+
+		tooBigFile: {
+			message: 'That file is too big.',
+			code: 'TOO_BIG_FILE',
+			id: 'dee9d4ed-ad07-43ed-8b34-b2856398bc60'
+		},
+
+		emptyFile: {
+			message: 'That file is empty.',
+			code: 'EMPTY_FILE',
+			id: '31a1b42c-06f7-42ae-8a38-a661c5c9f691'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const file = await DriveFiles.findOne(ps.fileId);
+
+	if (file == null) throw new ApiError(meta.errors.noSuchFile);
+	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+	if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+
+	createImportFollowingJob(user, file.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
new file mode 100644
index 0000000000..c17434c587
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { createImportMutingJob } from '@/queue/index';
+import * as ms from 'ms';
+import { ApiError } from '../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+
+	limit: {
+		duration: ms('1hour'),
+		max: 1,
+	},
+
+	params: {
+		fileId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'e674141e-bd2a-ba85-e616-aefb187c9c2a'
+		},
+
+		unexpectedFileType: {
+			message: 'We need csv file.',
+			code: 'UNEXPECTED_FILE_TYPE',
+			id: '568c6e42-c86c-ba09-c004-517f83f9f1a8'
+		},
+
+		tooBigFile: {
+			message: 'That file is too big.',
+			code: 'TOO_BIG_FILE',
+			id: '9b4ada6d-d7f7-0472-0713-4f558bd1ec9c'
+		},
+
+		emptyFile: {
+			message: 'That file is empty.',
+			code: 'EMPTY_FILE',
+			id: 'd2f12af1-e7b4-feac-86a3-519548f2728e'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const file = await DriveFiles.findOne(ps.fileId);
+
+	if (file == null) throw new ApiError(meta.errors.noSuchFile);
+	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+	if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+
+	createImportMutingJob(user, file.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
new file mode 100644
index 0000000000..9069a019a9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -0,0 +1,59 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { createImportUserListsJob } from '@/queue/index';
+import * as ms from 'ms';
+import { ApiError } from '../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+	secure: true,
+	requireCredential: true as const,
+	limit: {
+		duration: ms('1hour'),
+		max: 1,
+	},
+
+	params: {
+		fileId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'ea9cc34f-c415-4bc6-a6fe-28ac40357049'
+		},
+
+		unexpectedFileType: {
+			message: 'We need csv file.',
+			code: 'UNEXPECTED_FILE_TYPE',
+			id: 'a3c9edda-dd9b-4596-be6a-150ef813745c'
+		},
+
+		tooBigFile: {
+			message: 'That file is too big.',
+			code: 'TOO_BIG_FILE',
+			id: 'ae6e7a22-971b-4b52-b2be-fc0b9b121fe9'
+		},
+
+		emptyFile: {
+			message: 'That file is empty.',
+			code: 'EMPTY_FILE',
+			id: '99efe367-ce6e-4d44-93f8-5fae7b040356'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const file = await DriveFiles.findOne(ps.fileId);
+
+	if (file == null) throw new ApiError(meta.errors.noSuchFile);
+	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+	if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile);
+	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+
+	createImportUserListsJob(user, file.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
new file mode 100644
index 0000000000..56668d03b7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -0,0 +1,138 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { readNotification } from '../../common/read-notification';
+import define from '../../define';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notifications, Followings, Mutings, Users } from '@/models/index';
+import { notificationTypes } from '@/types';
+import read from '@/services/note/read';
+import { Brackets } from 'typeorm';
+
+export const meta = {
+	tags: ['account', 'notifications'],
+
+	requireCredential: true as const,
+
+	kind: 'read:notifications',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		following: {
+			validator: $.optional.bool,
+			default: false
+		},
+
+		unreadOnly: {
+			validator: $.optional.bool,
+			default: false
+		},
+
+		markAsRead: {
+			validator: $.optional.bool,
+			default: true
+		},
+
+		includeTypes: {
+			validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])),
+		},
+
+		excludeTypes: {
+			validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])),
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Notification',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	// includeTypes が空の場合はクエリしない
+	if (ps.includeTypes && ps.includeTypes.length === 0) {
+		return [];
+	}
+	// excludeTypes に全指定されている場合はクエリしない
+	if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
+		return [];
+	}
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: user.id });
+
+	const mutingQuery = Mutings.createQueryBuilder('muting')
+		.select('muting.muteeId')
+		.where('muting.muterId = :muterId', { muterId: user.id });
+
+	const suspendedQuery = Users.createQueryBuilder('users')
+		.select('users.id')
+		.where('users.isSuspended = TRUE');
+
+	const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
+		.andWhere(`notification.notifieeId = :meId`, { meId: user.id })
+		.leftJoinAndSelect('notification.notifier', 'notifier')
+		.leftJoinAndSelect('notification.note', 'note')
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	query.andWhere(new Brackets(qb => { qb
+		.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
+		.orWhere('notification.notifierId IS NULL');
+	}));
+	query.setParameters(mutingQuery.getParameters());
+
+	query.andWhere(new Brackets(qb => { qb
+		.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
+		.orWhere('notification.notifierId IS NULL');
+	}));
+
+	if (ps.following) {
+		query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id });
+		query.setParameters(followingQuery.getParameters());
+	}
+
+	if (ps.includeTypes && ps.includeTypes.length > 0) {
+		query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
+	} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
+		query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
+	}
+
+	if (ps.unreadOnly) {
+		query.andWhere(`notification.isRead = false`);
+	}
+
+	const notifications = await query.take(ps.limit!).getMany();
+
+	// Mark all as read
+	if (notifications.length > 0 && ps.markAsRead) {
+		readNotification(user.id, notifications.map(x => x.id));
+	}
+
+	const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!);
+
+	if (notes.length > 0) {
+		read(user.id, notes);
+	}
+
+	return await Notifications.packMany(notifications, user.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts
new file mode 100644
index 0000000000..fa2bc31730
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts
@@ -0,0 +1,57 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { PageLikes } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['account', 'pages'],
+
+	requireCredential: true as const,
+
+	kind: 'read:page-likes',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			id: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'id'
+			},
+			page: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				ref: 'Page'
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
+		.andWhere(`like.userId = :meId`, { meId: user.id })
+		.leftJoinAndSelect('like.page', 'page');
+
+	const likes = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await PageLikes.packMany(likes, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts
new file mode 100644
index 0000000000..ee87fffa2d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/pages.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Pages } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['account', 'pages'],
+
+	requireCredential: true as const,
+
+	kind: 'read:pages',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Page'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
+		.andWhere(`page.userId = :meId`, { meId: user.id });
+
+	const pages = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Pages.packMany(pages);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts
new file mode 100644
index 0000000000..de94220ba9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/pin.ts
@@ -0,0 +1,59 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { addPinned } from '@/services/i/pin';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['account', 'notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '56734f8b-3928-431e-bf80-6ff87df40cb3'
+		},
+
+		pinLimitExceeded: {
+			message: 'You can not pin notes any more.',
+			code: 'PIN_LIMIT_EXCEEDED',
+			id: '72dab508-c64d-498f-8740-a8eec1ba385a'
+		},
+
+		alreadyPinned: {
+			message: 'That note has already been pinned.',
+			code: 'ALREADY_PINNED',
+			id: '8b18c2b7-68fe-4edb-9892-c0cbaeb6c913'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	await addPinned(user, ps.noteId).catch(e => {
+		if (e.id === '70c4e51f-5bea-449c-a030-53bee3cce202') throw new ApiError(meta.errors.noSuchNote);
+		if (e.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError(meta.errors.pinLimitExceeded);
+		if (e.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError(meta.errors.alreadyPinned);
+		throw e;
+	});
+
+	return await Users.pack(user.id, user, {
+		detail: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts
new file mode 100644
index 0000000000..9aca7611c9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts
@@ -0,0 +1,37 @@
+import { publishMainStream } from '@/services/stream';
+import define from '../../define';
+import { MessagingMessages, UserGroupJoinings } from '@/models/index';
+
+export const meta = {
+	tags: ['account', 'messaging'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Update documents
+	await MessagingMessages.update({
+		recipientId: user.id,
+		isRead: false
+	}, {
+		isRead: true
+	});
+
+	const joinings = await UserGroupJoinings.find({ userId: user.id });
+
+	await Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder().update()
+		.set({
+			reads: (() => `array_append("reads", '${user.id}')`) as any
+		})
+		.where(`groupId = :groupId`, { groupId: j.userGroupId })
+		.andWhere('userId != :userId', { userId: user.id })
+		.andWhere('NOT (:userId = ANY(reads))', { userId: user.id })
+		.execute()));
+
+	publishMainStream(user.id, 'readAllMessagingMessages');
+});
diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts
new file mode 100644
index 0000000000..2a7102a590
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts
@@ -0,0 +1,25 @@
+import { publishMainStream } from '@/services/stream';
+import define from '../../define';
+import { NoteUnreads } from '@/models/index';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Remove documents
+	await NoteUnreads.delete({
+		userId: user.id
+	});
+
+	// 全て既読になったイベントを発行
+	publishMainStream(user.id, 'readAllUnreadMentions');
+	publishMainStream(user.id, 'readAllUnreadSpecifiedNotes');
+});
diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts
new file mode 100644
index 0000000000..2f5036f953
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { genId } from '@/misc/gen-id';
+import { AnnouncementReads, Announcements, Users } from '@/models/index';
+import { publishMainStream } from '@/services/stream';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		announcementId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchAnnouncement: {
+			message: 'No such announcement.',
+			code: 'NO_SUCH_ANNOUNCEMENT',
+			id: '184663db-df88-4bc2-8b52-fb85f0681939'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Check if announcement exists
+	const announcement = await Announcements.findOne(ps.announcementId);
+
+	if (announcement == null) {
+		throw new ApiError(meta.errors.noSuchAnnouncement);
+	}
+
+	// Check if already read
+	const read = await AnnouncementReads.findOne({
+		announcementId: ps.announcementId,
+		userId: user.id
+	});
+
+	if (read != null) {
+		return;
+	}
+
+	// Create read
+	await AnnouncementReads.insert({
+		id: genId(),
+		createdAt: new Date(),
+		announcementId: ps.announcementId,
+		userId: user.id,
+	});
+
+	if (!await Users.getHasUnreadAnnouncement(user.id)) {
+		publishMainStream(user.id, 'readAllAnnouncements');
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
new file mode 100644
index 0000000000..1cce2d37be
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
@@ -0,0 +1,44 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import { publishMainStream, publishUserEvent } from '@/services/stream';
+import generateUserToken from '../../common/generate-native-user-token';
+import define from '../../define';
+import { Users, UserProfiles } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		password: {
+			validator: $.str
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(ps.password, profile.password!);
+
+	if (!same) {
+		throw new Error('incorrect password');
+	}
+
+	// Generate secret
+	const secret = generateUserToken();
+
+	await Users.update(user.id, {
+		token: secret
+	});
+
+	// Publish event
+	publishMainStream(user.id, 'myTokenRegenerated');
+
+	// Terminate streaming
+	setTimeout(() => {
+		publishUserEvent(user.id, 'terminate', {});
+	}, 5000);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts
new file mode 100644
index 0000000000..c8eaf83a25
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts
@@ -0,0 +1,33 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { RegistryItems } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		scope: {
+			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
+			default: [],
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = RegistryItems.createQueryBuilder('item')
+		.where('item.domain IS NULL')
+		.andWhere('item.userId = :userId', { userId: user.id })
+		.andWhere('item.scope = :scope', { scope: ps.scope });
+
+	const items = await query.getMany();
+
+	const res = {} as Record<string, any>;
+
+	for (const item of items) {
+		res[item.key] = item.value;
+	}
+
+	return res;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
new file mode 100644
index 0000000000..992800c44c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts
@@ -0,0 +1,48 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { RegistryItems } from '@/models/index';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		key: {
+			validator: $.str
+		},
+
+		scope: {
+			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
+			default: [],
+		},
+	},
+
+	errors: {
+		noSuchKey: {
+			message: 'No such key.',
+			code: 'NO_SUCH_KEY',
+			id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a'
+		},
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = RegistryItems.createQueryBuilder('item')
+		.where('item.domain IS NULL')
+		.andWhere('item.userId = :userId', { userId: user.id })
+		.andWhere('item.key = :key', { key: ps.key })
+		.andWhere('item.scope = :scope', { scope: ps.scope });
+
+	const item = await query.getOne();
+
+	if (item == null) {
+		throw new ApiError(meta.errors.noSuchKey);
+	}
+
+	return {
+		updatedAt: item.updatedAt,
+		value: item.value,
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts
new file mode 100644
index 0000000000..569c3a9280
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { RegistryItems } from '@/models/index';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		key: {
+			validator: $.str
+		},
+
+		scope: {
+			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
+			default: [],
+		},
+	},
+
+	errors: {
+		noSuchKey: {
+			message: 'No such key.',
+			code: 'NO_SUCH_KEY',
+			id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a'
+		},
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = RegistryItems.createQueryBuilder('item')
+		.where('item.domain IS NULL')
+		.andWhere('item.userId = :userId', { userId: user.id })
+		.andWhere('item.key = :key', { key: ps.key })
+		.andWhere('item.scope = :scope', { scope: ps.scope });
+
+	const item = await query.getOne();
+
+	if (item == null) {
+		throw new ApiError(meta.errors.noSuchKey);
+	}
+
+	return item.value;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
new file mode 100644
index 0000000000..16a4fee374
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts
@@ -0,0 +1,41 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { RegistryItems } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		scope: {
+			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
+			default: [],
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = RegistryItems.createQueryBuilder('item')
+		.where('item.domain IS NULL')
+		.andWhere('item.userId = :userId', { userId: user.id })
+		.andWhere('item.scope = :scope', { scope: ps.scope });
+
+	const items = await query.getMany();
+
+	const res = {} as Record<string, string>;
+
+	for (const item of items) {
+		const type = typeof item.value;
+		res[item.key] =
+			item.value === null ? 'null' :
+			Array.isArray(item.value) ? 'array' :
+			type === 'number' ? 'number' :
+			type === 'string' ? 'string' :
+			type === 'boolean' ? 'boolean' :
+			type === 'object' ? 'object' :
+			null as never;
+	}
+
+	return res;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts
new file mode 100644
index 0000000000..3a8aeaa195
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts
@@ -0,0 +1,28 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { RegistryItems } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		scope: {
+			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
+			default: [],
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = RegistryItems.createQueryBuilder('item')
+		.select('item.key')
+		.where('item.domain IS NULL')
+		.andWhere('item.userId = :userId', { userId: user.id })
+		.andWhere('item.scope = :scope', { scope: ps.scope });
+
+	const items = await query.getMany();
+
+	return items.map(x => x.key);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts
new file mode 100644
index 0000000000..07bc23d4a6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { RegistryItems } from '@/models/index';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		key: {
+			validator: $.str
+		},
+
+		scope: {
+			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
+			default: [],
+		},
+	},
+
+	errors: {
+		noSuchKey: {
+			message: 'No such key.',
+			code: 'NO_SUCH_KEY',
+			id: '1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019'
+		},
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = RegistryItems.createQueryBuilder('item')
+		.where('item.domain IS NULL')
+		.andWhere('item.userId = :userId', { userId: user.id })
+		.andWhere('item.key = :key', { key: ps.key })
+		.andWhere('item.scope = :scope', { scope: ps.scope });
+
+	const item = await query.getOne();
+
+	if (item == null) {
+		throw new ApiError(meta.errors.noSuchKey);
+	}
+
+	await RegistryItems.remove(item);
+});
diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts
new file mode 100644
index 0000000000..ecbdb05a8e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts
@@ -0,0 +1,29 @@
+import define from '../../../define';
+import { RegistryItems } from '@/models/index';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = RegistryItems.createQueryBuilder('item')
+		.select('item.scope')
+		.where('item.domain IS NULL')
+		.andWhere('item.userId = :userId', { userId: user.id });
+
+	const items = await query.getMany();
+
+	const res = [] as string[][];
+
+	for (const item of items) {
+		if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue;
+		res.push(item.scope);
+	}
+
+	return res;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts
new file mode 100644
index 0000000000..f129ee1b70
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts
@@ -0,0 +1,61 @@
+import $ from 'cafy';
+import { publishMainStream } from '@/services/stream';
+import define from '../../../define';
+import { RegistryItems } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		key: {
+			validator: $.str.min(1)
+		},
+
+		value: {
+			validator: $.nullable.any
+		},
+
+		scope: {
+			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
+			default: [],
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = RegistryItems.createQueryBuilder('item')
+		.where('item.domain IS NULL')
+		.andWhere('item.userId = :userId', { userId: user.id })
+		.andWhere('item.key = :key', { key: ps.key })
+		.andWhere('item.scope = :scope', { scope: ps.scope });
+
+	const existingItem = await query.getOne();
+
+	if (existingItem) {
+		await RegistryItems.update(existingItem.id, {
+			updatedAt: new Date(),
+			value: ps.value
+		});
+	} else {
+		await RegistryItems.insert({
+			id: genId(),
+			createdAt: new Date(),
+			updatedAt: new Date(),
+			userId: user.id,
+			domain: null,
+			scope: ps.scope,
+			key: ps.key,
+			value: ps.value
+		});
+	}
+
+	// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
+	publishMainStream(user.id, 'registryUpdated', {
+		scope: ps.scope,
+		key: ps.key,
+		value: ps.value
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts
new file mode 100644
index 0000000000..bed868def4
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts
@@ -0,0 +1,31 @@
+import $ from 'cafy';
+import define from '../../define';
+import { AccessTokens } from '@/models/index';
+import { ID } from '@/misc/cafy-id';
+import { publishUserEvent } from '@/services/stream';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		tokenId: {
+			validator: $.type(ID)
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const token = await AccessTokens.findOne(ps.tokenId);
+
+	if (token) {
+		await AccessTokens.delete({
+			id: ps.tokenId,
+			userId: user.id,
+		});
+
+		// Terminate streaming
+		publishUserEvent(user.id, 'terminate');
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts
new file mode 100644
index 0000000000..a2c10148c6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/signin-history.ts
@@ -0,0 +1,35 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Signins } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId)
+		.andWhere(`signin.userId = :meId`, { meId: user.id });
+
+	const history = await query.take(ps.limit!).getMany();
+
+	return await Promise.all(history.map(record => Signins.pack(record)));
+});
diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts
new file mode 100644
index 0000000000..dc79e255ab
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/unpin.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { removePinned } from '@/services/i/pin';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['account', 'notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '454170ce-9d63-4a43-9da1-ea10afe81e21'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User'
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	await removePinned(user, ps.noteId).catch(e => {
+		if (e.id === 'b302d4cf-c050-400a-bbb3-be208681f40c') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	return await Users.pack(user.id, user, {
+		detail: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
new file mode 100644
index 0000000000..9b6fb9c410
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -0,0 +1,94 @@
+import $ from 'cafy';
+import { publishMainStream } from '@/services/stream';
+import define from '../../define';
+import rndstr from 'rndstr';
+import config from '@/config/index';
+import * as ms from 'ms';
+import * as bcrypt from 'bcryptjs';
+import { Users, UserProfiles } from '@/models/index';
+import { sendEmail } from '@/services/send-email';
+import { ApiError } from '../../error';
+import { validateEmailForAccount } from '@/services/validate-email-for-account';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	limit: {
+		duration: ms('1hour'),
+		max: 3
+	},
+
+	params: {
+		password: {
+			validator: $.str
+		},
+
+		email: {
+			validator: $.optional.nullable.str
+		},
+	},
+
+	errors: {
+		incorrectPassword: {
+			message: 'Incorrect password.',
+			code: 'INCORRECT_PASSWORD',
+			id: 'e54c1d7e-e7d6-4103-86b6-0a95069b4ad3'
+		},
+
+		unavailable: {
+			message: 'Unavailable email address.',
+			code: 'UNAVAILABLE',
+			id: 'a2defefb-f220-8849-0af6-17f816099323'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(ps.password, profile.password!);
+
+	if (!same) {
+		throw new ApiError(meta.errors.incorrectPassword);
+	}
+
+	if (ps.email != null) {
+		const available = await validateEmailForAccount(ps.email);
+		if (!available) {
+			throw new ApiError(meta.errors.unavailable);
+		}
+	}
+
+	await UserProfiles.update(user.id, {
+		email: ps.email,
+		emailVerified: false,
+		emailVerifyCode: null
+	});
+
+	const iObj = await Users.pack(user.id, user, {
+		detail: true,
+		includeSecrets: true
+	});
+
+	// Publish meUpdated event
+	publishMainStream(user.id, 'meUpdated', iObj);
+
+	if (ps.email != null) {
+		const code = rndstr('a-z0-9', 16);
+
+		await UserProfiles.update(user.id, {
+			emailVerifyCode: code
+		});
+
+		const link = `${config.url}/verify-email/${code}`;
+
+		sendEmail(ps.email, 'Email verification',
+			`To verify email, please click this link:<br><a href="${link}">${link}</a>`,
+			`To verify email, please click this link: ${link}`);
+	}
+
+	return iObj;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
new file mode 100644
index 0000000000..d0f201ab60
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -0,0 +1,294 @@
+import $ from 'cafy';
+import * as mfm from 'mfm-js';
+import { ID } from '@/misc/cafy-id';
+import { publishMainStream, publishUserEvent } from '@/services/stream';
+import acceptAllFollowRequests from '@/services/following/requests/accept-all';
+import { publishToFollowers } from '@/services/i/update';
+import define from '../../define';
+import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm';
+import { extractHashtags } from '@/misc/extract-hashtags';
+import * as langmap from 'langmap';
+import { updateUsertags } from '@/services/update-hashtag';
+import { ApiError } from '../../error';
+import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index';
+import { User } from '@/models/entities/user';
+import { UserProfile } from '@/models/entities/user-profile';
+import { notificationTypes } from '@/types';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		name: {
+			validator: $.optional.nullable.use(Users.validateName),
+		},
+
+		description: {
+			validator: $.optional.nullable.use(Users.validateDescription),
+		},
+
+		lang: {
+			validator: $.optional.nullable.str.or(Object.keys(langmap)),
+		},
+
+		location: {
+			validator: $.optional.nullable.use(Users.validateLocation),
+		},
+
+		birthday: {
+			validator: $.optional.nullable.use(Users.validateBirthday),
+		},
+
+		avatarId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		bannerId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		fields: {
+			validator: $.optional.arr($.object()).range(1, 4),
+		},
+
+		isLocked: {
+			validator: $.optional.bool,
+		},
+
+		isExplorable: {
+			validator: $.optional.bool,
+		},
+
+		hideOnlineStatus: {
+			validator: $.optional.bool,
+		},
+
+		publicReactions: {
+			validator: $.optional.bool,
+		},
+
+		ffVisibility: {
+			validator: $.optional.str,
+		},
+
+		carefulBot: {
+			validator: $.optional.bool,
+		},
+
+		autoAcceptFollowed: {
+			validator: $.optional.bool,
+		},
+
+		noCrawle: {
+			validator: $.optional.bool,
+		},
+
+		isBot: {
+			validator: $.optional.bool,
+		},
+
+		isCat: {
+			validator: $.optional.bool,
+		},
+
+		injectFeaturedNote: {
+			validator: $.optional.bool,
+		},
+
+		receiveAnnouncementEmail: {
+			validator: $.optional.bool,
+		},
+
+		alwaysMarkNsfw: {
+			validator: $.optional.bool,
+		},
+
+		pinnedPageId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		mutedWords: {
+			validator: $.optional.arr($.arr($.str))
+		},
+
+		mutingNotificationTypes: {
+			validator: $.optional.arr($.str.or(notificationTypes as unknown as string[]))
+		},
+
+		emailNotificationTypes: {
+			validator: $.optional.arr($.str)
+		},
+	},
+
+	errors: {
+		noSuchAvatar: {
+			message: 'No such avatar file.',
+			code: 'NO_SUCH_AVATAR',
+			id: '539f3a45-f215-4f81-a9a8-31293640207f'
+		},
+
+		noSuchBanner: {
+			message: 'No such banner file.',
+			code: 'NO_SUCH_BANNER',
+			id: '0d8f5629-f210-41c2-9433-735831a58595'
+		},
+
+		avatarNotAnImage: {
+			message: 'The file specified as an avatar is not an image.',
+			code: 'AVATAR_NOT_AN_IMAGE',
+			id: 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191'
+		},
+
+		bannerNotAnImage: {
+			message: 'The file specified as a banner is not an image.',
+			code: 'BANNER_NOT_AN_IMAGE',
+			id: '75aedb19-2afd-4e6d-87fc-67941256fa60'
+		},
+
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: '8e01b590-7eb9-431b-a239-860e086c408e'
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User'
+	}
+};
+
+export default define(meta, async (ps, _user, token) => {
+	const user = await Users.findOneOrFail(_user.id);
+	const isSecure = token == null;
+
+	const updates = {} as Partial<User>;
+	const profileUpdates = {} as Partial<UserProfile>;
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	if (ps.name !== undefined) updates.name = ps.name;
+	if (ps.description !== undefined) profileUpdates.description = ps.description;
+	if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
+	if (ps.location !== undefined) profileUpdates.location = ps.location;
+	if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
+	if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
+	if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
+	if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
+	if (ps.mutedWords !== undefined) {
+		profileUpdates.mutedWords = ps.mutedWords;
+		profileUpdates.enableWordMute = ps.mutedWords.length > 0;
+	}
+	if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][];
+	if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
+	if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
+	if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
+	if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
+	if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
+	if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
+	if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
+	if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
+	if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
+	if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
+	if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
+	if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
+	if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
+
+	if (ps.avatarId) {
+		const avatar = await DriveFiles.findOne(ps.avatarId);
+
+		if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
+		if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
+
+		updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
+
+		if (avatar.blurhash) {
+			updates.avatarBlurhash = avatar.blurhash;
+		}
+	}
+
+	if (ps.bannerId) {
+		const banner = await DriveFiles.findOne(ps.bannerId);
+
+		if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
+		if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
+
+		updates.bannerUrl = DriveFiles.getPublicUrl(banner, false);
+
+		if (banner.blurhash) {
+			updates.bannerBlurhash = banner.blurhash;
+		}
+	}
+
+	if (ps.pinnedPageId) {
+		const page = await Pages.findOne(ps.pinnedPageId);
+
+		if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
+
+		profileUpdates.pinnedPageId = page.id;
+	} else if (ps.pinnedPageId === null) {
+		profileUpdates.pinnedPageId = null;
+	}
+
+	if (ps.fields) {
+		profileUpdates.fields = ps.fields
+			.filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
+			.map(x => {
+				return { name: x.name, value: x.value };
+			});
+	}
+
+	//#region emojis/tags
+
+	let emojis = [] as string[];
+	let tags = [] as string[];
+
+	const newName = updates.name === undefined ? user.name : updates.name;
+	const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
+
+	if (newName != null) {
+		const tokens = mfm.parsePlain(newName);
+		emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
+	}
+
+	if (newDescription != null) {
+		const tokens = mfm.parse(newDescription);
+		emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
+		tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
+	}
+
+	updates.emojis = emojis;
+	updates.tags = tags;
+
+	// ハッシュタグ更新
+	updateUsertags(user, tags);
+	//#endregion
+
+	if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
+	if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates);
+
+	const iObj = await Users.pack(user.id, user, {
+		detail: true,
+		includeSecrets: isSecure
+	});
+
+	// Publish meUpdated event
+	publishMainStream(user.id, 'meUpdated', iObj);
+	publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.findOne(user.id));
+
+	// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
+	if (user.isLocked && ps.isLocked === false) {
+		acceptAllFollowRequests(user);
+	}
+
+	// フォロワーにUpdateを配信
+	publishToFollowers(user.id);
+
+	return iObj;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts
new file mode 100644
index 0000000000..1ebde243ca
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts
@@ -0,0 +1,61 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { UserGroupInvitations } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['account', 'groups'],
+
+	requireCredential: true as const,
+
+	kind: 'read:user-groups',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const,
+					format: 'id'
+				},
+				group: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					ref: 'UserGroup'
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(UserGroupInvitations.createQueryBuilder('invitation'), ps.sinceId, ps.untilId)
+		.andWhere(`invitation.userId = :meId`, { meId: user.id })
+		.leftJoinAndSelect('invitation.userGroup', 'user_group');
+
+	const invitations = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await UserGroupInvitations.packMany(invitations);
+});
diff --git a/packages/backend/src/server/api/endpoints/messaging/history.ts b/packages/backend/src/server/api/endpoints/messaging/history.ts
new file mode 100644
index 0000000000..e447703546
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/messaging/history.ts
@@ -0,0 +1,94 @@
+import $ from 'cafy';
+import define from '../../define';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { MessagingMessages, Mutings, UserGroupJoinings } from '@/models/index';
+import { Brackets } from 'typeorm';
+
+export const meta = {
+	tags: ['messaging'],
+
+	requireCredential: true as const,
+
+	kind: 'read:messaging',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		group: {
+			validator: $.optional.bool,
+			default: false
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'MessagingMessage',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const mute = await Mutings.find({
+		muterId: user.id,
+	});
+
+	const groups = ps.group ? await UserGroupJoinings.find({
+		userId: user.id,
+	}).then(xs => xs.map(x => x.userGroupId)) : [];
+
+	if (ps.group && groups.length === 0) {
+		return [];
+	}
+
+	const history: MessagingMessage[] = [];
+
+	for (let i = 0; i < ps.limit!; i++) {
+		const found = ps.group
+			? history.map(m => m.groupId!)
+			: history.map(m => (m.userId === user.id) ? m.recipientId! : m.userId!);
+
+		const query = MessagingMessages.createQueryBuilder('message')
+			.orderBy('message.createdAt', 'DESC');
+
+		if (ps.group) {
+			query.where(`message.groupId IN (:...groups)`, { groups: groups });
+
+			if (found.length > 0) {
+				query.andWhere(`message.groupId NOT IN (:...found)`, { found: found });
+			}
+		} else {
+			query.where(new Brackets(qb => { qb
+				.where(`message.userId = :userId`, { userId: user.id })
+				.orWhere(`message.recipientId = :userId`, { userId: user.id });
+			}));
+			query.andWhere(`message.groupId IS NULL`);
+
+			if (found.length > 0) {
+				query.andWhere(`message.userId NOT IN (:...found)`, { found: found });
+				query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found });
+			}
+
+			if (mute.length > 0) {
+				query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
+				query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
+			}
+		}
+
+		const message = await query.getOne();
+
+		if (message) {
+			history.push(message);
+		} else {
+			break;
+		}
+	}
+
+	return await Promise.all(history.map(h => MessagingMessages.pack(h.id, user)));
+});
diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts
new file mode 100644
index 0000000000..6baa24609e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts
@@ -0,0 +1,148 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { MessagingMessages, UserGroups, UserGroupJoinings, Users } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Brackets } from 'typeorm';
+import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message';
+
+export const meta = {
+	tags: ['messaging'],
+
+	requireCredential: true as const,
+
+	kind: 'read:messaging',
+
+	params: {
+		userId: {
+			validator: $.optional.type(ID),
+		},
+
+		groupId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		markAsRead: {
+			validator: $.optional.bool,
+			default: true
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'MessagingMessage',
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '11795c64-40ea-4198-b06e-3c873ed9039d'
+		},
+
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f'
+		},
+
+		groupAccessDenied: {
+			message: 'You can not read messages of groups that you have not joined.',
+			code: 'GROUP_ACCESS_DENIED',
+			id: 'a053a8dd-a491-4718-8f87-50775aad9284'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	if (ps.userId != null) {
+		// Fetch recipient (user)
+		const recipient = await getUser(ps.userId).catch(e => {
+			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+			throw e;
+		});
+
+		const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
+			.andWhere(new Brackets(qb => { qb
+				.where(new Brackets(qb => { qb
+					.where('message.userId = :meId')
+					.andWhere('message.recipientId = :recipientId');
+				}))
+				.orWhere(new Brackets(qb => { qb
+					.where('message.userId = :recipientId')
+					.andWhere('message.recipientId = :meId');
+				}));
+			}))
+			.setParameter('meId', user.id)
+			.setParameter('recipientId', recipient.id);
+
+		const messages = await query.take(ps.limit!).getMany();
+
+		// Mark all as read
+		if (ps.markAsRead) {
+			readUserMessagingMessage(user.id, recipient.id, messages.filter(m => m.recipientId === user.id).map(x => x.id));
+
+			// リモートユーザーとのメッセージだったら既読配信
+			if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) {
+				deliverReadActivity(user, recipient, messages);
+			}
+		}
+
+		return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
+			populateRecipient: false
+		})));
+	} else if (ps.groupId != null) {
+		// Fetch recipient (group)
+		const recipientGroup = await UserGroups.findOne(ps.groupId);
+
+		if (recipientGroup == null) {
+			throw new ApiError(meta.errors.noSuchGroup);
+		}
+
+		// check joined
+		const joining = await UserGroupJoinings.findOne({
+			userId: user.id,
+			userGroupId: recipientGroup.id
+		});
+
+		if (joining == null) {
+			throw new ApiError(meta.errors.groupAccessDenied);
+		}
+
+		const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
+			.andWhere(`message.groupId = :groupId`, { groupId: recipientGroup.id });
+
+		const messages = await query.take(ps.limit!).getMany();
+
+		// Mark all as read
+		if (ps.markAsRead) {
+			readGroupMessagingMessage(user.id, recipientGroup.id, messages.map(x => x.id));
+		}
+
+		return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
+			populateGroup: false
+		})));
+	} else {
+		throw new Error();
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts
new file mode 100644
index 0000000000..df0b455cbe
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts
@@ -0,0 +1,148 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+import { MessagingMessages, DriveFiles, UserGroups, UserGroupJoinings, Blockings } from '@/models/index';
+import { User } from '@/models/entities/user';
+import { UserGroup } from '@/models/entities/user-group';
+import { createMessage } from '@/services/messages/create';
+
+export const meta = {
+	tags: ['messaging'],
+
+	requireCredential: true as const,
+
+	kind: 'write:messaging',
+
+	params: {
+		userId: {
+			validator: $.optional.type(ID),
+		},
+
+		groupId: {
+			validator: $.optional.type(ID),
+		},
+
+		text: {
+			validator: $.optional.str.pipe(MessagingMessages.validateText)
+		},
+
+		fileId: {
+			validator: $.optional.type(ID),
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'MessagingMessage',
+	},
+
+	errors: {
+		recipientIsYourself: {
+			message: 'You can not send a message to yourself.',
+			code: 'RECIPIENT_IS_YOURSELF',
+			id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e'
+		},
+
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '11795c64-40ea-4198-b06e-3c873ed9039d'
+		},
+
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537'
+		},
+
+		groupAccessDenied: {
+			message: 'You can not send messages to groups that you have not joined.',
+			code: 'GROUP_ACCESS_DENIED',
+			id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd'
+		},
+
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: '4372b8e2-185d-4146-8749-2f68864a3e5f'
+		},
+
+		contentRequired: {
+			message: 'Content required. You need to set text or fileId.',
+			code: 'CONTENT_REQUIRED',
+			id: '25587321-b0e6-449c-9239-f8925092942c'
+		},
+
+		youHaveBeenBlocked: {
+			message: 'You cannot send a message because you have been blocked by this user.',
+			code: 'YOU_HAVE_BEEN_BLOCKED',
+			id: 'c15a5199-7422-4968-941a-2a462c478f7d'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	let recipientUser: User | undefined;
+	let recipientGroup: UserGroup | undefined;
+
+	if (ps.userId != null) {
+		// Myself
+		if (ps.userId === user.id) {
+			throw new ApiError(meta.errors.recipientIsYourself);
+		}
+
+		// Fetch recipient (user)
+		recipientUser = await getUser(ps.userId).catch(e => {
+			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+			throw e;
+		});
+
+		// Check blocking
+		const block = await Blockings.findOne({
+			blockerId: recipientUser.id,
+			blockeeId: user.id,
+		});
+		if (block) {
+			throw new ApiError(meta.errors.youHaveBeenBlocked);
+		}
+	} else if (ps.groupId != null) {
+		// Fetch recipient (group)
+		recipientGroup = await UserGroups.findOne(ps.groupId);
+
+		if (recipientGroup == null) {
+			throw new ApiError(meta.errors.noSuchGroup);
+		}
+
+		// check joined
+		const joining = await UserGroupJoinings.findOne({
+			userId: user.id,
+			userGroupId: recipientGroup.id
+		});
+
+		if (joining == null) {
+			throw new ApiError(meta.errors.groupAccessDenied);
+		}
+	}
+
+	let file = null;
+	if (ps.fileId != null) {
+		file = await DriveFiles.findOne({
+			id: ps.fileId,
+			userId: user.id
+		});
+
+		if (file == null) {
+			throw new ApiError(meta.errors.noSuchFile);
+		}
+	}
+
+	// テキストが無いかつ添付ファイルも無かったらエラー
+	if (ps.text == null && file == null) {
+		throw new ApiError(meta.errors.contentRequired);
+	}
+
+	return await createMessage(user, recipientUser, recipientGroup, ps.text, file);
+});
diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts
new file mode 100644
index 0000000000..bd4890fc8a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts
@@ -0,0 +1,48 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import * as ms from 'ms';
+import { ApiError } from '../../../error';
+import { MessagingMessages } from '@/models/index';
+import { deleteMessage } from '@/services/messages/delete';
+
+export const meta = {
+	tags: ['messaging'],
+
+	requireCredential: true as const,
+
+	kind: 'write:messaging',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300,
+		minInterval: ms('1sec')
+	},
+
+	params: {
+		messageId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchMessage: {
+			message: 'No such message.',
+			code: 'NO_SUCH_MESSAGE',
+			id: '54b5b326-7925-42cf-8019-130fda8b56af'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const message = await MessagingMessages.findOne({
+		id: ps.messageId,
+		userId: user.id
+	});
+
+	if (message == null) {
+		throw new ApiError(meta.errors.noSuchMessage);
+	}
+
+	await deleteMessage(message);
+});
diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts
new file mode 100644
index 0000000000..a1747310d3
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts
@@ -0,0 +1,48 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { MessagingMessages } from '@/models/index';
+import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message';
+
+export const meta = {
+	tags: ['messaging'],
+
+	requireCredential: true as const,
+
+	kind: 'write:messaging',
+
+	params: {
+		messageId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchMessage: {
+			message: 'No such message.',
+			code: 'NO_SUCH_MESSAGE',
+			id: '86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const message = await MessagingMessages.findOne(ps.messageId);
+
+	if (message == null) {
+		throw new ApiError(meta.errors.noSuchMessage);
+	}
+
+	if (message.recipientId) {
+		await readUserMessagingMessage(user.id, message.userId, [message.id]).catch(e => {
+			if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage);
+			throw e;
+		});
+	} else if (message.groupId) {
+		await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => {
+			if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage);
+			throw e;
+		});
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
new file mode 100644
index 0000000000..ce21556243
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -0,0 +1,598 @@
+import $ from 'cafy';
+import config from '@/config/index';
+import define from '../define';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Ads, Emojis, Users } from '@/models/index';
+import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
+import { MoreThan } from 'typeorm';
+
+export const meta = {
+	tags: ['meta'],
+
+	requireCredential: false as const,
+
+	params: {
+		detail: {
+			validator: $.optional.bool,
+			default: true
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			maintainerName: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			maintainerEmail: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			version: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				example: config.version
+			},
+			name: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			},
+			uri: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'url',
+				example: 'https://misskey.example.com'
+			},
+			description: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const,
+			},
+			langs: {
+				type: 'array' as const,
+				optional: false as const, nullable: false as const,
+				items: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				}
+			},
+			tosUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			repositoryUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				default: 'https://github.com/misskey-dev/misskey'
+			},
+			feedbackUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				default: 'https://github.com/misskey-dev/misskey/issues/new'
+			},
+			secure: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const,
+				default: false
+			},
+			disableRegistration: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const,
+			},
+			disableLocalTimeline: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const,
+			},
+			disableGlobalTimeline: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const,
+			},
+			driveCapacityPerLocalUserMb: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const
+			},
+			driveCapacityPerRemoteUserMb: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const
+			},
+			cacheRemoteFiles: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			proxyRemoteFiles: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			emailRequiredForSignup: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			enableHcaptcha: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			hcaptchaSiteKey: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			enableRecaptcha: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			recaptchaSiteKey: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			swPublickey: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			mascotImageUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				default: '/assets/ai.png'
+			},
+			bannerUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const
+			},
+			errorImageUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				default: 'https://xn--931a.moe/aiart/yubitun.png'
+			},
+			iconUrl: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			maxNoteTextLength: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+				default: 500
+			},
+			emojis: {
+				type: 'array' as const,
+				optional: false as const, nullable: false as const,
+				items: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					properties: {
+						id: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const,
+							format: 'id'
+						},
+						aliases: {
+							type: 'array' as const,
+							optional: false as const, nullable: false as const,
+							items: {
+								type: 'string' as const,
+								optional: false as const, nullable: false as const
+							}
+						},
+						category: {
+							type: 'string' as const,
+							optional: false as const, nullable: true as const
+						},
+						host: {
+							type: 'string' as const,
+							optional: false as const, nullable: true as const
+						},
+						url: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const,
+							format: 'url'
+						}
+					}
+				}
+			},
+			ads: {
+				type: 'array' as const,
+				optional: false as const, nullable: false as const,
+				items: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					properties: {
+						place: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const
+						},
+						url: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const,
+							format: 'url'
+						},
+						imageUrl: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const,
+							format: 'url'
+						},
+					}
+				}
+			},
+			requireSetup: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const,
+				example: false
+			},
+			enableEmail: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			enableTwitterIntegration: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			enableGithubIntegration: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			enableDiscordIntegration: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			enableServiceWorker: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			translatorAvailable: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			proxyAccountName: {
+				type: 'string' as const,
+				optional: false as const, nullable: true as const
+			},
+			features: {
+				type: 'object' as const,
+				optional: true as const, nullable: false as const,
+				properties: {
+					registration: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					localTimeLine: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					globalTimeLine: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					elasticsearch: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					hcaptcha: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					recaptcha: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					objectStorage: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					twitter: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					github: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					discord: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					serviceWorker: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					miauth: {
+						type: 'boolean' as const,
+						optional: true as const, nullable: false as const,
+						default: true
+					},
+				}
+			},
+			userStarForReactionFallback: {
+				type: 'boolean' as const,
+				optional: true as const, nullable: false as const,
+			},
+			pinnedUsers: {
+				type: 'array' as const,
+				optional: true as const, nullable: false as const,
+				items: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				}
+			},
+			hiddenTags: {
+				type: 'array' as const,
+				optional: true as const, nullable: false as const,
+				items: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				}
+			},
+			blockedHosts: {
+				type: 'array' as const,
+				optional: true as const, nullable: false as const,
+				items: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				}
+			},
+			hcaptchaSecretKey: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			recaptchaSecretKey: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			proxyAccountId: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const,
+				format: 'id'
+			},
+			twitterConsumerKey: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			twitterConsumerSecret: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			githubClientId: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			githubClientSecret: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			discordClientId: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			discordClientSecret: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			summaryProxy: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			email: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			smtpSecure: {
+				type: 'boolean' as const,
+				optional: true as const, nullable: false as const
+			},
+			smtpHost: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			smtpPort: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			smtpUser: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			smtpPass: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			swPrivateKey: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			useObjectStorage: {
+				type: 'boolean' as const,
+				optional: true as const, nullable: false as const
+			},
+			objectStorageBaseUrl: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			objectStorageBucket: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			objectStoragePrefix: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			objectStorageEndpoint: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			objectStorageRegion: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			objectStoragePort: {
+				type: 'number' as const,
+				optional: true as const, nullable: true as const
+			},
+			objectStorageAccessKey: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			objectStorageSecretKey: {
+				type: 'string' as const,
+				optional: true as const, nullable: true as const
+			},
+			objectStorageUseSSL: {
+				type: 'boolean' as const,
+				optional: true as const, nullable: false as const
+			},
+			objectStorageUseProxy: {
+				type: 'boolean' as const,
+				optional: true as const, nullable: false as const
+			},
+			objectStorageSetPublicRead: {
+				type: 'boolean' as const,
+				optional: true as const, nullable: false as const
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const instance = await fetchMeta(true);
+
+	const emojis = await Emojis.find({
+		where: {
+			host: null
+		},
+		order: {
+			category: 'ASC',
+			name: 'ASC'
+		},
+		cache: {
+			id: 'meta_emojis',
+			milliseconds: 3600000	// 1 hour
+		}
+	});
+
+	const ads = await Ads.find({
+		where: {
+			expiresAt: MoreThan(new Date())
+		},
+	});
+
+	const response: any = {
+		maintainerName: instance.maintainerName,
+		maintainerEmail: instance.maintainerEmail,
+
+		version: config.version,
+
+		name: instance.name,
+		uri: config.url,
+		description: instance.description,
+		langs: instance.langs,
+		tosUrl: instance.ToSUrl,
+		repositoryUrl: instance.repositoryUrl,
+		feedbackUrl: instance.feedbackUrl,
+
+		secure: config.https != null,
+
+		disableRegistration: instance.disableRegistration,
+		disableLocalTimeline: instance.disableLocalTimeline,
+		disableGlobalTimeline: instance.disableGlobalTimeline,
+		driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
+		driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
+		emailRequiredForSignup: instance.emailRequiredForSignup,
+		enableHcaptcha: instance.enableHcaptcha,
+		hcaptchaSiteKey: instance.hcaptchaSiteKey,
+		enableRecaptcha: instance.enableRecaptcha,
+		recaptchaSiteKey: instance.recaptchaSiteKey,
+		swPublickey: instance.swPublicKey,
+		mascotImageUrl: instance.mascotImageUrl,
+		bannerUrl: instance.bannerUrl,
+		errorImageUrl: instance.errorImageUrl,
+		iconUrl: instance.iconUrl,
+		backgroundImageUrl: instance.backgroundImageUrl,
+		logoImageUrl: instance.logoImageUrl,
+		maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
+		emojis: await Emojis.packMany(emojis),
+		ads: ads.map(ad => ({
+			id: ad.id,
+			url: ad.url,
+			place: ad.place,
+			ratio: ad.ratio,
+			imageUrl: ad.imageUrl,
+		})),
+		enableEmail: instance.enableEmail,
+
+		enableTwitterIntegration: instance.enableTwitterIntegration,
+		enableGithubIntegration: instance.enableGithubIntegration,
+		enableDiscordIntegration: instance.enableDiscordIntegration,
+
+		enableServiceWorker: instance.enableServiceWorker,
+
+		translatorAvailable: instance.deeplAuthKey != null,
+
+		...(ps.detail ? {
+			pinnedPages: instance.pinnedPages,
+			pinnedClipId: instance.pinnedClipId,
+			cacheRemoteFiles: instance.cacheRemoteFiles,
+			proxyRemoteFiles: instance.proxyRemoteFiles,
+			requireSetup: (await Users.count({
+				host: null,
+			})) === 0,
+		} : {})
+	};
+
+	if (ps.detail) {
+		const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
+
+		response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
+		response.features = {
+			registration: !instance.disableRegistration,
+			localTimeLine: !instance.disableLocalTimeline,
+			globalTimeLine: !instance.disableGlobalTimeline,
+			emailRequiredForSignup: instance.emailRequiredForSignup,
+			elasticsearch: config.elasticsearch ? true : false,
+			hcaptcha: instance.enableHcaptcha,
+			recaptcha: instance.enableRecaptcha,
+			objectStorage: instance.useObjectStorage,
+			twitter: instance.enableTwitterIntegration,
+			github: instance.enableGithubIntegration,
+			discord: instance.enableDiscordIntegration,
+			serviceWorker: instance.enableServiceWorker,
+			miauth: true,
+		};
+
+		if (me && me.isAdmin) {
+			response.useStarForReactionFallback = instance.useStarForReactionFallback;
+			response.pinnedUsers = instance.pinnedUsers;
+			response.hiddenTags = instance.hiddenTags;
+			response.blockedHosts = instance.blockedHosts;
+			response.hcaptchaSecretKey = instance.hcaptchaSecretKey;
+			response.recaptchaSecretKey = instance.recaptchaSecretKey;
+			response.proxyAccountId = instance.proxyAccountId;
+			response.twitterConsumerKey = instance.twitterConsumerKey;
+			response.twitterConsumerSecret = instance.twitterConsumerSecret;
+			response.githubClientId = instance.githubClientId;
+			response.githubClientSecret = instance.githubClientSecret;
+			response.discordClientId = instance.discordClientId;
+			response.discordClientSecret = instance.discordClientSecret;
+			response.summalyProxy = instance.summalyProxy;
+			response.email = instance.email;
+			response.smtpSecure = instance.smtpSecure;
+			response.smtpHost = instance.smtpHost;
+			response.smtpPort = instance.smtpPort;
+			response.smtpUser = instance.smtpUser;
+			response.smtpPass = instance.smtpPass;
+			response.swPrivateKey = instance.swPrivateKey;
+			response.useObjectStorage = instance.useObjectStorage;
+			response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
+			response.objectStorageBucket = instance.objectStorageBucket;
+			response.objectStoragePrefix = instance.objectStoragePrefix;
+			response.objectStorageEndpoint = instance.objectStorageEndpoint;
+			response.objectStorageRegion = instance.objectStorageRegion;
+			response.objectStoragePort = instance.objectStoragePort;
+			response.objectStorageAccessKey = instance.objectStorageAccessKey;
+			response.objectStorageSecretKey = instance.objectStorageSecretKey;
+			response.objectStorageUseSSL = instance.objectStorageUseSSL;
+			response.objectStorageUseProxy = instance.objectStorageUseProxy;
+			response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
+			response.objectStorageS3ForcePathStyle = instance.objectStorageS3ForcePathStyle;
+			response.deeplAuthKey = instance.deeplAuthKey;
+			response.deeplIsPro = instance.deeplIsPro;
+		}
+	}
+
+	return response;
+});
diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts
new file mode 100644
index 0000000000..321fa42fc9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts
@@ -0,0 +1,72 @@
+import $ from 'cafy';
+import define from '../../define';
+import { AccessTokens } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { secureRndstr } from '@/misc/secure-rndstr';
+
+export const meta = {
+	tags: ['auth'],
+
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		session: {
+			validator: $.nullable.str
+		},
+
+		name: {
+			validator: $.nullable.optional.str
+		},
+
+		description: {
+			validator: $.nullable.optional.str,
+		},
+
+		iconUrl: {
+			validator: $.nullable.optional.str,
+		},
+
+		permission: {
+			validator: $.arr($.str).unique(),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			token: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Generate access token
+	const accessToken = secureRndstr(32, true);
+
+	const now = new Date();
+
+	// Insert access token doc
+	await AccessTokens.insert({
+		id: genId(),
+		createdAt: now,
+		lastUsedAt: now,
+		session: ps.session,
+		userId: user.id,
+		token: accessToken,
+		hash: accessToken,
+		name: ps.name,
+		description: ps.description,
+		iconUrl: ps.iconUrl,
+		permission: ps.permission,
+	});
+
+	return {
+		token: accessToken
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts
new file mode 100644
index 0000000000..3fc64d3eba
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/mute/create.ts
@@ -0,0 +1,83 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { genId } from '@/misc/gen-id';
+import { Mutings, NoteWatchings } from '@/models/index';
+import { Muting } from '@/models/entities/muting';
+import { publishUserEvent } from '@/services/stream';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:mutes',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '6fef56f3-e765-4957-88e5-c6f65329b8a5'
+		},
+
+		muteeIsYourself: {
+			message: 'Mutee is yourself.',
+			code: 'MUTEE_IS_YOURSELF',
+			id: 'a4619cb2-5f23-484b-9301-94c903074e10'
+		},
+
+		alreadyMuting: {
+			message: 'You are already muting that user.',
+			code: 'ALREADY_MUTING',
+			id: '7e7359cb-160c-4956-b08f-4d1c653cd007'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const muter = user;
+
+	// 自分自身
+	if (user.id === ps.userId) {
+		throw new ApiError(meta.errors.muteeIsYourself);
+	}
+
+	// Get mutee
+	const mutee = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Check if already muting
+	const exist = await Mutings.findOne({
+		muterId: muter.id,
+		muteeId: mutee.id
+	});
+
+	if (exist != null) {
+		throw new ApiError(meta.errors.alreadyMuting);
+	}
+
+	// Create mute
+	await Mutings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		muterId: muter.id,
+		muteeId: mutee.id,
+	} as Muting);
+
+	publishUserEvent(user.id, 'mute', mutee);
+
+	NoteWatchings.delete({
+		userId: muter.id,
+		noteUserId: mutee.id
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts
new file mode 100644
index 0000000000..3ffd1f4562
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/mute/delete.ts
@@ -0,0 +1,73 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { Mutings } from '@/models/index';
+import { publishUserEvent } from '@/services/stream';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:mutes',
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: 'b851d00b-8ab1-4a56-8b1b-e24187cb48ef'
+		},
+
+		muteeIsYourself: {
+			message: 'Mutee is yourself.',
+			code: 'MUTEE_IS_YOURSELF',
+			id: 'f428b029-6b39-4d48-a1d2-cc1ae6dd5cf9'
+		},
+
+		notMuting: {
+			message: 'You are not muting that user.',
+			code: 'NOT_MUTING',
+			id: '5467d020-daa9-4553-81e1-135c0c35a96d'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const muter = user;
+
+	// Check if the mutee is yourself
+	if (user.id === ps.userId) {
+		throw new ApiError(meta.errors.muteeIsYourself);
+	}
+
+	// Get mutee
+	const mutee = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Check not muting
+	const exist = await Mutings.findOne({
+		muterId: muter.id,
+		muteeId: mutee.id
+	});
+
+	if (exist == null) {
+		throw new ApiError(meta.errors.notMuting);
+	}
+
+	// Delete mute
+	await Mutings.delete({
+		id: exist.id
+	});
+
+	publishUserEvent(user.id, 'unmute', mutee);
+});
diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts
new file mode 100644
index 0000000000..ae4c3a719d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/mute/list.ts
@@ -0,0 +1,49 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Mutings } from '@/models/index';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:mutes',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 30
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Muting',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Mutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
+		.andWhere(`muting.muterId = :meId`, { meId: me.id });
+
+	const mutings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Mutings.packMany(mutings, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts
new file mode 100644
index 0000000000..d91562b62f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/my/apps.ts
@@ -0,0 +1,86 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Apps } from '@/models/index';
+
+export const meta = {
+	tags: ['account', 'app'],
+
+	requireCredential: true as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				id: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				},
+				name: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				},
+				callbackUrl: {
+					type: 'string' as const,
+					optional: false as const, nullable: false as const
+				},
+				permission: {
+					type: 'array' as const,
+					optional: false as const, nullable: false as const,
+					items: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const
+					}
+				},
+				secret: {
+					type: 'string' as const,
+					optional: true as const, nullable: false as const
+				},
+				isAuthorized: {
+					type: 'object' as const,
+					optional: true as const, nullable: false as const,
+					properties: {
+						appId: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const
+						},
+						userId: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const
+						}
+					}
+				}
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = {
+		userId: user.id
+	};
+
+	const apps = await Apps.find({
+		where: query,
+		take: ps.limit!,
+		skip: ps.offset,
+	});
+
+	return await Promise.all(apps.map(app => Apps.pack(app, user, {
+		detail: true
+	})));
+});
diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts
new file mode 100644
index 0000000000..a3f6e187f2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes.ts
@@ -0,0 +1,94 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../define';
+import { makePaginationQuery } from '../common/make-pagination-query';
+import { Notes } from '@/models/index';
+
+export const meta = {
+	tags: ['notes'],
+
+	params: {
+		local: {
+			validator: $.optional.bool,
+		},
+
+		reply: {
+			validator: $.optional.bool,
+		},
+
+		renote: {
+			validator: $.optional.bool,
+		},
+
+		withFiles: {
+			validator: $.optional.bool,
+		},
+
+		poll: {
+			validator: $.optional.bool,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+};
+
+export default define(meta, async (ps) => {
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.visibility = 'public'`)
+		.andWhere(`note.localOnly = FALSE`)
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	if (ps.local) {
+		query.andWhere('note.userHost IS NULL');
+	}
+
+	if (ps.reply != undefined) {
+		query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL');
+	}
+
+	if (ps.renote != undefined) {
+		query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL');
+	}
+
+	if (ps.withFiles != undefined) {
+		query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`);
+	}
+
+	if (ps.poll != undefined) {
+		query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
+	}
+
+	// TODO
+	//if (bot != undefined) {
+	//	query.isBot = bot;
+	//}
+
+	const notes = await query.take(ps.limit!).getMany();
+
+	return await Notes.packMany(notes);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts
new file mode 100644
index 0000000000..68881fda9e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/children.ts
@@ -0,0 +1,72 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { Brackets } from 'typeorm';
+import { Notes } from '@/models/index';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(new Brackets(qb => { qb
+			.where(`note.replyId = :noteId`, { noteId: ps.noteId })
+			.orWhere(new Brackets(qb => { qb
+				.where(`note.renoteId = :noteId`, { noteId: ps.noteId })
+				.andWhere(new Brackets(qb => { qb
+					.where(`note.text IS NOT NULL`)
+					.orWhere(`note.fileIds != '{}'`)
+					.orWhere(`note.hasPoll = TRUE`);
+				}));
+			}));
+		}))
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	generateVisibilityQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
+	if (user) generateBlockedUserQuery(query, user);
+
+	const notes = await query.take(ps.limit!).getMany();
+
+	return await Notes.packMany(notes, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts
new file mode 100644
index 0000000000..6b303d87ec
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/clips.ts
@@ -0,0 +1,55 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ClipNotes, Clips } from '@/models/index';
+import { getNote } from '../../common/getters';
+import { ApiError } from '../../error';
+import { In } from 'typeorm';
+
+export const meta = {
+	tags: ['clips', 'notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const clipNotes = await ClipNotes.find({
+		noteId: note.id,
+	});
+
+	const clips = await Clips.find({
+		id: In(clipNotes.map(x => x.clipId)),
+		isPublic: true
+	});
+
+	return await Promise.all(clips.map(x => Clips.pack(x)));
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts
new file mode 100644
index 0000000000..0fe323ea00
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts
@@ -0,0 +1,81 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getNote } from '../../common/getters';
+import { Note } from '@/models/entities/note';
+import { Notes } from '@/models/index';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'e1035875-9551-45ec-afa8-1ded1fcb53c8'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const conversation: Note[] = [];
+	let i = 0;
+
+	async function get(id: any) {
+		i++;
+		const p = await Notes.findOne(id);
+		if (p == null) return;
+
+		if (i > ps.offset!) {
+			conversation.push(p);
+		}
+
+		if (conversation.length == ps.limit!) {
+			return;
+		}
+
+		if (p.replyId) {
+			await get(p.replyId);
+		}
+	}
+
+	if (note.replyId) {
+		await get(note.replyId);
+	}
+
+	return await Notes.packMany(conversation, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
new file mode 100644
index 0000000000..751673f955
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -0,0 +1,299 @@
+import $ from 'cafy';
+import * as ms from 'ms';
+import { length } from 'stringz';
+import create from '@/services/note/create';
+import define from '../../define';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { ApiError } from '../../error';
+import { ID } from '@/misc/cafy-id';
+import { User } from '@/models/entities/user';
+import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index';
+import { DriveFile } from '@/models/entities/drive-file';
+import { Note } from '@/models/entities/note';
+import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
+import { noteVisibilities } from '../../../../types';
+import { Channel } from '@/models/entities/channel';
+
+let maxNoteTextLength = 500;
+
+setInterval(() => {
+	fetchMeta().then(m => {
+		maxNoteTextLength = m.maxNoteTextLength;
+	});
+}, 3000);
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300
+	},
+
+	kind: 'write:notes',
+
+	params: {
+		visibility: {
+			validator: $.optional.str.or(noteVisibilities as unknown as string[]),
+			default: 'public',
+		},
+
+		visibleUserIds: {
+			validator: $.optional.arr($.type(ID)).unique().min(0),
+		},
+
+		text: {
+			validator: $.optional.nullable.str.pipe(text =>
+				text.trim() != ''
+					&& length(text.trim()) <= maxNoteTextLength
+					&& Array.from(text.trim()).length <= DB_MAX_NOTE_TEXT_LENGTH	// DB limit
+			),
+			default: null,
+		},
+
+		cw: {
+			validator: $.optional.nullable.str.pipe(Notes.validateCw),
+		},
+
+		viaMobile: {
+			validator: $.optional.bool,
+			default: false,
+		},
+
+		localOnly: {
+			validator: $.optional.bool,
+			default: false,
+		},
+
+		noExtractMentions: {
+			validator: $.optional.bool,
+			default: false,
+		},
+
+		noExtractHashtags: {
+			validator: $.optional.bool,
+			default: false,
+		},
+
+		noExtractEmojis: {
+			validator: $.optional.bool,
+			default: false,
+		},
+
+		fileIds: {
+			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
+		},
+
+		mediaIds: {
+			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
+			deprecated: true,
+		},
+
+		replyId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		renoteId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		channelId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		poll: {
+			validator: $.optional.nullable.obj({
+				choices: $.arr($.str)
+					.unique()
+					.range(2, 10)
+					.each(c => c.length > 0 && c.length < 50),
+				multiple: $.optional.bool,
+				expiresAt: $.optional.nullable.num.int(),
+				expiredAfter: $.optional.nullable.num.int().min(1)
+			}).strict(),
+			ref: 'poll'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			createdNote: {
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				ref: 'Note',
+			}
+		}
+	},
+
+	errors: {
+		noSuchRenoteTarget: {
+			message: 'No such renote target.',
+			code: 'NO_SUCH_RENOTE_TARGET',
+			id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4'
+		},
+
+		cannotReRenote: {
+			message: 'You can not Renote a pure Renote.',
+			code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE',
+			id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a'
+		},
+
+		noSuchReplyTarget: {
+			message: 'No such reply target.',
+			code: 'NO_SUCH_REPLY_TARGET',
+			id: '749ee0f6-d3da-459a-bf02-282e2da4292c'
+		},
+
+		cannotReplyToPureRenote: {
+			message: 'You can not reply to a pure Renote.',
+			code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
+			id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15'
+		},
+
+		contentRequired: {
+			message: 'Content required. You need to set text, fileIds, renoteId or poll.',
+			code: 'CONTENT_REQUIRED',
+			id: '6f57e42b-c348-439b-bc45-993995cc515a'
+		},
+
+		cannotCreateAlreadyExpiredPoll: {
+			message: 'Poll is already expired.',
+			code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
+			id: '04da457d-b083-4055-9082-955525eda5a5'
+		},
+
+		noSuchChannel: {
+			message: 'No such channel.',
+			code: 'NO_SUCH_CHANNEL',
+			id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb'
+		},
+
+		youHaveBeenBlocked: {
+			message: 'You have been blocked by this user.',
+			code: 'YOU_HAVE_BEEN_BLOCKED',
+			id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	let visibleUsers: User[] = [];
+	if (ps.visibleUserIds) {
+		visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id))))
+			.filter(x => x != null) as User[];
+	}
+
+	let files: DriveFile[] = [];
+	const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
+	if (fileIds != null) {
+		files = (await Promise.all(fileIds.map(fileId =>
+			DriveFiles.findOne({
+				id: fileId,
+				userId: user.id
+			})
+		))).filter(file => file != null) as DriveFile[];
+	}
+
+	let renote: Note | undefined;
+	if (ps.renoteId != null) {
+		// Fetch renote to note
+		renote = await Notes.findOne(ps.renoteId);
+
+		if (renote == null) {
+			throw new ApiError(meta.errors.noSuchRenoteTarget);
+		} else if (renote.renoteId && !renote.text && !renote.fileIds) {
+			throw new ApiError(meta.errors.cannotReRenote);
+		}
+
+		// Check blocking
+		if (renote.userId !== user.id) {
+			const block = await Blockings.findOne({
+				blockerId: renote.userId,
+				blockeeId: user.id,
+			});
+			if (block) {
+				throw new ApiError(meta.errors.youHaveBeenBlocked);
+			}
+		}
+	}
+
+	let reply: Note | undefined;
+	if (ps.replyId != null) {
+		// Fetch reply
+		reply = await Notes.findOne(ps.replyId);
+
+		if (reply == null) {
+			throw new ApiError(meta.errors.noSuchReplyTarget);
+		}
+
+		// 返信対象が引用でないRenoteだったらエラー
+		if (reply.renoteId && !reply.text && !reply.fileIds) {
+			throw new ApiError(meta.errors.cannotReplyToPureRenote);
+		}
+
+		// Check blocking
+		if (reply.userId !== user.id) {
+			const block = await Blockings.findOne({
+				blockerId: reply.userId,
+				blockeeId: user.id,
+			});
+			if (block) {
+				throw new ApiError(meta.errors.youHaveBeenBlocked);
+			}
+		}
+	}
+
+	if (ps.poll) {
+		if (typeof ps.poll.expiresAt === 'number') {
+			if (ps.poll.expiresAt < Date.now())
+				throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
+		} else if (typeof ps.poll.expiredAfter === 'number') {
+			ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
+		}
+	}
+
+	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
+	if (!(ps.text || files.length || renote || ps.poll)) {
+		throw new ApiError(meta.errors.contentRequired);
+	}
+
+	let channel: Channel | undefined;
+	if (ps.channelId != null) {
+		channel = await Channels.findOne(ps.channelId);
+
+		if (channel == null) {
+			throw new ApiError(meta.errors.noSuchChannel);
+		}
+	}
+
+	// 投稿を作成
+	const note = await create(user, {
+		createdAt: new Date(),
+		files: files,
+		poll: ps.poll ? {
+			choices: ps.poll.choices,
+			multiple: ps.poll.multiple || false,
+			expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null
+		} : undefined,
+		text: ps.text || undefined,
+		reply,
+		renote,
+		cw: ps.cw,
+		viaMobile: ps.viaMobile,
+		localOnly: ps.localOnly,
+		visibility: ps.visibility,
+		visibleUsers,
+		channel,
+		apMentions: ps.noExtractMentions ? [] : undefined,
+		apHashtags: ps.noExtractHashtags ? [] : undefined,
+		apEmojis: ps.noExtractEmojis ? [] : undefined,
+	});
+
+	return {
+		createdNote: await Notes.pack(note, user)
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts
new file mode 100644
index 0000000000..7163a2b9d2
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/delete.ts
@@ -0,0 +1,56 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import deleteNote from '@/services/note/delete';
+import define from '../../define';
+import * as ms from 'ms';
+import { getNote } from '../../common/getters';
+import { ApiError } from '../../error';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:notes',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300,
+		minInterval: ms('1sec')
+	},
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '490be23f-8c1f-4796-819f-94cb4f9d1630'
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: 'fe8d7103-0ea8-4ec3-814d-f8b401dc69e9'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) {
+		throw new ApiError(meta.errors.accessDenied);
+	}
+
+	// この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
+	await deleteNote(await Users.findOneOrFail(note.userId), note);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
new file mode 100644
index 0000000000..1bb25edd7f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
@@ -0,0 +1,61 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getNote } from '../../../common/getters';
+import { NoteFavorites } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['notes', 'favorites'],
+
+	requireCredential: true as const,
+
+	kind: 'write:favorites',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '6dd26674-e060-4816-909a-45ba3f4da458'
+		},
+
+		alreadyFavorited: {
+			message: 'The note has already been marked as a favorite.',
+			code: 'ALREADY_FAVORITED',
+			id: 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Get favoritee
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	// if already favorited
+	const exist = await NoteFavorites.findOne({
+		noteId: note.id,
+		userId: user.id
+	});
+
+	if (exist != null) {
+		throw new ApiError(meta.errors.alreadyFavorited);
+	}
+
+	// Create favorite
+	await NoteFavorites.insert({
+		id: genId(),
+		createdAt: new Date(),
+		noteId: note.id,
+		userId: user.id
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
new file mode 100644
index 0000000000..75eb9a359a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
@@ -0,0 +1,55 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getNote } from '../../../common/getters';
+import { NoteFavorites } from '@/models/index';
+
+export const meta = {
+	tags: ['notes', 'favorites'],
+
+	requireCredential: true as const,
+
+	kind: 'write:favorites',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '80848a2c-398f-4343-baa9-df1d57696c56'
+		},
+
+		notFavorited: {
+			message: 'You have not marked that note a favorite.',
+			code: 'NOT_FAVORITED',
+			id: 'b625fc69-635e-45e9-86f4-dbefbef35af5'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Get favoritee
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	// if already favorited
+	const exist = await NoteFavorites.findOne({
+		noteId: note.id,
+		userId: user.id
+	});
+
+	if (exist == null) {
+		throw new ApiError(meta.errors.notFavorited);
+	}
+
+	// Delete favorite
+	await NoteFavorites.delete(exist.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
new file mode 100644
index 0000000000..8d33c0e73d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -0,0 +1,64 @@
+import $ from 'cafy';
+import define from '../../define';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { Notes } from '@/models/index';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const max = 30;
+	const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
+
+	const query = Notes.createQueryBuilder('note')
+		.addSelect('note.score')
+		.where('note.userHost IS NULL')
+		.andWhere(`note.score > 0`)
+		.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
+		.andWhere(`note.visibility = 'public'`)
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	if (user) generateMutedUserQuery(query, user);
+	if (user) generateBlockedUserQuery(query, user);
+
+	let notes = await query
+		.orderBy('note.score', 'DESC')
+		.take(max)
+		.getMany();
+
+	notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
+
+	notes = notes.slice(ps.offset, ps.offset + ps.limit);
+
+	return await Notes.packMany(notes, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
new file mode 100644
index 0000000000..5902c0415c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -0,0 +1,101 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { ApiError } from '../../error';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '@/models/index';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { activeUsersChart } from '@/services/chart/index';
+import { generateRepliesQuery } from '../../common/generate-replies-query';
+import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	params: {
+		withFiles: {
+			validator: $.optional.bool,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		sinceDate: {
+			validator: $.optional.num
+		},
+
+		untilDate: {
+			validator: $.optional.num
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		gtlDisabled: {
+			message: 'Global timeline has been disabled.',
+			code: 'GTL_DISABLED',
+			id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const m = await fetchMeta();
+	if (m.disableGlobalTimeline) {
+		if (user == null || (!user.isAdmin && !user.isModerator)) {
+			throw new ApiError(meta.errors.gtlDisabled);
+		}
+	}
+
+	//#region Construct query
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere('note.visibility = \'public\'')
+		.andWhere('note.channelId IS NULL')
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	generateRepliesQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
+	if (user) generateMutedNoteQuery(query, user);
+	if (user) generateBlockedUserQuery(query, user);
+
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
+	}
+	//#endregion
+
+	const timeline = await query.take(ps.limit!).getMany();
+
+	process.nextTick(() => {
+		if (user) {
+			activeUsersChart.update(user);
+		}
+	});
+
+	return await Notes.packMany(timeline, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
new file mode 100644
index 0000000000..47f08f208b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -0,0 +1,158 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { ApiError } from '../../error';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Followings, Notes } from '@/models/index';
+import { Brackets } from 'typeorm';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { activeUsersChart } from '@/services/chart/index';
+import { generateRepliesQuery } from '../../common/generate-replies-query';
+import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
+import { generateChannelQuery } from '../../common/generate-channel-query';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		sinceDate: {
+			validator: $.optional.num,
+		},
+
+		untilDate: {
+			validator: $.optional.num,
+		},
+
+		includeMyRenotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		includeRenotedMyNotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		includeLocalRenotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		withFiles: {
+			validator: $.optional.bool,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		stlDisabled: {
+			message: 'Hybrid timeline has been disabled.',
+			code: 'STL_DISABLED',
+			id: '620763f4-f621-4533-ab33-0577a1a3c342'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const m = await fetchMeta();
+	if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
+		throw new ApiError(meta.errors.stlDisabled);
+	}
+
+	//#region Construct query
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: user.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere(new Brackets(qb => {
+			qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id })
+				.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
+		}))
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
+		.setParameters(followingQuery.getParameters());
+
+	generateChannelQuery(query, user);
+	generateRepliesQuery(query, user);
+	generateVisibilityQuery(query, user);
+	generateMutedUserQuery(query, user);
+	generateMutedNoteQuery(query, user);
+	generateBlockedUserQuery(query, user);
+
+	if (ps.includeMyRenotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.userId != :meId', { meId: user.id });
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.includeRenotedMyNotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.includeLocalRenotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.renoteUserHost IS NOT NULL');
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
+	}
+	//#endregion
+
+	const timeline = await query.take(ps.limit!).getMany();
+
+	process.nextTick(() => {
+		if (user) {
+			activeUsersChart.update(user);
+		}
+	});
+
+	return await Notes.packMany(timeline, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
new file mode 100644
index 0000000000..f670d478bf
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -0,0 +1,129 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { ApiError } from '../../error';
+import { Notes } from '@/models/index';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { activeUsersChart } from '@/services/chart/index';
+import { Brackets } from 'typeorm';
+import { generateRepliesQuery } from '../../common/generate-replies-query';
+import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
+import { generateChannelQuery } from '../../common/generate-channel-query';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	params: {
+		withFiles: {
+			validator: $.optional.bool,
+		},
+
+		fileType: {
+			validator: $.optional.arr($.str),
+		},
+
+		excludeNsfw: {
+			validator: $.optional.bool,
+			default: false,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		sinceDate: {
+			validator: $.optional.num,
+		},
+
+		untilDate: {
+			validator: $.optional.num,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		ltlDisabled: {
+			message: 'Local timeline has been disabled.',
+			code: 'LTL_DISABLED',
+			id: '45a6eb02-7695-4393-b023-dd3be9aaaefd'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const m = await fetchMeta();
+	if (m.disableLocalTimeline) {
+		if (user == null || (!user.isAdmin && !user.isModerator)) {
+			throw new ApiError(meta.errors.ltlDisabled);
+		}
+	}
+
+	//#region Construct query
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	generateChannelQuery(query, user);
+	generateRepliesQuery(query, user);
+	generateVisibilityQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
+	if (user) generateMutedNoteQuery(query, user);
+	if (user) generateBlockedUserQuery(query, user);
+
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
+	}
+
+	if (ps.fileType != null) {
+		query.andWhere('note.fileIds != \'{}\'');
+		query.andWhere(new Brackets(qb => {
+			for (const type of ps.fileType!) {
+				const i = ps.fileType!.indexOf(type);
+				qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+			}
+		}));
+
+		if (ps.excludeNsfw) {
+			query.andWhere('note.cw IS NULL');
+			query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
+		}
+	}
+	//#endregion
+
+	const timeline = await query.take(ps.limit!).getMany();
+
+	process.nextTick(() => {
+		if (user) {
+			activeUsersChart.update(user);
+		}
+	});
+
+	return await Notes.packMany(timeline, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts
new file mode 100644
index 0000000000..ffaebd6c95
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts
@@ -0,0 +1,88 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import read from '@/services/note/read';
+import { Notes, Followings } from '@/models/index';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Brackets } from 'typeorm';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	params: {
+		following: {
+			validator: $.optional.bool,
+			default: false
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		visibility: {
+			validator: $.optional.str,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: user.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(new Brackets(qb => { qb
+			.where(`'{"${user.id}"}' <@ note.mentions`)
+			.orWhere(`'{"${user.id}"}' <@ note.visibleUserIds`);
+		}))
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	generateVisibilityQuery(query, user);
+	generateMutedUserQuery(query, user);
+	generateMutedNoteThreadQuery(query, user);
+	generateBlockedUserQuery(query, user);
+
+	if (ps.visibility) {
+		query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
+	}
+
+	if (ps.following) {
+		query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id });
+		query.setParameters(followingQuery.getParameters());
+	}
+
+	const mentions = await query.take(ps.limit!).getMany();
+
+	read(user.id, mentions);
+
+	return await Notes.packMany(mentions, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
new file mode 100644
index 0000000000..0763f0c8fd
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -0,0 +1,77 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { Polls, Mutings, Notes, PollVotes } from '@/models/index';
+import { Brackets, In } from 'typeorm';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = Polls.createQueryBuilder('poll')
+		.where('poll.userHost IS NULL')
+		.andWhere(`poll.userId != :meId`, { meId: user.id })
+		.andWhere(`poll.noteVisibility = 'public'`)
+		.andWhere(new Brackets(qb => { qb
+			.where('poll.expiresAt IS NULL')
+			.orWhere('poll.expiresAt > :now', { now: new Date() });
+		}));
+
+	//#region exclude arleady voted polls
+	const votedQuery = PollVotes.createQueryBuilder('vote')
+		.select('vote.noteId')
+		.where('vote.userId = :meId', { meId: user.id });
+
+	query
+		.andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`);
+
+	query.setParameters(votedQuery.getParameters());
+	//#endregion
+
+	//#region mute
+	const mutingQuery = Mutings.createQueryBuilder('muting')
+		.select('muting.muteeId')
+		.where('muting.muterId = :muterId', { muterId: user.id });
+
+	query
+		.andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`);
+
+	query.setParameters(mutingQuery.getParameters());
+	//#endregion
+
+	const polls = await query.take(ps.limit!).skip(ps.offset).getMany();
+
+	if (polls.length === 0) return [];
+
+	const notes = await Notes.find({
+		id: In(polls.map(poll => poll.noteId))
+	});
+
+	return await Notes.packMany(notes, user, {
+		detail: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
new file mode 100644
index 0000000000..f670501385
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
@@ -0,0 +1,170 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { publishNoteStream } from '@/services/stream';
+import { createNotification } from '@/services/create-notification';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getNote } from '../../../common/getters';
+import { deliver } from '@/queue/index';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderVote from '@/remote/activitypub/renderer/vote';
+import { deliverQuestionUpdate } from '@/services/note/polls/update';
+import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index';
+import { Not } from 'typeorm';
+import { IRemoteUser } from '@/models/entities/user';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:votes',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+
+		choice: {
+			validator: $.num
+		},
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'ecafbd2e-c283-4d6d-aecb-1a0a33b75396'
+		},
+
+		noPoll: {
+			message: 'The note does not attach a poll.',
+			code: 'NO_POLL',
+			id: '5f979967-52d9-4314-a911-1c673727f92f'
+		},
+
+		invalidChoice: {
+			message: 'Choice ID is invalid.',
+			code: 'INVALID_CHOICE',
+			id: 'e0cc9a04-f2e8-41e4-a5f1-4127293260cc'
+		},
+
+		alreadyVoted: {
+			message: 'You have already voted.',
+			code: 'ALREADY_VOTED',
+			id: '0963fc77-efac-419b-9424-b391608dc6d8'
+		},
+
+		alreadyExpired: {
+			message: 'The poll is already expired.',
+			code: 'ALREADY_EXPIRED',
+			id: '1022a357-b085-4054-9083-8f8de358337e'
+		},
+
+		youHaveBeenBlocked: {
+			message: 'You cannot vote this poll because you have been blocked by this user.',
+			code: 'YOU_HAVE_BEEN_BLOCKED',
+			id: '85a5377e-b1e9-4617-b0b9-5bea73331e49'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const createdAt = new Date();
+
+	// Get votee
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	if (!note.hasPoll) {
+		throw new ApiError(meta.errors.noPoll);
+	}
+
+	// Check blocking
+	if (note.userId !== user.id) {
+		const block = await Blockings.findOne({
+			blockerId: note.userId,
+			blockeeId: user.id,
+		});
+		if (block) {
+			throw new ApiError(meta.errors.youHaveBeenBlocked);
+		}
+	}
+
+	const poll = await Polls.findOneOrFail({ noteId: note.id });
+
+	if (poll.expiresAt && poll.expiresAt < createdAt) {
+		throw new ApiError(meta.errors.alreadyExpired);
+	}
+
+	if (poll.choices[ps.choice] == null) {
+		throw new ApiError(meta.errors.invalidChoice);
+	}
+
+	// if already voted
+	const exist = await PollVotes.find({
+		noteId: note.id,
+		userId: user.id
+	});
+
+	if (exist.length) {
+		if (poll.multiple) {
+			if (exist.some(x => x.choice == ps.choice))
+				throw new ApiError(meta.errors.alreadyVoted);
+		} else {
+			throw new ApiError(meta.errors.alreadyVoted);
+		}
+	}
+
+	// Create vote
+	const vote = await PollVotes.insert({
+		id: genId(),
+		createdAt,
+		noteId: note.id,
+		userId: user.id,
+		choice: ps.choice
+	}).then(x => PollVotes.findOneOrFail(x.identifiers[0]));
+
+	// Increment votes count
+	const index = ps.choice + 1; // In SQL, array index is 1 based
+	await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
+
+	publishNoteStream(note.id, 'pollVoted', {
+		choice: ps.choice,
+		userId: user.id
+	});
+
+	// Notify
+	createNotification(note.userId, 'pollVote', {
+		notifierId: user.id,
+		noteId: note.id,
+		choice: ps.choice
+	});
+
+	// Fetch watchers
+	NoteWatchings.find({
+		noteId: note.id,
+		userId: Not(user.id),
+	}).then(watchers => {
+		for (const watcher of watchers) {
+			createNotification(watcher.userId, 'pollVote', {
+				notifierId: user.id,
+				noteId: note.id,
+				choice: ps.choice
+			});
+		}
+	});
+
+	// リモート投票の場合リプライ送信
+	if (note.userHost != null) {
+		const pollOwner = await Users.findOneOrFail(note.userId) as IRemoteUser;
+
+		deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
+	}
+
+	// リモートフォロワーにUpdate配信
+	deliverQuestionUpdate(note.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts
new file mode 100644
index 0000000000..09dd6b600b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts
@@ -0,0 +1,90 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { getNote } from '../../common/getters';
+import { ApiError } from '../../error';
+import { NoteReactions } from '@/models/index';
+import { DeepPartial } from 'typeorm';
+import { NoteReaction } from '@/models/entities/note-reaction';
+
+export const meta = {
+	tags: ['notes', 'reactions'],
+
+	requireCredential: false as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+
+		type: {
+			validator: $.optional.nullable.str,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.optional.num,
+			default: 0
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'NoteReaction',
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '263fff3d-d0e1-4af4-bea7-8408059b451a'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const query = {
+		noteId: note.id
+	} as DeepPartial<NoteReaction>;
+
+	if (ps.type) {
+		// ローカルリアクションはホスト名が . とされているが
+		// DB 上ではそうではないので、必要に応じて変換
+		const suffix = '@.:';
+		const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type;
+		query.reaction = type;
+	}
+
+	const reactions = await NoteReactions.find({
+		where: query,
+		take: ps.limit!,
+		skip: ps.offset,
+		order: {
+			id: -1
+		}
+	});
+
+	return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user)));
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
new file mode 100644
index 0000000000..24a73a8d4f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
@@ -0,0 +1,57 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import createReaction from '@/services/note/reaction/create';
+import define from '../../../define';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['reactions', 'notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:reactions',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+
+		reaction: {
+			validator: $.str,
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '033d0620-5bfe-4027-965d-980b0c85a3ea'
+		},
+
+		alreadyReacted: {
+			message: 'You are already reacting to that note.',
+			code: 'ALREADY_REACTED',
+			id: '71efcf98-86d6-4e2b-b2ad-9d032369366b'
+		},
+
+		youHaveBeenBlocked: {
+			message: 'You cannot react this note because you have been blocked by this user.',
+			code: 'YOU_HAVE_BEEN_BLOCKED',
+			id: '20ef5475-9f38-4e4c-bd33-de6d979498ec'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+	await createReaction(user, note, ps.reaction).catch(e => {
+		if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
+		if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
+		throw e;
+	});
+	return;
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
new file mode 100644
index 0000000000..69550f96de
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts
@@ -0,0 +1,52 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import * as ms from 'ms';
+import deleteReaction from '@/services/note/reaction/delete';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['reactions', 'notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:reactions',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 60,
+		minInterval: ms('3sec')
+	},
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '764d9fce-f9f2-4a0e-92b1-6ceac9a7ad37'
+		},
+
+		notReacted: {
+			message: 'You are not reacting to that note.',
+			code: 'NOT_REACTED',
+			id: '92f4426d-4196-4125-aa5b-02943e2ec8fc'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+	await deleteReaction(user, note).catch(e => {
+		if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted);
+		throw e;
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
new file mode 100644
index 0000000000..26bfc1657d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -0,0 +1,76 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { getNote } from '../../common/getters';
+import { ApiError } from '../../error';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '@/models/index';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '12908022-2e21-46cd-ba6a-3edaf6093f46'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	generateVisibilityQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
+	if (user) generateBlockedUserQuery(query, user);
+
+	const renotes = await query.take(ps.limit!).getMany();
+
+	return await Notes.packMany(renotes, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts
new file mode 100644
index 0000000000..0bb62413ae
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/replies.ts
@@ -0,0 +1,61 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Notes } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	generateVisibilityQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
+	if (user) generateBlockedUserQuery(query, user);
+
+	const timeline = await query.take(ps.limit!).getMany();
+
+	return await Notes.packMany(timeline, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
new file mode 100644
index 0000000000..40e1499736
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -0,0 +1,134 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes } from '@/models/index';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { Brackets } from 'typeorm';
+import { safeForSql } from '@/misc/safe-for-sql';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes', 'hashtags'],
+
+	params: {
+		tag: {
+			validator: $.optional.str,
+		},
+
+		query: {
+			validator: $.optional.arr($.arr($.str)),
+		},
+
+		reply: {
+			validator: $.optional.nullable.bool,
+			default: null,
+		},
+
+		renote: {
+			validator: $.optional.nullable.bool,
+			default: null,
+		},
+
+		withFiles: {
+			validator: $.optional.bool,
+		},
+
+		poll: {
+			validator: $.optional.nullable.bool,
+			default: null,
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	generateVisibilityQuery(query, me);
+	if (me) generateMutedUserQuery(query, me);
+	if (me) generateBlockedUserQuery(query, me);
+
+	try {
+		if (ps.tag) {
+			if (!safeForSql(ps.tag)) throw 'Injection';
+			query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
+		} else {
+			query.andWhere(new Brackets(qb => {
+				for (const tags of ps.query!) {
+					qb.orWhere(new Brackets(qb => {
+						for (const tag of tags) {
+							if (!safeForSql(tag)) throw 'Injection';
+							qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
+						}
+					}));
+				}
+			}));
+		}
+	} catch (e) {
+		if (e === 'Injection') return [];
+		throw e;
+	}
+
+	if (ps.reply != null) {
+		if (ps.reply) {
+			query.andWhere('note.replyId IS NOT NULL');
+		} else {
+			query.andWhere('note.replyId IS NULL');
+		}
+	}
+
+	if (ps.renote != null) {
+		if (ps.renote) {
+			query.andWhere('note.renoteId IS NOT NULL');
+		} else {
+			query.andWhere('note.renoteId IS NULL');
+		}
+	}
+
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
+	}
+
+	if (ps.poll != null) {
+		if (ps.poll) {
+			query.andWhere('note.hasPoll = TRUE');
+		} else {
+			query.andWhere('note.hasPoll = FALSE');
+		}
+	}
+
+	// Search notes
+	const notes = await query.take(ps.limit!).getMany();
+
+	return await Notes.packMany(notes, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
new file mode 100644
index 0000000000..eb832a6b31
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -0,0 +1,152 @@
+import $ from 'cafy';
+import es from '../../../../db/elasticsearch';
+import define from '../../define';
+import { Notes } from '@/models/index';
+import { In } from 'typeorm';
+import { ID } from '@/misc/cafy-id';
+import config from '@/config/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		query: {
+			validator: $.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		host: {
+			validator: $.optional.nullable.str,
+			default: undefined
+		},
+
+		userId: {
+			validator: $.optional.nullable.type(ID),
+			default: null
+		},
+
+		channelId: {
+			validator: $.optional.nullable.type(ID),
+			default: null
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	if (es == null) {
+		const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId);
+
+		if (ps.userId) {
+			query.andWhere('note.userId = :userId', { userId: ps.userId });
+		} else if (ps.channelId) {
+			query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
+		}
+
+		query
+			.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
+			.innerJoinAndSelect('note.user', 'user')
+			.leftJoinAndSelect('note.reply', 'reply')
+			.leftJoinAndSelect('note.renote', 'renote')
+			.leftJoinAndSelect('reply.user', 'replyUser')
+			.leftJoinAndSelect('renote.user', 'renoteUser');
+
+		generateVisibilityQuery(query, me);
+		if (me) generateMutedUserQuery(query, me);
+		if (me) generateBlockedUserQuery(query, me);
+
+		const notes = await query.take(ps.limit!).getMany();
+
+		return await Notes.packMany(notes, me);
+	} else {
+		const userQuery = ps.userId != null ? [{
+			term: {
+				userId: ps.userId
+			}
+		}] : [];
+
+		const hostQuery = ps.userId == null ?
+			ps.host === null ? [{
+				bool: {
+					must_not: {
+						exists: {
+							field: 'userHost'
+						}
+					}
+				}
+			}] : ps.host !== undefined ? [{
+				term: {
+					userHost: ps.host
+				}
+			}] : []
+		: [];
+
+		const result = await es.search({
+			index: config.elasticsearch.index || 'misskey_note',
+			body: {
+				size: ps.limit!,
+				from: ps.offset,
+				query: {
+					bool: {
+						must: [{
+							simple_query_string: {
+								fields: ['text'],
+								query: ps.query.toLowerCase(),
+								default_operator: 'and'
+							},
+						}, ...hostQuery, ...userQuery]
+					}
+				},
+				sort: [{
+					_doc: 'desc'
+				}]
+			}
+		});
+
+		const hits = result.body.hits.hits.map((hit: any) => hit._id);
+
+		if (hits.length === 0) return [];
+
+		// Fetch found notes
+		const notes = await Notes.find({
+			where: {
+				id: In(hits)
+			},
+			order: {
+				id: -1
+			}
+		});
+
+		return await Notes.packMany(notes, me);
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts
new file mode 100644
index 0000000000..fad63d6483
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/show.ts
@@ -0,0 +1,43 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { getNote } from '../../common/getters';
+import { ApiError } from '../../error';
+import { Notes } from '@/models/index';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Note',
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	return await Notes.pack(note, user, {
+		detail: true
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts
new file mode 100644
index 0000000000..b3913a5e79
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/state.ts
@@ -0,0 +1,69 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			isFavorited: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			isWatching: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+			isMutedThread: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const
+			},
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await Notes.findOneOrFail(ps.noteId);
+
+	const [favorite, watching, threadMuting] = await Promise.all([
+		NoteFavorites.count({
+			where: {
+				userId: user.id,
+				noteId: note.id,
+			},
+			take: 1
+		}),
+		NoteWatchings.count({
+			where: {
+				userId: user.id,
+				noteId: note.id,
+			},
+			take: 1
+		}),
+		NoteThreadMutings.count({
+			where: {
+				userId: user.id,
+				threadId: note.threadId || note.id,
+			},
+			take: 1
+		}),
+	]);
+
+	return {
+		isFavorited: favorite !== 0,
+		isWatching: watching !== 0,
+		isMutedThread: threadMuting !== 0,
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
new file mode 100644
index 0000000000..2010d54331
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts
@@ -0,0 +1,54 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+import { Notes, NoteThreadMutings } from '@/models';
+import { genId } from '@/misc/gen-id';
+import readNote from '@/services/note/read';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const mutedNotes = await Notes.find({
+		where: [{
+			id: note.threadId || note.id,
+		}, {
+			threadId: note.threadId || note.id,
+		}],
+	});
+
+	await readNote(user.id, mutedNotes);
+
+	await NoteThreadMutings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		threadId: note.threadId || note.id,
+		userId: user.id,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
new file mode 100644
index 0000000000..05d5691870
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+import { NoteThreadMutings } from '@/models';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	await NoteThreadMutings.delete({
+		threadId: note.threadId || note.id,
+		userId: user.id,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
new file mode 100644
index 0000000000..1bd0e57d34
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -0,0 +1,150 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { Notes, Followings } from '@/models/index';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { activeUsersChart } from '@/services/chart/index';
+import { Brackets } from 'typeorm';
+import { generateRepliesQuery } from '../../common/generate-replies-query';
+import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
+import { generateChannelQuery } from '../../common/generate-channel-query';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		sinceDate: {
+			validator: $.optional.num,
+		},
+
+		untilDate: {
+			validator: $.optional.num,
+		},
+
+		includeMyRenotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		includeRenotedMyNotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		includeLocalRenotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		withFiles: {
+			validator: $.optional.bool,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const hasFollowing = (await Followings.count({
+		where: {
+			followerId: user.id,
+		},
+		take: 1
+	})) !== 0;
+
+	//#region Construct query
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: user.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere(new Brackets(qb => { qb
+			.where('note.userId = :meId', { meId: user.id });
+			if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
+		}))
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
+		.setParameters(followingQuery.getParameters());
+
+	generateChannelQuery(query, user);
+	generateRepliesQuery(query, user);
+	generateVisibilityQuery(query, user);
+	generateMutedUserQuery(query, user);
+	generateMutedNoteQuery(query, user);
+	generateBlockedUserQuery(query, user);
+
+	if (ps.includeMyRenotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.userId != :meId', { meId: user.id });
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.includeRenotedMyNotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.includeLocalRenotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.renoteUserHost IS NOT NULL');
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
+	}
+	//#endregion
+
+	const timeline = await query.take(ps.limit!).getMany();
+
+	process.nextTick(() => {
+		if (user) {
+			activeUsersChart.update(user);
+		}
+	});
+
+	return await Notes.packMany(timeline, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
new file mode 100644
index 0000000000..b56b1debdd
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -0,0 +1,89 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { getNote } from '../../common/getters';
+import { ApiError } from '../../error';
+import fetch from 'node-fetch';
+import config from '@/config/index';
+import { getAgentByUrl } from '@/misc/fetch';
+import { URLSearchParams } from 'url';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Notes } from '@/models';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: false as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		},
+		targetLang: {
+			validator: $.str,
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'bea9b03f-36e0-49c5-a4db-627a029f8971'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) {
+		return 204; // TODO: 良い感じのエラー返す
+	}
+
+	if (note.text == null) {
+		return 204;
+	}
+
+	const instance = await fetchMeta();
+
+	if (instance.deeplAuthKey == null) {
+		return 204; // TODO: 良い感じのエラー返す
+	}
+
+	let targetLang = ps.targetLang;
+	if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
+
+	const params = new URLSearchParams();
+	params.append('auth_key', instance.deeplAuthKey);
+	params.append('text', note.text);
+	params.append('target_lang', targetLang);
+
+	const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
+
+	const res = await fetch(endpoint, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/x-www-form-urlencoded',
+			'User-Agent': config.userAgent,
+			Accept: 'application/json, */*'
+		},
+		body: params,
+		timeout: 10000,
+		agent: getAgentByUrl,
+	});
+
+	const json = await res.json();
+
+	return {
+		sourceLang: json.translations[0].detected_source_language,
+		text: json.translations[0].text
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts
new file mode 100644
index 0000000000..dce43d9d9c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts
@@ -0,0 +1,52 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import deleteNote from '@/services/note/delete';
+import define from '../../define';
+import * as ms from 'ms';
+import { getNote } from '../../common/getters';
+import { ApiError } from '../../error';
+import { Notes, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:notes',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300,
+		minInterval: ms('1sec')
+	},
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'efd4a259-2442-496b-8dd7-b255aa1a160f'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const renotes = await Notes.find({
+		userId: user.id,
+		renoteId: note.id
+	});
+
+	for (const note of renotes) {
+		deleteNote(await Users.findOneOrFail(user.id), note);
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
new file mode 100644
index 0000000000..32c370004c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -0,0 +1,147 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { UserLists, UserListJoinings, Notes } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { activeUsersChart } from '@/services/chart/index';
+import { Brackets } from 'typeorm';
+
+export const meta = {
+	tags: ['notes', 'lists'],
+
+	requireCredential: true as const,
+
+	params: {
+		listId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		sinceDate: {
+			validator: $.optional.num,
+		},
+
+		untilDate: {
+			validator: $.optional.num,
+		},
+
+		includeMyRenotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		includeRenotedMyNotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		includeLocalRenotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		withFiles: {
+			validator: $.optional.bool,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		noSuchList: {
+			message: 'No such list.',
+			code: 'NO_SUCH_LIST',
+			id: '8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const list = await UserLists.findOne({
+		id: ps.listId,
+		userId: user.id
+	});
+
+	if (list == null) {
+		throw new ApiError(meta.errors.noSuchList);
+	}
+
+	//#region Construct query
+	const listQuery = UserListJoinings.createQueryBuilder('joining')
+		.select('joining.userId')
+		.where('joining.userListId = :userListId', { userListId: list.id });
+
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+		.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
+		.setParameters(listQuery.getParameters());
+
+	generateVisibilityQuery(query, user);
+
+	if (ps.includeMyRenotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.userId != :meId', { meId: user.id });
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.includeRenotedMyNotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.includeLocalRenotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.renoteUserHost IS NOT NULL');
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
+	}
+	//#endregion
+
+	const timeline = await query.take(ps.limit!).getMany();
+
+	activeUsersChart.update(user);
+
+	return await Notes.packMany(timeline, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts
new file mode 100644
index 0000000000..4d182d3715
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/watching/create.ts
@@ -0,0 +1,37 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import watch from '@/services/note/watch';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'ea0e37a6-90a3-4f58-ba6b-c328ca206fc7'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	await watch(user.id, note);
+});
diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts
new file mode 100644
index 0000000000..dd58c52b57
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts
@@ -0,0 +1,37 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import unwatch from '@/services/note/unwatch';
+import { getNote } from '../../../common/getters';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: '09b3695c-f72c-4731-a428-7cff825fc82e'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	await unwatch(user.id, note);
+});
diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts
new file mode 100644
index 0000000000..8003c497ee
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notifications/create.ts
@@ -0,0 +1,37 @@
+import $ from 'cafy';
+import define from '../../define';
+import { createNotification } from '@/services/create-notification';
+
+export const meta = {
+	tags: ['notifications'],
+
+	requireCredential: true as const,
+
+	kind: 'write:notifications',
+
+	params: {
+		body: {
+			validator: $.str
+		},
+
+		header: {
+			validator: $.optional.nullable.str
+		},
+
+		icon: {
+			validator: $.optional.nullable.str
+		},
+	},
+
+	errors: {
+	}
+};
+
+export default define(meta, async (ps, user, token) => {
+	createNotification(user.id, 'app', {
+		appAccessTokenId: token ? token.id : null,
+		customBody: ps.body,
+		customHeader: ps.header,
+		customIcon: ps.icon,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
new file mode 100644
index 0000000000..8d4e512750
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
@@ -0,0 +1,24 @@
+import { publishMainStream } from '@/services/stream';
+import define from '../../define';
+import { Notifications } from '@/models/index';
+
+export const meta = {
+	tags: ['notifications', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:notifications'
+};
+
+export default define(meta, async (ps, user) => {
+	// Update documents
+	await Notifications.update({
+		notifieeId: user.id,
+		isRead: false,
+	}, {
+		isRead: true
+	});
+
+	// 全ての通知を読みましたよというイベントを発行
+	publishMainStream(user.id, 'readAllNotifications');
+});
diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts
new file mode 100644
index 0000000000..66bbc4efd7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notifications/read.ts
@@ -0,0 +1,42 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { publishMainStream } from '@/services/stream';
+import define from '../../define';
+import { Notifications } from '@/models/index';
+import { readNotification } from '../../common/read-notification';
+import { ApiError } from '../../error';
+
+export const meta = {
+	tags: ['notifications', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'write:notifications',
+
+	params: {
+		notificationId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchNotification: {
+			message: 'No such notification.',
+			code: 'NO_SUCH_NOTIFICATION',
+			id: 'efa929d5-05b5-47d1-beec-e6a4dbed011e'
+		},
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const notification = await Notifications.findOne({
+		notifieeId: user.id,
+		id: ps.notificationId,
+	});
+
+	if (notification == null) {
+		throw new ApiError(meta.errors.noSuchNotification);
+	}
+
+	readNotification(user.id, [notification.id]);
+});
diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts
new file mode 100644
index 0000000000..a0412e89f1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/page-push.ts
@@ -0,0 +1,50 @@
+import $ from 'cafy';
+import define from '../define';
+import { ID } from '@/misc/cafy-id';
+import { publishMainStream } from '@/services/stream';
+import { Users, Pages } from '@/models/index';
+import { ApiError } from '../error';
+
+export const meta = {
+	requireCredential: true as const,
+	secure: true,
+
+	params: {
+		pageId: {
+			validator: $.type(ID)
+		},
+
+		event: {
+			validator: $.str
+		},
+
+		var: {
+			validator: $.optional.nullable.any
+		}
+	},
+
+	errors: {
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: '4a13ad31-6729-46b4-b9af-e86b265c2e74'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const page = await Pages.findOne(ps.pageId);
+	if (page == null) {
+		throw new ApiError(meta.errors.noSuchPage);
+	}
+
+	publishMainStream(page.userId, 'pageEvent', {
+		pageId: ps.pageId,
+		event: ps.event,
+		var: ps.var,
+		userId: user.id,
+		user: await Users.pack(user.id, { id: page.userId }, {
+			detail: true
+		})
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts
new file mode 100644
index 0000000000..c23978f093
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/pages/create.ts
@@ -0,0 +1,128 @@
+import $ from 'cafy';
+import * as ms from 'ms';
+import define from '../../define';
+import { ID } from '@/misc/cafy-id';
+import { Pages, DriveFiles } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { Page } from '@/models/entities/page';
+import { ApiError } from '../../error';
+
+export const meta = {
+	tags: ['pages'],
+
+	requireCredential: true as const,
+
+	kind: 'write:pages',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300
+	},
+
+	params: {
+		title: {
+			validator: $.str,
+		},
+
+		name: {
+			validator: $.str.min(1),
+		},
+
+		summary: {
+			validator: $.optional.nullable.str,
+		},
+
+		content: {
+			validator: $.arr($.obj())
+		},
+
+		variables: {
+			validator: $.arr($.obj())
+		},
+
+		script: {
+			validator: $.str,
+		},
+
+		eyeCatchingImageId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		font: {
+			validator: $.optional.str.or(['serif', 'sans-serif']),
+			default: 'sans-serif'
+		},
+
+		alignCenter: {
+			validator: $.optional.bool,
+			default: false
+		},
+
+		hideTitleWhenPinned: {
+			validator: $.optional.bool,
+			default: false
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Page',
+	},
+
+	errors: {
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c'
+		},
+		nameAlreadyExists: {
+			message: 'Specified name already exists.',
+			code: 'NAME_ALREADY_EXISTS',
+			id: '4650348e-301c-499a-83c9-6aa988c66bc1'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	let eyeCatchingImage = null;
+	if (ps.eyeCatchingImageId != null) {
+		eyeCatchingImage = await DriveFiles.findOne({
+			id: ps.eyeCatchingImageId,
+			userId: user.id
+		});
+
+		if (eyeCatchingImage == null) {
+			throw new ApiError(meta.errors.noSuchFile);
+		}
+	}
+
+	await Pages.find({
+		userId: user.id,
+		name: ps.name
+	}).then(result => {
+		if (result.length > 0) {
+			throw new ApiError(meta.errors.nameAlreadyExists);
+		}
+	});
+
+	const page = await Pages.save(new Page({
+		id: genId(),
+		createdAt: new Date(),
+		updatedAt: new Date(),
+		title: ps.title,
+		name: ps.name,
+		summary: ps.summary,
+		content: ps.content,
+		variables: ps.variables,
+		script: ps.script,
+		eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null,
+		userId: user.id,
+		visibility: 'public',
+		alignCenter: ps.alignCenter,
+		hideTitleWhenPinned: ps.hideTitleWhenPinned,
+		font: ps.font
+	}));
+
+	return await Pages.pack(page);
+});
diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts
new file mode 100644
index 0000000000..b1f8c8a709
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/pages/delete.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Pages } from '@/models/index';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['pages'],
+
+	requireCredential: true as const,
+
+	kind: 'write:pages',
+
+	params: {
+		pageId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: 'eb0c6e1d-d519-4764-9486-52a7e1c6392a'
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '8b741b3e-2c22-44b3-a15f-29949aa1601e'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const page = await Pages.findOne(ps.pageId);
+	if (page == null) {
+		throw new ApiError(meta.errors.noSuchPage);
+	}
+	if (page.userId !== user.id) {
+		throw new ApiError(meta.errors.accessDenied);
+	}
+
+	await Pages.delete(page.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts
new file mode 100644
index 0000000000..f891c45f05
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/pages/featured.ts
@@ -0,0 +1,29 @@
+import define from '../../define';
+import { Pages } from '@/models/index';
+
+export const meta = {
+	tags: ['pages'],
+
+	requireCredential: false as const,
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Page',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = Pages.createQueryBuilder('page')
+		.where('page.visibility = \'public\'')
+		.andWhere('page.likedCount > 0')
+		.orderBy('page.likedCount', 'DESC');
+
+	const pages = await query.take(10).getMany();
+
+	return await Pages.packMany(pages, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts
new file mode 100644
index 0000000000..a95a377802
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/pages/like.ts
@@ -0,0 +1,71 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Pages, PageLikes } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['pages'],
+
+	requireCredential: true as const,
+
+	kind: 'write:page-likes',
+
+	params: {
+		pageId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3'
+		},
+
+		yourPage: {
+			message: 'You cannot like your page.',
+			code: 'YOUR_PAGE',
+			id: '28800466-e6db-40f2-8fae-bf9e82aa92b8'
+		},
+
+		alreadyLiked: {
+			message: 'The page has already been liked.',
+			code: 'ALREADY_LIKED',
+			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const page = await Pages.findOne(ps.pageId);
+	if (page == null) {
+		throw new ApiError(meta.errors.noSuchPage);
+	}
+
+	if (page.userId === user.id) {
+		throw new ApiError(meta.errors.yourPage);
+	}
+
+	// if already liked
+	const exist = await PageLikes.findOne({
+		pageId: page.id,
+		userId: user.id
+	});
+
+	if (exist != null) {
+		throw new ApiError(meta.errors.alreadyLiked);
+	}
+
+	// Create like
+	await PageLikes.insert({
+		id: genId(),
+		createdAt: new Date(),
+		pageId: page.id,
+		userId: user.id
+	});
+
+	Pages.increment({ id: page.id }, 'likedCount', 1);
+});
diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts
new file mode 100644
index 0000000000..7c55d4a9e6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/pages/show.ts
@@ -0,0 +1,65 @@
+import $ from 'cafy';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Pages, Users } from '@/models/index';
+import { ID } from '@/misc/cafy-id';
+import { Page } from '@/models/entities/page';
+
+export const meta = {
+	tags: ['pages'],
+
+	requireCredential: false as const,
+
+	params: {
+		pageId: {
+			validator: $.optional.type(ID),
+		},
+
+		name: {
+			validator: $.optional.str,
+		},
+
+		username: {
+			validator: $.optional.str,
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'Page',
+	},
+
+	errors: {
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: '222120c0-3ead-4528-811b-b96f233388d7'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	let page: Page | undefined;
+
+	if (ps.pageId) {
+		page = await Pages.findOne(ps.pageId);
+	} else if (ps.name && ps.username) {
+		const author = await Users.findOne({
+			host: null,
+			usernameLower: ps.username.toLowerCase()
+		});
+		if (author) {
+			page = await Pages.findOne({
+				name: ps.name,
+				userId: author.id
+			});
+		}
+	}
+
+	if (page == null) {
+		throw new ApiError(meta.errors.noSuchPage);
+	}
+
+	return await Pages.pack(page, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts
new file mode 100644
index 0000000000..facf2d6d5f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts
@@ -0,0 +1,54 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Pages, PageLikes } from '@/models/index';
+
+export const meta = {
+	tags: ['pages'],
+
+	requireCredential: true as const,
+
+	kind: 'write:page-likes',
+
+	params: {
+		pageId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: 'a0d41e20-1993-40bd-890e-f6e560ae648e'
+		},
+
+		notLiked: {
+			message: 'You have not liked that page.',
+			code: 'NOT_LIKED',
+			id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const page = await Pages.findOne(ps.pageId);
+	if (page == null) {
+		throw new ApiError(meta.errors.noSuchPage);
+	}
+
+	const exist = await PageLikes.findOne({
+		pageId: page.id,
+		userId: user.id
+	});
+
+	if (exist == null) {
+		throw new ApiError(meta.errors.notLiked);
+	}
+
+	// Delete like
+	await PageLikes.delete(exist.id);
+
+	Pages.decrement({ id: page.id }, 'likedCount', 1);
+});
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
new file mode 100644
index 0000000000..b3a7f26963
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -0,0 +1,141 @@
+import $ from 'cafy';
+import * as ms from 'ms';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Pages, DriveFiles } from '@/models/index';
+import { ID } from '@/misc/cafy-id';
+import { Not } from 'typeorm';
+
+export const meta = {
+	tags: ['pages'],
+
+	requireCredential: true as const,
+
+	kind: 'write:pages',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300
+	},
+
+	params: {
+		pageId: {
+			validator: $.type(ID),
+		},
+
+		title: {
+			validator: $.str,
+		},
+
+		name: {
+			validator: $.str.min(1),
+		},
+
+		summary: {
+			validator: $.optional.nullable.str,
+		},
+
+		content: {
+			validator: $.arr($.obj())
+		},
+
+		variables: {
+			validator: $.arr($.obj())
+		},
+
+		script: {
+			validator: $.str,
+		},
+
+		eyeCatchingImageId: {
+			validator: $.optional.nullable.type(ID),
+		},
+
+		font: {
+			validator: $.optional.str.or(['serif', 'sans-serif']),
+		},
+
+		alignCenter: {
+			validator: $.optional.bool,
+		},
+
+		hideTitleWhenPinned: {
+			validator: $.optional.bool,
+		},
+	},
+
+	errors: {
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: '21149b9e-3616-4778-9592-c4ce89f5a864'
+		},
+
+		accessDenied: {
+			message: 'Access denied.',
+			code: 'ACCESS_DENIED',
+			id: '3c15cd52-3b4b-4274-967d-6456fc4f792b'
+		},
+
+		noSuchFile: {
+			message: 'No such file.',
+			code: 'NO_SUCH_FILE',
+			id: 'cfc23c7c-3887-490e-af30-0ed576703c82'
+		},
+		nameAlreadyExists: {
+			message: 'Specified name already exists.',
+			code: 'NAME_ALREADY_EXISTS',
+			id: '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const page = await Pages.findOne(ps.pageId);
+	if (page == null) {
+		throw new ApiError(meta.errors.noSuchPage);
+	}
+	if (page.userId !== user.id) {
+		throw new ApiError(meta.errors.accessDenied);
+	}
+
+	let eyeCatchingImage = null;
+	if (ps.eyeCatchingImageId != null) {
+		eyeCatchingImage = await DriveFiles.findOne({
+			id: ps.eyeCatchingImageId,
+			userId: user.id
+		});
+
+		if (eyeCatchingImage == null) {
+			throw new ApiError(meta.errors.noSuchFile);
+		}
+	}
+
+	await Pages.find({
+		id: Not(ps.pageId),
+		userId: user.id,
+		name: ps.name
+	}).then(result => {
+		if (result.length > 0) {
+			throw new ApiError(meta.errors.nameAlreadyExists);
+		}
+	});
+
+	await Pages.update(page.id, {
+		updatedAt: new Date(),
+		title: ps.title,
+		name: ps.name === undefined ? page.name : ps.name,
+		summary: ps.name === undefined ? page.summary : ps.summary,
+		content: ps.content,
+		variables: ps.variables,
+		script: ps.script,
+		alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
+		hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
+		font: ps.font === undefined ? page.font : ps.font,
+		eyeCatchingImageId: ps.eyeCatchingImageId === null
+			? null
+			: ps.eyeCatchingImageId === undefined
+				? page.eyeCatchingImageId
+				: eyeCatchingImage!.id,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts
new file mode 100644
index 0000000000..0b1bb6e164
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/ping.ts
@@ -0,0 +1,27 @@
+import define from '../define';
+
+export const meta = {
+	requireCredential: false as const,
+
+	tags: ['meta'],
+
+	params: {
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			pong: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+		}
+	}
+};
+
+export default define(meta, async () => {
+	return {
+		pong: Date.now(),
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
new file mode 100644
index 0000000000..39cf7b0df1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -0,0 +1,32 @@
+import define from '../define';
+import { Users } from '@/models/index';
+import { fetchMeta } from '@/misc/fetch-meta';
+import * as Acct from 'misskey-js/built/acct';
+import { User } from '@/models/entities/user';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const meta = await fetchMeta();
+
+	const users = await Promise.all(meta.pinnedUsers.map(acct => Users.findOne(Acct.parse(acct))));
+
+	return await Users.packMany(users.filter(x => x !== undefined) as User[], me, { detail: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts
new file mode 100644
index 0000000000..ae57bf9cf1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/promo/read.ts
@@ -0,0 +1,50 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getNote } from '../../common/getters';
+import { PromoReads } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['notes'],
+
+	requireCredential: true as const,
+
+	params: {
+		noteId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchNote: {
+			message: 'No such note.',
+			code: 'NO_SUCH_NOTE',
+			id: 'd785b897-fcd3-4fe9-8fc3-b85c26e6c932'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const note = await getNote(ps.noteId).catch(e => {
+		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
+		throw e;
+	});
+
+	const exist = await PromoReads.findOne({
+		noteId: note.id,
+		userId: user.id
+	});
+
+	if (exist != null) {
+		return;
+	}
+
+	await PromoReads.insert({
+		id: genId(),
+		createdAt: new Date(),
+		noteId: note.id,
+		userId: user.id
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts
new file mode 100644
index 0000000000..f9928c2ee6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts
@@ -0,0 +1,73 @@
+import $ from 'cafy';
+import { publishMainStream } from '@/services/stream';
+import define from '../define';
+import rndstr from 'rndstr';
+import config from '@/config/index';
+import * as ms from 'ms';
+import { Users, UserProfiles, PasswordResetRequests } from '@/models/index';
+import { sendEmail } from '@/services/send-email';
+import { ApiError } from '../error';
+import { genId } from '@/misc/gen-id';
+import { IsNull } from 'typeorm';
+
+export const meta = {
+	requireCredential: false as const,
+
+	limit: {
+		duration: ms('1hour'),
+		max: 3
+	},
+
+	params: {
+		username: {
+			validator: $.str
+		},
+
+		email: {
+			validator: $.str
+		},
+	},
+
+	errors: {
+
+	}
+};
+
+export default define(meta, async (ps) => {
+	const user = await Users.findOne({
+		usernameLower: ps.username.toLowerCase(),
+		host: IsNull()
+	});
+
+	// 合致するユーザーが登録されていなかったら無視
+	if (user == null) {
+		return;
+	}
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// 合致するメアドが登録されていなかったら無視
+	if (profile.email !== ps.email) {
+		return;
+	}
+
+	// メアドが認証されていなかったら無視
+	if (!profile.emailVerified) {
+		return;
+	}
+
+	const token = rndstr('a-z0-9', 64);
+
+	await PasswordResetRequests.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: profile.userId,
+		token
+	});
+
+	const link = `${config.url}/reset-password/${token}`;
+
+	sendEmail(ps.email, 'Password reset requested',
+		`To reset password, please click this link:<br><a href="${link}">${link}</a>`,
+		`To reset password, please click this link: ${link}`);
+});
diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts
new file mode 100644
index 0000000000..f0a9dae4ff
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/reset-db.ts
@@ -0,0 +1,23 @@
+import $ from 'cafy';
+import define from '../define';
+import { ApiError } from '../error';
+import { resetDb } from '@/db/postgre';
+
+export const meta = {
+	requireCredential: false as const,
+
+	params: {
+	},
+
+	errors: {
+
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
+
+	await resetDb();
+
+	await new Promise(resolve => setTimeout(resolve, 1000));
+});
diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts
new file mode 100644
index 0000000000..53b0bfde0b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/reset-password.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import { publishMainStream } from '@/services/stream';
+import define from '../define';
+import { Users, UserProfiles, PasswordResetRequests } from '@/models/index';
+import { ApiError } from '../error';
+
+export const meta = {
+	requireCredential: false as const,
+
+	params: {
+		token: {
+			validator: $.str
+		},
+
+		password: {
+			validator: $.str
+		}
+	},
+
+	errors: {
+
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const req = await PasswordResetRequests.findOneOrFail({
+		token: ps.token,
+	});
+
+	// 発行してから30分以上経過していたら無効
+	if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) {
+		throw new Error(); // TODO
+	}
+
+	// Generate hash of password
+	const salt = await bcrypt.genSalt(8);
+	const hash = await bcrypt.hash(ps.password, salt);
+
+	await UserProfiles.update(req.userId, {
+		password: hash
+	});
+
+	PasswordResetRequests.delete(req.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/room/show.ts b/packages/backend/src/server/api/endpoints/room/show.ts
new file mode 100644
index 0000000000..a6461d4a6e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/room/show.ts
@@ -0,0 +1,159 @@
+import $ from 'cafy';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Users, UserProfiles } from '@/models/index';
+import { ID } from '@/misc/cafy-id';
+import { toPunyNullable } from '@/misc/convert-host';
+
+export const meta = {
+	tags: ['room'],
+
+	requireCredential: false as const,
+
+	params: {
+		userId: {
+			validator: $.optional.type(ID),
+		},
+
+		username: {
+			validator: $.optional.str
+		},
+
+		host: {
+			validator: $.optional.nullable.str
+		},
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '7ad3fa3e-5e12-42f0-b23a-f3d13f10ee4b'
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			roomType: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				enum: ['default', 'washitsu']
+			},
+			furnitures: {
+				type: 'array' as const,
+				optional: false as const, nullable: false as const,
+				items: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					properties: {
+						id: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const
+						},
+						type: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const
+						},
+						props: {
+							type: 'object' as const,
+							optional: true as const, nullable: false as const,
+						},
+						position: {
+							type: 'object' as const,
+							optional: false as const, nullable: false as const,
+							properties: {
+								x: {
+									type: 'number' as const,
+									optional: false as const, nullable: false as const
+								},
+								y: {
+									type: 'number' as const,
+									optional: false as const, nullable: false as const
+								},
+								z: {
+									type: 'number' as const,
+									optional: false as const, nullable: false as const
+								}
+							}
+						},
+						rotation: {
+							type: 'object' as const,
+							optional: false as const, nullable: false as const,
+							properties: {
+								x: {
+									type: 'number' as const,
+									optional: false as const, nullable: false as const
+								},
+								y: {
+									type: 'number' as const,
+									optional: false as const, nullable: false as const
+								},
+								z: {
+									type: 'number' as const,
+									optional: false as const, nullable: false as const
+								}
+							}
+						}
+					}
+				}
+			},
+			carpetColor: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				format: 'hex',
+				example: '#85CAF0'
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId != null
+		? { id: ps.userId }
+		: { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) });
+
+	if (user == null) {
+		throw new ApiError(meta.errors.noSuchUser);
+	}
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	if (profile.room.furnitures == null) {
+		await UserProfiles.update(user.id, {
+			room: {
+				furnitures: [],
+				...profile.room
+			}
+		});
+
+		profile.room.furnitures = [];
+	}
+
+	if (profile.room.roomType == null) {
+		const initialType = 'default';
+		await UserProfiles.update(user.id, {
+			room: {
+				roomType: initialType as any,
+				...profile.room
+			}
+		});
+
+		profile.room.roomType = initialType;
+	}
+
+	if (profile.room.carpetColor == null) {
+		const initialColor = '#85CAF0';
+		await UserProfiles.update(user.id, {
+			room: {
+				carpetColor: initialColor as any,
+				...profile.room
+			}
+		});
+
+		profile.room.carpetColor = initialColor;
+	}
+
+	return profile.room;
+});
diff --git a/packages/backend/src/server/api/endpoints/room/update.ts b/packages/backend/src/server/api/endpoints/room/update.ts
new file mode 100644
index 0000000000..8c4cfbdea6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/room/update.ts
@@ -0,0 +1,51 @@
+import $ from 'cafy';
+import { publishMainStream } from '@/services/stream';
+import define from '../../define';
+import { Users, UserProfiles } from '@/models/index';
+
+export const meta = {
+	tags: ['room'],
+
+	requireCredential: true as const,
+
+	params: {
+		room: {
+			validator: $.obj({
+				furnitures: $.arr($.obj({
+					id: $.str,
+					type: $.str,
+					position: $.obj({
+						x: $.num,
+						y: $.num,
+						z: $.num,
+					}),
+					rotation: $.obj({
+						x: $.num,
+						y: $.num,
+						z: $.num,
+					}),
+					props: $.optional.nullable.obj(),
+				})),
+				roomType: $.str,
+				carpetColor: $.str
+			})
+		},
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	await UserProfiles.update(user.id, {
+		room: ps.room as any
+	});
+
+	const iObj = await Users.pack(user.id, user, {
+		detail: true,
+		includeSecrets: true
+	});
+
+	// Publish meUpdated event
+	publishMainStream(user.id, 'meUpdated', iObj);
+
+	// TODO: レスポンスがおかしいと思う by YuzuRyo61
+	return iObj;
+});
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
new file mode 100644
index 0000000000..4e636d331c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -0,0 +1,35 @@
+import * as os from 'os';
+import * as si from 'systeminformation';
+import define from '../define';
+
+export const meta = {
+	requireCredential: false as const,
+
+	desc: {
+	},
+
+	tags: ['meta'],
+
+	params: {
+	},
+};
+
+export default define(meta, async () => {
+	const memStats = await si.mem();
+	const fsStats = await si.fsSize();
+
+	return {
+		machine: os.hostname(),
+		cpu: {
+			model: os.cpus()[0].model,
+			cores: os.cpus().length
+		},
+		mem: {
+			total: memStats.total
+		},
+		fs: {
+			total: fsStats[0].size,
+			used: fsStats[0].used,
+		},
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts
new file mode 100644
index 0000000000..15c8001742
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/stats.ts
@@ -0,0 +1,83 @@
+import define from '../define';
+import { NoteReactions, Notes, Users } from '@/models/index';
+import { federationChart, driveChart } from '@/services/chart/index';
+
+export const meta = {
+	requireCredential: false as const,
+
+	tags: ['meta'],
+
+	params: {
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			notesCount: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+			originalNotesCount: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+			usersCount: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+			originalUsersCount: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+			instances: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+			driveUsageLocal: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const
+			},
+			driveUsageRemote: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const
+			}
+		}
+	}
+};
+
+export default define(meta, async () => {
+	const [
+		notesCount,
+		originalNotesCount,
+		usersCount,
+		originalUsersCount,
+		reactionsCount,
+		//originalReactionsCount,
+		instances,
+		driveUsageLocal,
+		driveUsageRemote
+	] = await Promise.all([
+		Notes.count({ cache: 3600000 }), // 1 hour
+		Notes.count({ where: { userHost: null }, cache: 3600000 }),
+		Users.count({ cache: 3600000 }),
+		Users.count({ where: { host: null }, cache: 3600000 }),
+		NoteReactions.count({ cache: 3600000 }), // 1 hour
+		//NoteReactions.count({ where: { userHost: null }, cache: 3600000 }),
+		federationChart.getChart('hour', 1, null).then(chart => chart.instance.total[0]),
+		driveChart.getChart('hour', 1, null).then(chart => chart.local.totalSize[0]),
+		driveChart.getChart('hour', 1, null).then(chart => chart.remote.totalSize[0]),
+	]);
+
+	return {
+		notesCount,
+		originalNotesCount,
+		usersCount,
+		originalUsersCount,
+		reactionsCount,
+		//originalReactionsCount,
+		instances,
+		driveUsageLocal,
+		driveUsageRemote
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
new file mode 100644
index 0000000000..6e14ba2669
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -0,0 +1,74 @@
+import $ from 'cafy';
+import define from '../../define';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { genId } from '@/misc/gen-id';
+import { SwSubscriptions } from '@/models/index';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	params: {
+		endpoint: {
+			validator: $.str
+		},
+
+		auth: {
+			validator: $.str
+		},
+
+		publickey: {
+			validator: $.str
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			state: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+				enum: ['already-subscribed', 'subscribed']
+			},
+			key: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// if already subscribed
+	const exist = await SwSubscriptions.findOne({
+		userId: user.id,
+		endpoint: ps.endpoint,
+		auth: ps.auth,
+		publickey: ps.publickey,
+	});
+
+	const instance = await fetchMeta(true);
+
+	if (exist != null) {
+		return {
+			state: 'already-subscribed',
+			key: instance.swPublicKey
+		};
+	}
+
+	await SwSubscriptions.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		endpoint: ps.endpoint,
+		auth: ps.auth,
+		publickey: ps.publickey
+	});
+
+	return {
+		state: 'subscribed',
+		key: instance.swPublicKey
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts
new file mode 100644
index 0000000000..817ad1f517
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/sw/unregister.ts
@@ -0,0 +1,22 @@
+import $ from 'cafy';
+import define from '../../define';
+import { SwSubscriptions } from '../../../../models';
+
+export const meta = {
+	tags: ['account'],
+
+	requireCredential: true as const,
+
+	params: {
+		endpoint: {
+			validator: $.str
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	await SwSubscriptions.delete({
+		userId: user.id,
+		endpoint: ps.endpoint,
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts
new file mode 100644
index 0000000000..1ae75448ea
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/username/available.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Users, UsedUsernames } from '@/models/index';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		username: {
+			validator: $.use(Users.validateLocalUsername)
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			available: {
+				type: 'boolean' as const,
+				optional: false as const, nullable: false as const,
+			}
+		}
+	}
+};
+
+export default define(meta, async (ps) => {
+	// Get exist
+	const exist = await Users.count({
+		host: null,
+		usernameLower: ps.username.toLowerCase()
+	});
+
+	const exist2 = await UsedUsernames.count({ username: ps.username.toLowerCase() });
+
+	return {
+		available: exist === 0 && exist2 === 0
+	};
+});
diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts
new file mode 100644
index 0000000000..930dcc7616
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users.ts
@@ -0,0 +1,101 @@
+import $ from 'cafy';
+import define from '../define';
+import { Users } from '@/models/index';
+import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query';
+import { generateBlockQueryForUsers } from '../common/generate-block-query';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		},
+
+		sort: {
+			validator: $.optional.str.or([
+				'+follower',
+				'-follower',
+				'+createdAt',
+				'-createdAt',
+				'+updatedAt',
+				'-updatedAt',
+			]),
+		},
+
+		state: {
+			validator: $.optional.str.or([
+				'all',
+				'admin',
+				'moderator',
+				'adminOrModerator',
+				'alive'
+			]),
+			default: 'all'
+		},
+
+		origin: {
+			validator: $.optional.str.or([
+				'combined',
+				'local',
+				'remote',
+			]),
+			default: 'local'
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = Users.createQueryBuilder('user');
+	query.where('user.isExplorable = TRUE');
+
+	switch (ps.state) {
+		case 'admin': query.andWhere('user.isAdmin = TRUE'); break;
+		case 'moderator': query.andWhere('user.isModerator = TRUE'); break;
+		case 'adminOrModerator': query.andWhere('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
+		case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
+	}
+
+	switch (ps.origin) {
+		case 'local': query.andWhere('user.host IS NULL'); break;
+		case 'remote': query.andWhere('user.host IS NOT NULL'); break;
+	}
+
+	switch (ps.sort) {
+		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
+		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
+		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
+		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
+		case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break;
+		case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break;
+		default: query.orderBy('user.id', 'ASC'); break;
+	}
+
+	if (me) generateMutedUserQueryForUsers(query, me);
+	if (me) generateBlockQueryForUsers(query, me);
+
+	query.take(ps.limit!);
+	query.skip(ps.offset);
+
+	const users = await query.getMany();
+
+	return await Users.packMany(users, me, { detail: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts
new file mode 100644
index 0000000000..8feca9422a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/clips.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Clips } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['users', 'clips'],
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId)
+		.andWhere(`clip.userId = :userId`, { userId: ps.userId })
+		.andWhere('clip.isPublic = true');
+
+	const clips = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Clips.packMany(clips);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts
new file mode 100644
index 0000000000..6d042a2861
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/followers.ts
@@ -0,0 +1,104 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Users, Followings, UserProfiles } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { toPunyNullable } from '@/misc/convert-host';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		userId: {
+			validator: $.optional.type(ID),
+		},
+
+		username: {
+			validator: $.optional.str
+		},
+
+		host: {
+			validator: $.optional.nullable.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Following',
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '27fa5435-88ab-43de-9360-387de88727cd'
+		},
+
+		forbidden: {
+			message: 'Forbidden.',
+			code: 'FORBIDDEN',
+			id: '3c6a84db-d619-26af-ca14-06232a21df8a'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId != null
+		? { id: ps.userId }
+		: { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) });
+
+	if (user == null) {
+		throw new ApiError(meta.errors.noSuchUser);
+	}
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	if (profile.ffVisibility === 'private') {
+		if (me == null || (me.id !== user.id)) {
+			throw new ApiError(meta.errors.forbidden);
+		}
+	} else if (profile.ffVisibility === 'followers') {
+		if (me == null) {
+			throw new ApiError(meta.errors.forbidden);
+		} else if (me.id !== user.id) {
+			const following = await Followings.findOne({
+				followeeId: user.id,
+				followerId: me.id,
+			});
+			if (following == null) {
+				throw new ApiError(meta.errors.forbidden);
+			}
+		}
+	}
+
+	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+		.andWhere(`following.followeeId = :userId`, { userId: user.id })
+		.innerJoinAndSelect('following.follower', 'follower');
+
+	const followings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Followings.packMany(followings, me, { populateFollower: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
new file mode 100644
index 0000000000..1033117ef8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -0,0 +1,104 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { Users, Followings, UserProfiles } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { toPunyNullable } from '@/misc/convert-host';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		userId: {
+			validator: $.optional.type(ID),
+		},
+
+		username: {
+			validator: $.optional.str
+		},
+
+		host: {
+			validator: $.optional.nullable.str
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Following',
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '63e4aba4-4156-4e53-be25-c9559e42d71b'
+		},
+
+		forbidden: {
+			message: 'Forbidden.',
+			code: 'FORBIDDEN',
+			id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId != null
+		? { id: ps.userId }
+		: { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) });
+
+	if (user == null) {
+		throw new ApiError(meta.errors.noSuchUser);
+	}
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	if (profile.ffVisibility === 'private') {
+		if (me == null || (me.id !== user.id)) {
+			throw new ApiError(meta.errors.forbidden);
+		}
+	} else if (profile.ffVisibility === 'followers') {
+		if (me == null) {
+			throw new ApiError(meta.errors.forbidden);
+		} else if (me.id !== user.id) {
+			const following = await Followings.findOne({
+				followeeId: user.id,
+				followerId: me.id,
+			});
+			if (following == null) {
+				throw new ApiError(meta.errors.forbidden);
+			}
+		}
+	}
+
+	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
+		.andWhere(`following.followerId = :userId`, { userId: user.id })
+		.innerJoinAndSelect('following.followee', 'followee');
+
+	const followings = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Followings.packMany(followings, me, { populateFollowee: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts
new file mode 100644
index 0000000000..845de1089c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts
@@ -0,0 +1,39 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { GalleryPosts } from '@/models/index';
+import { makePaginationQuery } from '../../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['users', 'gallery'],
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
+		.andWhere(`post.userId = :userId`, { userId: ps.userId });
+
+	const posts = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await GalleryPosts.packMany(posts, user);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
new file mode 100644
index 0000000000..32ebfd683a
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
@@ -0,0 +1,105 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { maximum } from '@/prelude/array';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { Not, In, IsNull } from 'typeorm';
+import { Notes, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: 'e6965129-7b2a-40a4-bae2-cd84cd434822'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Lookup user
+	const user = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Fetch recent notes
+	const recentNotes = await Notes.find({
+		where: {
+			userId: user.id,
+			replyId: Not(IsNull())
+		},
+		order: {
+			id: -1
+		},
+		take: 1000,
+		select: ['replyId']
+	});
+
+	// 投稿が少なかったら中断
+	if (recentNotes.length === 0) {
+		return [];
+	}
+
+	// TODO ミュートを考慮
+	const replyTargetNotes = await Notes.find({
+		where: {
+			id: In(recentNotes.map(p => p.replyId)),
+		},
+		select: ['userId']
+	});
+
+	const repliedUsers: any = {};
+
+	// Extract replies from recent notes
+	for (const userId of replyTargetNotes.map(x => x.userId.toString())) {
+		if (repliedUsers[userId]) {
+			repliedUsers[userId]++;
+		} else {
+			repliedUsers[userId] = 1;
+		}
+	}
+
+	// Calc peak
+	const peak = maximum(Object.values(repliedUsers));
+
+	// Sort replies by frequency
+	const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
+
+	// Extract top replied users
+	const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit!);
+
+	// Make replies object (includes weights)
+	const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
+		user: await Users.pack(user, me, { detail: true }),
+		weight: repliedUsers[user] / peak
+	})));
+
+	return repliesObj;
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts
new file mode 100644
index 0000000000..dc1ee3879e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/create.ts
@@ -0,0 +1,45 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { UserGroups, UserGroupJoinings } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { UserGroup } from '@/models/entities/user-group';
+import { UserGroupJoining } from '@/models/entities/user-group-joining';
+
+export const meta = {
+	tags: ['groups'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		name: {
+			validator: $.str.range(1, 100)
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'UserGroup',
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const userGroup = await UserGroups.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		name: ps.name,
+	} as UserGroup).then(x => UserGroups.findOneOrFail(x.identifiers[0]));
+
+	// Push the owner
+	await UserGroupJoinings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		userGroupId: userGroup.id
+	} as UserGroupJoining);
+
+	return await UserGroups.pack(userGroup);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts
new file mode 100644
index 0000000000..7da1b4a273
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { UserGroups } from '@/models/index';
+
+export const meta = {
+	tags: ['groups'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		groupId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: '63dbd64c-cd77-413f-8e08-61781e210b38'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const userGroup = await UserGroups.findOne({
+		id: ps.groupId,
+		userId: user.id
+	});
+
+	if (userGroup == null) {
+		throw new ApiError(meta.errors.noSuchGroup);
+	}
+
+	await UserGroups.delete(userGroup.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts
new file mode 100644
index 0000000000..09e6ae2647
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts
@@ -0,0 +1,54 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../../define';
+import { ApiError } from '../../../../error';
+import { UserGroupJoinings, UserGroupInvitations } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { UserGroupJoining } from '@/models/entities/user-group-joining';
+
+export const meta = {
+	tags: ['groups', 'users'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		invitationId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchInvitation: {
+			message: 'No such invitation.',
+			code: 'NO_SUCH_INVITATION',
+			id: '98c11eca-c890-4f42-9806-c8c8303ebb5e'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch the invitation
+	const invitation = await UserGroupInvitations.findOne({
+		id: ps.invitationId,
+	});
+
+	if (invitation == null) {
+		throw new ApiError(meta.errors.noSuchInvitation);
+	}
+
+	if (invitation.userId !== user.id) {
+		throw new ApiError(meta.errors.noSuchInvitation);
+	}
+
+	// Push the user
+	await UserGroupJoinings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		userGroupId: invitation.userGroupId
+	} as UserGroupJoining);
+
+	UserGroupInvitations.delete(invitation.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts
new file mode 100644
index 0000000000..741fcefb35
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts
@@ -0,0 +1,44 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../../define';
+import { ApiError } from '../../../../error';
+import { UserGroupInvitations } from '@/models/index';
+
+export const meta = {
+	tags: ['groups', 'users'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		invitationId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchInvitation: {
+			message: 'No such invitation.',
+			code: 'NO_SUCH_INVITATION',
+			id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch the invitation
+	const invitation = await UserGroupInvitations.findOne({
+		id: ps.invitationId,
+	});
+
+	if (invitation == null) {
+		throw new ApiError(meta.errors.noSuchInvitation);
+	}
+
+	if (invitation.userId !== user.id) {
+		throw new ApiError(meta.errors.noSuchInvitation);
+	}
+
+	await UserGroupInvitations.delete(invitation.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts
new file mode 100644
index 0000000000..f1ee8bf8b7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/invite.ts
@@ -0,0 +1,102 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { UserGroupInvitation } from '@/models/entities/user-group-invitation';
+import { createNotification } from '@/services/create-notification';
+
+export const meta = {
+	tags: ['groups', 'users'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		groupId: {
+			validator: $.type(ID),
+		},
+
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: '583f8bc0-8eee-4b78-9299-1e14fc91e409'
+		},
+
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: 'da52de61-002c-475b-90e1-ba64f9cf13a8'
+		},
+
+		alreadyAdded: {
+			message: 'That user has already been added to that group.',
+			code: 'ALREADY_ADDED',
+			id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c'
+		},
+
+		alreadyInvited: {
+			message: 'That user has already been invited to that group.',
+			code: 'ALREADY_INVITED',
+			id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the group
+	const userGroup = await UserGroups.findOne({
+		id: ps.groupId,
+		userId: me.id,
+	});
+
+	if (userGroup == null) {
+		throw new ApiError(meta.errors.noSuchGroup);
+	}
+
+	// Fetch the user
+	const user = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	const joining = await UserGroupJoinings.findOne({
+		userGroupId: userGroup.id,
+		userId: user.id
+	});
+
+	if (joining) {
+		throw new ApiError(meta.errors.alreadyAdded);
+	}
+
+	const existInvitation = await UserGroupInvitations.findOne({
+		userGroupId: userGroup.id,
+		userId: user.id
+	});
+
+	if (existInvitation) {
+		throw new ApiError(meta.errors.alreadyInvited);
+	}
+
+	const invitation = await UserGroupInvitations.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		userGroupId: userGroup.id
+	} as UserGroupInvitation).then(x => UserGroupInvitations.findOneOrFail(x.identifiers[0]));
+
+	// 通知を作成
+	createNotification(user.id, 'groupInvited', {
+		notifierId: me.id,
+		userGroupInvitationId: invitation.id
+	});
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts
new file mode 100644
index 0000000000..d5e8fe4032
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/joined.ts
@@ -0,0 +1,36 @@
+import define from '../../../define';
+import { UserGroups, UserGroupJoinings } from '@/models/index';
+import { Not, In } from 'typeorm';
+
+export const meta = {
+	tags: ['groups', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:user-groups',
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'UserGroup',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const ownedGroups = await UserGroups.find({
+		userId: me.id,
+	});
+
+	const joinings = await UserGroupJoinings.find({
+		userId: me.id,
+		...(ownedGroups.length > 0 ? {
+			userGroupId: Not(In(ownedGroups.map(x => x.id)))
+		} : {})
+	});
+
+	return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId)));
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts
new file mode 100644
index 0000000000..0e52f2abdf
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/leave.ts
@@ -0,0 +1,50 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { UserGroups, UserGroupJoinings } from '@/models/index';
+
+export const meta = {
+	tags: ['groups', 'users'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		groupId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: '62780270-1f67-5dc0-daca-3eb510612e31'
+		},
+
+		youAreOwner: {
+			message: 'Your are the owner.',
+			code: 'YOU_ARE_OWNER',
+			id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the group
+	const userGroup = await UserGroups.findOne({
+		id: ps.groupId,
+	});
+
+	if (userGroup == null) {
+		throw new ApiError(meta.errors.noSuchGroup);
+	}
+
+	if (me.id === userGroup.userId) {
+		throw new ApiError(meta.errors.youAreOwner);
+	}
+
+	await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id });
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts
new file mode 100644
index 0000000000..17de370dbc
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/owned.ts
@@ -0,0 +1,28 @@
+import define from '../../../define';
+import { UserGroups } from '@/models/index';
+
+export const meta = {
+	tags: ['groups', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:user-groups',
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'UserGroup',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const userGroups = await UserGroups.find({
+		userId: me.id,
+	});
+
+	return await Promise.all(userGroups.map(x => UserGroups.pack(x)));
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts
new file mode 100644
index 0000000000..ce4d2e2881
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/pull.ts
@@ -0,0 +1,69 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+import { UserGroups, UserGroupJoinings } from '@/models/index';
+
+export const meta = {
+	tags: ['groups', 'users'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		groupId: {
+			validator: $.type(ID),
+		},
+
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: '4662487c-05b1-4b78-86e5-fd46998aba74'
+		},
+
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '0b5cc374-3681-41da-861e-8bc1146f7a55'
+		},
+
+		isOwner: {
+			message: 'The user is the owner.',
+			code: 'IS_OWNER',
+			id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the group
+	const userGroup = await UserGroups.findOne({
+		id: ps.groupId,
+		userId: me.id,
+	});
+
+	if (userGroup == null) {
+		throw new ApiError(meta.errors.noSuchGroup);
+	}
+
+	// Fetch the user
+	const user = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	if (user.id === userGroup.userId) {
+		throw new ApiError(meta.errors.isOwner);
+	}
+
+	// Pull the user
+	await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: user.id });
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts
new file mode 100644
index 0000000000..3c030bf3a5
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/show.ts
@@ -0,0 +1,55 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { UserGroups, UserGroupJoinings } from '@/models/index';
+
+export const meta = {
+	tags: ['groups', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:user-groups',
+
+	params: {
+		groupId: {
+			validator: $.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'UserGroup',
+	},
+
+	errors: {
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the group
+	const userGroup = await UserGroups.findOne({
+		id: ps.groupId,
+	});
+
+	if (userGroup == null) {
+		throw new ApiError(meta.errors.noSuchGroup);
+	}
+
+	const joining = await UserGroupJoinings.findOne({
+		userId: me.id,
+		userGroupId: userGroup.id
+	});
+
+	if (joining == null && userGroup.userId !== me.id) {
+		throw new ApiError(meta.errors.noSuchGroup);
+	}
+
+	return await UserGroups.pack(userGroup);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts
new file mode 100644
index 0000000000..17c42e1127
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts
@@ -0,0 +1,83 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+import { UserGroups, UserGroupJoinings } from '@/models/index';
+
+export const meta = {
+	tags: ['groups', 'users'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		groupId: {
+			validator: $.type(ID),
+		},
+
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'UserGroup',
+	},
+
+	errors: {
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db'
+		},
+
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9'
+		},
+
+		noSuchGroupMember: {
+			message: 'No such group member.',
+			code: 'NO_SUCH_GROUP_MEMBER',
+			id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the group
+	const userGroup = await UserGroups.findOne({
+		id: ps.groupId,
+		userId: me.id,
+	});
+
+	if (userGroup == null) {
+		throw new ApiError(meta.errors.noSuchGroup);
+	}
+
+	// Fetch the user
+	const user = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	const joining = await UserGroupJoinings.findOne({
+		userGroupId: userGroup.id,
+		userId: user.id
+	});
+
+	if (joining == null) {
+		throw new ApiError(meta.errors.noSuchGroupMember);
+	}
+
+	await UserGroups.update(userGroup.id, {
+		userId: ps.userId
+	});
+
+	return await UserGroups.pack(userGroup.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts
new file mode 100644
index 0000000000..127bbc47a1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/groups/update.ts
@@ -0,0 +1,55 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { UserGroups } from '@/models/index';
+
+export const meta = {
+	tags: ['groups'],
+
+	requireCredential: true as const,
+
+	kind: 'write:user-groups',
+
+	params: {
+		groupId: {
+			validator: $.type(ID),
+		},
+
+		name: {
+			validator: $.str.range(1, 100),
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'UserGroup',
+	},
+
+	errors: {
+		noSuchGroup: {
+			message: 'No such group.',
+			code: 'NO_SUCH_GROUP',
+			id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the group
+	const userGroup = await UserGroups.findOne({
+		id: ps.groupId,
+		userId: me.id
+	});
+
+	if (userGroup == null) {
+		throw new ApiError(meta.errors.noSuchGroup);
+	}
+
+	await UserGroups.update(userGroup.id, {
+		name: ps.name
+	});
+
+	return await UserGroups.pack(userGroup.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts
new file mode 100644
index 0000000000..e0bfe611fc
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts
@@ -0,0 +1,36 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { UserLists } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { UserList } from '@/models/entities/user-list';
+
+export const meta = {
+	tags: ['lists'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		name: {
+			validator: $.str.range(1, 100)
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'UserList',
+	},
+};
+
+export default define(meta, async (ps, user) => {
+	const userList = await UserLists.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		name: ps.name,
+	} as UserList).then(x => UserLists.findOneOrFail(x.identifiers[0]));
+
+	return await UserLists.pack(userList);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts
new file mode 100644
index 0000000000..5fe3bfb03d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { UserLists } from '@/models/index';
+
+export const meta = {
+	tags: ['lists'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		listId: {
+			validator: $.type(ID),
+		}
+	},
+
+	errors: {
+		noSuchList: {
+			message: 'No such list.',
+			code: 'NO_SUCH_LIST',
+			id: '78436795-db79-42f5-b1e2-55ea2cf19166'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: user.id
+	});
+
+	if (userList == null) {
+		throw new ApiError(meta.errors.noSuchList);
+	}
+
+	await UserLists.delete(userList.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts
new file mode 100644
index 0000000000..cf0c92bb84
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts
@@ -0,0 +1,28 @@
+import define from '../../../define';
+import { UserLists } from '@/models/index';
+
+export const meta = {
+	tags: ['lists', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:account',
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'UserList',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const userLists = await UserLists.find({
+		userId: me.id,
+	});
+
+	return await Promise.all(userLists.map(x => UserLists.pack(x)));
+});
diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts
new file mode 100644
index 0000000000..d4357fc5e7
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts
@@ -0,0 +1,62 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import { publishUserListStream } from '@/services/stream';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+import { UserLists, UserListJoinings, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['lists', 'users'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		listId: {
+			validator: $.type(ID),
+		},
+
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchList: {
+			message: 'No such list.',
+			code: 'NO_SUCH_LIST',
+			id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02'
+		},
+
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '588e7f72-c744-4a61-b180-d354e912bda2'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the list
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: me.id,
+	});
+
+	if (userList == null) {
+		throw new ApiError(meta.errors.noSuchList);
+	}
+
+	// Fetch the user
+	const user = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Pull the user
+	await UserListJoinings.delete({ userListId: userList.id, userId: user.id });
+
+	publishUserListStream(userList.id, 'userRemoved', await Users.pack(user));
+});
diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts
new file mode 100644
index 0000000000..8e21059d3d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts
@@ -0,0 +1,92 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { getUser } from '../../../common/getters';
+import { pushUserToUserList } from '@/services/user-list/push';
+import { UserLists, UserListJoinings, Blockings } from '@/models/index';
+
+export const meta = {
+	tags: ['lists', 'users'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		listId: {
+			validator: $.type(ID),
+		},
+
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchList: {
+			message: 'No such list.',
+			code: 'NO_SUCH_LIST',
+			id: '2214501d-ac96-4049-b717-91e42272a711'
+		},
+
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: 'a89abd3d-f0bc-4cce-beb1-2f446f4f1e6a'
+		},
+
+		alreadyAdded: {
+			message: 'That user has already been added to that list.',
+			code: 'ALREADY_ADDED',
+			id: '1de7c884-1595-49e9-857e-61f12f4d4fc5'
+		},
+
+		youHaveBeenBlocked: {
+			message: 'You cannot push this user because you have been blocked by this user.',
+			code: 'YOU_HAVE_BEEN_BLOCKED',
+			id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the list
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: me.id,
+	});
+
+	if (userList == null) {
+		throw new ApiError(meta.errors.noSuchList);
+	}
+
+	// Fetch the user
+	const user = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	// Check blocking
+	if (user.id !== me.id) {
+		const block = await Blockings.findOne({
+			blockerId: user.id,
+			blockeeId: me.id,
+		});
+		if (block) {
+			throw new ApiError(meta.errors.youHaveBeenBlocked);
+		}
+	}
+
+	const exist = await UserListJoinings.findOne({
+		userListId: userList.id,
+		userId: user.id
+	});
+
+	if (exist) {
+		throw new ApiError(meta.errors.alreadyAdded);
+	}
+
+	// Push the user
+	await pushUserToUserList(user, userList);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts
new file mode 100644
index 0000000000..f9a35cdab3
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts
@@ -0,0 +1,47 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { UserLists } from '@/models/index';
+
+export const meta = {
+	tags: ['lists', 'account'],
+
+	requireCredential: true as const,
+
+	kind: 'read:account',
+
+	params: {
+		listId: {
+			validator: $.type(ID),
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'UserList',
+	},
+
+	errors: {
+		noSuchList: {
+			message: 'No such list.',
+			code: 'NO_SUCH_LIST',
+			id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Fetch the list
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: me.id,
+	});
+
+	if (userList == null) {
+		throw new ApiError(meta.errors.noSuchList);
+	}
+
+	return await UserLists.pack(userList);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts
new file mode 100644
index 0000000000..1185af5043
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts
@@ -0,0 +1,55 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { UserLists } from '@/models/index';
+
+export const meta = {
+	tags: ['lists'],
+
+	requireCredential: true as const,
+
+	kind: 'write:account',
+
+	params: {
+		listId: {
+			validator: $.type(ID),
+		},
+
+		name: {
+			validator: $.str.range(1, 100),
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'UserList',
+	},
+
+	errors: {
+		noSuchList: {
+			message: 'No such list.',
+			code: 'NO_SUCH_LIST',
+			id: '796666fe-3dff-4d39-becb-8a5932c1d5b7'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	// Fetch the list
+	const userList = await UserLists.findOne({
+		id: ps.listId,
+		userId: user.id
+	});
+
+	if (userList == null) {
+		throw new ApiError(meta.errors.noSuchList);
+	}
+
+	await UserLists.update(userList.id, {
+		name: ps.name
+	});
+
+	return await UserLists.pack(userList.id);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
new file mode 100644
index 0000000000..0afbad9d04
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -0,0 +1,144 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { Notes } from '@/models/index';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
+import { Brackets } from 'typeorm';
+import { generateBlockedUserQuery } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['users', 'notes'],
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+
+		includeReplies: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		sinceDate: {
+			validator: $.optional.num,
+		},
+
+		untilDate: {
+			validator: $.optional.num,
+		},
+
+		includeMyRenotes: {
+			validator: $.optional.bool,
+			default: true,
+		},
+
+		withFiles: {
+			validator: $.optional.bool,
+			default: false,
+		},
+
+		fileType: {
+			validator: $.optional.arr($.str),
+		},
+
+		excludeNsfw: {
+			validator: $.optional.bool,
+			default: false,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'Note',
+		}
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Lookup user
+	const user = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	//#region Construct query
+	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere('note.userId = :userId', { userId: user.id })
+		.innerJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
+
+	generateVisibilityQuery(query, me);
+	if (me) generateMutedUserQuery(query, me, user);
+	if (me) generateBlockedUserQuery(query, me);
+
+	if (ps.withFiles) {
+		query.andWhere('note.fileIds != \'{}\'');
+	}
+
+	if (ps.fileType != null) {
+		query.andWhere('note.fileIds != \'{}\'');
+		query.andWhere(new Brackets(qb => {
+			for (const type of ps.fileType!) {
+				const i = ps.fileType!.indexOf(type);
+				qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
+			}
+		}));
+
+		if (ps.excludeNsfw) {
+			query.andWhere('note.cw IS NULL');
+			query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
+		}
+	}
+
+	if (!ps.includeReplies) {
+		query.andWhere('note.replyId IS NULL');
+	}
+
+	if (ps.includeMyRenotes === false) {
+		query.andWhere(new Brackets(qb => {
+			qb.orWhere('note.userId != :userId', { userId: user.id });
+			qb.orWhere('note.renoteId IS NULL');
+			qb.orWhere('note.text IS NOT NULL');
+			qb.orWhere('note.fileIds != \'{}\'');
+			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
+		}));
+	}
+
+	//#endregion
+
+	const timeline = await query.take(ps.limit!).getMany();
+
+	return await Notes.packMany(timeline, me);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts
new file mode 100644
index 0000000000..24e9e207fd
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/pages.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { Pages } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+	tags: ['users', 'pages'],
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
+		.andWhere(`page.userId = :userId`, { userId: ps.userId })
+		.andWhere('page.visibility = \'public\'');
+
+	const pages = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Pages.packMany(pages);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
new file mode 100644
index 0000000000..fe5e4d84a9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -0,0 +1,79 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { NoteReactions, UserProfiles } from '@/models/index';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+import { generateVisibilityQuery } from '../../common/generate-visibility-query';
+import { ApiError } from '../../error';
+
+export const meta = {
+	tags: ['users', 'reactions'],
+
+	requireCredential: false as const,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		sinceId: {
+			validator: $.optional.type(ID),
+		},
+
+		untilId: {
+			validator: $.optional.type(ID),
+		},
+
+		sinceDate: {
+			validator: $.optional.num,
+		},
+
+		untilDate: {
+			validator: $.optional.num,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'NoteReaction',
+		}
+	},
+
+	errors: {
+		reactionsNotPublic: {
+			message: 'Reactions of the user is not public.',
+			code: 'REACTIONS_NOT_PUBLIC',
+			id: '673a7dd2-6924-1093-e0c0-e68456ceae5c'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const profile = await UserProfiles.findOneOrFail(ps.userId);
+
+	if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
+		throw new ApiError(meta.errors.reactionsNotPublic);
+	}
+
+	const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'),
+			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+		.andWhere(`reaction.userId = :userId`, { userId: ps.userId })
+		.leftJoinAndSelect('reaction.note', 'note');
+
+	generateVisibilityQuery(query, me);
+
+	const reactions = await query
+		.take(ps.limit!)
+		.getMany();
+
+	return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true })));
+});
diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts
new file mode 100644
index 0000000000..dde6bb1037
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts
@@ -0,0 +1,63 @@
+import * as ms from 'ms';
+import $ from 'cafy';
+import define from '../../define';
+import { Users, Followings } from '@/models/index';
+import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query';
+import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: true as const,
+
+	kind: 'read:account',
+
+	params: {
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0
+		}
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const query = Users.createQueryBuilder('user')
+		.where('user.isLocked = FALSE')
+		.andWhere('user.isExplorable = TRUE')
+		.andWhere('user.host IS NULL')
+		.andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) })
+		.andWhere('user.id != :meId', { meId: me.id })
+		.orderBy('user.followersCount', 'DESC');
+
+	generateMutedUserQueryForUsers(query, me);
+	generateBlockQueryForUsers(query, me);
+	generateBlockedUserQuery(query, me);
+
+	const followingQuery = Followings.createQueryBuilder('following')
+		.select('following.followeeId')
+		.where('following.followerId = :followerId', { followerId: me.id });
+
+	query
+		.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`);
+
+	query.setParameters(followingQuery.getParameters());
+
+	const users = await query.take(ps.limit!).skip(ps.offset).getMany();
+
+	return await Users.packMany(users, me, { detail: true });
+});
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
new file mode 100644
index 0000000000..32d76a5322
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -0,0 +1,111 @@
+import $ from 'cafy';
+import define from '../../define';
+import { ID } from '@/misc/cafy-id';
+import { Users } from '@/models/index';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: true as const,
+
+	params: {
+		userId: {
+			validator: $.either($.type(ID), $.arr($.type(ID)).unique()),
+		}
+	},
+
+	res: {
+		oneOf: [
+			{
+				type: 'object' as const,
+				optional: false as const, nullable: false as const,
+				properties: {
+					id: {
+						type: 'string' as const,
+						optional: false as const, nullable: false as const,
+						format: 'id'
+					},
+					isFollowing: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					hasPendingFollowRequestFromYou: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					hasPendingFollowRequestToYou: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					isFollowed: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					isBlocking: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					isBlocked: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					},
+					isMuted: {
+						type: 'boolean' as const,
+						optional: false as const, nullable: false as const
+					}
+				}
+			},
+			{
+				type: 'array' as const,
+				optional: false as const, nullable: false as const,
+				items: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					properties: {
+						id: {
+							type: 'string' as const,
+							optional: false as const, nullable: false as const,
+							format: 'id'
+						},
+						isFollowing: {
+							type: 'boolean' as const,
+							optional: false as const, nullable: false as const
+						},
+						hasPendingFollowRequestFromYou: {
+							type: 'boolean' as const,
+							optional: false as const, nullable: false as const
+						},
+						hasPendingFollowRequestToYou: {
+							type: 'boolean' as const,
+							optional: false as const, nullable: false as const
+						},
+						isFollowed: {
+							type: 'boolean' as const,
+							optional: false as const, nullable: false as const
+						},
+						isBlocking: {
+							type: 'boolean' as const,
+							optional: false as const, nullable: false as const
+						},
+						isBlocked: {
+							type: 'boolean' as const,
+							optional: false as const, nullable: false as const
+						},
+						isMuted: {
+							type: 'boolean' as const,
+							optional: false as const, nullable: false as const
+						}
+					}
+				}
+			}
+		]
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
+
+	const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id)));
+
+	return Array.isArray(ps.userId) ? relations : relations[0];
+});
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
new file mode 100644
index 0000000000..2c8672cd47
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -0,0 +1,90 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { publishAdminStream } from '@/services/stream';
+import { ApiError } from '../../error';
+import { getUser } from '../../common/getters';
+import { AbuseUserReports, Users } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: true as const,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+
+		comment: {
+			validator: $.str.range(1, 2048),
+		},
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '1acefcb5-0959-43fd-9685-b48305736cb5'
+		},
+
+		cannotReportYourself: {
+			message: 'Cannot report yourself.',
+			code: 'CANNOT_REPORT_YOURSELF',
+			id: '1e13149e-b1e8-43cf-902e-c01dbfcb202f'
+		},
+
+		cannotReportAdmin: {
+			message: 'Cannot report the admin.',
+			code: 'CANNOT_REPORT_THE_ADMIN',
+			id: '35e166f5-05fb-4f87-a2d5-adb42676d48f'
+		}
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	// Lookup user
+	const user = await getUser(ps.userId).catch(e => {
+		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+		throw e;
+	});
+
+	if (user.id === me.id) {
+		throw new ApiError(meta.errors.cannotReportYourself);
+	}
+
+	if (user.isAdmin) {
+		throw new ApiError(meta.errors.cannotReportAdmin);
+	}
+
+	const report = await AbuseUserReports.save({
+		id: genId(),
+		createdAt: new Date(),
+		targetUserId: user.id,
+		targetUserHost: user.host,
+		reporterId: me.id,
+		reporterHost: null,
+		comment: ps.comment,
+	});
+
+	// Publish event to moderators
+	setTimeout(async () => {
+		const moderators = await Users.find({
+			where: [{
+				isAdmin: true
+			}, {
+				isModerator: true
+			}]
+		});
+
+		for (const moderator of moderators) {
+			publishAdminStream(moderator.id, 'newAbuseUserReport', {
+				id: report.id,
+				targetUserId: report.targetUserId,
+				reporterId: report.reporterId,
+				comment: report.comment
+			});
+		}
+	}, 1);
+});
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
new file mode 100644
index 0000000000..1ec5e1a743
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -0,0 +1,116 @@
+import $ from 'cafy';
+import define from '../../define';
+import { Followings, Users } from '@/models/index';
+import { Brackets } from 'typeorm';
+import { USER_ACTIVE_THRESHOLD } from '@/const';
+import { User } from '@/models/entities/user';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		username: {
+			validator: $.optional.nullable.str,
+		},
+
+		host: {
+			validator: $.optional.nullable.str,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		detail: {
+			validator: $.optional.bool,
+			default: true,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
+
+	if (ps.host) {
+		const q = Users.createQueryBuilder('user')
+			.where('user.isSuspended = FALSE')
+			.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
+
+		if (ps.username) {
+			q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' });
+		}
+
+		q.andWhere('user.updatedAt IS NOT NULL');
+		q.orderBy('user.updatedAt', 'DESC');
+
+		const users = await q.take(ps.limit!).getMany();
+
+		return await Users.packMany(users, me, { detail: ps.detail });
+	} else if (ps.username) {
+		let users: User[] = [];
+
+		if (me) {
+			const followingQuery = Followings.createQueryBuilder('following')
+				.select('following.followeeId')
+				.where('following.followerId = :followerId', { followerId: me.id });
+
+			const query = Users.createQueryBuilder('user')
+				.where(`user.id IN (${ followingQuery.getQuery() })`)
+				.andWhere(`user.id != :meId`, { meId: me.id })
+				.andWhere('user.isSuspended = FALSE')
+				.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
+				.andWhere(new Brackets(qb => { qb
+					.where('user.updatedAt IS NULL')
+					.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+				}));
+
+			query.setParameters(followingQuery.getParameters());
+
+			users = await query
+				.orderBy('user.usernameLower', 'ASC')
+				.take(ps.limit!)
+				.getMany();
+
+			if (users.length < ps.limit!) {
+				const otherQuery = await Users.createQueryBuilder('user')
+					.where(`user.id NOT IN (${ followingQuery.getQuery() })`)
+					.andWhere(`user.id != :meId`, { meId: me.id })
+					.andWhere('user.isSuspended = FALSE')
+					.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
+					.andWhere('user.updatedAt IS NOT NULL');
+
+				otherQuery.setParameters(followingQuery.getParameters());
+
+				const otherUsers = await otherQuery
+					.orderBy('user.updatedAt', 'DESC')
+					.take(ps.limit! - users.length)
+					.getMany();
+
+				users = users.concat(otherUsers);
+			}
+		} else {
+			users = await Users.createQueryBuilder('user')
+				.where('user.isSuspended = FALSE')
+				.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
+				.andWhere('user.updatedAt IS NOT NULL')
+				.orderBy('user.updatedAt', 'DESC')
+				.take(ps.limit! - users.length)
+				.getMany();
+		}
+
+		return await Users.packMany(users, me, { detail: ps.detail });
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
new file mode 100644
index 0000000000..9aa988d9ed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -0,0 +1,127 @@
+import $ from 'cafy';
+import define from '../../define';
+import { UserProfiles, Users } from '@/models/index';
+import { User } from '@/models/entities/user';
+import { Brackets } from 'typeorm';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		query: {
+			validator: $.str,
+		},
+
+		offset: {
+			validator: $.optional.num.min(0),
+			default: 0,
+		},
+
+		limit: {
+			validator: $.optional.num.range(1, 100),
+			default: 10,
+		},
+
+		origin: {
+			validator: $.optional.str.or(['local', 'remote', 'combined']),
+			default: 'combined',
+		},
+
+		detail: {
+			validator: $.optional.bool,
+			default: true,
+		},
+	},
+
+	res: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			ref: 'User',
+		}
+	},
+};
+
+export default define(meta, async (ps, me) => {
+	const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
+
+	const isUsername = ps.query.startsWith('@');
+
+	let users: User[] = [];
+
+	if (isUsername) {
+		const usernameQuery = Users.createQueryBuilder('user')
+			.where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
+			.andWhere(new Brackets(qb => { qb
+				.where('user.updatedAt IS NULL')
+				.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+			}))
+			.andWhere('user.isSuspended = FALSE');
+
+		if (ps.origin === 'local') {
+			usernameQuery.andWhere('user.host IS NULL');
+		} else if (ps.origin === 'remote') {
+			usernameQuery.andWhere('user.host IS NOT NULL');
+		}
+
+		users = await usernameQuery
+			.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+			.take(ps.limit!)
+			.skip(ps.offset)
+			.getMany();
+	} else {
+		const nameQuery = Users.createQueryBuilder('user')
+			.where('user.name ILIKE :query', { query: '%' + ps.query + '%' })
+			.andWhere(new Brackets(qb => { qb
+				.where('user.updatedAt IS NULL')
+				.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+			}))
+			.andWhere('user.isSuspended = FALSE');
+
+		if (ps.origin === 'local') {
+			nameQuery.andWhere('user.host IS NULL');
+		} else if (ps.origin === 'remote') {
+			nameQuery.andWhere('user.host IS NOT NULL');
+		}
+
+		users = await nameQuery
+			.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+			.take(ps.limit!)
+			.skip(ps.offset)
+			.getMany();
+
+		if (users.length < ps.limit!) {
+			const profQuery = UserProfiles.createQueryBuilder('prof')
+				.select('prof.userId')
+				.where('prof.description ILIKE :query', { query: '%' + ps.query + '%' });
+
+			if (ps.origin === 'local') {
+				profQuery.andWhere('prof.userHost IS NULL');
+			} else if (ps.origin === 'remote') {
+				profQuery.andWhere('prof.userHost IS NOT NULL');
+			}
+
+			const query = Users.createQueryBuilder('user')
+				.where(`user.id IN (${ profQuery.getQuery() })`)
+				.andWhere(new Brackets(qb => { qb
+					.where('user.updatedAt IS NULL')
+					.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+				}))
+				.andWhere('user.isSuspended = FALSE')
+				.setParameters(profQuery.getParameters());
+
+			users = users.concat(await query
+				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
+				.take(ps.limit!)
+				.skip(ps.offset)
+				.getMany()
+			);
+		}
+	}
+
+	return await Users.packMany(users, me, { detail: ps.detail });
+});
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
new file mode 100644
index 0000000000..f056983636
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -0,0 +1,105 @@
+import $ from 'cafy';
+import { resolveUser } from '@/remote/resolve-user';
+import define from '../../define';
+import { apiLogger } from '../../logger';
+import { ApiError } from '../../error';
+import { ID } from '@/misc/cafy-id';
+import { Users } from '@/models/index';
+import { In } from 'typeorm';
+import { User } from '@/models/entities/user';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		userId: {
+			validator: $.optional.type(ID),
+		},
+
+		userIds: {
+			validator: $.optional.arr($.type(ID)).unique(),
+		},
+
+		username: {
+			validator: $.optional.str
+		},
+
+		host: {
+			validator: $.optional.nullable.str
+		}
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'User',
+	},
+
+	errors: {
+		failedToResolveRemoteUser: {
+			message: 'Failed to resolve remote user.',
+			code: 'FAILED_TO_RESOLVE_REMOTE_USER',
+			id: 'ef7b9be4-9cba-4e6f-ab41-90ed171c7d3c',
+			kind: 'server' as const
+		},
+
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '4362f8dc-731f-4ad8-a694-be5a88922a24'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	let user;
+
+	const isAdminOrModerator = me && (me.isAdmin || me.isModerator);
+
+	if (ps.userIds) {
+		if (ps.userIds.length === 0) {
+			return [];
+		}
+
+		const users = await Users.find(isAdminOrModerator ? {
+			id: In(ps.userIds)
+		} : {
+			id: In(ps.userIds),
+			isSuspended: false
+		});
+
+		// リクエストされた通りに並べ替え
+		const _users: User[] = [];
+		for (const id of ps.userIds) {
+			_users.push(users.find(x => x.id === id)!);
+		}
+
+		return await Promise.all(_users.map(u => Users.pack(u, me, {
+			detail: true
+		})));
+	} else {
+		// Lookup user
+		if (typeof ps.host === 'string' && typeof ps.username === 'string') {
+			user = await resolveUser(ps.username, ps.host).catch(e => {
+				apiLogger.warn(`failed to resolve remote user: ${e}`);
+				throw new ApiError(meta.errors.failedToResolveRemoteUser);
+			});
+		} else {
+			const q: any = ps.userId != null
+				? { id: ps.userId }
+				: { usernameLower: ps.username!.toLowerCase(), host: null };
+
+			user = await Users.findOne(q);
+		}
+
+		if (user == null || (!isAdminOrModerator && user.isSuspended)) {
+			throw new ApiError(meta.errors.noSuchUser);
+		}
+
+		return await Users.pack(user, me, {
+			detail: true
+		});
+	}
+});
diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts
new file mode 100644
index 0000000000..ef8afd5625
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/stats.ts
@@ -0,0 +1,144 @@
+import $ from 'cafy';
+import define from '../../define';
+import { ApiError } from '../../error';
+import { ID } from '@/misc/cafy-id';
+import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, ReversiGames, Users } from '@/models/index';
+
+export const meta = {
+	tags: ['users'],
+
+	requireCredential: false as const,
+
+	params: {
+		userId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchUser: {
+			message: 'No such user.',
+			code: 'NO_SUCH_USER',
+			id: '9e638e45-3b25-4ef7-8f95-07e8498f1819'
+		},
+	}
+};
+
+export default define(meta, async (ps, me) => {
+	const user = await Users.findOne(ps.userId);
+	if (user == null) {
+		throw new ApiError(meta.errors.noSuchUser);
+	}
+
+	const [
+		notesCount,
+		repliesCount,
+		renotesCount,
+		repliedCount,
+		renotedCount,
+		pollVotesCount,
+		pollVotedCount,
+		localFollowingCount,
+		remoteFollowingCount,
+		localFollowersCount,
+		remoteFollowersCount,
+		sentReactionsCount,
+		receivedReactionsCount,
+		noteFavoritesCount,
+		pageLikesCount,
+		pageLikedCount,
+		driveFilesCount,
+		driveUsage,
+		reversiCount,
+	] = await Promise.all([
+		Notes.createQueryBuilder('note')
+			.where('note.userId = :userId', { userId: user.id })
+			.getCount(),
+		Notes.createQueryBuilder('note')
+			.where('note.userId = :userId', { userId: user.id })
+			.andWhere('note.replyId IS NOT NULL')
+			.getCount(),
+		Notes.createQueryBuilder('note')
+			.where('note.userId = :userId', { userId: user.id })
+			.andWhere('note.renoteId IS NOT NULL')
+			.getCount(),
+		Notes.createQueryBuilder('note')
+			.where('note.replyUserId = :userId', { userId: user.id })
+			.getCount(),
+		Notes.createQueryBuilder('note')
+			.where('note.renoteUserId = :userId', { userId: user.id })
+			.getCount(),
+		PollVotes.createQueryBuilder('vote')
+			.where('vote.userId = :userId', { userId: user.id })
+			.getCount(),
+		PollVotes.createQueryBuilder('vote')
+			.innerJoin('vote.note', 'note')
+			.where('note.userId = :userId', { userId: user.id })
+			.getCount(),
+		Followings.createQueryBuilder('following')
+			.where('following.followerId = :userId', { userId: user.id })
+			.andWhere('following.followeeHost IS NULL')
+			.getCount(),
+		Followings.createQueryBuilder('following')
+			.where('following.followerId = :userId', { userId: user.id })
+			.andWhere('following.followeeHost IS NOT NULL')
+			.getCount(),
+		Followings.createQueryBuilder('following')
+			.where('following.followeeId = :userId', { userId: user.id })
+			.andWhere('following.followerHost IS NULL')
+			.getCount(),
+		Followings.createQueryBuilder('following')
+			.where('following.followeeId = :userId', { userId: user.id })
+			.andWhere('following.followerHost IS NOT NULL')
+			.getCount(),
+		NoteReactions.createQueryBuilder('reaction')
+			.where('reaction.userId = :userId', { userId: user.id })
+			.getCount(),
+		NoteReactions.createQueryBuilder('reaction')
+			.innerJoin('reaction.note', 'note')
+			.where('note.userId = :userId', { userId: user.id })
+			.getCount(),
+		NoteFavorites.createQueryBuilder('favorite')
+			.where('favorite.userId = :userId', { userId: user.id })
+			.getCount(),
+		PageLikes.createQueryBuilder('like')
+			.where('like.userId = :userId', { userId: user.id })
+			.getCount(),
+		PageLikes.createQueryBuilder('like')
+			.innerJoin('like.page', 'page')
+			.where('page.userId = :userId', { userId: user.id })
+			.getCount(),
+		DriveFiles.createQueryBuilder('file')
+			.where('file.userId = :userId', { userId: user.id })
+			.getCount(),
+		DriveFiles.calcDriveUsageOf(user),
+		ReversiGames.createQueryBuilder('game')
+			.where('game.user1Id = :userId', { userId: user.id })
+			.orWhere('game.user2Id = :userId', { userId: user.id })
+			.getCount(),
+	]);
+
+	return {
+		notesCount,
+		repliesCount,
+		renotesCount,
+		repliedCount,
+		renotedCount,
+		pollVotesCount,
+		pollVotedCount,
+		localFollowingCount,
+		remoteFollowingCount,
+		localFollowersCount,
+		remoteFollowersCount,
+		followingCount: localFollowingCount + remoteFollowingCount,
+		followersCount: localFollowersCount + remoteFollowersCount,
+		sentReactionsCount,
+		receivedReactionsCount,
+		noteFavoritesCount,
+		pageLikesCount,
+		pageLikedCount,
+		driveFilesCount,
+		driveUsage,
+		reversiCount,
+	};
+});
diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts
new file mode 100644
index 0000000000..cb0bdc9f47
--- /dev/null
+++ b/packages/backend/src/server/api/error.ts
@@ -0,0 +1,28 @@
+type E = { message: string, code: string, id: string, kind?: 'client' | 'server', httpStatusCode?: number };
+
+export class ApiError extends Error {
+	public message: string;
+	public code: string;
+	public id: string;
+	public kind: string;
+	public httpStatusCode?: number;
+	public info?: any;
+
+	constructor(e?: E | null | undefined, info?: any | null | undefined) {
+		if (e == null) e = {
+			message: 'Internal error occurred. Please contact us if the error persists.',
+			code: 'INTERNAL_ERROR',
+			id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
+			kind: 'server',
+			httpStatusCode: 500
+		};
+
+		super(e.message);
+		this.message = e.message;
+		this.code = e.code;
+		this.id = e.id;
+		this.kind = e.kind || 'client';
+		this.httpStatusCode = e.httpStatusCode;
+		this.info = info;
+	}
+}
diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts
new file mode 100644
index 0000000000..82579075eb
--- /dev/null
+++ b/packages/backend/src/server/api/index.ts
@@ -0,0 +1,113 @@
+/**
+ * API Server
+ */
+
+import * as Koa from 'koa';
+import * as Router from '@koa/router';
+import * as multer from '@koa/multer';
+import * as bodyParser from 'koa-bodyparser';
+import * as cors from '@koa/cors';
+
+import endpoints from './endpoints';
+import handler from './api-handler';
+import signup from './private/signup';
+import signin from './private/signin';
+import signupPending from './private/signup-pending';
+import discord from './service/discord';
+import github from './service/github';
+import twitter from './service/twitter';
+import { Instances, AccessTokens, Users } from '@/models/index';
+import config from '@/config';
+
+// Init app
+const app = new Koa();
+
+app.use(cors({
+	origin: '*'
+}));
+
+// No caching
+app.use(async (ctx, next) => {
+	ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	await next();
+});
+
+app.use(bodyParser({
+	// リクエストが multipart/form-data でない限りはJSONだと見なす
+	detectJSON: ctx => !ctx.is('multipart/form-data')
+}));
+
+// Init multer instance
+const upload = multer({
+	storage: multer.diskStorage({}),
+	limits: {
+		fileSize: config.maxFileSize || 262144000,
+		files: 1,
+	}
+});
+
+// Init router
+const router = new Router();
+
+/**
+ * Register endpoint handlers
+ */
+for (const endpoint of endpoints) {
+	if (endpoint.meta.requireFile) {
+		router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint));
+	} else {
+		if (endpoint.name.includes('-')) {
+			// 後方互換性のため
+			router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint));
+		}
+		router.post(`/${endpoint.name}`, handler.bind(null, endpoint));
+	}
+}
+
+router.post('/signup', signup);
+router.post('/signin', signin);
+router.post('/signup-pending', signupPending);
+
+router.use(discord.routes());
+router.use(github.routes());
+router.use(twitter.routes());
+
+router.get('/v1/instance/peers', async ctx => {
+	const instances = await Instances.find({
+		select: ['host']
+	});
+
+	ctx.body = instances.map(instance => instance.host);
+});
+
+router.post('/miauth/:session/check', async ctx => {
+	const token = await AccessTokens.findOne({
+		session: ctx.params.session
+	});
+
+	if (token && token.session != null && !token.fetched) {
+		AccessTokens.update(token.id, {
+			fetched: true
+		});
+
+		ctx.body = {
+			ok: true,
+			token: token.token,
+			user: await Users.pack(token.userId, null, { detail: true })
+		};
+	} else {
+		ctx.body = {
+			ok: false,
+		};
+	}
+});
+
+// Return 404 for unknown API
+router.all('(.*)', async ctx => {
+	ctx.status = 404;
+});
+
+// Register router
+app.use(router.routes());
+
+export default app;
diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts
new file mode 100644
index 0000000000..1e2fe5bcb3
--- /dev/null
+++ b/packages/backend/src/server/api/limiter.ts
@@ -0,0 +1,83 @@
+import * as Limiter from 'ratelimiter';
+import { redisClient } from '../../db/redis';
+import { IEndpoint } from './endpoints';
+import * as Acct from 'misskey-js/built/acct';
+import { User } from '@/models/entities/user';
+import Logger from '@/services/logger';
+
+const logger = new Logger('limiter');
+
+export default (endpoint: IEndpoint, user: User) => new Promise<void>((ok, reject) => {
+	const limitation = endpoint.meta.limit!;
+
+	const key = limitation.hasOwnProperty('key')
+		? limitation.key
+		: endpoint.name;
+
+	const hasShortTermLimit =
+		limitation.hasOwnProperty('minInterval');
+
+	const hasLongTermLimit =
+		limitation.hasOwnProperty('duration') &&
+		limitation.hasOwnProperty('max');
+
+	if (hasShortTermLimit) {
+		min();
+	} else if (hasLongTermLimit) {
+		max();
+	} else {
+		ok();
+	}
+
+	// Short-term limit
+	function min() {
+		const minIntervalLimiter = new Limiter({
+			id: `${user.id}:${key}:min`,
+			duration: limitation.minInterval,
+			max: 1,
+			db: redisClient
+		});
+
+		minIntervalLimiter.get((err, info) => {
+			if (err) {
+				return reject('ERR');
+			}
+
+			logger.debug(`@${Acct.toString(user)} ${endpoint.name} min remaining: ${info.remaining}`);
+
+			if (info.remaining === 0) {
+				reject('BRIEF_REQUEST_INTERVAL');
+			} else {
+				if (hasLongTermLimit) {
+					max();
+				} else {
+					ok();
+				}
+			}
+		});
+	}
+
+	// Long term limit
+	function max() {
+		const limiter = new Limiter({
+			id: `${user.id}:${key}`,
+			duration: limitation.duration,
+			max: limitation.max,
+			db: redisClient
+		});
+
+		limiter.get((err, info) => {
+			if (err) {
+				return reject('ERR');
+			}
+
+			logger.debug(`@${Acct.toString(user)} ${endpoint.name} max remaining: ${info.remaining}`);
+
+			if (info.remaining === 0) {
+				reject('RATE_LIMIT_EXCEEDED');
+			} else {
+				ok();
+			}
+		});
+	}
+});
diff --git a/packages/backend/src/server/api/logger.ts b/packages/backend/src/server/api/logger.ts
new file mode 100644
index 0000000000..750defe547
--- /dev/null
+++ b/packages/backend/src/server/api/logger.ts
@@ -0,0 +1,3 @@
+import Logger from '@/services/logger';
+
+export const apiLogger = new Logger('api');
diff --git a/packages/backend/src/server/api/openapi/errors.ts b/packages/backend/src/server/api/openapi/errors.ts
new file mode 100644
index 0000000000..43bcc323ba
--- /dev/null
+++ b/packages/backend/src/server/api/openapi/errors.ts
@@ -0,0 +1,69 @@
+
+export const errors = {
+	'400': {
+		'INVALID_PARAM': {
+			value: {
+				error: {
+					message: 'Invalid param.',
+					code: 'INVALID_PARAM',
+					id: '3d81ceae-475f-4600-b2a8-2bc116157532',
+				}
+			}
+		}
+	},
+	'401': {
+		'CREDENTIAL_REQUIRED': {
+			value: {
+				error: {
+					message: 'Credential required.',
+					code: 'CREDENTIAL_REQUIRED',
+					id: '1384574d-a912-4b81-8601-c7b1c4085df1',
+				}
+			}
+		}
+	},
+	'403': {
+		'AUTHENTICATION_FAILED': {
+			value: {
+				error: {
+					message: 'Authentication failed. Please ensure your token is correct.',
+					code: 'AUTHENTICATION_FAILED',
+					id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
+				}
+			}
+		}
+	},
+	'418': {
+		'I_AM_AI': {
+			value: {
+				error: {
+					message: 'You sent a request to Ai-chan, Misskey\'s showgirl, instead of the server.',
+					code: 'I_AM_AI',
+					id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84',
+				}
+			}
+		}
+	},
+	'429': {
+		'RATE_LIMIT_EXCEEDED': {
+			value: {
+				error: {
+					message: 'Rate limit exceeded. Please try again later.',
+					code: 'RATE_LIMIT_EXCEEDED',
+					id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+				}
+			}
+		}
+	},
+	'500': {
+		'INTERNAL_ERROR': {
+			value: {
+				error: {
+					message: 'Internal error occurred. Please contact us if the error persists.',
+					code: 'INTERNAL_ERROR',
+					id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
+				}
+			}
+		}
+	}
+};
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
new file mode 100644
index 0000000000..48b819727f
--- /dev/null
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -0,0 +1,235 @@
+import endpoints from '../endpoints';
+import { Context } from 'cafy';
+import config from '@/config/index';
+import { errors as basicErrors } from './errors';
+import { schemas, convertSchemaToOpenApiSchema } from './schemas';
+
+export function genOpenapiSpec(lang = 'ja-JP') {
+	const spec = {
+		openapi: '3.0.0',
+
+		info: {
+			version: 'v1',
+			title: 'Misskey API',
+			'x-logo': { url: '/static-assets/api-doc.png' }
+		},
+
+		externalDocs: {
+			description: 'Repository',
+			url: 'https://github.com/misskey-dev/misskey'
+		},
+
+		servers: [{
+			url: config.apiUrl
+		}],
+
+		paths: {} as any,
+
+		components: {
+			schemas: schemas,
+
+			securitySchemes: {
+				ApiKeyAuth: {
+					type: 'apiKey',
+					in: 'body',
+					name: 'i'
+				}
+			}
+		}
+	};
+
+	function genProps(props: { [key: string]: Context; }) {
+		const properties = {} as any;
+
+		for (const [k, v] of Object.entries(props)) {
+			properties[k] = genProp(v);
+		}
+
+		return properties;
+	}
+
+	function genProp(param: Context): any {
+		const required = param.name === 'Object' ? (param as any).props ? Object.entries((param as any).props).filter(([k, v]: any) => !v.isOptional).map(([k, v]) => k) : [] : [];
+		return {
+			description: (param.data || {}).desc,
+			default: (param.data || {}).default,
+			deprecated: (param.data || {}).deprecated,
+			...((param.data || {}).default ? { default: (param.data || {}).default } : {}),
+			type: param.name === 'ID' ? 'string' : param.name.toLowerCase(),
+			...(param.name === 'ID' ? { example: 'xxxxxxxxxx', format: 'id' } : {}),
+			nullable: param.isNullable,
+			...(param.name === 'String' ? {
+				...((param as any).enum ? { enum: (param as any).enum } : {}),
+				...((param as any).minLength ? { minLength: (param as any).minLength } : {}),
+				...((param as any).maxLength ? { maxLength: (param as any).maxLength } : {}),
+			} : {}),
+			...(param.name === 'Number' ? {
+				...((param as any).minimum ? { minimum: (param as any).minimum } : {}),
+				...((param as any).maximum ? { maximum: (param as any).maximum } : {}),
+			} : {}),
+			...(param.name === 'Object' ? {
+				...(required.length > 0 ? { required } : {}),
+				properties: (param as any).props ? genProps((param as any).props) : {}
+			} : {}),
+			...(param.name === 'Array' ? {
+				items: (param as any).ctx ? genProp((param as any).ctx) : {}
+			} : {})
+		};
+	}
+
+	for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
+		const porops = {} as any;
+		const errors = {} as any;
+
+		if (endpoint.meta.errors) {
+			for (const e of Object.values(endpoint.meta.errors)) {
+				errors[e.code] = {
+					value: {
+						error: e
+					}
+				};
+			}
+		}
+
+		if (endpoint.meta.params) {
+			for (const [k, v] of Object.entries(endpoint.meta.params)) {
+				if (v.validator.data == null) v.validator.data = {};
+				if (v.desc) v.validator.data.desc = v.desc[lang];
+				if (v.deprecated) v.validator.data.deprecated = v.deprecated;
+				if (v.default) v.validator.data.default = v.default;
+				porops[k] = v.validator;
+			}
+		}
+
+		const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : [];
+
+		const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
+
+		let desc = (endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.') + '\n\n';
+		desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`;
+		if (endpoint.meta.kind) {
+			const kind = endpoint.meta.kind;
+			desc += ` / **Permission**: *${kind}*`;
+		}
+
+		const info = {
+			operationId: endpoint.name,
+			summary: endpoint.name,
+			description: desc,
+			externalDocs: {
+				description: 'Source code',
+				url: `https://github.com/misskey-dev/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts`
+			},
+			...(endpoint.meta.tags ? {
+				tags: [endpoint.meta.tags[0]]
+			} : {}),
+			...(endpoint.meta.requireCredential ? {
+				security: [{
+					ApiKeyAuth: []
+				}]
+			} : {}),
+			requestBody: {
+				required: true,
+				content: {
+					'application/json': {
+						schema: {
+							type: 'object',
+							...(required.length > 0 ? { required } : {}),
+							properties: endpoint.meta.params ? genProps(porops) : {}
+						}
+					}
+				}
+			},
+			responses: {
+				...(endpoint.meta.res ? {
+					'200': {
+						description: 'OK (with results)',
+						content: {
+							'application/json': {
+								schema: resSchema
+							}
+						}
+					}
+				} : {
+					'204': {
+						description: 'OK (without any results)',
+					}
+				}),
+				'400': {
+					description: 'Client error',
+					content: {
+						'application/json': {
+							schema: {
+								$ref: '#/components/schemas/Error'
+							},
+							examples: { ...errors, ...basicErrors['400'] }
+						}
+					}
+				},
+				'401': {
+					description: 'Authentication error',
+					content: {
+						'application/json': {
+							schema: {
+								$ref: '#/components/schemas/Error'
+							},
+							examples: basicErrors['401']
+						}
+					}
+				},
+				'403': {
+					description: 'Forbidden error',
+					content: {
+						'application/json': {
+							schema: {
+								$ref: '#/components/schemas/Error'
+							},
+							examples: basicErrors['403']
+						}
+					}
+				},
+				'418': {
+					description: 'I\'m Ai',
+					content: {
+						'application/json': {
+							schema: {
+								$ref: '#/components/schemas/Error'
+							},
+							examples: basicErrors['418']
+						}
+					}
+				},
+				...(endpoint.meta.limit ? {
+					'429': {
+						description: 'To many requests',
+						content: {
+							'application/json': {
+								schema: {
+									$ref: '#/components/schemas/Error'
+								},
+								examples: basicErrors['429']
+							}
+						}
+					}
+				} : {}),
+				'500': {
+					description: 'Internal server error',
+					content: {
+						'application/json': {
+							schema: {
+								$ref: '#/components/schemas/Error'
+							},
+							examples: basicErrors['500']
+						}
+					}
+				},
+			}
+		};
+
+		spec.paths['/' + endpoint.name] = {
+			post: info
+		};
+	}
+
+	return spec;
+}
diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts
new file mode 100644
index 0000000000..12fc207c47
--- /dev/null
+++ b/packages/backend/src/server/api/openapi/schemas.ts
@@ -0,0 +1,56 @@
+import { refs, Schema } from '@/misc/schema';
+
+export function convertSchemaToOpenApiSchema(schema: Schema) {
+	const res: any = schema;
+
+	if (schema.type === 'object' && schema.properties) {
+		res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
+
+		for (const k of Object.keys(schema.properties)) {
+			res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
+		}
+	}
+
+	if (schema.type === 'array' && schema.items) {
+		res.items = convertSchemaToOpenApiSchema(schema.items);
+	}
+
+	if (schema.ref) {
+		res.$ref = `#/components/schemas/${schema.ref}`;
+	}
+
+	return res;
+}
+
+export const schemas = {
+	Error: {
+		type: 'object',
+		properties: {
+			error: {
+				type: 'object',
+				description: 'An error object.',
+				properties: {
+					code: {
+						type: 'string',
+						description: 'An error code. Unique within the endpoint.',
+					},
+					message: {
+						type: 'string',
+						description: 'An error message.',
+					},
+					id: {
+						type: 'string',
+						format: 'uuid',
+						description: 'An error ID. This ID is static.',
+					}
+				},
+				required: ['code', 'id', 'message']
+			},
+		},
+		required: ['error']
+	},
+
+	...Object.fromEntries(
+		Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)])
+	),
+};
diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts
new file mode 100644
index 0000000000..83c3dfee94
--- /dev/null
+++ b/packages/backend/src/server/api/private/signin.ts
@@ -0,0 +1,232 @@
+import * as Koa from 'koa';
+import * as bcrypt from 'bcryptjs';
+import * as speakeasy from 'speakeasy';
+import signin from '../common/signin';
+import config from '@/config/index';
+import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index';
+import { ILocalUser } from '@/models/entities/user';
+import { genId } from '@/misc/gen-id';
+import { verifyLogin, hash } from '../2fa';
+import { randomBytes } from 'crypto';
+
+export default async (ctx: Koa.Context) => {
+	ctx.set('Access-Control-Allow-Origin', config.url);
+	ctx.set('Access-Control-Allow-Credentials', 'true');
+
+	const body = ctx.request.body as any;
+	const username = body['username'];
+	const password = body['password'];
+	const token = body['token'];
+
+	function error(status: number, error: { id: string }) {
+		ctx.status = status;
+		ctx.body = { error };
+	}
+
+	if (typeof username != 'string') {
+		ctx.status = 400;
+		return;
+	}
+
+	if (typeof password != 'string') {
+		ctx.status = 400;
+		return;
+	}
+
+	if (token != null && typeof token != 'string') {
+		ctx.status = 400;
+		return;
+	}
+
+	// Fetch user
+	const user = await Users.findOne({
+		usernameLower: username.toLowerCase(),
+		host: null
+	}) as ILocalUser;
+
+	if (user == null) {
+		error(404, {
+			id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
+		});
+		return;
+	}
+
+	if (user.isSuspended) {
+		error(403, {
+			id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
+		});
+		return;
+	}
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	// Compare password
+	const same = await bcrypt.compare(password, profile.password!);
+
+	async function fail(status?: number, failure?: { id: string }) {
+		// Append signin history
+		await Signins.insert({
+			id: genId(),
+			createdAt: new Date(),
+			userId: user.id,
+			ip: ctx.ip,
+			headers: ctx.headers,
+			success: false
+		});
+
+		error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
+	}
+
+	if (!profile.twoFactorEnabled) {
+		if (same) {
+			signin(ctx, user);
+			return;
+		} else {
+			await fail(403, {
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
+			});
+			return;
+		}
+	}
+
+	if (token) {
+		if (!same) {
+			await fail(403, {
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
+			});
+			return;
+		}
+
+		const verified = (speakeasy as any).totp.verify({
+			secret: profile.twoFactorSecret,
+			encoding: 'base32',
+			token: token,
+			window: 2
+		});
+
+		if (verified) {
+			signin(ctx, user);
+			return;
+		} else {
+			await fail(403, {
+				id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f'
+			});
+			return;
+		}
+	} else if (body.credentialId) {
+		if (!same && !profile.usePasswordLessLogin) {
+			await fail(403, {
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
+			});
+			return;
+		}
+
+		const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
+		const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
+		const challenge = await AttestationChallenges.findOne({
+			userId: user.id,
+			id: body.challengeId,
+			registrationChallenge: false,
+			challenge: hash(clientData.challenge).toString('hex')
+		});
+
+		if (!challenge) {
+			await fail(403, {
+				id: '2715a88a-2125-4013-932f-aa6fe72792da'
+			});
+			return;
+		}
+
+		await AttestationChallenges.delete({
+			userId: user.id,
+			id: body.challengeId
+		});
+
+		if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
+			await fail(403, {
+				id: '2715a88a-2125-4013-932f-aa6fe72792da'
+			});
+			return;
+		}
+
+		const securityKey = await UserSecurityKeys.findOne({
+			id: Buffer.from(
+				body.credentialId
+					.replace(/-/g, '+')
+					.replace(/_/g, '/'),
+					'base64'
+			).toString('hex')
+		});
+
+		if (!securityKey) {
+			await fail(403, {
+				id: '66269679-aeaf-4474-862b-eb761197e046'
+			});
+			return;
+		}
+
+		const isValid = verifyLogin({
+			publicKey: Buffer.from(securityKey.publicKey, 'hex'),
+			authenticatorData: Buffer.from(body.authenticatorData, 'hex'),
+			clientDataJSON,
+			clientData,
+			signature: Buffer.from(body.signature, 'hex'),
+			challenge: challenge.challenge
+		});
+
+		if (isValid) {
+			signin(ctx, user);
+			return;
+		} else {
+			await fail(403, {
+				id: '93b86c4b-72f9-40eb-9815-798928603d1e'
+			});
+			return;
+		}
+	} else {
+		if (!same && !profile.usePasswordLessLogin) {
+			await fail(403, {
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
+			});
+			return;
+		}
+
+		const keys = await UserSecurityKeys.find({
+			userId: user.id
+		});
+
+		if (keys.length === 0) {
+			await fail(403, {
+				id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4'
+			});
+			return;
+		}
+
+		// 32 byte challenge
+		const challenge = randomBytes(32).toString('base64')
+			.replace(/=/g, '')
+			.replace(/\+/g, '-')
+			.replace(/\//g, '_');
+
+		const challengeId = genId();
+
+		await AttestationChallenges.insert({
+			userId: user.id,
+			id: challengeId,
+			challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
+			createdAt: new Date(),
+			registrationChallenge: false
+		});
+
+		ctx.body = {
+			challenge,
+			challengeId,
+			securityKeys: keys.map(key => ({
+				id: key.id
+			}))
+		};
+		ctx.status = 200;
+		return;
+	}
+	// never get here
+};
diff --git a/packages/backend/src/server/api/private/signup-pending.ts b/packages/backend/src/server/api/private/signup-pending.ts
new file mode 100644
index 0000000000..c0638a1cda
--- /dev/null
+++ b/packages/backend/src/server/api/private/signup-pending.ts
@@ -0,0 +1,35 @@
+import * as Koa from 'koa';
+import { Users, UserPendings, UserProfiles } from '@/models/index';
+import { signup } from '../common/signup';
+import signin from '../common/signin';
+
+export default async (ctx: Koa.Context) => {
+	const body = ctx.request.body;
+
+	const code = body['code'];
+
+	try {
+		const pendingUser = await UserPendings.findOneOrFail({ code });
+
+		const { account, secret } = await signup({
+			username: pendingUser.username,
+			passwordHash: pendingUser.password,
+		});
+
+		UserPendings.delete({
+			id: pendingUser.id,
+		});
+
+		const profile = await UserProfiles.findOneOrFail(account.id);
+
+		await UserProfiles.update({ userId: profile.userId }, {
+			email: pendingUser.email,
+			emailVerified: true,
+			emailVerifyCode: null,
+		});
+
+		signin(ctx, account);
+	} catch (e) {
+		ctx.throw(400, e);
+	}
+};
diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts
new file mode 100644
index 0000000000..2b6a3eb00c
--- /dev/null
+++ b/packages/backend/src/server/api/private/signup.ts
@@ -0,0 +1,112 @@
+import * as Koa from 'koa';
+import rndstr from 'rndstr';
+import * as bcrypt from 'bcryptjs';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha';
+import { Users, RegistrationTickets, UserPendings } from '@/models/index';
+import { signup } from '../common/signup';
+import config from '@/config';
+import { sendEmail } from '@/services/send-email';
+import { genId } from '@/misc/gen-id';
+import { validateEmailForAccount } from '@/services/validate-email-for-account';
+
+export default async (ctx: Koa.Context) => {
+	const body = ctx.request.body;
+
+	const instance = await fetchMeta(true);
+
+	// Verify *Captcha
+	// ただしテスト時はこの機構は障害となるため無効にする
+	if (process.env.NODE_ENV !== 'test') {
+		if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
+			await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
+				ctx.throw(400, e);
+			});
+		}
+
+		if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
+			await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
+				ctx.throw(400, e);
+			});
+		}
+	}
+
+	const username = body['username'];
+	const password = body['password'];
+	const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null;
+	const invitationCode = body['invitationCode'];
+	const emailAddress = body['emailAddress'];
+
+	if (instance.emailRequiredForSignup) {
+		if (emailAddress == null || typeof emailAddress != 'string') {
+			ctx.status = 400;
+			return;
+		}
+
+		const available = await validateEmailForAccount(emailAddress);
+		if (!available) {
+			ctx.status = 400;
+			return;
+		}
+	}
+
+	if (instance.disableRegistration) {
+		if (invitationCode == null || typeof invitationCode != 'string') {
+			ctx.status = 400;
+			return;
+		}
+
+		const ticket = await RegistrationTickets.findOne({
+			code: invitationCode
+		});
+
+		if (ticket == null) {
+			ctx.status = 400;
+			return;
+		}
+
+		RegistrationTickets.delete(ticket.id);
+	}
+
+	if (instance.emailRequiredForSignup) {
+		const code = rndstr('a-z0-9', 16);
+
+		// Generate hash of password
+		const salt = await bcrypt.genSalt(8);
+		const hash = await bcrypt.hash(password, salt);
+
+		await UserPendings.insert({
+			id: genId(),
+			createdAt: new Date(),
+			code,
+			email: emailAddress,
+			username: username,
+			password: hash,
+		});
+
+		const link = `${config.url}/signup-complete/${code}`;
+
+		sendEmail(emailAddress, 'Signup',
+			`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
+			`To complete signup, please click this link: ${link}`);
+
+		ctx.status = 204;
+	} else {
+		try {
+			const { account, secret } = await signup({
+				username, password, host
+			});
+
+			const res = await Users.pack(account, account, {
+				detail: true,
+				includeSecrets: true
+			});
+
+			(res as any).token = secret;
+
+			ctx.body = res;
+		} catch (e) {
+			ctx.throw(400, e);
+		}
+	}
+};
diff --git a/packages/backend/src/server/api/service/discord.ts b/packages/backend/src/server/api/service/discord.ts
new file mode 100644
index 0000000000..dd52a23376
--- /dev/null
+++ b/packages/backend/src/server/api/service/discord.ts
@@ -0,0 +1,286 @@
+import * as Koa from 'koa';
+import * as Router from '@koa/router';
+import { getJson } from '@/misc/fetch';
+import { OAuth2 } from 'oauth';
+import config from '@/config/index';
+import { publishMainStream } from '@/services/stream';
+import { redisClient } from '../../../db/redis';
+import { v4 as uuid } from 'uuid';
+import signin from '../common/signin';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Users, UserProfiles } from '@/models/index';
+import { ILocalUser } from '@/models/entities/user';
+
+function getUserToken(ctx: Koa.Context) {
+	return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
+}
+
+function compareOrigin(ctx: Koa.Context) {
+	function normalizeUrl(url: string) {
+		return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
+	}
+
+	const referer = ctx.headers['referer'];
+
+	return (normalizeUrl(referer) == normalizeUrl(config.url));
+}
+
+// Init router
+const router = new Router();
+
+router.get('/disconnect/discord', async ctx => {
+	if (!compareOrigin(ctx)) {
+		ctx.throw(400, 'invalid origin');
+		return;
+	}
+
+	const userToken = getUserToken(ctx);
+	if (!userToken) {
+		ctx.throw(400, 'signin required');
+		return;
+	}
+
+	const user = await Users.findOneOrFail({
+		host: null,
+		token: userToken
+	});
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	delete profile.integrations.discord;
+
+	await UserProfiles.update(user.id, {
+		integrations: profile.integrations,
+	});
+
+	ctx.body = `Discordの連携を解除しました :v:`;
+
+	// Publish i updated event
+	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
+		detail: true,
+		includeSecrets: true
+	}));
+});
+
+async function getOAuth2() {
+	const meta = await fetchMeta(true);
+
+	if (meta.enableDiscordIntegration) {
+		return new OAuth2(
+			meta.discordClientId!,
+			meta.discordClientSecret!,
+			'https://discord.com/',
+			'api/oauth2/authorize',
+			'api/oauth2/token');
+	} else {
+		return null;
+	}
+}
+
+router.get('/connect/discord', async ctx => {
+	if (!compareOrigin(ctx)) {
+		ctx.throw(400, 'invalid origin');
+		return;
+	}
+
+	const userToken = getUserToken(ctx);
+	if (!userToken) {
+		ctx.throw(400, 'signin required');
+		return;
+	}
+
+	const params = {
+		redirect_uri: `${config.url}/api/dc/cb`,
+		scope: ['identify'],
+		state: uuid(),
+		response_type: 'code'
+	};
+
+	redisClient.set(userToken, JSON.stringify(params));
+
+	const oauth2 = await getOAuth2();
+	ctx.redirect(oauth2!.getAuthorizeUrl(params));
+});
+
+router.get('/signin/discord', async ctx => {
+	const sessid = uuid();
+
+	const params = {
+		redirect_uri: `${config.url}/api/dc/cb`,
+		scope: ['identify'],
+		state: uuid(),
+		response_type: 'code'
+	};
+
+	ctx.cookies.set('signin_with_discord_sid', sessid, {
+		path: '/',
+		secure: config.url.startsWith('https'),
+		httpOnly: true
+	});
+
+	redisClient.set(sessid, JSON.stringify(params));
+
+	const oauth2 = await getOAuth2();
+	ctx.redirect(oauth2!.getAuthorizeUrl(params));
+});
+
+router.get('/dc/cb', async ctx => {
+	const userToken = getUserToken(ctx);
+
+	const oauth2 = await getOAuth2();
+
+	if (!userToken) {
+		const sessid = ctx.cookies.get('signin_with_discord_sid');
+
+		if (!sessid) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const code = ctx.query.code;
+
+		if (!code) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const { redirect_uri, state } = await new Promise<any>((res, rej) => {
+			redisClient.get(sessid, async (_, state) => {
+				res(JSON.parse(state));
+			});
+		});
+
+		if (ctx.query.state !== state) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
+			oauth2!.getOAuthAccessToken(code, {
+				grant_type: 'authorization_code',
+				redirect_uri
+			}, (err, accessToken, refreshToken, result) => {
+				if (err) {
+					rej(err);
+				} else if (result.error) {
+					rej(result.error);
+				} else {
+					res({
+						accessToken,
+						refreshToken,
+						expiresDate: Date.now() + Number(result.expires_in) * 1000
+					});
+				}
+			}));
+
+		const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
+			'Authorization': `Bearer ${accessToken}`,
+		});
+
+		if (!id || !username || !discriminator) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const profile = await UserProfiles.createQueryBuilder()
+			.where(`"integrations"->'discord'->>'id' = :id`, { id: id })
+			.andWhere('"userHost" IS NULL')
+			.getOne();
+
+		if (profile == null) {
+			ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
+			return;
+		}
+
+		await UserProfiles.update(profile.userId, {
+			integrations: {
+				...profile.integrations,
+				discord: {
+					id: id,
+					accessToken: accessToken,
+					refreshToken: refreshToken,
+					expiresDate: expiresDate,
+					username: username,
+					discriminator: discriminator
+				}
+			},
+		});
+
+		signin(ctx, await Users.findOne(profile.userId) as ILocalUser, true);
+	} else {
+		const code = ctx.query.code;
+
+		if (!code) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const { redirect_uri, state } = await new Promise<any>((res, rej) => {
+			redisClient.get(userToken, async (_, state) => {
+				res(JSON.parse(state));
+			});
+		});
+
+		if (ctx.query.state !== state) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
+			oauth2!.getOAuthAccessToken(code, {
+				grant_type: 'authorization_code',
+				redirect_uri
+			}, (err, accessToken, refreshToken, result) => {
+				if (err) {
+					rej(err);
+				} else if (result.error) {
+					rej(result.error);
+				} else {
+					res({
+						accessToken,
+						refreshToken,
+						expiresDate: Date.now() + Number(result.expires_in) * 1000
+					});
+				}
+			}));
+
+		const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
+			'Authorization': `Bearer ${accessToken}`,
+		});
+		if (!id || !username || !discriminator) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const user = await Users.findOneOrFail({
+			host: null,
+			token: userToken
+		});
+
+		const profile = await UserProfiles.findOneOrFail(user.id);
+
+		await UserProfiles.update(user.id, {
+			integrations: {
+				...profile.integrations,
+				discord: {
+					accessToken: accessToken,
+					refreshToken: refreshToken,
+					expiresDate: expiresDate,
+					id: id,
+					username: username,
+					discriminator: discriminator
+				}
+			}
+		});
+
+		ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました！`;
+
+		// Publish i updated event
+		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
+			detail: true,
+			includeSecrets: true
+		}));
+	}
+});
+
+export default router;
diff --git a/packages/backend/src/server/api/service/github.ts b/packages/backend/src/server/api/service/github.ts
new file mode 100644
index 0000000000..0616f3f773
--- /dev/null
+++ b/packages/backend/src/server/api/service/github.ts
@@ -0,0 +1,257 @@
+import * as Koa from 'koa';
+import * as Router from '@koa/router';
+import { getJson } from '@/misc/fetch';
+import { OAuth2 } from 'oauth';
+import config from '@/config/index';
+import { publishMainStream } from '@/services/stream';
+import { redisClient } from '../../../db/redis';
+import { v4 as uuid } from 'uuid';
+import signin from '../common/signin';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Users, UserProfiles } from '@/models/index';
+import { ILocalUser } from '@/models/entities/user';
+
+function getUserToken(ctx: Koa.Context) {
+	return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
+}
+
+function compareOrigin(ctx: Koa.Context) {
+	function normalizeUrl(url: string) {
+		return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
+	}
+
+	const referer = ctx.headers['referer'];
+
+	return (normalizeUrl(referer) == normalizeUrl(config.url));
+}
+
+// Init router
+const router = new Router();
+
+router.get('/disconnect/github', async ctx => {
+	if (!compareOrigin(ctx)) {
+		ctx.throw(400, 'invalid origin');
+		return;
+	}
+
+	const userToken = getUserToken(ctx);
+	if (!userToken) {
+		ctx.throw(400, 'signin required');
+		return;
+	}
+
+	const user = await Users.findOneOrFail({
+		host: null,
+		token: userToken
+	});
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	delete profile.integrations.github;
+
+	await UserProfiles.update(user.id, {
+		integrations: profile.integrations,
+	});
+
+	ctx.body = `GitHubの連携を解除しました :v:`;
+
+	// Publish i updated event
+	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
+		detail: true,
+		includeSecrets: true
+	}));
+});
+
+async function getOath2() {
+	const meta = await fetchMeta(true);
+
+	if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) {
+		return new OAuth2(
+			meta.githubClientId,
+			meta.githubClientSecret,
+			'https://github.com/',
+			'login/oauth/authorize',
+			'login/oauth/access_token');
+	} else {
+		return null;
+	}
+}
+
+router.get('/connect/github', async ctx => {
+	if (!compareOrigin(ctx)) {
+		ctx.throw(400, 'invalid origin');
+		return;
+	}
+
+	const userToken = getUserToken(ctx);
+	if (!userToken) {
+		ctx.throw(400, 'signin required');
+		return;
+	}
+
+	const params = {
+		redirect_uri: `${config.url}/api/gh/cb`,
+		scope: ['read:user'],
+		state: uuid()
+	};
+
+	redisClient.set(userToken, JSON.stringify(params));
+
+	const oauth2 = await getOath2();
+	ctx.redirect(oauth2!.getAuthorizeUrl(params));
+});
+
+router.get('/signin/github', async ctx => {
+	const sessid = uuid();
+
+	const params = {
+		redirect_uri: `${config.url}/api/gh/cb`,
+		scope: ['read:user'],
+		state: uuid()
+	};
+
+	ctx.cookies.set('signin_with_github_sid', sessid, {
+		path: '/',
+		secure: config.url.startsWith('https'),
+		httpOnly: true
+	});
+
+	redisClient.set(sessid, JSON.stringify(params));
+
+	const oauth2 = await getOath2();
+	ctx.redirect(oauth2!.getAuthorizeUrl(params));
+});
+
+router.get('/gh/cb', async ctx => {
+	const userToken = getUserToken(ctx);
+
+	const oauth2 = await getOath2();
+
+	if (!userToken) {
+		const sessid = ctx.cookies.get('signin_with_github_sid');
+
+		if (!sessid) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const code = ctx.query.code;
+
+		if (!code) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const { redirect_uri, state } = await new Promise<any>((res, rej) => {
+			redisClient.get(sessid, async (_, state) => {
+				res(JSON.parse(state));
+			});
+		});
+
+		if (ctx.query.state !== state) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const { accessToken } = await new Promise<any>((res, rej) =>
+			oauth2!.getOAuthAccessToken(code, {
+				redirect_uri
+			}, (err, accessToken, refresh, result) => {
+				if (err) {
+					rej(err);
+				} else if (result.error) {
+					rej(result.error);
+				} else {
+					res({ accessToken });
+				}
+			}));
+
+		const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
+			'Authorization': `bearer ${accessToken}`
+		});
+		if (!login || !id) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const link = await UserProfiles.createQueryBuilder()
+			.where(`"integrations"->'github'->>'id' = :id`, { id: id })
+			.andWhere('"userHost" IS NULL')
+			.getOne();
+
+		if (link == null) {
+			ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
+			return;
+		}
+
+		signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
+	} else {
+		const code = ctx.query.code;
+
+		if (!code) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const { redirect_uri, state } = await new Promise<any>((res, rej) => {
+			redisClient.get(userToken, async (_, state) => {
+				res(JSON.parse(state));
+			});
+		});
+
+		if (ctx.query.state !== state) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const { accessToken } = await new Promise<any>((res, rej) =>
+			oauth2!.getOAuthAccessToken(
+				code,
+				{ redirect_uri },
+				(err, accessToken, refresh, result) => {
+					if (err)
+						rej(err);
+					else if (result.error)
+						rej(result.error);
+					else
+						res({ accessToken });
+				}));
+
+		const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
+			'Authorization': `bearer ${accessToken}`
+		});
+
+		if (!login || !id) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const user = await Users.findOneOrFail({
+			host: null,
+			token: userToken
+		});
+
+		const profile = await UserProfiles.findOneOrFail(user.id);
+
+		await UserProfiles.update(user.id, {
+			integrations: {
+				...profile.integrations,
+				github: {
+					accessToken: accessToken,
+					id: id,
+					login: login,
+				}
+			}
+		});
+
+		ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました！`;
+
+		// Publish i updated event
+		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
+			detail: true,
+			includeSecrets: true
+		}));
+	}
+});
+
+export default router;
diff --git a/packages/backend/src/server/api/service/twitter.ts b/packages/backend/src/server/api/service/twitter.ts
new file mode 100644
index 0000000000..8a6a58aeee
--- /dev/null
+++ b/packages/backend/src/server/api/service/twitter.ts
@@ -0,0 +1,194 @@
+import * as Koa from 'koa';
+import * as Router from '@koa/router';
+import { v4 as uuid } from 'uuid';
+import autwh from 'autwh';
+import { redisClient } from '../../../db/redis';
+import { publishMainStream } from '@/services/stream';
+import config from '@/config/index';
+import signin from '../common/signin';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Users, UserProfiles } from '@/models/index';
+import { ILocalUser } from '@/models/entities/user';
+
+function getUserToken(ctx: Koa.Context) {
+	return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
+}
+
+function compareOrigin(ctx: Koa.Context) {
+	function normalizeUrl(url: string) {
+		return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
+	}
+
+	const referer = ctx.headers['referer'];
+
+	return (normalizeUrl(referer) == normalizeUrl(config.url));
+}
+
+// Init router
+const router = new Router();
+
+router.get('/disconnect/twitter', async ctx => {
+	if (!compareOrigin(ctx)) {
+		ctx.throw(400, 'invalid origin');
+		return;
+	}
+
+	const userToken = getUserToken(ctx);
+	if (userToken == null) {
+		ctx.throw(400, 'signin required');
+		return;
+	}
+
+	const user = await Users.findOneOrFail({
+		host: null,
+		token: userToken
+	});
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	delete profile.integrations.twitter;
+
+	await UserProfiles.update(user.id, {
+		integrations: profile.integrations,
+	});
+
+	ctx.body = `Twitterの連携を解除しました :v:`;
+
+	// Publish i updated event
+	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
+		detail: true,
+		includeSecrets: true
+	}));
+});
+
+async function getTwAuth() {
+	const meta = await fetchMeta(true);
+
+	if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) {
+		return autwh({
+			consumerKey: meta.twitterConsumerKey,
+			consumerSecret: meta.twitterConsumerSecret,
+			callbackUrl: `${config.url}/api/tw/cb`
+		});
+	} else {
+		return null;
+	}
+}
+
+router.get('/connect/twitter', async ctx => {
+	if (!compareOrigin(ctx)) {
+		ctx.throw(400, 'invalid origin');
+		return;
+	}
+
+	const userToken = getUserToken(ctx);
+	if (userToken == null) {
+		ctx.throw(400, 'signin required');
+		return;
+	}
+
+	const twAuth = await getTwAuth();
+	const twCtx = await twAuth!.begin();
+	redisClient.set(userToken, JSON.stringify(twCtx));
+	ctx.redirect(twCtx.url);
+});
+
+router.get('/signin/twitter', async ctx => {
+	const twAuth = await getTwAuth();
+	const twCtx = await twAuth!.begin();
+
+	const sessid = uuid();
+
+	redisClient.set(sessid, JSON.stringify(twCtx));
+
+	ctx.cookies.set('signin_with_twitter_sid', sessid, {
+		path: '/',
+		secure: config.url.startsWith('https'),
+		httpOnly: true
+	});
+
+	ctx.redirect(twCtx.url);
+});
+
+router.get('/tw/cb', async ctx => {
+	const userToken = getUserToken(ctx);
+
+	const twAuth = await getTwAuth();
+
+	if (userToken == null) {
+		const sessid = ctx.cookies.get('signin_with_twitter_sid');
+
+		if (sessid == null) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const get = new Promise<any>((res, rej) => {
+			redisClient.get(sessid, async (_, twCtx) => {
+				res(twCtx);
+			});
+		});
+
+		const twCtx = await get;
+
+		const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
+
+		const link = await UserProfiles.createQueryBuilder()
+			.where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId })
+			.andWhere('"userHost" IS NULL')
+			.getOne();
+
+		if (link == null) {
+			ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
+			return;
+		}
+
+		signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
+	} else {
+		const verifier = ctx.query.oauth_verifier;
+
+		if (verifier == null) {
+			ctx.throw(400, 'invalid session');
+			return;
+		}
+
+		const get = new Promise<any>((res, rej) => {
+			redisClient.get(userToken, async (_, twCtx) => {
+				res(twCtx);
+			});
+		});
+
+		const twCtx = await get;
+
+		const result = await twAuth!.done(JSON.parse(twCtx), verifier);
+
+		const user = await Users.findOneOrFail({
+			host: null,
+			token: userToken
+		});
+
+		const profile = await UserProfiles.findOneOrFail(user.id);
+
+		await UserProfiles.update(user.id, {
+			integrations: {
+				...profile.integrations,
+				twitter: {
+					accessToken: result.accessToken,
+					accessTokenSecret: result.accessTokenSecret,
+					userId: result.userId,
+					screenName: result.screenName,
+				}
+			},
+		});
+
+		ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました！`;
+
+		// Publish i updated event
+		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
+			detail: true,
+			includeSecrets: true
+		}));
+	}
+});
+
+export default router;
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
new file mode 100644
index 0000000000..2824d7d1b8
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -0,0 +1,62 @@
+import autobind from 'autobind-decorator';
+import Connection from '.';
+
+/**
+ * Stream channel
+ */
+export default abstract class Channel {
+	protected connection: Connection;
+	public id: string;
+	public abstract readonly chName: string;
+	public static readonly shouldShare: boolean;
+	public static readonly requireCredential: boolean;
+
+	protected get user() {
+		return this.connection.user;
+	}
+
+	protected get userProfile() {
+		return this.connection.userProfile;
+	}
+
+	protected get following() {
+		return this.connection.following;
+	}
+
+	protected get muting() {
+		return this.connection.muting;
+	}
+
+	protected get blocking() {
+		return this.connection.blocking;
+	}
+
+	protected get followingChannels() {
+		return this.connection.followingChannels;
+	}
+
+	protected get subscriber() {
+		return this.connection.subscriber;
+	}
+
+	constructor(id: string, connection: Connection) {
+		this.id = id;
+		this.connection = connection;
+	}
+
+	@autobind
+	public send(typeOrPayload: any, payload?: any) {
+		const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
+		const body = payload === undefined ? typeOrPayload.body : payload;
+
+		this.connection.sendMessageToWs('channel', {
+			id: this.id,
+			type: type,
+			body: body
+		});
+	}
+
+	public abstract init(params: any): void;
+	public dispose?(): void;
+	public onMessage?(type: string, body: any): void;
+}
diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts
new file mode 100644
index 0000000000..1ff932d1dd
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/admin.ts
@@ -0,0 +1,16 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+
+export default class extends Channel {
+	public readonly chName = 'admin';
+	public static shouldShare = true;
+	public static requireCredential = true;
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe admin stream
+		this.subscriber.on(`adminStream:${this.user!.id}`, data => {
+			this.send(data);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
new file mode 100644
index 0000000000..3cbdfebb43
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -0,0 +1,45 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+import { Notes } from '@/models/index';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { StreamMessages } from '../types';
+
+export default class extends Channel {
+	public readonly chName = 'antenna';
+	public static shouldShare = false;
+	public static requireCredential = false;
+	private antennaId: string;
+
+	@autobind
+	public async init(params: any) {
+		this.antennaId = params.antennaId as string;
+
+		// Subscribe stream
+		this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
+	}
+
+	@autobind
+	private async onEvent(data: StreamMessages['antenna']['payload']) {
+		if (data.type === 'note') {
+			const note = await Notes.pack(data.body.id, this.user, { detail: true });
+
+			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+			if (isMutedUserRelated(note, this.muting)) return;
+			// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
+			if (isBlockerUserRelated(note, this.blocking)) return;
+
+			this.connection.cacheNote(note);
+
+			this.send('note', note);
+		} else {
+			this.send(data.type, data.body);
+		}
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
new file mode 100644
index 0000000000..bf7942f522
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -0,0 +1,92 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+import { Notes, Users } from '@/models/index';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { User } from '@/models/entities/user';
+import { StreamMessages } from '../types';
+import { Packed } from '@/misc/schema';
+
+export default class extends Channel {
+	public readonly chName = 'channel';
+	public static shouldShare = false;
+	public static requireCredential = false;
+	private channelId: string;
+	private typers: Record<User['id'], Date> = {};
+	private emitTypersIntervalId: ReturnType<typeof setInterval>;
+
+	@autobind
+	public async init(params: any) {
+		this.channelId = params.channelId as string;
+
+		// Subscribe stream
+		this.subscriber.on('notesStream', this.onNote);
+		this.subscriber.on(`channelStream:${this.channelId}`, this.onEvent);
+		this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
+	}
+
+	@autobind
+	private async onNote(note: Packed<'Note'>) {
+		if (note.channelId !== this.channelId) return;
+
+		// リプライなら再pack
+		if (note.replyId != null) {
+			note.reply = await Notes.pack(note.replyId, this.user, {
+				detail: true
+			});
+		}
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await Notes.pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (isMutedUserRelated(note, this.muting)) return;
+		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
+		if (isBlockerUserRelated(note, this.blocking)) return;
+
+		this.connection.cacheNote(note);
+
+		this.send('note', note);
+	}
+
+	@autobind
+	private onEvent(data: StreamMessages['channel']['payload']) {
+		if (data.type === 'typing') {
+			const id = data.body;
+			const begin = this.typers[id] == null;
+			this.typers[id] = new Date();
+			if (begin) {
+				this.emitTypers();
+			}
+		}
+	}
+
+	@autobind
+	private async emitTypers() {
+		const now = new Date();
+
+		// Remove not typing users
+		for (const [userId, date] of Object.entries(this.typers)) {
+			if (now.getTime() - date.getTime() > 5000) delete this.typers[userId];
+		}
+
+		const users = await Users.packMany(Object.keys(this.typers), null, { detail: false });
+
+		this.send({
+			type: 'typers',
+			body: users,
+		});
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('notesStream', this.onNote);
+		this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent);
+
+		clearInterval(this.emitTypersIntervalId);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts
new file mode 100644
index 0000000000..4112dd9b04
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/drive.ts
@@ -0,0 +1,16 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+
+export default class extends Channel {
+	public readonly chName = 'drive';
+	public static shouldShare = true;
+	public static requireCredential = true;
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe drive stream
+		this.subscriber.on(`driveStream:${this.user!.id}`, data => {
+			this.send(data);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/games/reversi-game.ts b/packages/backend/src/server/api/stream/channels/games/reversi-game.ts
new file mode 100644
index 0000000000..bfdbf1d266
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/games/reversi-game.ts
@@ -0,0 +1,372 @@
+import autobind from 'autobind-decorator';
+import * as CRC32 from 'crc-32';
+import { publishReversiGameStream } from '@/services/stream';
+import Reversi from '../../../../../games/reversi/core';
+import * as maps from '../../../../../games/reversi/maps';
+import Channel from '../../channel';
+import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { ReversiGames, Users } from '@/models/index';
+import { User } from '@/models/entities/user';
+
+export default class extends Channel {
+	public readonly chName = 'gamesReversiGame';
+	public static shouldShare = false;
+	public static requireCredential = false;
+
+	private gameId: ReversiGame['id'] | null = null;
+	private watchers: Record<User['id'], Date> = {};
+	private emitWatchersIntervalId: ReturnType<typeof setInterval>;
+
+	@autobind
+	public async init(params: any) {
+		this.gameId = params.gameId;
+
+		// Subscribe game stream
+		this.subscriber.on(`reversiGameStream:${this.gameId}`, this.onEvent);
+		this.emitWatchersIntervalId = setInterval(this.emitWatchers, 5000);
+
+		const game = await ReversiGames.findOne(this.gameId!);
+		if (game == null) throw new Error('game not found');
+
+		// 観戦者イベント
+		this.watch(game);
+	}
+
+	@autobind
+	private onEvent(data: any) {
+		if (data.type === 'watching') {
+			const id = data.body;
+			this.watchers[id] = new Date();
+		} else {
+			this.send(data);
+		}
+	}
+
+	@autobind
+	private async emitWatchers() {
+		const now = new Date();
+
+		// Remove not watching users
+		for (const [userId, date] of Object.entries(this.watchers)) {
+			if (now.getTime() - date.getTime() > 5000) delete this.watchers[userId];
+		}
+
+		const users = await Users.packMany(Object.keys(this.watchers), null, { detail: false });
+
+		this.send({
+			type: 'watchers',
+			body: users,
+		});
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off(`reversiGameStream:${this.gameId}`, this.onEvent);
+		clearInterval(this.emitWatchersIntervalId);
+	}
+
+	@autobind
+	public onMessage(type: string, body: any) {
+		switch (type) {
+			case 'accept': this.accept(true); break;
+			case 'cancelAccept': this.accept(false); break;
+			case 'updateSettings': this.updateSettings(body.key, body.value); break;
+			case 'initForm': this.initForm(body); break;
+			case 'updateForm': this.updateForm(body.id, body.value); break;
+			case 'message': this.message(body); break;
+			case 'set': this.set(body.pos); break;
+			case 'check': this.check(body.crc32); break;
+		}
+	}
+
+	@autobind
+	private async updateSettings(key: string, value: any) {
+		if (this.user == null) return;
+
+		const game = await ReversiGames.findOne(this.gameId!);
+		if (game == null) throw new Error('game not found');
+
+		if (game.isStarted) return;
+		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
+		if ((game.user1Id === this.user.id) && game.user1Accepted) return;
+		if ((game.user2Id === this.user.id) && game.user2Accepted) return;
+
+		if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
+
+		await ReversiGames.update(this.gameId!, {
+			[key]: value
+		});
+
+		publishReversiGameStream(this.gameId!, 'updateSettings', {
+			key: key,
+			value: value
+		});
+	}
+
+	@autobind
+	private async initForm(form: any) {
+		if (this.user == null) return;
+
+		const game = await ReversiGames.findOne(this.gameId!);
+		if (game == null) throw new Error('game not found');
+
+		if (game.isStarted) return;
+		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
+
+		const set = game.user1Id === this.user.id ? {
+			form1: form
+		} : {
+			form2: form
+		};
+
+		await ReversiGames.update(this.gameId!, set);
+
+		publishReversiGameStream(this.gameId!, 'initForm', {
+			userId: this.user.id,
+			form
+		});
+	}
+
+	@autobind
+	private async updateForm(id: string, value: any) {
+		if (this.user == null) return;
+
+		const game = await ReversiGames.findOne(this.gameId!);
+		if (game == null) throw new Error('game not found');
+
+		if (game.isStarted) return;
+		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
+
+		const form = game.user1Id === this.user.id ? game.form2 : game.form1;
+
+		const item = form.find((i: any) => i.id == id);
+
+		if (item == null) return;
+
+		item.value = value;
+
+		const set = game.user1Id === this.user.id ? {
+			form2: form
+		} : {
+				form1: form
+			};
+
+		await ReversiGames.update(this.gameId!, set);
+
+		publishReversiGameStream(this.gameId!, 'updateForm', {
+			userId: this.user.id,
+			id,
+			value
+		});
+	}
+
+	@autobind
+	private async message(message: any) {
+		if (this.user == null) return;
+
+		message.id = Math.random();
+		publishReversiGameStream(this.gameId!, 'message', {
+			userId: this.user.id,
+			message
+		});
+	}
+
+	@autobind
+	private async accept(accept: boolean) {
+		if (this.user == null) return;
+
+		const game = await ReversiGames.findOne(this.gameId!);
+		if (game == null) throw new Error('game not found');
+
+		if (game.isStarted) return;
+
+		let bothAccepted = false;
+
+		if (game.user1Id === this.user.id) {
+			await ReversiGames.update(this.gameId!, {
+				user1Accepted: accept
+			});
+
+			publishReversiGameStream(this.gameId!, 'changeAccepts', {
+				user1: accept,
+				user2: game.user2Accepted
+			});
+
+			if (accept && game.user2Accepted) bothAccepted = true;
+		} else if (game.user2Id === this.user.id) {
+			await ReversiGames.update(this.gameId!, {
+				user2Accepted: accept
+			});
+
+			publishReversiGameStream(this.gameId!, 'changeAccepts', {
+				user1: game.user1Accepted,
+				user2: accept
+			});
+
+			if (accept && game.user1Accepted) bothAccepted = true;
+		} else {
+			return;
+		}
+
+		if (bothAccepted) {
+			// 3秒後、まだacceptされていたらゲーム開始
+			setTimeout(async () => {
+				const freshGame = await ReversiGames.findOne(this.gameId!);
+				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
+				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
+
+				let bw: number;
+				if (freshGame.bw == 'random') {
+					bw = Math.random() > 0.5 ? 1 : 2;
+				} else {
+					bw = parseInt(freshGame.bw, 10);
+				}
+
+				function getRandomMap() {
+					const mapCount = Object.entries(maps).length;
+					const rnd = Math.floor(Math.random() * mapCount);
+					return Object.values(maps)[rnd].data;
+				}
+
+				const map = freshGame.map != null ? freshGame.map : getRandomMap();
+
+				await ReversiGames.update(this.gameId!, {
+					startedAt: new Date(),
+					isStarted: true,
+					black: bw,
+					map: map
+				});
+
+				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
+				const o = new Reversi(map, {
+					isLlotheo: freshGame.isLlotheo,
+					canPutEverywhere: freshGame.canPutEverywhere,
+					loopedBoard: freshGame.loopedBoard
+				});
+
+				if (o.isEnded) {
+					let winner;
+					if (o.winner === true) {
+						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id;
+					} else if (o.winner === false) {
+						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id;
+					} else {
+						winner = null;
+					}
+
+					await ReversiGames.update(this.gameId!, {
+						isEnded: true,
+						winnerId: winner
+					});
+
+					publishReversiGameStream(this.gameId!, 'ended', {
+						winnerId: winner,
+						game: await ReversiGames.pack(this.gameId!, this.user)
+					});
+				}
+				//#endregion
+
+				publishReversiGameStream(this.gameId!, 'started',
+					await ReversiGames.pack(this.gameId!, this.user));
+			}, 3000);
+		}
+	}
+
+	// 石を打つ
+	@autobind
+	private async set(pos: number) {
+		if (this.user == null) return;
+
+		const game = await ReversiGames.findOne(this.gameId!);
+		if (game == null) throw new Error('game not found');
+
+		if (!game.isStarted) return;
+		if (game.isEnded) return;
+		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
+
+		const myColor =
+			((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
+				? true
+				: false;
+
+		const o = new Reversi(game.map, {
+			isLlotheo: game.isLlotheo,
+			canPutEverywhere: game.canPutEverywhere,
+			loopedBoard: game.loopedBoard
+		});
+
+		// 盤面の状態を再生
+		for (const log of game.logs) {
+			o.put(log.color, log.pos);
+		}
+
+		if (o.turn !== myColor) return;
+
+		if (!o.canPut(myColor, pos)) return;
+		o.put(myColor, pos);
+
+		let winner;
+		if (o.isEnded) {
+			if (o.winner === true) {
+				winner = game.black == 1 ? game.user1Id : game.user2Id;
+			} else if (o.winner === false) {
+				winner = game.black == 1 ? game.user2Id : game.user1Id;
+			} else {
+				winner = null;
+			}
+		}
+
+		const log = {
+			at: new Date(),
+			color: myColor,
+			pos
+		};
+
+		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
+
+		game.logs.push(log);
+
+		await ReversiGames.update(this.gameId!, {
+			crc32,
+			isEnded: o.isEnded,
+			winnerId: winner,
+			logs: game.logs
+		});
+
+		publishReversiGameStream(this.gameId!, 'set', Object.assign(log, {
+			next: o.turn
+		}));
+
+		if (o.isEnded) {
+			publishReversiGameStream(this.gameId!, 'ended', {
+				winnerId: winner,
+				game: await ReversiGames.pack(this.gameId!, this.user)
+			});
+		}
+	}
+
+	@autobind
+	private async check(crc32: string | number) {
+		const game = await ReversiGames.findOne(this.gameId!);
+		if (game == null) throw new Error('game not found');
+
+		if (!game.isStarted) return;
+
+		if (crc32.toString() !== game.crc32) {
+			this.send('rescue', await ReversiGames.pack(game, this.user));
+		}
+
+		// ついでに観戦者イベントを発行
+		this.watch(game);
+	}
+
+	@autobind
+	private watch(game: ReversiGame) {
+		if (this.user != null) {
+			if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) {
+				publishReversiGameStream(this.gameId!, 'watching', this.user.id);
+			}
+		}
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/games/reversi.ts b/packages/backend/src/server/api/stream/channels/games/reversi.ts
new file mode 100644
index 0000000000..3b89aac35c
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/games/reversi.ts
@@ -0,0 +1,33 @@
+import autobind from 'autobind-decorator';
+import { publishMainStream } from '@/services/stream';
+import Channel from '../../channel';
+import { ReversiMatchings } from '@/models/index';
+
+export default class extends Channel {
+	public readonly chName = 'gamesReversi';
+	public static shouldShare = true;
+	public static requireCredential = true;
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe reversi stream
+		this.subscriber.on(`reversiStream:${this.user!.id}`, data => {
+			this.send(data);
+		});
+	}
+
+	@autobind
+	public async onMessage(type: string, body: any) {
+		switch (type) {
+			case 'ping':
+				if (body.id == null) return;
+				const matching = await ReversiMatchings.findOne({
+					parentId: this.user!.id,
+					childId: body.id
+				});
+				if (matching == null) return;
+				publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, { id: matching.childId }));
+				break;
+		}
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
new file mode 100644
index 0000000000..f5983ab472
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -0,0 +1,73 @@
+import autobind from 'autobind-decorator';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import Channel from '../channel';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Notes } from '@/models/index';
+import { checkWordMute } from '@/misc/check-word-mute';
+import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
+
+export default class extends Channel {
+	public readonly chName = 'globalTimeline';
+	public static shouldShare = true;
+	public static requireCredential = false;
+
+	@autobind
+	public async init(params: any) {
+		const meta = await fetchMeta();
+		if (meta.disableGlobalTimeline) {
+			if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return;
+		}
+
+		// Subscribe events
+		this.subscriber.on('notesStream', this.onNote);
+	}
+
+	@autobind
+	private async onNote(note: Packed<'Note'>) {
+		if (note.visibility !== 'public') return;
+		if (note.channelId != null) return;
+
+		// リプライなら再pack
+		if (note.replyId != null) {
+			note.reply = await Notes.pack(note.replyId, this.user, {
+				detail: true
+			});
+		}
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await Notes.pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 関係ない返信は除外
+		if (note.reply) {
+			const reply = note.reply;
+			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
+			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (isMutedUserRelated(note, this.muting)) return;
+		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
+		if (isBlockerUserRelated(note, this.blocking)) return;
+
+		// 流れてきたNoteがミュートすべきNoteだったら無視する
+		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
+		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
+		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
+		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
+		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+
+		this.connection.cacheNote(note);
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('notesStream', this.onNote);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
new file mode 100644
index 0000000000..281be4f2eb
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -0,0 +1,53 @@
+import autobind from 'autobind-decorator';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import Channel from '../channel';
+import { Notes } from '@/models/index';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
+
+export default class extends Channel {
+	public readonly chName = 'hashtag';
+	public static shouldShare = false;
+	public static requireCredential = false;
+	private q: string[][];
+
+	@autobind
+	public async init(params: any) {
+		this.q = params.q;
+
+		if (this.q == null) return;
+
+		// Subscribe stream
+		this.subscriber.on('notesStream', this.onNote);
+	}
+
+	@autobind
+	private async onNote(note: Packed<'Note'>) {
+		const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
+		const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
+		if (!matched) return;
+
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await Notes.pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (isMutedUserRelated(note, this.muting)) return;
+		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
+		if (isBlockerUserRelated(note, this.blocking)) return;
+
+		this.connection.cacheNote(note);
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('notesStream', this.onNote);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
new file mode 100644
index 0000000000..52e9aec250
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -0,0 +1,81 @@
+import autobind from 'autobind-decorator';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import Channel from '../channel';
+import { Notes } from '@/models/index';
+import { checkWordMute } from '@/misc/check-word-mute';
+import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
+
+export default class extends Channel {
+	public readonly chName = 'homeTimeline';
+	public static shouldShare = true;
+	public static requireCredential = true;
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe events
+		this.subscriber.on('notesStream', this.onNote);
+	}
+
+	@autobind
+	private async onNote(note: Packed<'Note'>) {
+		if (note.channelId) {
+			if (!this.followingChannels.has(note.channelId)) return;
+		} else {
+			// その投稿のユーザーをフォローしていなかったら弾く
+			if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return;
+		}
+
+		if (['followers', 'specified'].includes(note.visibility)) {
+			note = await Notes.pack(note.id, this.user!, {
+				detail: true
+			});
+
+			if (note.isHidden) {
+				return;
+			}
+		} else {
+			// リプライなら再pack
+			if (note.replyId != null) {
+				note.reply = await Notes.pack(note.replyId, this.user!, {
+					detail: true
+				});
+			}
+			// Renoteなら再pack
+			if (note.renoteId != null) {
+				note.renote = await Notes.pack(note.renoteId, this.user!, {
+					detail: true
+				});
+			}
+		}
+
+		// 関係ない返信は除外
+		if (note.reply) {
+			const reply = note.reply;
+			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
+			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (isMutedUserRelated(note, this.muting)) return;
+		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
+		if (isBlockerUserRelated(note, this.blocking)) return;
+
+		// 流れてきたNoteがミュートすべきNoteだったら無視する
+		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
+		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
+		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
+		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
+		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+
+		this.connection.cacheNote(note);
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('notesStream', this.onNote);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
new file mode 100644
index 0000000000..51f95fc0cd
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -0,0 +1,89 @@
+import autobind from 'autobind-decorator';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import Channel from '../channel';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Notes } from '@/models/index';
+import { checkWordMute } from '@/misc/check-word-mute';
+import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
+
+export default class extends Channel {
+	public readonly chName = 'hybridTimeline';
+	public static shouldShare = true;
+	public static requireCredential = true;
+
+	@autobind
+	public async init(params: any) {
+		const meta = await fetchMeta();
+		if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return;
+
+		// Subscribe events
+		this.subscriber.on('notesStream', this.onNote);
+	}
+
+	@autobind
+	private async onNote(note: Packed<'Note'>) {
+		// チャンネルの投稿ではなく、自分自身の投稿 または
+		// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
+		// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
+		// フォローしているチャンネルの投稿 の場合だけ
+		if (!(
+			(note.channelId == null && this.user!.id === note.userId) ||
+			(note.channelId == null && this.following.has(note.userId)) ||
+			(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
+			(note.channelId != null && this.followingChannels.has(note.channelId))
+		)) return;
+
+		if (['followers', 'specified'].includes(note.visibility)) {
+			note = await Notes.pack(note.id, this.user!, {
+				detail: true
+			});
+
+			if (note.isHidden) {
+				return;
+			}
+		} else {
+			// リプライなら再pack
+			if (note.replyId != null) {
+				note.reply = await Notes.pack(note.replyId, this.user!, {
+					detail: true
+				});
+			}
+			// Renoteなら再pack
+			if (note.renoteId != null) {
+				note.renote = await Notes.pack(note.renoteId, this.user!, {
+					detail: true
+				});
+			}
+		}
+
+		// 関係ない返信は除外
+		if (note.reply) {
+			const reply = note.reply;
+			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
+			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (isMutedUserRelated(note, this.muting)) return;
+		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
+		if (isBlockerUserRelated(note, this.blocking)) return;
+
+		// 流れてきたNoteがミュートすべきNoteだったら無視する
+		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
+		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
+		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
+		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
+		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+
+		this.connection.cacheNote(note);
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('notesStream', this.onNote);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/index.ts b/packages/backend/src/server/api/stream/channels/index.ts
new file mode 100644
index 0000000000..1841573043
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/index.ts
@@ -0,0 +1,37 @@
+import main from './main';
+import homeTimeline from './home-timeline';
+import localTimeline from './local-timeline';
+import hybridTimeline from './hybrid-timeline';
+import globalTimeline from './global-timeline';
+import serverStats from './server-stats';
+import queueStats from './queue-stats';
+import userList from './user-list';
+import antenna from './antenna';
+import messaging from './messaging';
+import messagingIndex from './messaging-index';
+import drive from './drive';
+import hashtag from './hashtag';
+import channel from './channel';
+import admin from './admin';
+import gamesReversi from './games/reversi';
+import gamesReversiGame from './games/reversi-game';
+
+export default {
+	main,
+	homeTimeline,
+	localTimeline,
+	hybridTimeline,
+	globalTimeline,
+	serverStats,
+	queueStats,
+	userList,
+	antenna,
+	messaging,
+	messagingIndex,
+	drive,
+	hashtag,
+	channel,
+	admin,
+	gamesReversi,
+	gamesReversiGame
+};
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
new file mode 100644
index 0000000000..a6166c2be2
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -0,0 +1,74 @@
+import autobind from 'autobind-decorator';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import Channel from '../channel';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Notes } from '@/models/index';
+import { checkWordMute } from '@/misc/check-word-mute';
+import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
+
+export default class extends Channel {
+	public readonly chName = 'localTimeline';
+	public static shouldShare = true;
+	public static requireCredential = false;
+
+	@autobind
+	public async init(params: any) {
+		const meta = await fetchMeta();
+		if (meta.disableLocalTimeline) {
+			if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return;
+		}
+
+		// Subscribe events
+		this.subscriber.on('notesStream', this.onNote);
+	}
+
+	@autobind
+	private async onNote(note: Packed<'Note'>) {
+		if (note.user.host !== null) return;
+		if (note.visibility !== 'public') return;
+		if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
+
+		// リプライなら再pack
+		if (note.replyId != null) {
+			note.reply = await Notes.pack(note.replyId, this.user, {
+				detail: true
+			});
+		}
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await Notes.pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 関係ない返信は除外
+		if (note.reply) {
+			const reply = note.reply;
+			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
+			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (isMutedUserRelated(note, this.muting)) return;
+		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
+		if (isBlockerUserRelated(note, this.blocking)) return;
+
+		// 流れてきたNoteがミュートすべきNoteだったら無視する
+		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
+		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
+		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
+		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
+		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+
+		this.connection.cacheNote(note);
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('notesStream', this.onNote);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts
new file mode 100644
index 0000000000..131ac30472
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/main.ts
@@ -0,0 +1,43 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+import { Notes } from '@/models/index';
+
+export default class extends Channel {
+	public readonly chName = 'main';
+	public static shouldShare = true;
+	public static requireCredential = true;
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe main stream channel
+		this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
+			switch (data.type) {
+				case 'notification': {
+					if (data.body.userId && this.muting.has(data.body.userId)) return;
+
+					if (data.body.note && data.body.note.isHidden) {
+						const note = await Notes.pack(data.body.note.id, this.user, {
+							detail: true
+						});
+						this.connection.cacheNote(note);
+						data.body.note = note;
+					}
+					break;
+				}
+				case 'mention': {
+					if (this.muting.has(data.body.userId)) return;
+					if (data.body.isHidden) {
+						const note = await Notes.pack(data.body.id, this.user, {
+							detail: true
+						});
+						this.connection.cacheNote(note);
+						data.body = note;
+					}
+					break;
+				}
+			}
+
+			this.send(data.type, data.body);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/messaging-index.ts b/packages/backend/src/server/api/stream/channels/messaging-index.ts
new file mode 100644
index 0000000000..0c495398ab
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/messaging-index.ts
@@ -0,0 +1,16 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+
+export default class extends Channel {
+	public readonly chName = 'messagingIndex';
+	public static shouldShare = true;
+	public static requireCredential = true;
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe messaging index stream
+		this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => {
+			this.send(data);
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts
new file mode 100644
index 0000000000..c049e880b9
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/messaging.ts
@@ -0,0 +1,106 @@
+import autobind from 'autobind-decorator';
+import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message';
+import Channel from '../channel';
+import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index';
+import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
+import { UserGroup } from '@/models/entities/user-group';
+import { StreamMessages } from '../types';
+
+export default class extends Channel {
+	public readonly chName = 'messaging';
+	public static shouldShare = false;
+	public static requireCredential = true;
+
+	private otherpartyId: string | null;
+	private otherparty: User | null;
+	private groupId: string | null;
+	private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`;
+	private typers: Record<User['id'], Date> = {};
+	private emitTypersIntervalId: ReturnType<typeof setInterval>;
+
+	@autobind
+	public async init(params: any) {
+		this.otherpartyId = params.otherparty;
+		this.otherparty = this.otherpartyId ? await Users.findOneOrFail({ id: this.otherpartyId }) : null;
+		this.groupId = params.group;
+
+		// Check joining
+		if (this.groupId) {
+			const joining = await UserGroupJoinings.findOne({
+				userId: this.user!.id,
+				userGroupId: this.groupId
+			});
+
+			if (joining == null) {
+				return;
+			}
+		}
+
+		this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
+
+		this.subCh = this.otherpartyId
+			? `messagingStream:${this.user!.id}-${this.otherpartyId}`
+			: `messagingStream:${this.groupId}`;
+
+		// Subscribe messaging stream
+		this.subscriber.on(this.subCh, this.onEvent);
+	}
+
+	@autobind
+	private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) {
+		if (data.type === 'typing') {
+			const id = data.body;
+			const begin = this.typers[id] == null;
+			this.typers[id] = new Date();
+			if (begin) {
+				this.emitTypers();
+			}
+		} else {
+			this.send(data);
+		}
+	}
+
+	@autobind
+	public onMessage(type: string, body: any) {
+		switch (type) {
+			case 'read':
+				if (this.otherpartyId) {
+					readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]);
+
+					// リモートユーザーからのメッセージだったら既読配信
+					if (Users.isLocalUser(this.user!) && Users.isRemoteUser(this.otherparty!)) {
+						MessagingMessages.findOne(body.id).then(message => {
+							if (message) deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message);
+						});
+					}
+				} else if (this.groupId) {
+					readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]);
+				}
+				break;
+		}
+	}
+
+	@autobind
+	private async emitTypers() {
+		const now = new Date();
+
+		// Remove not typing users
+		for (const [userId, date] of Object.entries(this.typers)) {
+			if (now.getTime() - date.getTime() > 5000) delete this.typers[userId];
+		}
+
+		const users = await Users.packMany(Object.keys(this.typers), null, { detail: false });
+
+		this.send({
+			type: 'typers',
+			body: users,
+		});
+	}
+
+	@autobind
+	public dispose() {
+		this.subscriber.off(this.subCh, this.onEvent);
+
+		clearInterval(this.emitTypersIntervalId);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts
new file mode 100644
index 0000000000..0bda0cfcb9
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts
@@ -0,0 +1,41 @@
+import autobind from 'autobind-decorator';
+import Xev from 'xev';
+import Channel from '../channel';
+
+const ev = new Xev();
+
+export default class extends Channel {
+	public readonly chName = 'queueStats';
+	public static shouldShare = true;
+	public static requireCredential = false;
+
+	@autobind
+	public async init(params: any) {
+		ev.addListener('queueStats', this.onStats);
+	}
+
+	@autobind
+	private onStats(stats: any) {
+		this.send('stats', stats);
+	}
+
+	@autobind
+	public onMessage(type: string, body: any) {
+		switch (type) {
+			case 'requestLog':
+				ev.once(`queueStatsLog:${body.id}`, statsLog => {
+					this.send('statsLog', statsLog);
+				});
+				ev.emit('requestQueueStatsLog', {
+					id: body.id,
+					length: body.length
+				});
+				break;
+		}
+	}
+
+	@autobind
+	public dispose() {
+		ev.removeListener('queueStats', this.onStats);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts
new file mode 100644
index 0000000000..d245a7f70c
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/server-stats.ts
@@ -0,0 +1,41 @@
+import autobind from 'autobind-decorator';
+import Xev from 'xev';
+import Channel from '../channel';
+
+const ev = new Xev();
+
+export default class extends Channel {
+	public readonly chName = 'serverStats';
+	public static shouldShare = true;
+	public static requireCredential = false;
+
+	@autobind
+	public async init(params: any) {
+		ev.addListener('serverStats', this.onStats);
+	}
+
+	@autobind
+	private onStats(stats: any) {
+		this.send('stats', stats);
+	}
+
+	@autobind
+	public onMessage(type: string, body: any) {
+		switch (type) {
+			case 'requestLog':
+				ev.once(`serverStatsLog:${body.id}`, statsLog => {
+					this.send('statsLog', statsLog);
+				});
+				ev.emit('requestServerStatsLog', {
+					id: body.id,
+					length: body.length
+				});
+				break;
+		}
+	}
+
+	@autobind
+	public dispose() {
+		ev.removeListener('serverStats', this.onStats);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
new file mode 100644
index 0000000000..63b254605b
--- /dev/null
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -0,0 +1,92 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+import { Notes, UserListJoinings, UserLists } from '@/models/index';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import { User } from '@/models/entities/user';
+import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
+import { Packed } from '@/misc/schema';
+
+export default class extends Channel {
+	public readonly chName = 'userList';
+	public static shouldShare = false;
+	public static requireCredential = false;
+	private listId: string;
+	public listUsers: User['id'][] = [];
+	private listUsersClock: NodeJS.Timer;
+
+	@autobind
+	public async init(params: any) {
+		this.listId = params.listId as string;
+
+		// Check existence and owner
+		const list = await UserLists.findOne({
+			id: this.listId,
+			userId: this.user!.id
+		});
+		if (!list) return;
+
+		// Subscribe stream
+		this.subscriber.on(`userListStream:${this.listId}`, this.send);
+
+		this.subscriber.on('notesStream', this.onNote);
+
+		this.updateListUsers();
+		this.listUsersClock = setInterval(this.updateListUsers, 5000);
+	}
+
+	@autobind
+	private async updateListUsers() {
+		const users = await UserListJoinings.find({
+			where: {
+				userListId: this.listId,
+			},
+			select: ['userId']
+		});
+
+		this.listUsers = users.map(x => x.userId);
+	}
+
+	@autobind
+	private async onNote(note: Packed<'Note'>) {
+		if (!this.listUsers.includes(note.userId)) return;
+
+		if (['followers', 'specified'].includes(note.visibility)) {
+			note = await Notes.pack(note.id, this.user, {
+				detail: true
+			});
+
+			if (note.isHidden) {
+				return;
+			}
+		} else {
+			// リプライなら再pack
+			if (note.replyId != null) {
+				note.reply = await Notes.pack(note.replyId, this.user, {
+					detail: true
+				});
+			}
+			// Renoteなら再pack
+			if (note.renoteId != null) {
+				note.renote = await Notes.pack(note.renoteId, this.user, {
+					detail: true
+				});
+			}
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (isMutedUserRelated(note, this.muting)) return;
+		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
+		if (isBlockerUserRelated(note, this.blocking)) return;
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off(`userListStream:${this.listId}`, this.send);
+		this.subscriber.off('notesStream', this.onNote);
+
+		clearInterval(this.listUsersClock);
+	}
+}
diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts
new file mode 100644
index 0000000000..da4ea5ec99
--- /dev/null
+++ b/packages/backend/src/server/api/stream/index.ts
@@ -0,0 +1,421 @@
+import autobind from 'autobind-decorator';
+import * as websocket from 'websocket';
+import { readNotification } from '../common/read-notification';
+import call from '../call';
+import readNote from '@/services/note/read';
+import Channel from './channel';
+import channels from './channels/index';
+import { EventEmitter } from 'events';
+import { User } from '@/models/entities/user';
+import { Channel as ChannelModel } from '@/models/entities/channel';
+import { Users, Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index';
+import { ApiError } from '../error';
+import { AccessToken } from '@/models/entities/access-token';
+import { UserProfile } from '@/models/entities/user-profile';
+import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream';
+import { UserGroup } from '@/models/entities/user-group';
+import { StreamEventEmitter, StreamMessages } from './types';
+import { Packed } from '@/misc/schema';
+
+/**
+ * Main stream connection
+ */
+export default class Connection {
+	public user?: User;
+	public userProfile?: UserProfile;
+	public following: Set<User['id']> = new Set();
+	public muting: Set<User['id']> = new Set();
+	public blocking: Set<User['id']> = new Set(); // "被"blocking
+	public followingChannels: Set<ChannelModel['id']> = new Set();
+	public token?: AccessToken;
+	private wsConnection: websocket.connection;
+	public subscriber: StreamEventEmitter;
+	private channels: Channel[] = [];
+	private subscribingNotes: any = {};
+	private cachedNotes: Packed<'Note'>[] = [];
+
+	constructor(
+		wsConnection: websocket.connection,
+		subscriber: EventEmitter,
+		user: User | null | undefined,
+		token: AccessToken | null | undefined
+	) {
+		this.wsConnection = wsConnection;
+		this.subscriber = subscriber;
+		if (user) this.user = user;
+		if (token) this.token = token;
+
+		this.wsConnection.on('message', this.onWsConnectionMessage);
+
+		this.subscriber.on('broadcast', data => {
+			this.onBroadcastMessage(data);
+		});
+
+		if (this.user) {
+			this.updateFollowing();
+			this.updateMuting();
+			this.updateBlocking();
+			this.updateFollowingChannels();
+			this.updateUserProfile();
+
+			this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
+		}
+	}
+
+	@autobind
+	private onUserEvent(data: StreamMessages['user']['payload']) { // { type, body }と展開するとそれぞれ型が分離してしまう
+		switch (data.type) {
+			case 'follow':
+				this.following.add(data.body.id);
+				break;
+
+			case 'unfollow':
+				this.following.delete(data.body.id);
+				break;
+
+			case 'mute':
+				this.muting.add(data.body.id);
+				break;
+
+			case 'unmute':
+				this.muting.delete(data.body.id);
+				break;
+
+			// TODO: block events
+
+			case 'followChannel':
+				this.followingChannels.add(data.body.id);
+				break;
+
+			case 'unfollowChannel':
+				this.followingChannels.delete(data.body.id);
+				break;
+
+			case 'updateUserProfile':
+				this.userProfile = data.body;
+				break;
+
+			case 'terminate':
+				this.wsConnection.close();
+				this.dispose();
+				break;
+
+			default:
+				break;
+		}
+	}
+
+	/**
+	 * クライアントからメッセージ受信時
+	 */
+	@autobind
+	private async onWsConnectionMessage(data: websocket.IMessage) {
+		if (data.utf8Data == null) return;
+
+		let obj: Record<string, any>;
+
+		try {
+			obj = JSON.parse(data.utf8Data);
+		} catch (e) {
+			return;
+		}
+
+		const { type, body } = obj;
+
+		switch (type) {
+			case 'api': this.onApiRequest(body); break;
+			case 'readNotification': this.onReadNotification(body); break;
+			case 'subNote': this.onSubscribeNote(body); break;
+			case 's': this.onSubscribeNote(body); break; // alias
+			case 'sr': this.onSubscribeNote(body); this.readNote(body); break;
+			case 'unsubNote': this.onUnsubscribeNote(body); break;
+			case 'un': this.onUnsubscribeNote(body); break; // alias
+			case 'connect': this.onChannelConnectRequested(body); break;
+			case 'disconnect': this.onChannelDisconnectRequested(body); break;
+			case 'channel': this.onChannelMessageRequested(body); break;
+			case 'ch': this.onChannelMessageRequested(body); break; // alias
+
+			// 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、
+			// クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別
+			// なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。
+			case 'typingOnChannel': this.typingOnChannel(body.channel); break;
+			case 'typingOnMessaging': this.typingOnMessaging(body); break;
+		}
+	}
+
+	@autobind
+	private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) {
+		this.sendMessageToWs(data.type, data.body);
+	}
+
+	@autobind
+	public cacheNote(note: Packed<'Note'>) {
+		const add = (note: Packed<'Note'>) => {
+			const existIndex = this.cachedNotes.findIndex(n => n.id === note.id);
+			if (existIndex > -1) {
+				this.cachedNotes[existIndex] = note;
+				return;
+			}
+
+			this.cachedNotes.unshift(note);
+			if (this.cachedNotes.length > 32) {
+				this.cachedNotes.splice(32);
+			}
+		};
+
+		add(note);
+		if (note.reply) add(note.reply);
+		if (note.renote) add(note.renote);
+	}
+
+	@autobind
+	private readNote(body: any) {
+		const id = body.id;
+
+		const note = this.cachedNotes.find(n => n.id === id);
+		if (note == null) return;
+
+		if (this.user && (note.userId !== this.user.id)) {
+			readNote(this.user.id, [note], {
+				following: this.following,
+				followingChannels: this.followingChannels,
+			});
+		}
+	}
+
+	/**
+	 * APIリクエスト要求時
+	 */
+	@autobind
+	private async onApiRequest(payload: any) {
+		// 新鮮なデータを利用するためにユーザーをフェッチ
+		const user = this.user ? await Users.findOne(this.user.id) : null;
+
+		const endpoint = payload.endpoint || payload.ep; // alias
+
+		// 呼び出し
+		call(endpoint, user, this.token, payload.data).then(res => {
+			this.sendMessageToWs(`api:${payload.id}`, { res });
+		}).catch((e: ApiError) => {
+			this.sendMessageToWs(`api:${payload.id}`, {
+				error: {
+					message: e.message,
+					code: e.code,
+					id: e.id,
+					kind: e.kind,
+					...(e.info ? { info: e.info } : {})
+				}
+			});
+		});
+	}
+
+	@autobind
+	private onReadNotification(payload: any) {
+		if (!payload.id) return;
+		readNotification(this.user!.id, [payload.id]);
+	}
+
+	/**
+	 * 投稿購読要求時
+	 */
+	@autobind
+	private onSubscribeNote(payload: any) {
+		if (!payload.id) return;
+
+		if (this.subscribingNotes[payload.id] == null) {
+			this.subscribingNotes[payload.id] = 0;
+		}
+
+		this.subscribingNotes[payload.id]++;
+
+		if (this.subscribingNotes[payload.id] === 1) {
+			this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
+		}
+	}
+
+	/**
+	 * 投稿購読解除要求時
+	 */
+	@autobind
+	private onUnsubscribeNote(payload: any) {
+		if (!payload.id) return;
+
+		this.subscribingNotes[payload.id]--;
+		if (this.subscribingNotes[payload.id] <= 0) {
+			delete this.subscribingNotes[payload.id];
+			this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
+		}
+	}
+
+	@autobind
+	private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
+		this.sendMessageToWs('noteUpdated', {
+			id: data.body.id,
+			type: data.type,
+			body: data.body.body,
+		});
+	}
+
+	/**
+	 * チャンネル接続要求時
+	 */
+	@autobind
+	private onChannelConnectRequested(payload: any) {
+		const { channel, id, params, pong } = payload;
+		this.connectChannel(id, params, channel, pong);
+	}
+
+	/**
+	 * チャンネル切断要求時
+	 */
+	@autobind
+	private onChannelDisconnectRequested(payload: any) {
+		const { id } = payload;
+		this.disconnectChannel(id);
+	}
+
+	/**
+	 * クライアントにメッセージ送信
+	 */
+	@autobind
+	public sendMessageToWs(type: string, payload: any) {
+		this.wsConnection.send(JSON.stringify({
+			type: type,
+			body: payload
+		}));
+	}
+
+	/**
+	 * チャンネルに接続
+	 */
+	@autobind
+	public connectChannel(id: string, params: any, channel: string, pong = false) {
+		if ((channels as any)[channel].requireCredential && this.user == null) {
+			return;
+		}
+
+		// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
+		if ((channels as any)[channel].shouldShare && this.channels.some(c => c.chName === channel)) {
+			return;
+		}
+
+		const ch: Channel = new (channels as any)[channel](id, this);
+		this.channels.push(ch);
+		ch.init(params);
+
+		if (pong) {
+			this.sendMessageToWs('connected', {
+				id: id
+			});
+		}
+	}
+
+	/**
+	 * チャンネルから切断
+	 * @param id チャンネルコネクションID
+	 */
+	@autobind
+	public disconnectChannel(id: string) {
+		const channel = this.channels.find(c => c.id === id);
+
+		if (channel) {
+			if (channel.dispose) channel.dispose();
+			this.channels = this.channels.filter(c => c.id !== id);
+		}
+	}
+
+	/**
+	 * チャンネルへメッセージ送信要求時
+	 * @param data メッセージ
+	 */
+	@autobind
+	private onChannelMessageRequested(data: any) {
+		const channel = this.channels.find(c => c.id === data.id);
+		if (channel != null && channel.onMessage != null) {
+			channel.onMessage(data.type, data.body);
+		}
+	}
+
+	@autobind
+	private typingOnChannel(channel: ChannelModel['id']) {
+		if (this.user) {
+			publishChannelStream(channel, 'typing', this.user.id);
+		}
+	}
+
+	@autobind
+	private typingOnMessaging(param: { partner?: User['id']; group?: UserGroup['id']; }) {
+		if (this.user) {
+			if (param.partner) {
+				publishMessagingStream(param.partner, this.user.id, 'typing', this.user.id);
+			} else if (param.group) {
+				publishGroupMessagingStream(param.group, 'typing', this.user.id);
+			}
+		}
+	}
+
+	@autobind
+	private async updateFollowing() {
+		const followings = await Followings.find({
+			where: {
+				followerId: this.user!.id
+			},
+			select: ['followeeId']
+		});
+
+		this.following = new Set<string>(followings.map(x => x.followeeId));
+	}
+
+	@autobind
+	private async updateMuting() {
+		const mutings = await Mutings.find({
+			where: {
+				muterId: this.user!.id
+			},
+			select: ['muteeId']
+		});
+
+		this.muting = new Set<string>(mutings.map(x => x.muteeId));
+	}
+
+	@autobind
+	private async updateBlocking() { // ここでいうBlockingは被Blockingの意
+		const blockings = await Blockings.find({
+			where: {
+				blockeeId: this.user!.id
+			},
+			select: ['blockerId']
+		});
+
+		this.blocking = new Set<string>(blockings.map(x => x.blockerId));
+	}
+
+	@autobind
+	private async updateFollowingChannels() {
+		const followings = await ChannelFollowings.find({
+			where: {
+				followerId: this.user!.id
+			},
+			select: ['followeeId']
+		});
+
+		this.followingChannels = new Set<string>(followings.map(x => x.followeeId));
+	}
+
+	@autobind
+	private async updateUserProfile() {
+		this.userProfile = await UserProfiles.findOne({
+			userId: this.user!.id
+		});
+	}
+
+	/**
+	 * ストリームが切れたとき
+	 */
+	@autobind
+	public dispose() {
+		for (const c of this.channels.filter(c => c.dispose)) {
+			if (c.dispose) c.dispose();
+		}
+	}
+}
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
new file mode 100644
index 0000000000..70eb5c5ce5
--- /dev/null
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -0,0 +1,299 @@
+import { EventEmitter } from 'events';
+import Emitter from 'strict-event-emitter-types';
+import { Channel } from '@/models/entities/channel';
+import { User } from '@/models/entities/user';
+import { UserProfile } from '@/models/entities/user-profile';
+import { Note } from '@/models/entities/note';
+import { Antenna } from '@/models/entities/antenna';
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFolder } from '@/models/entities/drive-folder';
+import { Emoji } from '@/models/entities/emoji';
+import { UserList } from '@/models/entities/user-list';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { UserGroup } from '@/models/entities/user-group';
+import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { AbuseUserReport } from '@/models/entities/abuse-user-report';
+import { Signin } from '@/models/entities/signin';
+import { Page } from '@/models/entities/page';
+import { Packed } from '@/misc/schema';
+
+//#region Stream type-body definitions
+export interface InternalStreamTypes {
+	antennaCreated: Antenna;
+	antennaDeleted: Antenna;
+	antennaUpdated: Antenna;
+}
+
+export interface BroadcastTypes {
+	emojiAdded: {
+		emoji: Packed<'Emoji'>;
+	};
+}
+
+export interface UserStreamTypes {
+	terminate: {};
+	followChannel: Channel;
+	unfollowChannel: Channel;
+	updateUserProfile: UserProfile;
+	mute: User;
+	unmute: User;
+	follow: Packed<'User'>;
+	unfollow: Packed<'User'>;
+	userAdded: Packed<'User'>;
+}
+
+export interface MainStreamTypes {
+	notification: Packed<'Notification'>;
+	mention: Packed<'Note'>;
+	reply: Packed<'Note'>;
+	renote: Packed<'Note'>;
+	follow: Packed<'User'>;
+	followed: Packed<'User'>;
+	unfollow: Packed<'User'>;
+	meUpdated: Packed<'User'>;
+	pageEvent: {
+		pageId: Page['id'];
+		event: string;
+		var: any;
+		userId: User['id'];
+		user: Packed<'User'>;
+	};
+	urlUploadFinished: {
+		marker?: string | null;
+		file: Packed<'DriveFile'>;
+	};
+	readAllNotifications: undefined;
+	unreadNotification: Packed<'Notification'>;
+	unreadMention: Note['id'];
+	readAllUnreadMentions: undefined;
+	unreadSpecifiedNote: Note['id'];
+	readAllUnreadSpecifiedNotes: undefined;
+	readAllMessagingMessages: undefined;
+	messagingMessage: Packed<'MessagingMessage'>;
+	unreadMessagingMessage: Packed<'MessagingMessage'>;
+	readAllAntennas: undefined;
+	unreadAntenna: Antenna;
+	readAllAnnouncements: undefined;
+	readAllChannels: undefined;
+	unreadChannel: Note['id'];
+	myTokenRegenerated: undefined;
+	reversiNoInvites: undefined;
+	reversiInvited: Packed<'ReversiMatching'>;
+	signin: Signin;
+	registryUpdated: {
+		scope?: string[];
+		key: string;
+		value: any | null;
+	};
+	driveFileCreated: Packed<'DriveFile'>;
+	readAntenna: Antenna;
+}
+
+export interface DriveStreamTypes {
+	fileCreated: Packed<'DriveFile'>;
+	fileDeleted: DriveFile['id'];
+	fileUpdated: Packed<'DriveFile'>;
+	folderCreated: Packed<'DriveFolder'>;
+	folderDeleted: DriveFolder['id'];
+	folderUpdated: Packed<'DriveFolder'>;
+}
+
+export interface NoteStreamTypes {
+	pollVoted: {
+		choice: number;
+		userId: User['id'];
+	};
+	deleted: {
+		deletedAt: Date;
+	};
+	reacted: {
+		reaction: string;
+		emoji?: Emoji;
+		userId: User['id'];
+	};
+	unreacted: {
+		reaction: string;
+		userId: User['id'];
+	};
+}
+type NoteStreamEventTypes = {
+	[key in keyof NoteStreamTypes]: {
+		id: Note['id'];
+		body: NoteStreamTypes[key];
+	};
+};
+
+export interface ChannelStreamTypes {
+	typing: User['id'];
+}
+
+export interface UserListStreamTypes {
+	userAdded: Packed<'User'>;
+	userRemoved: Packed<'User'>;
+}
+
+export interface AntennaStreamTypes {
+	note: Note;
+}
+
+export interface MessagingStreamTypes {
+	read: MessagingMessage['id'][];
+	typing: User['id'];
+	message: Packed<'MessagingMessage'>;
+	deleted: MessagingMessage['id'];
+}
+
+export interface GroupMessagingStreamTypes {
+	read: {
+		ids: MessagingMessage['id'][];
+		userId: User['id'];
+	};
+	typing: User['id'];
+	message: Packed<'MessagingMessage'>;
+	deleted: MessagingMessage['id'];
+}
+
+export interface MessagingIndexStreamTypes {
+	read: MessagingMessage['id'][];
+	message: Packed<'MessagingMessage'>;
+}
+
+export interface ReversiStreamTypes {
+	matched: Packed<'ReversiGame'>;
+	invited: Packed<'ReversiMatching'>;
+}
+
+export interface ReversiGameStreamTypes {
+	started: Packed<'ReversiGame'>;
+	ended: {
+		winnerId?: User['id'] | null,
+		game: Packed<'ReversiGame'>;
+	};
+	updateSettings: {
+		key: string;
+		value: FIXME;
+	};
+	initForm: {
+		userId: User['id'];
+		form: FIXME;
+	};
+	updateForm: {
+		userId: User['id'];
+		id: string;
+		value: FIXME;
+	};
+	message: {
+		userId: User['id'];
+		message: FIXME;
+	};
+	changeAccepts: {
+		user1: boolean;
+		user2: boolean;
+	};
+	set: {
+		at: Date;
+		color: boolean;
+		pos: number;
+		next: boolean;
+	};
+	watching: User['id'];
+}
+
+export interface AdminStreamTypes {
+	newAbuseUserReport: {
+		id: AbuseUserReport['id'];
+		targetUserId: User['id'],
+		reporterId: User['id'],
+		comment: string;
+	};
+}
+//#endregion
+
+// 辞書(interface or type)から{ type, body }ユニオンを定義
+// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
+// VS Codeの展開を防止するためにEvents型を定義
+type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
+type EventUnionFromDictionary<
+	T extends object,
+	U = Events<T>
+> = U[keyof U];
+
+// name/messages(spec) pairs dictionary
+export type StreamMessages = {
+	internal: {
+		name: 'internal';
+		payload: EventUnionFromDictionary<InternalStreamTypes>;
+	};
+	broadcast: {
+		name: 'broadcast';
+		payload: EventUnionFromDictionary<BroadcastTypes>;
+	};
+	user: {
+		name: `user:${User['id']}`;
+		payload: EventUnionFromDictionary<UserStreamTypes>;
+	};
+	main: {
+		name: `mainStream:${User['id']}`;
+		payload: EventUnionFromDictionary<MainStreamTypes>;
+	};
+	drive: {
+		name: `driveStream:${User['id']}`;
+		payload: EventUnionFromDictionary<DriveStreamTypes>;
+	};
+	note: {
+		name: `noteStream:${Note['id']}`;
+		payload: EventUnionFromDictionary<NoteStreamEventTypes>;
+	};
+	channel: {
+		name: `channelStream:${Channel['id']}`;
+		payload: EventUnionFromDictionary<ChannelStreamTypes>;
+	};
+	userList: {
+		name: `userListStream:${UserList['id']}`;
+		payload: EventUnionFromDictionary<UserListStreamTypes>;
+	};
+	antenna: {
+		name: `antennaStream:${Antenna['id']}`;
+		payload: EventUnionFromDictionary<AntennaStreamTypes>;
+	};
+	messaging: {
+		name: `messagingStream:${User['id']}-${User['id']}`;
+		payload: EventUnionFromDictionary<MessagingStreamTypes>;
+	};
+	groupMessaging: {
+		name: `messagingStream:${UserGroup['id']}`;
+		payload: EventUnionFromDictionary<GroupMessagingStreamTypes>;
+	};
+	messagingIndex: {
+		name: `messagingIndexStream:${User['id']}`;
+		payload: EventUnionFromDictionary<MessagingIndexStreamTypes>;
+	};
+	reversi: {
+		name: `reversiStream:${User['id']}`;
+		payload: EventUnionFromDictionary<ReversiStreamTypes>;
+	};
+	reversiGame: {
+		name: `reversiGameStream:${ReversiGame['id']}`;
+		payload: EventUnionFromDictionary<ReversiGameStreamTypes>;
+	};
+	admin: {
+		name: `adminStream:${User['id']}`;
+		payload: EventUnionFromDictionary<AdminStreamTypes>;
+	};
+	notes: {
+		name: 'notesStream';
+		payload: Packed<'Note'>;
+	};
+};
+
+// API event definitions
+// ストリームごとのEmitterの辞書を用意
+type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> };
+// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
+// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
+export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
+// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
+
+// provide stream channels union
+export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];
diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts
new file mode 100644
index 0000000000..8808bc9860
--- /dev/null
+++ b/packages/backend/src/server/api/streaming.ts
@@ -0,0 +1,67 @@
+import * as http from 'http';
+import * as websocket from 'websocket';
+
+import MainStreamConnection from './stream/index';
+import { ParsedUrlQuery } from 'querystring';
+import authenticate from './authenticate';
+import { EventEmitter } from 'events';
+import { subsdcriber as redisClient } from '../../db/redis';
+import { Users } from '@/models/index';
+
+module.exports = (server: http.Server) => {
+	// Init websocket server
+	const ws = new websocket.server({
+		httpServer: server
+	});
+
+	ws.on('request', async (request) => {
+		const q = request.resourceURL.query as ParsedUrlQuery;
+
+		// TODO: トークンが間違ってるなどしてauthenticateに失敗したら
+		// コネクション切断するなりエラーメッセージ返すなりする
+		// (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので)
+		const [user, app] = await authenticate(q.i as string);
+
+		if (user?.isSuspended) {
+			request.reject(400);
+			return;
+		}
+
+		const connection = request.accept();
+
+		const ev = new EventEmitter();
+
+		async function onRedisMessage(_: string, data: string) {
+			const parsed = JSON.parse(data);
+			ev.emit(parsed.channel, parsed.message);
+		}
+
+		redisClient.on('message', onRedisMessage);
+
+		const main = new MainStreamConnection(connection, ev, user, app);
+
+		const intervalId = user ? setInterval(() => {
+			Users.update(user.id, {
+				lastActiveDate: new Date(),
+			});
+		}, 1000 * 60 * 5) : null;
+		if (user) {
+			Users.update(user.id, {
+				lastActiveDate: new Date(),
+			});
+		}
+
+		connection.once('close', () => {
+			ev.removeAllListeners();
+			main.dispose();
+			redisClient.off('message', onRedisMessage);
+			if (intervalId) clearInterval(intervalId);
+		});
+
+		connection.on('message', async (data) => {
+			if (data.utf8Data === 'ping') {
+				connection.send('pong');
+			}
+		});
+	});
+};
diff --git a/packages/backend/src/server/file/assets/bad-egg.png b/packages/backend/src/server/file/assets/bad-egg.png
new file mode 100644
index 0000000000..e96ba0dcc1
Binary files /dev/null and b/packages/backend/src/server/file/assets/bad-egg.png differ
diff --git a/packages/backend/src/server/file/assets/cache-expired.png b/packages/backend/src/server/file/assets/cache-expired.png
new file mode 100644
index 0000000000..5d988c502b
Binary files /dev/null and b/packages/backend/src/server/file/assets/cache-expired.png differ
diff --git a/packages/backend/src/server/file/assets/dummy.png b/packages/backend/src/server/file/assets/dummy.png
new file mode 100644
index 0000000000..39332b0c1b
Binary files /dev/null and b/packages/backend/src/server/file/assets/dummy.png differ
diff --git a/packages/backend/src/server/file/assets/not-an-image.png b/packages/backend/src/server/file/assets/not-an-image.png
new file mode 100644
index 0000000000..39e4aa0892
Binary files /dev/null and b/packages/backend/src/server/file/assets/not-an-image.png differ
diff --git a/packages/backend/src/server/file/assets/thumbnail-not-available.png b/packages/backend/src/server/file/assets/thumbnail-not-available.png
new file mode 100644
index 0000000000..07cad9919c
Binary files /dev/null and b/packages/backend/src/server/file/assets/thumbnail-not-available.png differ
diff --git a/packages/backend/src/server/file/assets/tombstone.png b/packages/backend/src/server/file/assets/tombstone.png
new file mode 100644
index 0000000000..83159d6b3c
Binary files /dev/null and b/packages/backend/src/server/file/assets/tombstone.png differ
diff --git a/packages/backend/src/server/file/index.ts b/packages/backend/src/server/file/index.ts
new file mode 100644
index 0000000000..a455acd1cf
--- /dev/null
+++ b/packages/backend/src/server/file/index.ts
@@ -0,0 +1,41 @@
+/**
+ * File Server
+ */
+
+import * as fs from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import * as Koa from 'koa';
+import * as cors from '@koa/cors';
+import * as Router from '@koa/router';
+import sendDriveFile from './send-drive-file';
+
+//const _filename = fileURLToPath(import.meta.url);
+const _filename = __filename;
+const _dirname = dirname(_filename);
+
+// Init app
+const app = new Koa();
+app.use(cors());
+app.use(async (ctx, next) => {
+	ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`);
+	await next();
+});
+
+// Init router
+const router = new Router();
+
+router.get('/app-default.jpg', ctx => {
+	const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
+	ctx.body = file;
+	ctx.set('Content-Type', 'image/jpeg');
+	ctx.set('Cache-Control', 'max-age=31536000, immutable');
+});
+
+router.get('/:key', sendDriveFile);
+router.get('/:key/(.*)', sendDriveFile);
+
+// Register router
+app.use(router.routes());
+
+module.exports = app;
diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts
new file mode 100644
index 0000000000..1908c969a5
--- /dev/null
+++ b/packages/backend/src/server/file/send-drive-file.ts
@@ -0,0 +1,126 @@
+import * as fs from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import * as Koa from 'koa';
+import * as send from 'koa-send';
+import * as rename from 'rename';
+import * as tmp from 'tmp';
+import { serverLogger } from '../index';
+import { contentDisposition } from '@/misc/content-disposition';
+import { DriveFiles } from '@/models/index';
+import { InternalStorage } from '@/services/drive/internal-storage';
+import { downloadUrl } from '@/misc/download-url';
+import { detectType } from '@/misc/get-file-info';
+import { convertToJpeg, convertToPngOrJpeg } from '@/services/drive/image-processor';
+import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail';
+import { StatusError } from '@/misc/fetch';
+
+//const _filename = fileURLToPath(import.meta.url);
+const _filename = __filename;
+const _dirname = dirname(_filename);
+
+const assets = `${_dirname}/../../server/file/assets/`;
+
+const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => {
+	serverLogger.error(e);
+	ctx.status = 500;
+	ctx.set('Cache-Control', 'max-age=300');
+};
+
+export default async function(ctx: Koa.Context) {
+	const key = ctx.params.key;
+
+	// Fetch drive file
+	const file = await DriveFiles.createQueryBuilder('file')
+		.where('file.accessKey = :accessKey', { accessKey: key })
+		.orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
+		.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
+		.getOne();
+
+	if (file == null) {
+		ctx.status = 404;
+		ctx.set('Cache-Control', 'max-age=86400');
+		await send(ctx as any, '/dummy.png', { root: assets });
+		return;
+	}
+
+	const isThumbnail = file.thumbnailAccessKey === key;
+	const isWebpublic = file.webpublicAccessKey === key;
+
+	if (!file.storedInternal) {
+		if (file.isLink && file.uri) {	// 期限切れリモートファイル
+			const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
+				tmp.file((e, path, fd, cleanup) => {
+					if (e) return rej(e);
+					res([path, cleanup]);
+				});
+			});
+
+			try {
+				await downloadUrl(file.uri, path);
+
+				const { mime, ext } = await detectType(path);
+
+				const convertFile = async () => {
+					if (isThumbnail) {
+						if (['image/jpeg', 'image/webp'].includes(mime)) {
+							return await convertToJpeg(path, 498, 280);
+						} else if (['image/png'].includes(mime)) {
+							return await convertToPngOrJpeg(path, 498, 280);
+						} else if (mime.startsWith('video/')) {
+							return await GenerateVideoThumbnail(path);
+						}
+					}
+
+					return {
+						data: fs.readFileSync(path),
+						ext,
+						type: mime,
+					};
+				};
+
+				const image = await convertFile();
+				ctx.body = image.data;
+				ctx.set('Content-Type', image.type);
+				ctx.set('Cache-Control', 'max-age=31536000, immutable');
+			} catch (e) {
+				serverLogger.error(`${e}`);
+
+				if (e instanceof StatusError && e.isClientError) {
+					ctx.status = e.statusCode;
+					ctx.set('Cache-Control', 'max-age=86400');
+				} else {
+					ctx.status = 500;
+					ctx.set('Cache-Control', 'max-age=300');
+				}
+			} finally {
+				cleanup();
+			}
+			return;
+		}
+
+		ctx.status = 204;
+		ctx.set('Cache-Control', 'max-age=86400');
+		return;
+	}
+
+	if (isThumbnail || isWebpublic) {
+		const { mime, ext } = await detectType(InternalStorage.resolvePath(key));
+		const filename = rename(file.name, {
+			suffix: isThumbnail ? '-thumb' : '-web',
+			extname: ext ? `.${ext}` : undefined
+		}).toString();
+
+		ctx.body = InternalStorage.read(key);
+		ctx.set('Content-Type', mime);
+		ctx.set('Cache-Control', 'max-age=31536000, immutable');
+		ctx.set('Content-Disposition', contentDisposition('inline', filename));
+	} else {
+		const readable = InternalStorage.read(file.accessKey!);
+		readable.on('error', commonReadableHandlerGenerator(ctx));
+		ctx.body = readable;
+		ctx.set('Content-Type', file.type);
+		ctx.set('Cache-Control', 'max-age=31536000, immutable');
+		ctx.set('Content-Disposition', contentDisposition('inline', file.name));
+	}
+}
diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts
new file mode 100644
index 0000000000..507178a46c
--- /dev/null
+++ b/packages/backend/src/server/index.ts
@@ -0,0 +1,179 @@
+/**
+ * Core Server
+ */
+
+import * as fs from 'fs';
+import * as http from 'http';
+import * as http2 from 'http2';
+import * as https from 'https';
+import * as Koa from 'koa';
+import * as Router from '@koa/router';
+import * as mount from 'koa-mount';
+import * as koaLogger from 'koa-logger';
+import * as requestStats from 'request-stats';
+import * as slow from 'koa-slow';
+
+import activityPub from './activitypub';
+import nodeinfo from './nodeinfo';
+import wellKnown from './well-known';
+import config from '@/config/index';
+import apiServer from './api/index';
+import { sum } from '@/prelude/array';
+import Logger from '@/services/logger';
+import { envOption } from '../env';
+import { UserProfiles, Users } from '@/models/index';
+import { networkChart } from '@/services/chart/index';
+import { genAvatar } from '@/misc/gen-avatar';
+import { createTemp } from '@/misc/create-temp';
+import { publishMainStream } from '@/services/stream';
+import * as Acct from 'misskey-js/built/acct';
+
+export const serverLogger = new Logger('server', 'gray', false);
+
+// Init app
+const app = new Koa();
+app.proxy = true;
+
+if (!['production', 'test'].includes(process.env.NODE_ENV || '')) {
+	// Logger
+	app.use(koaLogger(str => {
+		serverLogger.info(str);
+	}));
+
+	// Delay
+	if (envOption.slow) {
+		app.use(slow({
+			delay: 3000
+		}));
+	}
+}
+
+// HSTS
+// 6months (15552000sec)
+if (config.url.startsWith('https') && !config.disableHsts) {
+	app.use(async (ctx, next) => {
+		ctx.set('strict-transport-security', 'max-age=15552000; preload');
+		await next();
+	});
+}
+
+app.use(mount('/api', apiServer));
+app.use(mount('/files', require('./file')));
+app.use(mount('/proxy', require('./proxy')));
+
+// Init router
+const router = new Router();
+
+// Routing
+router.use(activityPub.routes());
+router.use(nodeinfo.routes());
+router.use(wellKnown.routes());
+
+router.get('/avatar/@:acct', async ctx => {
+	const { username, host } = Acct.parse(ctx.params.acct);
+	const user = await Users.findOne({
+		usernameLower: username.toLowerCase(),
+		host: host === config.host ? null : host,
+		isSuspended: false
+	});
+
+	if (user) {
+		ctx.redirect(Users.getAvatarUrl(user));
+	} else {
+		ctx.redirect('/static-assets/user-unknown.png');
+	}
+});
+
+router.get('/random-avatar/:x', async ctx => {
+	const [temp] = await createTemp();
+	await genAvatar(ctx.params.x, fs.createWriteStream(temp));
+	ctx.set('Content-Type', 'image/png');
+	ctx.body = fs.createReadStream(temp);
+});
+
+router.get('/verify-email/:code', async ctx => {
+	const profile = await UserProfiles.findOne({
+		emailVerifyCode: ctx.params.code
+	});
+
+	if (profile != null) {
+		ctx.body = 'Verify succeeded!';
+		ctx.status = 200;
+
+		await UserProfiles.update({ userId: profile.userId }, {
+			emailVerified: true,
+			emailVerifyCode: null
+		});
+
+		publishMainStream(profile.userId, 'meUpdated', await Users.pack(profile.userId, { id: profile.userId }, {
+			detail: true,
+			includeSecrets: true
+		}));
+	} else {
+		ctx.status = 404;
+	}
+});
+
+// Register router
+app.use(router.routes());
+
+app.use(mount(require('./web')));
+
+function createServer() {
+	if (config.https) {
+		const certs: any = {};
+		for (const k of Object.keys(config.https)) {
+			certs[k] = fs.readFileSync(config.https[k]);
+		}
+		certs['allowHTTP1'] = true;
+		return http2.createSecureServer(certs, app.callback()) as https.Server;
+	} else {
+		return http.createServer(app.callback());
+	}
+}
+
+// For testing
+export const startServer = () => {
+	const server = createServer();
+
+	// Init stream server
+	require('./api/streaming')(server);
+
+	// Listen
+	server.listen(config.port);
+
+	return server;
+};
+
+export default () => new Promise(resolve => {
+	const server = createServer();
+
+	// Init stream server
+	require('./api/streaming')(server);
+
+	// Listen
+	server.listen(config.port, resolve);
+
+	//#region Network stats
+	let queue: any[] = [];
+
+	requestStats(server, (stats: any) => {
+		if (stats.ok) {
+			queue.push(stats);
+		}
+	});
+
+	// Bulk write
+	setInterval(() => {
+		if (queue.length === 0) return;
+
+		const requests = queue.length;
+		const time = sum(queue.map(x => x.time));
+		const incomingBytes = sum(queue.map(x => x.req.byets));
+		const outgoingBytes = sum(queue.map(x => x.res.byets));
+		queue = [];
+
+		networkChart.update(requests, time, incomingBytes, outgoingBytes);
+	}, 5000);
+	//#endregion
+});
diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts
new file mode 100644
index 0000000000..6a864fcc52
--- /dev/null
+++ b/packages/backend/src/server/nodeinfo.ts
@@ -0,0 +1,101 @@
+import * as Router from '@koa/router';
+import config from '@/config/index';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Users } from '@/models/index';
+// import User from '../models/user';
+// import Note from '../models/note';
+
+const router = new Router();
+
+const nodeinfo2_1path = '/nodeinfo/2.1';
+const nodeinfo2_0path = '/nodeinfo/2.0';
+
+export const links = [/* (awaiting release) {
+	rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
+	href: config.url + nodeinfo2_1path
+}, */{
+	rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
+	href: config.url + nodeinfo2_0path
+}];
+
+const nodeinfo2 = async () => {
+	const [
+		meta,
+		// total,
+		// activeHalfyear,
+		// activeMonth,
+		// localPosts,
+		// localComments
+	] = await Promise.all([
+		fetchMeta(true),
+		// User.count({ host: null }),
+		// User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 15552000000) } }),
+		// User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 2592000000) } }),
+		// Note.count({ '_user.host': null, replyId: null }),
+		// Note.count({ '_user.host': null, replyId: { $ne: null } })
+	]);
+
+	const proxyAccount = meta.proxyAccountId ? await Users.pack(meta.proxyAccountId).catch(() => null) : null;
+
+	return {
+		software: {
+			name: 'misskey',
+			version: config.version,
+			repository: meta.repositoryUrl,
+		},
+		protocols: ['activitypub'],
+		services: {
+			inbound: [] as string[],
+			outbound: ['atom1.0', 'rss2.0']
+		},
+		openRegistrations: !meta.disableRegistration,
+		usage: {
+			users: {} // { total, activeHalfyear, activeMonth },
+			// localPosts,
+			// localComments
+		},
+		metadata: {
+			nodeName: meta.name,
+			nodeDescription: meta.description,
+			maintainer: {
+				name: meta.maintainerName,
+				email: meta.maintainerEmail
+			},
+			langs: meta.langs,
+			tosUrl: meta.ToSUrl,
+			repositoryUrl: meta.repositoryUrl,
+			feedbackUrl: meta.feedbackUrl,
+			disableRegistration: meta.disableRegistration,
+			disableLocalTimeline: meta.disableLocalTimeline,
+			disableGlobalTimeline: meta.disableGlobalTimeline,
+			emailRequiredForSignup: meta.emailRequiredForSignup,
+			enableHcaptcha: meta.enableHcaptcha,
+			enableRecaptcha: meta.enableRecaptcha,
+			maxNoteTextLength: meta.maxNoteTextLength,
+			enableTwitterIntegration: meta.enableTwitterIntegration,
+			enableGithubIntegration: meta.enableGithubIntegration,
+			enableDiscordIntegration: meta.enableDiscordIntegration,
+			enableEmail: meta.enableEmail,
+			enableServiceWorker: meta.enableServiceWorker,
+			proxyAccountName: proxyAccount ? proxyAccount.username : null,
+		}
+	};
+};
+
+router.get(nodeinfo2_1path, async ctx => {
+	const base = await nodeinfo2();
+
+	ctx.body = { version: '2.1', ...base };
+	ctx.set('Cache-Control', 'public, max-age=600');
+});
+
+router.get(nodeinfo2_0path, async ctx => {
+	const base = await nodeinfo2();
+
+	delete base.software.repository;
+
+	ctx.body = { version: '2.0', ...base };
+	ctx.set('Cache-Control', 'public, max-age=600');
+});
+
+export default router;
diff --git a/packages/backend/src/server/proxy/index.ts b/packages/backend/src/server/proxy/index.ts
new file mode 100644
index 0000000000..b8993f19f8
--- /dev/null
+++ b/packages/backend/src/server/proxy/index.ts
@@ -0,0 +1,26 @@
+/**
+ * Media Proxy
+ */
+
+import * as Koa from 'koa';
+import * as cors from '@koa/cors';
+import * as Router from '@koa/router';
+import { proxyMedia } from './proxy-media';
+
+// Init app
+const app = new Koa();
+app.use(cors());
+app.use(async (ctx, next) => {
+	ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`);
+	await next();
+});
+
+// Init router
+const router = new Router();
+
+router.get('/:url*', proxyMedia);
+
+// Register router
+app.use(router.routes());
+
+module.exports = app;
diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts
new file mode 100644
index 0000000000..9e13c0877f
--- /dev/null
+++ b/packages/backend/src/server/proxy/proxy-media.ts
@@ -0,0 +1,51 @@
+import * as fs from 'fs';
+import * as Koa from 'koa';
+import { serverLogger } from '../index';
+import { IImage, convertToPng, convertToJpeg } from '@/services/drive/image-processor';
+import { createTemp } from '@/misc/create-temp';
+import { downloadUrl } from '@/misc/download-url';
+import { detectType } from '@/misc/get-file-info';
+import { StatusError } from '@/misc/fetch';
+
+export async function proxyMedia(ctx: Koa.Context) {
+	const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
+
+	// Create temp file
+	const [path, cleanup] = await createTemp();
+
+	try {
+		await downloadUrl(url, path);
+
+		const { mime, ext } = await detectType(path);
+
+		if (!mime.startsWith('image/')) throw 403;
+
+		let image: IImage;
+
+		if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp'].includes(mime)) {
+			image = await convertToPng(path, 498, 280);
+		} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) {
+			image = await convertToJpeg(path, 200, 200);
+		} else {
+			image = {
+				data: fs.readFileSync(path),
+				ext,
+				type: mime,
+			};
+		}
+
+		ctx.set('Content-Type', image.type);
+		ctx.set('Cache-Control', 'max-age=31536000, immutable');
+		ctx.body = image.data;
+	} catch (e) {
+		serverLogger.error(`${e}`);
+
+		if (e instanceof StatusError && e.isClientError) {
+			ctx.status = e.statusCode;
+		} else {
+			ctx.status = 500;
+		}
+	} finally {
+		cleanup();
+	}
+}
diff --git a/packages/backend/src/server/web/bios.css b/packages/backend/src/server/web/bios.css
new file mode 100644
index 0000000000..b0da3ee39b
--- /dev/null
+++ b/packages/backend/src/server/web/bios.css
@@ -0,0 +1,40 @@
+* {
+	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
+}
+
+html {
+	background: #ffb4e1;
+}
+
+main {
+	background: #dedede;
+}
+main > .tabs {
+	padding: 16px;
+	border-bottom: solid 4px #c3c3c3;
+}
+
+#lsEditor > .adder {
+	margin: 16px;
+	padding: 16px;
+	border: solid 2px #c3c3c3;
+}
+#lsEditor > .adder > textarea {
+	display: block;
+	width: 100%;
+	min-height: 5em;
+	box-sizing: border-box;
+}
+#lsEditor > .record {
+	padding: 16px;
+	border-bottom: solid 1px #c3c3c3;
+}
+#lsEditor > .record > header {
+	font-weight: bold;
+}
+#lsEditor > .record > textarea {
+	display: block;
+	width: 100%;
+	min-height: 5em;
+	box-sizing: border-box;
+}
diff --git a/packages/backend/src/server/web/bios.js b/packages/backend/src/server/web/bios.js
new file mode 100644
index 0000000000..d06dee801a
--- /dev/null
+++ b/packages/backend/src/server/web/bios.js
@@ -0,0 +1,87 @@
+'use strict';
+
+window.onload = async () => {
+	const account = JSON.parse(localStorage.getItem('account'));
+	const i = account.token;
+
+	const api = (endpoint, data = {}) => {
+		const promise = new Promise((resolve, reject) => {
+			// Append a credential
+			if (i) data.i = i;
+	
+			// Send request
+			fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
+				method: 'POST',
+				body: JSON.stringify(data),
+				credentials: 'omit',
+				cache: 'no-cache'
+			}).then(async (res) => {
+				const body = res.status === 204 ? null : await res.json();
+	
+				if (res.status === 200) {
+					resolve(body);
+				} else if (res.status === 204) {
+					resolve();
+				} else {
+					reject(body.error);
+				}
+			}).catch(reject);
+		});
+		
+		return promise;
+	};
+
+	const content = document.getElementById('content');
+
+	document.getElementById('ls').addEventListener('click', () => {
+		content.innerHTML = '';
+
+		const lsEditor = document.createElement('div');
+		lsEditor.id = 'lsEditor';
+
+		const adder = document.createElement('div');
+		adder.classList.add('adder');
+		const addKeyInput = document.createElement('input');
+		const addValueTextarea = document.createElement('textarea');
+		const addButton = document.createElement('button');
+		addButton.textContent = 'add';
+		addButton.addEventListener('click', () => {
+			localStorage.setItem(addKeyInput.value, addValueTextarea.value);
+			location.reload();
+		});
+
+		adder.appendChild(addKeyInput);
+		adder.appendChild(addValueTextarea);
+		adder.appendChild(addButton);
+		lsEditor.appendChild(adder);
+
+		for (let i = 0; i < localStorage.length; i++) {
+			const k = localStorage.key(i);
+			const record = document.createElement('div');
+			record.classList.add('record');
+			const header = document.createElement('header');
+			header.textContent = k;
+			const textarea = document.createElement('textarea');
+			textarea.textContent = localStorage.getItem(k);
+			const saveButton = document.createElement('button');
+			saveButton.textContent = 'save';
+			saveButton.addEventListener('click', () => {
+				localStorage.setItem(k, textarea.value);
+				location.reload();
+			});
+			const removeButton = document.createElement('button');
+			removeButton.textContent = 'remove';
+			removeButton.addEventListener('click', () => {
+				localStorage.removeItem(k);
+				location.reload();
+			});
+			record.appendChild(header);
+			record.appendChild(textarea);
+			record.appendChild(saveButton);
+			record.appendChild(removeButton);
+			lsEditor.appendChild(record);
+		}
+
+		content.appendChild(lsEditor);
+	});
+};
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
new file mode 100644
index 0000000000..d4a2529e63
--- /dev/null
+++ b/packages/backend/src/server/web/boot.js
@@ -0,0 +1,166 @@
+/**
+ * BOOT LOADER
+ * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
+ * - 翻訳ファイルをフェッチする。
+ * - バージョンに基づいて適切なメインスクリプトを読み込む。
+ * - キャッシュされたコンパイル済みテーマを適用する。
+ * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。
+ * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
+ * 注: webpackは介さないため、このファイルではrequireやimportは使えません。
+ */
+
+'use strict';
+
+// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
+(async () => {
+	window.onerror = (e) => {
+		renderError('SOMETHING_HAPPENED', e.toString());
+	};
+	window.onunhandledrejection = (e) => {
+		renderError('SOMETHING_HAPPENED_IN_PROMISE', e.toString());
+	};
+
+	const v = localStorage.getItem('v') || VERSION;
+
+	//#region Detect language & fetch translations
+	const localeVersion = localStorage.getItem('localeVersion');
+	const localeOutdated = (localeVersion == null || localeVersion !== v);
+
+	if (!localStorage.hasOwnProperty('locale') || localeOutdated) {
+		const supportedLangs = LANGS;
+		let lang = localStorage.getItem('lang');
+		if (lang == null || !supportedLangs.includes(lang)) {
+			if (supportedLangs.includes(navigator.language)) {
+				lang = navigator.language;
+			} else {
+				lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+
+				// Fallback
+				if (lang == null) lang = 'en-US';
+			}
+		}
+
+		const res = await fetch(`/assets/locales/${lang}.${v}.json`);
+		if (res.status === 200) {
+			localStorage.setItem('lang', lang);
+			localStorage.setItem('locale', await res.text());
+			localStorage.setItem('localeVersion', v);
+		} else if (localeOutdated) {
+			// nop
+		} else {
+			await checkUpdate();
+			renderError('LOCALE_FETCH_FAILED');
+			return;
+		}
+	}
+	//#endregion
+
+	//#region Script
+	const salt = localStorage.getItem('salt')
+		? `?salt=${localStorage.getItem('salt')}`
+		: '';
+
+	const script = document.createElement('script');
+	script.setAttribute('src', `/assets/app.${v}.js${salt}`);
+	script.setAttribute('async', 'true');
+	script.setAttribute('defer', 'true');
+	script.addEventListener('error', async () => {
+		await checkUpdate();
+		renderError('APP_FETCH_FAILED');
+	});
+	document.head.appendChild(script);
+	//#endregion
+
+	//#region Theme
+	const theme = localStorage.getItem('theme');
+	if (theme) {
+		for (const [k, v] of Object.entries(JSON.parse(theme))) {
+			document.documentElement.style.setProperty(`--${k}`, v.toString());
+
+			// HTMLの theme-color 適用
+			if (k === 'htmlThemeColor') {
+				for (const tag of document.head.children) {
+					if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+						tag.setAttribute('content', v);
+						break;
+					}
+				}
+			}
+		}
+	}
+	//#endregion
+
+	const fontSize = localStorage.getItem('fontSize');
+	if (fontSize) {
+		document.documentElement.classList.add('f-' + fontSize);
+	}
+
+	const useSystemFont = localStorage.getItem('useSystemFont');
+	if (useSystemFont) {
+		document.documentElement.classList.add('useSystemFont');
+	}
+
+	const wallpaper = localStorage.getItem('wallpaper');
+	if (wallpaper) {
+		document.documentElement.style.backgroundImage = `url(${wallpaper})`;
+	}
+
+	const customCss = localStorage.getItem('customCss');
+	if (customCss && customCss.length > 0) {
+		const style = document.createElement('style');
+		style.innerHTML = customCss;
+		document.head.appendChild(style);
+	}
+
+	// eslint-disable-next-line no-inner-declarations
+	function renderError(code, details) {
+		document.documentElement.innerHTML = `
+			<h1>⚠エラーが発生しました</h1>
+			<p>問題が解決しない場合は管理者までお問い合わせください。以下のオプションを試すこともできます:</p>
+			<ul>
+				<li><a href="/cli">簡易クライアント</a>を起動</li>
+				<li><a href="/bios">BIOS</a>で修復を試みる</li>
+				<li><a href="/flush">キャッシュをクリア</a>する</li>
+			</ul>
+			<hr>
+			<code>ERROR CODE: ${code}</code>
+			<details>
+				${details}
+			</details>
+		`;
+	}
+
+	// eslint-disable-next-line no-inner-declarations
+	async function checkUpdate() {
+		// TODO: サーバーが落ちている場合などのエラーハンドリング
+		const res = await fetch('/api/meta', {
+			method: 'POST',
+			cache: 'no-cache'
+		});
+
+		const meta = await res.json();
+
+		if (meta.version != v) {
+			localStorage.setItem('v', meta.version);
+			refresh();
+		}
+	}
+
+	// eslint-disable-next-line no-inner-declarations
+	function refresh() {
+		// Random
+		localStorage.setItem('salt', Math.random().toString().substr(2, 8));
+
+		// Clear cache (service worker)
+		try {
+			navigator.serviceWorker.controller.postMessage('clear');
+			navigator.serviceWorker.getRegistrations().then(registrations => {
+				registrations.forEach(registration => registration.unregister());
+			});
+		} catch (e) {
+			console.error(e);
+		}
+
+		location.reload();
+	}
+})();
diff --git a/packages/backend/src/server/web/cli.css b/packages/backend/src/server/web/cli.css
new file mode 100644
index 0000000000..07cd27830b
--- /dev/null
+++ b/packages/backend/src/server/web/cli.css
@@ -0,0 +1,19 @@
+* {
+	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
+}
+
+html {
+	background: #ffb4e1;
+}
+
+main {
+	background: #dedede;
+}
+
+#tl > div {
+	padding: 16px;
+	border-bottom: solid 1px #c3c3c3;
+}
+#tl > div > header {
+	font-weight: bold;
+}
diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js
new file mode 100644
index 0000000000..3dff1d4860
--- /dev/null
+++ b/packages/backend/src/server/web/cli.js
@@ -0,0 +1,55 @@
+'use strict';
+
+window.onload = async () => {
+	const account = JSON.parse(localStorage.getItem('account'));
+	const i = account.token;
+
+	const api = (endpoint, data = {}) => {
+		const promise = new Promise((resolve, reject) => {
+			// Append a credential
+			if (i) data.i = i;
+	
+			// Send request
+			fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
+				method: 'POST',
+				body: JSON.stringify(data),
+				credentials: 'omit',
+				cache: 'no-cache'
+			}).then(async (res) => {
+				const body = res.status === 204 ? null : await res.json();
+	
+				if (res.status === 200) {
+					resolve(body);
+				} else if (res.status === 204) {
+					resolve();
+				} else {
+					reject(body.error);
+				}
+			}).catch(reject);
+		});
+		
+		return promise;
+	};
+
+	document.getElementById('submit').addEventListener('click', () => {
+		api('notes/create', {
+			text: document.getElementById('text').value
+		}).then(() => {
+			location.reload();
+		});
+	});
+
+	api('notes/timeline').then(notes => {
+		const tl = document.getElementById('tl');
+		for (const note of notes) {
+			const el = document.createElement('div');
+			const name = document.createElement('header');
+			name.textContent = `${note.user.name} @${note.user.username}`;
+			const text = document.createElement('div');
+			text.textContent = `${note.text}`;
+			el.appendChild(name);
+			el.appendChild(text);
+			tl.appendChild(el);
+		}
+	});
+};
diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts
new file mode 100644
index 0000000000..1d4c47dafb
--- /dev/null
+++ b/packages/backend/src/server/web/feed.ts
@@ -0,0 +1,58 @@
+import { Feed } from 'feed';
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+import { Notes, DriveFiles, UserProfiles } from '@/models/index';
+import { In } from 'typeorm';
+
+export default async function(user: User) {
+	const author = {
+		link: `${config.url}/@${user.username}`,
+		name: user.name || user.username
+	};
+
+	const profile = await UserProfiles.findOneOrFail(user.id);
+
+	const notes = await Notes.find({
+		where: {
+			userId: user.id,
+			renoteId: null,
+			visibility: In(['public', 'home'])
+		},
+		order: { createdAt: -1 },
+		take: 20
+	});
+
+	const feed = new Feed({
+		id: author.link,
+		title: `${author.name} (@${user.username}@${config.host})`,
+		updated: notes[0].createdAt,
+		generator: 'Misskey',
+		description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
+		link: author.link,
+		image: user.avatarUrl ? user.avatarUrl : undefined,
+		feedLinks: {
+			json: `${author.link}.json`,
+			atom: `${author.link}.atom`,
+		},
+		author,
+		copyright: user.name || user.username
+	});
+
+	for (const note of notes) {
+		const files = note.fileIds.length > 0 ? await DriveFiles.find({
+			id: In(note.fileIds)
+		}) : [];
+		const file = files.find(file => file.type.startsWith('image/'));
+
+		feed.addItem({
+			title: `New note by ${author.name}`,
+			link: `${config.url}/notes/${note.id}`,
+			date: note.createdAt,
+			description: note.cw || undefined,
+			content: note.text || undefined,
+			image: file ? DriveFiles.getPublicUrl(file) || undefined : undefined
+		});
+	}
+
+	return feed;
+}
diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
new file mode 100644
index 0000000000..969b155d4d
--- /dev/null
+++ b/packages/backend/src/server/web/index.ts
@@ -0,0 +1,414 @@
+/**
+ * Web Client Server
+ */
+
+import { dirname } from 'path';
+import * as ms from 'ms';
+import * as Koa from 'koa';
+import * as Router from '@koa/router';
+import * as send from 'koa-send';
+import * as favicon from 'koa-favicon';
+import * as views from 'koa-views';
+
+import packFeed from './feed';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { genOpenapiSpec } from '../api/openapi/gen-spec';
+import config from '@/config/index';
+import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index';
+import * as Acct from 'misskey-js/built/acct';
+import { getNoteSummary } from '@/misc/get-note-summary';
+
+//const _filename = fileURLToPath(import.meta.url);
+const _filename = __filename;
+const _dirname = dirname(_filename);
+
+const staticAssets = `${_dirname}/../../../assets/`;
+const assets = `${_dirname}/../../../../../built/_client_dist_/`;
+
+// Init app
+const app = new Koa();
+
+// Init renderer
+app.use(views(_dirname + '/views', {
+	extension: 'pug',
+	options: {
+		version: config.version,
+		config
+	}
+}));
+
+// Serve favicon
+app.use(favicon(`${_dirname}/../../../assets/favicon.ico`));
+
+// Common request handler
+app.use(async (ctx, next) => {
+	// IFrameの中に入れられないようにする
+	ctx.set('X-Frame-Options', 'DENY');
+	await next();
+});
+
+// Init router
+const router = new Router();
+
+//#region static assets
+
+router.get('/static-assets/(.*)', async ctx => {
+	await send(ctx as any, ctx.path.replace('/static-assets/', ''), {
+		root: staticAssets,
+		maxage: ms('7 days'),
+	});
+});
+
+router.get('/assets/(.*)', async ctx => {
+	await send(ctx as any, ctx.path.replace('/assets/', ''), {
+		root: assets,
+		maxage: ms('7 days'),
+	});
+});
+
+// Apple touch icon
+router.get('/apple-touch-icon.png', async ctx => {
+	await send(ctx as any, '/apple-touch-icon.png', {
+		root: staticAssets
+	});
+});
+
+router.get('/twemoji/(.*)', async ctx => {
+	const path = ctx.path.replace('/twemoji/', '');
+
+	if (!path.match(/^[0-9a-f-]+\.svg$/)) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`);
+
+	await send(ctx as any, path, {
+		root: `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`,
+		maxage: ms('30 days'),
+	});
+});
+
+// ServiceWorker
+router.get('/sw.js', async ctx => {
+	await send(ctx as any, `/sw.${config.version}.js`, {
+		root: assets
+	});
+});
+
+// Manifest
+router.get('/manifest.json', require('./manifest'));
+
+router.get('/robots.txt', async ctx => {
+	await send(ctx as any, '/robots.txt', {
+		root: staticAssets
+	});
+});
+
+//#endregion
+
+// Docs
+router.get('/api-doc', async ctx => {
+	await send(ctx as any, '/redoc.html', {
+		root: staticAssets
+	});
+});
+
+// URL preview endpoint
+router.get('/url', require('./url-preview'));
+
+router.get('/api.json', async ctx => {
+	ctx.body = genOpenapiSpec();
+});
+
+const getFeed = async (acct: string) => {
+	const { username, host } = Acct.parse(acct);
+	const user = await Users.findOne({
+		usernameLower: username.toLowerCase(),
+		host,
+		isSuspended: false
+	});
+
+	return user && await packFeed(user);
+};
+
+// Atom
+router.get('/@:user.atom', async ctx => {
+	const feed = await getFeed(ctx.params.user);
+
+	if (feed) {
+		ctx.set('Content-Type', 'application/atom+xml; charset=utf-8');
+		ctx.body = feed.atom1();
+	} else {
+		ctx.status = 404;
+	}
+});
+
+// RSS
+router.get('/@:user.rss', async ctx => {
+	const feed = await getFeed(ctx.params.user);
+
+	if (feed) {
+		ctx.set('Content-Type', 'application/rss+xml; charset=utf-8');
+		ctx.body = feed.rss2();
+	} else {
+		ctx.status = 404;
+	}
+});
+
+// JSON
+router.get('/@:user.json', async ctx => {
+	const feed = await getFeed(ctx.params.user);
+
+	if (feed) {
+		ctx.set('Content-Type', 'application/json; charset=utf-8');
+		ctx.body = feed.json1();
+	} else {
+		ctx.status = 404;
+	}
+});
+
+//#region SSR (for crawlers)
+// User
+router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
+	const { username, host } = Acct.parse(ctx.params.user);
+	const user = await Users.findOne({
+		usernameLower: username.toLowerCase(),
+		host,
+		isSuspended: false
+	});
+
+	if (user != null) {
+		const profile = await UserProfiles.findOneOrFail(user.id);
+		const meta = await fetchMeta();
+		const me = profile.fields
+			? profile.fields
+				.filter(filed => filed.value != null && filed.value.match(/^https?:/))
+				.map(field => field.value)
+			: [];
+
+		await ctx.render('user', {
+			user, profile, me,
+			sub: ctx.params.sub,
+			instanceName: meta.name || 'Misskey',
+			icon: meta.iconUrl
+		});
+		ctx.set('Cache-Control', 'public, max-age=30');
+	} else {
+		// リモートユーザーなので
+		// モデレータがAPI経由で参照可能にするために404にはしない
+		await next();
+	}
+});
+
+router.get('/users/:user', async ctx => {
+	const user = await Users.findOne({
+		id: ctx.params.user,
+		host: null,
+		isSuspended: false
+	});
+
+	if (user == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
+});
+
+// Note
+router.get('/notes/:note', async (ctx, next) => {
+	const note = await Notes.findOne(ctx.params.note);
+
+	if (note) {
+		const _note = await Notes.pack(note);
+		const profile = await UserProfiles.findOneOrFail(note.userId);
+		const meta = await fetchMeta();
+		await ctx.render('note', {
+			note: _note,
+			profile,
+			// TODO: Let locale changeable by instance setting
+			summary: getNoteSummary(_note),
+			instanceName: meta.name || 'Misskey',
+			icon: meta.iconUrl
+		});
+
+		if (['public', 'home'].includes(note.visibility)) {
+			ctx.set('Cache-Control', 'public, max-age=180');
+		} else {
+			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+		}
+
+		return;
+	}
+
+	await next();
+});
+
+// Page
+router.get('/@:user/pages/:page', async (ctx, next) => {
+	const { username, host } = Acct.parse(ctx.params.user);
+	const user = await Users.findOne({
+		usernameLower: username.toLowerCase(),
+		host
+	});
+
+	if (user == null) return;
+
+	const page = await Pages.findOne({
+		name: ctx.params.page,
+		userId: user.id
+	});
+
+	if (page) {
+		const _page = await Pages.pack(page);
+		const profile = await UserProfiles.findOneOrFail(page.userId);
+		const meta = await fetchMeta();
+		await ctx.render('page', {
+			page: _page,
+			profile,
+			instanceName: meta.name || 'Misskey'
+		});
+
+		if (['public'].includes(page.visibility)) {
+			ctx.set('Cache-Control', 'public, max-age=180');
+		} else {
+			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+		}
+
+		return;
+	}
+
+	await next();
+});
+
+// Clip
+// TODO: 非publicなclipのハンドリング
+router.get('/clips/:clip', async (ctx, next) => {
+	const clip = await Clips.findOne({
+		id: ctx.params.clip,
+	});
+
+	if (clip) {
+		const _clip = await Clips.pack(clip);
+		const profile = await UserProfiles.findOneOrFail(clip.userId);
+		const meta = await fetchMeta();
+		await ctx.render('clip', {
+			clip: _clip,
+			profile,
+			instanceName: meta.name || 'Misskey'
+		});
+
+		ctx.set('Cache-Control', 'public, max-age=180');
+
+		return;
+	}
+
+	await next();
+});
+
+// Gallery post
+router.get('/gallery/:post', async (ctx, next) => {
+	const post = await GalleryPosts.findOne(ctx.params.post);
+
+	if (post) {
+		const _post = await GalleryPosts.pack(post);
+		const profile = await UserProfiles.findOneOrFail(post.userId);
+		const meta = await fetchMeta();
+		await ctx.render('gallery-post', {
+			post: _post,
+			profile,
+			instanceName: meta.name || 'Misskey',
+			icon: meta.iconUrl
+		});
+
+		ctx.set('Cache-Control', 'public, max-age=180');
+
+		return;
+	}
+
+	await next();
+});
+
+// Channel
+router.get('/channels/:channel', async (ctx, next) => {
+	const channel = await Channels.findOne({
+		id: ctx.params.channel,
+	});
+
+	if (channel) {
+		const _channel = await Channels.pack(channel);
+		const meta = await fetchMeta();
+		await ctx.render('channel', {
+			channel: _channel,
+			instanceName: meta.name || 'Misskey'
+		});
+
+		ctx.set('Cache-Control', 'public, max-age=180');
+
+		return;
+	}
+
+	await next();
+});
+//#endregion
+
+router.get('/_info_card_', async ctx => {
+	const meta = await fetchMeta(true);
+
+	ctx.remove('X-Frame-Options');
+
+	await ctx.render('info-card', {
+		version: config.version,
+		host: config.host,
+		meta: meta,
+		originalUsersCount: await Users.count({ host: null }),
+		originalNotesCount: await Notes.count({ userHost: null })
+	});
+});
+
+router.get('/bios', async ctx => {
+	await ctx.render('bios', {
+		version: config.version,
+	});
+});
+
+router.get('/cli', async ctx => {
+	await ctx.render('cli', {
+		version: config.version,
+	});
+});
+
+const override = (source: string, target: string, depth: number = 0) =>
+	[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
+
+router.get('/othello', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games/reversi', 1)));
+router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games')));
+
+router.get('/flush', async ctx => {
+	await ctx.render('flush');
+});
+
+// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
+router.get('/streaming', async ctx => {
+	ctx.status = 503;
+	ctx.set('Cache-Control', 'private, max-age=0');
+});
+
+// Render base html for all requests
+router.get('(.*)', async ctx => {
+	const meta = await fetchMeta();
+	await ctx.render('base', {
+		img: meta.bannerUrl,
+		title: meta.name || 'Misskey',
+		instanceName: meta.name || 'Misskey',
+		desc: meta.description,
+		icon: meta.iconUrl
+	});
+	ctx.set('Cache-Control', 'public, max-age=300');
+});
+
+// Register router
+app.use(router.routes());
+
+module.exports = app;
diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json
new file mode 100644
index 0000000000..48030a2980
--- /dev/null
+++ b/packages/backend/src/server/web/manifest.json
@@ -0,0 +1,28 @@
+{
+	"short_name": "Misskey",
+	"name": "Misskey",
+	"start_url": "/",
+	"display": "standalone",
+	"background_color": "#313a42",
+	"theme_color": "#86b300",
+	"icons": [
+		{
+			"src": "/static-assets/icons/192.png",
+			"sizes": "192x192",
+			"type": "image/png"
+		},
+		{
+			"src": "/static-assets/icons/512.png",
+			"sizes": "512x512",
+			"type": "image/png"
+		}
+	],
+	"share_target": {
+		"action": "/share/",
+		"params": {
+			"title": "title",
+			"text": "text",
+			"url": "url"
+		}
+	}
+}
diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts
new file mode 100644
index 0000000000..918fe27c03
--- /dev/null
+++ b/packages/backend/src/server/web/manifest.ts
@@ -0,0 +1,15 @@
+import * as Koa from 'koa';
+import * as manifest from './manifest.json';
+import { fetchMeta } from '@/misc/fetch-meta';
+
+module.exports = async (ctx: Koa.Context) => {
+	const json = JSON.parse(JSON.stringify(manifest));
+
+	const instance = await fetchMeta(true);
+
+	json.short_name = instance.name || 'Misskey';
+	json.name = instance.name || 'Misskey';
+
+	ctx.set('Cache-Control', 'max-age=300');
+	ctx.body = json;
+};
diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css
new file mode 100644
index 0000000000..43fbe1ab06
--- /dev/null
+++ b/packages/backend/src/server/web/style.css
@@ -0,0 +1,29 @@
+html {
+	background-color: var(--bg);
+	color: var(--fg);
+}
+
+#splash {
+	position: fixed;
+	z-index: 10000;
+	top: 0;
+	left: 0;
+	width: 100vw;
+	height: 100vh;
+	cursor: wait;
+	background-color: var(--bg);
+	opacity: 1;
+	transition: opacity 0.5s ease;
+}
+
+#splash > img {
+	position: absolute;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	left: 0;
+	margin: auto;
+	width: 64px;
+	height: 64px;
+	pointer-events: none;
+}
diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts
new file mode 100644
index 0000000000..1375420c0a
--- /dev/null
+++ b/packages/backend/src/server/web/url-preview.ts
@@ -0,0 +1,53 @@
+import * as Koa from 'koa';
+import summaly from 'summaly';
+import { fetchMeta } from '@/misc/fetch-meta';
+import Logger from '@/services/logger';
+import config from '@/config/index';
+import { query } from '@/prelude/url';
+import { getJson } from '@/misc/fetch';
+
+const logger = new Logger('url-preview');
+
+module.exports = async (ctx: Koa.Context) => {
+	const meta = await fetchMeta();
+
+	logger.info(meta.summalyProxy
+		? `(Proxy) Getting preview of ${ctx.query.url}@${ctx.query.lang} ...`
+		: `Getting preview of ${ctx.query.url}@${ctx.query.lang} ...`);
+
+	try {
+		const summary = meta.summalyProxy ? await getJson(`${meta.summalyProxy}?${query({
+			url: ctx.query.url,
+			lang: ctx.query.lang || 'ja-JP'
+		})}`) : await summaly(ctx.query.url, {
+			followRedirects: false,
+			lang: ctx.query.lang || 'ja-JP'
+		});
+
+		logger.succ(`Got preview of ${ctx.query.url}: ${summary.title}`);
+
+		summary.icon = wrap(summary.icon);
+		summary.thumbnail = wrap(summary.thumbnail);
+
+		// Cache 7days
+		ctx.set('Cache-Control', 'max-age=604800, immutable');
+
+		ctx.body = summary;
+	} catch (e) {
+		logger.warn(`Failed to get preview of ${ctx.query.url}: ${e}`);
+		ctx.status = 200;
+		ctx.set('Cache-Control', 'max-age=86400, immutable');
+		ctx.body = '{}';
+	}
+};
+
+function wrap(url?: string): string | null {
+	return url != null
+		? url.match(/^https?:\/\//)
+			? `${config.url}/proxy/preview.jpg?${query({
+				url,
+				preview: '1'
+			})}`
+			: url
+		: null;
+}
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
new file mode 100644
index 0000000000..42c068c403
--- /dev/null
+++ b/packages/backend/src/server/web/views/base.pug
@@ -0,0 +1,60 @@
+block vars
+
+doctype html
+
+!= '<!--\n'
+!= '  _____ _         _           \n'
+!= ' |     |_|___ ___| |_ ___ _ _ \n'
+!= ' | | | | |_ -|_ -| \'_| -_| | |\n'
+!= ' |_|_|_|_|___|___|_,_|___|_  |\n'
+!= '                         |___|\n'
+!= ' Thank you for using Misskey!\n'
+!= ' If you are reading this message... how about joining the development?\n'
+!= ' https://github.com/misskey-dev/misskey'
+!= '\n-->\n'
+
+html
+
+	head
+		meta(charset='utf-8')
+		meta(name='application-name' content='Misskey')
+		meta(name='referrer' content='origin')
+		meta(name='theme-color' content='#86b300')
+		meta(name='theme-color-orig' content='#86b300')
+		meta(property='og:site_name' content= instanceName || 'Misskey')
+		meta(name='viewport' content='width=device-width, initial-scale=1')
+		link(rel='icon' href= icon || '/favicon.ico')
+		link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
+		link(rel='manifest' href='/manifest.json')
+		link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
+		link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
+		link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
+		link(rel='preload' href='https://use.fontawesome.com/releases/v5.15.3/css/all.css' as='style')
+		link(rel='stylesheet' href='https://use.fontawesome.com/releases/v5.15.3/css/all.css')
+
+		title
+			block title
+				= title || 'Misskey'
+
+		block desc
+			meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
+
+		block meta
+
+		block og
+			meta(property='og:image' content=img)
+
+		style
+			include ../style.css
+
+		script
+			include ../boot.js
+
+	body
+		noscript: p
+			| JavaScriptを有効にしてください
+			br
+			| Please turn on your JavaScript
+		div#splash
+			img(src='/favicon.ico')
+		block content
diff --git a/packages/backend/src/server/web/views/bios.pug b/packages/backend/src/server/web/views/bios.pug
new file mode 100644
index 0000000000..d81a3ee67f
--- /dev/null
+++ b/packages/backend/src/server/web/views/bios.pug
@@ -0,0 +1,20 @@
+doctype html
+
+html
+
+	head
+		meta(charset='utf-8')
+		meta(name='application-name' content='Misskey')
+		title Misskey BIOS
+		style
+			include ../bios.css
+		script
+			include ../bios.js
+
+	body
+		header
+			h1 Misskey BIOS #{version}
+		main
+			div.tabs
+				button#ls edit local storage
+			div#content
diff --git a/packages/backend/src/server/web/views/channel.pug b/packages/backend/src/server/web/views/channel.pug
new file mode 100644
index 0000000000..273632f0e0
--- /dev/null
+++ b/packages/backend/src/server/web/views/channel.pug
@@ -0,0 +1,21 @@
+extends ./base
+
+block vars
+	- const title = channel.name;
+	- const url = `${config.url}/channels/${channel.id}`;
+
+block title
+	= `${title} | ${instanceName}`
+
+block desc
+	meta(name='description' content= channel.description)
+
+block og
+	meta(property='og:type'        content='article')
+	meta(property='og:title'       content= title)
+	meta(property='og:description' content= channel.description)
+	meta(property='og:url'         content= url)
+	meta(property='og:image'       content= channel.bannerUrl)
+
+block meta
+	meta(name='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/cli.pug b/packages/backend/src/server/web/views/cli.pug
new file mode 100644
index 0000000000..d2cf7c4335
--- /dev/null
+++ b/packages/backend/src/server/web/views/cli.pug
@@ -0,0 +1,21 @@
+doctype html
+
+html
+
+	head
+		meta(charset='utf-8')
+		meta(name='application-name' content='Misskey')
+		title Misskey Cli
+		style
+			include ../cli.css
+		script
+			include ../cli.js
+
+	body
+		header
+			h1 Misskey Cli #{version}
+		main
+			div#form
+				textarea#text
+				button#submit submit
+			div#tl
diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug
new file mode 100644
index 0000000000..8de53f19d6
--- /dev/null
+++ b/packages/backend/src/server/web/views/clip.pug
@@ -0,0 +1,33 @@
+extends ./base
+
+block vars
+	- const user = clip.user;
+	- const title = clip.name;
+	- const url = `${config.url}/clips/${clip.id}`;
+
+block title
+	= `${title} | ${instanceName}`
+
+block desc
+	meta(name='description' content= clip.description)
+
+block og
+	meta(property='og:type'        content='article')
+	meta(property='og:title'       content= title)
+	meta(property='og:description' content= clip.description)
+	meta(property='og:url'         content= url)
+	meta(property='og:image'       content= user.avatarUrl)
+
+block meta
+	if profile.noCrawle
+		meta(name='robots' content='noindex')
+
+	meta(name='misskey:user-username' content=user.username)
+	meta(name='misskey:user-id' content=user.id)
+	meta(name='misskey:clip-id' content=clip.id)
+
+	meta(name='twitter:card' content='summary')
+
+	// todo
+	if user.twitter
+		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/flush.pug b/packages/backend/src/server/web/views/flush.pug
new file mode 100644
index 0000000000..ec585a34db
--- /dev/null
+++ b/packages/backend/src/server/web/views/flush.pug
@@ -0,0 +1,47 @@
+doctype html
+
+html
+	#msg
+	script.
+		const msg = document.getElementById('msg');
+		const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
+
+		message('Start flushing.');
+
+		(async function() {
+			try {
+				localStorage.clear();
+				message('localStorage cleared.');
+
+				const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
+					const delidb = indexedDB.deleteDatabase(name);
+					delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
+					delidb.onerror = e => rej(e)
+				}));
+
+				await Promise.all(idbPromises);
+
+				if (navigator.serviceWorker.controller) {
+					navigator.serviceWorker.controller.postMessage('clear');
+					await navigator.serviceWorker.getRegistrations()
+						.then(registrations => {
+							return Promise.all(registrations.map(registration => registration.unregister()));
+						})
+						.catch(e => { throw Error(e) });
+				}
+
+				message(successText);
+			} catch (e) {
+				message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
+				message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
+
+				console.error(e);
+				setTimeout(() => {
+					location = '/';
+				}, 10000)
+			}
+		})();
+
+		function message(text) {
+			msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)
+		}
diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug
new file mode 100644
index 0000000000..95bbb2437c
--- /dev/null
+++ b/packages/backend/src/server/web/views/gallery-post.pug
@@ -0,0 +1,35 @@
+extends ./base
+
+block vars
+	- const user = post.user;
+	- const title = post.title;
+	- const url = `${config.url}/gallery/${post.id}`;
+
+block title
+	= `${title} | ${instanceName}`
+
+block desc
+	meta(name='description' content= post.description)
+
+block og
+	meta(property='og:type'        content='article')
+	meta(property='og:title'       content= title)
+	meta(property='og:description' content= post.description)
+	meta(property='og:url'         content= url)
+	meta(property='og:image'       content= post.files[0].thumbnailUrl)
+
+block meta
+	if user.host || profile.noCrawle
+		meta(name='robots' content='noindex')
+
+	meta(name='misskey:user-username' content=user.username)
+	meta(name='misskey:user-id' content=user.id)
+
+	meta(name='twitter:card' content='summary')
+
+	// todo
+	if user.twitter
+		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+
+	if !user.host
+		link(rel='alternate' href=url type='application/activity+json')
diff --git a/packages/backend/src/server/web/views/info-card.pug b/packages/backend/src/server/web/views/info-card.pug
new file mode 100644
index 0000000000..1d62778ce1
--- /dev/null
+++ b/packages/backend/src/server/web/views/info-card.pug
@@ -0,0 +1,50 @@
+doctype html
+
+html
+
+	head
+		meta(charset='utf-8')
+		meta(name='application-name' content='Misskey')
+		title= meta.name || host
+		style.
+			html, body {
+				margin: 0;
+				padding: 0;
+				min-height: 100vh;
+				background: #fff;
+			}
+
+			#a {
+				display: block;
+			}
+
+			#banner {
+				background-size: cover;
+				background-position: center center;
+			}
+
+			#title {
+				display: inline-block;
+				margin: 24px;
+				padding: 0.5em 0.8em;
+				color: #fff;
+				background: rgba(0, 0, 0, 0.5);
+				font-weight: bold;
+				font-size: 1.3em;
+			}
+
+			#content {
+				overflow: auto;
+				color: #353c3e;
+			}
+
+			#description {
+				margin: 24px;
+			}
+
+	body
+		a#a(href=`https://${host}` target="_blank")
+			header#banner(style=`background-image: url(${meta.bannerUrl})`)
+				div#title= meta.name || host
+		div#content
+			div#description= meta.description
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
new file mode 100644
index 0000000000..7030936975
--- /dev/null
+++ b/packages/backend/src/server/web/views/note.pug
@@ -0,0 +1,43 @@
+extends ./base
+
+block vars
+	- const user = note.user;
+	- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
+	- const url = `${config.url}/notes/${note.id}`;
+
+block title
+	= `${title} | ${instanceName}`
+
+block desc
+	meta(name='description' content= summary)
+
+block og
+	meta(property='og:type'        content='article')
+	meta(property='og:title'       content= title)
+	meta(property='og:description' content= summary)
+	meta(property='og:url'         content= url)
+	meta(property='og:image'       content= user.avatarUrl)
+
+block meta
+	if user.host || profile.noCrawle
+		meta(name='robots' content='noindex')
+
+	meta(name='misskey:user-username' content=user.username)
+	meta(name='misskey:user-id' content=user.id)
+	meta(name='misskey:note-id' content=note.id)
+
+	meta(name='twitter:card' content='summary')
+
+	// todo
+	if user.twitter
+		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+
+	if note.prev
+		link(rel='prev' href=`${config.url}/notes/${note.prev}`)
+	if note.next
+		link(rel='next' href=`${config.url}/notes/${note.next}`)
+
+	if !user.host
+		link(rel='alternate' href=url type='application/activity+json')
+	if note.uri
+		link(rel='alternate' href=note.uri type='application/activity+json')
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
new file mode 100644
index 0000000000..cb9e1039e1
--- /dev/null
+++ b/packages/backend/src/server/web/views/page.pug
@@ -0,0 +1,33 @@
+extends ./base
+
+block vars
+	- const user = page.user;
+	- const title = page.title;
+	- const url = `${config.url}/@${user.username}/${page.name}`;
+
+block title
+	= `${title} | ${instanceName}`
+
+block desc
+	meta(name='description' content= page.summary)
+
+block og
+	meta(property='og:type'        content='article')
+	meta(property='og:title'       content= title)
+	meta(property='og:description' content= page.summary)
+	meta(property='og:url'         content= url)
+	meta(property='og:image'       content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : user.avatarUrl)
+
+block meta
+	if profile.noCrawle
+		meta(name='robots' content='noindex')
+
+	meta(name='misskey:user-username' content=user.username)
+	meta(name='misskey:user-id' content=user.id)
+	meta(name='misskey:page-id' content=page.id)
+
+	meta(name='twitter:card' content='summary')
+
+	// todo
+	if user.twitter
+		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug
new file mode 100644
index 0000000000..1a8a6b4413
--- /dev/null
+++ b/packages/backend/src/server/web/views/user.pug
@@ -0,0 +1,42 @@
+extends ./base
+
+block vars
+	- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
+	- const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
+	- const img = user.avatarUrl || null;
+
+block title
+	= `${title} | ${instanceName}`
+
+block desc
+	meta(name='description' content= profile.description)
+
+block og
+	meta(property='og:type'        content='blog')
+	meta(property='og:title'       content= title)
+	meta(property='og:description' content= profile.description)
+	meta(property='og:url'         content= url)
+	meta(property='og:image'       content= img)
+
+block meta
+	if user.host || profile.noCrawle
+		meta(name='robots' content='noindex')
+
+	meta(name='misskey:user-username' content=user.username)
+	meta(name='misskey:user-id' content=user.id)
+
+	meta(name='twitter:card' content='summary')
+
+	if profile.twitter
+		meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
+
+	if !sub
+		if !user.host
+			link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
+		if user.uri
+			link(rel='alternate' href=user.uri type='application/activity+json')
+		if profile.url
+			link(rel='alternate' href=profile.url type='text/html')
+
+	each m in me
+		link(rel='me' href=`${m}`)
diff --git a/packages/backend/src/server/well-known.ts b/packages/backend/src/server/well-known.ts
new file mode 100644
index 0000000000..5e99b0065c
--- /dev/null
+++ b/packages/backend/src/server/well-known.ts
@@ -0,0 +1,149 @@
+import * as Router from '@koa/router';
+
+import config from '@/config/index';
+import * as Acct from 'misskey-js/built/acct';
+import { links } from './nodeinfo';
+import { escapeAttribute, escapeValue } from '@/prelude/xml';
+import { Users } from '@/models/index';
+import { User } from '@/models/entities/user';
+
+// Init router
+const router = new Router();
+
+const XRD = (...x: { element: string, value?: string, attributes?: Record<string, string> }[]) =>
+	`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">${x.map(({ element, value, attributes }) =>
+	`<${
+		Object.entries(typeof attributes === 'object' && attributes || {}).reduce((a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`, element)
+	}${
+		typeof value === 'string' ? `>${escapeValue(value)}</${element}` : '/'
+	}>`).reduce((a, c) => a + c, '')}</XRD>`;
+
+const allPath = '/.well-known/(.*)';
+const webFingerPath = '/.well-known/webfinger';
+const jrd = 'application/jrd+json';
+const xrd = 'application/xrd+xml';
+
+router.use(allPath, async (ctx, next) => {
+	ctx.set({
+		'Access-Control-Allow-Headers': 'Accept',
+		'Access-Control-Allow-Methods': 'GET, OPTIONS',
+		'Access-Control-Allow-Origin': '*',
+		'Access-Control-Expose-Headers': 'Vary',
+	});
+	await next();
+});
+
+router.options(allPath, async ctx => {
+	ctx.status = 204;
+});
+
+router.get('/.well-known/host-meta', async ctx => {
+	ctx.set('Content-Type', xrd);
+	ctx.body = XRD({ element: 'Link', attributes: {
+		type: xrd,
+		template: `${config.url}${webFingerPath}?resource={uri}`
+	}});
+});
+
+router.get('/.well-known/host-meta.json', async ctx => {
+	ctx.set('Content-Type', jrd);
+	ctx.body = {
+		links: [{
+			rel: 'lrdd',
+			type: jrd,
+			template: `${config.url}${webFingerPath}?resource={uri}`
+		}]
+	};
+});
+
+router.get('/.well-known/nodeinfo', async ctx => {
+	ctx.body = { links };
+});
+
+/* TODO
+router.get('/.well-known/change-password', async ctx => {
+});
+*/
+
+router.get(webFingerPath, async ctx => {
+	const fromId = (id: User['id']): Record<string, any> => ({
+		id,
+		host: null,
+		isSuspended: false
+	});
+
+	const generateQuery = (resource: string) =>
+		resource.startsWith(`${config.url.toLowerCase()}/users/`) ?
+			fromId(resource.split('/').pop()!) :
+			fromAcct(Acct.parse(
+				resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop()! :
+				resource.startsWith('acct:') ? resource.slice('acct:'.length) :
+				resource));
+
+	const fromAcct = (acct: Acct.Acct): Record<string, any> | number =>
+		!acct.host || acct.host === config.host.toLowerCase() ? {
+			usernameLower: acct.username,
+			host: null,
+			isSuspended: false
+		} : 422;
+
+	if (typeof ctx.query.resource !== 'string') {
+		ctx.status = 400;
+		return;
+	}
+
+	const query = generateQuery(ctx.query.resource.toLowerCase());
+
+	if (typeof query === 'number') {
+		ctx.status = query;
+		return;
+	}
+
+	const user = await Users.findOne(query);
+
+	if (user == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	const subject = `acct:${user.username}@${config.host}`;
+	const self = {
+		rel: 'self',
+		type: 'application/activity+json',
+		href: `${config.url}/users/${user.id}`
+	};
+	const profilePage = {
+		rel: 'http://webfinger.net/rel/profile-page',
+		type: 'text/html',
+		href: `${config.url}/@${user.username}`
+	};
+	const subscribe = {
+		rel: 'http://ostatus.org/schema/1.0/subscribe',
+		template: `${config.url}/authorize-follow?acct={uri}`
+	};
+
+	if (ctx.accepts(jrd, xrd) === xrd) {
+		ctx.body = XRD(
+			{ element: 'Subject', value: subject },
+			{ element: 'Link', attributes: self },
+			{ element: 'Link', attributes: profilePage },
+			{ element: 'Link', attributes: subscribe });
+		ctx.type = xrd;
+	} else {
+		ctx.body = {
+			subject,
+			links: [self, profilePage, subscribe]
+		};
+		ctx.type = jrd;
+	}
+
+	ctx.vary('Accept');
+	ctx.set('Cache-Control', 'public, max-age=180');
+});
+
+// Return 404 for other .well-known
+router.all(allPath, async ctx => {
+	ctx.status = 404;
+});
+
+export default router;
diff --git a/packages/backend/src/services/add-note-to-antenna.ts b/packages/backend/src/services/add-note-to-antenna.ts
new file mode 100644
index 0000000000..3aedbd2c32
--- /dev/null
+++ b/packages/backend/src/services/add-note-to-antenna.ts
@@ -0,0 +1,54 @@
+import { Antenna } from '@/models/entities/antenna';
+import { Note } from '@/models/entities/note';
+import { AntennaNotes, Mutings, Notes } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { isMutedUserRelated } from '@/misc/is-muted-user-related';
+import { publishAntennaStream, publishMainStream } from '@/services/stream';
+import { User } from '@/models/entities/user';
+
+export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }) {
+	// 通知しない設定になっているか、自分自身の投稿なら既読にする
+	const read = !antenna.notify || (antenna.userId === noteUser.id);
+
+	AntennaNotes.insert({
+		id: genId(),
+		antennaId: antenna.id,
+		noteId: note.id,
+		read: read,
+	});
+
+	publishAntennaStream(antenna.id, 'note', note);
+
+	if (!read) {
+		const mutings = await Mutings.find({
+			where: {
+				muterId: antenna.userId
+			},
+			select: ['muteeId']
+		});
+
+		// Copy
+		const _note: Note = {
+			...note
+		};
+
+		if (note.replyId != null) {
+			_note.reply = await Notes.findOneOrFail(note.replyId);
+		}
+		if (note.renoteId != null) {
+			_note.renote = await Notes.findOneOrFail(note.renoteId);
+		}
+
+		if (isMutedUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
+			return;
+		}
+
+		// 2秒経っても既読にならなかったら通知
+		setTimeout(async () => {
+			const unread = await AntennaNotes.findOne({ antennaId: antenna.id, read: false });
+			if (unread) {
+				publishMainStream(antenna.userId, 'unreadAntenna', antenna);
+			}
+		}, 2000);
+	}
+}
diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts
new file mode 100644
index 0000000000..6aadc847a9
--- /dev/null
+++ b/packages/backend/src/services/blocking/create.ts
@@ -0,0 +1,129 @@
+import { publishMainStream, publishUserEvent } from '@/services/stream';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderFollow from '@/remote/activitypub/renderer/follow';
+import renderUndo from '@/remote/activitypub/renderer/undo';
+import renderBlock from '@/remote/activitypub/renderer/block';
+import { deliver } from '@/queue/index';
+import renderReject from '@/remote/activitypub/renderer/reject';
+import { User } from '@/models/entities/user';
+import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index';
+import { perUserFollowingChart } from '@/services/chart/index';
+import { genId } from '@/misc/gen-id';
+import { IdentifiableError } from '@/misc/identifiable-error';
+
+export default async function(blocker: User, blockee: User) {
+	await Promise.all([
+		cancelRequest(blocker, blockee),
+		cancelRequest(blockee, blocker),
+		unFollow(blocker, blockee),
+		unFollow(blockee, blocker),
+		removeFromList(blockee, blocker),
+	]);
+
+	await Blockings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		blockerId: blocker.id,
+		blockeeId: blockee.id,
+	});
+
+	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
+		const content = renderActivity(renderBlock(blocker, blockee));
+		deliver(blocker, content, blockee.inbox);
+	}
+}
+
+async function cancelRequest(follower: User, followee: User) {
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	if (request == null) {
+		return;
+	}
+
+	await FollowRequests.delete({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	if (Users.isLocalUser(followee)) {
+		Users.pack(followee, followee, {
+			detail: true
+		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
+	}
+
+	if (Users.isLocalUser(follower)) {
+		Users.pack(followee, follower, {
+			detail: true
+		}).then(packed => {
+			publishUserEvent(follower.id, 'unfollow', packed);
+			publishMainStream(follower.id, 'unfollow', packed);
+		});
+	}
+
+	// リモートにフォローリクエストをしていたらUndoFollow送信
+	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
+		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
+		deliver(follower, content, followee.inbox);
+	}
+
+	// リモートからフォローリクエストを受けていたらReject送信
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+		const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId!), followee));
+		deliver(followee, content, follower.inbox);
+	}
+}
+
+async function unFollow(follower: User, followee: User) {
+	const following = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
+	});
+
+	if (following == null) {
+		return;
+	}
+
+	Followings.delete(following.id);
+
+	//#region Decrement following count
+	Users.decrement({ id: follower.id }, 'followingCount', 1);
+	//#endregion
+
+	//#region Decrement followers count
+	Users.decrement({ id: followee.id }, 'followersCount', 1);
+	//#endregion
+
+	perUserFollowingChart.update(follower, followee, false);
+
+	// Publish unfollow event
+	if (Users.isLocalUser(follower)) {
+		Users.pack(followee, follower, {
+			detail: true
+		}).then(packed => {
+			publishUserEvent(follower.id, 'unfollow', packed);
+			publishMainStream(follower.id, 'unfollow', packed);
+		});
+	}
+
+	// リモートにフォローをしていたらUndoFollow送信
+	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
+		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
+		deliver(follower, content, followee.inbox);
+	}
+}
+
+async function removeFromList(listOwner: User, user: User) {
+	const userLists = await UserLists.find({
+		userId: listOwner.id,
+	});
+
+	for (const userList of userLists) {
+		await UserListJoinings.delete({
+			userListId: userList.id,
+			userId: user.id,
+		});
+	}
+}
diff --git a/packages/backend/src/services/blocking/delete.ts b/packages/backend/src/services/blocking/delete.ts
new file mode 100644
index 0000000000..de7efb1558
--- /dev/null
+++ b/packages/backend/src/services/blocking/delete.ts
@@ -0,0 +1,29 @@
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderBlock from '@/remote/activitypub/renderer/block';
+import renderUndo from '@/remote/activitypub/renderer/undo';
+import { deliver } from '@/queue/index';
+import Logger from '../logger';
+import { User } from '@/models/entities/user';
+import { Blockings, Users } from '@/models/index';
+
+const logger = new Logger('blocking/delete');
+
+export default async function(blocker: User, blockee: User) {
+	const blocking = await Blockings.findOne({
+		blockerId: blocker.id,
+		blockeeId: blockee.id
+	});
+
+	if (blocking == null) {
+		logger.warn('ブロック解除がリクエストされましたがブロックしていませんでした');
+		return;
+	}
+
+	Blockings.delete(blocking.id);
+
+	// deliver if remote bloking
+	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
+		const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker));
+		deliver(blocker, content, blockee.inbox);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/active-users.ts b/packages/backend/src/services/chart/charts/classes/active-users.ts
new file mode 100644
index 0000000000..f80d8a3322
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/active-users.ts
@@ -0,0 +1,47 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '@/models/entities/user';
+import { SchemaType } from '@/misc/schema';
+import { Users } from '@/models/index';
+import { name, schema } from '../schemas/active-users';
+
+type ActiveUsersLog = SchemaType<typeof schema>;
+
+export default class ActiveUsersChart extends Chart<ActiveUsersLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: ActiveUsersLog): DeepPartial<ActiveUsersLog> {
+		return {};
+	}
+
+	@autobind
+	protected aggregate(logs: ActiveUsersLog[]): ActiveUsersLog {
+		return {
+			local: {
+				users: logs.reduce((a, b) => a.concat(b.local.users), [] as ActiveUsersLog['local']['users']),
+			},
+			remote: {
+				users: logs.reduce((a, b) => a.concat(b.remote.users), [] as ActiveUsersLog['remote']['users']),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<ActiveUsersLog>> {
+		return {};
+	}
+
+	@autobind
+	public async update(user: { id: User['id'], host: User['host'] }) {
+		const update: Obj = {
+			users: [user.id]
+		};
+
+		await this.inc({
+			[Users.isLocalUser(user) ? 'local' : 'remote']: update
+		});
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/drive.ts b/packages/backend/src/services/chart/charts/classes/drive.ts
new file mode 100644
index 0000000000..93eabf3096
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/drive.ts
@@ -0,0 +1,91 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { DriveFiles } from '@/models/index';
+import { Not, IsNull } from 'typeorm';
+import { DriveFile } from '@/models/entities/drive-file';
+import { name, schema } from '../schemas/drive';
+
+type DriveLog = SchemaType<typeof schema>;
+
+export default class DriveChart extends Chart<DriveLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: DriveLog): DeepPartial<DriveLog> {
+		return {
+			local: {
+				totalCount: latest.local.totalCount,
+				totalSize: latest.local.totalSize,
+			},
+			remote: {
+				totalCount: latest.remote.totalCount,
+				totalSize: latest.remote.totalSize,
+			}
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: DriveLog[]): DriveLog {
+		return {
+			local: {
+				totalCount: logs[0].local.totalCount,
+				totalSize: logs[0].local.totalSize,
+				incCount: logs.reduce((a, b) => a + b.local.incCount, 0),
+				incSize: logs.reduce((a, b) => a + b.local.incSize, 0),
+				decCount: logs.reduce((a, b) => a + b.local.decCount, 0),
+				decSize: logs.reduce((a, b) => a + b.local.decSize, 0),
+			},
+			remote: {
+				totalCount: logs[0].remote.totalCount,
+				totalSize: logs[0].remote.totalSize,
+				incCount: logs.reduce((a, b) => a + b.remote.incCount, 0),
+				incSize: logs.reduce((a, b) => a + b.remote.incSize, 0),
+				decCount: logs.reduce((a, b) => a + b.remote.decCount, 0),
+				decSize: logs.reduce((a, b) => a + b.remote.decSize, 0),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<DriveLog>> {
+		const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([
+			DriveFiles.count({ userHost: null }),
+			DriveFiles.count({ userHost: Not(IsNull()) }),
+			DriveFiles.calcDriveUsageOfLocal(),
+			DriveFiles.calcDriveUsageOfRemote()
+		]);
+
+		return {
+			local: {
+				totalCount: localCount,
+				totalSize: localSize,
+			},
+			remote: {
+				totalCount: remoteCount,
+				totalSize: remoteSize,
+			}
+		};
+	}
+
+	@autobind
+	public async update(file: DriveFile, isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.totalCount = isAdditional ? 1 : -1;
+		update.totalSize = isAdditional ? file.size : -file.size;
+		if (isAdditional) {
+			update.incCount = 1;
+			update.incSize = file.size;
+		} else {
+			update.decCount = 1;
+			update.decSize = file.size;
+		}
+
+		await this.inc({
+			[file.userHost === null ? 'local' : 'remote']: update
+		});
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/federation.ts b/packages/backend/src/services/chart/charts/classes/federation.ts
new file mode 100644
index 0000000000..5f918b294f
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/federation.ts
@@ -0,0 +1,62 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { Instances } from '@/models/index';
+import { name, schema } from '../schemas/federation';
+
+type FederationLog = SchemaType<typeof schema>;
+
+export default class FederationChart extends Chart<FederationLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: FederationLog): DeepPartial<FederationLog> {
+		return {
+			instance: {
+				total: latest.instance.total,
+			}
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: FederationLog[]): FederationLog {
+		return {
+			instance: {
+				total: logs[0].instance.total,
+				inc: logs.reduce((a, b) => a + b.instance.inc, 0),
+				dec: logs.reduce((a, b) => a + b.instance.dec, 0),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<FederationLog>> {
+		const [total] = await Promise.all([
+			Instances.count({})
+		]);
+
+		return {
+			instance: {
+				total: total,
+			}
+		};
+	}
+
+	@autobind
+	public async update(isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.total = isAdditional ? 1 : -1;
+		if (isAdditional) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		await this.inc({
+			instance: update
+		});
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/hashtag.ts b/packages/backend/src/services/chart/charts/classes/hashtag.ts
new file mode 100644
index 0000000000..f7f5e17dec
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/hashtag.ts
@@ -0,0 +1,47 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '@/models/entities/user';
+import { SchemaType } from '@/misc/schema';
+import { Users } from '@/models/index';
+import { name, schema } from '../schemas/hashtag';
+
+type HashtagLog = SchemaType<typeof schema>;
+
+export default class HashtagChart extends Chart<HashtagLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: HashtagLog): DeepPartial<HashtagLog> {
+		return {};
+	}
+
+	@autobind
+	protected aggregate(logs: HashtagLog[]): HashtagLog {
+		return {
+			local: {
+				users: logs.reduce((a, b) => a.concat(b.local.users), [] as HashtagLog['local']['users']),
+			},
+			remote: {
+				users: logs.reduce((a, b) => a.concat(b.remote.users), [] as HashtagLog['remote']['users']),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<HashtagLog>> {
+		return {};
+	}
+
+	@autobind
+	public async update(hashtag: string, user: { id: User['id'], host: User['host'] }) {
+		const update: Obj = {
+			users: [user.id]
+		};
+
+		await this.inc({
+			[Users.isLocalUser(user) ? 'local' : 'remote']: update
+		}, hashtag);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/instance.ts b/packages/backend/src/services/chart/charts/classes/instance.ts
new file mode 100644
index 0000000000..1032de7bc0
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/instance.ts
@@ -0,0 +1,217 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { DriveFiles, Followings, Users, Notes } from '@/models/index';
+import { DriveFile } from '@/models/entities/drive-file';
+import { name, schema } from '../schemas/instance';
+import { Note } from '@/models/entities/note';
+import { toPuny } from '@/misc/convert-host';
+
+type InstanceLog = SchemaType<typeof schema>;
+
+export default class InstanceChart extends Chart<InstanceLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: InstanceLog): DeepPartial<InstanceLog> {
+		return {
+			notes: {
+				total: latest.notes.total,
+			},
+			users: {
+				total: latest.users.total,
+			},
+			following: {
+				total: latest.following.total,
+			},
+			followers: {
+				total: latest.followers.total,
+			},
+			drive: {
+				totalFiles: latest.drive.totalFiles,
+				totalUsage: latest.drive.totalUsage,
+			}
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: InstanceLog[]): InstanceLog {
+		return {
+			requests: {
+				failed: logs.reduce((a, b) => a + b.requests.failed, 0),
+				succeeded: logs.reduce((a, b) => a + b.requests.succeeded, 0),
+				received: logs.reduce((a, b) => a + b.requests.received, 0),
+			},
+			notes: {
+				total: logs[0].notes.total,
+				inc: logs.reduce((a, b) => a + b.notes.inc, 0),
+				dec: logs.reduce((a, b) => a + b.notes.dec, 0),
+				diffs: {
+					reply: logs.reduce((a, b) => a + b.notes.diffs.reply, 0),
+					renote: logs.reduce((a, b) => a + b.notes.diffs.renote, 0),
+					normal: logs.reduce((a, b) => a + b.notes.diffs.normal, 0),
+				},
+			},
+			users: {
+				total: logs[0].users.total,
+				inc: logs.reduce((a, b) => a + b.users.inc, 0),
+				dec: logs.reduce((a, b) => a + b.users.dec, 0),
+			},
+			following: {
+				total: logs[0].following.total,
+				inc: logs.reduce((a, b) => a + b.following.inc, 0),
+				dec: logs.reduce((a, b) => a + b.following.dec, 0),
+			},
+			followers: {
+				total: logs[0].followers.total,
+				inc: logs.reduce((a, b) => a + b.followers.inc, 0),
+				dec: logs.reduce((a, b) => a + b.followers.dec, 0),
+			},
+			drive: {
+				totalFiles: logs[0].drive.totalFiles,
+				totalUsage: logs[0].drive.totalUsage,
+				incFiles: logs.reduce((a, b) => a + b.drive.incFiles, 0),
+				incUsage: logs.reduce((a, b) => a + b.drive.incUsage, 0),
+				decFiles: logs.reduce((a, b) => a + b.drive.decFiles, 0),
+				decUsage: logs.reduce((a, b) => a + b.drive.decUsage, 0),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<InstanceLog>> {
+		const [
+			notesCount,
+			usersCount,
+			followingCount,
+			followersCount,
+			driveFiles,
+			driveUsage,
+		] = await Promise.all([
+			Notes.count({ userHost: group }),
+			Users.count({ host: group }),
+			Followings.count({ followerHost: group }),
+			Followings.count({ followeeHost: group }),
+			DriveFiles.count({ userHost: group }),
+			DriveFiles.calcDriveUsageOfHost(group),
+		]);
+
+		return {
+			notes: {
+				total: notesCount,
+			},
+			users: {
+				total: usersCount,
+			},
+			following: {
+				total: followingCount,
+			},
+			followers: {
+				total: followersCount,
+			},
+			drive: {
+				totalFiles: driveFiles,
+				totalUsage: driveUsage,
+			}
+		};
+	}
+
+	@autobind
+	public async requestReceived(host: string) {
+		await this.inc({
+			requests: {
+				received: 1
+			}
+		}, toPuny(host));
+	}
+
+	@autobind
+	public async requestSent(host: string, isSucceeded: boolean) {
+		const update: Obj = {};
+
+		if (isSucceeded) {
+			update.succeeded = 1;
+		} else {
+			update.failed = 1;
+		}
+
+		await this.inc({
+			requests: update
+		}, toPuny(host));
+	}
+
+	@autobind
+	public async newUser(host: string) {
+		await this.inc({
+			users: {
+				total: 1,
+				inc: 1
+			}
+		}, toPuny(host));
+	}
+
+	@autobind
+	public async updateNote(host: string, note: Note, isAdditional: boolean) {
+		const diffs = {} as any;
+
+		if (note.replyId != null) {
+			diffs.reply = isAdditional ? 1 : -1;
+		} else if (note.renoteId != null) {
+			diffs.renote = isAdditional ? 1 : -1;
+		} else {
+			diffs.normal = isAdditional ? 1 : -1;
+		}
+
+		await this.inc({
+			notes: {
+				total: isAdditional ? 1 : -1,
+				inc: isAdditional ? 1 : 0,
+				dec: isAdditional ? 0 : 1,
+				diffs: diffs
+			}
+		}, toPuny(host));
+	}
+
+	@autobind
+	public async updateFollowing(host: string, isAdditional: boolean) {
+		await this.inc({
+			following: {
+				total: isAdditional ? 1 : -1,
+				inc: isAdditional ? 1 : 0,
+				dec: isAdditional ? 0 : 1,
+			}
+		}, toPuny(host));
+	}
+
+	@autobind
+	public async updateFollowers(host: string, isAdditional: boolean) {
+		await this.inc({
+			followers: {
+				total: isAdditional ? 1 : -1,
+				inc: isAdditional ? 1 : 0,
+				dec: isAdditional ? 0 : 1,
+			}
+		}, toPuny(host));
+	}
+
+	@autobind
+	public async updateDrive(file: DriveFile, isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.totalFiles = isAdditional ? 1 : -1;
+		update.totalUsage = isAdditional ? file.size : -file.size;
+		if (isAdditional) {
+			update.incFiles = 1;
+			update.incUsage = file.size;
+		} else {
+			update.decFiles = 1;
+			update.decUsage = file.size;
+		}
+
+		await this.inc({
+			drive: update
+		}, file.userHost);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/network.ts b/packages/backend/src/services/chart/charts/classes/network.ts
new file mode 100644
index 0000000000..2ce75e0b34
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/network.ts
@@ -0,0 +1,45 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { name, schema } from '../schemas/network';
+
+type NetworkLog = SchemaType<typeof schema>;
+
+export default class NetworkChart extends Chart<NetworkLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: NetworkLog): DeepPartial<NetworkLog> {
+		return {};
+	}
+
+	@autobind
+	protected aggregate(logs: NetworkLog[]): NetworkLog {
+		return {
+			incomingRequests: logs.reduce((a, b) => a + b.incomingRequests, 0),
+			outgoingRequests: logs.reduce((a, b) => a + b.outgoingRequests, 0),
+			totalTime: logs.reduce((a, b) => a + b.totalTime, 0),
+			incomingBytes: logs.reduce((a, b) => a + b.incomingBytes, 0),
+			outgoingBytes: logs.reduce((a, b) => a + b.outgoingBytes, 0),
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<NetworkLog>> {
+		return {};
+	}
+
+	@autobind
+	public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
+		const inc: DeepPartial<NetworkLog> = {
+			incomingRequests: incomingRequests,
+			totalTime: time,
+			incomingBytes: incomingBytes,
+			outgoingBytes: outgoingBytes
+		};
+
+		await this.inc(inc);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/notes.ts b/packages/backend/src/services/chart/charts/classes/notes.ts
new file mode 100644
index 0000000000..0675d346d1
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/notes.ts
@@ -0,0 +1,97 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { Notes } from '@/models/index';
+import { Not, IsNull } from 'typeorm';
+import { Note } from '@/models/entities/note';
+import { name, schema } from '../schemas/notes';
+
+type NotesLog = SchemaType<typeof schema>;
+
+export default class NotesChart extends Chart<NotesLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: NotesLog): DeepPartial<NotesLog> {
+		return {
+			local: {
+				total: latest.local.total,
+			},
+			remote: {
+				total: latest.remote.total,
+			}
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: NotesLog[]): NotesLog {
+		return {
+			local: {
+				total: logs[0].local.total,
+				inc: logs.reduce((a, b) => a + b.local.inc, 0),
+				dec: logs.reduce((a, b) => a + b.local.dec, 0),
+				diffs: {
+					reply: logs.reduce((a, b) => a + b.local.diffs.reply, 0),
+					renote: logs.reduce((a, b) => a + b.local.diffs.renote, 0),
+					normal: logs.reduce((a, b) => a + b.local.diffs.normal, 0),
+				},
+			},
+			remote: {
+				total: logs[0].remote.total,
+				inc: logs.reduce((a, b) => a + b.remote.inc, 0),
+				dec: logs.reduce((a, b) => a + b.remote.dec, 0),
+				diffs: {
+					reply: logs.reduce((a, b) => a + b.remote.diffs.reply, 0),
+					renote: logs.reduce((a, b) => a + b.remote.diffs.renote, 0),
+					normal: logs.reduce((a, b) => a + b.remote.diffs.normal, 0),
+				},
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<NotesLog>> {
+		const [localCount, remoteCount] = await Promise.all([
+			Notes.count({ userHost: null }),
+			Notes.count({ userHost: Not(IsNull()) })
+		]);
+
+		return {
+			local: {
+				total: localCount,
+			},
+			remote: {
+				total: remoteCount,
+			}
+		};
+	}
+
+	@autobind
+	public async update(note: Note, isAdditional: boolean) {
+		const update: Obj = {
+			diffs: {}
+		};
+
+		update.total = isAdditional ? 1 : -1;
+
+		if (isAdditional) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		if (note.replyId != null) {
+			update.diffs.reply = isAdditional ? 1 : -1;
+		} else if (note.renoteId != null) {
+			update.diffs.renote = isAdditional ? 1 : -1;
+		} else {
+			update.diffs.normal = isAdditional ? 1 : -1;
+		}
+
+		await this.inc({
+			[note.userHost === null ? 'local' : 'remote']: update
+		});
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/per-user-drive.ts b/packages/backend/src/services/chart/charts/classes/per-user-drive.ts
new file mode 100644
index 0000000000..f28987191b
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/per-user-drive.ts
@@ -0,0 +1,64 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { DriveFiles } from '@/models/index';
+import { DriveFile } from '@/models/entities/drive-file';
+import { name, schema } from '../schemas/per-user-drive';
+
+type PerUserDriveLog = SchemaType<typeof schema>;
+
+export default class PerUserDriveChart extends Chart<PerUserDriveLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: PerUserDriveLog): DeepPartial<PerUserDriveLog> {
+		return {
+			totalCount: latest.totalCount,
+			totalSize: latest.totalSize,
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: PerUserDriveLog[]): PerUserDriveLog {
+		return {
+			totalCount: logs[0].totalCount,
+			totalSize: logs[0].totalSize,
+			incCount: logs.reduce((a, b) => a + b.incCount, 0),
+			incSize: logs.reduce((a, b) => a + b.incSize, 0),
+			decCount: logs.reduce((a, b) => a + b.decCount, 0),
+			decSize: logs.reduce((a, b) => a + b.decSize, 0),
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<PerUserDriveLog>> {
+		const [count, size] = await Promise.all([
+			DriveFiles.count({ userId: group }),
+			DriveFiles.calcDriveUsageOf(group)
+		]);
+
+		return {
+			totalCount: count,
+			totalSize: size,
+		};
+	}
+
+	@autobind
+	public async update(file: DriveFile, isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.totalCount = isAdditional ? 1 : -1;
+		update.totalSize = isAdditional ? file.size : -file.size;
+		if (isAdditional) {
+			update.incCount = 1;
+			update.incSize = file.size;
+		} else {
+			update.decCount = 1;
+			update.decSize = file.size;
+		}
+
+		await this.inc(update, file.userId);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/per-user-following.ts b/packages/backend/src/services/chart/charts/classes/per-user-following.ts
new file mode 100644
index 0000000000..08a9ad1d2b
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/per-user-following.ts
@@ -0,0 +1,121 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { Followings, Users } from '@/models/index';
+import { Not, IsNull } from 'typeorm';
+import { User } from '@/models/entities/user';
+import { name, schema } from '../schemas/per-user-following';
+
+type PerUserFollowingLog = SchemaType<typeof schema>;
+
+export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: PerUserFollowingLog): DeepPartial<PerUserFollowingLog> {
+		return {
+			local: {
+				followings: {
+					total: latest.local.followings.total,
+				},
+				followers: {
+					total: latest.local.followers.total,
+				}
+			},
+			remote: {
+				followings: {
+					total: latest.remote.followings.total,
+				},
+				followers: {
+					total: latest.remote.followers.total,
+				}
+			}
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: PerUserFollowingLog[]): PerUserFollowingLog {
+		return {
+			local: {
+				followings: {
+					total: logs[0].local.followings.total,
+					inc: logs.reduce((a, b) => a + b.local.followings.inc, 0),
+					dec: logs.reduce((a, b) => a + b.local.followings.dec, 0),
+				},
+				followers: {
+					total: logs[0].local.followers.total,
+					inc: logs.reduce((a, b) => a + b.local.followers.inc, 0),
+					dec: logs.reduce((a, b) => a + b.local.followers.dec, 0),
+				},
+			},
+			remote: {
+				followings: {
+					total: logs[0].remote.followings.total,
+					inc: logs.reduce((a, b) => a + b.remote.followings.inc, 0),
+					dec: logs.reduce((a, b) => a + b.remote.followings.dec, 0),
+				},
+				followers: {
+					total: logs[0].remote.followers.total,
+					inc: logs.reduce((a, b) => a + b.remote.followers.inc, 0),
+					dec: logs.reduce((a, b) => a + b.remote.followers.dec, 0),
+				},
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<PerUserFollowingLog>> {
+		const [
+			localFollowingsCount,
+			localFollowersCount,
+			remoteFollowingsCount,
+			remoteFollowersCount
+		] = await Promise.all([
+			Followings.count({ followerId: group, followeeHost: null }),
+			Followings.count({ followeeId: group, followerHost: null }),
+			Followings.count({ followerId: group, followeeHost: Not(IsNull()) }),
+			Followings.count({ followeeId: group, followerHost: Not(IsNull()) })
+		]);
+
+		return {
+			local: {
+				followings: {
+					total: localFollowingsCount,
+				},
+				followers: {
+					total: localFollowersCount,
+				}
+			},
+			remote: {
+				followings: {
+					total: remoteFollowingsCount,
+				},
+				followers: {
+					total: remoteFollowersCount,
+				}
+			}
+		};
+	}
+
+	@autobind
+	public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean) {
+		const update: Obj = {};
+
+		update.total = isFollow ? 1 : -1;
+
+		if (isFollow) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		this.inc({
+			[Users.isLocalUser(follower) ? 'local' : 'remote']: { followings: update }
+		}, follower.id);
+		this.inc({
+			[Users.isLocalUser(followee) ? 'local' : 'remote']: { followers: update }
+		}, followee.id);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/per-user-notes.ts b/packages/backend/src/services/chart/charts/classes/per-user-notes.ts
new file mode 100644
index 0000000000..0e808766f5
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/per-user-notes.ts
@@ -0,0 +1,72 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { User } from '@/models/entities/user';
+import { SchemaType } from '@/misc/schema';
+import { Notes } from '@/models/index';
+import { Note } from '@/models/entities/note';
+import { name, schema } from '../schemas/per-user-notes';
+
+type PerUserNotesLog = SchemaType<typeof schema>;
+
+export default class PerUserNotesChart extends Chart<PerUserNotesLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: PerUserNotesLog): DeepPartial<PerUserNotesLog> {
+		return {
+			total: latest.total,
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: PerUserNotesLog[]): PerUserNotesLog {
+		return {
+			total: logs[0].total,
+			inc: logs.reduce((a, b) => a + b.inc, 0),
+			dec: logs.reduce((a, b) => a + b.dec, 0),
+			diffs: {
+				reply: logs.reduce((a, b) => a + b.diffs.reply, 0),
+				renote: logs.reduce((a, b) => a + b.diffs.renote, 0),
+				normal: logs.reduce((a, b) => a + b.diffs.normal, 0),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<PerUserNotesLog>> {
+		const [count] = await Promise.all([
+			Notes.count({ userId: group }),
+		]);
+
+		return {
+			total: count,
+		};
+	}
+
+	@autobind
+	public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean) {
+		const update: Obj = {
+			diffs: {}
+		};
+
+		update.total = isAdditional ? 1 : -1;
+
+		if (isAdditional) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		if (note.replyId != null) {
+			update.diffs.reply = isAdditional ? 1 : -1;
+		} else if (note.renoteId != null) {
+			update.diffs.renote = isAdditional ? 1 : -1;
+		} else {
+			update.diffs.normal = isAdditional ? 1 : -1;
+		}
+
+		await this.inc(update, user.id);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/per-user-reactions.ts b/packages/backend/src/services/chart/charts/classes/per-user-reactions.ts
new file mode 100644
index 0000000000..e71bcb71c4
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/per-user-reactions.ts
@@ -0,0 +1,44 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { User } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { SchemaType } from '@/misc/schema';
+import { Users } from '@/models/index';
+import { name, schema } from '../schemas/per-user-reactions';
+
+type PerUserReactionsLog = SchemaType<typeof schema>;
+
+export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: PerUserReactionsLog): DeepPartial<PerUserReactionsLog> {
+		return {};
+	}
+
+	@autobind
+	protected aggregate(logs: PerUserReactionsLog[]): PerUserReactionsLog {
+		return {
+			local: {
+				count: logs.reduce((a, b) => a + b.local.count, 0),
+			},
+			remote: {
+				count: logs.reduce((a, b) => a + b.remote.count, 0),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<PerUserReactionsLog>> {
+		return {};
+	}
+
+	@autobind
+	public async update(user: { id: User['id'], host: User['host'] }, note: Note) {
+		this.inc({
+			[Users.isLocalUser(user) ? 'local' : 'remote']: { count: 1 }
+		}, note.userId);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/test-grouped.ts b/packages/backend/src/services/chart/charts/classes/test-grouped.ts
new file mode 100644
index 0000000000..84e6d5e33f
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/test-grouped.ts
@@ -0,0 +1,58 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { name, schema } from '../schemas/test-grouped';
+
+type TestGroupedLog = SchemaType<typeof schema>;
+
+export default class TestGroupedChart extends Chart<TestGroupedLog> {
+	private total = {} as Record<string, number>;
+
+	constructor() {
+		super(name, schema, true);
+	}
+
+	@autobind
+	protected genNewLog(latest: TestGroupedLog): DeepPartial<TestGroupedLog> {
+		return {
+			foo: {
+				total: latest.foo.total,
+			},
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: TestGroupedLog[]): TestGroupedLog {
+		return {
+			foo: {
+				total: logs[0].foo.total,
+				inc: logs.reduce((a, b) => a + b.foo.inc, 0),
+				dec: logs.reduce((a, b) => a + b.foo.dec, 0),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(group: string): Promise<DeepPartial<TestGroupedLog>> {
+		return {
+			foo: {
+				total: this.total[group],
+			},
+		};
+	}
+
+	@autobind
+	public async increment(group: string) {
+		if (this.total[group] == null) this.total[group] = 0;
+
+		const update: Obj = {};
+
+		update.total = 1;
+		update.inc = 1;
+		this.total[group]++;
+
+		await this.inc({
+			foo: update
+		}, group);
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/test-unique.ts b/packages/backend/src/services/chart/charts/classes/test-unique.ts
new file mode 100644
index 0000000000..559fda13c9
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/test-unique.ts
@@ -0,0 +1,36 @@
+import autobind from 'autobind-decorator';
+import Chart, { DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { name, schema } from '../schemas/test-unique';
+
+type TestUniqueLog = SchemaType<typeof schema>;
+
+export default class TestUniqueChart extends Chart<TestUniqueLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: TestUniqueLog): DeepPartial<TestUniqueLog> {
+		return {};
+	}
+
+	@autobind
+	protected aggregate(logs: TestUniqueLog[]): TestUniqueLog {
+		return {
+			foo: logs.reduce((a, b) => a.concat(b.foo), [] as TestUniqueLog['foo']),
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<TestUniqueLog>> {
+		return {};
+	}
+
+	@autobind
+	public async uniqueIncrement(key: string) {
+		await this.inc({
+			foo: [key]
+		});
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/test.ts b/packages/backend/src/services/chart/charts/classes/test.ts
new file mode 100644
index 0000000000..a91d5e1895
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/test.ts
@@ -0,0 +1,69 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { name, schema } from '../schemas/test';
+
+type TestLog = SchemaType<typeof schema>;
+
+export default class TestChart extends Chart<TestLog> {
+	public total = 0; // publicにするのはテストのため
+
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: TestLog): DeepPartial<TestLog> {
+		return {
+			foo: {
+				total: latest.foo.total,
+			},
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: TestLog[]): TestLog {
+		return {
+			foo: {
+				total: logs[0].foo.total,
+				inc: logs.reduce((a, b) => a + b.foo.inc, 0),
+				dec: logs.reduce((a, b) => a + b.foo.dec, 0),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<TestLog>> {
+		return {
+			foo: {
+				total: this.total,
+			},
+		};
+	}
+
+	@autobind
+	public async increment() {
+		const update: Obj = {};
+
+		update.total = 1;
+		update.inc = 1;
+		this.total++;
+
+		await this.inc({
+			foo: update
+		});
+	}
+
+	@autobind
+	public async decrement() {
+		const update: Obj = {};
+
+		update.total = -1;
+		update.dec = 1;
+		this.total--;
+
+		await this.inc({
+			foo: update
+		});
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/classes/users.ts b/packages/backend/src/services/chart/charts/classes/users.ts
new file mode 100644
index 0000000000..89b480ef77
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/classes/users.ts
@@ -0,0 +1,76 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj, DeepPartial } from '../../core';
+import { SchemaType } from '@/misc/schema';
+import { Users } from '@/models/index';
+import { Not, IsNull } from 'typeorm';
+import { User } from '@/models/entities/user';
+import { name, schema } from '../schemas/users';
+
+type UsersLog = SchemaType<typeof schema>;
+
+export default class UsersChart extends Chart<UsersLog> {
+	constructor() {
+		super(name, schema);
+	}
+
+	@autobind
+	protected genNewLog(latest: UsersLog): DeepPartial<UsersLog> {
+		return {
+			local: {
+				total: latest.local.total,
+			},
+			remote: {
+				total: latest.remote.total,
+			}
+		};
+	}
+
+	@autobind
+	protected aggregate(logs: UsersLog[]): UsersLog {
+		return {
+			local: {
+				total: logs[0].local.total,
+				inc: logs.reduce((a, b) => a + b.local.inc, 0),
+				dec: logs.reduce((a, b) => a + b.local.dec, 0),
+			},
+			remote: {
+				total: logs[0].remote.total,
+				inc: logs.reduce((a, b) => a + b.remote.inc, 0),
+				dec: logs.reduce((a, b) => a + b.remote.dec, 0),
+			},
+		};
+	}
+
+	@autobind
+	protected async fetchActual(): Promise<DeepPartial<UsersLog>> {
+		const [localCount, remoteCount] = await Promise.all([
+			Users.count({ host: null }),
+			Users.count({ host: Not(IsNull()) })
+		]);
+
+		return {
+			local: {
+				total: localCount,
+			},
+			remote: {
+				total: remoteCount,
+			}
+		};
+	}
+
+	@autobind
+	public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean) {
+		const update: Obj = {};
+
+		update.total = isAdditional ? 1 : -1;
+		if (isAdditional) {
+			update.inc = 1;
+		} else {
+			update.dec = 1;
+		}
+
+		await this.inc({
+			[Users.isLocalUser(user) ? 'local' : 'remote']: update
+		});
+	}
+}
diff --git a/packages/backend/src/services/chart/charts/schemas/active-users.ts b/packages/backend/src/services/chart/charts/schemas/active-users.ts
new file mode 100644
index 0000000000..1d65f280b0
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/active-users.ts
@@ -0,0 +1,35 @@
+export const logSchema = {
+	/**
+	 * アクティブユーザー
+	 */
+	users: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		}
+	},
+};
+
+/**
+ * アクティブユーザーに関するチャート
+ */
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		local: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'activeUsers';
diff --git a/packages/backend/src/services/chart/charts/schemas/drive.ts b/packages/backend/src/services/chart/charts/schemas/drive.ts
new file mode 100644
index 0000000000..133b47846a
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/drive.ts
@@ -0,0 +1,68 @@
+const logSchema = {
+	/**
+	 * 集計期間時点での、全ドライブファイル数
+	 */
+	totalCount: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	/**
+	 * 集計期間時点での、全ドライブファイルの合計サイズ
+	 */
+	totalSize: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	/**
+	 * 増加したドライブファイル数
+	 */
+	incCount: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	/**
+	 * 増加したドライブ使用量
+	 */
+	incSize: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	/**
+	 * 減少したドライブファイル数
+	 */
+	decCount: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	/**
+	 * 減少したドライブ使用量
+	 */
+	decSize: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+};
+
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		local: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'drive';
diff --git a/packages/backend/src/services/chart/charts/schemas/federation.ts b/packages/backend/src/services/chart/charts/schemas/federation.ts
new file mode 100644
index 0000000000..dca4587cac
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/federation.ts
@@ -0,0 +1,29 @@
+/**
+ * フェデレーションに関するチャート
+ */
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		instance: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				total: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				inc: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				dec: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		}
+	}
+};
+
+export const name = 'federation';
diff --git a/packages/backend/src/services/chart/charts/schemas/hashtag.ts b/packages/backend/src/services/chart/charts/schemas/hashtag.ts
new file mode 100644
index 0000000000..4e7c542bbc
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/hashtag.ts
@@ -0,0 +1,35 @@
+export const logSchema = {
+	/**
+	 * 投稿したユーザー
+	 */
+	users: {
+		type: 'array' as const,
+		optional: false as const, nullable: false as const,
+		items: {
+			type: 'string' as const,
+			optional: false as const, nullable: false as const,
+		}
+	},
+};
+
+/**
+ * ハッシュタグに関するチャート
+ */
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		local: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'hashtag';
diff --git a/packages/backend/src/services/chart/charts/schemas/instance.ts b/packages/backend/src/services/chart/charts/schemas/instance.ts
new file mode 100644
index 0000000000..785d6ae7ce
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/instance.ts
@@ -0,0 +1,157 @@
+/**
+ * インスタンスごとのチャート
+ */
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		requests: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				failed: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				succeeded: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				received: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		},
+
+		notes: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				total: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				inc: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				dec: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+
+				diffs: {
+					type: 'object' as const,
+					optional: false as const, nullable: false as const,
+					properties: {
+						normal: {
+							type: 'number' as const,
+							optional: false as const, nullable: false as const,
+						},
+
+						reply: {
+							type: 'number' as const,
+							optional: false as const, nullable: false as const,
+						},
+
+						renote: {
+							type: 'number' as const,
+							optional: false as const, nullable: false as const,
+						},
+					}
+				},
+			}
+		},
+
+		users: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				total: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				inc: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				dec: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		},
+
+		following: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				total: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				inc: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				dec: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		},
+
+		followers: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				total: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				inc: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				dec: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		},
+
+		drive: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				totalFiles: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				totalUsage: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				incFiles: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				incUsage: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				decFiles: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+				decUsage: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		},
+	}
+};
+
+export const name = 'instance';
diff --git a/packages/backend/src/services/chart/charts/schemas/network.ts b/packages/backend/src/services/chart/charts/schemas/network.ts
new file mode 100644
index 0000000000..49a364debc
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/network.ts
@@ -0,0 +1,31 @@
+/**
+ * ネットワークに関するチャート
+ */
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		incomingRequests: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		outgoingRequests: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		totalTime: { // TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		incomingBytes: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+		outgoingBytes: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+	}
+};
+
+export const name = 'network';
diff --git a/packages/backend/src/services/chart/charts/schemas/notes.ts b/packages/backend/src/services/chart/charts/schemas/notes.ts
new file mode 100644
index 0000000000..2b5105348c
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/notes.ts
@@ -0,0 +1,56 @@
+const logSchema = {
+	total: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	inc: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	dec: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	diffs: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			normal: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+
+			reply: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+
+			renote: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+		}
+	},
+};
+
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		local: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'notes';
diff --git a/packages/backend/src/services/chart/charts/schemas/per-user-drive.ts b/packages/backend/src/services/chart/charts/schemas/per-user-drive.ts
new file mode 100644
index 0000000000..856f1e0439
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/per-user-drive.ts
@@ -0,0 +1,55 @@
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		/**
+		 * 集計期間時点での、全ドライブファイル数
+		 */
+		totalCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+
+		/**
+		 * 集計期間時点での、全ドライブファイルの合計サイズ
+		 */
+		totalSize: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+
+		/**
+		 * 増加したドライブファイル数
+		 */
+		incCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+
+		/**
+		 * 増加したドライブ使用量
+		 */
+		incSize: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+
+		/**
+		 * 減少したドライブファイル数
+		 */
+		decCount: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+
+		/**
+		 * 減少したドライブ使用量
+		 */
+		decSize: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+	}
+};
+
+export const name = 'perUserDrive';
diff --git a/packages/backend/src/services/chart/charts/schemas/per-user-following.ts b/packages/backend/src/services/chart/charts/schemas/per-user-following.ts
new file mode 100644
index 0000000000..eaf74aaf77
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/per-user-following.ts
@@ -0,0 +1,86 @@
+export const logSchema = {
+	/**
+	 * フォローしている
+	 */
+	followings: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			/**
+			 * フォローしている合計
+			 */
+			total: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+
+			/**
+			 * フォローした数
+			 */
+			inc: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+
+			/**
+			 * フォロー解除した数
+			 */
+			dec: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+		}
+	},
+
+	/**
+	 * フォローされている
+	 */
+	followers: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		properties: {
+			/**
+			 * フォローされている合計
+			 */
+			total: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+
+			/**
+			 * フォローされた数
+			 */
+			inc: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+
+			/**
+			 * フォロー解除された数
+			 */
+			dec: {
+				type: 'number' as const,
+				optional: false as const, nullable: false as const,
+			},
+		}
+	},
+};
+
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		local: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'perUserFollowing';
diff --git a/packages/backend/src/services/chart/charts/schemas/per-user-notes.ts b/packages/backend/src/services/chart/charts/schemas/per-user-notes.ts
new file mode 100644
index 0000000000..72b3ff0210
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/per-user-notes.ts
@@ -0,0 +1,43 @@
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		total: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+
+		inc: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+
+		dec: {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		},
+
+		diffs: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				normal: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+
+				reply: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+
+				renote: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		},
+	}
+};
+
+export const name = 'perUserNotes';
diff --git a/packages/backend/src/services/chart/charts/schemas/per-user-reactions.ts b/packages/backend/src/services/chart/charts/schemas/per-user-reactions.ts
new file mode 100644
index 0000000000..2a8520db37
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/per-user-reactions.ts
@@ -0,0 +1,31 @@
+export const logSchema = {
+	/**
+	 * フォローしている合計
+	 */
+	count: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+};
+
+/**
+ * ユーザーごとのリアクションに関するチャート
+ */
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		local: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'perUserReaction';
diff --git a/packages/backend/src/services/chart/charts/schemas/test-grouped.ts b/packages/backend/src/services/chart/charts/schemas/test-grouped.ts
new file mode 100644
index 0000000000..f8c8250e79
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/test-grouped.ts
@@ -0,0 +1,28 @@
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		foo: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				total: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+
+				inc: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+
+				dec: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		}
+	}
+};
+
+export const name = 'testGrouped';
diff --git a/packages/backend/src/services/chart/charts/schemas/test-unique.ts b/packages/backend/src/services/chart/charts/schemas/test-unique.ts
new file mode 100644
index 0000000000..51280400ac
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/test-unique.ts
@@ -0,0 +1,16 @@
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		foo: {
+			type: 'array' as const,
+			optional: false as const, nullable: false as const,
+			items: {
+				type: 'string' as const,
+				optional: false as const, nullable: false as const,
+			}
+		},
+	}
+};
+
+export const name = 'testUnique';
diff --git a/packages/backend/src/services/chart/charts/schemas/test.ts b/packages/backend/src/services/chart/charts/schemas/test.ts
new file mode 100644
index 0000000000..4b48d4d417
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/test.ts
@@ -0,0 +1,28 @@
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		foo: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: {
+				total: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+
+				inc: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+
+				dec: {
+					type: 'number' as const,
+					optional: false as const, nullable: false as const,
+				},
+			}
+		}
+	}
+};
+
+export const name = 'test';
diff --git a/packages/backend/src/services/chart/charts/schemas/users.ts b/packages/backend/src/services/chart/charts/schemas/users.ts
new file mode 100644
index 0000000000..2bf9d3c50f
--- /dev/null
+++ b/packages/backend/src/services/chart/charts/schemas/users.ts
@@ -0,0 +1,44 @@
+const logSchema = {
+	/**
+	 * 集計期間時点での、全ユーザー数
+	 */
+	total: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	/**
+	 * 増加したユーザー数
+	 */
+	inc: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+
+	/**
+	 * 減少したユーザー数
+	 */
+	dec: {
+		type: 'number' as const,
+		optional: false as const, nullable: false as const,
+	},
+};
+
+export const schema = {
+	type: 'object' as const,
+	optional: false as const, nullable: false as const,
+	properties: {
+		local: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+		remote: {
+			type: 'object' as const,
+			optional: false as const, nullable: false as const,
+			properties: logSchema
+		},
+	}
+};
+
+export const name = 'users';
diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts
new file mode 100644
index 0000000000..c0d3280c2b
--- /dev/null
+++ b/packages/backend/src/services/chart/core.ts
@@ -0,0 +1,563 @@
+/**
+ * チャートエンジン
+ *
+ * Tests located in test/chart
+ */
+
+import * as nestedProperty from 'nested-property';
+import autobind from 'autobind-decorator';
+import Logger from '../logger';
+import { SimpleSchema } from '@/misc/simple-schema';
+import { EntitySchema, getRepository, Repository, LessThan, Between } from 'typeorm';
+import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time';
+import { getChartInsertLock } from '@/misc/app-lock';
+
+const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test');
+
+export type Obj = { [key: string]: any };
+
+export type DeepPartial<T> = {
+	[P in keyof T]?: DeepPartial<T[P]>;
+};
+
+type ArrayValue<T> = {
+	[P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>;
+};
+
+type Log = {
+	id: number;
+
+	/**
+	 * 集計のグループ
+	 */
+	group: string | null;
+
+	/**
+	 * 集計日時のUnixタイムスタンプ(秒)
+	 */
+	date: number;
+};
+
+const camelToSnake = (str: string) => {
+	return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase());
+};
+
+const removeDuplicates = (array: any[]) => Array.from(new Set(array));
+
+/**
+ * 様々なチャートの管理を司るクラス
+ */
+export default abstract class Chart<T extends Record<string, any>> {
+	private static readonly columnPrefix = '___';
+	private static readonly columnDot = '_';
+
+	private name: string;
+	private buffer: {
+		diff: DeepPartial<T>;
+		group: string | null;
+	}[] = [];
+	public schema: SimpleSchema;
+	protected repository: Repository<Log>;
+
+	protected abstract genNewLog(latest: T): DeepPartial<T>;
+
+	/**
+	 * @param logs 日時が新しい方が先頭
+	 */
+	protected abstract aggregate(logs: T[]): T;
+
+	protected abstract fetchActual(group: string | null): Promise<DeepPartial<T>>;
+
+	@autobind
+	private static convertSchemaToFlatColumnDefinitions(schema: SimpleSchema) {
+		const columns = {} as any;
+		const flatColumns = (x: Obj, path?: string) => {
+			for (const [k, v] of Object.entries(x)) {
+				const p = path ? `${path}${this.columnDot}${k}` : k;
+				if (v.type === 'object') {
+					flatColumns(v.properties, p);
+				} else if (v.type === 'number') {
+					columns[this.columnPrefix + p] = {
+						type: 'bigint',
+					};
+				} else if (v.type === 'array' && v.items.type === 'string') {
+					columns[this.columnPrefix + p] = {
+						type: 'varchar',
+						array: true,
+					};
+				}
+			}
+		};
+		flatColumns(schema.properties!);
+		return columns;
+	}
+
+	@autobind
+	private static convertFlattenColumnsToObject(x: Record<string, any>): Record<string, any> {
+		const obj = {} as any;
+		for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) {
+			// now k is ___x_y_z
+			const path = k.substr(Chart.columnPrefix.length).split(Chart.columnDot).join('.');
+			nestedProperty.set(obj, path, x[k]);
+		}
+		return obj;
+	}
+
+	@autobind
+	private static convertObjectToFlattenColumns(x: Record<string, any>) {
+		const columns = {} as Record<string, number | unknown[]>;
+		const flatten = (x: Obj, path?: string) => {
+			for (const [k, v] of Object.entries(x)) {
+				const p = path ? `${path}${this.columnDot}${k}` : k;
+				if (typeof v === 'object' && !Array.isArray(v)) {
+					flatten(v, p);
+				} else {
+					columns[this.columnPrefix + p] = v;
+				}
+			}
+		};
+		flatten(x);
+		return columns;
+	}
+
+	@autobind
+	private static countUniqueFields(x: Record<string, any>) {
+		const exec = (x: Obj) => {
+			const res = {} as Record<string, any>;
+			for (const [k, v] of Object.entries(x)) {
+				if (typeof v === 'object' && !Array.isArray(v)) {
+					res[k] = exec(v);
+				} else if (Array.isArray(v)) {
+					res[k] = Array.from(new Set(v)).length;
+				} else {
+					res[k] = v;
+				}
+			}
+			return res;
+		};
+		return exec(x);
+	}
+
+	@autobind
+	private static convertQuery(diff: Record<string, number | unknown[]>) {
+		const query: Record<string, Function> = {};
+
+		for (const [k, v] of Object.entries(diff)) {
+			if (typeof v === 'number') {
+				if (v > 0) query[k] = () => `"${k}" + ${v}`;
+				if (v < 0) query[k] = () => `"${k}" - ${Math.abs(v)}`;
+			} else if (Array.isArray(v)) {
+				// TODO: item が文字列以外の場合も対応
+				// TODO: item をSQLエスケープ
+				const items = v.map(item => `"${item}"`).join(',');
+				query[k] = () => `array_cat("${k}", '{${items}}'::varchar[])`;
+			}
+		}
+
+		return query;
+	}
+
+	@autobind
+	private static dateToTimestamp(x: Date): Log['date'] {
+		return Math.floor(x.getTime() / 1000);
+	}
+
+	@autobind
+	private static parseDate(date: Date): [number, number, number, number, number, number, number] {
+		const y = date.getUTCFullYear();
+		const m = date.getUTCMonth();
+		const d = date.getUTCDate();
+		const h = date.getUTCHours();
+		const _m = date.getUTCMinutes();
+		const _s = date.getUTCSeconds();
+		const _ms = date.getUTCMilliseconds();
+
+		return [y, m, d, h, _m, _s, _ms];
+	}
+
+	@autobind
+	private static getCurrentDate() {
+		return Chart.parseDate(new Date());
+	}
+
+	@autobind
+	public static schemaToEntity(name: string, schema: SimpleSchema): EntitySchema {
+		return new EntitySchema({
+			name: `__chart__${camelToSnake(name)}`,
+			columns: {
+				id: {
+					type: 'integer',
+					primary: true,
+					generated: true
+				},
+				date: {
+					type: 'integer',
+				},
+				group: {
+					type: 'varchar',
+					length: 128,
+					nullable: true
+				},
+				...Chart.convertSchemaToFlatColumnDefinitions(schema)
+			},
+			indices: [{
+				columns: ['date', 'group'],
+				unique: true,
+			}, { // groupにnullが含まれると↑のuniqueは機能しないので↓の部分インデックスでカバー
+				columns: ['date'],
+				unique: true,
+				where: '"group" IS NULL'
+			}]
+		});
+	}
+
+	constructor(name: string, schema: SimpleSchema, grouped = false) {
+		this.name = name;
+		this.schema = schema;
+		const entity = Chart.schemaToEntity(name, schema);
+
+		const keys = ['date'];
+		if (grouped) keys.push('group');
+
+		entity.options.uniques = [{
+			columns: keys
+		}];
+
+		this.repository = getRepository<Log>(entity);
+	}
+
+	@autobind
+	private getNewLog(latest: T | null): T {
+		const log = latest ? this.genNewLog(latest) : {};
+		const flatColumns = (x: Obj, path?: string) => {
+			for (const [k, v] of Object.entries(x)) {
+				const p = path ? `${path}.${k}` : k;
+				if (v.type === 'object') {
+					flatColumns(v.properties, p);
+				} else {
+					if (nestedProperty.get(log, p) == null) {
+						const emptyValue = v.type === 'number' ? 0 : [];
+						nestedProperty.set(log, p, emptyValue);
+					}
+				}
+			}
+		};
+		flatColumns(this.schema.properties!);
+		return log as T;
+	}
+
+	@autobind
+	private getLatestLog(group: string | null = null): Promise<Log | null> {
+		return this.repository.findOne({
+			group: group,
+		}, {
+			order: {
+				date: -1
+			}
+		}).then(x => x || null);
+	}
+
+	@autobind
+	private async getCurrentLog(group: string | null = null): Promise<Log> {
+		const [y, m, d, h] = Chart.getCurrentDate();
+
+		const current = dateUTC([y, m, d, h]);
+
+		// 現在(=今のHour)のログ
+		const currentLog = await this.repository.findOne({
+			date: Chart.dateToTimestamp(current),
+			...(group ? { group: group } : {})
+		});
+
+		// ログがあればそれを返して終了
+		if (currentLog != null) {
+			return currentLog;
+		}
+
+		let log: Log;
+		let data: T;
+
+		// 集計期間が変わってから、初めてのチャート更新なら
+		// 最も最近のログを持ってくる
+		// * 例えば集計期間が「日」である場合で考えると、
+		// * 昨日何もチャートを更新するような出来事がなかった場合は、
+		// * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
+		// * 「昨日の」と決め打ちせずに「もっとも最近の」とします
+		const latest = await this.getLatestLog(group);
+
+		if (latest != null) {
+			const obj = Chart.convertFlattenColumnsToObject(latest) as T;
+
+			// 空ログデータを作成
+			data = this.getNewLog(obj);
+		} else {
+			// ログが存在しなかったら
+			// (Misskeyインスタンスを建てて初めてのチャート更新時など)
+
+			// 初期ログデータを作成
+			data = this.getNewLog(null);
+
+			logger.info(`${this.name + (group ? `:${group}` : '')}: Initial commit created`);
+		}
+
+		const date = Chart.dateToTimestamp(current);
+		const lockKey = `${this.name}:${date}:${group}`;
+
+		const unlock = await getChartInsertLock(lockKey);
+		try {
+			// ロック内でもう1回チェックする
+			const currentLog = await this.repository.findOne({
+				date: date,
+				...(group ? { group: group } : {})
+			});
+
+			// ログがあればそれを返して終了
+			if (currentLog != null) return currentLog;
+
+			// 新規ログ挿入
+			log = await this.repository.insert({
+				group: group,
+				date: date,
+				...Chart.convertObjectToFlattenColumns(data)
+			}).then(x => this.repository.findOneOrFail(x.identifiers[0]));
+
+			logger.info(`${this.name + (group ? `:${group}` : '')}: New commit created`);
+
+			return log;
+		} finally {
+			unlock();
+		}
+	}
+
+	@autobind
+	protected commit(diff: DeepPartial<T>, group: string | null = null): void {
+		this.buffer.push({
+			diff, group,
+		});
+	}
+
+	@autobind
+	public async save() {
+		if (this.buffer.length === 0) {
+			logger.info(`${this.name}: Write skipped`);
+			return;
+		}
+
+		// TODO: 前の時間のログがbufferにあった場合のハンドリング
+		// 例えば、save が20分ごとに行われるとして、前回行われたのは 01:50 だったとする。
+		// 次に save が行われるのは 02:10 ということになるが、もし 01:55 に新規ログが buffer に追加されたとすると、
+		// そのログは本来は 01:00~ のログとしてDBに保存されて欲しいのに、02:00~ のログ扱いになってしまう。
+		// これを回避するための実装は複雑になりそうなため、一旦保留。
+
+		const update = async (log: Log) => {
+			const finalDiffs = {} as Record<string, number | unknown[]>;
+
+			for (const diff of this.buffer.filter(q => q.group === log.group).map(q => q.diff)) {
+				const columns = Chart.convertObjectToFlattenColumns(diff);
+
+				for (const [k, v] of Object.entries(columns)) {
+					if (finalDiffs[k] == null) {
+						finalDiffs[k] = v;
+					} else {
+						if (typeof finalDiffs[k] === 'number') {
+							(finalDiffs[k] as number) += v as number;
+						} else {
+							(finalDiffs[k] as unknown[]) = (finalDiffs[k] as unknown[]).concat(v);
+						}
+					}
+				}
+			}
+
+			const query = Chart.convertQuery(finalDiffs);
+
+			// ログ更新
+			await this.repository.createQueryBuilder()
+				.update()
+				.set(query)
+				.where('id = :id', { id: log.id })
+				.execute();
+
+			logger.info(`${this.name + (log.group ? `:${log.group}` : '')}: Updated`);
+
+			// TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする
+			this.buffer = this.buffer.filter(q => q.group !== log.group);
+		};
+
+		const groups = removeDuplicates(this.buffer.map(log => log.group));
+
+		await Promise.all(groups.map(group => this.getCurrentLog(group).then(log => update(log))));
+	}
+
+	@autobind
+	public async resync(group: string | null = null): Promise<any> {
+		const data = await this.fetchActual(group);
+
+		const update = async (log: Log) => {
+			await this.repository.createQueryBuilder()
+				.update()
+				.set(Chart.convertObjectToFlattenColumns(data))
+				.where('id = :id', { id: log.id })
+				.execute();
+		};
+
+		return this.getCurrentLog(group).then(log => update(log));
+	}
+
+	@autobind
+	protected async inc(inc: DeepPartial<T>, group: string | null = null): Promise<void> {
+		await this.commit(inc, group);
+	}
+
+	@autobind
+	public async getChart(span: 'hour' | 'day', amount: number, cursor: Date | null, group: string | null = null): Promise<ArrayValue<T>> {
+		const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
+		const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
+
+		const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
+
+		const gt =
+			span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
+			span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
+			null as never;
+
+		// ログ取得
+		let logs = await this.repository.find({
+			where: {
+				group: group,
+				date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt))
+			},
+			order: {
+				date: -1
+			},
+		});
+
+		// 要求された範囲にログがひとつもなかったら
+		if (logs.length === 0) {
+			// もっとも新しいログを持ってくる
+			// (すくなくともひとつログが無いと隙間埋めできないため)
+			const recentLog = await this.repository.findOne({
+				group: group,
+			}, {
+				order: {
+					date: -1
+				},
+			});
+
+			if (recentLog) {
+				logs = [recentLog];
+			}
+
+		// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
+		} else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) {
+			// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
+			// (隙間埋めできないため)
+			const outdatedLog = await this.repository.findOne({
+				group: group,
+				date: LessThan(Chart.dateToTimestamp(gt))
+			}, {
+				order: {
+					date: -1
+				},
+			});
+
+			if (outdatedLog) {
+				logs.push(outdatedLog);
+			}
+		}
+
+		const chart: T[] = [];
+
+		if (span === 'hour') {
+			for (let i = (amount - 1); i >= 0; i--) {
+				const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour');
+
+				const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
+
+				if (log) {
+					const data = Chart.convertFlattenColumnsToObject(log);
+					chart.unshift(Chart.countUniqueFields(data) as T);
+				} else {
+					// 隙間埋め
+					const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
+					const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
+					chart.unshift(Chart.countUniqueFields(this.getNewLog(data)) as T);
+				}
+			}
+		} else if (span === 'day') {
+			const logsForEachDays: T[][] = [];
+			let currentDay = -1;
+			let currentDayIndex = -1;
+			for (let i = ((amount - 1) * 24) + h; i >= 0; i--) {
+				const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour');
+				const _currentDay = Chart.parseDate(current)[2];
+				if (currentDay != _currentDay) currentDayIndex++;
+				currentDay = _currentDay;
+
+				const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
+
+				if (log) {
+					if (logsForEachDays[currentDayIndex]) {
+						logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log) as T);
+					} else {
+						logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log) as T];
+					}
+				} else {
+					// 隙間埋め
+					const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
+					const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
+					const newLog = this.getNewLog(data);
+					if (logsForEachDays[currentDayIndex]) {
+						logsForEachDays[currentDayIndex].unshift(newLog);
+					} else {
+						logsForEachDays[currentDayIndex] = [newLog];
+					}
+				}
+			}
+
+			for (const logs of logsForEachDays) {
+				const log = this.aggregate(logs);
+				chart.unshift(Chart.countUniqueFields(log) as T);
+			}
+		}
+
+		const res: ArrayValue<T> = {} as any;
+
+		/**
+		 * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
+		 * を
+		 * { foo: [1, 2, 3], bar: [5, 6, 7] }
+		 * にする
+		 */
+		const compact = (x: Obj, path?: string) => {
+			for (const [k, v] of Object.entries(x)) {
+				const p = path ? `${path}.${k}` : k;
+				if (typeof v === 'object' && !Array.isArray(v)) {
+					compact(v, p);
+				} else {
+					const values = chart.map(s => nestedProperty.get(s, p));
+					nestedProperty.set(res, p, values);
+				}
+			}
+		};
+
+		compact(chart[0]);
+
+		return res;
+	}
+}
+
+export function convertLog(logSchema: SimpleSchema): SimpleSchema {
+	const v: SimpleSchema = JSON.parse(JSON.stringify(logSchema)); // copy
+	if (v.type === 'number') {
+		v.type = 'array';
+		v.items = {
+			type: 'number' as const,
+			optional: false as const, nullable: false as const,
+		};
+	} else if (v.type === 'object') {
+		for (const k of Object.keys(v.properties!)) {
+			v.properties![k] = convertLog(v.properties![k]);
+		}
+	}
+	return v;
+}
diff --git a/packages/backend/src/services/chart/entities.ts b/packages/backend/src/services/chart/entities.ts
new file mode 100644
index 0000000000..23a97607eb
--- /dev/null
+++ b/packages/backend/src/services/chart/entities.ts
@@ -0,0 +1,15 @@
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import Chart from './core';
+
+//const _filename = fileURLToPath(import.meta.url);
+const _filename = __filename;
+const _dirname = dirname(_filename);
+
+export const entities = Object.values(require('require-all')({
+	dirname: _dirname + '/charts/schemas',
+	filter: /^.+\.[jt]s$/,
+	resolve: (x: any) => {
+		return Chart.schemaToEntity(x.name, x.schema);
+	}
+}));
diff --git a/packages/backend/src/services/chart/index.ts b/packages/backend/src/services/chart/index.ts
new file mode 100644
index 0000000000..61eb431ea3
--- /dev/null
+++ b/packages/backend/src/services/chart/index.ts
@@ -0,0 +1,50 @@
+import FederationChart from './charts/classes/federation';
+import NotesChart from './charts/classes/notes';
+import UsersChart from './charts/classes/users';
+import NetworkChart from './charts/classes/network';
+import ActiveUsersChart from './charts/classes/active-users';
+import InstanceChart from './charts/classes/instance';
+import PerUserNotesChart from './charts/classes/per-user-notes';
+import DriveChart from './charts/classes/drive';
+import PerUserReactionsChart from './charts/classes/per-user-reactions';
+import HashtagChart from './charts/classes/hashtag';
+import PerUserFollowingChart from './charts/classes/per-user-following';
+import PerUserDriveChart from './charts/classes/per-user-drive';
+import { beforeShutdown } from '@/misc/before-shutdown';
+
+export const federationChart = new FederationChart();
+export const notesChart = new NotesChart();
+export const usersChart = new UsersChart();
+export const networkChart = new NetworkChart();
+export const activeUsersChart = new ActiveUsersChart();
+export const instanceChart = new InstanceChart();
+export const perUserNotesChart = new PerUserNotesChart();
+export const driveChart = new DriveChart();
+export const perUserReactionsChart = new PerUserReactionsChart();
+export const hashtagChart = new HashtagChart();
+export const perUserFollowingChart = new PerUserFollowingChart();
+export const perUserDriveChart = new PerUserDriveChart();
+
+const charts = [
+	federationChart,
+	notesChart,
+	usersChart,
+	networkChart,
+	activeUsersChart,
+	instanceChart,
+	perUserNotesChart,
+	driveChart,
+	perUserReactionsChart,
+	hashtagChart,
+	perUserFollowingChart,
+	perUserDriveChart,
+];
+
+// 20分おきにメモリ情報をDBに書き込み
+setInterval(() => {
+	for (const chart of charts) {
+		chart.save();
+	}
+}, 1000 * 60 * 20);
+
+beforeShutdown(() => Promise.all(charts.map(chart => chart.save())));
diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts
new file mode 100644
index 0000000000..5398d486c0
--- /dev/null
+++ b/packages/backend/src/services/create-notification.ts
@@ -0,0 +1,61 @@
+import { publishMainStream } from '@/services/stream';
+import pushSw from './push-notification';
+import { Notifications, Mutings, UserProfiles, Users } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { User } from '@/models/entities/user';
+import { Notification } from '@/models/entities/notification';
+import { sendEmailNotification } from './send-email-notification';
+
+export async function createNotification(
+	notifieeId: User['id'],
+	type: Notification['type'],
+	data: Partial<Notification>
+) {
+	if (data.notifierId && (notifieeId === data.notifierId)) {
+		return null;
+	}
+
+	const profile = await UserProfiles.findOne({ userId: notifieeId });
+
+	const isMuted = profile?.mutingNotificationTypes.includes(type);
+
+	// Create notification
+	const notification = await Notifications.save({
+		id: genId(),
+		createdAt: new Date(),
+		notifieeId: notifieeId,
+		type: type,
+		// 相手がこの通知をミュートしているようなら、既読を予めつけておく
+		isRead: isMuted,
+		...data
+	} as Partial<Notification>);
+
+	const packed = await Notifications.pack(notification, {});
+
+	// Publish notification event
+	publishMainStream(notifieeId, 'notification', packed);
+
+	// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
+	setTimeout(async () => {
+		const fresh = await Notifications.findOne(notification.id);
+		if (fresh == null) return; // 既に削除されているかもしれない
+		if (fresh.isRead) return;
+
+		//#region ただしミュートしているユーザーからの通知なら無視
+		const mutings = await Mutings.find({
+			muterId: notifieeId
+		});
+		if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
+			return;
+		}
+		//#endregion
+
+		publishMainStream(notifieeId, 'unreadNotification', packed);
+
+		pushSw(notifieeId, 'notification', packed);
+		if (type === 'follow') sendEmailNotification.follow(notifieeId, await Users.findOneOrFail(data.notifierId!));
+		if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, await Users.findOneOrFail(data.notifierId!));
+	}, 2000);
+
+	return notification;
+}
diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts
new file mode 100644
index 0000000000..71be8d4abf
--- /dev/null
+++ b/packages/backend/src/services/create-system-user.ts
@@ -0,0 +1,67 @@
+import * as bcrypt from 'bcryptjs';
+import { v4 as uuid } from 'uuid';
+import generateNativeUserToken from '../server/api/common/generate-native-user-token';
+import { genRsaKeyPair } from '@/misc/gen-key-pair';
+import { User } from '@/models/entities/user';
+import { UserProfile } from '@/models/entities/user-profile';
+import { getConnection } from 'typeorm';
+import { genId } from '@/misc/gen-id';
+import { UserKeypair } from '@/models/entities/user-keypair';
+import { UsedUsername } from '@/models/entities/used-username';
+
+export async function createSystemUser(username: string) {
+	const password = uuid();
+
+	// Generate hash of password
+	const salt = await bcrypt.genSalt(8);
+	const hash = await bcrypt.hash(password, salt);
+
+	// Generate secret
+	const secret = generateNativeUserToken();
+
+	const keyPair = await genRsaKeyPair(4096);
+
+	let account!: User;
+
+	// Start transaction
+	await getConnection().transaction(async transactionalEntityManager => {
+		const exist = await transactionalEntityManager.findOne(User, {
+			usernameLower: username.toLowerCase(),
+			host: null
+		});
+
+		if (exist) throw new Error('the user is already exists');
+
+		account = await transactionalEntityManager.insert(User, {
+			id: genId(),
+			createdAt: new Date(),
+			username: username,
+			usernameLower: username.toLowerCase(),
+			host: null,
+			token: secret,
+			isAdmin: false,
+			isLocked: true,
+			isExplorable: false,
+			isBot: true,
+		}).then(x => transactionalEntityManager.findOneOrFail(User, x.identifiers[0]));
+
+		await transactionalEntityManager.insert(UserKeypair, {
+			publicKey: keyPair.publicKey,
+			privateKey: keyPair.privateKey,
+			userId: account.id
+		});
+
+		await transactionalEntityManager.insert(UserProfile, {
+			userId: account.id,
+			autoAcceptFollowed: false,
+			password: hash,
+		});
+
+		await transactionalEntityManager.insert(UsedUsername, {
+			createdAt: new Date(),
+			username: username.toLowerCase(),
+		});
+	});
+
+	return account;
+}
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
new file mode 100644
index 0000000000..6c5fefd4ad
--- /dev/null
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -0,0 +1,466 @@
+import * as fs from 'fs';
+
+import { v4 as uuid } from 'uuid';
+
+import { publishMainStream, publishDriveStream } from '@/services/stream';
+import { deleteFile } from './delete-file';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { GenerateVideoThumbnail } from './generate-video-thumbnail';
+import { driveLogger } from './logger';
+import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng, convertSharpToPngOrJpeg } from './image-processor';
+import { contentDisposition } from '@/misc/content-disposition';
+import { getFileInfo } from '@/misc/get-file-info';
+import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index';
+import { InternalStorage } from './internal-storage';
+import { DriveFile } from '@/models/entities/drive-file';
+import { IRemoteUser, User } from '@/models/entities/user';
+import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index';
+import { genId } from '@/misc/gen-id';
+import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
+import * as S3 from 'aws-sdk/clients/s3';
+import { getS3 } from './s3';
+import * as sharp from 'sharp';
+
+const logger = driveLogger.createSubLogger('register', 'yellow');
+
+/***
+ * Save file
+ * @param path Path for original
+ * @param name Name for original
+ * @param type Content-Type for original
+ * @param hash Hash for original
+ * @param size Size for original
+ */
+async function save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<DriveFile> {
+	// thunbnail, webpublic を必要なら生成
+	const alts = await generateAlts(path, type, !file.uri);
+
+	const meta = await fetchMeta();
+
+	if (meta.useObjectStorage) {
+		//#region ObjectStorage params
+		let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']);
+
+		if (ext === '') {
+			if (type === 'image/jpeg') ext = '.jpg';
+			if (type === 'image/png') ext = '.png';
+			if (type === 'image/webp') ext = '.webp';
+			if (type === 'image/apng') ext = '.apng';
+			if (type === 'image/vnd.mozilla.apng') ext = '.apng';
+		}
+
+		const baseUrl = meta.objectStorageBaseUrl
+			|| `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
+
+		// for original
+		const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`;
+		const url = `${ baseUrl }/${ key }`;
+
+		// for alts
+		let webpublicKey: string | null = null;
+		let webpublicUrl: string | null = null;
+		let thumbnailKey: string | null = null;
+		let thumbnailUrl: string | null = null;
+		//#endregion
+
+		//#region Uploads
+		logger.info(`uploading original: ${key}`);
+		const uploads = [
+			upload(key, fs.createReadStream(path), type, name)
+		];
+
+		if (alts.webpublic) {
+			webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`;
+			webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
+
+			logger.info(`uploading webpublic: ${webpublicKey}`);
+			uploads.push(upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, name));
+		}
+
+		if (alts.thumbnail) {
+			thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`;
+			thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
+
+			logger.info(`uploading thumbnail: ${thumbnailKey}`);
+			uploads.push(upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type));
+		}
+
+		await Promise.all(uploads);
+		//#endregion
+
+		file.url = url;
+		file.thumbnailUrl = thumbnailUrl;
+		file.webpublicUrl = webpublicUrl;
+		file.accessKey = key;
+		file.thumbnailAccessKey = thumbnailKey;
+		file.webpublicAccessKey = webpublicKey;
+		file.name = name;
+		file.type = type;
+		file.md5 = hash;
+		file.size = size;
+		file.storedInternal = false;
+
+		return await DriveFiles.save(file);
+	} else { // use internal storage
+		const accessKey = uuid();
+		const thumbnailAccessKey = 'thumbnail-' + uuid();
+		const webpublicAccessKey = 'webpublic-' + uuid();
+
+		const url = InternalStorage.saveFromPath(accessKey, path);
+
+		let thumbnailUrl: string | null = null;
+		let webpublicUrl: string | null = null;
+
+		if (alts.thumbnail) {
+			thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data);
+			logger.info(`thumbnail stored: ${thumbnailAccessKey}`);
+		}
+
+		if (alts.webpublic) {
+			webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data);
+			logger.info(`web stored: ${webpublicAccessKey}`);
+		}
+
+		file.storedInternal = true;
+		file.url = url;
+		file.thumbnailUrl = thumbnailUrl;
+		file.webpublicUrl = webpublicUrl;
+		file.accessKey = accessKey;
+		file.thumbnailAccessKey = thumbnailAccessKey;
+		file.webpublicAccessKey = webpublicAccessKey;
+		file.name = name;
+		file.type = type;
+		file.md5 = hash;
+		file.size = size;
+
+		return await DriveFiles.save(file);
+	}
+}
+
+/**
+ * Generate webpublic, thumbnail, etc
+ * @param path Path for original
+ * @param type Content-Type for original
+ * @param generateWeb Generate webpublic or not
+ */
+export async function generateAlts(path: string, type: string, generateWeb: boolean) {
+	if (type.startsWith('video/')) {
+		try {
+			const thumbnail = await GenerateVideoThumbnail(path);
+			return {
+				webpublic: null,
+				thumbnail
+			};
+		} catch (e) {
+			logger.warn(`GenerateVideoThumbnail failed: ${e}`);
+			return {
+				webpublic: null,
+				thumbnail: null
+			};
+		}
+	}
+
+	if (!['image/jpeg', 'image/png', 'image/webp'].includes(type)) {
+		logger.debug(`web image and thumbnail not created (not an required file)`);
+		return {
+			webpublic: null,
+			thumbnail: null
+		};
+	}
+
+	let img: sharp.Sharp | null = null;
+
+	try {
+		img = sharp(path);
+		const metadata = await img.metadata();
+		const isAnimated = metadata.pages && metadata.pages > 1;
+
+		// skip animated
+		if (isAnimated) {
+			return {
+				webpublic: null,
+				thumbnail: null
+			};
+		}
+	} catch (e) {
+		logger.warn(`sharp failed: ${e}`);
+		return {
+			webpublic: null,
+			thumbnail: null
+		};
+	}
+
+	// #region webpublic
+	let webpublic: IImage | null = null;
+
+	if (generateWeb) {
+		logger.info(`creating web image`);
+
+		try {
+			if (['image/jpeg'].includes(type)) {
+				webpublic = await convertSharpToJpeg(img, 2048, 2048);
+			} else if (['image/webp'].includes(type)) {
+				webpublic = await convertSharpToWebp(img, 2048, 2048);
+			} else if (['image/png'].includes(type)) {
+				webpublic = await convertSharpToPng(img, 2048, 2048);
+			} else {
+				logger.debug(`web image not created (not an required image)`);
+			}
+		} catch (e) {
+			logger.warn(`web image not created (an error occured)`, e);
+		}
+	} else {
+		logger.info(`web image not created (from remote)`);
+	}
+	// #endregion webpublic
+
+	// #region thumbnail
+	let thumbnail: IImage | null = null;
+
+	try {
+		if (['image/jpeg', 'image/webp'].includes(type)) {
+			thumbnail = await convertSharpToJpeg(img, 498, 280);
+		} else if (['image/png'].includes(type)) {
+			thumbnail = await convertSharpToPngOrJpeg(img, 498, 280);
+		} else {
+			logger.debug(`thumbnail not created (not an required file)`);
+		}
+	} catch (e) {
+		logger.warn(`thumbnail not created (an error occured)`, e);
+	}
+	// #endregion thumbnail
+
+	return {
+		webpublic,
+		thumbnail,
+	};
+}
+
+/**
+ * Upload to ObjectStorage
+ */
+async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) {
+	if (type === 'image/apng') type = 'image/png';
+
+	const meta = await fetchMeta();
+
+	const params = {
+		Bucket: meta.objectStorageBucket,
+		Key: key,
+		Body: stream,
+		ContentType: type,
+		CacheControl: 'max-age=31536000, immutable',
+	} as S3.PutObjectRequest;
+
+	if (filename) params.ContentDisposition = contentDisposition('inline', filename);
+	if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
+
+	const s3 = getS3(meta);
+
+	const upload = s3.upload(params, {
+		partSize: s3.endpoint?.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024
+	});
+
+	const result = await upload.promise();
+	if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
+}
+
+async function deleteOldFile(user: IRemoteUser) {
+	const q = DriveFiles.createQueryBuilder('file')
+		.where('file.userId = :userId', { userId: user.id })
+		.andWhere('file.isLink = FALSE');
+
+	if (user.avatarId) {
+		q.andWhere('file.id != :avatarId', { avatarId: user.avatarId });
+	}
+
+	if (user.bannerId) {
+		q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
+	}
+
+	q.orderBy('file.id', 'ASC');
+
+	const oldFile = await q.getOne();
+
+	if (oldFile) {
+		deleteFile(oldFile, true);
+	}
+}
+
+/**
+ * Add file to drive
+ *
+ * @param user User who wish to add file
+ * @param path File path
+ * @param name Name
+ * @param comment Comment
+ * @param folderId Folder ID
+ * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
+ * @param isLink Do not save file to local
+ * @param url URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL)
+ * @param uri URL of source (リモートインスタンスのURLからアップロードされた場合の元URL)
+ * @param sensitive Mark file as sensitive
+ * @return Created drive file
+ */
+export default async function(
+	user: { id: User['id']; host: User['host'] } | null,
+	path: string,
+	name: string | null = null,
+	comment: string | null = null,
+	folderId: any = null,
+	force: boolean = false,
+	isLink: boolean = false,
+	url: string | null = null,
+	uri: string | null = null,
+	sensitive: boolean | null = null
+): Promise<DriveFile> {
+	const info = await getFileInfo(path);
+	logger.info(`${JSON.stringify(info)}`);
+
+	// detect name
+	const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled');
+
+	if (user && !force) {
+		// Check if there is a file with the same hash
+		const much = await DriveFiles.findOne({
+			md5: info.md5,
+			userId: user.id,
+		});
+
+		if (much) {
+			logger.info(`file with same hash is found: ${much.id}`);
+			return much;
+		}
+	}
+
+	//#region Check drive usage
+	if (user && !isLink) {
+		const usage = await DriveFiles.calcDriveUsageOf(user);
+
+		const instance = await fetchMeta();
+		const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
+
+		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
+
+		// If usage limit exceeded
+		if (usage + info.size > driveCapacity) {
+			if (Users.isLocalUser(user)) {
+				throw new Error('no-free-space');
+			} else {
+				// (アバターまたはバナーを含まず)最も古いファイルを削除する
+				deleteOldFile(await Users.findOneOrFail(user.id) as IRemoteUser);
+			}
+		}
+	}
+	//#endregion
+
+	const fetchFolder = async () => {
+		if (!folderId) {
+			return null;
+		}
+
+		const driveFolder = await DriveFolders.findOne({
+			id: folderId,
+			userId: user ? user.id : null
+		});
+
+		if (driveFolder == null) throw new Error('folder-not-found');
+
+		return driveFolder;
+	};
+
+	const properties: {
+		width?: number;
+		height?: number;
+	} = {};
+
+	if (info.width) {
+		properties['width'] = info.width;
+		properties['height'] = info.height;
+	}
+
+	const profile = user ? await UserProfiles.findOne(user.id) : null;
+
+	const folder = await fetchFolder();
+
+	let file = new DriveFile();
+	file.id = genId();
+	file.createdAt = new Date();
+	file.userId = user ? user.id : null;
+	file.userHost = user ? user.host : null;
+	file.folderId = folder !== null ? folder.id : null;
+	file.comment = comment;
+	file.properties = properties;
+	file.blurhash = info.blurhash || null;
+	file.isLink = isLink;
+	file.isSensitive = user
+		? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
+			(sensitive !== null && sensitive !== undefined)
+				? sensitive
+				: false
+		: false;
+
+	if (url !== null) {
+		file.src = url;
+
+		if (isLink) {
+			file.url = url;
+			// ローカルプロキシ用
+			file.accessKey = uuid();
+			file.thumbnailAccessKey = 'thumbnail-' + uuid();
+			file.webpublicAccessKey = 'webpublic-' + uuid();
+		}
+	}
+
+	if (uri !== null) {
+		file.uri = uri;
+	}
+
+	if (isLink) {
+		try {
+			file.size = 0;
+			file.md5 = info.md5;
+			file.name = detectedName;
+			file.type = info.type.mime;
+			file.storedInternal = false;
+
+			file = await DriveFiles.save(file);
+		} catch (e) {
+			// duplicate key error (when already registered)
+			if (isDuplicateKeyValueError(e)) {
+				logger.info(`already registered ${file.uri}`);
+
+				file = await DriveFiles.findOne({
+					uri: file.uri,
+					userId: user ? user.id : null
+				}) as DriveFile;
+			} else {
+				logger.error(e);
+				throw e;
+			}
+		}
+	} else {
+		file = await (save(file, path, detectedName, info.type.mime, info.md5, info.size));
+	}
+
+	logger.succ(`drive file has been created ${file.id}`);
+
+	if (user) {
+		DriveFiles.pack(file, { self: true }).then(packedFile => {
+			// Publish driveFileCreated event
+			publishMainStream(user.id, 'driveFileCreated', packedFile);
+			publishDriveStream(user.id, 'fileCreated', packedFile);
+		});
+	}
+
+	// 統計を更新
+	driveChart.update(file, true);
+	perUserDriveChart.update(file, true);
+	if (file.userHost !== null) {
+		instanceChart.updateDrive(file, true);
+		Instances.increment({ host: file.userHost }, 'driveUsage', file.size);
+		Instances.increment({ host: file.userHost }, 'driveFiles', 1);
+	}
+
+	return file;
+}
diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts
new file mode 100644
index 0000000000..2ac11b8295
--- /dev/null
+++ b/packages/backend/src/services/drive/delete-file.ts
@@ -0,0 +1,103 @@
+import { DriveFile } from '@/models/entities/drive-file';
+import { InternalStorage } from './internal-storage';
+import { DriveFiles, Instances } from '@/models/index';
+import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index';
+import { createDeleteObjectStorageFileJob } from '@/queue/index';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { getS3 } from './s3';
+import { v4 as uuid } from 'uuid';
+
+export async function deleteFile(file: DriveFile, isExpired = false) {
+	if (file.storedInternal) {
+		InternalStorage.del(file.accessKey!);
+
+		if (file.thumbnailUrl) {
+			InternalStorage.del(file.thumbnailAccessKey!);
+		}
+
+		if (file.webpublicUrl) {
+			InternalStorage.del(file.webpublicAccessKey!);
+		}
+	} else if (!file.isLink) {
+		createDeleteObjectStorageFileJob(file.accessKey!);
+
+		if (file.thumbnailUrl) {
+			createDeleteObjectStorageFileJob(file.thumbnailAccessKey!);
+		}
+
+		if (file.webpublicUrl) {
+			createDeleteObjectStorageFileJob(file.webpublicAccessKey!);
+		}
+	}
+
+	postProcess(file, isExpired);
+}
+
+export async function deleteFileSync(file: DriveFile, isExpired = false) {
+	if (file.storedInternal) {
+		InternalStorage.del(file.accessKey!);
+
+		if (file.thumbnailUrl) {
+			InternalStorage.del(file.thumbnailAccessKey!);
+		}
+
+		if (file.webpublicUrl) {
+			InternalStorage.del(file.webpublicAccessKey!);
+		}
+	} else if (!file.isLink) {
+		const promises = [];
+
+		promises.push(deleteObjectStorageFile(file.accessKey!));
+
+		if (file.thumbnailUrl) {
+			promises.push(deleteObjectStorageFile(file.thumbnailAccessKey!));
+		}
+
+		if (file.webpublicUrl) {
+			promises.push(deleteObjectStorageFile(file.webpublicAccessKey!));
+		}
+
+		await Promise.all(promises);
+	}
+
+	postProcess(file, isExpired);
+}
+
+async function postProcess(file: DriveFile, isExpired = false) {
+	// リモートファイル期限切れ削除後は直リンクにする
+	if (isExpired && file.userHost !== null && file.uri != null) {
+		DriveFiles.update(file.id, {
+			isLink: true,
+			url: file.uri,
+			thumbnailUrl: null,
+			webpublicUrl: null,
+			storedInternal: false,
+			// ローカルプロキシ用
+			accessKey: uuid(),
+			thumbnailAccessKey: 'thumbnail-' + uuid(),
+			webpublicAccessKey: 'webpublic-' + uuid(),
+		});
+	} else {
+		DriveFiles.delete(file.id);
+	}
+
+	// 統計を更新
+	driveChart.update(file, false);
+	perUserDriveChart.update(file, false);
+	if (file.userHost !== null) {
+		instanceChart.updateDrive(file, false);
+		Instances.decrement({ host: file.userHost }, 'driveUsage', file.size);
+		Instances.decrement({ host: file.userHost }, 'driveFiles', 1);
+	}
+}
+
+export async function deleteObjectStorageFile(key: string) {
+	const meta = await fetchMeta();
+
+	const s3 = getS3(meta);
+
+	await s3.deleteObject({
+		Bucket: meta.objectStorageBucket!,
+		Key: key
+	}).promise();
+}
diff --git a/packages/backend/src/services/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts
new file mode 100644
index 0000000000..f0adc7c338
--- /dev/null
+++ b/packages/backend/src/services/drive/generate-video-thumbnail.ts
@@ -0,0 +1,37 @@
+import * as fs from 'fs';
+import * as tmp from 'tmp';
+import { IImage, convertToJpeg } from './image-processor';
+import * as FFmpeg from 'fluent-ffmpeg';
+
+export async function GenerateVideoThumbnail(path: string): Promise<IImage> {
+	const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => {
+		tmp.dir((e, path, cleanup) => {
+			if (e) return rej(e);
+			res([path, cleanup]);
+		});
+	});
+
+	await new Promise((res, rej) => {
+		FFmpeg({
+			source: path
+		})
+		.on('end', res)
+		.on('error', rej)
+		.screenshot({
+			folder: outDir,
+			filename: 'output.png',
+			count: 1,
+			timestamps: ['5%']
+		});
+	});
+
+	const outPath = `${outDir}/output.png`;
+
+	const thumbnail = await convertToJpeg(outPath, 498, 280);
+
+	// cleanup
+	await fs.promises.unlink(outPath);
+	cleanup();
+
+	return thumbnail;
+}
diff --git a/packages/backend/src/services/drive/image-processor.ts b/packages/backend/src/services/drive/image-processor.ts
new file mode 100644
index 0000000000..493bf5c1cc
--- /dev/null
+++ b/packages/backend/src/services/drive/image-processor.ts
@@ -0,0 +1,107 @@
+import * as sharp from 'sharp';
+
+export type IImage = {
+	data: Buffer;
+	ext: string | null;
+	type: string;
+};
+
+/**
+ * Convert to JPEG
+ *   with resize, remove metadata, resolve orientation, stop animation
+ */
+export async function convertToJpeg(path: string, width: number, height: number): Promise<IImage> {
+	return convertSharpToJpeg(await sharp(path), width, height);
+}
+
+export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
+	const data = await sharp
+		.resize(width, height, {
+			fit: 'inside',
+			withoutEnlargement: true
+		})
+		.rotate()
+		.jpeg({
+			quality: 85,
+			progressive: true
+		})
+		.toBuffer();
+
+	return {
+		data,
+		ext: 'jpg',
+		type: 'image/jpeg'
+	};
+}
+
+/**
+ * Convert to WebP
+ *   with resize, remove metadata, resolve orientation, stop animation
+ */
+export async function convertToWebp(path: string, width: number, height: number): Promise<IImage> {
+	return convertSharpToWebp(await sharp(path), width, height);
+}
+
+export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
+	const data = await sharp
+		.resize(width, height, {
+			fit: 'inside',
+			withoutEnlargement: true
+		})
+		.rotate()
+		.webp({
+			quality: 85
+		})
+		.toBuffer();
+
+	return {
+		data,
+		ext: 'webp',
+		type: 'image/webp'
+	};
+}
+
+/**
+ * Convert to PNG
+ *   with resize, remove metadata, resolve orientation, stop animation
+ */
+export async function convertToPng(path: string, width: number, height: number): Promise<IImage> {
+	return convertSharpToPng(await sharp(path), width, height);
+}
+
+export async function convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
+	const data = await sharp
+		.resize(width, height, {
+			fit: 'inside',
+			withoutEnlargement: true
+		})
+		.rotate()
+		.png()
+		.toBuffer();
+
+	return {
+		data,
+		ext: 'png',
+		type: 'image/png'
+	};
+}
+
+/**
+ * Convert to PNG or JPEG
+ *   with resize, remove metadata, resolve orientation, stop animation
+ */
+export async function convertToPngOrJpeg(path: string, width: number, height: number): Promise<IImage> {
+	return convertSharpToPngOrJpeg(await sharp(path), width, height);
+}
+
+export async function convertSharpToPngOrJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
+	const stats = await sharp.stats();
+	const metadata = await sharp.metadata();
+
+	// 不透明で300x300pxの範囲を超えていればJPEG
+	if (stats.isOpaque && ((metadata.width && metadata.width >= 300) || (metadata.height && metadata!.height >= 300))) {
+		return await convertSharpToJpeg(sharp, width, height);
+	} else {
+		return await convertSharpToPng(sharp, width, height);
+	}
+}
diff --git a/packages/backend/src/services/drive/internal-storage.ts b/packages/backend/src/services/drive/internal-storage.ts
new file mode 100644
index 0000000000..fe190a028c
--- /dev/null
+++ b/packages/backend/src/services/drive/internal-storage.ts
@@ -0,0 +1,35 @@
+import * as fs from 'fs';
+import * as Path from 'path';
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import config from '@/config/index';
+
+//const _filename = fileURLToPath(import.meta.url);
+const _filename = __filename;
+const _dirname = dirname(_filename);
+
+export class InternalStorage {
+	private static readonly path = Path.resolve(_dirname, '../../../../../files');
+
+	public static resolvePath = (key: string) => Path.resolve(InternalStorage.path, key);
+
+	public static read(key: string) {
+		return fs.createReadStream(InternalStorage.resolvePath(key));
+	}
+
+	public static saveFromPath(key: string, srcPath: string) {
+		fs.mkdirSync(InternalStorage.path, { recursive: true });
+		fs.copyFileSync(srcPath, InternalStorage.resolvePath(key));
+		return `${config.url}/files/${key}`;
+	}
+
+	public static saveFromBuffer(key: string, data: Buffer) {
+		fs.mkdirSync(InternalStorage.path, { recursive: true });
+		fs.writeFileSync(InternalStorage.resolvePath(key), data);
+		return `${config.url}/files/${key}`;
+	}
+
+	public static del(key: string) {
+		fs.unlink(InternalStorage.resolvePath(key), () => {});
+	}
+}
diff --git a/packages/backend/src/services/drive/logger.ts b/packages/backend/src/services/drive/logger.ts
new file mode 100644
index 0000000000..655d074d6e
--- /dev/null
+++ b/packages/backend/src/services/drive/logger.ts
@@ -0,0 +1,3 @@
+import Logger from '../logger';
+
+export const driveLogger = new Logger('drive', 'blue');
diff --git a/packages/backend/src/services/drive/s3.ts b/packages/backend/src/services/drive/s3.ts
new file mode 100644
index 0000000000..f473c4a203
--- /dev/null
+++ b/packages/backend/src/services/drive/s3.ts
@@ -0,0 +1,24 @@
+import { URL } from 'url';
+import * as S3 from 'aws-sdk/clients/s3';
+import { Meta } from '@/models/entities/meta';
+import { getAgentByUrl } from '@/misc/fetch';
+
+export function getS3(meta: Meta) {
+	const u = meta.objectStorageEndpoint != null
+		? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
+		: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
+
+	return new S3({
+		endpoint: meta.objectStorageEndpoint || undefined,
+		accessKeyId: meta.objectStorageAccessKey!,
+		secretAccessKey: meta.objectStorageSecretKey!,
+		region: meta.objectStorageRegion || undefined,
+		sslEnabled: meta.objectStorageUseSSL,
+		s3ForcePathStyle: !meta.objectStorageEndpoint	// AWS with endPoint omitted
+			? false
+			: meta.objectStorageS3ForcePathStyle,
+		httpOptions: {
+			agent: getAgentByUrl(new URL(u), !meta.objectStorageUseProxy)
+		}
+	});
+}
diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts
new file mode 100644
index 0000000000..29788c4af4
--- /dev/null
+++ b/packages/backend/src/services/drive/upload-from-url.ts
@@ -0,0 +1,62 @@
+import { URL } from 'url';
+import create from './add-file';
+import { User } from '@/models/entities/user';
+import { driveLogger } from './logger';
+import { createTemp } from '@/misc/create-temp';
+import { downloadUrl } from '@/misc/download-url';
+import { DriveFolder } from '@/models/entities/drive-folder';
+import { DriveFile } from '@/models/entities/drive-file';
+import { DriveFiles } from '@/models/index';
+
+const logger = driveLogger.createSubLogger('downloader');
+
+export default async (
+	url: string,
+	user: { id: User['id']; host: User['host'] } | null,
+	folderId: DriveFolder['id'] | null = null,
+	uri: string | null = null,
+	sensitive = false,
+	force = false,
+	link = false,
+	comment = null
+): Promise<DriveFile> => {
+	let name = new URL(url).pathname.split('/').pop() || null;
+	if (name == null || !DriveFiles.validateFileName(name)) {
+		name = null;
+	}
+
+	// If the comment is same as the name, skip comment
+	// (image.name is passed in when receiving attachment)
+	if (comment !== null && name == comment) {
+		comment = null;
+	}
+
+	// Create temp file
+	const [path, cleanup] = await createTemp();
+
+	// write content at URL to temp file
+	await downloadUrl(url, path);
+
+	let driveFile: DriveFile;
+	let error;
+
+	try {
+		driveFile = await create(user, path, name, comment, folderId, force, link, url, uri, sensitive);
+		logger.succ(`Got: ${driveFile.id}`);
+	} catch (e) {
+		error = e;
+		logger.error(`Failed to create drive file: ${e}`, {
+			url: url,
+			e: e
+		});
+	}
+
+	// clean-up
+	cleanup();
+
+	if (error) {
+		throw error;
+	} else {
+		return driveFile!;
+	}
+};
diff --git a/packages/backend/src/services/fetch-instance-metadata.ts b/packages/backend/src/services/fetch-instance-metadata.ts
new file mode 100644
index 0000000000..2c401508a9
--- /dev/null
+++ b/packages/backend/src/services/fetch-instance-metadata.ts
@@ -0,0 +1,265 @@
+import { DOMWindow, JSDOM } from 'jsdom';
+import fetch from 'node-fetch';
+import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch';
+import { Instance } from '@/models/entities/instance';
+import { Instances } from '@/models/index';
+import { getFetchInstanceMetadataLock } from '@/misc/app-lock';
+import Logger from './logger';
+import { URL } from 'url';
+
+const logger = new Logger('metadata', 'cyan');
+
+export async function fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
+	const unlock = await getFetchInstanceMetadataLock(instance.host);
+
+	if (!force) {
+		const _instance = await Instances.findOne({ host: instance.host });
+		const now = Date.now();
+		if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
+			unlock();
+			return;
+		}
+	}
+
+	logger.info(`Fetching metadata of ${instance.host} ...`);
+
+	try {
+		const [info, dom, manifest] = await Promise.all([
+			fetchNodeinfo(instance).catch(() => null),
+			fetchDom(instance).catch(() => null),
+			fetchManifest(instance).catch(() => null),
+		]);
+
+		const [favicon, icon, themeColor, name, description] = await Promise.all([
+			fetchFaviconUrl(instance, dom).catch(() => null),
+			fetchIconUrl(instance, dom, manifest).catch(() => null),
+			getThemeColor(dom, manifest).catch(() => null),
+			getSiteName(info, dom, manifest).catch(() => null),
+			getDescription(info, dom, manifest).catch(() => null),
+		]);
+
+		logger.succ(`Successfuly fetched metadata of ${instance.host}`);
+
+		const updates = {
+			infoUpdatedAt: new Date(),
+		} as Record<string, any>;
+
+		if (info) {
+			updates.softwareName = info.software?.name.toLowerCase();
+			updates.softwareVersion = info.software?.version;
+			updates.openRegistrations = info.openRegistrations;
+			updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null;
+			updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null;
+		}
+
+		if (name) updates.name = name;
+		if (description) updates.description = description;
+		if (icon || favicon) updates.iconUrl = icon || favicon;
+		if (favicon) updates.faviconUrl = favicon;
+		if (themeColor) updates.themeColor = themeColor;
+
+		await Instances.update(instance.id, updates);
+
+		logger.succ(`Successfuly updated metadata of ${instance.host}`);
+	} catch (e) {
+		logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
+	} finally {
+		unlock();
+	}
+}
+
+type NodeInfo = {
+	openRegistrations?: any;
+	software?: {
+		name?: any;
+		version?: any;
+	};
+	metadata?: {
+		name?: any;
+		nodeName?: any;
+		nodeDescription?: any;
+		description?: any;
+		maintainer?: {
+			name?: any;
+			email?: any;
+		};
+	};
+};
+
+async function fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
+	logger.info(`Fetching nodeinfo of ${instance.host} ...`);
+
+	try {
+		const wellknown = await getJson('https://' + instance.host + '/.well-known/nodeinfo')
+			.catch(e => {
+				if (e.statusCode === 404) {
+					throw 'No nodeinfo provided';
+				} else {
+					throw e.statusCode || e.message;
+				}
+			});
+
+		if (wellknown.links == null || !Array.isArray(wellknown.links)) {
+			throw 'No wellknown links';
+		}
+
+		const links = wellknown.links as any[];
+
+		const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
+		const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
+		const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
+		const link = lnik2_1 || lnik2_0 || lnik1_0;
+
+		if (link == null) {
+			throw 'No nodeinfo link provided';
+		}
+
+		const info = await getJson(link.href)
+			.catch(e => {
+				throw e.statusCode || e.message;
+			});
+
+		logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
+
+		return info;
+	} catch (e) {
+		logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e}`);
+
+		throw e;
+	}
+}
+
+async function fetchDom(instance: Instance): Promise<DOMWindow['document']> {
+	logger.info(`Fetching HTML of ${instance.host} ...`);
+
+	const url = 'https://' + instance.host;
+
+	const html = await getHtml(url);
+
+	const { window } = new JSDOM(html);
+	const doc = window.document;
+
+	return doc;
+}
+
+async function fetchManifest(instance: Instance): Promise<Record<string, any> | null> {
+	const url = 'https://' + instance.host;
+
+	const manifestUrl = url + '/manifest.json';
+
+	const manifest = await getJson(manifestUrl);
+
+	return manifest;
+}
+
+async function fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
+	const url = 'https://' + instance.host;
+
+	if (doc) {
+		const href = doc.querySelector('link[rel="icon"]')?.getAttribute('href');
+
+		if (href) {
+			return (new URL(href, url)).href;
+		}
+	}
+
+	const faviconUrl = url + '/favicon.ico';
+
+	const favicon = await fetch(faviconUrl, {
+		timeout: 10000,
+		agent: getAgentByUrl,
+	});
+
+	if (favicon.ok) {
+		return faviconUrl;
+	}
+
+	return null;
+}
+
+async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
+	if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
+		const url = 'https://' + instance.host;
+		return (new URL(manifest.icons[0].src, url)).href;
+	}
+
+	if (doc) {
+		const url = 'https://' + instance.host;
+
+		const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href');
+		const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href');
+		const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href');
+
+		const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon;
+
+		if (href) {
+			return (new URL(href, url)).href;
+		}
+	}
+
+	return null;
+}
+
+async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
+	if (doc) {
+		const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
+
+		if (themeColor) {
+			return themeColor;
+		}
+	}
+
+	if (manifest) {
+		return manifest.theme_color;
+	}
+
+	return null;
+}
+
+async function getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
+	if (info && info.metadata) {
+		if (info.metadata.nodeName || info.metadata.name) {
+			return info.metadata.nodeName || info.metadata.name;
+		}
+	}
+
+	if (doc) {
+		const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
+
+		if (og) {
+			return og;
+		}
+	}
+
+	if (manifest) {
+		return manifest?.name || manifest?.short_name;
+	}
+
+	return null;
+}
+
+async function getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
+	if (info && info.metadata) {
+		if (info.metadata.nodeDescription || info.metadata.description) {
+			return info.metadata.nodeDescription || info.metadata.description;
+		}
+	}
+
+	if (doc) {
+		const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
+		if (meta) {
+			return meta;
+		}
+
+		const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
+		if (og) {
+			return og;
+		}
+	}
+
+	if (manifest) {
+		return manifest?.name || manifest?.short_name;
+	}
+
+	return null;
+}
diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts
new file mode 100644
index 0000000000..4d0754b504
--- /dev/null
+++ b/packages/backend/src/services/following/create.ts
@@ -0,0 +1,180 @@
+import { publishMainStream, publishUserEvent } from '@/services/stream';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderFollow from '@/remote/activitypub/renderer/follow';
+import renderAccept from '@/remote/activitypub/renderer/accept';
+import renderReject from '@/remote/activitypub/renderer/reject';
+import { deliver } from '@/queue/index';
+import createFollowRequest from './requests/create';
+import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
+import Logger from '../logger';
+import { IdentifiableError } from '@/misc/identifiable-error';
+import { User } from '@/models/entities/user';
+import { Followings, Users, FollowRequests, Blockings, Instances, UserProfiles } from '@/models/index';
+import { instanceChart, perUserFollowingChart } from '@/services/chart/index';
+import { genId } from '@/misc/gen-id';
+import { createNotification } from '../create-notification';
+import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
+
+const logger = new Logger('following/create');
+
+export async function insertFollowingDoc(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }, follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }) {
+	if (follower.id === followee.id) return;
+
+	let alreadyFollowed = false;
+
+	await Followings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		followerId: follower.id,
+		followeeId: followee.id,
+
+		// 非正規化
+		followerHost: follower.host,
+		followerInbox: Users.isRemoteUser(follower) ? follower.inbox : null,
+		followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : null,
+		followeeHost: followee.host,
+		followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : null,
+		followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : null
+	}).catch(e => {
+		if (isDuplicateKeyValueError(e) && Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+			logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
+			alreadyFollowed = true;
+		} else {
+			throw e;
+		}
+	});
+
+	const req = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	if (req) {
+		await FollowRequests.delete({
+			followeeId: followee.id,
+			followerId: follower.id
+		});
+
+		// 通知を作成
+		createNotification(follower.id, 'followRequestAccepted', {
+			notifierId: followee.id,
+		});
+	}
+
+	if (alreadyFollowed) return;
+
+	//#region Increment counts
+	Users.increment({ id: follower.id }, 'followingCount', 1);
+	Users.increment({ id: followee.id }, 'followersCount', 1);
+	//#endregion
+
+	//#region Update instance stats
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+		registerOrFetchInstanceDoc(follower.host).then(i => {
+			Instances.increment({ id: i.id }, 'followingCount', 1);
+			instanceChart.updateFollowing(i.host, true);
+		});
+	} else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
+		registerOrFetchInstanceDoc(followee.host).then(i => {
+			Instances.increment({ id: i.id }, 'followersCount', 1);
+			instanceChart.updateFollowers(i.host, true);
+		});
+	}
+	//#endregion
+
+	perUserFollowingChart.update(follower, followee, true);
+
+	// Publish follow event
+	if (Users.isLocalUser(follower)) {
+		Users.pack(followee.id, follower, {
+			detail: true
+		}).then(packed => {
+			publishUserEvent(follower.id, 'follow', packed);
+			publishMainStream(follower.id, 'follow', packed);
+		});
+	}
+
+	// Publish followed event
+	if (Users.isLocalUser(followee)) {
+		Users.pack(follower.id, followee).then(packed => publishMainStream(followee.id, 'followed', packed));
+
+		// 通知を作成
+		createNotification(followee.id, 'follow', {
+			notifierId: follower.id
+		});
+	}
+}
+
+export default async function(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string) {
+	const [follower, followee] = await Promise.all([
+		Users.findOneOrFail(_follower.id),
+		Users.findOneOrFail(_followee.id)
+	]);
+
+	// check blocking
+	const [blocking, blocked] = await Promise.all([
+		Blockings.findOne({
+			blockerId: follower.id,
+			blockeeId: followee.id,
+		}),
+		Blockings.findOne({
+			blockerId: followee.id,
+			blockeeId: follower.id,
+		})
+	]);
+
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocked) {
+		// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
+		const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee));
+		deliver(followee , content, follower.inbox);
+		return;
+	} else if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocking) {
+		// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
+		await Blockings.delete(blocking.id);
+	} else {
+		// それ以外は単純に例外
+		if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
+		if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
+	}
+
+	const followeeProfile = await UserProfiles.findOneOrFail(followee.id);
+
+	// フォロー対象が鍵アカウントである or
+	// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
+	// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
+	// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
+	if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) {
+		let autoAccept = false;
+
+		// 鍵アカウントであっても、既にフォローされていた場合はスルー
+		const following = await Followings.findOne({
+			followerId: follower.id,
+			followeeId: followee.id,
+		});
+		if (following) {
+			autoAccept = true;
+		}
+
+		// フォローしているユーザーは自動承認オプション
+		if (!autoAccept && (Users.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
+			const followed = await Followings.findOne({
+				followerId: followee.id,
+				followeeId: follower.id
+			});
+
+			if (followed) autoAccept = true;
+		}
+
+		if (!autoAccept) {
+			await createFollowRequest(follower, followee, requestId);
+			return;
+		}
+	}
+
+	await insertFollowingDoc(followee, follower);
+
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+		const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee));
+		deliver(followee, content, follower.inbox);
+	}
+}
diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts
new file mode 100644
index 0000000000..29e3372b6a
--- /dev/null
+++ b/packages/backend/src/services/following/delete.ts
@@ -0,0 +1,69 @@
+import { publishMainStream, publishUserEvent } from '@/services/stream';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderFollow from '@/remote/activitypub/renderer/follow';
+import renderUndo from '@/remote/activitypub/renderer/undo';
+import { deliver } from '@/queue/index';
+import Logger from '../logger';
+import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
+import { User } from '@/models/entities/user';
+import { Followings, Users, Instances } from '@/models/index';
+import { instanceChart, perUserFollowingChart } from '@/services/chart/index';
+
+const logger = new Logger('following/delete');
+
+export default async function(follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, silent = false) {
+	const following = await Followings.findOne({
+		followerId: follower.id,
+		followeeId: followee.id
+	});
+
+	if (following == null) {
+		logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
+		return;
+	}
+
+	await Followings.delete(following.id);
+
+	decrementFollowing(follower, followee);
+
+	// Publish unfollow event
+	if (!silent && Users.isLocalUser(follower)) {
+		Users.pack(followee.id, follower, {
+			detail: true
+		}).then(packed => {
+			publishUserEvent(follower.id, 'unfollow', packed);
+			publishMainStream(follower.id, 'unfollow', packed);
+		});
+	}
+
+	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
+		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
+		deliver(follower, content, followee.inbox);
+	}
+}
+
+export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) {
+	//#region Decrement following count
+	Users.decrement({ id: follower.id }, 'followingCount', 1);
+	//#endregion
+
+	//#region Decrement followers count
+	Users.decrement({ id: followee.id }, 'followersCount', 1);
+	//#endregion
+
+	//#region Update instance stats
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+		registerOrFetchInstanceDoc(follower.host).then(i => {
+			Instances.decrement({ id: i.id }, 'followingCount', 1);
+			instanceChart.updateFollowing(i.host, false);
+		});
+	} else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
+		registerOrFetchInstanceDoc(followee.host).then(i => {
+			Instances.decrement({ id: i.id }, 'followersCount', 1);
+			instanceChart.updateFollowers(i.host, false);
+		});
+	}
+	//#endregion
+
+	perUserFollowingChart.update(follower, followee, false);
+}
diff --git a/packages/backend/src/services/following/requests/accept-all.ts b/packages/backend/src/services/following/requests/accept-all.ts
new file mode 100644
index 0000000000..23b4fd0a46
--- /dev/null
+++ b/packages/backend/src/services/following/requests/accept-all.ts
@@ -0,0 +1,18 @@
+import accept from './accept';
+import { User } from '@/models/entities/user';
+import { FollowRequests, Users } from '@/models/index';
+
+/**
+ * 指定したユーザー宛てのフォローリクエストをすべて承認
+ * @param user ユーザー
+ */
+export default async function(user: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }) {
+	const requests = await FollowRequests.find({
+		followeeId: user.id
+	});
+
+	for (const request of requests) {
+		const follower = await Users.findOneOrFail(request.followerId);
+		accept(user, follower);
+	}
+}
diff --git a/packages/backend/src/services/following/requests/accept.ts b/packages/backend/src/services/following/requests/accept.ts
new file mode 100644
index 0000000000..316a6f1c12
--- /dev/null
+++ b/packages/backend/src/services/following/requests/accept.ts
@@ -0,0 +1,31 @@
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderFollow from '@/remote/activitypub/renderer/follow';
+import renderAccept from '@/remote/activitypub/renderer/accept';
+import { deliver } from '@/queue/index';
+import { publishMainStream } from '@/services/stream';
+import { insertFollowingDoc } from '../create';
+import { User, ILocalUser } from '@/models/entities/user';
+import { FollowRequests, Users } from '@/models/index';
+import { IdentifiableError } from '@/misc/identifiable-error';
+
+export default async function(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, follower: User) {
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	if (request == null) {
+		throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
+	}
+
+	await insertFollowingDoc(followee, follower);
+
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+		const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId!), followee));
+		deliver(followee, content, follower.inbox);
+	}
+
+	Users.pack(followee.id, followee, {
+		detail: true
+	}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
+}
diff --git a/packages/backend/src/services/following/requests/cancel.ts b/packages/backend/src/services/following/requests/cancel.ts
new file mode 100644
index 0000000000..8895849857
--- /dev/null
+++ b/packages/backend/src/services/following/requests/cancel.ts
@@ -0,0 +1,36 @@
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderFollow from '@/remote/activitypub/renderer/follow';
+import renderUndo from '@/remote/activitypub/renderer/undo';
+import { deliver } from '@/queue/index';
+import { publishMainStream } from '@/services/stream';
+import { IdentifiableError } from '@/misc/identifiable-error';
+import { User, ILocalUser } from '@/models/entities/user';
+import { Users, FollowRequests } from '@/models/index';
+
+export default async function(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox'] }, follower: { id: User['id']; host: User['host']; uri: User['host'] }) {
+	if (Users.isRemoteUser(followee)) {
+		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
+
+		if (Users.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
+			deliver(follower, content, followee.inbox);
+		}
+	}
+
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	if (request == null) {
+		throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
+	}
+
+	await FollowRequests.delete({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	Users.pack(followee.id, followee, {
+		detail: true
+	}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
+}
diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts
new file mode 100644
index 0000000000..507cb2b7d1
--- /dev/null
+++ b/packages/backend/src/services/following/requests/create.ts
@@ -0,0 +1,63 @@
+import { publishMainStream } from '@/services/stream';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderFollow from '@/remote/activitypub/renderer/follow';
+import { deliver } from '@/queue/index';
+import { User } from '@/models/entities/user';
+import { Blockings, FollowRequests, Users } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { createNotification } from '../../create-notification';
+
+export default async function(follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, requestId?: string) {
+	if (follower.id === followee.id) return;
+
+	// check blocking
+	const [blocking, blocked] = await Promise.all([
+		Blockings.findOne({
+			blockerId: follower.id,
+			blockeeId: followee.id,
+		}),
+		Blockings.findOne({
+			blockerId: followee.id,
+			blockeeId: follower.id,
+		})
+	]);
+
+	if (blocking != null) throw new Error('blocking');
+	if (blocked != null) throw new Error('blocked');
+
+	const followRequest = await FollowRequests.save({
+		id: genId(),
+		createdAt: new Date(),
+		followerId: follower.id,
+		followeeId: followee.id,
+		requestId,
+
+		// 非正規化
+		followerHost: follower.host,
+		followerInbox: Users.isRemoteUser(follower) ? follower.inbox : undefined,
+		followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : undefined,
+		followeeHost: followee.host,
+		followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined,
+		followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined
+	});
+
+	// Publish receiveRequest event
+	if (Users.isLocalUser(followee)) {
+		Users.pack(follower.id, followee).then(packed => publishMainStream(followee.id, 'receiveFollowRequest', packed));
+
+		Users.pack(followee.id, followee, {
+			detail: true
+		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
+
+		// 通知を作成
+		createNotification(followee.id, 'receiveFollowRequest', {
+			notifierId: follower.id,
+			followRequestId: followRequest.id
+		});
+	}
+
+	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
+		const content = renderActivity(renderFollow(follower, followee));
+		deliver(follower, content, followee.inbox);
+	}
+}
diff --git a/packages/backend/src/services/following/requests/reject.ts b/packages/backend/src/services/following/requests/reject.ts
new file mode 100644
index 0000000000..41cebd9e41
--- /dev/null
+++ b/packages/backend/src/services/following/requests/reject.ts
@@ -0,0 +1,46 @@
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderFollow from '@/remote/activitypub/renderer/follow';
+import renderReject from '@/remote/activitypub/renderer/reject';
+import { deliver } from '@/queue/index';
+import { publishMainStream, publishUserEvent } from '@/services/stream';
+import { User, ILocalUser } from '@/models/entities/user';
+import { Users, FollowRequests, Followings } from '@/models/index';
+import { decrementFollowing } from '../delete';
+
+export default async function(followee: { id: User['id']; host: User['host']; uri: User['host'] }, follower: User) {
+	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
+		const request = await FollowRequests.findOne({
+			followeeId: followee.id,
+			followerId: follower.id
+		});
+
+		const content = renderActivity(renderReject(renderFollow(follower, followee, request!.requestId!), followee));
+		deliver(followee, content, follower.inbox);
+	}
+
+	const request = await FollowRequests.findOne({
+		followeeId: followee.id,
+		followerId: follower.id
+	});
+
+	if (request) {
+		await FollowRequests.delete(request.id);
+	} else {
+		const following = await Followings.findOne({
+			followeeId: followee.id,
+			followerId: follower.id
+		});
+
+		if (following) {
+			await Followings.delete(following.id);
+			decrementFollowing(follower, followee);
+		}
+	}
+
+	Users.pack(followee.id, follower, {
+		detail: true
+	}).then(packed => {
+		publishUserEvent(follower.id, 'unfollow', packed);
+		publishMainStream(follower.id, 'unfollow', packed);
+	});
+}
diff --git a/packages/backend/src/services/i/pin.ts b/packages/backend/src/services/i/pin.ts
new file mode 100644
index 0000000000..b31beb6e1a
--- /dev/null
+++ b/packages/backend/src/services/i/pin.ts
@@ -0,0 +1,92 @@
+import config from '@/config/index';
+import renderAdd from '@/remote/activitypub/renderer/add';
+import renderRemove from '@/remote/activitypub/renderer/remove';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { IdentifiableError } from '@/misc/identifiable-error';
+import { User } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { Notes, UserNotePinings, Users } from '@/models/index';
+import { UserNotePining } from '@/models/entities/user-note-pining';
+import { genId } from '@/misc/gen-id';
+import { deliverToFollowers } from '@/remote/activitypub/deliver-manager';
+import { deliverToRelays } from '../relay';
+
+/**
+ * 指定した投稿をピン留めします
+ * @param user
+ * @param noteId
+ */
+export async function addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) {
+	// Fetch pinee
+	const note = await Notes.findOne({
+		id: noteId,
+		userId: user.id
+	});
+
+	if (note == null) {
+		throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.');
+	}
+
+	const pinings = await UserNotePinings.find({ userId: user.id });
+
+	if (pinings.length >= 5) {
+		throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
+	}
+
+	if (pinings.some(pining => pining.noteId === note.id)) {
+		throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.');
+	}
+
+	await UserNotePinings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: user.id,
+		noteId: note.id
+	} as UserNotePining);
+
+	// Deliver to remote followers
+	if (Users.isLocalUser(user)) {
+		deliverPinnedChange(user.id, note.id, true);
+	}
+}
+
+/**
+ * 指定した投稿のピン留めを解除します
+ * @param user
+ * @param noteId
+ */
+export async function removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) {
+	// Fetch unpinee
+	const note = await Notes.findOne({
+		id: noteId,
+		userId: user.id
+	});
+
+	if (note == null) {
+		throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.');
+	}
+
+	UserNotePinings.delete({
+		userId: user.id,
+		noteId: note.id
+	});
+
+	// Deliver to remote followers
+	if (Users.isLocalUser(user)) {
+		deliverPinnedChange(user.id, noteId, false);
+	}
+}
+
+export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) {
+	const user = await Users.findOne(userId);
+	if (user == null) throw new Error('user not found');
+
+	if (!Users.isLocalUser(user)) return;
+
+	const target = `${config.url}/users/${user.id}/collections/featured`;
+	const item = `${config.url}/notes/${noteId}`;
+	const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
+
+	deliverToFollowers(user, content);
+	deliverToRelays(user, content);
+}
diff --git a/packages/backend/src/services/i/update.ts b/packages/backend/src/services/i/update.ts
new file mode 100644
index 0000000000..f700d9b48b
--- /dev/null
+++ b/packages/backend/src/services/i/update.ts
@@ -0,0 +1,19 @@
+import renderUpdate from '@/remote/activitypub/renderer/update';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { Users } from '@/models/index';
+import { User } from '@/models/entities/user';
+import { renderPerson } from '@/remote/activitypub/renderer/person';
+import { deliverToFollowers } from '@/remote/activitypub/deliver-manager';
+import { deliverToRelays } from '../relay';
+
+export async function publishToFollowers(userId: User['id']) {
+	const user = await Users.findOne(userId);
+	if (user == null) throw new Error('user not found');
+
+	// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
+	if (Users.isLocalUser(user)) {
+		const content = renderActivity(renderUpdate(await renderPerson(user), user));
+		deliverToFollowers(user, content);
+		deliverToRelays(user, content);
+	}
+}
diff --git a/packages/backend/src/services/insert-moderation-log.ts b/packages/backend/src/services/insert-moderation-log.ts
new file mode 100644
index 0000000000..00397652ee
--- /dev/null
+++ b/packages/backend/src/services/insert-moderation-log.ts
@@ -0,0 +1,13 @@
+import { ModerationLogs } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { User } from '@/models/entities/user';
+
+export async function insertModerationLog(moderator: { id: User['id'] }, type: string, info?: Record<string, any>) {
+	await ModerationLogs.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: moderator.id,
+		type: type,
+		info: info || {}
+	});
+}
diff --git a/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts
new file mode 100644
index 0000000000..b3625226c3
--- /dev/null
+++ b/packages/backend/src/services/instance-actor.ts
@@ -0,0 +1,27 @@
+import { createSystemUser } from './create-system-user';
+import { ILocalUser } from '@/models/entities/user';
+import { Users } from '@/models/index';
+import { Cache } from '@/misc/cache';
+
+const ACTOR_USERNAME = 'instance.actor' as const;
+
+const cache = new Cache<ILocalUser>(Infinity);
+
+export async function getInstanceActor(): Promise<ILocalUser> {
+	const cached = cache.get(null);
+	if (cached) return cached;
+
+	const user = await Users.findOne({
+		host: null,
+		username: ACTOR_USERNAME
+	}) as ILocalUser | undefined;
+
+	if (user) {
+		cache.set(null, user);
+		return user;
+	} else {
+		const created = await createSystemUser(ACTOR_USERNAME) as ILocalUser;
+		cache.set(null, created);
+		return created;
+	}
+}
diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts
new file mode 100644
index 0000000000..709b9bae5c
--- /dev/null
+++ b/packages/backend/src/services/logger.ts
@@ -0,0 +1,127 @@
+import * as cluster from 'cluster';
+import * as chalk from 'chalk';
+import * as dateformat from 'dateformat';
+import { envOption } from '../env';
+import config from '@/config/index';
+
+import * as SyslogPro from 'syslog-pro';
+
+type Domain = {
+	name: string;
+	color?: string;
+};
+
+type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
+
+export default class Logger {
+	private domain: Domain;
+	private parentLogger: Logger | null = null;
+	private store: boolean;
+	private syslogClient: any | null = null;
+
+	constructor(domain: string, color?: string, store = true) {
+		this.domain = {
+			name: domain,
+			color: color,
+		};
+		this.store = store;
+
+		if (config.syslog) {
+			this.syslogClient = new SyslogPro.RFC5424({
+				applacationName: 'Misskey',
+				timestamp: true,
+				encludeStructuredData: true,
+				color: true,
+				extendedColor: true,
+				server: {
+					target: config.syslog.host,
+					port: config.syslog.port,
+				}
+			});
+		}
+	}
+
+	public createSubLogger(domain: string, color?: string, store = true): Logger {
+		const logger = new Logger(domain, color, store);
+		logger.parentLogger = this;
+		return logger;
+	}
+
+	private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subDomains: Domain[] = [], store = true): void {
+		if (envOption.quiet) return;
+		if (!this.store) store = false;
+		if (level === 'debug') store = false;
+
+		if (this.parentLogger) {
+			this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store);
+			return;
+		}
+
+		const time = dateformat(new Date(), 'HH:MM:ss');
+		const worker = cluster.isPrimary ? '*' : cluster.worker.id;
+		const l =
+			level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
+			level === 'warning' ? chalk.yellow('WARN') :
+			level === 'success' ? important ? chalk.bgGreen.white('DONE') : chalk.green('DONE') :
+			level === 'debug' ? chalk.gray('VERB') :
+			level === 'info' ? chalk.blue('INFO') :
+			null;
+		const domains = [this.domain].concat(subDomains).map(d => d.color ? chalk.keyword(d.color)(d.name) : chalk.white(d.name));
+		const m =
+			level === 'error' ? chalk.red(message) :
+			level === 'warning' ? chalk.yellow(message) :
+			level === 'success' ? chalk.green(message) :
+			level === 'debug' ? chalk.gray(message) :
+			level === 'info' ? message :
+			null;
+
+		let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`;
+		if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
+
+		console.log(important ? chalk.bold(log) : log);
+
+		if (store) {
+			if (this.syslogClient) {
+				const send =
+					level === 'error' ? this.syslogClient.error :
+					level === 'warning' ? this.syslogClient.warning :
+					level === 'success' ? this.syslogClient.info :
+					level === 'debug' ? this.syslogClient.info :
+					level === 'info' ? this.syslogClient.info :
+					null as never;
+
+				send.bind(this.syslogClient)(message).catch(() => {});
+			}
+		}
+	}
+
+	public error(x: string | Error, data?: Record<string, any> | null, important = false): void { // 実行を継続できない状況で使う
+		if (x instanceof Error) {
+			data = data || {};
+			data.e = x;
+			this.log('error', x.toString(), data, important);
+		} else if (typeof x === 'object') {
+			this.log('error', `${(x as any).message || (x as any).name || x}`, data, important);
+		} else {
+			this.log('error', `${x}`, data, important);
+		}
+	}
+
+	public warn(message: string, data?: Record<string, any> | null, important = false): void { // 実行を継続できるが改善すべき状況で使う
+		this.log('warning', message, data, important);
+	}
+
+	public succ(message: string, data?: Record<string, any> | null, important = false): void { // 何かに成功した状況で使う
+		this.log('success', message, data, important);
+	}
+
+	public debug(message: string, data?: Record<string, any> | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
+		if (process.env.NODE_ENV != 'production' || envOption.verbose) {
+			this.log('debug', message, data, important);
+		}
+	}
+
+	public info(message: string, data?: Record<string, any> | null, important = false): void { // それ以外
+		this.log('info', message, data, important);
+	}
+}
diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts
new file mode 100644
index 0000000000..948b6726b9
--- /dev/null
+++ b/packages/backend/src/services/messages/create.ts
@@ -0,0 +1,108 @@
+import { User } from '@/models/entities/user';
+import { UserGroup } from '@/models/entities/user-group';
+import { DriveFile } from '@/models/entities/drive-file';
+import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream';
+import pushNotification from '../push-notification';
+import { Not } from 'typeorm';
+import { Note } from '@/models/entities/note';
+import renderNote from '@/remote/activitypub/renderer/note';
+import renderCreate from '@/remote/activitypub/renderer/create';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { deliver } from '@/queue/index';
+
+export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | undefined, file: DriveFile | null, uri?: string) {
+	const message = {
+		id: genId(),
+		createdAt: new Date(),
+		fileId: file ? file.id : null,
+		recipientId: recipientUser ? recipientUser.id : null,
+		groupId: recipientGroup ? recipientGroup.id : null,
+		text: text ? text.trim() : null,
+		userId: user.id,
+		isRead: false,
+		reads: [] as any[],
+		uri
+	} as MessagingMessage;
+
+	await MessagingMessages.insert(message);
+
+	const messageObj = await MessagingMessages.pack(message);
+
+	if (recipientUser) {
+		if (Users.isLocalUser(user)) {
+			// 自分のストリーム
+			publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj);
+			publishMessagingIndexStream(message.userId, 'message', messageObj);
+			publishMainStream(message.userId, 'messagingMessage', messageObj);
+		}
+
+		if (Users.isLocalUser(recipientUser)) {
+			// 相手のストリーム
+			publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj);
+			publishMessagingIndexStream(recipientUser.id, 'message', messageObj);
+			publishMainStream(recipientUser.id, 'messagingMessage', messageObj);
+		}
+	} else if (recipientGroup) {
+		// グループのストリーム
+		publishGroupMessagingStream(recipientGroup.id, 'message', messageObj);
+
+		// メンバーのストリーム
+		const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id });
+		for (const joining of joinings) {
+			publishMessagingIndexStream(joining.userId, 'message', messageObj);
+			publishMainStream(joining.userId, 'messagingMessage', messageObj);
+		}
+	}
+
+	// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
+	setTimeout(async () => {
+		const freshMessage = await MessagingMessages.findOne(message.id);
+		if (freshMessage == null) return; // メッセージが削除されている場合もある
+
+		if (recipientUser && Users.isLocalUser(recipientUser)) {
+			if (freshMessage.isRead) return; // 既読
+
+			//#region ただしミュートされているなら発行しない
+			const mute = await Mutings.find({
+				muterId: recipientUser.id,
+			});
+			if (mute.map(m => m.muteeId).includes(user.id)) return;
+			//#endregion
+
+			publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj);
+			pushNotification(recipientUser.id, 'unreadMessagingMessage', messageObj);
+		} else if (recipientGroup) {
+			const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id, userId: Not(user.id) });
+			for (const joining of joinings) {
+				if (freshMessage.reads.includes(joining.userId)) return; // 既読
+				publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj);
+				pushNotification(joining.userId, 'unreadMessagingMessage', messageObj);
+			}
+		}
+	}, 2000);
+
+	if (recipientUser && Users.isLocalUser(user) && Users.isRemoteUser(recipientUser)) {
+		const note = {
+			id: message.id,
+			createdAt: message.createdAt,
+			fileIds: message.fileId ? [ message.fileId ] : [],
+			text: message.text,
+			userId: message.userId,
+			visibility: 'specified',
+			mentions: [ recipientUser ].map(u => u.id),
+			mentionedRemoteUsers: JSON.stringify([ recipientUser ].map(u => ({
+				uri: u.uri,
+				username: u.username,
+				host: u.host
+			}))),
+		} as Note;
+
+		const activity = renderActivity(renderCreate(await renderNote(note, false, true), note));
+
+		deliver(user, activity, recipientUser.inbox);
+	}
+	return messageObj;
+}
diff --git a/packages/backend/src/services/messages/delete.ts b/packages/backend/src/services/messages/delete.ts
new file mode 100644
index 0000000000..5c299c9a50
--- /dev/null
+++ b/packages/backend/src/services/messages/delete.ts
@@ -0,0 +1,30 @@
+import config from '@/config/index';
+import { MessagingMessages, Users } from '@/models/index';
+import { MessagingMessage } from '@/models/entities/messaging-message';
+import { publishGroupMessagingStream, publishMessagingStream } from '@/services/stream';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderDelete from '@/remote/activitypub/renderer/delete';
+import renderTombstone from '@/remote/activitypub/renderer/tombstone';
+import { deliver } from '@/queue/index';
+
+export async function deleteMessage(message: MessagingMessage) {
+	await MessagingMessages.delete(message.id);
+	postDeleteMessage(message);
+}
+
+async function postDeleteMessage(message: MessagingMessage) {
+	if (message.recipientId) {
+		const user = await Users.findOneOrFail(message.userId);
+		const recipient = await Users.findOneOrFail(message.recipientId);
+
+		if (Users.isLocalUser(user)) publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id);
+		if (Users.isLocalUser(recipient)) publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
+
+		if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) {
+			const activity = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${message.id}`), user));
+			deliver(user, activity, recipient.inbox);
+		}
+	} else if (message.groupId) {
+		publishGroupMessagingStream(message.groupId, 'deleted', message.id);
+	}
+}
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
new file mode 100644
index 0000000000..69d854ab1a
--- /dev/null
+++ b/packages/backend/src/services/note/create.ts
@@ -0,0 +1,645 @@
+import * as mfm from 'mfm-js';
+import es from '../../db/elasticsearch';
+import { publishMainStream, publishNotesStream } from '@/services/stream';
+import DeliverManager from '@/remote/activitypub/deliver-manager';
+import renderNote from '@/remote/activitypub/renderer/note';
+import renderCreate from '@/remote/activitypub/renderer/create';
+import renderAnnounce from '@/remote/activitypub/renderer/announce';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { resolveUser } from '@/remote/resolve-user';
+import config from '@/config/index';
+import { updateHashtags } from '../update-hashtag';
+import { concat } from '@/prelude/array';
+import { insertNoteUnread } from '@/services/note/unread';
+import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
+import { extractMentions } from '@/misc/extract-mentions';
+import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm';
+import { extractHashtags } from '@/misc/extract-hashtags';
+import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
+import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings, NoteThreadMutings } from '@/models/index';
+import { DriveFile } from '@/models/entities/drive-file';
+import { App } from '@/models/entities/app';
+import { Not, getConnection, In } from 'typeorm';
+import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
+import { genId } from '@/misc/gen-id';
+import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '@/services/chart/index';
+import { Poll, IPoll } from '@/models/entities/poll';
+import { createNotification } from '../create-notification';
+import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
+import { checkHitAntenna } from '@/misc/check-hit-antenna';
+import { checkWordMute } from '@/misc/check-word-mute';
+import { addNoteToAntenna } from '../add-note-to-antenna';
+import { countSameRenotes } from '@/misc/count-same-renotes';
+import { deliverToRelays } from '../relay';
+import { Channel } from '@/models/entities/channel';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+import { getAntennas } from '@/misc/antenna-cache';
+
+type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
+
+class NotificationManager {
+	private notifier: { id: User['id']; };
+	private note: Note;
+	private queue: {
+		target: ILocalUser['id'];
+		reason: NotificationType;
+	}[];
+
+	constructor(notifier: { id: User['id']; }, note: Note) {
+		this.notifier = notifier;
+		this.note = note;
+		this.queue = [];
+	}
+
+	public push(notifiee: ILocalUser['id'], reason: NotificationType) {
+		// 自分自身へは通知しない
+		if (this.notifier.id === notifiee) return;
+
+		const exist = this.queue.find(x => x.target === notifiee);
+
+		if (exist) {
+			// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
+			if (reason != 'mention') {
+				exist.reason = reason;
+			}
+		} else {
+			this.queue.push({
+				reason: reason,
+				target: notifiee
+			});
+		}
+	}
+
+	public async deliver() {
+		for (const x of this.queue) {
+			// ミュート情報を取得
+			const mentioneeMutes = await Mutings.find({
+				muterId: x.target
+			});
+
+			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId);
+
+			// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
+			if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
+				createNotification(x.target, x.reason, {
+					notifierId: this.notifier.id,
+					noteId: this.note.id
+				});
+			}
+		}
+	}
+}
+
+type Option = {
+	createdAt?: Date | null;
+	name?: string | null;
+	text?: string | null;
+	reply?: Note | null;
+	renote?: Note | null;
+	files?: DriveFile[] | null;
+	poll?: IPoll | null;
+	viaMobile?: boolean | null;
+	localOnly?: boolean | null;
+	cw?: string | null;
+	visibility?: string;
+	visibleUsers?: User[] | null;
+	channel?: Channel | null;
+	apMentions?: User[] | null;
+	apHashtags?: string[] | null;
+	apEmojis?: string[] | null;
+	uri?: string | null;
+	url?: string | null;
+	app?: App | null;
+};
+
+export default async (user: { id: User['id']; username: User['username']; host: User['host']; isSilenced: User['isSilenced']; }, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
+	// チャンネル外にリプライしたら対象のスコープに合わせる
+	// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
+	if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
+		if (data.reply.channelId) {
+			data.channel = await Channels.findOne(data.reply.channelId);
+		} else {
+			data.channel = null;
+		}
+	}
+
+	// チャンネル内にリプライしたら対象のスコープに合わせる
+	// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
+	if (data.reply && (data.channel == null) && data.reply.channelId) {
+		data.channel = await Channels.findOne(data.reply.channelId);
+	}
+
+	if (data.createdAt == null) data.createdAt = new Date();
+	if (data.visibility == null) data.visibility = 'public';
+	if (data.viaMobile == null) data.viaMobile = false;
+	if (data.localOnly == null) data.localOnly = false;
+	if (data.channel != null) data.visibility = 'public';
+	if (data.channel != null) data.visibleUsers = [];
+	if (data.channel != null) data.localOnly = true;
+
+	// サイレンス
+	if (user.isSilenced && data.visibility === 'public' && data.channel == null) {
+		data.visibility = 'home';
+	}
+
+	// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
+	if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) {
+		return rej('Renote target is not public or home');
+	}
+
+	// Renote対象がpublicではないならhomeにする
+	if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
+		data.visibility = 'home';
+	}
+
+	// Renote対象がfollowersならfollowersにする
+	if (data.renote && data.renote.visibility === 'followers') {
+		data.visibility = 'followers';
+	}
+
+	// 返信対象がpublicではないならhomeにする
+	if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
+		data.visibility = 'home';
+	}
+
+	// ローカルのみをRenoteしたらローカルのみにする
+	if (data.renote && data.renote.localOnly && data.channel == null) {
+		data.localOnly = true;
+	}
+
+	// ローカルのみにリプライしたらローカルのみにする
+	if (data.reply && data.reply.localOnly && data.channel == null) {
+		data.localOnly = true;
+	}
+
+	if (data.text) {
+		data.text = data.text.trim();
+	}
+
+	let tags = data.apHashtags;
+	let emojis = data.apEmojis;
+	let mentionedUsers = data.apMentions;
+
+	// Parse MFM if needed
+	if (!tags || !emojis || !mentionedUsers) {
+		const tokens = data.text ? mfm.parse(data.text)! : [];
+		const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
+		const choiceTokens = data.poll && data.poll.choices
+			? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
+			: [];
+
+		const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
+
+		tags = data.apHashtags || extractHashtags(combinedTokens);
+
+		emojis = data.apEmojis || extractCustomEmojisFromMfm(combinedTokens);
+
+		mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
+	}
+
+	tags = tags.filter(tag => Array.from(tag || '').length <= 128).splice(0, 32);
+
+	if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
+		mentionedUsers.push(await Users.findOneOrFail(data.reply.userId));
+	}
+
+	if (data.visibility == 'specified') {
+		if (data.visibleUsers == null) throw new Error('invalid param');
+
+		for (const u of data.visibleUsers) {
+			if (!mentionedUsers.some(x => x.id === u.id)) {
+				mentionedUsers.push(u);
+			}
+		}
+
+		if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) {
+			data.visibleUsers.push(await Users.findOneOrFail(data.reply.userId));
+		}
+	}
+
+	const note = await insertNote(user, data, tags, emojis, mentionedUsers);
+
+	res(note);
+
+	// 統計を更新
+	notesChart.update(note, true);
+	perUserNotesChart.update(user, note, true);
+
+	// Register host
+	if (Users.isRemoteUser(user)) {
+		registerOrFetchInstanceDoc(user.host).then(i => {
+			Instances.increment({ id: i.id }, 'notesCount', 1);
+			instanceChart.updateNote(i.host, note, true);
+		});
+	}
+
+	// ハッシュタグ更新
+	if (data.visibility === 'public' || data.visibility === 'home') {
+		updateHashtags(user, tags);
+	}
+
+	// Increment notes count (user)
+	incNotesCountOfUser(user);
+
+	// Word mute
+	// TODO: cache
+	UserProfiles.find({
+		enableWordMute: true
+	}).then(us => {
+		for (const u of us) {
+			checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
+				if (shouldMute) {
+					MutedNotes.insert({
+						id: genId(),
+						userId: u.userId,
+						noteId: note.id,
+						reason: 'word',
+					});
+				}
+			});
+		}
+	});
+
+	// Antenna
+	Followings.createQueryBuilder('following')
+		.andWhere(`following.followeeId = :userId`, { userId: note.userId })
+		.getMany()
+		.then(async followings => {
+			const blockings = await Blockings.find({ blockerId: user.id }); // TODO: キャッシュしたい
+			const followers = followings.map(f => f.followerId);
+			for (const antenna of (await getAntennas())) {
+				if (blockings.some(blocking => blocking.blockeeId === antenna.userId)) continue; // この処理は checkHitAntenna 内でやるようにしてもいいかも
+				checkHitAntenna(antenna, note, user, followers).then(hit => {
+					if (hit) {
+						addNoteToAntenna(antenna, note, user);
+					}
+				});
+			}
+		});
+
+	// Channel
+	if (note.channelId) {
+		ChannelFollowings.find({ followeeId: note.channelId }).then(followings => {
+			for (const following of followings) {
+				insertNoteUnread(following.followerId, note, {
+					isSpecified: false,
+					isMentioned: false,
+				});
+			}
+		});
+	}
+
+	if (data.reply) {
+		saveReply(data.reply, note);
+	}
+
+	// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
+	if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
+		incRenoteCount(data.renote);
+	}
+
+	if (!silent) {
+		// ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい
+		if (Users.isRemoteUser(user)) activeUsersChart.update(user);
+
+		// 未読通知を作成
+		if (data.visibility == 'specified') {
+			if (data.visibleUsers == null) throw new Error('invalid param');
+
+			for (const u of data.visibleUsers) {
+				// ローカルユーザーのみ
+				if (!Users.isLocalUser(u)) continue;
+
+				insertNoteUnread(u.id, note, {
+					isSpecified: true,
+					isMentioned: false,
+				});
+			}
+		} else {
+			for (const u of mentionedUsers) {
+				// ローカルユーザーのみ
+				if (!Users.isLocalUser(u)) continue;
+
+				insertNoteUnread(u.id, note, {
+					isSpecified: false,
+					isMentioned: true,
+				});
+			}
+		}
+
+		// Pack the note
+		const noteObj = await Notes.pack(note);
+
+		publishNotesStream(noteObj);
+
+		const nm = new NotificationManager(user, note);
+		const nmRelatedPromises = [];
+
+		await createMentionedEvents(mentionedUsers, note, nm);
+
+		// If has in reply to note
+		if (data.reply) {
+			// Fetch watchers
+			nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
+
+			// 通知
+			if (data.reply.userHost === null) {
+				const threadMuted = await NoteThreadMutings.findOne({
+					userId: data.reply.userId,
+					threadId: data.reply.threadId || data.reply.id,
+				});
+
+				if (!threadMuted) {
+					nm.push(data.reply.userId, 'reply');
+					publishMainStream(data.reply.userId, 'reply', noteObj);
+				}
+			}
+		}
+
+		// If it is renote
+		if (data.renote) {
+			const type = data.text ? 'quote' : 'renote';
+
+			// Notify
+			if (data.renote.userHost === null) {
+				nm.push(data.renote.userId, type);
+			}
+
+			// Fetch watchers
+			nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type));
+
+			// Publish event
+			if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
+				publishMainStream(data.renote.userId, 'renote', noteObj);
+			}
+		}
+
+		Promise.all(nmRelatedPromises).then(() => {
+			nm.deliver();
+		});
+
+		//#region AP deliver
+		if (Users.isLocalUser(user)) {
+			(async () => {
+				const noteActivity = await renderNoteOrRenoteActivity(data, note);
+				const dm = new DeliverManager(user, noteActivity);
+
+				// メンションされたリモートユーザーに配送
+				for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) {
+					dm.addDirectRecipe(u as IRemoteUser);
+				}
+
+				// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
+				if (data.reply && data.reply.userHost !== null) {
+					const u = await Users.findOne(data.reply.userId);
+					if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
+				}
+
+				// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
+				if (data.renote && data.renote.userHost !== null) {
+					const u = await Users.findOne(data.renote.userId);
+					if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
+				}
+
+				// フォロワーに配送
+				if (['public', 'home', 'followers'].includes(note.visibility)) {
+					dm.addFollowersRecipe();
+				}
+
+				if (['public'].includes(note.visibility)) {
+					deliverToRelays(user, noteActivity);
+				}
+
+				dm.execute();
+			})();
+		}
+		//#endregion
+	}
+
+	if (data.channel) {
+		Channels.increment({ id: data.channel.id }, 'notesCount', 1);
+		Channels.update(data.channel.id, {
+			lastNotedAt: new Date(),
+		});
+
+		Notes.count({
+			userId: user.id,
+			channelId: data.channel.id,
+		}).then(count => {
+			// この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる
+			// TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい
+			if (count === 1) {
+				Channels.increment({ id: data.channel!.id }, 'usersCount', 1);
+			}
+		});
+	}
+
+	// Register to search database
+	index(note);
+});
+
+async function renderNoteOrRenoteActivity(data: Option, note: Note) {
+	if (data.localOnly) return null;
+
+	const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
+		? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note)
+		: renderCreate(await renderNote(note, false), note);
+
+	return renderActivity(content);
+}
+
+function incRenoteCount(renote: Note) {
+	Notes.createQueryBuilder().update()
+		.set({
+			renoteCount: () => '"renoteCount" + 1',
+			score: () => '"score" + 1'
+		})
+		.where('id = :id', { id: renote.id })
+		.execute();
+}
+
+async function insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) {
+	const insert = new Note({
+		id: genId(data.createdAt!),
+		createdAt: data.createdAt!,
+		fileIds: data.files ? data.files.map(file => file.id) : [],
+		replyId: data.reply ? data.reply.id : null,
+		renoteId: data.renote ? data.renote.id : null,
+		channelId: data.channel ? data.channel.id : null,
+		threadId: data.reply
+			? data.reply.threadId
+				? data.reply.threadId
+				: data.reply.id
+			: null,
+		name: data.name,
+		text: data.text,
+		hasPoll: data.poll != null,
+		cw: data.cw == null ? null : data.cw,
+		tags: tags.map(tag => normalizeForSearch(tag)),
+		emojis,
+		userId: user.id,
+		viaMobile: data.viaMobile!,
+		localOnly: data.localOnly!,
+		visibility: data.visibility as any,
+		visibleUserIds: data.visibility == 'specified'
+			? data.visibleUsers
+				? data.visibleUsers.map(u => u.id)
+				: []
+			: [],
+
+		attachedFileTypes: data.files ? data.files.map(file => file.type) : [],
+
+		// 以下非正規化データ
+		replyUserId: data.reply ? data.reply.userId : null,
+		replyUserHost: data.reply ? data.reply.userHost : null,
+		renoteUserId: data.renote ? data.renote.userId : null,
+		renoteUserHost: data.renote ? data.renote.userHost : null,
+		userHost: user.host,
+	});
+
+	if (data.uri != null) insert.uri = data.uri;
+	if (data.url != null) insert.url = data.url;
+
+	// Append mentions data
+	if (mentionedUsers.length > 0) {
+		insert.mentions = mentionedUsers.map(u => u.id);
+		const profiles = await UserProfiles.find({ userId: In(insert.mentions) });
+		insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => {
+			const profile = profiles.find(p => p.userId == u.id);
+			const url = profile != null ? profile.url : null;
+			return {
+				uri: u.uri,
+				url: url == null ? undefined : url,
+				username: u.username,
+				host: u.host
+			} as IMentionedRemoteUsers[0];
+		}));
+	}
+
+	// 投稿を作成
+	try {
+		if (insert.hasPoll) {
+			// Start transaction
+			await getConnection().transaction(async transactionalEntityManager => {
+				await transactionalEntityManager.insert(Note, insert);
+
+				const poll = new Poll({
+					noteId: insert.id,
+					choices: data.poll!.choices,
+					expiresAt: data.poll!.expiresAt,
+					multiple: data.poll!.multiple,
+					votes: new Array(data.poll!.choices.length).fill(0),
+					noteVisibility: insert.visibility,
+					userId: user.id,
+					userHost: user.host
+				});
+
+				await transactionalEntityManager.insert(Poll, poll);
+			});
+		} else {
+			await Notes.insert(insert);
+		}
+
+		return insert;
+	} catch (e) {
+		// duplicate key error
+		if (isDuplicateKeyValueError(e)) {
+			const err = new Error('Duplicated note');
+			err.name = 'duplicated';
+			throw err;
+		}
+
+		console.error(e);
+
+		throw e;
+	}
+}
+
+function index(note: Note) {
+	if (note.text == null || config.elasticsearch == null) return;
+
+	es!.index({
+		index: config.elasticsearch.index || 'misskey_note',
+		id: note.id.toString(),
+		body: {
+			text: normalizeForSearch(note.text),
+			userId: note.userId,
+			userHost: note.userHost
+		}
+	});
+}
+
+async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType) {
+	const watchers = await NoteWatchings.find({
+		noteId: renote.id,
+		userId: Not(user.id)
+	});
+
+	for (const watcher of watchers) {
+		nm.push(watcher.userId, type);
+	}
+}
+
+async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, nm: NotificationManager) {
+	const watchers = await NoteWatchings.find({
+		noteId: reply.id,
+		userId: Not(user.id)
+	});
+
+	for (const watcher of watchers) {
+		nm.push(watcher.userId, 'reply');
+	}
+}
+
+async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) {
+	for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) {
+		const threadMuted = await NoteThreadMutings.findOne({
+			userId: u.id,
+			threadId: note.threadId || note.id,
+		});
+
+		if (threadMuted) {
+			continue;
+		}
+
+		const detailPackedNote = await Notes.pack(note, u, {
+			detail: true
+		});
+
+		publishMainStream(u.id, 'mention', detailPackedNote);
+
+		// Create notification
+		nm.push(u.id, 'mention');
+	}
+}
+
+function saveReply(reply: Note, note: Note) {
+	Notes.increment({ id: reply.id }, 'repliesCount', 1);
+}
+
+function incNotesCountOfUser(user: { id: User['id']; }) {
+	Users.createQueryBuilder().update()
+		.set({
+			updatedAt: new Date(),
+			notesCount: () => '"notesCount" + 1'
+		})
+		.where('id = :id', { id: user.id })
+		.execute();
+}
+
+async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise<User[]> {
+	if (tokens == null) return [];
+
+	const mentions = extractMentions(tokens);
+
+	let mentionedUsers = (await Promise.all(mentions.map(m =>
+		resolveUser(m.username, m.host || user.host).catch(() => null)
+	))).filter(x => x != null) as User[];
+
+	// Drop duplicate users
+	mentionedUsers = mentionedUsers.filter((u, i, self) =>
+		i === self.findIndex(u2 => u.id === u2.id)
+	);
+
+	return mentionedUsers;
+}
diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts
new file mode 100644
index 0000000000..a14d84e7b2
--- /dev/null
+++ b/packages/backend/src/services/note/delete.ts
@@ -0,0 +1,137 @@
+import { publishNoteStream } from '@/services/stream';
+import renderDelete from '@/remote/activitypub/renderer/delete';
+import renderAnnounce from '@/remote/activitypub/renderer/announce';
+import renderUndo from '@/remote/activitypub/renderer/undo';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderTombstone from '@/remote/activitypub/renderer/tombstone';
+import config from '@/config/index';
+import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
+import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
+import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
+import { Notes, Users, Instances } from '@/models/index';
+import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index';
+import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager';
+import { countSameRenotes } from '@/misc/count-same-renotes';
+import { deliverToRelays } from '../relay';
+import { Brackets, In } from 'typeorm';
+
+/**
+ * 投稿を削除します。
+ * @param user 投稿者
+ * @param note 投稿
+ */
+export default async function(user: User, note: Note, quiet = false) {
+	const deletedAt = new Date();
+
+	// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
+	if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
+		Notes.decrement({ id: note.renoteId }, 'renoteCount', 1);
+		Notes.decrement({ id: note.renoteId }, 'score', 1);
+	}
+
+	if (!quiet) {
+		publishNoteStream(note.id, 'deleted', {
+			deletedAt: deletedAt
+		});
+
+		//#region ローカルの投稿なら削除アクティビティを配送
+		if (Users.isLocalUser(user) && !note.localOnly) {
+			let renote: Note | undefined;
+
+			// if deletd note is renote
+			if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) {
+				renote = await Notes.findOne({
+					id: note.renoteId
+				});
+			}
+
+			const content = renderActivity(renote
+				? renderUndo(renderAnnounce(renote.uri || `${config.url}/notes/${renote.id}`, note), user)
+				: renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user));
+
+			deliverToConcerned(user, note, content);
+		}
+
+		// also deliever delete activity to cascaded notes
+		const cascadingNotes = (await findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes
+		for (const cascadingNote of cascadingNotes) {
+			if (!cascadingNote.user) continue;
+			if (!Users.isLocalUser(cascadingNote.user)) continue;
+			const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
+			deliverToConcerned(cascadingNote.user, cascadingNote, content);
+		}
+		//#endregion
+
+		// 統計を更新
+		notesChart.update(note, false);
+		perUserNotesChart.update(user, note, false);
+
+		if (Users.isRemoteUser(user)) {
+			registerOrFetchInstanceDoc(user.host).then(i => {
+				Instances.decrement({ id: i.id }, 'notesCount', 1);
+				instanceChart.updateNote(i.host, note, false);
+			});
+		}
+	}
+
+	await Notes.delete({
+		id: note.id,
+		userId: user.id
+	});
+}
+
+async function findCascadingNotes(note: Note) {
+	const cascadingNotes: Note[] = [];
+
+	const recursive = async (noteId: string) => {
+		const query = Notes.createQueryBuilder('note')
+			.where('note.replyId = :noteId', { noteId })
+			.orWhere(new Brackets(q => {
+				q.where('note.renoteId = :noteId', { noteId })
+				.andWhere('note.text IS NOT NULL');
+			}))
+			.leftJoinAndSelect('note.user', 'user');
+		const replies = await query.getMany();
+		for (const reply of replies) {
+			cascadingNotes.push(reply);
+			await recursive(reply.id);
+		}
+	};
+	await recursive(note.id);
+
+	return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
+}
+
+async function getMentionedRemoteUsers(note: Note) {
+	const where = [] as any[];
+
+	// mention / reply / dm
+	const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
+	if (uris.length > 0) {
+		where.push(
+			{ uri: In(uris) }
+		);
+	}
+
+	// renote / quote
+	if (note.renoteUserId) {
+		where.push({
+			id: note.renoteUserId
+		});
+	}
+
+	if (where.length === 0) return [];
+
+	return await Users.find({
+		where
+	}) as IRemoteUser[];
+}
+
+async function deliverToConcerned(user: ILocalUser, note: Note, content: any) {
+	deliverToFollowers(user, content);
+	deliverToRelays(user, content);
+	const remoteUsers = await getMentionedRemoteUsers(note);
+	for (const remoteUser of remoteUsers) {
+		deliverToUser(user, content, remoteUser);
+	}
+}
diff --git a/packages/backend/src/services/note/polls/update.ts b/packages/backend/src/services/note/polls/update.ts
new file mode 100644
index 0000000000..a22ce8e373
--- /dev/null
+++ b/packages/backend/src/services/note/polls/update.ts
@@ -0,0 +1,22 @@
+import renderUpdate from '@/remote/activitypub/renderer/update';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import renderNote from '@/remote/activitypub/renderer/note';
+import { Users, Notes } from '@/models/index';
+import { Note } from '@/models/entities/note';
+import { deliverToFollowers } from '@/remote/activitypub/deliver-manager';
+import { deliverToRelays } from '../../relay';
+
+export async function deliverQuestionUpdate(noteId: Note['id']) {
+	const note = await Notes.findOne(noteId);
+	if (note == null) throw new Error('note not found');
+
+	const user = await Users.findOne(note.userId);
+	if (user == null) throw new Error('note not found');
+
+	if (Users.isLocalUser(user)) {
+
+		const content = renderActivity(renderUpdate(await renderNote(note, false), user));
+		deliverToFollowers(user, content);
+		deliverToRelays(user, content);
+	}
+}
diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts
new file mode 100644
index 0000000000..886a09dde9
--- /dev/null
+++ b/packages/backend/src/services/note/polls/vote.ts
@@ -0,0 +1,81 @@
+import { publishNoteStream } from '@/services/stream';
+import { User } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { PollVotes, NoteWatchings, Polls, Blockings } from '@/models/index';
+import { Not } from 'typeorm';
+import { genId } from '@/misc/gen-id';
+import { createNotification } from '../../create-notification';
+
+export default async function(user: User, note: Note, choice: number) {
+	const poll = await Polls.findOne(note.id);
+
+	if (poll == null) throw new Error('poll not found');
+
+	// Check whether is valid choice
+	if (poll.choices[choice] == null) throw new Error('invalid choice param');
+
+	// Check blocking
+	if (note.userId !== user.id) {
+		const block = await Blockings.findOne({
+			blockerId: note.userId,
+			blockeeId: user.id,
+		});
+		if (block) {
+			throw new Error('blocked');
+		}
+	}
+
+	// if already voted
+	const exist = await PollVotes.find({
+		noteId: note.id,
+		userId: user.id
+	});
+
+	if (poll.multiple) {
+		if (exist.some(x => x.choice === choice)) {
+			throw new Error('already voted');
+		}
+	} else if (exist.length !== 0) {
+		throw new Error('already voted');
+	}
+
+	// Create vote
+	await PollVotes.insert({
+		id: genId(),
+		createdAt: new Date(),
+		noteId: note.id,
+		userId: user.id,
+		choice: choice
+	});
+
+	// Increment votes count
+	const index = choice + 1; // In SQL, array index is 1 based
+	await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
+
+	publishNoteStream(note.id, 'pollVoted', {
+		choice: choice,
+		userId: user.id
+	});
+
+	// Notify
+	createNotification(note.userId, 'pollVote', {
+		notifierId: user.id,
+		noteId: note.id,
+		choice: choice
+	});
+
+	// Fetch watchers
+	NoteWatchings.find({
+		noteId: note.id,
+		userId: Not(user.id),
+	})
+	.then(watchers => {
+		for (const watcher of watchers) {
+			createNotification(watcher.userId, 'pollVote', {
+				notifierId: user.id,
+				noteId: note.id,
+				choice: choice
+			});
+		}
+	});
+}
diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts
new file mode 100644
index 0000000000..308bd4dff7
--- /dev/null
+++ b/packages/backend/src/services/note/reaction/create.ts
@@ -0,0 +1,135 @@
+import { publishNoteStream } from '@/services/stream';
+import { renderLike } from '@/remote/activitypub/renderer/like';
+import DeliverManager from '@/remote/activitypub/deliver-manager';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { toDbReaction, decodeReaction } from '@/misc/reaction-lib';
+import { User, IRemoteUser } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { NoteReactions, Users, NoteWatchings, Notes, Emojis, Blockings } from '@/models/index';
+import { Not } from 'typeorm';
+import { perUserReactionsChart } from '@/services/chart/index';
+import { genId } from '@/misc/gen-id';
+import { createNotification } from '../../create-notification';
+import deleteReaction from './delete';
+import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
+import { NoteReaction } from '@/models/entities/note-reaction';
+import { IdentifiableError } from '@/misc/identifiable-error';
+
+export default async (user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string) => {
+	// Check blocking
+	if (note.userId !== user.id) {
+		const block = await Blockings.findOne({
+			blockerId: note.userId,
+			blockeeId: user.id,
+		});
+		if (block) {
+			throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
+		}
+	}
+
+	// TODO: cache
+	reaction = await toDbReaction(reaction, user.host);
+
+	const record: NoteReaction = {
+		id: genId(),
+		createdAt: new Date(),
+		noteId: note.id,
+		userId: user.id,
+		reaction
+	};
+
+	// Create reaction
+	try {
+		await NoteReactions.insert(record);
+	} catch (e) {
+		if (isDuplicateKeyValueError(e)) {
+			const exists = await NoteReactions.findOneOrFail({
+				noteId: note.id,
+				userId: user.id,
+			});
+
+			if (exists.reaction !== reaction) {
+				// 別のリアクションがすでにされていたら置き換える
+				await deleteReaction(user, note);
+				await NoteReactions.insert(record);
+			} else {
+				// 同じリアクションがすでにされていたらエラー
+				throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
+			}
+		} else {
+			throw e;
+		}
+	}
+
+	// Increment reactions count
+	const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
+	await Notes.createQueryBuilder().update()
+		.set({
+			reactions: () => sql,
+			score: () => '"score" + 1'
+		})
+		.where('id = :id', { id: note.id })
+		.execute();
+
+	perUserReactionsChart.update(user, note);
+
+	// カスタム絵文字リアクションだったら絵文字情報も送る
+	const decodedReaction = decodeReaction(reaction);
+
+	let emoji = await Emojis.findOne({
+		where: {
+			name: decodedReaction.name,
+			host: decodedReaction.host
+		},
+		select: ['name', 'host', 'url']
+	});
+
+	if (emoji) {
+		emoji = {
+			name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
+			url: emoji.url
+		} as any;
+	}
+
+	publishNoteStream(note.id, 'reacted', {
+		reaction: decodedReaction.reaction,
+		emoji: emoji,
+		userId: user.id
+	});
+
+	// リアクションされたユーザーがローカルユーザーなら通知を作成
+	if (note.userHost === null) {
+		createNotification(note.userId, 'reaction', {
+			notifierId: user.id,
+			noteId: note.id,
+			reaction: reaction
+		});
+	}
+
+	// Fetch watchers
+	NoteWatchings.find({
+		noteId: note.id,
+		userId: Not(user.id)
+	}).then(watchers => {
+		for (const watcher of watchers) {
+			createNotification(watcher.userId, 'reaction', {
+				notifierId: user.id,
+				noteId: note.id,
+				reaction: reaction
+			});
+		}
+	});
+
+	//#region 配信
+	if (Users.isLocalUser(user) && !note.localOnly) {
+		const content = renderActivity(await renderLike(record, note));
+		const dm = new DeliverManager(user, content);
+		if (note.userHost !== null) {
+			const reactee = await Users.findOne(note.userId);
+			dm.addDirectRecipe(reactee as IRemoteUser);
+		}
+		dm.addFollowersRecipe();
+		dm.execute();
+	}
+	//#endregion
+};
diff --git a/packages/backend/src/services/note/reaction/delete.ts b/packages/backend/src/services/note/reaction/delete.ts
new file mode 100644
index 0000000000..062dbad6f1
--- /dev/null
+++ b/packages/backend/src/services/note/reaction/delete.ts
@@ -0,0 +1,58 @@
+import { publishNoteStream } from '@/services/stream';
+import { renderLike } from '@/remote/activitypub/renderer/like';
+import renderUndo from '@/remote/activitypub/renderer/undo';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import DeliverManager from '@/remote/activitypub/deliver-manager';
+import { IdentifiableError } from '@/misc/identifiable-error';
+import { User, IRemoteUser } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { NoteReactions, Users, Notes } from '@/models/index';
+import { decodeReaction } from '@/misc/reaction-lib';
+
+export default async (user: { id: User['id']; host: User['host']; }, note: Note) => {
+	// if already unreacted
+	const exist = await NoteReactions.findOne({
+		noteId: note.id,
+		userId: user.id,
+	});
+
+	if (exist == null) {
+		throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
+	}
+
+	// Delete reaction
+	const result = await NoteReactions.delete(exist.id);
+
+	if (result.affected !== 1) {
+		throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
+	}
+
+	// Decrement reactions count
+	const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
+	await Notes.createQueryBuilder().update()
+		.set({
+			reactions: () => sql,
+		})
+		.where('id = :id', { id: note.id })
+		.execute();
+
+	Notes.decrement({ id: note.id }, 'score', 1);
+
+	publishNoteStream(note.id, 'unreacted', {
+		reaction: decodeReaction(exist.reaction).reaction,
+		userId: user.id
+	});
+
+	//#region 配信
+	if (Users.isLocalUser(user) && !note.localOnly) {
+		const content = renderActivity(renderUndo(await renderLike(exist, note), user));
+		const dm = new DeliverManager(user, content);
+		if (note.userHost !== null) {
+			const reactee = await Users.findOne(note.userId);
+			dm.addDirectRecipe(reactee as IRemoteUser);
+		}
+		dm.addFollowersRecipe();
+		dm.execute();
+	}
+	//#endregion
+};
diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts
new file mode 100644
index 0000000000..f25f86da9c
--- /dev/null
+++ b/packages/backend/src/services/note/read.ts
@@ -0,0 +1,132 @@
+import { publishMainStream } from '@/services/stream';
+import { Note } from '@/models/entities/note';
+import { User } from '@/models/entities/user';
+import { NoteUnreads, AntennaNotes, Users, Followings, ChannelFollowings } from '@/models/index';
+import { Not, IsNull, In } from 'typeorm';
+import { Channel } from '@/models/entities/channel';
+import { checkHitAntenna } from '@/misc/check-hit-antenna';
+import { getAntennas } from '@/misc/antenna-cache';
+import { readNotificationByQuery } from '@/server/api/common/read-notification';
+import { Packed } from '@/misc/schema';
+
+/**
+ * Mark notes as read
+ */
+export default async function(
+	userId: User['id'],
+	notes: (Note | Packed<'Note'>)[],
+	info?: {
+		following: Set<User['id']>;
+		followingChannels: Set<Channel['id']>;
+	}
+) {
+	const following = info?.following ? info.following : new Set<string>((await Followings.find({
+		where: {
+			followerId: userId
+		},
+		select: ['followeeId']
+	})).map(x => x.followeeId));
+	const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await ChannelFollowings.find({
+		where: {
+			followerId: userId
+		},
+		select: ['followeeId']
+	})).map(x => x.followeeId));
+
+	const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
+	const readMentions: (Note | Packed<'Note'>)[] = [];
+	const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
+	const readChannelNotes: (Note | Packed<'Note'>)[] = [];
+	const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
+
+	for (const note of notes) {
+		if (note.mentions && note.mentions.includes(userId)) {
+			readMentions.push(note);
+		} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
+			readSpecifiedNotes.push(note);
+		}
+
+		if (note.channelId && followingChannels.has(note.channelId)) {
+			readChannelNotes.push(note);
+		}
+
+		if (note.user != null) { // たぶんnullになることは無いはずだけど一応
+			for (const antenna of myAntennas) {
+				if (await checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
+					readAntennaNotes.push(note);
+				}
+			}
+		}
+	}
+
+	if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
+		// Remove the record
+		await NoteUnreads.delete({
+			userId: userId,
+			noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
+		});
+
+		// TODO: ↓まとめてクエリしたい
+
+		NoteUnreads.count({
+			userId: userId,
+			isMentioned: true
+		}).then(mentionsCount => {
+			if (mentionsCount === 0) {
+				// 全て既読になったイベントを発行
+				publishMainStream(userId, 'readAllUnreadMentions');
+			}
+		});
+
+		NoteUnreads.count({
+			userId: userId,
+			isSpecified: true
+		}).then(specifiedCount => {
+			if (specifiedCount === 0) {
+				// 全て既読になったイベントを発行
+				publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
+			}
+		});
+
+		NoteUnreads.count({
+			userId: userId,
+			noteChannelId: Not(IsNull())
+		}).then(channelNoteCount => {
+			if (channelNoteCount === 0) {
+				// 全て既読になったイベントを発行
+				publishMainStream(userId, 'readAllChannels');
+			}
+		});
+
+		readNotificationByQuery(userId, {
+			noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
+		});
+	}
+
+	if (readAntennaNotes.length > 0) {
+		await AntennaNotes.update({
+			antennaId: In(myAntennas.map(a => a.id)),
+			noteId: In(readAntennaNotes.map(n => n.id))
+		}, {
+			read: true
+		});
+
+		// TODO: まとめてクエリしたい
+		for (const antenna of myAntennas) {
+			const count = await AntennaNotes.count({
+				antennaId: antenna.id,
+				read: false
+			});
+
+			if (count === 0) {
+				publishMainStream(userId, 'readAntenna', antenna);
+			}
+		}
+
+		Users.getHasUnreadAntenna(userId).then(unread => {
+			if (!unread) {
+				publishMainStream(userId, 'readAllAntennas');
+			}
+		});
+	}
+}
diff --git a/packages/backend/src/services/note/unread.ts b/packages/backend/src/services/note/unread.ts
new file mode 100644
index 0000000000..29d2b54af8
--- /dev/null
+++ b/packages/backend/src/services/note/unread.ts
@@ -0,0 +1,55 @@
+import { Note } from '@/models/entities/note';
+import { publishMainStream } from '@/services/stream';
+import { User } from '@/models/entities/user';
+import { Mutings, NoteThreadMutings, NoteUnreads } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+export async function insertNoteUnread(userId: User['id'], note: Note, params: {
+	// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
+	isSpecified: boolean;
+	isMentioned: boolean;
+}) {
+	//#region ミュートしているなら無視
+	// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
+	const mute = await Mutings.find({
+		muterId: userId
+	});
+	if (mute.map(m => m.muteeId).includes(note.userId)) return;
+	//#endregion
+
+	// スレッドミュート
+	const threadMute = await NoteThreadMutings.findOne({
+		userId: userId,
+		threadId: note.threadId || note.id,
+	});
+	if (threadMute) return;
+
+	const unread = {
+		id: genId(),
+		noteId: note.id,
+		userId: userId,
+		isSpecified: params.isSpecified,
+		isMentioned: params.isMentioned,
+		noteChannelId: note.channelId,
+		noteUserId: note.userId,
+	};
+
+	await NoteUnreads.insert(unread);
+
+	// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
+	setTimeout(async () => {
+		const exist = await NoteUnreads.findOne(unread.id);
+
+		if (exist == null) return;
+
+		if (params.isMentioned) {
+			publishMainStream(userId, 'unreadMention', note.id);
+		}
+		if (params.isSpecified) {
+			publishMainStream(userId, 'unreadSpecifiedNote', note.id);
+		}
+		if (note.channelId) {
+			publishMainStream(userId, 'unreadChannel', note.id);
+		}
+	}, 2000);
+}
diff --git a/packages/backend/src/services/note/unwatch.ts b/packages/backend/src/services/note/unwatch.ts
new file mode 100644
index 0000000000..8ea02fe33c
--- /dev/null
+++ b/packages/backend/src/services/note/unwatch.ts
@@ -0,0 +1,10 @@
+import { User } from '@/models/entities/user';
+import { NoteWatchings } from '@/models/index';
+import { Note } from '@/models/entities/note';
+
+export default async (me: User['id'], note: Note) => {
+	await NoteWatchings.delete({
+		noteId: note.id,
+		userId: me
+	});
+};
diff --git a/packages/backend/src/services/note/watch.ts b/packages/backend/src/services/note/watch.ts
new file mode 100644
index 0000000000..e457191d99
--- /dev/null
+++ b/packages/backend/src/services/note/watch.ts
@@ -0,0 +1,20 @@
+import { User } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { NoteWatchings } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+import { NoteWatching } from '@/models/entities/note-watching';
+
+export default async (me: User['id'], note: Note) => {
+	// 自分の投稿はwatchできない
+	if (me === note.userId) {
+		return;
+	}
+
+	await NoteWatchings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		noteId: note.id,
+		userId: me,
+		noteUserId: note.userId
+	} as NoteWatching);
+};
diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts
new file mode 100644
index 0000000000..5949d11b3b
--- /dev/null
+++ b/packages/backend/src/services/push-notification.ts
@@ -0,0 +1,53 @@
+import * as push from 'web-push';
+import config from '@/config/index';
+import { SwSubscriptions } from '@/models/index';
+import { fetchMeta } from '@/misc/fetch-meta';
+import { Packed } from '@/misc/schema';
+
+type notificationType = 'notification' | 'unreadMessagingMessage';
+type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>;
+
+export default async function(userId: string, type: notificationType, body: notificationBody) {
+	const meta = await fetchMeta();
+
+	if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
+
+	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
+	push.setVapidDetails(config.url,
+		meta.swPublicKey,
+		meta.swPrivateKey);
+
+	// Fetch
+	const subscriptions = await SwSubscriptions.find({
+		userId: userId
+	});
+
+	for (const subscription of subscriptions) {
+		const pushSubscription = {
+			endpoint: subscription.endpoint,
+			keys: {
+				auth: subscription.auth,
+				p256dh: subscription.publickey
+			}
+		};
+
+		push.sendNotification(pushSubscription, JSON.stringify({
+			type, body
+		}), {
+			proxy: config.proxy
+		}).catch((err: any) => {
+			//swLogger.info(err.statusCode);
+			//swLogger.info(err.headers);
+			//swLogger.info(err.body);
+
+			if (err.statusCode === 410) {
+				SwSubscriptions.delete({
+					userId: userId,
+					endpoint: subscription.endpoint,
+					auth: subscription.auth,
+					publickey: subscription.publickey
+				});
+			}
+		});
+	}
+}
diff --git a/packages/backend/src/services/register-or-fetch-instance-doc.ts b/packages/backend/src/services/register-or-fetch-instance-doc.ts
new file mode 100644
index 0000000000..a548ab0497
--- /dev/null
+++ b/packages/backend/src/services/register-or-fetch-instance-doc.ts
@@ -0,0 +1,34 @@
+import { Instance } from '@/models/entities/instance';
+import { Instances } from '@/models/index';
+import { federationChart } from '@/services/chart/index';
+import { genId } from '@/misc/gen-id';
+import { toPuny } from '@/misc/convert-host';
+import { Cache } from '@/misc/cache';
+
+const cache = new Cache<Instance>(1000 * 60 * 60);
+
+export async function registerOrFetchInstanceDoc(host: string): Promise<Instance> {
+	host = toPuny(host);
+
+	const cached = cache.get(host);
+	if (cached) return cached;
+
+	const index = await Instances.findOne({ host });
+
+	if (index == null) {
+		const i = await Instances.save({
+			id: genId(),
+			host,
+			caughtAt: new Date(),
+			lastCommunicatedAt: new Date(),
+		});
+
+		federationChart.update(true);
+
+		cache.set(host, i);
+		return i;
+	} else {
+		cache.set(host, index);
+		return index;
+	}
+}
diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts
new file mode 100644
index 0000000000..04775524fa
--- /dev/null
+++ b/packages/backend/src/services/relay.ts
@@ -0,0 +1,94 @@
+import { createSystemUser } from './create-system-user';
+import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay';
+import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index';
+import renderUndo from '@/remote/activitypub/renderer/undo';
+import { deliver } from '@/queue/index';
+import { ILocalUser, User } from '@/models/entities/user';
+import { Users, Relays } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+const ACTOR_USERNAME = 'relay.actor' as const;
+
+export async function getRelayActor(): Promise<ILocalUser> {
+	const user = await Users.findOne({
+		host: null,
+		username: ACTOR_USERNAME
+	});
+
+	if (user) return user as ILocalUser;
+
+	const created = await createSystemUser(ACTOR_USERNAME);
+	return created as ILocalUser;
+}
+
+export async function addRelay(inbox: string) {
+	const relay = await Relays.save({
+		id: genId(),
+		inbox,
+		status: 'requesting'
+	});
+
+	const relayActor = await getRelayActor();
+	const follow = await renderFollowRelay(relay, relayActor);
+	const activity = renderActivity(follow);
+	deliver(relayActor, activity, relay.inbox);
+
+	return relay;
+}
+
+export async function removeRelay(inbox: string) {
+	const relay = await Relays.findOne({
+		inbox
+	});
+
+	if (relay == null) {
+		throw 'relay not found';
+	}
+
+	const relayActor = await getRelayActor();
+	const follow = renderFollowRelay(relay, relayActor);
+	const undo = renderUndo(follow, relayActor);
+	const activity = renderActivity(undo);
+	deliver(relayActor, activity, relay.inbox);
+
+	await Relays.delete(relay.id);
+}
+
+export async function listRelay() {
+	const relays = await Relays.find();
+	return relays;
+}
+
+export async function relayAccepted(id: string) {
+	const result = await Relays.update(id, {
+		status: 'accepted'
+	});
+
+	return JSON.stringify(result);
+}
+
+export async function relayRejected(id: string) {
+	const result = await Relays.update(id, {
+		status: 'rejected'
+	});
+
+	return JSON.stringify(result);
+}
+
+export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any) {
+	if (activity == null) return;
+
+	const relays = await Relays.find({
+		status: 'accepted'
+	});
+	if (relays.length === 0) return;
+
+	const copy = JSON.parse(JSON.stringify(activity));
+	if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
+
+	const signed = await attachLdSignature(copy, user);
+
+	for (const relay of relays) {
+		deliver(user, signed, relay.inbox);
+	}
+}
diff --git a/packages/backend/src/services/send-email-notification.ts b/packages/backend/src/services/send-email-notification.ts
new file mode 100644
index 0000000000..157bacb46d
--- /dev/null
+++ b/packages/backend/src/services/send-email-notification.ts
@@ -0,0 +1,31 @@
+import { UserProfiles } from '@/models/index';
+import { User } from '@/models/entities/user';
+import { sendEmail } from './send-email';
+import { I18n } from '@/misc/i18n';
+import * as Acct from 'misskey-js/built/acct';
+const locales = require('../../../../locales/index.js');
+
+// TODO: locale ファイルをクライアント用とサーバー用で分けたい
+
+async function follow(userId: User['id'], follower: User) {
+	const userProfile = await UserProfiles.findOneOrFail({ userId: userId });
+	if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return;
+	const locale = locales[userProfile.lang || 'ja-JP'];
+	const i18n = new I18n(locale);
+	// TODO: render user information html
+	sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
+}
+
+async function receiveFollowRequest(userId: User['id'], follower: User) {
+	const userProfile = await UserProfiles.findOneOrFail({ userId: userId });
+	if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return;
+	const locale = locales[userProfile.lang || 'ja-JP'];
+	const i18n = new I18n(locale);
+	// TODO: render user information html
+	sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
+}
+
+export const sendEmailNotification = {
+	follow,
+	receiveFollowRequest,
+};
diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts
new file mode 100644
index 0000000000..d24168ec46
--- /dev/null
+++ b/packages/backend/src/services/send-email.ts
@@ -0,0 +1,123 @@
+import * as nodemailer from 'nodemailer';
+import { fetchMeta } from '@/misc/fetch-meta';
+import Logger from './logger';
+import config from '@/config/index';
+
+export const logger = new Logger('email');
+
+export async function sendEmail(to: string, subject: string, html: string, text: string) {
+	const meta = await fetchMeta(true);
+
+	const iconUrl = `${config.url}/static-assets/mi-white.png`;
+	const emailSettingUrl = `${config.url}/settings/email`;
+
+	const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
+
+	const transporter = nodemailer.createTransport({
+		host: meta.smtpHost,
+		port: meta.smtpPort,
+		secure: meta.smtpSecure,
+		ignoreTLS: !enableAuth,
+		proxy: config.proxySmtp,
+		auth: enableAuth ? {
+			user: meta.smtpUser,
+			pass: meta.smtpPass
+		} : undefined
+	} as any);
+
+	try {
+		// TODO: htmlサニタイズ
+		const info = await transporter.sendMail({
+			from: meta.email!,
+			to: to,
+			subject: subject,
+			text: text,
+			html: `<!doctype html>
+			<html>
+				<head>
+					<meta charset="utf-8">
+					<title>${ subject }</title>
+					<style>
+						html {
+							background: #eee;
+						}
+
+						body {
+							padding: 16px;
+							margin: 0;
+							font-family: sans-serif;
+							font-size: 14px;
+						}
+
+						a {
+							text-decoration: none;
+							color: #86b300;
+						}
+						a:hover {
+							text-decoration: underline;
+						}
+
+						main {
+							max-width: 500px;
+							margin: 0 auto;
+							background: #fff;
+							color: #555;
+						}
+							main > header {
+								padding: 32px;
+								background: #86b300;
+							}
+								main > header > img {
+									max-width: 128px;
+									max-height: 28px;
+									vertical-align: bottom;
+								}
+							main > article {
+								padding: 32px;
+							}
+								main > article > h1 {
+									margin: 0 0 1em 0;
+								}
+							main > footer {
+								padding: 32px;
+								border-top: solid 1px #eee;
+							}
+
+						nav {
+							box-sizing: border-box;
+							max-width: 500px;
+							margin: 16px auto 0 auto;
+							padding: 0 32px;
+						}
+							nav > a {
+								color: #888;
+							}
+					</style>
+				</head>
+				<body>
+					<main>
+						<header>
+							<img src="${ meta.logoImageUrl || meta.iconUrl || iconUrl }"/>
+						</header>
+						<article>
+							<h1>${ subject }</h1>
+							<div>${ html }</div>
+						</article>
+						<footer>
+							<a href="${ emailSettingUrl }">${ 'Email setting' }</a>
+						</footer>
+					</main>
+					<nav>
+						<a href="${ config.url }">${ config.host }</a>
+					</nav>
+				</body>
+			</html>
+			`
+		});
+
+		logger.info('Message sent: %s', info.messageId);
+	} catch (e) {
+		logger.error(e);
+		throw e;
+	}
+}
diff --git a/packages/backend/src/services/stream.ts b/packages/backend/src/services/stream.ts
new file mode 100644
index 0000000000..2c308a1b54
--- /dev/null
+++ b/packages/backend/src/services/stream.ts
@@ -0,0 +1,129 @@
+import { redisClient } from '../db/redis';
+import { User } from '@/models/entities/user';
+import { Note } from '@/models/entities/note';
+import { UserList } from '@/models/entities/user-list';
+import { ReversiGame } from '@/models/entities/games/reversi/game';
+import { UserGroup } from '@/models/entities/user-group';
+import config from '@/config/index';
+import { Antenna } from '@/models/entities/antenna';
+import { Channel } from '@/models/entities/channel';
+import {
+	StreamChannels,
+	AdminStreamTypes,
+	AntennaStreamTypes,
+	BroadcastTypes,
+	ChannelStreamTypes,
+	DriveStreamTypes,
+	GroupMessagingStreamTypes,
+	InternalStreamTypes,
+	MainStreamTypes,
+	MessagingIndexStreamTypes,
+	MessagingStreamTypes,
+	NoteStreamTypes,
+	ReversiGameStreamTypes,
+	ReversiStreamTypes,
+	UserListStreamTypes,
+	UserStreamTypes
+} from '@/server/api/stream/types';
+import { Packed } from '@/misc/schema';
+
+class Publisher {
+	private publish = (channel: StreamChannels, type: string | null, value?: any): void => {
+		const message = type == null ? value : value == null ?
+			{ type: type, body: null } :
+			{ type: type, body: value };
+
+		redisClient.publish(config.host, JSON.stringify({
+			channel: channel,
+			message: message
+		}));
+	}
+
+	public publishInternalEvent = <K extends keyof InternalStreamTypes>(type: K, value?: InternalStreamTypes[K]): void => {
+		this.publish('internal', type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishUserEvent = <K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void => {
+		this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishBroadcastStream = <K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void => {
+		this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishMainStream = <K extends keyof MainStreamTypes>(userId: User['id'], type: K, value?: MainStreamTypes[K]): void => {
+		this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishDriveStream = <K extends keyof DriveStreamTypes>(userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => {
+		this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishNoteStream = <K extends keyof NoteStreamTypes>(noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => {
+		this.publish(`noteStream:${noteId}`, type, {
+			id: noteId,
+			body: value
+		});
+	}
+
+	public publishChannelStream = <K extends keyof ChannelStreamTypes>(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => {
+		this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishUserListStream = <K extends keyof UserListStreamTypes>(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => {
+		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishAntennaStream = <K extends keyof AntennaStreamTypes>(antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => {
+		this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishMessagingStream = <K extends keyof MessagingStreamTypes>(userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void => {
+		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishGroupMessagingStream = <K extends keyof GroupMessagingStreamTypes>(groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => {
+		this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishMessagingIndexStream = <K extends keyof MessagingIndexStreamTypes>(userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => {
+		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishReversiStream = <K extends keyof ReversiStreamTypes>(userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => {
+		this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishReversiGameStream = <K extends keyof ReversiGameStreamTypes>(gameId: ReversiGame['id'], type: K, value?: ReversiGameStreamTypes[K]): void => {
+		this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishNotesStream = (note: Packed<'Note'>): void => {
+		this.publish('notesStream', null, note);
+	}
+
+	public publishAdminStream = <K extends keyof AdminStreamTypes>(userId: User['id'], type: K, value?: AdminStreamTypes[K]): void => {
+		this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	}
+}
+
+const publisher = new Publisher();
+
+export default publisher;
+
+export const publishInternalEvent = publisher.publishInternalEvent;
+export const publishUserEvent = publisher.publishUserEvent;
+export const publishBroadcastStream = publisher.publishBroadcastStream;
+export const publishMainStream = publisher.publishMainStream;
+export const publishDriveStream = publisher.publishDriveStream;
+export const publishNoteStream = publisher.publishNoteStream;
+export const publishNotesStream = publisher.publishNotesStream;
+export const publishChannelStream = publisher.publishChannelStream;
+export const publishUserListStream = publisher.publishUserListStream;
+export const publishAntennaStream = publisher.publishAntennaStream;
+export const publishMessagingStream = publisher.publishMessagingStream;
+export const publishGroupMessagingStream = publisher.publishGroupMessagingStream;
+export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
+export const publishReversiStream = publisher.publishReversiStream;
+export const publishReversiGameStream = publisher.publishReversiGameStream;
+export const publishAdminStream = publisher.publishAdminStream;
diff --git a/packages/backend/src/services/suspend-user.ts b/packages/backend/src/services/suspend-user.ts
new file mode 100644
index 0000000000..55be63172f
--- /dev/null
+++ b/packages/backend/src/services/suspend-user.ts
@@ -0,0 +1,34 @@
+import renderDelete from '@/remote/activitypub/renderer/delete';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { deliver } from '@/queue/index';
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+import { Users, Followings } from '@/models/index';
+import { Not, IsNull } from 'typeorm';
+
+export async function doPostSuspend(user: { id: User['id']; host: User['host'] }) {
+	if (Users.isLocalUser(user)) {
+		// 知り得る全SharedInboxにDelete配信
+		const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user));
+
+		const queue: string[] = [];
+
+		const followings = await Followings.find({
+			where: [
+				{ followerSharedInbox: Not(IsNull()) },
+				{ followeeSharedInbox: Not(IsNull()) }
+			],
+			select: ['followerSharedInbox', 'followeeSharedInbox']
+		});
+
+		const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox);
+
+		for (const inbox of inboxes) {
+			if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
+		}
+
+		for (const inbox of queue) {
+			deliver(user, content, inbox);
+		}
+	}
+}
diff --git a/packages/backend/src/services/unsuspend-user.ts b/packages/backend/src/services/unsuspend-user.ts
new file mode 100644
index 0000000000..bfffa036e5
--- /dev/null
+++ b/packages/backend/src/services/unsuspend-user.ts
@@ -0,0 +1,35 @@
+import renderDelete from '@/remote/activitypub/renderer/delete';
+import renderUndo from '@/remote/activitypub/renderer/undo';
+import { renderActivity } from '@/remote/activitypub/renderer/index';
+import { deliver } from '@/queue/index';
+import config from '@/config/index';
+import { User } from '@/models/entities/user';
+import { Users, Followings } from '@/models/index';
+import { Not, IsNull } from 'typeorm';
+
+export async function doPostUnsuspend(user: User) {
+	if (Users.isLocalUser(user)) {
+		// 知り得る全SharedInboxにUndo Delete配信
+		const content = renderActivity(renderUndo(renderDelete(`${config.url}/users/${user.id}`, user), user));
+
+		const queue: string[] = [];
+
+		const followings = await Followings.find({
+			where: [
+				{ followerSharedInbox: Not(IsNull()) },
+				{ followeeSharedInbox: Not(IsNull()) }
+			],
+			select: ['followerSharedInbox', 'followeeSharedInbox']
+		});
+
+		const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox);
+
+		for (const inbox of inboxes) {
+			if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
+		}
+
+		for (const inbox of queue) {
+			deliver(user as any, content, inbox);
+		}
+	}
+}
diff --git a/packages/backend/src/services/update-hashtag.ts b/packages/backend/src/services/update-hashtag.ts
new file mode 100644
index 0000000000..e8504f6ff0
--- /dev/null
+++ b/packages/backend/src/services/update-hashtag.ts
@@ -0,0 +1,128 @@
+import { User } from '@/models/entities/user';
+import { Hashtags, Users } from '@/models/index';
+import { hashtagChart } from '@/services/chart/index';
+import { genId } from '@/misc/gen-id';
+import { Hashtag } from '@/models/entities/hashtag';
+import { normalizeForSearch } from '@/misc/normalize-for-search';
+
+export async function updateHashtags(user: { id: User['id']; host: User['host']; }, tags: string[]) {
+	for (const tag of tags) {
+		await updateHashtag(user, tag);
+	}
+}
+
+export async function updateUsertags(user: User, tags: string[]) {
+	for (const tag of tags) {
+		await updateHashtag(user, tag, true, true);
+	}
+
+	for (const tag of (user.tags || []).filter(x => !tags.includes(x))) {
+		await updateHashtag(user, tag, true, false);
+	}
+}
+
+export async function updateHashtag(user: { id: User['id']; host: User['host']; }, tag: string, isUserAttached = false, inc = true) {
+	tag = normalizeForSearch(tag);
+
+	const index = await Hashtags.findOne({ name: tag });
+
+	if (index == null && !inc) return;
+
+	if (index != null) {
+		const q = Hashtags.createQueryBuilder('tag').update()
+			.where('name = :name', { name: tag });
+
+		const set = {} as any;
+
+		if (isUserAttached) {
+			if (inc) {
+				// 自分が初めてこのタグを使ったなら
+				if (!index.attachedUserIds.some(id => id === user.id)) {
+					set.attachedUserIds = () => `array_append("attachedUserIds", '${user.id}')`;
+					set.attachedUsersCount = () => `"attachedUsersCount" + 1`;
+				}
+				// 自分が(ローカル内で)初めてこのタグを使ったなら
+				if (Users.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) {
+					set.attachedLocalUserIds = () => `array_append("attachedLocalUserIds", '${user.id}')`;
+					set.attachedLocalUsersCount = () => `"attachedLocalUsersCount" + 1`;
+				}
+				// 自分が(リモートで)初めてこのタグを使ったなら
+				if (Users.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) {
+					set.attachedRemoteUserIds = () => `array_append("attachedRemoteUserIds", '${user.id}')`;
+					set.attachedRemoteUsersCount = () => `"attachedRemoteUsersCount" + 1`;
+				}
+			} else {
+				set.attachedUserIds = () => `array_remove("attachedUserIds", '${user.id}')`;
+				set.attachedUsersCount = () => `"attachedUsersCount" - 1`;
+				if (Users.isLocalUser(user)) {
+					set.attachedLocalUserIds = () => `array_remove("attachedLocalUserIds", '${user.id}')`;
+					set.attachedLocalUsersCount = () => `"attachedLocalUsersCount" - 1`;
+				} else {
+					set.attachedRemoteUserIds = () => `array_remove("attachedRemoteUserIds", '${user.id}')`;
+					set.attachedRemoteUsersCount = () => `"attachedRemoteUsersCount" - 1`;
+				}
+			}
+		} else {
+			// 自分が初めてこのタグを使ったなら
+			if (!index.mentionedUserIds.some(id => id === user.id)) {
+				set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`;
+				set.mentionedUsersCount = () => `"mentionedUsersCount" + 1`;
+			}
+			// 自分が(ローカル内で)初めてこのタグを使ったなら
+			if (Users.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) {
+				set.mentionedLocalUserIds = () => `array_append("mentionedLocalUserIds", '${user.id}')`;
+				set.mentionedLocalUsersCount = () => `"mentionedLocalUsersCount" + 1`;
+			}
+			// 自分が(リモートで)初めてこのタグを使ったなら
+			if (Users.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) {
+				set.mentionedRemoteUserIds = () => `array_append("mentionedRemoteUserIds", '${user.id}')`;
+				set.mentionedRemoteUsersCount = () => `"mentionedRemoteUsersCount" + 1`;
+			}
+		}
+
+		if (Object.keys(set).length > 0) {
+			q.set(set);
+			q.execute();
+		}
+	} else {
+		if (isUserAttached) {
+			Hashtags.insert({
+				id: genId(),
+				name: tag,
+				mentionedUserIds: [],
+				mentionedUsersCount: 0,
+				mentionedLocalUserIds: [],
+				mentionedLocalUsersCount: 0,
+				mentionedRemoteUserIds: [],
+				mentionedRemoteUsersCount: 0,
+				attachedUserIds: [user.id],
+				attachedUsersCount: 1,
+				attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [],
+				attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0,
+				attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [],
+				attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
+			} as Hashtag);
+		} else {
+			Hashtags.insert({
+				id: genId(),
+				name: tag,
+				mentionedUserIds: [user.id],
+				mentionedUsersCount: 1,
+				mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [],
+				mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0,
+				mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [],
+				mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
+				attachedUserIds: [],
+				attachedUsersCount: 0,
+				attachedLocalUserIds: [],
+				attachedLocalUsersCount: 0,
+				attachedRemoteUserIds: [],
+				attachedRemoteUsersCount: 0,
+			} as Hashtag);
+		}
+	}
+
+	if (!isUserAttached) {
+		hashtagChart.update(tag, user);
+	}
+}
diff --git a/packages/backend/src/services/user-list/push.ts b/packages/backend/src/services/user-list/push.ts
new file mode 100644
index 0000000000..29d561b519
--- /dev/null
+++ b/packages/backend/src/services/user-list/push.ts
@@ -0,0 +1,27 @@
+import { publishUserListStream } from '@/services/stream';
+import { User } from '@/models/entities/user';
+import { UserList } from '@/models/entities/user-list';
+import { UserListJoinings, Users } from '@/models/index';
+import { UserListJoining } from '@/models/entities/user-list-joining';
+import { genId } from '@/misc/gen-id';
+import { fetchProxyAccount } from '@/misc/fetch-proxy-account';
+import createFollowing from '../following/create';
+
+export async function pushUserToUserList(target: User, list: UserList) {
+	await UserListJoinings.insert({
+		id: genId(),
+		createdAt: new Date(),
+		userId: target.id,
+		userListId: list.id
+	} as UserListJoining);
+
+	publishUserListStream(list.id, 'userAdded', await Users.pack(target));
+
+	// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
+	if (Users.isRemoteUser(target)) {
+		const proxy = await fetchProxyAccount();
+		if (proxy) {
+			createFollowing(proxy, target);
+		}
+	}
+}
diff --git a/packages/backend/src/services/validate-email-for-account.ts b/packages/backend/src/services/validate-email-for-account.ts
new file mode 100644
index 0000000000..1d039fb263
--- /dev/null
+++ b/packages/backend/src/services/validate-email-for-account.ts
@@ -0,0 +1,34 @@
+import validateEmail from 'deep-email-validator';
+import { UserProfiles } from '@/models';
+
+export async function validateEmailForAccount(emailAddress: string): Promise<{
+	available: boolean;
+	reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
+}> {
+	const exist = await UserProfiles.count({
+		emailVerified: true,
+		email: emailAddress,
+	});
+
+	const validated = await validateEmail({
+		email: emailAddress,
+		validateRegex: true,
+		validateMx: true,
+		validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
+		validateDisposable: true, // 捨てアドかどうかチェック
+		validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
+	});
+
+	const available = exist === 0 && validated.valid;
+
+	return {
+		available,
+		reason: available ? null :
+			exist !== 0 ? 'used' :
+			validated.reason === 'regex' ? 'format' :
+			validated.reason === 'disposable' ? 'disposable' :
+			validated.reason === 'mx' ? 'mx' :
+			validated.reason === 'smtp' ? 'smtp' :
+			null,
+	};
+}
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
new file mode 100644
index 0000000000..20f6f8bb88
--- /dev/null
+++ b/packages/backend/src/types.ts
@@ -0,0 +1,7 @@
+export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
+
+export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
+
+export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
+
+export const ffVisibility = ['public', 'followers', 'private'] as const;
diff --git a/packages/backend/tools/accept-migration.js b/packages/backend/tools/accept-migration.js
new file mode 100644
index 0000000000..309a4ab5e3
--- /dev/null
+++ b/packages/backend/tools/accept-migration.js
@@ -0,0 +1,26 @@
+"use strict";
+// ex) node built/tools/accept-migration Yo 1000000000001
+Object.defineProperty(exports, "__esModule", { value: true });
+const typeorm_1 = require("typeorm");
+const index_1 = require("@/config/index");
+(0, typeorm_1.createConnection)({
+    type: 'postgres',
+    host: index_1.default.db.host,
+    port: index_1.default.db.port,
+    username: index_1.default.db.user,
+    password: index_1.default.db.pass,
+    database: index_1.default.db.db,
+    extra: index_1.default.db.extra,
+    synchronize: false,
+    dropSchema: false,
+}).then(c => {
+    c.query(`INSERT INTO migrations(timestamp,name) VALUES (${process.argv[3]}, '${process.argv[2]}${process.argv[3]}');`).then(() => {
+        console.log('done');
+        process.exit(0);
+    }).catch(e => {
+        console.log('ERROR:');
+        console.log(e);
+        process.exit(1);
+    });
+});
+//# sourceMappingURL=accept-migration.js.map
\ No newline at end of file
diff --git a/packages/backend/tools/accept-migration.js.map b/packages/backend/tools/accept-migration.js.map
new file mode 100644
index 0000000000..c158a49661
--- /dev/null
+++ b/packages/backend/tools/accept-migration.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"accept-migration.js","sourceRoot":"","sources":["accept-migration.ts"],"names":[],"mappings":";AAAA,yDAAyD;;AAEzD,qCAA2C;AAC3C,0CAAoC;AAEpC,IAAA,0BAAgB,EAAC;IAChB,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACpB,IAAI,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACpB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACxB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACxB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,EAAE;IACtB,KAAK,EAAE,eAAM,CAAC,EAAE,CAAC,KAAK;IACtB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,KAAK;CACjB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;IACX,CAAC,CAAC,KAAK,CAAC,kDAAkD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;QAChI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACZ,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
\ No newline at end of file
diff --git a/packages/backend/tools/accept-migration.ts b/packages/backend/tools/accept-migration.ts
new file mode 100644
index 0000000000..2e54fc129f
--- /dev/null
+++ b/packages/backend/tools/accept-migration.ts
@@ -0,0 +1,25 @@
+// ex) node built/tools/accept-migration Yo 1000000000001
+
+import { createConnection } from 'typeorm';
+import config from '@/config/index';
+
+createConnection({
+	type: 'postgres',
+	host: config.db.host,
+	port: config.db.port,
+	username: config.db.user,
+	password: config.db.pass,
+	database: config.db.db,
+	extra: config.db.extra,
+	synchronize: false,
+	dropSchema: false,
+}).then(c => {
+	c.query(`INSERT INTO migrations(timestamp,name) VALUES (${process.argv[3]}, '${process.argv[2]}${process.argv[3]}');`).then(() => {
+		console.log('done');
+		process.exit(0);
+	}).catch(e => {
+		console.log('ERROR:');
+		console.log(e);
+		process.exit(1);
+	});
+});
diff --git a/packages/backend/tools/add-emoji.js b/packages/backend/tools/add-emoji.js
new file mode 100644
index 0000000000..df02f554a5
--- /dev/null
+++ b/packages/backend/tools/add-emoji.js
@@ -0,0 +1,30 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const index_1 = require("@/models/index");
+const gen_id_1 = require("@/misc/gen-id");
+async function main(name, url, alias) {
+    const aliases = alias != null ? [alias] : [];
+    await index_1.Emojis.save({
+        id: (0, gen_id_1.genId)(),
+        host: null,
+        name,
+        url,
+        aliases,
+        updatedAt: new Date()
+    });
+}
+const args = process.argv.slice(2);
+const name = args[0];
+const url = args[1];
+if (!name)
+    throw new Error('require name');
+if (!url)
+    throw new Error('require url');
+main(name, url).then(() => {
+    console.log('success');
+    process.exit(0);
+}).catch(e => {
+    console.warn(e);
+    process.exit(1);
+});
+//# sourceMappingURL=add-emoji.js.map
\ No newline at end of file
diff --git a/packages/backend/tools/add-emoji.js.map b/packages/backend/tools/add-emoji.js.map
new file mode 100644
index 0000000000..03bf31b984
--- /dev/null
+++ b/packages/backend/tools/add-emoji.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"add-emoji.js","sourceRoot":"","sources":["add-emoji.ts"],"names":[],"mappings":";;AAAA,0CAAwC;AACxC,0CAAsC;AAEtC,KAAK,UAAU,IAAI,CAAC,IAAY,EAAE,GAAW,EAAE,KAAc;IAC5D,MAAM,OAAO,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAE,KAAK,CAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAE/C,MAAM,cAAM,CAAC,IAAI,CAAC;QACjB,EAAE,EAAE,IAAA,cAAK,GAAE;QACX,IAAI,EAAE,IAAI;QACV,IAAI;QACJ,GAAG;QACH,OAAO;QACP,SAAS,EAAE,IAAI,IAAI,EAAE;KACrB,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACrB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEpB,IAAI,CAAC,IAAI;IAAE,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;AAC3C,IAAI,CAAC,GAAG;IAAE,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC;AAEzC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACzB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
\ No newline at end of file
diff --git a/packages/backend/tools/add-emoji.ts b/packages/backend/tools/add-emoji.ts
new file mode 100644
index 0000000000..9ffe7dfa81
--- /dev/null
+++ b/packages/backend/tools/add-emoji.ts
@@ -0,0 +1,30 @@
+import { Emojis } from '@/models/index';
+import { genId } from '@/misc/gen-id';
+
+async function main(name: string, url: string, alias?: string): Promise<any> {
+	const aliases = alias != null ? [ alias ] : [];
+
+	await Emojis.save({
+		id: genId(),
+		host: null,
+		name,
+		url,
+		aliases,
+		updatedAt: new Date()
+	});
+}
+
+const args = process.argv.slice(2);
+const name = args[0];
+const url = args[1];
+
+if (!name) throw new Error('require name');
+if (!url) throw new Error('require url');
+
+main(name, url).then(() => {
+	console.log('success');
+	process.exit(0);
+}).catch(e => {
+	console.warn(e);
+	process.exit(1);
+});
diff --git a/packages/backend/tools/demote-admin.js b/packages/backend/tools/demote-admin.js
new file mode 100644
index 0000000000..d6a296883a
--- /dev/null
+++ b/packages/backend/tools/demote-admin.js
@@ -0,0 +1,30 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const postgre_1 = require("../db/postgre");
+const typeorm_1 = require("typeorm");
+const user_1 = require("@/models/entities/user");
+async function main(username) {
+    if (!username)
+        throw `username required`;
+    username = username.replace(/^@/, '');
+    await (0, postgre_1.initDb)();
+    const Users = (0, typeorm_1.getRepository)(user_1.User);
+    const res = await Users.update({
+        usernameLower: username.toLowerCase(),
+        host: null
+    }, {
+        isAdmin: false
+    });
+    if (res.affected !== 1) {
+        throw 'Failed';
+    }
+}
+const args = process.argv.slice(2);
+main(args[0]).then(() => {
+    console.log('Success');
+    process.exit(0);
+}).catch(e => {
+    console.error(`Error: ${e.message || e}`);
+    process.exit(1);
+});
+//# sourceMappingURL=demote-admin.js.map
\ No newline at end of file
diff --git a/packages/backend/tools/demote-admin.js.map b/packages/backend/tools/demote-admin.js.map
new file mode 100644
index 0000000000..4403e2bab0
--- /dev/null
+++ b/packages/backend/tools/demote-admin.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"demote-admin.js","sourceRoot":"","sources":["demote-admin.ts"],"names":[],"mappings":";;AAAA,2CAAuC;AACvC,qCAAwC;AACxC,iDAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,QAAgB;IACnC,IAAI,CAAC,QAAQ;QAAE,MAAM,mBAAmB,CAAC;IACzC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAEtC,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,KAAK,GAAG,IAAA,uBAAa,EAAC,WAAI,CAAC,CAAC;IAElC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;QAC9B,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;QACrC,IAAI,EAAE,IAAI;KACV,EAAE;QACF,OAAO,EAAE,KAAK;KACd,CAAC,CAAC;IAEH,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,EAAE;QACvB,MAAM,QAAQ,CAAC;KACf;AACF,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
\ No newline at end of file
diff --git a/packages/backend/tools/demote-admin.ts b/packages/backend/tools/demote-admin.ts
new file mode 100644
index 0000000000..d7c6d1cec2
--- /dev/null
+++ b/packages/backend/tools/demote-admin.ts
@@ -0,0 +1,32 @@
+import { initDb } from '../db/postgre';
+import { getRepository } from 'typeorm';
+import { User } from '@/models/entities/user';
+
+async function main(username: string) {
+	if (!username) throw `username required`;
+	username = username.replace(/^@/, '');
+
+	await initDb();
+	const Users = getRepository(User);
+
+	const res = await Users.update({
+		usernameLower: username.toLowerCase(),
+		host: null
+	}, {
+		isAdmin: false
+	});
+
+	if (res.affected !== 1) {
+		throw 'Failed';
+	}
+}
+
+const args = process.argv.slice(2);
+
+main(args[0]).then(() => {
+	console.log('Success');
+	process.exit(0);
+}).catch(e => {
+	console.error(`Error: ${e.message || e}`);
+	process.exit(1);
+});
diff --git a/packages/backend/tools/mark-admin.js b/packages/backend/tools/mark-admin.js
new file mode 100644
index 0000000000..4563d9c332
--- /dev/null
+++ b/packages/backend/tools/mark-admin.js
@@ -0,0 +1,30 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const postgre_1 = require("../db/postgre");
+const typeorm_1 = require("typeorm");
+const user_1 = require("@/models/entities/user");
+async function main(username) {
+    if (!username)
+        throw `username required`;
+    username = username.replace(/^@/, '');
+    await (0, postgre_1.initDb)();
+    const Users = (0, typeorm_1.getRepository)(user_1.User);
+    const res = await Users.update({
+        usernameLower: username.toLowerCase(),
+        host: null
+    }, {
+        isAdmin: true
+    });
+    if (res.affected !== 1) {
+        throw 'Failed';
+    }
+}
+const args = process.argv.slice(2);
+main(args[0]).then(() => {
+    console.log('Success');
+    process.exit(0);
+}).catch(e => {
+    console.error(`Error: ${e.message || e}`);
+    process.exit(1);
+});
+//# sourceMappingURL=mark-admin.js.map
\ No newline at end of file
diff --git a/packages/backend/tools/mark-admin.js.map b/packages/backend/tools/mark-admin.js.map
new file mode 100644
index 0000000000..5633e2f894
--- /dev/null
+++ b/packages/backend/tools/mark-admin.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"mark-admin.js","sourceRoot":"","sources":["mark-admin.ts"],"names":[],"mappings":";;AAAA,2CAAuC;AACvC,qCAAwC;AACxC,iDAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,QAAgB;IACnC,IAAI,CAAC,QAAQ;QAAE,MAAM,mBAAmB,CAAC;IACzC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAEtC,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,KAAK,GAAG,IAAA,uBAAa,EAAC,WAAI,CAAC,CAAC;IAElC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;QAC9B,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;QACrC,IAAI,EAAE,IAAI;KACV,EAAE;QACF,OAAO,EAAE,IAAI;KACb,CAAC,CAAC;IAEH,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,EAAE;QACvB,MAAM,QAAQ,CAAC;KACf;AACF,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
\ No newline at end of file
diff --git a/packages/backend/tools/mark-admin.ts b/packages/backend/tools/mark-admin.ts
new file mode 100644
index 0000000000..62ed0f09ee
--- /dev/null
+++ b/packages/backend/tools/mark-admin.ts
@@ -0,0 +1,32 @@
+import { initDb } from '../db/postgre';
+import { getRepository } from 'typeorm';
+import { User } from '@/models/entities/user';
+
+async function main(username: string) {
+	if (!username) throw `username required`;
+	username = username.replace(/^@/, '');
+
+	await initDb();
+	const Users = getRepository(User);
+
+	const res = await Users.update({
+		usernameLower: username.toLowerCase(),
+		host: null
+	}, {
+		isAdmin: true
+	});
+
+	if (res.affected !== 1) {
+		throw 'Failed';
+	}
+}
+
+const args = process.argv.slice(2);
+
+main(args[0]).then(() => {
+	console.log('Success');
+	process.exit(0);
+}).catch(e => {
+	console.error(`Error: ${e.message || e}`);
+	process.exit(1);
+});
diff --git a/packages/backend/tools/refresh-question.js b/packages/backend/tools/refresh-question.js
new file mode 100644
index 0000000000..0536c38207
--- /dev/null
+++ b/packages/backend/tools/refresh-question.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const question_1 = require("@/remote/activitypub/models/question");
+async function main(uri) {
+    return await (0, question_1.updateQuestion)(uri);
+}
+const args = process.argv.slice(2);
+const uri = args[0];
+main(uri).then(result => {
+    console.log(`Done: ${result}`);
+}).catch(e => {
+    console.warn(e);
+});
+//# sourceMappingURL=refresh-question.js.map
\ No newline at end of file
diff --git a/packages/backend/tools/refresh-question.js.map b/packages/backend/tools/refresh-question.js.map
new file mode 100644
index 0000000000..3db6d0b8bb
--- /dev/null
+++ b/packages/backend/tools/refresh-question.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"refresh-question.js","sourceRoot":"","sources":["refresh-question.ts"],"names":[],"mappings":";;AAAA,mEAAsE;AAEtE,KAAK,UAAU,IAAI,CAAC,GAAW;IAC9B,OAAO,MAAM,IAAA,yBAAc,EAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEpB,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
\ No newline at end of file
diff --git a/packages/backend/tools/refresh-question.ts b/packages/backend/tools/refresh-question.ts
new file mode 100644
index 0000000000..98a3c2865f
--- /dev/null
+++ b/packages/backend/tools/refresh-question.ts
@@ -0,0 +1,14 @@
+import { updateQuestion } from '@/remote/activitypub/models/question';
+
+async function main(uri: string): Promise<any> {
+	return await updateQuestion(uri);
+}
+
+const args = process.argv.slice(2);
+const uri = args[0];
+
+main(uri).then(result => {
+	console.log(`Done: ${result}`);
+}).catch(e => {
+	console.warn(e);
+});
diff --git a/packages/backend/tools/resync-remote-user.js b/packages/backend/tools/resync-remote-user.js
new file mode 100644
index 0000000000..05d76ecc97
--- /dev/null
+++ b/packages/backend/tools/resync-remote-user.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const postgre_1 = require("@/db/postgre");
+const Acct = require("misskey-js/built/acct");
+async function main(acct) {
+    await (0, postgre_1.initDb)();
+    const { resolveUser } = await Promise.resolve().then(() => require('@/remote/resolve-user'));
+    const { username, host } = Acct.parse(acct);
+    await resolveUser(username, host, {}, true);
+}
+// get args
+const args = process.argv.slice(2);
+let acct = args[0];
+// normalize args
+acct = acct.replace(/^@/, '');
+// check args
+if (!acct.match(/^\w+@\w/)) {
+    throw `Invalid acct format. Valid format are user@host`;
+}
+console.log(`resync ${acct}`);
+main(acct).then(() => {
+    console.log('Done');
+}).catch(e => {
+    console.warn(e);
+});
+//# sourceMappingURL=resync-remote-user.js.map
\ No newline at end of file
diff --git a/packages/backend/tools/resync-remote-user.js.map b/packages/backend/tools/resync-remote-user.js.map
new file mode 100644
index 0000000000..456d7da1d1
--- /dev/null
+++ b/packages/backend/tools/resync-remote-user.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"resync-remote-user.js","sourceRoot":"","sources":["resync-remote-user.ts"],"names":[],"mappings":";;AAAA,0CAAsC;AACtC,8CAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,IAAY;IAC/B,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,EAAE,WAAW,EAAE,GAAG,2CAAa,uBAAuB,EAAC,CAAC;IAE9D,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,WAAW;AACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEnB,iBAAiB;AACjB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAE9B,aAAa;AACb,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE;IAC3B,MAAM,iDAAiD,CAAC;CACxD;AAED,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;AAE9B,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACrB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
\ No newline at end of file
diff --git a/packages/backend/tools/resync-remote-user.ts b/packages/backend/tools/resync-remote-user.ts
new file mode 100644
index 0000000000..22d2f7c895
--- /dev/null
+++ b/packages/backend/tools/resync-remote-user.ts
@@ -0,0 +1,30 @@
+import { initDb } from '@/db/postgre';
+import * as Acct from 'misskey-js/built/acct';
+
+async function main(acct: string): Promise<any> {
+	await initDb();
+	const { resolveUser } = await import('@/remote/resolve-user');
+
+	const { username, host } = Acct.parse(acct);
+	await resolveUser(username, host, {}, true);
+}
+
+// get args
+const args = process.argv.slice(2);
+let acct = args[0];
+
+// normalize args
+acct = acct.replace(/^@/, '');
+
+// check args
+if (!acct.match(/^\w+@\w/)) {
+	throw `Invalid acct format. Valid format are user@host`;
+}
+
+console.log(`resync ${acct}`);
+
+main(acct).then(() => {
+	console.log('Done');
+}).catch(e => {
+	console.warn(e);
+});
diff --git a/packages/backend/tools/show-signin-history.js b/packages/backend/tools/show-signin-history.js
new file mode 100644
index 0000000000..daeea84613
--- /dev/null
+++ b/packages/backend/tools/show-signin-history.js
@@ -0,0 +1,47 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const index_1 = require("@/models/index");
+// node built/tools/show-signin-history username
+//  => {Success} {Date} {IPAddrsss}
+// node built/tools/show-signin-history username user-agent,x-forwarded-for
+//  with user-agent and x-forwarded-for
+// node built/tools/show-signin-history username all
+//  with full request headers
+async function main(username, headers) {
+    const user = await index_1.Users.findOne({
+        host: null,
+        usernameLower: username.toLowerCase(),
+    });
+    if (user == null)
+        throw new Error('User not found');
+    const history = await index_1.Signins.find({
+        userId: user.id
+    });
+    for (const signin of history) {
+        console.log(`${signin.success ? 'OK' : 'NG'} ${signin.createdAt ? signin.createdAt.toISOString() : 'Unknown'} ${signin.ip}`);
+        // headers
+        if (headers != null) {
+            for (const key of Object.keys(signin.headers)) {
+                if (headers.includes('all') || headers.includes(key)) {
+                    console.log(`   ${key}: ${signin.headers[key]}`);
+                }
+            }
+        }
+    }
+}
+// get args
+const args = process.argv.slice(2);
+let username = args[0];
+let headers;
+if (args[1] != null) {
+    headers = args[1].split(/,/).map(header => header.toLowerCase());
+}
+// normalize args
+username = username.replace(/^@/, '');
+main(username, headers).then(() => {
+    process.exit(0);
+}).catch(e => {
+    console.warn(e);
+    process.exit(1);
+});
+//# sourceMappingURL=show-signin-history.js.map
\ No newline at end of file
diff --git a/packages/backend/tools/show-signin-history.js.map b/packages/backend/tools/show-signin-history.js.map
new file mode 100644
index 0000000000..b421825bce
--- /dev/null
+++ b/packages/backend/tools/show-signin-history.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"show-signin-history.js","sourceRoot":"","sources":["show-signin-history.ts"],"names":[],"mappings":";;AAAA,0CAAgD;AAEhD,gDAAgD;AAChD,mCAAmC;AAEnC,2EAA2E;AAC3E,uCAAuC;AAEvC,oDAAoD;AACpD,6BAA6B;AAE7B,KAAK,UAAU,IAAI,CAAC,QAAgB,EAAE,OAAkB;IACvD,MAAM,IAAI,GAAG,MAAM,aAAK,CAAC,OAAO,CAAC;QAChC,IAAI,EAAE,IAAI;QACV,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;KACrC,CAAC,CAAC;IAEH,IAAI,IAAI,IAAI,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAEpD,MAAM,OAAO,GAAG,MAAM,eAAO,CAAC,IAAI,CAAC;QAClC,MAAM,EAAE,IAAI,CAAC,EAAE;KACf,CAAC,CAAC;IAEH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE;QAC7B,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAE7H,UAAU;QACV,IAAI,OAAO,IAAI,IAAI,EAAE;YACpB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;gBAC9C,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;oBACrD,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,KAAK,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;iBACjD;aACD;SACD;KACD;AACF,CAAC;AAED,WAAW;AACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACvB,IAAI,OAA6B,CAAC;AAElC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE;IACpB,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;CACjE;AAED,iBAAiB;AACjB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAEtC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACjC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
\ No newline at end of file
diff --git a/packages/backend/tools/show-signin-history.ts b/packages/backend/tools/show-signin-history.ts
new file mode 100644
index 0000000000..ad92316314
--- /dev/null
+++ b/packages/backend/tools/show-signin-history.ts
@@ -0,0 +1,56 @@
+import { Users, Signins } from '@/models/index';
+
+// node built/tools/show-signin-history username
+//  => {Success} {Date} {IPAddrsss}
+
+// node built/tools/show-signin-history username user-agent,x-forwarded-for
+//  with user-agent and x-forwarded-for
+
+// node built/tools/show-signin-history username all
+//  with full request headers
+
+async function main(username: string, headers?: string[]) {
+	const user = await Users.findOne({
+		host: null,
+		usernameLower: username.toLowerCase(),
+	});
+
+	if (user == null) throw new Error('User not found');
+
+	const history = await Signins.find({
+		userId: user.id
+	});
+
+	for (const signin of history) {
+		console.log(`${signin.success ? 'OK' : 'NG'} ${signin.createdAt ? signin.createdAt.toISOString() : 'Unknown'} ${signin.ip}`);
+
+		// headers
+		if (headers != null) {
+			for (const key of Object.keys(signin.headers)) {
+				if (headers.includes('all') || headers.includes(key)) {
+					console.log(`   ${key}: ${signin.headers[key]}`);
+				}
+			}
+		}
+	}
+}
+
+// get args
+const args = process.argv.slice(2);
+
+let username = args[0];
+let headers: string[] | undefined;
+
+if (args[1] != null) {
+	headers = args[1].split(/,/).map(header => header.toLowerCase());
+}
+
+// normalize args
+username = username.replace(/^@/, '');
+
+main(username, headers).then(() => {
+	process.exit(0);
+}).catch(e => {
+	console.warn(e);
+	process.exit(1);
+});
diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json
new file mode 100644
index 0000000000..3311e117de
--- /dev/null
+++ b/packages/backend/tsconfig.json
@@ -0,0 +1,43 @@
+{
+	"compilerOptions": {
+		"allowJs": true,
+		"noEmitOnError": false,
+		"noImplicitAny": true,
+		"noImplicitReturns": true,
+		"noUnusedParameters": false,
+		"noUnusedLocals": false,
+		"noFallthroughCasesInSwitch": true,
+		"declaration": false,
+		"sourceMap": false,
+		"target": "es2017",
+		"module": "commonjs",
+		"moduleResolution": "node",
+		"allowSyntheticDefaultImports": true,
+		"removeComments": false,
+		"noLib": false,
+		"strict": true,
+		"strictNullChecks": true,
+		"strictPropertyInitialization": false,
+		"experimentalDecorators": true,
+		"emitDecoratorMetadata": true,
+		"resolveJsonModule": true,
+		"isolatedModules": true,
+		"rootDir": "./src",
+		"baseUrl": "./",
+		"paths": {
+			"@/*": ["./src/*"]
+		},
+		"outDir": "./built",
+		"typeRoots": [
+			"./node_modules/@types",
+			"./@types"
+		],
+		"lib": [
+			"esnext"
+		]
+	},
+	"compileOnSave": false,
+	"include": [
+		"./src/**/*.ts"
+	],
+}
diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock
new file mode 100644
index 0000000000..bbf69fd626
--- /dev/null
+++ b/packages/backend/yarn.lock
@@ -0,0 +1,8274 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+  version "7.12.13"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
+  integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
+  dependencies:
+    "@babel/highlight" "^7.12.13"
+
+"@babel/helper-validator-identifier@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
+  integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
+
+"@babel/highlight@^7.12.13":
+  version "7.12.13"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c"
+  integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.12.11"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
+  version "7.13.9"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99"
+  integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==
+
+"@babel/runtime@^7.16.0":
+  version "7.16.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
+  integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/runtime@^7.6.2":
+  version "7.12.13"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d"
+  integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/types@^7.6.1", "@babel/types@^7.9.6":
+  version "7.13.0"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80"
+  integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.12.11"
+    lodash "^4.17.19"
+    to-fast-properties "^2.0.0"
+
+"@cspotcode/source-map-consumer@0.8.0":
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
+  integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
+
+"@cspotcode/source-map-support@0.7.0":
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5"
+  integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==
+  dependencies:
+    "@cspotcode/source-map-consumer" "0.8.0"
+
+"@cto.af/textdecoder@^0.0.0":
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/@cto.af/textdecoder/-/textdecoder-0.0.0.tgz#e1e8d84c936c30a0f4619971f19ca41941af9fdc"
+  integrity sha512-sJpx3F5xcVV/9jNYJQtvimo4Vfld/nD3ph+ZWtQzZ03Zo8rJC7QKQTRcIGS13Rcz80DwFNthCWMrd58vpY4ZAQ==
+
+"@digitalbazaar/http-client@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-1.1.0.tgz#cac383b24ace04b18b919deab773462b03d3d7b0"
+  integrity sha512-ks7hqa6hm9NyULdbm9qL6TRS8rADzBw8R0lETvUgvdNXu9H62XG2YqoKRDThtfgWzWxLwRJ3Z2o4ev81dZZbyQ==
+  dependencies:
+    esm "^3.2.22"
+    ky "^0.25.1"
+    ky-universal "^0.8.2"
+
+"@discordapp/twemoji@13.1.0":
+  version "13.1.0"
+  resolved "https://registry.yarnpkg.com/@discordapp/twemoji/-/twemoji-13.1.0.tgz#6b25f3958fa8fd68692248c87776bc737fd009a9"
+  integrity sha512-KEw/te+ylD2MHutzigafyptv0kdTU05Dbgxr9Y5J9IAQw8PbFz16nKtlPnJtA23BLp9fZQeNXzUmegkRi7fpDA==
+  dependencies:
+    fs-extra "^8.0.1"
+    jsonfile "^5.0.0"
+    twemoji-parser "13.1.0"
+    universalify "^0.1.2"
+
+"@elastic/elasticsearch@7.11.0":
+  version "7.11.0"
+  resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.11.0.tgz#e196243d0ed026742fc160d72cc5b4b5b6c7807d"
+  integrity sha512-AFVVuANIdbV1qYjuOi4hnsX/DehWYG+bbhQO4amq9K4/NnzU7mpGWOPgVlRQTiX+vBfBkx7SL6h4QEjIlM3ztA==
+  dependencies:
+    debug "^4.1.1"
+    hpagent "^0.1.1"
+    ms "^2.1.1"
+    pump "^3.0.0"
+    secure-json-parse "^2.1.0"
+
+"@eslint/eslintrc@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.4.tgz#dfe0ff7ba270848d10c5add0715e04964c034b31"
+  integrity sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==
+  dependencies:
+    ajv "^6.12.4"
+    debug "^4.3.2"
+    espree "^9.0.0"
+    globals "^13.9.0"
+    ignore "^4.0.6"
+    import-fresh "^3.2.1"
+    js-yaml "^4.1.0"
+    minimatch "^3.0.4"
+    strip-json-comments "^3.1.1"
+
+"@humanwhocodes/config-array@^0.6.0":
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.6.0.tgz#b5621fdb3b32309d2d16575456cbc277fa8f021a"
+  integrity sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==
+  dependencies:
+    "@humanwhocodes/object-schema" "^1.2.0"
+    debug "^4.1.1"
+    minimatch "^3.0.4"
+
+"@humanwhocodes/object-schema@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
+  integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
+
+"@jfonx/console-utils@^1.0.3":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@jfonx/console-utils/-/console-utils-1.0.3.tgz#cbb7f911e4191a4a2fe1ba4807d29f100b5d099f"
+  integrity sha512-/XbnqjWc7yNZVLAJJO9rimfIz9DYte+cj3EF9hwhIv7vw6ok2t3cjl0huYsmD89srKH03vWjeqAcIH86CuYj3g==
+  dependencies:
+    colors "^1.3.3"
+
+"@jfonx/file-utils@^3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@jfonx/file-utils/-/file-utils-3.0.1.tgz#8d3d6e931a283420fe29802ea71c28dd397cd8d3"
+  integrity sha512-qwH0CuzWmghtTHGMyuPHj6SJPQgWeiXFJBfrxCWMbzxVCa3aLZPEfzSdlSnC/UABsk6feRkNdHXw59rVshNPqw==
+  dependencies:
+    "@jfonx/console-utils" "^1.0.3"
+    comment-json "^4.1.0"
+    find-up "^4.1.0"
+
+"@koa/cors@3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.1.0.tgz#618bb073438cfdbd3ebd0e648a76e33b84f3a3b2"
+  integrity sha512-7ulRC1da/rBa6kj6P4g2aJfnET3z8Uf3SWu60cjbtxTA5g8lxRdX/Bd2P92EagGwwAhANeNw8T8if99rJliR6Q==
+  dependencies:
+    vary "^1.1.2"
+
+"@koa/multer@3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@koa/multer/-/multer-3.0.0.tgz#439777949f28097d7b329c0b4ce3048074c862f8"
+  integrity sha512-y+OQBmex5D1jIl723gAEUYcAWPEicIXppaAKw/zCMfpllQ08ZNweDPwoCLxEoatqd5pCu2XG6V8dl67JRq3RJw==
+
+"@koa/router@9.0.1":
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/@koa/router/-/router-9.0.1.tgz#4090a14223ea7e78aa13b632761209cba69acd95"
+  integrity sha512-OI+OU49CJV4px0WkIMmayBeqVXB/JS1ZMq7UoGlTZt6Y7ijK7kdeQ18+SEHHJPytmtI1y6Hf8XLrpxva3mhv5Q==
+  dependencies:
+    debug "^4.1.1"
+    http-errors "^1.7.3"
+    koa-compose "^4.1.0"
+    methods "^1.1.2"
+    path-to-regexp "^6.1.0"
+
+"@nodelib/fs.scandir@2.1.3":
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
+  integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.3"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3"
+  integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
+
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976"
+  integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.3"
+    fastq "^1.6.0"
+
+"@npmcli/move-file@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
+  integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
+  dependencies:
+    mkdirp "^1.0.4"
+    rimraf "^3.0.2"
+
+"@redocly/ajv@^8.6.2":
+  version "8.6.2"
+  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.2.tgz#8c4e485e72f7864f91fae40093bed548ec2619b2"
+  integrity sha512-tU8fQs0D76ZKhJ2cWtnfQthWqiZgGBx0gH0+5D8JvaBEBaqA8foPPBt3Nonwr3ygyv5xrw2IzKWgIY86BlGs+w==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+    uri-js "^4.2.2"
+
+"@redocly/openapi-core@1.0.0-beta.54":
+  version "1.0.0-beta.54"
+  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.54.tgz#42575a849c4dd54b9d0c6413fb8ca547e087cd11"
+  integrity sha512-uYs0N1Trjkh7u8IMIuCU2VxCXhMyGWSZUkP/WNdTR1OgBUtvNdF9C32zoQV+hyCIH4gVu42ROHkjisy333ZX+w==
+  dependencies:
+    "@redocly/ajv" "^8.6.2"
+    "@types/node" "^14.11.8"
+    colorette "^1.2.0"
+    js-levenshtein "^1.1.6"
+    js-yaml "^3.14.1"
+    lodash.isequal "^4.5.0"
+    minimatch "^3.0.4"
+    node-fetch "^2.6.1"
+    yaml-ast-parser "0.0.43"
+
+"@sindresorhus/is@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4"
+  integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==
+
+"@sinonjs/commons@^1.7.0":
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
+  integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==
+  dependencies:
+    type-detect "4.0.8"
+
+"@sinonjs/fake-timers@7.1.2":
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5"
+  integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+
+"@sqltools/formatter@^1.2.2":
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.3.tgz#1185726610acc37317ddab11c3c7f9066966bd20"
+  integrity sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==
+
+"@syuilo/aiscript@0.11.1":
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.11.1.tgz#52c14692113c58d1d62e6ae696352ba49abdf2eb"
+  integrity sha512-chwOIA3yLUKvOB0G611hjLArKTeOWNmTm3lHERSaDW1d+dS6do56naX6Lkwy2UpnwWC0qzeNSgg35elk6t2gZg==
+  dependencies:
+    autobind-decorator "2.4.0"
+    chalk "4.0.0"
+    seedrandom "3.0.5"
+    stringz "2.1.0"
+    uuid "7.0.3"
+
+"@szmarczak/http-timer@^4.0.5":
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152"
+  integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==
+  dependencies:
+    defer-to-connect "^2.0.0"
+
+"@tokenizer/token@^0.3.0":
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
+  integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
+
+"@tootallnate/once@1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
+  integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
+
+"@trysound/sax@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
+  integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
+
+"@tsconfig/node10@^1.0.7":
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.7.tgz#1eb1de36c73478a2479cc661ef5af1c16d86d606"
+  integrity sha512-aBvUmXLQbayM4w3A8TrjwrXs4DZ8iduJnuJLLRGdkWlyakCf1q6uHZJBzXoRA/huAEknG5tcUyQxN3A+In5euQ==
+
+"@tsconfig/node12@^1.0.7":
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.7.tgz#677bd9117e8164dc319987dd6ff5fc1ba6fbf18b"
+  integrity sha512-dgasobK/Y0wVMswcipr3k0HpevxFJLijN03A8mYfEPvWvOs14v0ZlYTR4kIgMx8g4+fTyTFv8/jLCIfRqLDJ4A==
+
+"@tsconfig/node14@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.0.tgz#5bd046e508b1ee90bc091766758838741fdefd6e"
+  integrity sha512-RKkL8eTdPv6t5EHgFKIVQgsDapugbuOptNd9OOunN/HAkzmmTnZELx1kNCK0rSdUYGmiFMM3rRQMAWiyp023LQ==
+
+"@tsconfig/node16@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
+  integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
+
+"@types/accepts@*":
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
+  integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
+  dependencies:
+    "@types/node" "*"
+
+"@types/anymatch@*":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
+  integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
+
+"@types/bcryptjs@2.4.2":
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae"
+  integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==
+
+"@types/body-parser@*":
+  version "1.19.0"
+  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
+  integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
+  dependencies:
+    "@types/connect" "*"
+    "@types/node" "*"
+
+"@types/bull@3.15.5":
+  version "3.15.5"
+  resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.5.tgz#a4459c127c5b10fb847531579a2cd5db35751366"
+  integrity sha512-XgJQWJ03jyKMfdoL8IAIoHIo7JkkL74kcxuujTONkSJswm0giIJ9kuVgDNHS0OvD+OiPNcFmbBl0H3scj2+A8A==
+  dependencies:
+    "@types/ioredis" "*"
+
+"@types/cacheable-request@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
+  integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==
+  dependencies:
+    "@types/http-cache-semantics" "*"
+    "@types/keyv" "*"
+    "@types/node" "*"
+    "@types/responselike" "*"
+
+"@types/cbor@6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@types/cbor/-/cbor-6.0.0.tgz#ddead015e14ef4463287d40cd92a6297a34dac8d"
+  integrity sha512-mGQ1lbYOwVti5Xlarn1bTeBZqgY0kstsdjnkoEovgohYKdBjGejHyNGXHdMBeqyQazIv32Jjp33+5pBEaSRy2w==
+  dependencies:
+    cbor "*"
+
+"@types/cheerio@0.22.18":
+  version "0.22.18"
+  resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.18.tgz#19018dceae691509901e339d63edf1e935978fe6"
+  integrity sha512-Fq7R3fINAPSdUEhOyjG4iVxgHrOnqDJbY0/BUuiN0pvD/rfmZWekVZnv+vcs8TtpA2XF50uv50LaE4EnpEL/Hw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/color-name@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+
+"@types/connect@*":
+  version "3.4.33"
+  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
+  integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
+  dependencies:
+    "@types/node" "*"
+
+"@types/content-disposition@*":
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.3.tgz#0aa116701955c2faa0717fc69cd1596095e49d96"
+  integrity sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg==
+
+"@types/cookies@*":
+  version "0.7.4"
+  resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.4.tgz#26dedf791701abc0e36b5b79a5722f40e455f87b"
+  integrity sha512-oTGtMzZZAVuEjTwCjIh8T8FrC8n/uwy+PG0yTvQcdZ7etoel7C7/3MSd7qrukENTgQtotG7gvBlBojuVs7X5rw==
+  dependencies:
+    "@types/connect" "*"
+    "@types/express" "*"
+    "@types/keygrip" "*"
+    "@types/node" "*"
+
+"@types/dateformat@3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
+  integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
+
+"@types/disposable-email-domains@^1.0.1":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/disposable-email-domains/-/disposable-email-domains-1.0.2.tgz#0280f6b38fa7f14e54b056a434135ecd254483b1"
+  integrity sha512-SDKwyYTjk3y5aZBxxc38yRecpJPjsqn57STz1bNxYYlv4k11bBe7QB8w4llXDTmQXKT1mFvgGmJv+8Zdu3YmJw==
+
+"@types/escape-regexp@0.0.0":
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.0.tgz#bff0225f9ef30d0dbdbe0e2a24283ee5342990c3"
+  integrity sha512-HTansGo4tJ7K7W9I9LBdQqnHtPB/Y7tlS+EMrkboaAQLsRPhRpHaqAHe01K1HVXM5e1u1IplRd8EBh+pJrp7Dg==
+
+"@types/eslint-scope@^3.7.0":
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86"
+  integrity sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==
+  dependencies:
+    "@types/eslint" "*"
+    "@types/estree" "*"
+
+"@types/eslint@*":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.0.tgz#eb5c5b575237334df24c53195e37b53d66478d7b"
+  integrity sha512-LpUXkr7fnmPXWGxB0ZuLEzNeTURuHPavkC5zuU4sg62/TgL5ZEjamr5Y8b6AftwHtx2bPJasI+CL0TT2JwQ7aA==
+  dependencies:
+    "@types/estree" "*"
+    "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^0.0.46":
+  version "0.0.46"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe"
+  integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==
+
+"@types/events@*":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
+  integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
+
+"@types/express-serve-static-core@*":
+  version "4.17.5"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz#a00ac7dadd746ae82477443e4d480a6a93ea083c"
+  integrity sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==
+  dependencies:
+    "@types/node" "*"
+    "@types/range-parser" "*"
+
+"@types/express@*":
+  version "4.17.6"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.6.tgz#6bce49e49570507b86ea1b07b806f04697fac45e"
+  integrity sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==
+  dependencies:
+    "@types/body-parser" "*"
+    "@types/express-serve-static-core" "*"
+    "@types/qs" "*"
+    "@types/serve-static" "*"
+
+"@types/fluent-ffmpeg@2.1.17":
+  version "2.1.17"
+  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.17.tgz#6958dda400fe1b33c21f3683db76905cb210d053"
+  integrity sha512-/bdvjKw/mtBHlJ2370d04nt4CsWqU5MrwS/NtO96V01jxitJ4+iq8OFNcqc5CegeV3TQOK3uueK02kvRK+zjUg==
+  dependencies:
+    "@types/node" "*"
+
+"@types/glob@*":
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
+  integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
+  dependencies:
+    "@types/events" "*"
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/glob@7.2.0":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
+  integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/http-assert@*":
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b"
+  integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==
+
+"@types/http-cache-semantics@*":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a"
+  integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==
+
+"@types/http-errors@*":
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.0.tgz#682477dbbbd07cd032731cb3b0e7eaee3d026b69"
+  integrity sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==
+
+"@types/ioredis@*":
+  version "4.14.9"
+  resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.14.9.tgz#774387d44d3ad60e1b849044b2b28b96e5813866"
+  integrity sha512-yNdzppM6vY4DYqXCnt4A3PXArxsMWeJCYxFlyl4AJKrNSGMEAP9TPcXR+8Q6zh9glcCtxmwMQhi4pwdqqHH3OA==
+  dependencies:
+    "@types/node" "*"
+
+"@types/is-url@1.2.30":
+  version "1.2.30"
+  resolved "https://registry.yarnpkg.com/@types/is-url/-/is-url-1.2.30.tgz#85567e8bee4fee69202bc3448f9fb34b0d56c50a"
+  integrity sha512-AnlNFwjzC8XLda5VjRl4ItSd8qp8pSNowvsut0WwQyBWHpOxjxRJm8iO6uETWqEyLdYdb9/1j+Qd9gQ4l5I4fw==
+
+"@types/js-yaml@4.0.4":
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.4.tgz#cc38781257612581a1a0eb25f1709d2b06812fce"
+  integrity sha512-AuHubXUmg0AzkXH0Mx6sIxeY/1C110mm/EkE/gB1sTRz3h2dao2W/63q42SlVST+lICxz5Oki2hzYA6+KnnieQ==
+
+"@types/jsdom@16.2.13":
+  version "16.2.13"
+  resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.13.tgz#126c8b7441b159d6234610a48de77b6066f1823f"
+  integrity sha512-8JQCjdeAidptSsOcRWk2iTm9wCcwn9l+kRG6k5bzUacrnm1ezV4forq0kWjUih/tumAeoG+OspOvQEbbRucBTw==
+  dependencies:
+    "@types/node" "*"
+    "@types/parse5" "*"
+    "@types/tough-cookie" "*"
+
+"@types/json-schema@*":
+  version "7.0.5"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
+  integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
+
+"@types/json-schema@^7.0.6":
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
+  integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
+
+"@types/json5@^0.0.29":
+  version "0.0.29"
+  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+  integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+
+"@types/jsonld@1.5.6":
+  version "1.5.6"
+  resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.6.tgz#4396c0b17128abf5773bb68b5453b88fc565b0d4"
+  integrity sha512-OUcfMjRie5IOrJulUQwVNvV57SOdKcTfBj3pjXNxzXqeOIrY2aGDNGW/Tlp83EQPkz4tCE6YWVrGuc/ZeaAQGg==
+
+"@types/keygrip@*":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
+  integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==
+
+"@types/keyv@*":
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"
+  integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/koa-bodyparser@4.3.3":
+  version "4.3.3"
+  resolved "https://registry.yarnpkg.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.3.tgz#9c7d4295576bc863d550002f732f1c57dd88cc58"
+  integrity sha512-/ileIpXsy1fFEzgZhZ07eZH8rAVL7jwuk/kaoVEfauO6s80g2LIDIJKEyDbuAL9S/BWflKzEC0PHD6aXkmaSbw==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa-compose@*":
+  version "3.2.5"
+  resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d"
+  integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa-cors@0.0.2":
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/@types/koa-cors/-/koa-cors-0.0.2.tgz#369c753fb383640f225579c70a4f9a286b4931b7"
+  integrity sha512-uNaDY26HUVO+2C6arK8ZFODs9mBjYprD8mlvkVe2bYdX9wzEeKtycVXPafXpUkePhMh4sffIMkhRDyedokG/QA==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa-favicon@2.0.21":
+  version "2.0.21"
+  resolved "https://registry.yarnpkg.com/@types/koa-favicon/-/koa-favicon-2.0.21.tgz#d8a0ed062a6f5e3f838fe09c21e8b3f0490369cd"
+  integrity sha512-paH1nheVhijx/VduoR/RCD/qTCiX+OI/6fHLi3mZae053Ts+gUBOrKtzl3pMTDbdEBqdLolfLje3PZbb6jW0jQ==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa-logger@3.1.2":
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@types/koa-logger/-/koa-logger-3.1.2.tgz#91e890f405ddb0626bc385767e4cc0cd7226d1a8"
+  integrity sha512-sioTA1xlKYiIgryANWPRHBkG3XGbWftw9slWADUPC+qvPIY/yRLSrhvX7zkJwMrntub5dPO0GuAoyGGf0yitfQ==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa-mount@4.0.1":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@types/koa-mount/-/koa-mount-4.0.1.tgz#2994be86eaa3d9dc97365e6ebfa227cee3c5f157"
+  integrity sha512-HNeg80CVS9Dfq8dGYqCZZCAUm7g6jPCNJ1ydqVLEJxLrjmeburpvq+lOZkE4rxBZ6O38dr3tj9IA3IfbdoI05w==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa-send@4.1.3":
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/@types/koa-send/-/koa-send-4.1.3.tgz#17193c6472ae9e5d1b99ae8086949cc4fd69179d"
+  integrity sha512-daaTqPZlgjIJycSTNjKpHYuKhXYP30atFc1pBcy6HHqB9+vcymDgYTguPdx9tO4HMOqNyz6bz/zqpxt5eLR+VA==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa-views@7.0.0":
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/@types/koa-views/-/koa-views-7.0.0.tgz#5613450c77ab69c980c47104378da4b7669c5f2e"
+  integrity sha512-AB/NB+oFHcLOZJYFv3bG5Af8YbwYCD9/zK0WcKALsbjI/FRKrcXTUTC64RebDrkyOkBm3bpCgpGndhAH/3YQ2Q==
+  dependencies:
+    koa-views "*"
+
+"@types/koa@*", "@types/koa@^2.13.1":
+  version "2.13.1"
+  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.1.tgz#e29877a6b5ad3744ab1024f6ec75b8cbf6ec45db"
+  integrity sha512-Qbno7FWom9nNqu0yHZ6A0+RWt4mrYBhw3wpBAQ3+IuzGcLlfeYkzZrnMq5wsxulN2np8M4KKeUpTodsOsSad5Q==
+  dependencies:
+    "@types/accepts" "*"
+    "@types/content-disposition" "*"
+    "@types/cookies" "*"
+    "@types/http-assert" "*"
+    "@types/http-errors" "*"
+    "@types/keygrip" "*"
+    "@types/koa-compose" "*"
+    "@types/node" "*"
+
+"@types/koa@2.13.4":
+  version "2.13.4"
+  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
+  integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==
+  dependencies:
+    "@types/accepts" "*"
+    "@types/content-disposition" "*"
+    "@types/cookies" "*"
+    "@types/http-assert" "*"
+    "@types/http-errors" "*"
+    "@types/keygrip" "*"
+    "@types/koa-compose" "*"
+    "@types/node" "*"
+
+"@types/koa__cors@3.0.3":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-3.0.3.tgz#49d75813b443ba3d4da28ea6cf6244b7e99a3b23"
+  integrity sha512-74Xb4hJOPGKlrQ4PRBk1A/p0gfLpgbnpT0o67OMVbwyeMXvlBN+ZCRztAAmkKZs+8hKbgMutUlZVbA52Hr/0IA==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa__multer@2.0.4":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@types/koa__multer/-/koa__multer-2.0.4.tgz#e0f0fd1800a46b51886bebab480a57100f2488b0"
+  integrity sha512-WRkshXhE5rpYFUbbtAjyMhdOOSdbu1XX+2AQlRNM6AZtgxd0/WXMU4lrP7e9tk5HWVTWbx8DOOsVBmfHjSGJ4w==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/koa__router@8.0.8":
+  version "8.0.8"
+  resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-8.0.8.tgz#b1e0e9a512498777d3366bbdf0e853df27ec831c"
+  integrity sha512-9pGCaDtzCsj4HJ8HmGuqzk8+s57sPj4njWd08GG5o92n5Xp9io2snc40CPpXFhoKcZ8OKhuu6ht4gNou9e1C2w==
+  dependencies:
+    "@types/koa" "*"
+
+"@types/mime@*":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
+  integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
+
+"@types/minimatch@*":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
+  integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+
+"@types/mocha@8.2.3":
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
+  integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
+
+"@types/node-fetch@2.5.12":
+  version "2.5.12"
+  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66"
+  integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==
+  dependencies:
+    "@types/node" "*"
+    form-data "^3.0.0"
+
+"@types/node@*":
+  version "16.6.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
+  integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
+
+"@types/node@16.11.7":
+  version "16.11.7"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
+  integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==
+
+"@types/node@^14.11.8":
+  version "14.17.9"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd"
+  integrity sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==
+
+"@types/nodemailer@6.4.4":
+  version "6.4.4"
+  resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
+  integrity sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/nprogress@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f"
+  integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A==
+
+"@types/oauth@0.9.1":
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.1.tgz#e17221e7f7936b0459ae7d006255dff61adca305"
+  integrity sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==
+  dependencies:
+    "@types/node" "*"
+
+"@types/parse5@*":
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
+  integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
+
+"@types/parse5@6.0.2":
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.2.tgz#99f6b72d82e34cea03a4d8f2ed72114d909c1c61"
+  integrity sha512-+hQX+WyJAOne7Fh3zF5CxPemILIbuhNcqHHodzK9caYOLnC8pD5efmPleRnw0z++LfKUC/sVNMwk0Gap+B0baA==
+
+"@types/parsimmon@1.10.6":
+  version "1.10.6"
+  resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.6.tgz#8fcf95990514d2a7624aa5f630c13bf2427f9cdd"
+  integrity sha512-FwAQwMRbkhx0J6YELkwIpciVzCcgEqXEbIrIn3a2P5d3kGEHQ3wVhlN3YdVepYP+bZzCYO6OjmD4o9TGOZ40rA==
+
+"@types/portscanner@2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@types/portscanner/-/portscanner-2.1.1.tgz#89d5094e16f3d941f20f3889dfa5d3a164b3dd3b"
+  integrity sha512-1NsVIbgBKvrqxwtMN0V6CLji1ERwKSI/RWz0J3y++CzSwYNGBStCfpIFgxV3ZwxsDR5PoZqoUWhwraDm+Ztn0Q==
+
+"@types/pug@2.0.5":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.5.tgz#69bc700934dd473c7ab97270bd2dbacefe562231"
+  integrity sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==
+
+"@types/punycode@2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83"
+  integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g==
+
+"@types/qrcode@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.1.tgz#0689f400c3a95d2db040c99c99834faa09ee9dc1"
+  integrity sha512-vxMyr7JM7tYPxu8vUE83NiosWX5DZieCyYeJRoOIg0pAkyofCBzknJ2ycUZkPGDFis2RS8GN/BeJLnRnAPxeCA==
+  dependencies:
+    "@types/node" "*"
+
+"@types/qs@*":
+  version "6.9.1"
+  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7"
+  integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==
+
+"@types/random-seed@0.3.3":
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/@types/random-seed/-/random-seed-0.3.3.tgz#7741f7b0a4513198a9396ce4ad25832f799a6727"
+  integrity sha512-kHsCbIRHNXJo6EN5W8EA5b4i1hdT6jaZke5crBPLUcLqaLdZ0QBq8QVMbafHzhjFF83Cl9qlee2dChD18d/kPg==
+
+"@types/range-parser@*":
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
+  integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
+
+"@types/ratelimiter@3.4.2":
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/@types/ratelimiter/-/ratelimiter-3.4.2.tgz#adf1a6d0cbe72d42207efc510a9170602e23456c"
+  integrity sha512-iz+yyY+ViphaM8ZwrX1mUQzelIeC59zyaaLKTJ0YVOOCkCpIYpaysiIM4z5Xv9HdXYqIb80S+DhH7J22A0rW2w==
+
+"@types/readable-stream@^2.3.9":
+  version "2.3.9"
+  resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.9.tgz#40a8349e6ace3afd2dd1b6d8e9b02945de4566a9"
+  integrity sha512-sqsgQqFT7HmQz/V5jH1O0fvQQnXAJO46Gg9LRO/JPfjmVmGUlcx831TZZO3Y3HtWhIkzf3kTsNT0Z0kzIhIvZw==
+  dependencies:
+    "@types/node" "*"
+    safe-buffer "*"
+
+"@types/redis@2.8.32":
+  version "2.8.32"
+  resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11"
+  integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==
+  dependencies:
+    "@types/node" "*"
+
+"@types/rename@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@types/rename/-/rename-1.0.4.tgz#30c6f0306042591a560361ea02639e89647dd173"
+  integrity sha512-eV81+6bVv2mdCBahkMefjEUwAjKDAP3AuyhqWCWRxcRaeVdUeHUBaoq2zSz+5HNHF2jzTajMcfLvJsy4K3cbwA==
+
+"@types/request-stats@3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/request-stats/-/request-stats-3.0.0.tgz#d3909a9f778b8ae0b42fb8c1ed20cb936ed95f99"
+  integrity sha512-POsDF7nETH8up49iBNvbZuO0pEk9F+TG0rXCkvjxCClcOS99xfF+mKmJteYlwKYpuRKkixzysKlL8rwN1hU2lw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/responselike@*", "@types/responselike@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
+  integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
+  dependencies:
+    "@types/node" "*"
+
+"@types/rimraf@3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8"
+  integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==
+  dependencies:
+    "@types/glob" "*"
+    "@types/node" "*"
+
+"@types/rsvp@^4.0.4":
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/@types/rsvp/-/rsvp-4.0.4.tgz#55e93e7054027f1ad4b4ebc1e60e59eb091e2d32"
+  integrity sha512-J3Ol++HCC7/hwZhanDvggFYU/GtxHxE/e7cGRWxR04BF7Tt3TqJZ84BkzQgDxmX0uu8IagiyfmfoUlBACh2Ilg==
+
+"@types/seedrandom@2.4.28":
+  version "2.4.28"
+  resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f"
+  integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA==
+
+"@types/serve-static@*":
+  version "1.13.3"
+  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1"
+  integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==
+  dependencies:
+    "@types/express-serve-static-core" "*"
+    "@types/mime" "*"
+
+"@types/sharp@0.29.3":
+  version "0.29.3"
+  resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.3.tgz#54ceb81b68bb99a0e62db2b52f4fb0bea84878ac"
+  integrity sha512-83Xp05eK2hvfNnmKLr2Fz0C2A0jrr2TnSLqKRbkLTYuAu+Erj6mKQLoEMGafE73Om8p3q3ryZxtHFM/7hy4Adg==
+  dependencies:
+    "@types/node" "*"
+
+"@types/sinonjs__fake-timers@6.0.4":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
+  integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
+
+"@types/source-list-map@*":
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
+  integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
+
+"@types/speakeasy@2.0.6":
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/@types/speakeasy/-/speakeasy-2.0.6.tgz#12540f7b64d08180393ae2c5c8c280866a85da61"
+  integrity sha512-2wIXZp5yJUddhsSZarYCZIakCvzwQgTVdtT29DYVdFzc0cHttanaQx9THRhtjY4kDqVaF2jhyFOEofozOioFdQ==
+  dependencies:
+    "@types/node" "*"
+
+"@types/tapable@^1":
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4"
+  integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ==
+
+"@types/throttle-debounce@2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776"
+  integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
+
+"@types/tinycolor2@1.4.3":
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706"
+  integrity sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==
+
+"@types/tmp@0.2.2":
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.2.tgz#424537a3b91828cb26aaf697f21ae3cd1b69f7e7"
+  integrity sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==
+
+"@types/tough-cookie@*":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
+  integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
+
+"@types/uglify-js@*":
+  version "3.9.0"
+  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.0.tgz#4490a140ca82aa855ad68093829e7fd6ae94ea87"
+  integrity sha512-3ZcoyPYHVOCcLpnfZwD47KFLr8W/mpUcgjpf1M4Q78TMJIw7KMAHSjiCLJp1z3ZrBR9pTLbe191O0TldFK5zcw==
+  dependencies:
+    source-map "^0.6.1"
+
+"@types/uuid@8.3.1":
+  version "8.3.1"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
+  integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
+
+"@types/web-push@3.3.2":
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.3.2.tgz#8c32434147c0396415862e86405c9edc9c50fc15"
+  integrity sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/webpack-sources@*":
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.7.tgz#0a330a9456113410c74a5d64180af0cbca007141"
+  integrity sha512-XyaHrJILjK1VHVC4aVlKsdNN5KBTwufMb43cQs+flGxtPAf/1Qwl8+Q0tp5BwEGaI8D6XT1L+9bSWXckgkjTLw==
+  dependencies:
+    "@types/node" "*"
+    "@types/source-list-map" "*"
+    source-map "^0.6.1"
+
+"@types/webpack-stream@3.2.12":
+  version "3.2.12"
+  resolved "https://registry.yarnpkg.com/@types/webpack-stream/-/webpack-stream-3.2.12.tgz#cf13e64067a662a7acd8cd0524b3f64c86b0ecb6"
+  integrity sha512-znMUl4kKT0V0SwkUgRgwUNSAO7J5I/jdTCBNy3utkCsgMJ3IHp4FBTDwsQC+tfQ73TWeKIH05QNmbUYmeGThGw==
+  dependencies:
+    "@types/node" "*"
+    "@types/webpack" "^4"
+
+"@types/webpack@5.28.0":
+  version "5.28.0"
+  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03"
+  integrity sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==
+  dependencies:
+    "@types/node" "*"
+    tapable "^2.2.0"
+    webpack "^5"
+
+"@types/webpack@^4":
+  version "4.41.27"
+  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.27.tgz#f47da488c8037e7f1b2dbf2714fbbacb61ec0ffc"
+  integrity sha512-wK/oi5gcHi72VMTbOaQ70VcDxSQ1uX8S2tukBK9ARuGXrYM/+u4ou73roc7trXDNmCxCoerE8zruQqX/wuHszA==
+  dependencies:
+    "@types/anymatch" "*"
+    "@types/node" "*"
+    "@types/tapable" "^1"
+    "@types/uglify-js" "*"
+    "@types/webpack-sources" "*"
+    source-map "^0.6.0"
+
+"@types/websocket@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8"
+  integrity sha512-qn1LkcFEKK8RPp459jkjzsfpbsx36BBt3oC3pITYtkoBw/aVX+EZFa5j3ThCRTNpLFvIMr5dSTD4RaMdilIOpA==
+  dependencies:
+    "@types/node" "*"
+
+"@types/ws@8.2.0":
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.0.tgz#75faefbe2328f3b833cb8dc640658328990d04f3"
+  integrity sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==
+  dependencies:
+    "@types/node" "*"
+
+"@types/zen-observable@^0.8.2":
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71"
+  integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg==
+
+"@typescript-eslint/parser@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.1.0.tgz#6c7f837d210d2bc0a811e7ea742af414f4e00908"
+  integrity sha512-vx1P+mhCtYw3+bRHmbalq/VKP2Y3gnzNgxGxfEWc6OFpuEL7iQdAeq11Ke3Rhy8NjgB+AHsIWEwni3e+Y7djKA==
+  dependencies:
+    "@typescript-eslint/scope-manager" "5.1.0"
+    "@typescript-eslint/types" "5.1.0"
+    "@typescript-eslint/typescript-estree" "5.1.0"
+    debug "^4.3.2"
+
+"@typescript-eslint/scope-manager@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.1.0.tgz#6f1f26ad66a8f71bbb33b635e74fec43f76b44df"
+  integrity sha512-yYlyVjvn5lvwCL37i4hPsa1s0ORsjkauhTqbb8MnpvUs7xykmcjGqwlNZ2Q5QpoqkJ1odlM2bqHqJwa28qV6Tw==
+  dependencies:
+    "@typescript-eslint/types" "5.1.0"
+    "@typescript-eslint/visitor-keys" "5.1.0"
+
+"@typescript-eslint/types@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.1.0.tgz#a8a75ddfc611660de6be17d3ad950302385607a9"
+  integrity sha512-sEwNINVxcB4ZgC6Fe6rUyMlvsB2jvVdgxjZEjQUQVlaSPMNamDOwO6/TB98kFt4sYYfNhdhTPBEQqNQZjMMswA==
+
+"@typescript-eslint/typescript-estree@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.1.0.tgz#132aea34372df09decda961cb42457433aa6e83d"
+  integrity sha512-SSz+l9YrIIsW4s0ZqaEfnjl156XQ4VRmJsbA0ZE1XkXrD3cRpzuZSVCyqeCMR3EBjF27IisWakbBDGhGNIOvfQ==
+  dependencies:
+    "@typescript-eslint/types" "5.1.0"
+    "@typescript-eslint/visitor-keys" "5.1.0"
+    debug "^4.3.2"
+    globby "^11.0.4"
+    is-glob "^4.0.3"
+    semver "^7.3.5"
+    tsutils "^3.21.0"
+
+"@typescript-eslint/visitor-keys@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.1.0.tgz#e01a01b27eb173092705ae983aa1451bd1842630"
+  integrity sha512-uqNXepKBg81JVwjuqAxYrXa1Ql/YDzM+8g/pS+TCPxba0wZttl8m5DkrasbfnmJGHs4lQ2jTbcZ5azGhI7kK+w==
+  dependencies:
+    "@typescript-eslint/types" "5.1.0"
+    eslint-visitor-keys "^3.0.0"
+
+"@ungap/promise-all-settled@1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
+  integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
+
+"@webassemblyjs/ast@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f"
+  integrity sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==
+  dependencies:
+    "@webassemblyjs/helper-numbers" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz#34d62052f453cd43101d72eab4966a022587947c"
+  integrity sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==
+
+"@webassemblyjs/helper-api-error@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz#aaea8fb3b923f4aaa9b512ff541b013ffb68d2d4"
+  integrity sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==
+
+"@webassemblyjs/helper-buffer@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz#d026c25d175e388a7dbda9694e91e743cbe9b642"
+  integrity sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==
+
+"@webassemblyjs/helper-numbers@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz#7ab04172d54e312cc6ea4286d7d9fa27c88cd4f9"
+  integrity sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==
+  dependencies:
+    "@webassemblyjs/floating-point-hex-parser" "1.11.0"
+    "@webassemblyjs/helper-api-error" "1.11.0"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz#85fdcda4129902fe86f81abf7e7236953ec5a4e1"
+  integrity sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==
+
+"@webassemblyjs/helper-wasm-section@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz#9ce2cc89300262509c801b4af113d1ca25c1a75b"
+  integrity sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-buffer" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+    "@webassemblyjs/wasm-gen" "1.11.0"
+
+"@webassemblyjs/ieee754@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz#46975d583f9828f5d094ac210e219441c4e6f5cf"
+  integrity sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.0.tgz#f7353de1df38aa201cba9fb88b43f41f75ff403b"
+  integrity sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.0.tgz#86e48f959cf49e0e5091f069a709b862f5a2cadf"
+  integrity sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==
+
+"@webassemblyjs/wasm-edit@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz#ee4a5c9f677046a210542ae63897094c2027cb78"
+  integrity sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-buffer" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+    "@webassemblyjs/helper-wasm-section" "1.11.0"
+    "@webassemblyjs/wasm-gen" "1.11.0"
+    "@webassemblyjs/wasm-opt" "1.11.0"
+    "@webassemblyjs/wasm-parser" "1.11.0"
+    "@webassemblyjs/wast-printer" "1.11.0"
+
+"@webassemblyjs/wasm-gen@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe"
+  integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+    "@webassemblyjs/ieee754" "1.11.0"
+    "@webassemblyjs/leb128" "1.11.0"
+    "@webassemblyjs/utf8" "1.11.0"
+
+"@webassemblyjs/wasm-opt@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz#1638ae188137f4bb031f568a413cd24d32f92978"
+  integrity sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-buffer" "1.11.0"
+    "@webassemblyjs/wasm-gen" "1.11.0"
+    "@webassemblyjs/wasm-parser" "1.11.0"
+
+"@webassemblyjs/wasm-parser@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz#3e680b8830d5b13d1ec86cc42f38f3d4a7700754"
+  integrity sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-api-error" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+    "@webassemblyjs/ieee754" "1.11.0"
+    "@webassemblyjs/leb128" "1.11.0"
+    "@webassemblyjs/utf8" "1.11.0"
+
+"@webassemblyjs/wast-printer@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz#680d1f6a5365d6d401974a8e949e05474e1fab7e"
+  integrity sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@xtuc/long" "4.2.2"
+
+"@xtuc/ieee754@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+  integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+  integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+abab@^2.0.3, abab@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
+  integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
+
+abbrev@1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+  integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+abort-controller@3.0.0, abort-controller@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
+  integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+  dependencies:
+    event-target-shim "^5.0.0"
+
+accepts@^1.3.5:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
+  integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
+  dependencies:
+    mime-types "~2.1.24"
+    negotiator "0.6.2"
+
+acorn-globals@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
+  integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==
+  dependencies:
+    acorn "^7.1.1"
+    acorn-walk "^7.1.1"
+
+acorn-jsx@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
+  integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
+
+acorn-walk@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
+  integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
+
+acorn-walk@^8.1.1:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.1.tgz#3ddab7f84e4a7e2313f6c414c5b7dac85f4e3ebc"
+  integrity sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==
+
+acorn@^7.1.1:
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+
+acorn@^8.0.4:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe"
+  integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==
+
+acorn@^8.2.4:
+  version "8.2.4"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.4.tgz#caba24b08185c3b56e3168e97d15ed17f4d31fd0"
+  integrity sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg==
+
+acorn@^8.4.1:
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
+  integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
+
+acorn@^8.5.0:
+  version "8.5.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
+  integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
+
+agent-base@6:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+  dependencies:
+    debug "4"
+
+agentkeepalive@^4.1.3:
+  version "4.1.4"
+  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b"
+  integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ==
+  dependencies:
+    debug "^4.1.0"
+    depd "^1.1.2"
+    humanize-ms "^1.2.1"
+
+aggregate-error@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
+  integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
+  dependencies:
+    clean-stack "^2.0.0"
+    indent-string "^4.0.0"
+
+ajv-keywords@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5:
+  version "6.12.5"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
+  integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+alphanum-sort@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+  integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
+ansi-colors@4.1.1, ansi-colors@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
+ansi-regex@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+  integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+  integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+
+ansi-regex@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
+  integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  dependencies:
+    "@types/color-name" "^1.1.1"
+    color-convert "^2.0.1"
+
+any-promise@^1.0.0, any-promise@^1.1.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
+  integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
+
+anymatch@~3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+  integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+app-root-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad"
+  integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==
+
+append-field@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
+  integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=
+
+aproba@^1.0.3:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+  integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+are-we-there-yet@~1.1.2:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+  integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+  dependencies:
+    delegates "^1.0.0"
+    readable-stream "^2.0.6"
+
+arg@^4.1.0:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+  integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+  dependencies:
+    sprintf-js "~1.0.2"
+
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+array-timsort@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926"
+  integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==
+
+array-union@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
+asap@~2.0.3:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+  integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+
+asn1.js@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.3.0.tgz#439099fe9174e09cff5a54a9dda70260517e8689"
+  integrity sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==
+  dependencies:
+    bn.js "^4.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+    safer-buffer "^2.1.0"
+
+asn1@~0.2.3:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+  integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+  dependencies:
+    safer-buffer "~2.1.0"
+
+assert-never@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe"
+  integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+async@>=0.2.9:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
+  integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
+
+async@^2.6.0:
+  version "2.6.3"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
+  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
+  dependencies:
+    lodash "^4.17.14"
+
+async@^3.2.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
+  integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+autobind-decorator@2.4.0, autobind-decorator@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.4.0.tgz#ea9e1c98708cf3b5b356f7cf9f10f265ff18239c"
+  integrity sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw==
+
+autosize@4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03"
+  integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==
+
+autwh@0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/autwh/-/autwh-0.1.0.tgz#24a5300923309d105133401a2568f9c8ab7d7e03"
+  integrity sha512-IkGZ4kjVlZMkEmDiVtZpGG3lDGHPqsMBIh4IpQKN7idYOJ5EGedqKPO+ychNqh8zrJEEqYsN0NcBkcmoE2uFAw==
+  dependencies:
+    oauth "0.9.15"
+
+aws-sdk@2.1013.0:
+  version "2.1013.0"
+  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1013.0.tgz#85babc473b0bc90cc1160eb48baf616ddb86e346"
+  integrity sha512-TXxkp/meAdofpC15goFpNuur7fvh/mcMRfHJoP1jYzTtD0wcoB4FK16GLcny0uDYgkQgZuiO9QYv3Rq5bhGCqQ==
+  dependencies:
+    buffer "4.9.2"
+    events "1.1.1"
+    ieee754 "1.1.13"
+    jmespath "0.15.0"
+    querystring "0.2.0"
+    sax "1.2.1"
+    url "0.10.3"
+    uuid "3.3.2"
+    xml2js "0.4.19"
+
+aws-sign2@~0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
+  integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+
+axios@^0.19.2:
+  version "0.19.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
+  integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+  dependencies:
+    follow-redirects "1.5.10"
+
+babel-walk@3.0.0-canary-5:
+  version "3.0.0-canary-5"
+  resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11"
+  integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==
+  dependencies:
+    "@babel/types" "^7.9.6"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base32.js@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.0.1.tgz#d045736a57b1f6c139f0c7df42518a84e91bb2ba"
+  integrity sha1-0EVzalex9sE58MffQlGKhOkbsro=
+
+base64-js@^1.0.2:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
+  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+  integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+  dependencies:
+    tweetnacl "^0.14.3"
+
+bcryptjs@2.4.3:
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
+  integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
+
+big-integer@^1.6.16:
+  version "1.6.48"
+  resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
+  integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
+
+big.js@^5.2.2:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+  integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
+binary-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
+  integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
+
+bl@^4.0.1, bl@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
+  integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==
+  dependencies:
+    buffer "^5.5.0"
+    inherits "^2.0.4"
+    readable-stream "^3.4.0"
+
+bluebird@^3.7.2:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+blurhash@1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244"
+  integrity sha512-MXIPz6zwYUKayju+Uidf83KhH0vodZfeRl6Ich8Gu+KGl0JgKiFq9LsfqV7cVU5fKD/AotmduZqvOfrGKOfTaA==
+
+bn.js@^4.0.0:
+  version "4.11.8"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+  integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+
+boolbase@^1.0.0, boolbase@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^3.0.1, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+broadcast-channel@4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.5.0.tgz#d4717c493e219908fcb7f2f9078fe0baf95b77c1"
+  integrity sha512-jp+VPlQ1HyR0CM3uIYUrdpXupBvhTMFRkjR6mEmt5W4HaGDPFEzrO2Jqvi2PZ6zCC4zwLeco7CC5EUJPrVH8Tw==
+  dependencies:
+    "@babel/runtime" "^7.16.0"
+    detect-node "^2.1.0"
+    microseconds "0.2.0"
+    nano-time "1.0.0"
+    oblivious-set "1.0.0"
+    rimraf "3.0.2"
+    unload "2.3.1"
+
+browser-process-hrtime@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
+  integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+
+browser-stdout@1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+  integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+
+browserslist@^4.0.0, browserslist@^4.14.5:
+  version "4.16.3"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
+  integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
+  dependencies:
+    caniuse-lite "^1.0.30001181"
+    colorette "^1.2.1"
+    electron-to-chromium "^1.3.649"
+    escalade "^3.1.1"
+    node-releases "^1.1.70"
+
+browserslist@^4.16.0:
+  version "4.16.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.4.tgz#7ebf913487f40caf4637b892b268069951c35d58"
+  integrity sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==
+  dependencies:
+    caniuse-lite "^1.0.30001208"
+    colorette "^1.2.2"
+    electron-to-chromium "^1.3.712"
+    escalade "^3.1.1"
+    node-releases "^1.1.71"
+
+browserslist@^4.16.6:
+  version "4.16.6"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
+  integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
+  dependencies:
+    caniuse-lite "^1.0.30001219"
+    colorette "^1.2.2"
+    electron-to-chromium "^1.3.723"
+    escalade "^3.1.1"
+    node-releases "^1.1.71"
+
+bs-logger@0.x:
+  version "0.2.6"
+  resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
+  integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
+  dependencies:
+    fast-json-stable-stringify "2.x"
+
+buffer-alloc-unsafe@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
+  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+
+buffer-alloc@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
+  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+  dependencies:
+    buffer-alloc-unsafe "^1.1.0"
+    buffer-fill "^1.0.0"
+
+buffer-equal-constant-time@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+  integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
+
+buffer-fill@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
+  integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
+
+buffer-from@1.x:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+buffer-from@^1.0.0, buffer-from@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+buffer-writer@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
+  integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
+
+buffer@4.9.2:
+  version "4.9.2"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
+  integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+    isarray "^1.0.0"
+
+buffer@^5.4.3, buffer@^5.5.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
+  integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+
+buffer@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
+  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.2.1"
+
+bufferutil@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
+  integrity sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==
+  dependencies:
+    node-gyp-build "~3.7.0"
+
+builtin-modules@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+  integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+
+bull@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/bull/-/bull-4.1.0.tgz#ff8f628694e7dbbdf89b6a6860a1f33e990527fd"
+  integrity sha512-rQcLuAmzZIv1dHJO/yKrWu497xcTxMpYeTEBfpStrJFZ1UZpBSGHSx+defbtFVeGEeY8Pn0aMGRvRtldUBVUyQ==
+  dependencies:
+    cron-parser "^2.13.0"
+    debuglog "^1.0.0"
+    get-port "^5.1.1"
+    ioredis "^4.27.0"
+    lodash "^4.17.21"
+    p-timeout "^3.2.0"
+    promise.prototype.finally "^3.1.2"
+    semver "^7.3.2"
+    util.promisify "^1.0.1"
+    uuid "^8.3.0"
+
+busboy@^0.2.11:
+  version "0.2.14"
+  resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
+  integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=
+  dependencies:
+    dicer "0.2.5"
+    readable-stream "1.1.x"
+
+bytes@3.1.0, bytes@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
+  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
+cacache@^15.0.5:
+  version "15.1.0"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.1.0.tgz#164c2f857ee606e4cc793c63018fefd0ea5eba7b"
+  integrity sha512-mfx0C+mCfWjD1PnwQ9yaOrwG1ou9FkKnx0SvzUHWdFt7r7GaRtzT+9M8HAvLu62zIHtnpQ/1m93nWNDCckJGXQ==
+  dependencies:
+    "@npmcli/move-file" "^1.0.1"
+    chownr "^2.0.0"
+    fs-minipass "^2.0.0"
+    glob "^7.1.4"
+    infer-owner "^1.0.4"
+    lru-cache "^6.0.0"
+    minipass "^3.1.1"
+    minipass-collect "^1.0.2"
+    minipass-flush "^1.0.5"
+    minipass-pipeline "^1.2.2"
+    mkdirp "^1.0.3"
+    p-map "^4.0.0"
+    promise-inflight "^1.0.1"
+    rimraf "^3.0.2"
+    ssri "^8.0.1"
+    tar "^6.0.2"
+    unique-filename "^1.1.1"
+
+cache-content-type@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
+  integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==
+  dependencies:
+    mime-types "^2.1.18"
+    ylru "^1.2.0"
+
+cacheable-lookup@6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz#65c0e51721bb7f9f2cb513aed6da4a1b93ad7dc8"
+  integrity sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==
+
+cacheable-lookup@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3"
+  integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==
+
+cacheable-request@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58"
+  integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==
+  dependencies:
+    clone-response "^1.0.2"
+    get-stream "^5.1.0"
+    http-cache-semantics "^4.0.0"
+    keyv "^4.0.0"
+    lowercase-keys "^2.0.0"
+    normalize-url "^4.1.0"
+    responselike "^2.0.0"
+
+cafy@15.2.1:
+  version "15.2.1"
+  resolved "https://registry.yarnpkg.com/cafy/-/cafy-15.2.1.tgz#5a55eaeb721c604c7dca652f3d555c392e5f995a"
+  integrity sha512-g2zOmFb63p6XcZ/zeMWKYP8YKQYNWnhJmi6K71Ql4EAFTAay31xF0PBPtdBCCfQ0fiETgWTMxKtySAVI/Od6aQ==
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+camelcase@^6.0.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
+  integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
+
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0:
+  version "1.0.30001048"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz#4bb4f1bc2eb304e5e1154da80b93dee3f1cf447e"
+  integrity sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg==
+
+caniuse-lite@^1.0.30001181:
+  version "1.0.30001191"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz#bacb432b6701f690c8c5f7c680166b9a9f0843d9"
+  integrity sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==
+
+caniuse-lite@^1.0.30001208:
+  version "1.0.30001208"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9"
+  integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==
+
+caniuse-lite@^1.0.30001219:
+  version "1.0.30001230"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71"
+  integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==
+
+canonicalize@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.1.tgz#657b4f3fa38a6ecb97a9e5b7b26d7a19cc6e0da9"
+  integrity sha512-N3cmB3QLhS5TJ5smKFf1w42rJXWe6C1qP01z4dxJiI5v269buii4fLHWETDyf7yEd0azGLNC63VxNMiPd2u0Cg==
+
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+  integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+cbor@*:
+  version "7.0.5"
+  resolved "https://registry.yarnpkg.com/cbor/-/cbor-7.0.5.tgz#ed54cdbc19fa7352bb328d00a5393aa7ce45a10f"
+  integrity sha512-0aaAPgW92lLmypb9iCd22k7tSD1FbF6dps8VQzmIBKY6ych2gO09b2vo/SbaLTmezJuB8Kh88Rvpl/Uq52mNZg==
+  dependencies:
+    "@cto.af/textdecoder" "^0.0.0"
+    nofilter "^2.0.3"
+
+cbor@8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/cbor/-/cbor-8.1.0.tgz#cfc56437e770b73417a2ecbfc9caf6b771af60d5"
+  integrity sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==
+  dependencies:
+    nofilter "^3.1.0"
+
+chalk@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
+  integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chalk@4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^4.0.0, chalk@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
+  integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+char-regex@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
+  integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
+
+character-parser@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
+  integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A=
+  dependencies:
+    is-regex "^1.0.3"
+
+chart.js@3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.6.0.tgz#a87fce8431d4e7c5523d721f487f53aada1e42fe"
+  integrity sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ==
+
+chartjs-adapter-date-fns@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
+  integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
+
+chartjs-plugin-zoom@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.1.1.tgz#8a28923a17fcb5eb57a0dc94c5113bf402677647"
+  integrity sha512-1q54WOzK7FtAjkbemQeqvmFUV0btNYIQny2HbQ6Awq9wUtCz7Zmj6vIgp3C1DYMQwN0nqgpC3vnApqiwI7cSdQ==
+  dependencies:
+    hammerjs "^2.0.8"
+
+cheerio-httpcli@0.8.2:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/cheerio-httpcli/-/cheerio-httpcli-0.8.2.tgz#0189bda71c8bd2852de78e154291e2288184fbf2"
+  integrity sha512-grIzTwQg/nE7Oy6VvL19pf0UlM6wiluy/AOpXfQLVFrSi21F8wnO3dLchtaH2hfMF6jz68ot0/ngyQQVrp2VTw==
+  dependencies:
+    "@types/cheerio" "0.22.18"
+    "@types/rsvp" "^4.0.4"
+    async "^3.2.0"
+    cheerio "^0.22.0"
+    colors "^1.4.0"
+    foreach "^2.0.5"
+    he "^1.2.0"
+    iconv-lite "^0.6.3"
+    import-fresh "^3.3.0"
+    jschardet "^3.0.0"
+    object-assign "^4.1.1"
+    os-locale "^5.0.0"
+    prettyjson "^1.2.1"
+    request "^2.88.2"
+    rsvp "^4.8.5"
+    tough-cookie "^2.5.0"
+    type-of "^2.0.1"
+    valid-url "^1.0.9"
+
+cheerio@^0.22.0:
+  version "0.22.0"
+  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
+  integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=
+  dependencies:
+    css-select "~1.2.0"
+    dom-serializer "~0.1.0"
+    entities "~1.1.1"
+    htmlparser2 "^3.9.1"
+    lodash.assignin "^4.0.9"
+    lodash.bind "^4.1.4"
+    lodash.defaults "^4.0.1"
+    lodash.filter "^4.4.0"
+    lodash.flatten "^4.2.0"
+    lodash.foreach "^4.3.0"
+    lodash.map "^4.4.0"
+    lodash.merge "^4.4.0"
+    lodash.pick "^4.2.1"
+    lodash.reduce "^4.4.0"
+    lodash.reject "^4.4.0"
+    lodash.some "^4.4.0"
+
+chokidar@3.5.1, chokidar@^3.3.1, chokidar@^3.5.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
+  integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.3.0"
+  optionalDependencies:
+    fsevents "~2.1.2"
+
+chownr@^1.1.1:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+chownr@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
+  integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
+
+chrome-trace-event@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
+  integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==
+  dependencies:
+    tslib "^1.9.0"
+
+clean-stack@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
+  integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+
+cli-highlight@2.1.11, cli-highlight@^2.1.11:
+  version "2.1.11"
+  resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf"
+  integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==
+  dependencies:
+    chalk "^4.0.0"
+    highlight.js "^10.7.1"
+    mz "^2.4.0"
+    parse5 "^5.1.1"
+    parse5-htmlparser2-tree-adapter "^6.0.0"
+    yargs "^16.0.0"
+
+cliui@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+  dependencies:
+    string-width "^3.1.0"
+    strip-ansi "^5.2.0"
+    wrap-ansi "^5.1.0"
+
+cliui@^7.0.2:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^7.0.0"
+
+clone-response@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
+  integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
+  dependencies:
+    mimic-response "^1.0.0"
+
+cluster-key-slot@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
+  integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
+
+co-body@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124"
+  integrity sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==
+  dependencies:
+    inflation "^2.0.0"
+    qs "^6.4.0"
+    raw-body "^2.2.0"
+    type-is "^1.6.14"
+
+co-body@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.0.0.tgz#965b9337d7f5655480787471f4237664820827e3"
+  integrity sha512-9ZIcixguuuKIptnY8yemEOuhb71L/lLf+Rl5JfJEUiDNJk0e02MBt7BPxR2GEh5mw8dPthQYR4jPI/BnS1MQgw==
+  dependencies:
+    inflation "^2.0.0"
+    qs "^6.5.2"
+    raw-body "^2.3.3"
+    type-is "^1.6.16"
+
+co@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+  integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
+code-point-at@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+  integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@^1.0.0, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
+  integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
+
+color@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67"
+  integrity sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==
+  dependencies:
+    color-convert "^2.0.1"
+    color-string "^1.6.0"
+
+colord@^2.9.1:
+  version "2.9.1"
+  resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e"
+  integrity sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw==
+
+colorette@^1.2.0, colorette@^1.2.1, colorette@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
+  integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
+
+colors@^1.1.2, colors@^1.3.3, colors@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+
+combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+commander@^2.12.1, commander@^2.19.0, commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@^6.2.1:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+  integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
+
+commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+comment-json@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.1.1.tgz#49df4948704bebb1cc0ffa6910e25669b668b7c5"
+  integrity sha512-v8gmtPvxhBlhdRBLwdHSjGy9BgA23t9H1FctdQKyUrErPjSrJcdDMqBq9B4Irtm7w3TNYLQJNH6ARKnpyag1sA==
+  dependencies:
+    array-timsort "^1.0.3"
+    core-util-is "^1.0.2"
+    esprima "^4.0.1"
+    has-own-prop "^2.0.0"
+    repeat-string "^1.6.1"
+
+compare-versions@3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
+  integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concat-stream@^1.5.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+  dependencies:
+    buffer-from "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
+
+concurrently@6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.3.0.tgz#63128cb4a6ed54d3c0ed8528728590a5fe54582a"
+  integrity sha512-k4k1jQGHHKsfbqzkUszVf29qECBrkvBKkcPJEUDTyVR7tZd1G/JOfnst4g1sYbFvJ4UjHZisj1aWQR8yLKpGPw==
+  dependencies:
+    chalk "^4.1.0"
+    date-fns "^2.16.1"
+    lodash "^4.17.21"
+    rxjs "^6.6.3"
+    spawn-command "^0.0.2-1"
+    supports-color "^8.1.0"
+    tree-kill "^1.2.2"
+    yargs "^16.2.0"
+
+condense-newlines@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f"
+  integrity sha1-PemFVTE5R10yUCyDsC9gaE0kxV8=
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-whitespace "^0.3.0"
+    kind-of "^3.0.2"
+
+config-chain@^1.1.12:
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
+  integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
+  dependencies:
+    ini "^1.3.4"
+    proto-list "~1.2.1"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+  integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
+consolidate@^0.16.0:
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16"
+  integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==
+  dependencies:
+    bluebird "^3.7.2"
+
+constantinople@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151"
+  integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==
+  dependencies:
+    "@babel/parser" "^7.6.0"
+    "@babel/types" "^7.6.1"
+
+content-disposition@0.5.3, content-disposition@~0.5.2:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
+  integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+  dependencies:
+    safe-buffer "5.1.2"
+
+content-type@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+  integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+cookies@~0.8.0:
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
+  integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
+  dependencies:
+    depd "~2.0.0"
+    keygrip "~1.1.0"
+
+copy-to@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5"
+  integrity sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+core-util-is@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+crc-32@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
+  integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==
+  dependencies:
+    exit-on-epipe "~1.0.1"
+    printj "~1.1.0"
+
+create-require@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
+  integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+
+cron-parser@^2.13.0:
+  version "2.13.0"
+  resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.13.0.tgz#6f930bb6f2931790d2a9eec83b3ec276e27a6725"
+  integrity sha512-UWeIpnRb0eyoWPVk+pD3TDpNx3KCFQeezO224oJIkktBrcW6RoAPOx5zIKprZGfk6vcYSmA8yQXItejSaDBhbQ==
+  dependencies:
+    is-nan "^1.2.1"
+    moment-timezone "^0.5.25"
+
+cross-env@7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
+  integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
+  dependencies:
+    cross-spawn "^7.0.1"
+
+cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+css-color-names@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67"
+  integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==
+
+css-declaration-sorter@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz#9dfd8ea0df4cc7846827876fafb52314890c21a9"
+  integrity sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==
+  dependencies:
+    timsort "^0.3.0"
+
+css-loader@6.5.1:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.5.1.tgz#0c43d4fbe0d97f699c91e9818cb585759091d1b1"
+  integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==
+  dependencies:
+    icss-utils "^5.1.0"
+    postcss "^8.2.15"
+    postcss-modules-extract-imports "^3.0.0"
+    postcss-modules-local-by-default "^4.0.0"
+    postcss-modules-scope "^3.0.0"
+    postcss-modules-values "^4.0.0"
+    postcss-value-parser "^4.1.0"
+    semver "^7.3.5"
+
+css-select@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
+  integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^5.0.0"
+    domhandler "^4.2.0"
+    domutils "^2.6.0"
+    nth-check "^2.0.0"
+
+css-select@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+  integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+  dependencies:
+    boolbase "~1.0.0"
+    css-what "2.1"
+    domutils "1.5.1"
+    nth-check "~1.0.1"
+
+css-tree@^1.1.2, css-tree@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+  dependencies:
+    mdn-data "2.0.14"
+    source-map "^0.6.1"
+
+css-what@2.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+  integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+
+css-what@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
+  integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^5.1.6:
+  version "5.1.6"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.6.tgz#1bdb83be6a6b1fee6dc5e9ec2e61286bcadcc7a6"
+  integrity sha512-X2nDeNGBXc0486oHjT2vSj+TdeyVsxRvJUxaOH50hOM6vSDLkKd0+59YXpSZRInJ4sNtBOykS4KsPfhdrU/35w==
+  dependencies:
+    css-declaration-sorter "^6.0.3"
+    cssnano-utils "^2.0.1"
+    postcss-calc "^8.0.0"
+    postcss-colormin "^5.2.1"
+    postcss-convert-values "^5.0.2"
+    postcss-discard-comments "^5.0.1"
+    postcss-discard-duplicates "^5.0.1"
+    postcss-discard-empty "^5.0.1"
+    postcss-discard-overridden "^5.0.1"
+    postcss-merge-longhand "^5.0.3"
+    postcss-merge-rules "^5.0.2"
+    postcss-minify-font-values "^5.0.1"
+    postcss-minify-gradients "^5.0.3"
+    postcss-minify-params "^5.0.1"
+    postcss-minify-selectors "^5.1.0"
+    postcss-normalize-charset "^5.0.1"
+    postcss-normalize-display-values "^5.0.1"
+    postcss-normalize-positions "^5.0.1"
+    postcss-normalize-repeat-style "^5.0.1"
+    postcss-normalize-string "^5.0.1"
+    postcss-normalize-timing-functions "^5.0.1"
+    postcss-normalize-unicode "^5.0.1"
+    postcss-normalize-url "^5.0.2"
+    postcss-normalize-whitespace "^5.0.1"
+    postcss-ordered-values "^5.0.2"
+    postcss-reduce-initial "^5.0.1"
+    postcss-reduce-transforms "^5.0.1"
+    postcss-svgo "^5.0.3"
+    postcss-unique-selectors "^5.0.1"
+
+cssnano-utils@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2"
+  integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==
+
+cssnano@5.0.10:
+  version "5.0.10"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.10.tgz#92207eb7c9c6dc08d318050726f9fad0adf7220b"
+  integrity sha512-YfNhVJJ04imffOpbPbXP2zjIoByf0m8E2c/s/HnvSvjXgzXMfgopVjAEGvxYOjkOpWuRQDg/OZFjO7WW94Ri8w==
+  dependencies:
+    cssnano-preset-default "^5.1.6"
+    is-resolvable "^1.1.0"
+    lilconfig "^2.0.3"
+    yaml "^1.10.2"
+
+csso@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
+  dependencies:
+    css-tree "^1.1.2"
+
+cssom@^0.4.4:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
+  integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
+
+cssom@~0.3.6:
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+  integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssstyle@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
+  integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
+  dependencies:
+    cssom "~0.3.6"
+
+d@1, d@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
+  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+  dependencies:
+    es5-ext "^0.10.50"
+    type "^1.0.1"
+
+dashdash@^1.12.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+  integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+  dependencies:
+    assert-plus "^1.0.0"
+
+data-uri-to-buffer@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636"
+  integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==
+
+data-urls@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
+  integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==
+  dependencies:
+    abab "^2.0.3"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^8.0.0"
+
+date-fns@2.25.0:
+  version "2.25.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
+  integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
+
+date-fns@^2.16.1:
+  version "2.19.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
+  integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg==
+
+dateformat@4.5.1:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.5.1.tgz#c20e7a9ca77d147906b6dc2261a8be0a5bd2173c"
+  integrity sha512-OD0TZ+B7yP7ZgpJf5K2DIbj3FZvFvxgFUuaqA/V5zTjAtAAXZ1E8bktHxmAGs4x5b7PflqA9LeQ84Og7wYtF7Q==
+
+debug@2, debug@^2.2.0, debug@^2.5.2:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
+
+debug@4, debug@4.3.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+  dependencies:
+    ms "2.1.2"
+
+debug@4.3.2, debug@^4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
+  dependencies:
+    ms "2.1.2"
+
+debug@=3.1.0, debug@~3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+  dependencies:
+    ms "2.0.0"
+
+debug@^3.2.6:
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+  dependencies:
+    ms "^2.1.1"
+
+debuglog@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
+  integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
+
+decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decamelize@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
+  integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
+
+decimal.js@^10.2.1:
+  version "10.2.1"
+  resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
+  integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==
+
+decompress-response@^4.2.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
+  integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
+  dependencies:
+    mimic-response "^2.0.0"
+
+decompress-response@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
+  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
+  dependencies:
+    mimic-response "^3.1.0"
+
+deep-email-validator@0.1.18:
+  version "0.1.18"
+  resolved "https://registry.yarnpkg.com/deep-email-validator/-/deep-email-validator-0.1.18.tgz#a072a93f28e11863cc6b9ca3ae964e0e45b3ece8"
+  integrity sha512-eo2WEUidQvppg6Qdek8iwOqmXvaxRJ2D2VJKbIOwUgLZNFveDDdJMBsFc+yq0S+lILEUcmzrJRrCWbyoe7QUzQ==
+  dependencies:
+    "@types/disposable-email-domains" "^1.0.1"
+    axios "^0.19.2"
+    disposable-email-domains "^1.0.53"
+    lodash "^4.17.15"
+    mailcheck "^1.1.1"
+    ts-jest "^25.2.1"
+
+deep-equal@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+  integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
+
+deep-extend@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deep-is@^0.1.3, deep-is@~0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+defer-to-connect@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1"
+  integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+  dependencies:
+    object-keys "^1.0.12"
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+delegates@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+  integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+denque@^1.1.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
+  integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
+
+denque@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
+  integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
+
+depd@^1.1.2, depd@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+  integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+depd@^2.0.0, depd@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+  integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+detect-file@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
+  integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
+
+detect-libc@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+  integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
+
+detect-node@2.1.0, detect-node@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
+  integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
+
+dicer@0.2.5:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
+  integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=
+  dependencies:
+    readable-stream "1.1.x"
+    streamsearch "0.1.2"
+
+diff@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
+  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
+
+diff@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
+dijkstrajs@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
+  integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
+
+dir-glob@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+  integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+  dependencies:
+    path-type "^4.0.0"
+
+disposable-email-domains@^1.0.53:
+  version "1.0.58"
+  resolved "https://registry.yarnpkg.com/disposable-email-domains/-/disposable-email-domains-1.0.58.tgz#ac9c879c02c4f0898bfb6c0c80b959c0b0b7bc51"
+  integrity sha512-frnNCPqTjk6t/sosPoco6EIFHbP9SazHQkeltJNfZeUyNgewaVf+kFjEfVkVDVd436Vln43YElJPb8JozhBs7Q==
+
+doctrine@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+  dependencies:
+    esutils "^2.0.2"
+
+doctypes@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
+  integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
+
+dom-serializer@0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+  dependencies:
+    domelementtype "^2.0.1"
+    entities "^2.0.0"
+
+dom-serializer@^1.0.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be"
+  integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+    entities "^2.0.0"
+
+dom-serializer@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+  integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+  dependencies:
+    domelementtype "^1.3.0"
+    entities "^1.1.1"
+
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domelementtype@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
+  integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
+
+domelementtype@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
+  integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
+
+domexception@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
+  integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==
+  dependencies:
+    webidl-conversions "^5.0.0"
+
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+  dependencies:
+    domelementtype "1"
+
+domhandler@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.1.0.tgz#c1d8d494d5ec6db22de99e46a149c2a4d23ddd43"
+  integrity sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domhandler@^4.2.0:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
+  integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domutils@1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+  integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+domutils@^1.5.1:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+domutils@^2.6.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.0"
+
+dotenv@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
+  integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
+
+ecc-jsbn@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+  integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+  dependencies:
+    jsbn "~0.1.0"
+    safer-buffer "^2.1.0"
+
+ecdsa-sig-formatter@1.0.11:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
+  integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
+  dependencies:
+    safe-buffer "^5.0.1"
+
+editorconfig@^0.15.3:
+  version "0.15.3"
+  resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
+  integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==
+  dependencies:
+    commander "^2.19.0"
+    lru-cache "^4.1.5"
+    semver "^5.6.0"
+    sigmund "^1.0.1"
+
+ee-first@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+  integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+electron-to-chromium@^1.3.649:
+  version "1.3.672"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.672.tgz#3a6e335016dab4bc584d5292adc4f98f54541f6a"
+  integrity sha512-gFQe7HBb0lbOMqK2GAS5/1F+B0IMdYiAgB9OT/w1F4M7lgJK2aNOMNOM622aEax+nS1cTMytkiT0uMOkbtFmHw==
+
+electron-to-chromium@^1.3.712:
+  version "1.3.717"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz#78d4c857070755fb58ab64bcc173db1d51cbc25f"
+  integrity sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==
+
+electron-to-chromium@^1.3.723:
+  version "1.3.742"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.742.tgz#7223215acbbd3a5284962ebcb6df85d88b95f200"
+  integrity sha512-ihL14knI9FikJmH2XUIDdZFWJxvr14rPSdOhJ7PpS27xbz8qmaRwCwyg/bmFwjWKmWK9QyamiCZVCvXm5CH//Q==
+
+emoji-regex@^7.0.1:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+emojis-list@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
+  integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+
+encodeurl@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+  integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+encoding@^0.1.12:
+  version "0.1.13"
+  resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+  integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
+  dependencies:
+    iconv-lite "^0.6.2"
+
+end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+  dependencies:
+    once "^1.4.0"
+
+enhanced-resolve@^5.0.0:
+  version "5.8.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.0.tgz#d9deae58f9d3773b6a111a5a46831da5be5c9ac0"
+  integrity sha512-Sl3KRpJA8OpprrtaIswVki3cWPiPKxXuFxJXBp+zNb6s6VwNWwFRUdtmzd2ReUut8n+sCPx7QCtQ7w5wfJhSgQ==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+enhanced-resolve@^5.7.0:
+  version "5.7.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz#525c5d856680fbd5052de453ac83e32049958b5c"
+  integrity sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+enquirer@^2.3.5:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+  integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+  dependencies:
+    ansi-colors "^4.1.1"
+
+entities@^1.1.1, entities@~1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
+  integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+
+env-paths@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
+  integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
+
+err-code@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
+  integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
+
+es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
+  version "1.17.5"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
+  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
+  dependencies:
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+    is-callable "^1.1.5"
+    is-regex "^1.0.5"
+    object-inspect "^1.7.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.0"
+    string.prototype.trimleft "^2.1.1"
+    string.prototype.trimright "^2.1.1"
+
+es-module-lexer@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.4.0.tgz#21f4181cc8b7eee06855f1c59e6087c7bc4f77b0"
+  integrity sha512-iuEGihqqhKWFgh72Q/Jtch7V2t/ft8w8IPP2aEN8ArYKO+IWyo6hsi96hCdgyeEDQIV3InhYQ9BlwUFPGXrbEQ==
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+es5-ext@^0.10.35, es5-ext@^0.10.50:
+  version "0.10.53"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
+  integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
+  dependencies:
+    es6-iterator "~2.0.3"
+    es6-symbol "~3.1.3"
+    next-tick "~1.0.0"
+
+es6-iterator@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
+  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
+  dependencies:
+    d "^1.0.1"
+    ext "^1.1.2"
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-html@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+escape-regexp@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/escape-regexp/-/escape-regexp-0.0.1.tgz#f44bda12d45bbdf9cb7f862ee7e4827b3dd32254"
+  integrity sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ=
+
+escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escodegen@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
+  integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==
+  dependencies:
+    esprima "^4.0.1"
+    estraverse "^5.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+eslint-plugin-vue@8.0.3:
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-8.0.3.tgz#791cc4543940319e612ea61a1d779e8c87cf749a"
+  integrity sha512-Rlhhy5ltzde0sRwSkqHuNePTXLMMaJ5+qsQubM4RYloYsQ8cXlnJT5MDaCzSirkGADipOHtmQXIbbPFAzUrADg==
+  dependencies:
+    eslint-utils "^3.0.0"
+    natural-compare "^1.4.0"
+    semver "^7.3.5"
+    vue-eslint-parser "^8.0.1"
+
+eslint-scope@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
+
+eslint-scope@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-6.0.0.tgz#9cf45b13c5ac8f3d4c50f46a5121f61b3e318978"
+  integrity sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^5.2.0"
+
+eslint-utils@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
+  integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
+  dependencies:
+    eslint-visitor-keys "^2.0.0"
+
+eslint-visitor-keys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
+  integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
+
+eslint-visitor-keys@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186"
+  integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==
+
+eslint@8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.2.0.tgz#44d3fb506d0f866a506d97a0fc0e90ee6d06a815"
+  integrity sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==
+  dependencies:
+    "@eslint/eslintrc" "^1.0.4"
+    "@humanwhocodes/config-array" "^0.6.0"
+    ajv "^6.10.0"
+    chalk "^4.0.0"
+    cross-spawn "^7.0.2"
+    debug "^4.3.2"
+    doctrine "^3.0.0"
+    enquirer "^2.3.5"
+    escape-string-regexp "^4.0.0"
+    eslint-scope "^6.0.0"
+    eslint-utils "^3.0.0"
+    eslint-visitor-keys "^3.0.0"
+    espree "^9.0.0"
+    esquery "^1.4.0"
+    esutils "^2.0.2"
+    fast-deep-equal "^3.1.3"
+    file-entry-cache "^6.0.1"
+    functional-red-black-tree "^1.0.1"
+    glob-parent "^6.0.1"
+    globals "^13.6.0"
+    ignore "^4.0.6"
+    import-fresh "^3.0.0"
+    imurmurhash "^0.1.4"
+    is-glob "^4.0.0"
+    js-yaml "^4.1.0"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.4.1"
+    lodash.merge "^4.6.2"
+    minimatch "^3.0.4"
+    natural-compare "^1.4.0"
+    optionator "^0.9.1"
+    progress "^2.0.0"
+    regexpp "^3.2.0"
+    semver "^7.2.1"
+    strip-ansi "^6.0.1"
+    strip-json-comments "^3.1.0"
+    text-table "^0.2.0"
+    v8-compile-cache "^2.0.3"
+
+esm@^3.2.22:
+  version "3.2.25"
+  resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
+  integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
+
+espree@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.0.0.tgz#e90a2965698228502e771c7a58489b1a9d107090"
+  integrity sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==
+  dependencies:
+    acorn "^8.5.0"
+    acorn-jsx "^5.3.1"
+    eslint-visitor-keys "^3.0.0"
+
+esprima@^4.0.0, esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+esquery@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
+  integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
+  dependencies:
+    estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
+  integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
+
+estraverse@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
+  integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+event-target-shim@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
+  integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
+eventemitter3@4.0.7, eventemitter3@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+events@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+  integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
+
+events@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
+  integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
+
+execa@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
+  integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
+  dependencies:
+    cross-spawn "^7.0.0"
+    get-stream "^5.0.0"
+    human-signals "^1.1.1"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.0"
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+    strip-final-newline "^2.0.0"
+
+exit-on-epipe@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
+  integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
+
+expand-template@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
+  integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
+
+expand-tilde@^2.0.0, expand-tilde@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
+  integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
+  dependencies:
+    homedir-polyfill "^1.0.1"
+
+ext@^1.1.2:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
+  integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
+  dependencies:
+    type "^2.0.0"
+
+extend-shallow@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+  integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+  dependencies:
+    is-extendable "^0.1.0"
+
+extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extsprintf@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+  integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+  integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+fast-deep-equal@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
+  integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+
+fast-deep-equal@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-glob@^3.1.1:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
+  integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.0"
+    merge2 "^1.3.0"
+    micromatch "^4.0.2"
+    picomatch "^2.2.1"
+
+fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fast-xml-parser@^3.19.0:
+  version "3.19.0"
+  resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01"
+  integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==
+
+fastq@^1.6.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481"
+  integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==
+  dependencies:
+    reusify "^1.0.4"
+
+feed@4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
+  integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==
+  dependencies:
+    xml-js "^1.6.11"
+
+fetch-blob@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c"
+  integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==
+
+file-entry-cache@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+  dependencies:
+    flat-cache "^3.0.4"
+
+file-type@16.5.3:
+  version "16.5.3"
+  resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.3.tgz#474b7e88c74724046abb505e9b8ed4db30c4fc06"
+  integrity sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==
+  dependencies:
+    readable-web-to-node-stream "^3.0.0"
+    strtok3 "^6.2.4"
+    token-types "^4.1.1"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-node-modules@^2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-2.1.2.tgz#57565a3455baf671b835bc6b2134a9b938b9c53c"
+  integrity sha512-x+3P4mbtRPlSiVE1Qco0Z4YLU8WFiFcuWTf3m75OV9Uzcfs2Bg+O9N+r/K0AnmINBW06KpfqKwYJbFlFq4qNug==
+  dependencies:
+    findup-sync "^4.0.0"
+    merge "^2.1.0"
+
+find-up@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+  dependencies:
+    locate-path "^6.0.0"
+    path-exists "^4.0.0"
+
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
+find-up@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+findup-sync@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0"
+  integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==
+  dependencies:
+    detect-file "^1.0.0"
+    is-glob "^4.0.0"
+    micromatch "^4.0.2"
+    resolve-dir "^1.0.1"
+
+flat-cache@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+  dependencies:
+    flatted "^3.1.0"
+    rimraf "^3.0.2"
+
+flat@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
+  integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
+
+flatted@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067"
+  integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==
+
+fluent-ffmpeg@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz#c952de2240f812ebda0aa8006d7776ee2acf7d74"
+  integrity sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=
+  dependencies:
+    async ">=0.2.9"
+    which "^1.1.1"
+
+follow-redirects@1.5.10:
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+  dependencies:
+    debug "=3.1.0"
+
+foreach@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+  integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
+
+forever-agent@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+  integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
+  integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+form-data@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
+fresh@~0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+  integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+fs-constants@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
+fs-extra@^8.0.1:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+  dependencies:
+    graceful-fs "^4.2.0"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
+fs-minipass@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
+  integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
+  dependencies:
+    minipass "^3.0.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@~2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
+  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+functional-red-black-tree@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+  integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
+gauge@~2.7.3:
+  version "2.7.4"
+  resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+  integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+  dependencies:
+    aproba "^1.0.3"
+    console-control-strings "^1.0.0"
+    has-unicode "^2.0.0"
+    object-assign "^4.1.0"
+    signal-exit "^3.0.0"
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+    wide-align "^1.1.0"
+
+get-caller-file@^2.0.1, get-caller-file@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-paths@0.0.7:
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/get-paths/-/get-paths-0.0.7.tgz#15331086752077cf130166ccd233a1cdbeefcf38"
+  integrity sha512-0wdJt7C1XKQxuCgouqd+ZvLJ56FQixKoki9MrFaO4EriqzXOiH9gbukaDE1ou08S8Ns3/yDzoBAISNPqj6e6tA==
+  dependencies:
+    pify "^4.0.1"
+
+get-port@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
+  integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
+
+get-stream@^5.0.0, get-stream@^5.1.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
+  dependencies:
+    pump "^3.0.0"
+
+getpass@^0.1.1:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+  integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+  dependencies:
+    assert-plus "^1.0.0"
+
+github-from-package@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
+  integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
+
+glob-parent@^5.1.0, glob-parent@~5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-parent@^6.0.1:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+  integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+  dependencies:
+    is-glob "^4.0.3"
+
+glob-to-regexp@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+glob@7.1.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
+  version "7.1.6"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+global-modules@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
+  integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
+  dependencies:
+    global-prefix "^1.0.1"
+    is-windows "^1.0.1"
+    resolve-dir "^1.0.0"
+
+global-prefix@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
+  integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
+  dependencies:
+    expand-tilde "^2.0.2"
+    homedir-polyfill "^1.0.1"
+    ini "^1.3.4"
+    is-windows "^1.0.1"
+    which "^1.2.14"
+
+globals@^13.6.0:
+  version "13.7.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.7.0.tgz#aed3bcefd80ad3ec0f0be2cf0c895110c0591795"
+  integrity sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA==
+  dependencies:
+    type-fest "^0.20.2"
+
+globals@^13.9.0:
+  version "13.9.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb"
+  integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==
+  dependencies:
+    type-fest "^0.20.2"
+
+globby@^11.0.2, globby@^11.0.4:
+  version "11.0.4"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
+  integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
+  dependencies:
+    array-union "^2.1.0"
+    dir-glob "^3.0.1"
+    fast-glob "^3.1.1"
+    ignore "^5.1.4"
+    merge2 "^1.3.0"
+    slash "^3.0.0"
+
+got@11.8.2:
+  version "11.8.2"
+  resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
+  integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
+  dependencies:
+    "@sindresorhus/is" "^4.0.0"
+    "@szmarczak/http-timer" "^4.0.5"
+    "@types/cacheable-request" "^6.0.1"
+    "@types/responselike" "^1.0.0"
+    cacheable-lookup "^5.0.3"
+    cacheable-request "^7.0.1"
+    decompress-response "^6.0.0"
+    http2-wrapper "^1.0.0-beta.5.2"
+    lowercase-keys "^2.0.0"
+    p-cancelable "^2.0.0"
+    responselike "^2.0.0"
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
+  integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+
+graceful-fs@^4.2.0:
+  version "4.2.8"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
+  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
+
+graceful-fs@^4.2.6:
+  version "4.2.6"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
+  integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
+
+growl@1.10.5:
+  version "1.10.5"
+  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
+  integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
+
+hammerjs@^2.0.8:
+  version "2.0.8"
+  resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
+  integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
+
+har-schema@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+  integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+  dependencies:
+    ajv "^6.5.5"
+    har-schema "^2.0.0"
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-own-prop@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af"
+  integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==
+
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has-unicode@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+  integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+he@1.2.0, he@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+highlight.js@^10.7.1:
+  version "10.7.2"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360"
+  integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg==
+
+homedir-polyfill@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
+  integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
+  dependencies:
+    parse-passwd "^1.0.0"
+
+hpagent@0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9"
+  integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==
+
+hpagent@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.1.tgz#66f67f16e5c7a8b59a068e40c2658c2c749ad5e2"
+  integrity sha512-IxJWQiY0vmEjetHdoE9HZjD4Cx+mYTr25tR7JCxXaiI3QxW0YqYyM11KyZbHufoa/piWhMb2+D3FGpMgmA2cFQ==
+
+html-encoding-sniffer@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
+  integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==
+  dependencies:
+    whatwg-encoding "^1.0.5"
+
+html-entities@2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488"
+  integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==
+
+htmlparser2@^3.9.1:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+  dependencies:
+    domelementtype "^1.3.1"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^3.1.1"
+
+http-assert@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878"
+  integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==
+  dependencies:
+    deep-equal "~1.0.1"
+    http-errors "~1.7.2"
+
+http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
+  integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
+
+http-errors@1.7.3, http-errors@^1.6.3, http-errors@^1.7.3, http-errors@~1.7.2:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
+http-errors@~1.6.2:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+  integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.3"
+    setprototypeof "1.1.0"
+    statuses ">= 1.4.0 < 2"
+
+http-headers@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/http-headers/-/http-headers-3.0.2.tgz#5147771292f0b39d6778d930a3a59a76fc7ef44d"
+  integrity sha512-87E1I+2Wg4dxxz4rcxElo3dxO/w1ZtgL1yA0Sb6vH3qU16vRKq1NjWQv9SCY3ly2OQROcoxHZOUpmelS+k6wOw==
+  dependencies:
+    next-line "^1.1.0"
+
+http-proxy-agent@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
+  integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==
+  dependencies:
+    "@tootallnate/once" "1"
+    agent-base "6"
+    debug "4"
+
+http-signature@1.3.5:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"
+  integrity sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw==
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.14.1"
+
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+http2-wrapper@^1.0.0-beta.5.2:
+  version "1.0.0-beta.5.2"
+  resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3"
+  integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==
+  dependencies:
+    quick-lru "^5.1.1"
+    resolve-alpn "^1.0.0"
+
+http_ece@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75"
+  integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==
+  dependencies:
+    urlsafe-base64 "~1.0.0"
+
+https-proxy-agent@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
+  integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
+  dependencies:
+    agent-base "6"
+    debug "4"
+
+human-signals@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
+  integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
+
+humanize-ms@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
+  integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=
+  dependencies:
+    ms "^2.0.0"
+
+humanize-number@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/humanize-number/-/humanize-number-0.0.2.tgz#11c0af6a471643633588588048f1799541489c18"
+  integrity sha1-EcCvakcWQ2M1iFiASPF5lUFInBg=
+
+iconv-lite@0.4.24, iconv-lite@^0.4.4:
+  version "0.4.24"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
+iconv-lite@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
+  integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
+iconv-lite@^0.6.3:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
+icss-utils@^5.0.0, icss-utils@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
+  integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
+
+idb-keyval@5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.1.3.tgz#6ef5dff371897c23f144322dc6374eadd6a345d9"
+  integrity sha512-N9HbCK/FaXSRVI+k6Xq4QgWxbcZRUv+SfG1y7HJ28JdV8yEJu6k+C/YLea7npGckX2DQJeEVuMc4bKOBeU/2LQ==
+  dependencies:
+    safari-14-idb-fix "^1.0.4"
+
+ieee754@1.1.13, ieee754@^1.1.4:
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
+  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+
+ieee754@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+ignore@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+  integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
+ignore@^5.1.4:
+  version "5.1.8"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
+  integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
+
+immutable@^3.8.2:
+  version "3.8.2"
+  resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
+  integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
+
+import-fresh@^3.0.0, import-fresh@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
+  integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+import-fresh@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+  integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+indent-string@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
+indexes-of@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+  integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+
+infer-owner@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
+  integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
+
+inflation@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f"
+  integrity sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+  integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+ini@^1.3.4, ini@~1.3.0:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
+  integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
+
+insert-text-at-cursor@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/insert-text-at-cursor/-/insert-text-at-cursor-0.3.0.tgz#1819607680ec1570618347c4cd475e791faa25da"
+  integrity sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==
+
+install-artifact-from-github@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.2.0.tgz#adcbd123c16a4337ec44ea76d0ebf253cc16b074"
+  integrity sha512-3OxCPcY55XlVM3kkfIpeCgmoSKnMsz2A3Dbhsq0RXpIknKQmrX1YiznCeW9cD2ItFmDxziA3w6Eg8d80AoL3oA==
+
+invert-kv@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-3.0.1.tgz#a93c7a3d4386a1dc8325b97da9bb1620c0282523"
+  integrity sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==
+
+ioredis@^4.27.0:
+  version "4.27.6"
+  resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.6.tgz#a53d427d3fe75fbd10ed7ad150ce00559df8dcf8"
+  integrity sha512-6W3ZHMbpCa8ByMyC1LJGOi7P2WiOKP9B3resoZOVLDhi+6dDBOW+KNsRq3yI36Hmnb2sifCxHX+YSarTeXh48A==
+  dependencies:
+    cluster-key-slot "^1.1.0"
+    debug "^4.3.1"
+    denque "^1.1.0"
+    lodash.defaults "^4.2.0"
+    lodash.flatten "^4.4.0"
+    p-map "^2.1.0"
+    redis-commands "1.7.0"
+    redis-errors "^1.2.0"
+    redis-parser "^3.0.0"
+    standard-as-callback "^2.1.0"
+
+ip-address@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-7.1.0.tgz#4a9c699e75b51cbeb18b38de8ed216efa1a490c5"
+  integrity sha512-V9pWC/VJf2lsXqP7IWJ+pe3P1/HCYGBMZrrnT62niLGjAfCbeiwXMUxaeHvnVlz19O27pvXP4azs+Pj/A0x+SQ==
+  dependencies:
+    jsbn "1.1.0"
+    sprintf-js "1.1.2"
+
+ip-cidr@3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.4.tgz#a915c47e00f47ea8d5f8ed662ea6161471c44375"
+  integrity sha512-pKNiqmBlTvEkhaLAa3+FOmYSY0/jjADVxxjA3NbujZZTT8mjLI90Q+6mwg6kd0fNm0RuAOkWJ1u1a/ETmlrPNQ==
+  dependencies:
+    ip-address "^7.1.0"
+    jsbn "^1.1.0"
+
+ip-regex@^4.0.0, ip-regex@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
+  integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
+
+ip@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+  integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
+ipaddr.js@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
+  integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
+
+is-absolute-url@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
+  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+
+is-arrayish@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-buffer@^1.1.5:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-callable@^1.1.4, is-callable@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
+  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
+
+is-core-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.0.0.tgz#58531b70aed1db7c0e8d4eb1a0a2d1ddd64bd12d"
+  integrity sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw==
+  dependencies:
+    has "^1.0.3"
+
+is-core-module@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
+  integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
+  dependencies:
+    has "^1.0.3"
+
+is-date-object@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-expression@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab"
+  integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==
+  dependencies:
+    acorn "^7.1.1"
+    object-assign "^4.1.1"
+
+is-extendable@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+  integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+  integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+  dependencies:
+    number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-generator-function@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522"
+  integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-glob@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-ip@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8"
+  integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==
+  dependencies:
+    ip-regex "^4.0.0"
+
+is-lambda@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
+  integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=
+
+is-nan@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03"
+  integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==
+  dependencies:
+    define-properties "^1.1.3"
+
+is-number-like@^1.0.3:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3"
+  integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==
+  dependencies:
+    lodash.isfinite "^3.3.2"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-obj@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+  integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
+is-potential-custom-element-name@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
+  integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
+
+is-promise@^2.0.0:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
+  integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
+is-regex@^1.0.3, is-regex@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
+  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
+  dependencies:
+    has "^1.0.3"
+
+is-resolvable@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+
+is-svg@4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.1.tgz#8c63ec8c67c8c7f0a8de0a71c8c7d58eccf4406b"
+  integrity sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==
+  dependencies:
+    fast-xml-parser "^3.19.0"
+
+is-symbol@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+  dependencies:
+    has-symbols "^1.0.1"
+
+is-typedarray@^1.0.0, is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+  integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-whitespace@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f"
+  integrity sha1-Fjnssb4DauxppUy7QBz77XEUq38=
+
+is-windows@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+  integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+isarray@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
+isarray@^1.0.0, isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isarray@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+jest-worker@^26.6.2:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
+  integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^7.0.0"
+
+jmespath@0.15.0:
+  version "0.15.0"
+  resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
+  integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
+
+jpeg-js@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.1.tgz#937a3ae911eb6427f151760f8123f04c8bfe6ef7"
+  integrity sha512-jA55yJiB5tCXEddos8JBbvW+IMrqY0y1tjjx9KNVtA+QPmu7ND5j0zkKopClpUTsaETL135uOM2XfcYG4XRjmw==
+
+js-beautify@^1.6.12:
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.11.0.tgz#afb873dc47d58986360093dcb69951e8bcd5ded2"
+  integrity sha512-a26B+Cx7USQGSWnz9YxgJNMmML/QG2nqIaL7VVYPCXbqiKz8PN0waSNvroMtvAK6tY7g/wPdNWGEP+JTNIBr6A==
+  dependencies:
+    config-chain "^1.1.12"
+    editorconfig "^0.15.3"
+    glob "^7.1.3"
+    mkdirp "~1.0.3"
+    nopt "^4.0.3"
+
+js-levenshtein@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
+  integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
+
+js-stringify@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
+  integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds=
+
+js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
+  integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
+  dependencies:
+    argparse "^2.0.1"
+
+js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
+js-yaml@^3.13.1, js-yaml@^3.14.1:
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+jsbn@1.1.0, jsbn@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
+  integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA=
+
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+  integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jschardet@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882"
+  integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==
+
+jsdom@16.7.0:
+  version "16.7.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710"
+  integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==
+  dependencies:
+    abab "^2.0.5"
+    acorn "^8.2.4"
+    acorn-globals "^6.0.0"
+    cssom "^0.4.4"
+    cssstyle "^2.3.0"
+    data-urls "^2.0.0"
+    decimal.js "^10.2.1"
+    domexception "^2.0.1"
+    escodegen "^2.0.0"
+    form-data "^3.0.0"
+    html-encoding-sniffer "^2.0.1"
+    http-proxy-agent "^4.0.1"
+    https-proxy-agent "^5.0.0"
+    is-potential-custom-element-name "^1.0.1"
+    nwsapi "^2.2.0"
+    parse5 "6.0.1"
+    saxes "^5.0.1"
+    symbol-tree "^3.2.4"
+    tough-cookie "^4.0.0"
+    w3c-hr-time "^1.0.2"
+    w3c-xmlserializer "^2.0.0"
+    webidl-conversions "^6.1.0"
+    whatwg-encoding "^1.0.5"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^8.5.0"
+    ws "^7.4.6"
+    xml-name-validator "^3.0.0"
+
+json-buffer@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
+json-parse-better-errors@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema-traverse@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+  integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
+json-schema@0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stable-stringify-without-jsonify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+  integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+
+json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+  integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5-loader@4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/json5-loader/-/json5-loader-4.0.1.tgz#6d17a1181e8f3c3d9204dca2a4ce4627306c8498"
+  integrity sha512-c9viNZlZTz0MTIcf/4qvek5Dz1/PU3DNCB4PwUhlEZIV3qb1bSD6vQQymlV17/Wm6ncra1aCvmIPsuRj+KfEEg==
+  dependencies:
+    json5 "^2.1.3"
+    loader-utils "^2.0.0"
+    schema-utils "^3.0.0"
+
+json5@2.2.0, json5@2.x:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
+  integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
+  dependencies:
+    minimist "^1.2.5"
+
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
+json5@^2.1.2, json5@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
+  integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
+  dependencies:
+    minimist "^1.2.5"
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsonfile@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
+  integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
+  dependencies:
+    universalify "^0.1.2"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsonld@5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-5.2.0.tgz#d1e8af38a334cb95edf0f2ae4e2b58baf8d2b5a9"
+  integrity sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw==
+  dependencies:
+    "@digitalbazaar/http-client" "^1.1.0"
+    canonicalize "^1.0.1"
+    lru-cache "^6.0.0"
+    rdf-canonize "^3.0.0"
+
+jsprim@^1.2.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+  dependencies:
+    assert-plus "1.0.0"
+    extsprintf "1.3.0"
+    json-schema "0.2.3"
+    verror "1.10.0"
+
+jsrsasign@8.0.20:
+  version "8.0.20"
+  resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.20.tgz#37d8029c9d8f794d8ac8d8998bce319921491f11"
+  integrity sha512-JTXt9+nqdynIB8wFsS6e8ffHhIjilhywXwdaEVHSj9OVmwldG2H0EoCqkQ+KXkm2tVqREfH/HEmklY4k1/6Rcg==
+
+jstransformer@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
+  integrity sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=
+  dependencies:
+    is-promise "^2.0.0"
+    promise "^7.0.1"
+
+jwa@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
+  integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
+  dependencies:
+    buffer-equal-constant-time "1.0.1"
+    ecdsa-sig-formatter "1.0.11"
+    safe-buffer "^5.0.1"
+
+jws@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
+  integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
+  dependencies:
+    jwa "^2.0.0"
+    safe-buffer "^5.0.1"
+
+keygrip@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
+  integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==
+  dependencies:
+    tsscmp "1.0.6"
+
+keyv@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254"
+  integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==
+  dependencies:
+    json-buffer "3.0.1"
+
+kind-of@^3.0.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+  integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+  dependencies:
+    is-buffer "^1.1.5"
+
+koa-bodyparser@4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz#274c778555ff48fa221ee7f36a9fbdbace22759a"
+  integrity sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==
+  dependencies:
+    co-body "^6.0.0"
+    copy-to "^2.0.1"
+
+koa-compose@^3.0.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
+  integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=
+  dependencies:
+    any-promise "^1.1.0"
+
+koa-compose@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
+  integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
+
+koa-convert@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
+  integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=
+  dependencies:
+    co "^4.6.0"
+    koa-compose "^3.0.0"
+
+koa-convert@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5"
+  integrity sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==
+  dependencies:
+    co "^4.6.0"
+    koa-compose "^4.1.0"
+
+koa-favicon@2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/koa-favicon/-/koa-favicon-2.1.0.tgz#c430cc594614fb494adcb5ee1196a2f7f53ea442"
+  integrity sha512-LvukcooYjxKtnZq0RXdBup+JDhaHwLgnLlDHB/xvjwQEjbc4rbp/0WkmOzpOvaHujc+fIwPear0dpKX1V+dHVg==
+  dependencies:
+    mz "^2.7.0"
+
+koa-json-body@5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/koa-json-body/-/koa-json-body-5.3.0.tgz#64aad3f400adfb81df54b63f7a5eb38bad62d980"
+  integrity sha1-ZKrT9ACt+4HfVLY/el6zi61i2YA=
+  dependencies:
+    co-body "^5.0.0"
+
+koa-logger@3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/koa-logger/-/koa-logger-3.2.1.tgz#ab9db879526db3837cc9ce4fd983c025b1689f22"
+  integrity sha512-MjlznhLLKy9+kG8nAXKJLM0/ClsQp/Or2vI3a5rbSQmgl8IJBQO0KI5FA70BvW+hqjtxjp49SpH2E7okS6NmHg==
+  dependencies:
+    bytes "^3.1.0"
+    chalk "^2.4.2"
+    humanize-number "0.0.2"
+    passthrough-counter "^1.0.0"
+
+koa-mount@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/koa-mount/-/koa-mount-4.0.0.tgz#e0265e58198e1a14ef889514c607254ff386329c"
+  integrity sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==
+  dependencies:
+    debug "^4.0.1"
+    koa-compose "^4.1.0"
+
+koa-send@5.0.1, koa-send@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79"
+  integrity sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==
+  dependencies:
+    debug "^4.1.1"
+    http-errors "^1.7.3"
+    resolve-path "^1.4.0"
+
+koa-slow@2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/koa-slow/-/koa-slow-2.1.0.tgz#39007ca628c620f2b307b90dbf423d7a0c9be971"
+  integrity sha1-OQB8pijGIPKzB7kNv0I9egyb6XE=
+  dependencies:
+    lodash.isregexp "3.0.5"
+    q "1.4.1"
+
+koa-views@*:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/koa-views/-/koa-views-7.0.1.tgz#0c8f8e65d5cd2e08249430cb83dc361e49a17a5a"
+  integrity sha512-yS8751DXHXXDbdl/oUZd0PsgnxR0MLiguu77Eqrgu6yawE9Hi99wNKiVENb0Kfgsmvq/8px7YCI+USgxaTB1LA==
+  dependencies:
+    "@types/koa" "^2.13.1"
+    consolidate "^0.16.0"
+    debug "^4.1.0"
+    get-paths "0.0.7"
+    koa-send "^5.0.0"
+    mz "^2.4.0"
+    pretty "^2.0.0"
+    resolve-path "^1.4.0"
+
+koa-views@7.0.2:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/koa-views/-/koa-views-7.0.2.tgz#c96fd9e2143ef00c29dc5160c5ed639891aa723d"
+  integrity sha512-dvx3mdVeSVuIPEaKAoGbxLcenudvhl821xxyuRbcoA+bOJ2dvN8wlGjkLu0ZFMlkCscXZV6lzxy28rafeazI/w==
+  dependencies:
+    consolidate "^0.16.0"
+    debug "^4.1.0"
+    get-paths "0.0.7"
+    koa-send "^5.0.0"
+    mz "^2.4.0"
+    pretty "^2.0.0"
+    resolve-path "^1.4.0"
+
+koa@2.13.1:
+  version "2.13.1"
+  resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.1.tgz#6275172875b27bcfe1d454356a5b6b9f5a9b1051"
+  integrity sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w==
+  dependencies:
+    accepts "^1.3.5"
+    cache-content-type "^1.0.0"
+    content-disposition "~0.5.2"
+    content-type "^1.0.4"
+    cookies "~0.8.0"
+    debug "~3.1.0"
+    delegates "^1.0.0"
+    depd "^2.0.0"
+    destroy "^1.0.4"
+    encodeurl "^1.0.2"
+    escape-html "^1.0.3"
+    fresh "~0.5.2"
+    http-assert "^1.3.0"
+    http-errors "^1.6.3"
+    is-generator-function "^1.0.7"
+    koa-compose "^4.1.0"
+    koa-convert "^1.2.0"
+    on-finished "^2.3.0"
+    only "~0.0.2"
+    parseurl "^1.3.2"
+    statuses "^1.5.0"
+    type-is "^1.6.16"
+    vary "^1.1.2"
+
+koa@2.13.4:
+  version "2.13.4"
+  resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e"
+  integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==
+  dependencies:
+    accepts "^1.3.5"
+    cache-content-type "^1.0.0"
+    content-disposition "~0.5.2"
+    content-type "^1.0.4"
+    cookies "~0.8.0"
+    debug "^4.3.2"
+    delegates "^1.0.0"
+    depd "^2.0.0"
+    destroy "^1.0.4"
+    encodeurl "^1.0.2"
+    escape-html "^1.0.3"
+    fresh "~0.5.2"
+    http-assert "^1.3.0"
+    http-errors "^1.6.3"
+    is-generator-function "^1.0.7"
+    koa-compose "^4.1.0"
+    koa-convert "^2.0.0"
+    on-finished "^2.3.0"
+    only "~0.0.2"
+    parseurl "^1.3.2"
+    statuses "^1.5.0"
+    type-is "^1.6.16"
+    vary "^1.1.2"
+
+ky-universal@^0.8.2:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.8.2.tgz#edc398d54cf495d7d6830aa1ab69559a3cc7f824"
+  integrity sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ==
+  dependencies:
+    abort-controller "^3.0.0"
+    node-fetch "3.0.0-beta.9"
+
+ky@^0.25.1:
+  version "0.25.1"
+  resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc"
+  integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA==
+
+langmap@0.0.16:
+  version "0.0.16"
+  resolved "https://registry.yarnpkg.com/langmap/-/langmap-0.0.16.tgz#2fe3e98a531fec0fec546624ebe168c2855bab56"
+  integrity sha512-AtYvBK7BsDvWwnSfmO7CfgeUy7GUT1wK3QX8eKH/Ey/eXodqoHuAtvdQ82hmWD9QVFVKnuiNjym9fGY4qSJeLA==
+
+lcid@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/lcid/-/lcid-3.1.1.tgz#9030ec479a058fc36b5e8243ebaac8b6ac582fd0"
+  integrity sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==
+  dependencies:
+    invert-kv "^3.0.0"
+
+levn@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+  dependencies:
+    prelude-ls "^1.2.1"
+    type-check "~0.4.0"
+
+levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
+lilconfig@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
+  integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
+
+loader-runner@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
+  integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
+
+loader-utils@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
+  integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^2.1.2"
+
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
+locate-path@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+  dependencies:
+    p-locate "^5.0.0"
+
+lodash.assignin@^4.0.9:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
+  integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI=
+
+lodash.bind@^4.1.4:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
+  integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=
+
+lodash.defaults@^4.0.1, lodash.defaults@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+  integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
+
+lodash.filter@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
+  integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=
+
+lodash.flatten@^4.2.0, lodash.flatten@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+  integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
+
+lodash.foreach@^4.3.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
+  integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=
+
+lodash.isequal@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.isfinite@^3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
+  integrity sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=
+
+lodash.isregexp@3.0.5:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/lodash.isregexp/-/lodash.isregexp-3.0.5.tgz#e0f596242f2fa228a840086b6c8ad82e4b71fd2d"
+  integrity sha1-4PWWJC8voiioQAhrbIrYLktx/S0=
+
+lodash.map@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
+  integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=
+
+lodash.memoize@4.x, lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.merge@^4.4.0, lodash.merge@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+lodash.pick@^4.2.1:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
+  integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
+
+lodash.reduce@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
+  integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=
+
+lodash.reject@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
+  integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=
+
+lodash.some@^4.4.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
+  integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=
+
+lodash.sortby@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+  integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+log-symbols@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
+  integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
+  dependencies:
+    chalk "^4.0.0"
+
+lowercase-keys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
+  integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
+
+lru-cache@^4.1.5:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
+  integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
+  dependencies:
+    pseudomap "^1.0.2"
+    yallist "^2.1.2"
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+mailcheck@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/mailcheck/-/mailcheck-1.1.1.tgz#d87cf6ba0b64ba512199dbf93f1489f479591e34"
+  integrity sha1-2Hz2ugtkulEhmdv5PxSJ9HlZHjQ=
+
+make-error@1.x, make-error@^1.1.1:
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+  integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
+make-fetch-happen@^8.0.14:
+  version "8.0.14"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222"
+  integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==
+  dependencies:
+    agentkeepalive "^4.1.3"
+    cacache "^15.0.5"
+    http-cache-semantics "^4.1.0"
+    http-proxy-agent "^4.0.1"
+    https-proxy-agent "^5.0.0"
+    is-lambda "^1.0.1"
+    lru-cache "^6.0.0"
+    minipass "^3.1.3"
+    minipass-collect "^1.0.2"
+    minipass-fetch "^1.3.2"
+    minipass-flush "^1.0.5"
+    minipass-pipeline "^1.2.4"
+    promise-retry "^2.0.1"
+    socks-proxy-agent "^5.0.0"
+    ssri "^8.0.0"
+
+map-age-cleaner@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+  integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+  dependencies:
+    p-defer "^1.0.0"
+
+mdn-data@2.0.14:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
+media-typer@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+  integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+mem@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/mem/-/mem-5.1.1.tgz#7059b67bf9ac2c924c9f1cff7155a064394adfb3"
+  integrity sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==
+  dependencies:
+    map-age-cleaner "^0.1.3"
+    mimic-fn "^2.1.0"
+    p-is-promise "^2.1.0"
+
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+merge@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98"
+  integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==
+
+methods@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+  integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+
+mfm-js@0.20.0:
+  version "0.20.0"
+  resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.20.0.tgz#3afdcd7959461fd825aa8af9b9e8a57cdbddc290"
+  integrity sha512-1+3tV3nWUKQNh/ztX3wXu5iLBtdsg6q3wUhl+XyOhc2H3sQdG+sih/w2c0nR9TIawjN+Z1/pvgGzxMJHfmKQmA==
+  dependencies:
+    twemoji-parser "13.1.x"
+
+micromatch@4.x:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
+  integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.2.3"
+
+micromatch@^4.0.0, micromatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.0.5"
+
+microseconds@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
+  integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
+
+mime-db@1.44.0:
+  version "1.44.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
+  integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
+
+mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24:
+  version "2.1.27"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
+  integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
+  dependencies:
+    mime-db "1.44.0"
+
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+mimic-response@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
+  integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
+
+mimic-response@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
+  integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
+
+mimic-response@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
+  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
+
+minimalistic-assert@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimatch@3.0.4, minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+minipass-collect@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
+  integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass-fetch@^1.3.2:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a"
+  integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==
+  dependencies:
+    minipass "^3.1.0"
+    minipass-sized "^1.0.3"
+    minizlib "^2.0.0"
+  optionalDependencies:
+    encoding "^0.1.12"
+
+minipass-flush@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
+  integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
+  integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass-sized@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70"
+  integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5"
+  integrity sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==
+  dependencies:
+    yallist "^4.0.0"
+
+minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
+  integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
+  dependencies:
+    yallist "^4.0.0"
+
+minizlib@^2.0.0, minizlib@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
+  integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
+  dependencies:
+    minipass "^3.0.0"
+    yallist "^4.0.0"
+
+misskey-js@0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.8.tgz#b47a1ec54bff96b23cc2a6b307a3895f99a94424"
+  integrity sha512-Q1L6FaroVz8kpW7T4xQyJmJKSwjOYPbNY3TspOUWmbIBDf2JP0HVeKEFLI9dvdSL0kSkdQNz3MSVLjlHiyPqLQ==
+  dependencies:
+    autobind-decorator "^2.4.0"
+    eventemitter3 "^4.0.7"
+    reconnecting-websocket "^4.4.0"
+
+mkdirp-classic@^0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz#54c441ce4c96cd7790e10b41a87aa51068ecab2b"
+  integrity sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==
+
+mkdirp-classic@^0.5.3:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
+  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+
+mkdirp@0.x, mkdirp@^0.5.3, mkdirp@^0.5.4:
+  version "0.5.5"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
+  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
+  dependencies:
+    minimist "^1.2.5"
+
+mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
+mocha@8.4.0:
+  version "8.4.0"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.4.0.tgz#677be88bf15980a3cae03a73e10a0fc3997f0cff"
+  integrity sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==
+  dependencies:
+    "@ungap/promise-all-settled" "1.1.2"
+    ansi-colors "4.1.1"
+    browser-stdout "1.3.1"
+    chokidar "3.5.1"
+    debug "4.3.1"
+    diff "5.0.0"
+    escape-string-regexp "4.0.0"
+    find-up "5.0.0"
+    glob "7.1.6"
+    growl "1.10.5"
+    he "1.2.0"
+    js-yaml "4.0.0"
+    log-symbols "4.0.0"
+    minimatch "3.0.4"
+    ms "2.1.3"
+    nanoid "3.1.20"
+    serialize-javascript "5.0.1"
+    strip-json-comments "3.1.1"
+    supports-color "8.1.1"
+    which "2.0.2"
+    wide-align "1.1.3"
+    workerpool "6.1.0"
+    yargs "16.2.0"
+    yargs-parser "20.2.4"
+    yargs-unparser "2.0.0"
+
+moment-timezone@^0.5.25:
+  version "0.5.28"
+  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338"
+  integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==
+  dependencies:
+    moment ">= 2.9.0"
+
+"moment@>= 2.9.0", moment@^2.22.2:
+  version "2.24.0"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+multer@1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b"
+  integrity sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==
+  dependencies:
+    append-field "^1.0.0"
+    busboy "^0.2.11"
+    concat-stream "^1.5.2"
+    mkdirp "^0.5.4"
+    object-assign "^4.1.1"
+    on-finished "^2.3.0"
+    type-is "^1.6.4"
+    xtend "^4.0.0"
+
+mz@^2.4.0, mz@^2.7.0:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
+  integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
+  dependencies:
+    any-promise "^1.0.0"
+    object-assign "^4.0.1"
+    thenify-all "^1.0.0"
+
+nan@^2.14.2:
+  version "2.14.2"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
+  integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
+
+nano-time@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
+  integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
+  dependencies:
+    big-integer "^1.6.16"
+
+nanoid@3.1.20:
+  version "3.1.20"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
+  integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
+
+nanoid@^3.1.23:
+  version "3.1.23"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
+  integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
+
+napi-build-utils@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
+  integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
+
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+needle@^2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.2.tgz#cf1a8fce382b5a280108bba90a14993c00e4010a"
+  integrity sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==
+  dependencies:
+    debug "^3.2.6"
+    iconv-lite "^0.4.4"
+    sax "^1.2.4"
+
+negotiator@0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
+  integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
+
+neo-async@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+nested-property@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-4.0.0.tgz#a67b5a31991e701e03cdbaa6453bc5b1011bb88d"
+  integrity sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==
+
+netmask@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
+  integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
+
+next-line@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603"
+  integrity sha1-/K5XhTBStqm66CCOQN19PC0wRgM=
+
+next-tick@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+  integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
+
+node-abi@^2.21.0:
+  version "2.21.0"
+  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.21.0.tgz#c2dc9ebad6f4f53d6ea9b531e7b8faad81041d48"
+  integrity sha512-smhrivuPqEM3H5LmnY3KU6HfYv0u4QklgAxfFyRNujKUzbUcYZ+Jc2EhukB9SRcD2VpqhxM7n/MIcp1Ua1/JMg==
+  dependencies:
+    semver "^5.4.1"
+
+node-addon-api@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87"
+  integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==
+
+node-fetch@2.6.1, node-fetch@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
+  integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+
+node-fetch@3.0.0-beta.9:
+  version "3.0.0-beta.9"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0-beta.9.tgz#0a7554cfb824380dd6812864389923c783c80d9b"
+  integrity sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==
+  dependencies:
+    data-uri-to-buffer "^3.0.1"
+    fetch-blob "^2.1.1"
+
+node-gyp-build@~3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
+  integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
+
+node-gyp@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.0.0.tgz#225af2b06b8419ae81f924bf25ae4c167f6378a5"
+  integrity sha512-Jod6NxyWtcwrpAQe0O/aXOpC5QfncotgtG73dg65z6VW/C6g/G4jiajXQUBIJ8pk/VfM6mBYE9BN/HvudTunUQ==
+  dependencies:
+    env-paths "^2.2.0"
+    glob "^7.1.4"
+    graceful-fs "^4.2.6"
+    make-fetch-happen "^8.0.14"
+    nopt "^5.0.0"
+    npmlog "^4.1.2"
+    rimraf "^3.0.2"
+    semver "^7.3.5"
+    tar "^6.1.0"
+    which "^2.0.2"
+
+node-releases@^1.1.70, node-releases@^1.1.71:
+  version "1.1.71"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb"
+  integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==
+
+nodemailer@6.7.0:
+  version "6.7.0"
+  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.0.tgz#86614722c4e0c33d1b5b02aecb90d6d629932b0d"
+  integrity sha512-AtiTVUFHLiiDnMQ43zi0YgkzHOEWUkhDgPlBXrsDzJiJvB29Alo4OKxHQ0ugF3gRqRQIneCLtZU3yiUo7pItZw==
+
+nofilter@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-2.0.3.tgz#f5460f3cb33147005883e3f5d4476239501fa187"
+  integrity sha512-FbuXC+lK+GU2+63D1kC1ETiZo+Z7SIi7B+mxKTCH1byrh6WFvfBCN/wpherFz0a0bjGd7EKTst/cz0yLeNngug==
+  dependencies:
+    "@cto.af/textdecoder" "^0.0.0"
+
+nofilter@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-3.1.0.tgz#c757ba68801d41ff930ba2ec55bab52ca184aa66"
+  integrity sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==
+
+nopt@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
+  integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
+  dependencies:
+    abbrev "1"
+    osenv "^0.1.4"
+
+nopt@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
+  integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
+  dependencies:
+    abbrev "1"
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-url@^4.1.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
+  integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
+
+normalize-url@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
+  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
+
+npm-run-path@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
+
+npmlog@^4.0.1, npmlog@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+  integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+  dependencies:
+    are-we-there-yet "~1.1.2"
+    console-control-strings "~1.1.0"
+    gauge "~2.7.3"
+    set-blocking "~2.0.0"
+
+nth-check@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
+  integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
+  dependencies:
+    boolbase "^1.0.0"
+
+nth-check@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+  dependencies:
+    boolbase "~1.0.0"
+
+number-is-nan@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+  integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
+nwsapi@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+  integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+
+oauth-sign@~0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+oauth@0.9.15:
+  version "0.9.15"
+  resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
+  integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
+
+object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-inspect@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+  integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+  dependencies:
+    define-properties "^1.1.2"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    object-keys "^1.0.11"
+
+object.getownpropertydescriptors@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+oblivious-set@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
+  integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
+
+on-finished@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+  integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+  dependencies:
+    ee-first "1.1.1"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^5.1.0:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+  dependencies:
+    mimic-fn "^2.1.0"
+
+only@~0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
+  integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
+
+opentype.js@^0.4.3:
+  version "0.4.11"
+  resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.4.11.tgz#281a2390639cc15931c955d8d63c14a7c7772b41"
+  integrity sha1-KBojkGOcwVkxyVXY1jwUp8d3K0E=
+
+optionator@^0.8.1:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
+optionator@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
+  dependencies:
+    deep-is "^0.1.3"
+    fast-levenshtein "^2.0.6"
+    levn "^0.4.1"
+    prelude-ls "^1.2.1"
+    type-check "^0.4.0"
+    word-wrap "^1.2.3"
+
+os-homedir@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-locale@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-5.0.0.tgz#6d26c1d95b6597c5d5317bf5fba37eccec3672e0"
+  integrity sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==
+  dependencies:
+    execa "^4.0.0"
+    lcid "^3.0.0"
+    mem "^5.0.0"
+
+os-tmpdir@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+  integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+os-utils@0.0.14:
+  version "0.0.14"
+  resolved "https://registry.yarnpkg.com/os-utils/-/os-utils-0.0.14.tgz#29e511697b1982b8c627722175fe39797ef64156"
+  integrity sha1-KeURaXsZgrjGJ3Ihdf45eX72QVY=
+
+osenv@^0.1.4:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+  integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
+  dependencies:
+    os-homedir "^1.0.0"
+    os-tmpdir "^1.0.0"
+
+p-cancelable@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e"
+  integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==
+
+p-defer@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+  integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+  integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-is-promise@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
+  integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
+
+p-limit@^2.0.0, p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe"
+  integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
+p-locate@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+  dependencies:
+    p-limit "^3.0.2"
+
+p-map@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
+  integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
+p-map@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
+  integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
+  dependencies:
+    aggregate-error "^3.0.0"
+
+p-timeout@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+  integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
+  dependencies:
+    p-finally "^1.0.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+packet-reader@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74"
+  integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-passwd@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+  integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
+
+parse5-htmlparser2-tree-adapter@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
+  integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
+  dependencies:
+    parse5 "^6.0.1"
+
+parse5@6.0.1, parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+
+parse5@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
+  integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
+
+parseurl@^1.3.2:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+passthrough-counter@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/passthrough-counter/-/passthrough-counter-1.0.0.tgz#1967d9e66da572b5c023c787db112a387ab166fa"
+  integrity sha1-GWfZ5m2lcrXAI8eH2xEqOHqxZvo=
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@1.0.1, path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+  integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-to-regexp@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427"
+  integrity sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==
+
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+peek-readable@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.0.1.tgz#9a045f291db254111c3412c1ce4fec27ddd4d202"
+  integrity sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ==
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+pg-connection-string@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
+  integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
+
+pg-int8@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
+  integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
+
+pg-pool@^3.4.1:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.4.1.tgz#0e71ce2c67b442a5e862a9c182172c37eda71e9c"
+  integrity sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==
+
+pg-protocol@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0"
+  integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==
+
+pg-types@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
+  integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
+  dependencies:
+    pg-int8 "1.0.1"
+    postgres-array "~2.0.0"
+    postgres-bytea "~1.0.0"
+    postgres-date "~1.0.4"
+    postgres-interval "^1.1.0"
+
+pg@8.7.1:
+  version "8.7.1"
+  resolved "https://registry.yarnpkg.com/pg/-/pg-8.7.1.tgz#9ea9d1ec225980c36f94e181d009ab9f4ce4c471"
+  integrity sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==
+  dependencies:
+    buffer-writer "2.0.0"
+    packet-reader "1.0.0"
+    pg-connection-string "^2.5.0"
+    pg-pool "^3.4.1"
+    pg-protocol "^1.5.0"
+    pg-types "^2.1.0"
+    pgpass "1.x"
+
+pgpass@1.x:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306"
+  integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=
+  dependencies:
+    split "^1.0.0"
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7, picomatch@^2.2.1:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+  integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
+picomatch@^2.2.3:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
+  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
+
+pify@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+  integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pngjs@^3.3.0, pngjs@^3.3.1:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
+  integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
+
+portscanner@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1"
+  integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==
+  dependencies:
+    async "^2.6.0"
+    is-number-like "^1.0.3"
+
+postcss-calc@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a"
+  integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==
+  dependencies:
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.2"
+
+postcss-colormin@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.1.tgz#6e444a806fd3c578827dbad022762df19334414d"
+  integrity sha512-VVwMrEYLcHYePUYV99Ymuoi7WhKrMGy/V9/kTS0DkCoJYmmjdOMneyhzYUxcNgteKDVbrewOkSM7Wje/MFwxzA==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    colord "^2.9.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-convert-values@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz#879b849dc3677c7d6bc94b6a2c1a3f0808798059"
+  integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-discard-comments@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe"
+  integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==
+
+postcss-discard-duplicates@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d"
+  integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==
+
+postcss-discard-empty@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8"
+  integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==
+
+postcss-discard-overridden@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6"
+  integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==
+
+postcss-merge-longhand@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.3.tgz#42194a5ffbaa5513edbf606ef79c44958564658b"
+  integrity sha512-kmB+1TjMTj/bPw6MCDUiqSA5e/x4fvLffiAdthra3a0m2/IjTrWsTmD3FdSskzUjEwkj5ZHBDEbv5dOcqD7CMQ==
+  dependencies:
+    css-color-names "^1.0.1"
+    postcss-value-parser "^4.1.0"
+    stylehacks "^5.0.1"
+
+postcss-merge-rules@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a"
+  integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    cssnano-utils "^2.0.1"
+    postcss-selector-parser "^6.0.5"
+    vendors "^1.0.3"
+
+postcss-minify-font-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf"
+  integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-minify-gradients@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.3.tgz#f970a11cc71e08e9095e78ec3a6b34b91c19550e"
+  integrity sha512-Z91Ol22nB6XJW+5oe31+YxRsYooxOdFKcbOqY/V8Fxse1Y3vqlNRpi1cxCqoACZTQEhl+xvt4hsbWiV5R+XI9Q==
+  dependencies:
+    colord "^2.9.1"
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-minify-params@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c"
+  integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    browserslist "^4.16.0"
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+    uniqs "^2.0.0"
+
+postcss-minify-selectors@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54"
+  integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    postcss-selector-parser "^6.0.5"
+
+postcss-modules-extract-imports@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
+  integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
+
+postcss-modules-local-by-default@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
+  integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
+  dependencies:
+    icss-utils "^5.0.0"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.1.0"
+
+postcss-modules-scope@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
+  integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
+  dependencies:
+    postcss-selector-parser "^6.0.4"
+
+postcss-modules-values@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
+  integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
+  dependencies:
+    icss-utils "^5.0.0"
+
+postcss-normalize-charset@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0"
+  integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==
+
+postcss-normalize-display-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd"
+  integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-positions@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5"
+  integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-repeat-style@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5"
+  integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-string@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0"
+  integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-timing-functions@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c"
+  integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-unicode@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37"
+  integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==
+  dependencies:
+    browserslist "^4.16.0"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-url@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz#ddcdfb7cede1270740cf3e4dfc6008bd96abc763"
+  integrity sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==
+  dependencies:
+    is-absolute-url "^3.0.3"
+    normalize-url "^6.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-whitespace@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a"
+  integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-ordered-values@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044"
+  integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-reduce-initial@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946"
+  integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==
+  dependencies:
+    browserslist "^4.16.0"
+    caniuse-api "^3.0.0"
+
+postcss-reduce-transforms@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640"
+  integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
+  integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
+  dependencies:
+    cssesc "^3.0.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+    util-deprecate "^1.0.2"
+
+postcss-selector-parser@^6.0.5:
+  version "6.0.6"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea"
+  integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-svgo@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30"
+  integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+    svgo "^2.7.0"
+
+postcss-unique-selectors@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc"
+  integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    postcss-selector-parser "^6.0.5"
+    uniqs "^2.0.0"
+
+postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
+  integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
+
+postcss@^8.2.15:
+  version "8.3.0"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f"
+  integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==
+  dependencies:
+    colorette "^1.2.2"
+    nanoid "^3.1.23"
+    source-map-js "^0.6.2"
+
+postgres-array@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
+  integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
+
+postgres-bytea@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35"
+  integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=
+
+postgres-date@~1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.5.tgz#710b27de5f27d550f6e80b5d34f7ba189213c2ee"
+  integrity sha512-pdau6GRPERdAYUQwkBnGKxEfPyhVZXG/JiS44iZWiNdSOWE09N2lUgN6yshuq6fVSon4Pm0VMXd1srUUkLe9iA==
+
+postgres-interval@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695"
+  integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==
+  dependencies:
+    xtend "^4.0.0"
+
+prebuild-install@^6.1.4:
+  version "6.1.4"
+  resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f"
+  integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==
+  dependencies:
+    detect-libc "^1.0.3"
+    expand-template "^2.0.3"
+    github-from-package "0.0.0"
+    minimist "^1.2.3"
+    mkdirp-classic "^0.5.3"
+    napi-build-utils "^1.0.1"
+    node-abi "^2.21.0"
+    npmlog "^4.0.1"
+    pump "^3.0.0"
+    rc "^1.2.7"
+    simple-get "^3.0.3"
+    tar-fs "^2.0.0"
+    tunnel-agent "^0.6.0"
+
+prelude-ls@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+pretty@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pretty/-/pretty-2.0.0.tgz#adbc7960b7bbfe289a557dc5f737619a220d06a5"
+  integrity sha1-rbx5YLe7/iiaVX3F9zdhmiINBqU=
+  dependencies:
+    condense-newlines "^0.2.1"
+    extend-shallow "^2.0.1"
+    js-beautify "^1.6.12"
+
+prettyjson@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289"
+  integrity sha1-/P+rQdGcq0365eV15kJGYZsS0ok=
+  dependencies:
+    colors "^1.1.2"
+    minimist "^1.2.0"
+
+printj@~1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
+  integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
+
+prismjs@1.25.0:
+  version "1.25.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
+  integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
+
+private-ip@2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/private-ip/-/private-ip-2.3.3.tgz#1e80ff8443e5ac78f555631aec3ea6ff027fa6aa"
+  integrity sha512-5zyFfekIVUOTVbL92hc8LJOtE/gyGHeREHkJ2yTyByP8Q2YZVoBqLg3EfYLeF0oVvGqtaEX2t2Qovja0/gStXw==
+  dependencies:
+    ip-regex "^4.3.0"
+    ipaddr.js "^2.0.1"
+    is-ip "^3.1.0"
+    netmask "^2.0.2"
+
+probe-image-size@7.2.1:
+  version "7.2.1"
+  resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.1.tgz#df0c924e67e247bc94f8fcb0fad7f0081061fc44"
+  integrity sha512-d+6L3NvQBCNt4peRDoEfA7r9bPm6/qy18FnLKwg4NWBC5JrJm0pMLRg1kF4XNsPe1bUdt3WIMonPJzQWN2HXjQ==
+  dependencies:
+    lodash.merge "^4.6.2"
+    needle "^2.5.2"
+    stream-parser "~0.3.1"
+
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+progress@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
+promise-inflight@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+  integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+
+promise-limit@2.7.0:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/promise-limit/-/promise-limit-2.7.0.tgz#eb5737c33342a030eaeaecea9b3d3a93cb592b26"
+  integrity sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==
+
+promise-retry@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
+  integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
+  dependencies:
+    err-code "^2.0.2"
+    retry "^0.12.0"
+
+promise.prototype.finally@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz#b8af89160c9c673cefe3b4c4435b53cfd0287067"
+  integrity sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.0"
+    function-bind "^1.1.1"
+
+promise@^7.0.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+  integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+  dependencies:
+    asap "~2.0.3"
+
+proto-list@~1.2.1:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
+  integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
+
+pseudomap@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+  integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+
+psl@^1.1.28, psl@^1.1.33:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+  integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+
+pug-attrs@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41"
+  integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==
+  dependencies:
+    constantinople "^4.0.1"
+    js-stringify "^1.0.2"
+    pug-runtime "^3.0.0"
+
+pug-code-gen@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce"
+  integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==
+  dependencies:
+    constantinople "^4.0.1"
+    doctypes "^1.1.0"
+    js-stringify "^1.0.2"
+    pug-attrs "^3.0.0"
+    pug-error "^2.0.0"
+    pug-runtime "^3.0.0"
+    void-elements "^3.1.0"
+    with "^7.0.0"
+
+pug-error@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5"
+  integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==
+
+pug-filters@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e"
+  integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==
+  dependencies:
+    constantinople "^4.0.1"
+    jstransformer "1.0.0"
+    pug-error "^2.0.0"
+    pug-walk "^2.0.0"
+    resolve "^1.15.1"
+
+pug-lexer@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5"
+  integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==
+  dependencies:
+    character-parser "^2.2.0"
+    is-expression "^4.0.0"
+    pug-error "^2.0.0"
+
+pug-linker@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708"
+  integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==
+  dependencies:
+    pug-error "^2.0.0"
+    pug-walk "^2.0.0"
+
+pug-load@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662"
+  integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==
+  dependencies:
+    object-assign "^4.1.1"
+    pug-walk "^2.0.0"
+
+pug-parser@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260"
+  integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==
+  dependencies:
+    pug-error "^2.0.0"
+    token-stream "1.0.0"
+
+pug-runtime@^3.0.0, pug-runtime@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7"
+  integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==
+
+pug-strip-comments@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e"
+  integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==
+  dependencies:
+    pug-error "^2.0.0"
+
+pug-walk@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe"
+  integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==
+
+pug@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535"
+  integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==
+  dependencies:
+    pug-code-gen "^3.0.2"
+    pug-filters "^4.0.0"
+    pug-lexer "^5.0.1"
+    pug-linker "^4.0.0"
+    pug-load "^3.0.0"
+    pug-parser "^6.0.0"
+    pug-runtime "^3.0.1"
+    pug-strip-comments "^2.0.0"
+
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+punycode@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+  integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@2.1.1, punycode@^2.1.0, punycode@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+pureimage@0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/pureimage/-/pureimage-0.3.5.tgz#cd5e91f7b6409fcf4880297aaa3e7fc0afc24d5e"
+  integrity sha512-+CFUEpoX6GemlKlHihI7Ii4IqKqF5KZjd682sAxwzbc4t4zU4Gwhxd4W3UMZW94nJzf0n4nA9zJrwTR4jZB4TA==
+  dependencies:
+    jpeg-js "^0.4.1"
+    opentype.js "^0.4.3"
+    pngjs "^3.3.1"
+
+q@1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
+  integrity sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=
+
+qrcode@1.4.4:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
+  integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
+  dependencies:
+    buffer "^5.4.3"
+    buffer-alloc "^1.2.0"
+    buffer-from "^1.1.1"
+    dijkstrajs "^1.0.1"
+    isarray "^2.0.1"
+    pngjs "^3.3.0"
+    yargs "^13.2.4"
+
+qs@^6.4.0, qs@^6.5.2:
+  version "6.9.3"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e"
+  integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==
+
+qs@~6.5.2:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystring@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+  integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+quick-lru@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
+  integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+
+random-seed@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/random-seed/-/random-seed-0.3.0.tgz#d945f2e1f38f49e8d58913431b8bf6bb937556cd"
+  integrity sha1-2UXy4fOPSejViRNDG4v2u5N1Vs0=
+  dependencies:
+    json-stringify-safe "^5.0.1"
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+rangestr@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/rangestr/-/rangestr-0.0.1.tgz#f72ff9246f10f2a7d7c16e14616f617be2c2635a"
+  integrity sha1-9y/5JG8Q8qfXwW4UYW9he+LCY1o=
+
+ratelimiter@3.4.1:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/ratelimiter/-/ratelimiter-3.4.1.tgz#fa69e94937413382a926aaa17aaeaa6263af4659"
+  integrity sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==
+
+raw-body@^2.2.0, raw-body@^2.3.3:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c"
+  integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==
+  dependencies:
+    bytes "3.1.0"
+    http-errors "1.7.3"
+    iconv-lite "0.4.24"
+    unpipe "1.0.0"
+
+rc@^1.2.7:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+  integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+  dependencies:
+    deep-extend "^0.6.0"
+    ini "~1.3.0"
+    minimist "^1.2.0"
+    strip-json-comments "~2.0.1"
+
+rdf-canonize@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-3.0.0.tgz#f5bade563e5e58f5cc5881afcba3c43839e8c747"
+  integrity sha512-LXRkhab1QaPJnhUIt1gtXXKswQCZ9zpflsSZFczG7mCLAkMvVjdqCGk9VXCUss0aOUeEyV2jtFxGcdX8DSkj9w==
+  dependencies:
+    setimmediate "^1.0.5"
+
+re2@1.16.0:
+  version "1.16.0"
+  resolved "https://registry.yarnpkg.com/re2/-/re2-1.16.0.tgz#f311eb4865b1296123800ea8e013cec8dab25590"
+  integrity sha512-eizTZL2ZO0ZseLqfD4t3Qd0M3b3Nr0MBWpX81EbPMIud/1d/CSfUIx2GQK8fWiAeHoSekO5EOeFib2udTZLwYw==
+  dependencies:
+    install-artifact-from-github "^1.2.0"
+    nan "^2.14.2"
+    node-gyp "^8.0.0"
+
+readable-stream@1.1.x:
+  version "1.1.14"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+  integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "0.0.1"
+    string_decoder "~0.10.x"
+
+readable-stream@^2.0.6, readable-stream@^2.2.2:
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+readable-web-to-node-stream@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.0.tgz#4ca5408e70471069119d691934141a52de413955"
+  integrity sha512-HNmLb3n0SteGAs8HQlErYPGeO+y7cvL/mVUKtXeUkl0iCZ/2GIgKGrCFHyS7UXFnO8uc9U+0y3pYIzAPsjFfvA==
+  dependencies:
+    "@types/readable-stream" "^2.3.9"
+    readable-stream "^3.6.0"
+
+readdirp@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17"
+  integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==
+  dependencies:
+    picomatch "^2.0.7"
+
+reconnecting-websocket@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
+  integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
+
+redis-commands@1.7.0, redis-commands@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
+  integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
+
+redis-errors@^1.0.0, redis-errors@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
+  integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
+
+redis-lock@0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/redis-lock/-/redis-lock-0.1.4.tgz#e83590bee22b5f01cdb65bfbd88d988045356272"
+  integrity sha512-7/+zu86XVQfJVx1nHTzux5reglDiyUCDwmW7TSlvVezfhH2YLc/Rc8NE0ejQG+8/0lwKzm29/u/4+ogKeLosiA==
+
+redis-parser@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
+  integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
+  dependencies:
+    redis-errors "^1.0.0"
+
+redis@3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c"
+  integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==
+  dependencies:
+    denque "^1.5.0"
+    redis-commands "^1.7.0"
+    redis-errors "^1.2.0"
+    redis-parser "^3.0.0"
+
+reflect-metadata@0.1.13, reflect-metadata@^0.1.13:
+  version "0.1.13"
+  resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
+  integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
+
+regenerator-runtime@^0.13.4:
+  version "0.13.7"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+  integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
+
+regexpp@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
+  integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
+
+rename@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/rename/-/rename-1.0.4.tgz#a0f25078fa4195e650f73050c7c12ccf689f430b"
+  integrity sha1-oPJQePpBleZQ9zBQx8Esz2ifQws=
+  dependencies:
+    debug "^2.5.2"
+
+repeat-string@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+  integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+request-promise-core@1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
+  integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
+  dependencies:
+    lodash "^4.17.19"
+
+request-promise-native@1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
+  integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
+  dependencies:
+    request-promise-core "1.1.4"
+    stealthy-require "^1.1.1"
+    tough-cookie "^2.3.3"
+
+request-stats@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/request-stats/-/request-stats-3.0.0.tgz#769155dc8974d78d4a1cb87bbf14eaab985afe25"
+  integrity sha1-dpFV3Il0141KHLh7vxTqq5ha/iU=
+  dependencies:
+    http-headers "^3.0.1"
+    once "^1.4.0"
+
+request@2.88.2, request@^2.88.2:
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
+require-all@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/require-all/-/require-all-3.0.0.tgz#473d49704be310115ce124f77383b1ebd8671312"
+  integrity sha1-Rz1JcEvjEBFc4ST3c4Ox69hnExI=
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+require-main-filename@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+resolve-alpn@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c"
+  integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==
+
+resolve-dir@^1.0.0, resolve-dir@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
+  integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
+  dependencies:
+    expand-tilde "^2.0.0"
+    global-modules "^1.0.0"
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-path@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
+  integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=
+  dependencies:
+    http-errors "~1.6.2"
+    path-is-absolute "1.0.1"
+
+resolve@^1.15.1:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+  integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
+  dependencies:
+    is-core-module "^2.2.0"
+    path-parse "^1.0.6"
+
+resolve@^1.3.2:
+  version "1.18.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130"
+  integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==
+  dependencies:
+    is-core-module "^2.0.0"
+    path-parse "^1.0.6"
+
+responselike@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723"
+  integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==
+  dependencies:
+    lowercase-keys "^2.0.0"
+
+retry@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
+  integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
+
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+  dependencies:
+    glob "^7.1.3"
+
+rndstr@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/rndstr/-/rndstr-1.0.0.tgz#77e66fa8f9b4836853fdd91e50719591bb67d349"
+  integrity sha1-d+ZvqPm0g2hT/dkeUHGVkbtn00k=
+  dependencies:
+    rangestr "0.0.1"
+    seedrandom "2.4.2"
+
+rsvp@^4.8.5:
+  version "4.8.5"
+  resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
+  integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
+
+run-parallel@^1.1.9:
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
+  integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
+
+rxjs@^6.6.3:
+  version "6.6.6"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70"
+  integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==
+  dependencies:
+    tslib "^1.9.0"
+
+s-age@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/s-age/-/s-age-1.1.2.tgz#c0cf15233ccc93f41de92ea42c36d957977d1ea2"
+  integrity sha512-aSN2TlF39WLoZA/6cgYSJZhKt63kJ4EaadejPWjWY9/h4rksIqvfWY3gfd+3uAegSM1IXsA9aWeEhJtkxkFQtA==
+
+safari-14-idb-fix@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-1.0.4.tgz#5c68ba63e2a8ae0d89a0aa1e13fe89e3aef7da19"
+  integrity sha512-4+Y2baQdgJpzu84d0QjySl70Kyygzf0pepVg8NVg4NnQEPpfC91fAn0baNvtStlCjUUxxiu0BOMiafa98fRRuA==
+
+safe-buffer@*:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sax@1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
+  integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
+
+sax@>=0.6.0, sax@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+saxes@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
+  integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
+  dependencies:
+    xmlchars "^2.2.0"
+
+schema-utils@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
+  integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
+  dependencies:
+    "@types/json-schema" "^7.0.6"
+    ajv "^6.12.5"
+    ajv-keywords "^3.5.2"
+
+secure-json-parse@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20"
+  integrity sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA==
+
+seedrandom@2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.2.tgz#18d78c41287d13aff8eadb29e235938b248aa9ff"
+  integrity sha1-GNeMQSh9E6/46tsp4jWTiySKqf8=
+
+seedrandom@3.0.5:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
+  integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
+
+semver@6.x:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+semver@^5.3.0, semver@^5.4.1, semver@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
+  version "7.3.4"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
+  integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
+  dependencies:
+    lru-cache "^6.0.0"
+
+semver@^7.3.5:
+  version "7.3.5"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
+serialize-javascript@5.0.1, serialize-javascript@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
+  integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
+  dependencies:
+    randombytes "^2.1.0"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+setimmediate@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+  integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+  integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+
+setprototypeof@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
+  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+
+sha.js@^2.4.11:
+  version "2.4.11"
+  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+  integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+  dependencies:
+    inherits "^2.0.1"
+    safe-buffer "^5.0.1"
+
+sharp@0.29.2:
+  version "0.29.2"
+  resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.2.tgz#e8c003cd9cb321585b32dbda6eed3baa7d6f2308"
+  integrity sha512-XWRdiYLIJ3tDUejRyG24KERnJzMfIoyiJBntd2S6/uj3NEeNgRFRLgiBlvPxMa8aml14dKKD98yHinSNKp1xzQ==
+  dependencies:
+    color "^4.0.1"
+    detect-libc "^1.0.3"
+    node-addon-api "^4.2.0"
+    prebuild-install "^6.1.4"
+    semver "^7.3.5"
+    simple-get "^3.1.0"
+    tar-fs "^2.1.1"
+    tunnel-agent "^0.6.0"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+sigmund@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
+  integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
+  integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+
+simple-concat@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
+  integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
+
+simple-get@^3.0.3, simple-get@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3"
+  integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==
+  dependencies:
+    decompress-response "^4.2.0"
+    once "^1.3.1"
+    simple-concat "^1.0.0"
+
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
+  dependencies:
+    is-arrayish "^0.3.1"
+
+slash@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+smart-buffer@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba"
+  integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==
+
+socks-proxy-agent@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60"
+  integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==
+  dependencies:
+    agent-base "6"
+    debug "4"
+    socks "^2.3.3"
+
+socks@^2.3.3:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e"
+  integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==
+  dependencies:
+    ip "^1.1.5"
+    smart-buffer "^4.1.0"
+
+source-list-map@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
+  integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+
+source-map-js@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
+  integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
+
+source-map-support@~0.5.19:
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@~0.7.2:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
+spawn-command@^0.0.2-1:
+  version "0.0.2-1"
+  resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
+  integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=
+
+speakeasy@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/speakeasy/-/speakeasy-2.0.0.tgz#85c91a071b09a5cb8642590d983566165f57613a"
+  integrity sha1-hckaBxsJpcuGQlkNmDVmFl9XYTo=
+  dependencies:
+    base32.js "0.0.1"
+
+split@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
+  integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
+  dependencies:
+    through "2"
+
+sprintf-js@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
+  integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.14.1, sshpk@^1.7.0:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+  integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+  dependencies:
+    asn1 "~0.2.3"
+    assert-plus "^1.0.0"
+    bcrypt-pbkdf "^1.0.0"
+    dashdash "^1.12.0"
+    ecc-jsbn "~0.1.1"
+    getpass "^0.1.1"
+    jsbn "~0.1.0"
+    safer-buffer "^2.0.2"
+    tweetnacl "~0.14.0"
+
+ssri@^8.0.0, ssri@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
+  integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
+  dependencies:
+    minipass "^3.1.1"
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+standard-as-callback@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
+  integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
+
+"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+  integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+stealthy-require@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+  integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+stream-parser@~0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
+  integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=
+  dependencies:
+    debug "2"
+
+streamsearch@0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
+  integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
+
+strict-event-emitter-types@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f"
+  integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==
+
+string-width@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+  integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+  dependencies:
+    code-point-at "^1.0.0"
+    is-fullwidth-code-point "^1.0.0"
+    strip-ansi "^3.0.0"
+
+"string-width@^1.0.2 || 2":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+  integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+  dependencies:
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^4.0.0"
+
+string-width@^3.0.0, string-width@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+  dependencies:
+    emoji-regex "^7.0.1"
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^5.1.0"
+
+string-width@^4.1.0, string-width@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
+  integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.0"
+
+string.prototype.trimend@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
+  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+
+string.prototype.trimleft@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc"
+  integrity sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+    string.prototype.trimstart "^1.0.0"
+
+string.prototype.trimright@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3"
+  integrity sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+    string.prototype.trimend "^1.0.0"
+
+string.prototype.trimstart@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
+  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+string_decoder@~0.10.x:
+  version "0.10.31"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+  integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
+stringz@2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/stringz/-/stringz-2.1.0.tgz#5896b4713eac31157556040fb90258fb02c1630c"
+  integrity sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==
+  dependencies:
+    char-regex "^1.0.2"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+  integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+  dependencies:
+    ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+  integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+  dependencies:
+    ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+  dependencies:
+    ansi-regex "^4.1.0"
+
+strip-ansi@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
+  integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
+  dependencies:
+    ansi-regex "^5.0.0"
+
+strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+  integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+strip-json-comments@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+  integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+
+strtok3@^6.2.4:
+  version "6.2.4"
+  resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.2.4.tgz#302aea64c0fa25d12a0385069ba66253fdc38a81"
+  integrity sha512-GO8IcFF9GmFDvqduIspUBwCzCbqzegyVKIsSymcMgiZKeCfrN9SowtUoi8+b59WZMAjIzVZic/Ft97+pynR3Iw==
+  dependencies:
+    "@tokenizer/token" "^0.3.0"
+    peek-readable "^4.0.1"
+
+style-loader@3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
+  integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
+
+stylehacks@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb"
+  integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==
+  dependencies:
+    browserslist "^4.16.0"
+    postcss-selector-parser "^6.0.4"
+
+summaly@2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.4.1.tgz#d2a8fa6bad10c1651eb0b849aab3009e87216a3d"
+  integrity sha512-1gETEQXqK5RD7yIGgdGeTwGL1uh+uj14u99atzNLNmvsxwdtZbPvDHZBPXkAW0cqsd8teoBJln5Dh1QeAhvGIg==
+  dependencies:
+    cheerio-httpcli "0.8.2"
+    debug "4.3.2"
+    escape-regexp "0.0.1"
+    html-entities "2.3.2"
+    koa "2.13.1"
+    request "2.88.2"
+    request-promise-native "1.0.9"
+    require-all "3.0.0"
+    trace-redirect "1.0.6"
+
+supports-color@8.1.1, supports-color@^8.1.0:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.0.0, supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+svgo@^2.7.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
+  integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
+  dependencies:
+    "@trysound/sax" "0.2.0"
+    commander "^7.2.0"
+    css-select "^4.1.3"
+    css-tree "^1.1.3"
+    csso "^4.2.0"
+    picocolors "^1.0.0"
+    stable "^0.1.8"
+
+symbol-tree@^3.2.4:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+  integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
+syslog-pro@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/syslog-pro/-/syslog-pro-1.0.0.tgz#e46bfd39f58937352645091e84a3b903f39e12ea"
+  integrity sha512-7SNMJKtQBJlwBUp1jxFT7bXya71cnINXPCYJ2AVhlQE4MKL7o2QiPdAXbMdWRiLeykQ2rx+7TNrnoGzvzhO+eA==
+  dependencies:
+    moment "^2.22.2"
+
+systeminformation@5.9.9:
+  version "5.9.9"
+  resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.9.9.tgz#aa8234a138363bd988f438fed3273370f79d7e30"
+  integrity sha512-xciy6NKCLfs4dqMD1Tdlo7v1/g0NfdA1EKsIptUQjlcVvpwHyjifAbNOF7ppFezGSMXxYE8me+l2+RlFF4lyTg==
+
+tapable@^2.1.1, tapable@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
+  integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
+
+tar-fs@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2"
+  integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==
+  dependencies:
+    chownr "^1.1.1"
+    mkdirp-classic "^0.5.2"
+    pump "^3.0.0"
+    tar-stream "^2.0.0"
+
+tar-fs@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
+  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
+  dependencies:
+    chownr "^1.1.1"
+    mkdirp-classic "^0.5.2"
+    pump "^3.0.0"
+    tar-stream "^2.1.4"
+
+tar-stream@^2.0.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325"
+  integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==
+  dependencies:
+    bl "^4.0.1"
+    end-of-stream "^1.4.1"
+    fs-constants "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^3.1.1"
+
+tar-stream@^2.1.4:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+  dependencies:
+    bl "^4.0.3"
+    end-of-stream "^1.4.1"
+    fs-constants "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^3.1.1"
+
+tar@^6.0.2:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
+  integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
+  dependencies:
+    chownr "^2.0.0"
+    fs-minipass "^2.0.0"
+    minipass "^3.0.0"
+    minizlib "^2.1.1"
+    mkdirp "^1.0.3"
+    yallist "^4.0.0"
+
+tar@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
+  integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
+  dependencies:
+    chownr "^2.0.0"
+    fs-minipass "^2.0.0"
+    minipass "^3.0.0"
+    minizlib "^2.1.1"
+    mkdirp "^1.0.3"
+    yallist "^4.0.0"
+
+terser-webpack-plugin@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz#7effadee06f7ecfa093dbbd3e9ab23f5f3ed8673"
+  integrity sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q==
+  dependencies:
+    jest-worker "^26.6.2"
+    p-limit "^3.1.0"
+    schema-utils "^3.0.0"
+    serialize-javascript "^5.0.1"
+    source-map "^0.6.1"
+    terser "^5.5.1"
+
+terser@^5.5.1:
+  version "5.5.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289"
+  integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.7.2"
+    source-map-support "~0.5.19"
+
+text-table@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+  integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+
+thenify-all@^1.0.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
+  integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
+  dependencies:
+    thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
+  integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
+  dependencies:
+    any-promise "^1.0.0"
+
+throttle-debounce@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
+  integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
+
+through@2:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+  integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
+tinycolor2@1.4.2:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
+  integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
+
+tmp@0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
+  integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
+  dependencies:
+    rimraf "^3.0.0"
+
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+  integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+toidentifier@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
+  integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+
+token-stream@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
+  integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=
+
+token-types@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.1.1.tgz#ef9e8c8e2e0ded9f1b3f8dbaa46a3228b113ba1a"
+  integrity sha512-hD+QyuUAyI2spzsI0B7gf/jJ2ggR4RjkAo37j3StuePhApJUwcWDjnHDOFdIWYSwNR28H14hpwm4EI+V1Ted1w==
+  dependencies:
+    "@tokenizer/token" "^0.3.0"
+    ieee754 "^1.2.1"
+
+tough-cookie@^2.3.3, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tough-cookie@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
+  integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==
+  dependencies:
+    psl "^1.1.33"
+    punycode "^2.1.1"
+    universalify "^0.1.2"
+
+tr46@^2.0.0, tr46@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479"
+  integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==
+  dependencies:
+    punycode "^2.1.1"
+
+trace-redirect@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/trace-redirect/-/trace-redirect-1.0.6.tgz#ac629b5bf8247d30dde5a35fe9811b811075b504"
+  integrity sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==
+
+tree-kill@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
+  integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
+
+ts-jest@^25.2.1:
+  version "25.5.1"
+  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7"
+  integrity sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==
+  dependencies:
+    bs-logger "0.x"
+    buffer-from "1.x"
+    fast-json-stable-stringify "2.x"
+    json5 "2.x"
+    lodash.memoize "4.x"
+    make-error "1.x"
+    micromatch "4.x"
+    mkdirp "0.x"
+    semver "6.x"
+    yargs-parser "18.x"
+
+ts-loader@9.2.6:
+  version "9.2.6"
+  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74"
+  integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==
+  dependencies:
+    chalk "^4.1.0"
+    enhanced-resolve "^5.0.0"
+    micromatch "^4.0.0"
+    semver "^7.3.4"
+
+ts-node@10.4.0:
+  version "10.4.0"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7"
+  integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==
+  dependencies:
+    "@cspotcode/source-map-support" "0.7.0"
+    "@tsconfig/node10" "^1.0.7"
+    "@tsconfig/node12" "^1.0.7"
+    "@tsconfig/node14" "^1.0.0"
+    "@tsconfig/node16" "^1.0.2"
+    acorn "^8.4.1"
+    acorn-walk "^8.1.1"
+    arg "^4.1.0"
+    create-require "^1.1.0"
+    diff "^4.0.1"
+    make-error "^1.1.1"
+    yn "3.1.1"
+
+tsc-alias@1.3.10:
+  version "1.3.10"
+  resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.3.10.tgz#6ccf81c644092387ab9be3a3a75549a95eeffd80"
+  integrity sha512-7SF56qiV7Oh/bON+XjF/uAzEFqbmwCuEIHQyoTyVJAK80WnxaIyhO9TBwD/x8InIMU8lnvExQBOrgKkRPsHH+w==
+  dependencies:
+    "@jfonx/console-utils" "^1.0.3"
+    "@jfonx/file-utils" "^3.0.1"
+    chokidar "^3.5.0"
+    commander "^6.2.1"
+    find-node-modules "^2.1.0"
+    globby "^11.0.2"
+    normalize-path "^3.0.0"
+
+tsconfig-paths@3.11.0:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
+  integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==
+  dependencies:
+    "@types/json5" "^0.0.29"
+    json5 "^1.0.1"
+    minimist "^1.2.0"
+    strip-bom "^3.0.0"
+
+tslib@^1.13.0:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
+  integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
+
+tslib@^1.8.1, tslib@^1.9.0:
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
+  integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
+
+tslib@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
+  integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
+
+tslint-sonarts@1.9.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/tslint-sonarts/-/tslint-sonarts-1.9.0.tgz#feb593e92db328c0328b430b838adbe65d504de9"
+  integrity sha512-CJWt+IiYI8qggb2O/JPkS6CkC5DY1IcqRsm9EHJ+AxoWK70lvtP7jguochyNDMP2vIz/giGdWCfEM39x/I/Vnw==
+  dependencies:
+    immutable "^3.8.2"
+
+tslint@6.1.3:
+  version "6.1.3"
+  resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904"
+  integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    builtin-modules "^1.1.1"
+    chalk "^2.3.0"
+    commander "^2.12.1"
+    diff "^4.0.1"
+    glob "^7.1.1"
+    js-yaml "^3.13.1"
+    minimatch "^3.0.4"
+    mkdirp "^0.5.3"
+    resolve "^1.3.2"
+    semver "^5.3.0"
+    tslib "^1.13.0"
+    tsutils "^2.29.0"
+
+tsscmp@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
+  integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
+
+tsutils@^2.29.0:
+  version "2.29.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
+  integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==
+  dependencies:
+    tslib "^1.8.1"
+
+tsutils@^3.21.0:
+  version "3.21.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
+  dependencies:
+    tslib "^1.8.1"
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+  dependencies:
+    safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+twemoji-parser@13.1.0, twemoji-parser@13.1.x:
+  version "13.1.0"
+  resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4"
+  integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg==
+
+type-check@^0.4.0, type-check@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+  dependencies:
+    prelude-ls "^1.2.1"
+
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+  dependencies:
+    prelude-ls "~1.1.2"
+
+type-detect@4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+  integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
+type-fest@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+type-is@^1.6.14, type-is@^1.6.16, type-is@^1.6.4:
+  version "1.6.18"
+  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+  dependencies:
+    media-typer "0.3.0"
+    mime-types "~2.1.24"
+
+type-of@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/type-of/-/type-of-2.0.1.tgz#e72a1741896568e9f628378d816d6912f7f23972"
+  integrity sha1-5yoXQYllaOn2KDeNgW1pEvfyOXI=
+
+type@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
+  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
+
+type@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
+  integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
+
+typedarray-to-buffer@^3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+  integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+  dependencies:
+    is-typedarray "^1.0.0"
+
+typedarray@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+typeorm@0.2.39:
+  version "0.2.39"
+  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.39.tgz#4d22fc68d114b2ca88a8d7b064f31af15e836ade"
+  integrity sha512-yQdvDpmmmn8wp1We25V76KIBPYR/lDbymNbGC++Uq8mSRhpHIPnlg26VAT4CF6Ypqx72zn8eqr+/72uSo7HdJQ==
+  dependencies:
+    "@sqltools/formatter" "^1.2.2"
+    app-root-path "^3.0.0"
+    buffer "^6.0.3"
+    chalk "^4.1.0"
+    cli-highlight "^2.1.11"
+    debug "^4.3.1"
+    dotenv "^8.2.0"
+    glob "^7.1.6"
+    js-yaml "^4.0.0"
+    mkdirp "^1.0.4"
+    reflect-metadata "^0.1.13"
+    sha.js "^2.4.11"
+    tslib "^2.1.0"
+    xml2js "^0.4.23"
+    yargs "^17.0.1"
+    zen-observable-ts "^1.0.0"
+
+typescript@4.4.4:
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
+  integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
+
+ulid@2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f"
+  integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==
+
+uniq@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+  integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+
+uniqs@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+  integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
+unique-filename@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+  integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+  dependencies:
+    unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
+  integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
+  dependencies:
+    imurmurhash "^0.1.4"
+
+universalify@^0.1.0, universalify@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+unload@2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/unload/-/unload-2.3.1.tgz#9d16862d372a5ce5cb630ad1309c2fd6e35dacfe"
+  integrity sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA==
+  dependencies:
+    "@babel/runtime" "^7.6.2"
+    detect-node "2.1.0"
+
+unpipe@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+  integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+uri-js@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+  dependencies:
+    punycode "^2.1.0"
+
+url@0.10.3:
+  version "0.10.3"
+  resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
+  integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
+  dependencies:
+    punycode "1.3.2"
+    querystring "0.2.0"
+
+urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6"
+  integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY=
+
+utf-8-validate@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.2.tgz#63cfbccd85dc1f2b66cf7a1d0eebc08ed056bfb3"
+  integrity sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==
+  dependencies:
+    node-gyp-build "~3.7.0"
+
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
+  integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.2"
+    has-symbols "^1.0.1"
+    object.getownpropertydescriptors "^2.1.0"
+
+uuid@3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+  integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+
+uuid@7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
+  integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
+
+uuid@8.3.2, uuid@^8.3.0:
+  version "8.3.2"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+  integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+uuid@^3.3.2:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
+v8-compile-cache@^2.0.3:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
+  integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
+
+valid-url@^1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
+  integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=
+
+vary@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+  integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+
+vendors@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
+  integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+
+verror@1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+  integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+  dependencies:
+    assert-plus "^1.0.0"
+    core-util-is "1.0.2"
+    extsprintf "^1.2.0"
+
+void-elements@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
+  integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
+
+vue-eslint-parser@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-8.0.1.tgz#25e08b20a414551531f3e19f999902e1ecf45f13"
+  integrity sha512-lhWjDXJhe3UZw2uu3ztX51SJAPGPey1Tff2RK3TyZURwbuI4vximQLzz4nQfCv8CZq4xx7uIiogHMMoSJPr33A==
+  dependencies:
+    debug "^4.3.2"
+    eslint-scope "^6.0.0"
+    eslint-visitor-keys "^3.0.0"
+    espree "^9.0.0"
+    esquery "^1.4.0"
+    lodash "^4.17.21"
+    semver "^7.3.5"
+
+w3c-hr-time@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
+  integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+  dependencies:
+    browser-process-hrtime "^1.0.0"
+
+w3c-xmlserializer@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"
+  integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==
+  dependencies:
+    xml-name-validator "^3.0.0"
+
+watchpack@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.0.0.tgz#b12248f32f0fd4799b7be0802ad1f6573a45955c"
+  integrity sha512-xSdCxxYZWNk3VK13bZRYhsQpfa8Vg63zXG+3pyU8ouqSLRCv4IGXIp9Kr226q6GBkGRlZrST2wwKtjfKz2m7Cg==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"
+
+web-push@3.4.5:
+  version "3.4.5"
+  resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.5.tgz#f94074ff150538872c7183e4d8881c8305920cf1"
+  integrity sha512-2njbTqZ6Q7ZqqK14YpK1GGmaZs3NmuGYF5b7abCXulUIWFSlSYcZ3NBJQRFcMiQDceD7vQknb8FUuvI1F7Qe/g==
+  dependencies:
+    asn1.js "^5.3.0"
+    http_ece "1.1.0"
+    https-proxy-agent "^5.0.0"
+    jws "^4.0.0"
+    minimist "^1.2.5"
+    urlsafe-base64 "^1.0.0"
+
+webidl-conversions@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
+  integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
+
+webidl-conversions@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
+  integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
+
+webpack-sources@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac"
+  integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==
+  dependencies:
+    source-list-map "^2.0.1"
+    source-map "^0.6.1"
+
+webpack@^5:
+  version "5.33.2"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.33.2.tgz#c049717c9b038febf5a72fd2f53319ad59a8c1fc"
+  integrity sha512-X4b7F1sYBmJx8mlh2B7mV5szEkE0jYNJ2y3akgAP0ERi0vLCG1VvdsIxt8lFd4st6SUy0lf7W0CCQS566MBpJg==
+  dependencies:
+    "@types/eslint-scope" "^3.7.0"
+    "@types/estree" "^0.0.46"
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/wasm-edit" "1.11.0"
+    "@webassemblyjs/wasm-parser" "1.11.0"
+    acorn "^8.0.4"
+    browserslist "^4.14.5"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^5.7.0"
+    es-module-lexer "^0.4.0"
+    eslint-scope "^5.1.1"
+    events "^3.2.0"
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.2.4"
+    json-parse-better-errors "^1.0.2"
+    loader-runner "^4.2.0"
+    mime-types "^2.1.27"
+    neo-async "^2.6.2"
+    schema-utils "^3.0.0"
+    tapable "^2.1.1"
+    terser-webpack-plugin "^5.1.1"
+    watchpack "^2.0.0"
+    webpack-sources "^2.1.1"
+
+websocket@1.0.34:
+  version "1.0.34"
+  resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111"
+  integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==
+  dependencies:
+    bufferutil "^4.0.1"
+    debug "^2.2.0"
+    es5-ext "^0.10.50"
+    typedarray-to-buffer "^3.1.5"
+    utf-8-validate "^5.0.2"
+    yaeti "^0.0.6"
+
+whatwg-encoding@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+  integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+  dependencies:
+    iconv-lite "0.4.24"
+
+whatwg-mimetype@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+  integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.0.0.tgz#37f256cb746398e19b107bd6ef820b4ae2d15871"
+  integrity sha512-41ou2Dugpij8/LPO5Pq64K5q++MnRCBpEHvQr26/mArEKTkCV5aoXIqyhuYtE0pkqScXwhf2JP57rkRTYM29lQ==
+  dependencies:
+    lodash.sortby "^4.7.0"
+    tr46 "^2.0.0"
+    webidl-conversions "^5.0.0"
+
+whatwg-url@^8.5.0:
+  version "8.5.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.5.0.tgz#7752b8464fc0903fec89aa9846fc9efe07351fd3"
+  integrity sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==
+  dependencies:
+    lodash "^4.7.0"
+    tr46 "^2.0.2"
+    webidl-conversions "^6.1.0"
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@2.0.2, which@^2.0.1, which@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+which@^1.1.1, which@^1.2.14:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+wide-align@1.1.3, wide-align@^1.1.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+  integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+  dependencies:
+    string-width "^1.0.2 || 2"
+
+with@^7.0.0:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac"
+  integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==
+  dependencies:
+    "@babel/parser" "^7.9.6"
+    "@babel/types" "^7.9.6"
+    assert-never "^1.2.1"
+    babel-walk "3.0.0-canary-5"
+
+word-wrap@^1.2.3, word-wrap@~1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+workerpool@6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b"
+  integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==
+
+wrap-ansi@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+  dependencies:
+    ansi-styles "^3.2.0"
+    string-width "^3.0.0"
+    strip-ansi "^5.0.0"
+
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+ws@8.2.3:
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
+ws@^7.4.6:
+  version "7.5.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
+  integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
+
+xev@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/xev/-/xev-2.0.1.tgz#24484173a22115bc8a990ef5d4d5129695b827a7"
+  integrity sha512-icDf9M67bDge0F2qf02WKZq+s7mMO/SbPv67ZQPym6JThLEOdlWWLdB7VTVgRJp3ekgaiVItCAyH6aoKCPvfIA==
+
+xml-js@^1.6.11:
+  version "1.6.11"
+  resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
+  integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
+  dependencies:
+    sax "^1.2.4"
+
+xml-name-validator@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+  integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+xml2js@0.4.19:
+  version "0.4.19"
+  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
+  integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
+  dependencies:
+    sax ">=0.6.0"
+    xmlbuilder "~9.0.1"
+
+xml2js@^0.4.23:
+  version "0.4.23"
+  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
+  integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
+  dependencies:
+    sax ">=0.6.0"
+    xmlbuilder "~11.0.0"
+
+xmlbuilder@~11.0.0:
+  version "11.0.1"
+  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
+  integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
+
+xmlbuilder@~9.0.1:
+  version "9.0.7"
+  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
+  integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
+
+xmlchars@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+  integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
+xtend@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y18n@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
+  integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
+
+y18n@^5.0.5:
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
+  integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
+
+yaeti@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
+  integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=
+
+yallist@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+  integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yaml-ast-parser@0.0.43:
+  version "0.0.43"
+  resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb"
+  integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
+
+yaml@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yargs-parser@18.x:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-parser@20.2.4, yargs-parser@^20.2.2:
+  version "20.2.4"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
+  integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
+
+yargs-parser@^13.1.2:
+  version "13.1.2"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
+  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-unparser@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
+  integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
+  dependencies:
+    camelcase "^6.0.0"
+    decamelize "^4.0.0"
+    flat "^5.0.2"
+    is-plain-obj "^2.1.0"
+
+yargs@16.2.0, yargs@^16.0.0, yargs@^16.2.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
+yargs@^13.2.4:
+  version "13.3.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
+  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+  dependencies:
+    cliui "^5.0.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^3.0.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^13.1.2"
+
+yargs@^17.0.1:
+  version "17.1.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.1.tgz#c2a8091564bdb196f7c0a67c1d12e5b85b8067ba"
+  integrity sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
+ylru@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
+  integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==
+
+yn@3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+  integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zen-observable-ts@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.0.0.tgz#30d1202b81d8ba4c489e3781e8ca09abf0075e70"
+  integrity sha512-KmWcbz+9kKUeAQ8btY8m1SsEFgBcp7h/Uf3V5quhan7ZWdjGsf0JcGLULQiwOZibbFWnHkYq8Nn2AZbJabovQg==
+  dependencies:
+    "@types/zen-observable" "^0.8.2"
+    zen-observable "^0.8.15"
+
+zen-observable@^0.8.15:
+  version "0.8.15"
+  resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
+  integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
diff --git a/packages/client/.eslintrc b/packages/client/.eslintrc
new file mode 100644
index 0000000000..b3b631e1de
--- /dev/null
+++ b/packages/client/.eslintrc
@@ -0,0 +1,32 @@
+{
+	"env": {
+		"node": false
+	},
+	"extends": [
+		"eslint:recommended",
+		"plugin:vue/recommended"
+	],
+	"rules": {
+		"vue/require-v-for-key": 0,
+		"vue/max-attributes-per-line": 0,
+		"vue/html-indent": 0,
+		"vue/html-self-closing": 0,
+		"vue/no-unused-vars": 0,
+		"vue/attributes-order": 0,
+		"vue/require-prop-types": 0,
+		"vue/require-default-prop": 0,
+		"vue/html-closing-bracket-spacing": 0,
+		"vue/singleline-html-element-content-newline": 0,
+		"vue/no-v-html": 0
+	},
+	"globals": {
+		"_DEV_": false,
+		"_LANGS_": false,
+		"_VERSION_": false,
+		"_ENV_": false,
+		"_PERF_PREFIX_": false,
+		"_DATA_TRANSFER_DRIVE_FILE_": false,
+		"_DATA_TRANSFER_DRIVE_FOLDER_": false,
+		"_DATA_TRANSFER_DECK_COLUMN_": false
+	}
+}
diff --git a/packages/client/.npmrc b/packages/client/.npmrc
new file mode 100644
index 0000000000..6b5f38e890
--- /dev/null
+++ b/packages/client/.npmrc
@@ -0,0 +1,2 @@
+save-exact = true
+package-lock = false
diff --git a/packages/client/@types/global.d.ts b/packages/client/@types/global.d.ts
new file mode 100644
index 0000000000..c757482900
--- /dev/null
+++ b/packages/client/@types/global.d.ts
@@ -0,0 +1,10 @@
+type FIXME = any;
+
+declare const _LANGS_: string[][];
+declare const _VERSION_: string;
+declare const _ENV_: string;
+declare const _DEV_: boolean;
+declare const _PERF_PREFIX_: string;
+declare const _DATA_TRANSFER_DRIVE_FILE_: string;
+declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
+declare const _DATA_TRANSFER_DECK_COLUMN_: string;
diff --git a/packages/client/@types/vue.d.ts b/packages/client/@types/vue.d.ts
new file mode 100644
index 0000000000..8cb6130629
--- /dev/null
+++ b/packages/client/@types/vue.d.ts
@@ -0,0 +1,5 @@
+declare module '*.vue' {
+	import type { DefineComponent } from 'vue';
+	const component: DefineComponent<{}, {}, any>;
+	export default component;
+}
diff --git a/packages/client/assets/about-icon.png b/packages/client/assets/about-icon.png
new file mode 100644
index 0000000000..afc1f0c728
Binary files /dev/null and b/packages/client/assets/about-icon.png differ
diff --git a/packages/client/assets/fedi.jpg b/packages/client/assets/fedi.jpg
new file mode 100644
index 0000000000..cbf3748eb8
Binary files /dev/null and b/packages/client/assets/fedi.jpg differ
diff --git a/packages/client/assets/label-red.svg b/packages/client/assets/label-red.svg
new file mode 100644
index 0000000000..45996aa9ce
--- /dev/null
+++ b/packages/client/assets/label-red.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
+	 y="0px" width="96px" height="96px" viewBox="0 0 96 96" enable-background="new 0 0 96 96" xml:space="preserve">
+<polygon fill="#ea2412" points="0,45.255 45.254,0 84.854,0 0,84.854 "/>
+</svg>
diff --git a/packages/client/assets/label.svg b/packages/client/assets/label.svg
new file mode 100644
index 0000000000..b1f85f3c07
--- /dev/null
+++ b/packages/client/assets/label.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
+	 y="0px" width="96px" height="96px" viewBox="0 0 96 96" enable-background="new 0 0 96 96" xml:space="preserve">
+<polygon fill="#0B8AEA" points="0,45.255 45.254,0 84.854,0 0,84.854 "/>
+</svg>
diff --git a/packages/client/assets/misskey.svg b/packages/client/assets/misskey.svg
new file mode 100644
index 0000000000..3fcb2d3ecb
--- /dev/null
+++ b/packages/client/assets/misskey.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="515px" height="136px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g id="path2" transform="matrix(0.264585,0,0,0.264585,-50.0005,-50.0001)">
+        <path d="M256.418,188.976C248.558,188.944 240.758,190.308 233.379,193.013C220.308,197.613 209.533,205.888 201.091,217.802C193.02,229.329 188.977,242.195 188.977,256.409L188.977,508.89C188.977,527.332 195.52,543.29 208.576,556.732C222.032,569.803 237.99,576.331 256.418,576.331C275.259,576.331 291.204,569.803 304.274,556.747C317.73,543.291 324.441,527.332 324.441,508.89L324.441,462.983C324.584,453.04 334.824,455.655 340.01,462.983C349.695,479.767 370.251,494.19 394.193,494.105L394.193,494.119C418.119,494.019 438.005,482.196 448.375,462.983C452.304,458.354 463.377,450.455 464.52,462.983L464.52,508.89C464.52,527.332 471.047,543.29 484.104,556.732C497.574,569.803 513.511,576.331 531.953,576.331C550.78,576.331 566.739,569.803 579.809,556.747C593.265,543.291 599.977,527.332 599.977,508.89L599.977,256.409C599.977,242.195 595.752,229.329 587.309,217.802C579.224,205.874 568.653,197.613 555.597,193.013C547.912,190.314 540.228,188.976 532.543,188.976C511.788,188.976 494.301,197.046 480.073,213.188L411.636,293.281C410.107,294.438 405.006,303.247 394.178,303.247C383.379,303.247 378.868,294.439 377.325,293.296L308.297,213.188C294.47,197.046 277.173,188.976 256.418,188.976ZM682.904,188.983C666.763,188.983 652.926,194.748 641.404,206.271C630.261,217.413 624.691,231.054 624.691,247.196C624.691,263.338 630.261,277.174 641.404,288.697C652.926,299.839 666.763,305.41 682.904,305.41C699.046,305.41 712.88,299.839 724.412,288.697C735.935,277.174 741.693,263.338 741.693,247.196C741.693,231.054 735.935,217.413 724.412,206.271C712.88,194.748 699.046,188.983 682.904,188.983ZM1327.58,193.013C1311.45,193.013 1297.62,198.779 1286.1,210.302C1274.96,221.444 1269.38,235.08 1269.38,251.212L1269.38,519.72C1269.38,535.861 1274.96,549.697 1286.1,561.22C1297.62,572.362 1311.45,577.933 1327.58,577.933C1343.73,577.933 1357.55,572.362 1369.08,561.22C1380.6,549.697 1386.37,535.867 1386.37,519.734C1386.37,508.211 1387.9,502.453 1390.96,502.453C1392.51,502.453 1394.05,503.023 1395.58,504.166L1453.2,560.061C1464.72,571.203 1478.36,576.774 1494.11,576.774C1510.62,576.774 1524.45,571.009 1535.6,559.486C1546.74,547.572 1552.31,533.936 1552.31,518.575C1552.31,502.053 1546.36,488.029 1534.45,476.506C1508.32,450.765 1494.69,437.517 1493.54,436.755C1488.54,431.755 1488.73,427.53 1494.11,424.073L1495.27,423.497L1495.27,422.929L1531.57,399.875C1548.85,388.352 1557.5,372.026 1557.5,350.904C1557.5,339.381 1554.42,328.622 1548.28,318.623C1536.76,301.339 1520.43,292.691 1499.3,292.691C1487.78,292.691 1477.02,295.768 1467.04,301.911C1422.48,331.499 1399.42,346.678 1397.88,347.449C1395.2,349.363 1392.7,349.738 1390.4,348.586C1387.7,347.434 1386.35,344.939 1386.35,341.101L1386.35,251.212C1386.35,235.08 1380.59,221.444 1369.07,210.302C1357.55,198.779 1343.72,193.013 1327.58,193.013ZM1716.37,291.738C1676.42,291.738 1642.24,305.949 1613.81,334.376C1585.76,362.422 1571.74,396.227 1571.74,435.795C1571.74,475.745 1585.76,509.932 1613.81,538.359C1642.24,566.404 1676.42,580.428 1716.37,580.428C1755.94,580.428 1789.94,566.404 1818.37,538.359C1827.2,529.521 1831.62,518.773 1831.62,506.107C1831.62,493.423 1827.2,482.664 1818.37,473.827C1809.53,464.999 1798.77,460.584 1786.11,460.584C1773.42,460.584 1762.66,464.999 1753.83,473.827C1743.46,484.588 1730.97,489.963 1716.37,489.963C1701.4,489.963 1688.53,484.78 1677.77,474.41C1667.39,463.649 1662.2,450.775 1662.2,435.795C1662.2,421.206 1667.59,408.72 1678.35,398.34C1683.73,392.578 1690.26,388.74 1697.93,386.817C1699.87,386.436 1701.4,386.623 1702.55,387.385C1703.32,388.547 1702.93,389.702 1701.39,390.854L1689.87,402.953C1681.03,411.791 1676.61,422.359 1676.61,434.644C1676.61,447.319 1680.45,457.497 1688.13,465.182C1695.81,472.868 1706.57,476.705 1720.41,476.705C1730.01,476.705 1739.61,471.91 1749.21,462.311L1816.06,396.044C1824.9,387.197 1829.32,376.436 1829.32,363.77C1829.32,351.086 1824.9,340.332 1816.06,331.504C1789.17,304.992 1755.94,291.738 1716.37,291.738ZM877.977,292.668C841.947,292.194 813.839,301.679 793.662,321.133C775.996,338.036 767.168,359.358 767.168,385.089C767.549,417.363 780.035,441.565 804.624,457.697C811.918,462.687 820.941,466.72 831.693,469.796C837.083,471.72 846.111,474.02 858.777,476.705C869.919,479.391 882.023,481.886 895.088,484.191C897.774,484.962 898.924,486.312 898.543,488.236C898.543,490.541 897.58,491.691 895.657,491.691C890.667,492.072 886.059,492.266 881.831,492.266C850.328,488.81 829.001,485.927 817.859,483.622C814.793,482.851 811.535,482.463 808.078,482.463C796.165,482.463 785.787,486.884 776.949,495.721C768.502,504.178 764.282,514.551 764.282,526.836C764.282,536.825 767.352,545.854 773.494,553.92C780.027,561.986 788.486,567.169 798.866,569.473C831.13,576.778 860.317,580.428 886.429,580.428C922.16,580.428 950.013,570.825 969.992,551.617C987.277,535.094 995.925,513.775 995.925,487.653C995.925,455.388 983.626,431.187 959.037,415.045C945.972,406.598 927.915,400.45 904.869,396.612C891.042,393.927 879.518,391.427 870.3,389.112L870.3,389.127C867.605,388.356 866.067,386.818 865.686,384.513C865.686,382.59 867.224,381.44 870.3,381.059C873.757,380.678 877.415,380.678 881.262,381.059C913.146,384.135 934.652,386.823 945.794,389.127C948.861,389.889 951.931,390.271 955.007,390.271C967.301,390.271 977.674,386.051 986.121,377.604C994.959,368.767 999.379,358.393 999.379,346.49C999.379,336.109 996.109,326.894 989.576,318.837C983.043,310.761 974.79,305.566 964.81,303.261C938.298,297.5 911.788,294.042 885.285,292.89C882.813,292.77 880.379,292.7 877.977,292.668ZM1128.73,292.668C1092.7,292.194 1064.59,301.679 1044.42,321.133C1026.75,338.036 1017.92,359.358 1017.92,385.089C1018.3,417.363 1030.79,441.565 1055.38,457.697C1062.67,462.687 1071.7,466.72 1082.46,469.796C1087.84,471.72 1096.86,474.02 1109.54,476.705C1120.68,479.391 1132.79,481.886 1145.84,484.191C1148.53,484.962 1149.68,486.312 1149.3,488.236C1149.3,490.541 1148.34,491.691 1146.41,491.691C1141.42,492.072 1136.81,492.266 1132.59,492.266C1101.09,488.81 1079.77,485.927 1068.63,483.622C1065.55,482.851 1062.29,482.463 1058.83,482.463C1046.92,482.463 1036.55,486.884 1027.72,495.721C1019.26,504.178 1015.04,514.551 1015.04,526.836C1015.04,536.825 1018.11,545.854 1024.26,553.92C1030.79,561.986 1039.24,567.169 1049.62,569.473C1081.88,576.778 1111.08,580.428 1137.2,580.428C1172.92,580.428 1200.77,570.825 1220.75,551.617C1238.03,535.094 1246.68,513.775 1246.68,487.653C1246.68,455.388 1234.39,431.187 1209.81,415.045C1196.74,406.598 1178.68,400.45 1155.64,396.612C1141.81,393.927 1130.29,391.427 1121.07,389.112L1121.05,389.127C1118.37,388.356 1116.84,386.818 1116.45,384.513C1116.45,382.59 1117.99,381.44 1121.05,381.059C1124.52,380.678 1128.17,380.678 1132.01,381.059C1163.89,384.135 1185.41,386.823 1196.55,389.127C1199.62,389.889 1202.69,390.271 1205.76,390.271C1218.06,390.271 1228.43,386.051 1236.89,377.604C1245.72,368.767 1250.13,358.393 1250.13,346.49C1250.13,336.109 1246.87,326.894 1240.35,318.837C1233.81,310.761 1225.55,305.566 1215.56,303.261C1189.06,297.5 1162.55,294.042 1136.04,292.89C1133.57,292.77 1131.13,292.7 1128.73,292.668ZM1910.17,296.736C1894.04,296.736 1880.21,302.501 1868.69,314.024C1857.55,325.157 1851.98,338.793 1851.98,354.934L1851.98,435.028C1851.98,473.825 1865.8,507.05 1893.45,534.705C1921.12,562.36 1954.36,576.191 1993.15,576.191C2000.84,576.191 2007.95,575.614 2014.48,574.471C2018.32,573.699 2021,574.469 2022.53,576.774C2023.69,578.307 2023.3,580.42 2021.39,583.115C2016.39,590.029 2005.82,593.486 1989.69,593.486C1983.55,593.486 1975.68,591.949 1966.07,588.873C1956.47,585.797 1948.98,584.259 1943.6,584.259C1920.93,584.259 1904.99,594.638 1895.77,615.388C1892.32,622.302 1890.58,629.598 1890.58,637.283C1890.58,659.948 1900.77,675.892 1921.13,685.11C1941.49,694.709 1964.34,699.505 1989.69,699.505C2033.49,699.505 2068.25,686.639 2093.98,660.898C2120.11,635.166 2133.18,600.784 2133.18,557.758L2133.18,452.308C2133.94,446.157 2134.32,440.399 2134.32,435.028L2134.32,354.934C2134.32,338.802 2128.57,325.166 2117.04,314.024C2105.9,302.501 2092.27,296.736 2076.13,296.736C2059.99,296.736 2046.16,302.501 2034.63,314.024C2023.11,325.157 2017.35,338.793 2017.35,354.934L2017.35,435.028C2017.35,441.551 2015.04,447.309 2010.43,452.308C2005.83,456.918 2000.07,459.225 1993.15,459.225C1986.62,459.225 1980.86,456.918 1975.87,452.308C1971.26,447.309 1968.95,441.551 1968.95,435.028L1968.95,354.934C1968.95,338.802 1963.19,325.166 1951.67,314.024C1940.14,302.501 1926.3,296.736 1910.17,296.736ZM683.473,316.947C667.331,316.947 653.495,322.713 641.972,334.236C630.449,345.768 624.691,359.602 624.691,375.744L624.691,518.118C624.691,534.259 630.449,548.095 641.972,559.618C653.504,570.761 667.341,576.331 683.473,576.331C699.624,576.331 713.27,570.761 724.412,559.618C735.935,548.095 741.693,534.259 741.693,518.118L741.693,375.744C741.693,359.593 735.935,345.759 724.412,334.236C713.261,322.713 699.614,316.947 683.473,316.947Z" style="fill:white;fill-rule:nonzero;"/>
+    </g>
+</svg>
diff --git a/packages/client/assets/remove.png b/packages/client/assets/remove.png
new file mode 100644
index 0000000000..c2e222a0fc
Binary files /dev/null and b/packages/client/assets/remove.png differ
diff --git a/packages/client/assets/room/furnitures/banknote/banknote.blend b/packages/client/assets/room/furnitures/banknote/banknote.blend
new file mode 100644
index 0000000000..60b1968a29
Binary files /dev/null and b/packages/client/assets/room/furnitures/banknote/banknote.blend differ
diff --git a/packages/client/assets/room/furnitures/banknote/banknote.glb b/packages/client/assets/room/furnitures/banknote/banknote.glb
new file mode 100644
index 0000000000..f4ef0b91e7
Binary files /dev/null and b/packages/client/assets/room/furnitures/banknote/banknote.glb differ
diff --git a/packages/client/assets/room/furnitures/banknote/tex.png b/packages/client/assets/room/furnitures/banknote/tex.png
new file mode 100644
index 0000000000..9106dc1457
Binary files /dev/null and b/packages/client/assets/room/furnitures/banknote/tex.png differ
diff --git a/packages/client/assets/room/furnitures/bed/bed.blend b/packages/client/assets/room/furnitures/bed/bed.blend
new file mode 100644
index 0000000000..731df76d0c
Binary files /dev/null and b/packages/client/assets/room/furnitures/bed/bed.blend differ
diff --git a/packages/client/assets/room/furnitures/bed/bed.glb b/packages/client/assets/room/furnitures/bed/bed.glb
new file mode 100644
index 0000000000..f35ecb9ef4
Binary files /dev/null and b/packages/client/assets/room/furnitures/bed/bed.glb differ
diff --git a/packages/client/assets/room/furnitures/bin/bin.blend b/packages/client/assets/room/furnitures/bin/bin.blend
new file mode 100644
index 0000000000..8d459a0869
Binary files /dev/null and b/packages/client/assets/room/furnitures/bin/bin.blend differ
diff --git a/packages/client/assets/room/furnitures/bin/bin.glb b/packages/client/assets/room/furnitures/bin/bin.glb
new file mode 100644
index 0000000000..b45f203802
Binary files /dev/null and b/packages/client/assets/room/furnitures/bin/bin.glb differ
diff --git a/packages/client/assets/room/furnitures/book/book.blend b/packages/client/assets/room/furnitures/book/book.blend
new file mode 100644
index 0000000000..0d4899d4ae
Binary files /dev/null and b/packages/client/assets/room/furnitures/book/book.blend differ
diff --git a/packages/client/assets/room/furnitures/book/book.glb b/packages/client/assets/room/furnitures/book/book.glb
new file mode 100644
index 0000000000..546893da06
Binary files /dev/null and b/packages/client/assets/room/furnitures/book/book.glb differ
diff --git a/packages/client/assets/room/furnitures/book2/barcode.png b/packages/client/assets/room/furnitures/book2/barcode.png
new file mode 100644
index 0000000000..37cfe5add3
Binary files /dev/null and b/packages/client/assets/room/furnitures/book2/barcode.png differ
diff --git a/packages/client/assets/room/furnitures/book2/book2.blend b/packages/client/assets/room/furnitures/book2/book2.blend
new file mode 100644
index 0000000000..e0fdb48101
Binary files /dev/null and b/packages/client/assets/room/furnitures/book2/book2.blend differ
diff --git a/packages/client/assets/room/furnitures/book2/book2.glb b/packages/client/assets/room/furnitures/book2/book2.glb
new file mode 100644
index 0000000000..2b26402f8c
Binary files /dev/null and b/packages/client/assets/room/furnitures/book2/book2.glb differ
diff --git a/packages/client/assets/room/furnitures/book2/texture.afdesign b/packages/client/assets/room/furnitures/book2/texture.afdesign
new file mode 100644
index 0000000000..b63771607a
Binary files /dev/null and b/packages/client/assets/room/furnitures/book2/texture.afdesign differ
diff --git a/packages/client/assets/room/furnitures/book2/texture.png b/packages/client/assets/room/furnitures/book2/texture.png
new file mode 100644
index 0000000000..5aa84f0340
Binary files /dev/null and b/packages/client/assets/room/furnitures/book2/texture.png differ
diff --git a/packages/client/assets/room/furnitures/book2/uv.png b/packages/client/assets/room/furnitures/book2/uv.png
new file mode 100644
index 0000000000..61c4fb0400
Binary files /dev/null and b/packages/client/assets/room/furnitures/book2/uv.png differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box/cardboard-box.blend b/packages/client/assets/room/furnitures/cardboard-box/cardboard-box.blend
new file mode 100644
index 0000000000..3a528de32a
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box/cardboard-box.blend differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box/cardboard-box.glb b/packages/client/assets/room/furnitures/cardboard-box/cardboard-box.glb
new file mode 100644
index 0000000000..bed372e94f
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box/cardboard-box.glb differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend b/packages/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend
new file mode 100644
index 0000000000..5f146267ac
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box2/cardboard-box2.blend differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb b/packages/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb
new file mode 100644
index 0000000000..85fcb5c0b6
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box2/cardboard-box2.glb differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box2/texture.png b/packages/client/assets/room/furnitures/cardboard-box2/texture.png
new file mode 100644
index 0000000000..e498d8f65b
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box2/texture.png differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box2/uv.png b/packages/client/assets/room/furnitures/cardboard-box2/uv.png
new file mode 100644
index 0000000000..d547843ee0
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box2/uv.png differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend b/packages/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend
new file mode 100644
index 0000000000..00681a3cfd
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box3/cardboard-box3.blend differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb b/packages/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb
new file mode 100644
index 0000000000..1ef0427689
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box3/cardboard-box3.glb differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box3/texture.png b/packages/client/assets/room/furnitures/cardboard-box3/texture.png
new file mode 100644
index 0000000000..56c914cb9d
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box3/texture.png differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box3/texture.xcf b/packages/client/assets/room/furnitures/cardboard-box3/texture.xcf
new file mode 100644
index 0000000000..7ffb3e3439
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box3/texture.xcf differ
diff --git a/packages/client/assets/room/furnitures/cardboard-box3/uv.png b/packages/client/assets/room/furnitures/cardboard-box3/uv.png
new file mode 100644
index 0000000000..797ac509db
Binary files /dev/null and b/packages/client/assets/room/furnitures/cardboard-box3/uv.png differ
diff --git a/packages/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend b/packages/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend
new file mode 100644
index 0000000000..750343d4f0
Binary files /dev/null and b/packages/client/assets/room/furnitures/carpet-stripe/carpet-stripe.blend differ
diff --git a/packages/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb b/packages/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb
new file mode 100644
index 0000000000..3066a69e35
Binary files /dev/null and b/packages/client/assets/room/furnitures/carpet-stripe/carpet-stripe.glb differ
diff --git a/packages/client/assets/room/furnitures/chair/chair.blend b/packages/client/assets/room/furnitures/chair/chair.blend
new file mode 100644
index 0000000000..79c29a8401
Binary files /dev/null and b/packages/client/assets/room/furnitures/chair/chair.blend differ
diff --git a/packages/client/assets/room/furnitures/chair/chair.glb b/packages/client/assets/room/furnitures/chair/chair.glb
new file mode 100644
index 0000000000..08ee1a0bb0
Binary files /dev/null and b/packages/client/assets/room/furnitures/chair/chair.glb differ
diff --git a/packages/client/assets/room/furnitures/chair2/chair2.blend b/packages/client/assets/room/furnitures/chair2/chair2.blend
new file mode 100644
index 0000000000..c6a1acd96f
Binary files /dev/null and b/packages/client/assets/room/furnitures/chair2/chair2.blend differ
diff --git a/packages/client/assets/room/furnitures/chair2/chair2.glb b/packages/client/assets/room/furnitures/chair2/chair2.glb
new file mode 100644
index 0000000000..5ea2f3518b
Binary files /dev/null and b/packages/client/assets/room/furnitures/chair2/chair2.glb differ
diff --git a/packages/client/assets/room/furnitures/color-box/color-box.blend b/packages/client/assets/room/furnitures/color-box/color-box.blend
new file mode 100644
index 0000000000..f96a4ff766
Binary files /dev/null and b/packages/client/assets/room/furnitures/color-box/color-box.blend differ
diff --git a/packages/client/assets/room/furnitures/color-box/color-box.glb b/packages/client/assets/room/furnitures/color-box/color-box.glb
new file mode 100644
index 0000000000..43f2abcae8
Binary files /dev/null and b/packages/client/assets/room/furnitures/color-box/color-box.glb differ
diff --git a/packages/client/assets/room/furnitures/corkboard/corkboard.blend b/packages/client/assets/room/furnitures/corkboard/corkboard.blend
new file mode 100644
index 0000000000..9a7e1878cd
Binary files /dev/null and b/packages/client/assets/room/furnitures/corkboard/corkboard.blend differ
diff --git a/packages/client/assets/room/furnitures/corkboard/corkboard.glb b/packages/client/assets/room/furnitures/corkboard/corkboard.glb
new file mode 100644
index 0000000000..fee108fb91
Binary files /dev/null and b/packages/client/assets/room/furnitures/corkboard/corkboard.glb differ
diff --git a/packages/client/assets/room/furnitures/cube/cube.blend b/packages/client/assets/room/furnitures/cube/cube.blend
new file mode 100644
index 0000000000..1af5bf40a9
Binary files /dev/null and b/packages/client/assets/room/furnitures/cube/cube.blend differ
diff --git a/packages/client/assets/room/furnitures/cube/cube.glb b/packages/client/assets/room/furnitures/cube/cube.glb
new file mode 100644
index 0000000000..4ac8b6036d
Binary files /dev/null and b/packages/client/assets/room/furnitures/cube/cube.glb differ
diff --git a/packages/client/assets/room/furnitures/cup-noodle/cup-noodle.blend b/packages/client/assets/room/furnitures/cup-noodle/cup-noodle.blend
new file mode 100644
index 0000000000..37ca8868c7
Binary files /dev/null and b/packages/client/assets/room/furnitures/cup-noodle/cup-noodle.blend differ
diff --git a/packages/client/assets/room/furnitures/cup-noodle/cup-noodle.glb b/packages/client/assets/room/furnitures/cup-noodle/cup-noodle.glb
new file mode 100644
index 0000000000..58efb1b3b4
Binary files /dev/null and b/packages/client/assets/room/furnitures/cup-noodle/cup-noodle.glb differ
diff --git a/packages/client/assets/room/furnitures/cup-noodle/noodle.png b/packages/client/assets/room/furnitures/cup-noodle/noodle.png
new file mode 100644
index 0000000000..1d74e0bbe7
Binary files /dev/null and b/packages/client/assets/room/furnitures/cup-noodle/noodle.png differ
diff --git a/packages/client/assets/room/furnitures/desk/desk.blend b/packages/client/assets/room/furnitures/desk/desk.blend
new file mode 100644
index 0000000000..c88d01f0b2
Binary files /dev/null and b/packages/client/assets/room/furnitures/desk/desk.blend differ
diff --git a/packages/client/assets/room/furnitures/desk/desk.glb b/packages/client/assets/room/furnitures/desk/desk.glb
new file mode 100644
index 0000000000..4a58513095
Binary files /dev/null and b/packages/client/assets/room/furnitures/desk/desk.glb differ
diff --git a/packages/client/assets/room/furnitures/doll-ai/doll-ai.blend b/packages/client/assets/room/furnitures/doll-ai/doll-ai.blend
new file mode 100644
index 0000000000..a912231ecb
Binary files /dev/null and b/packages/client/assets/room/furnitures/doll-ai/doll-ai.blend differ
diff --git a/packages/client/assets/room/furnitures/doll-ai/doll-ai.glb b/packages/client/assets/room/furnitures/doll-ai/doll-ai.glb
new file mode 100644
index 0000000000..ec55a7bd7b
Binary files /dev/null and b/packages/client/assets/room/furnitures/doll-ai/doll-ai.glb differ
diff --git a/packages/client/assets/room/furnitures/doll-ai/doll_ai_tex.png b/packages/client/assets/room/furnitures/doll-ai/doll_ai_tex.png
new file mode 100644
index 0000000000..370ca5f75b
Binary files /dev/null and b/packages/client/assets/room/furnitures/doll-ai/doll_ai_tex.png differ
diff --git a/packages/client/assets/room/furnitures/energy-drink/energy-drink.blend b/packages/client/assets/room/furnitures/energy-drink/energy-drink.blend
new file mode 100644
index 0000000000..65fc41273e
Binary files /dev/null and b/packages/client/assets/room/furnitures/energy-drink/energy-drink.blend differ
diff --git a/packages/client/assets/room/furnitures/energy-drink/energy-drink.glb b/packages/client/assets/room/furnitures/energy-drink/energy-drink.glb
new file mode 100644
index 0000000000..7fb1c27836
Binary files /dev/null and b/packages/client/assets/room/furnitures/energy-drink/energy-drink.glb differ
diff --git a/packages/client/assets/room/furnitures/energy-drink/texture.afdesign b/packages/client/assets/room/furnitures/energy-drink/texture.afdesign
new file mode 100644
index 0000000000..8c117a49b1
Binary files /dev/null and b/packages/client/assets/room/furnitures/energy-drink/texture.afdesign differ
diff --git a/packages/client/assets/room/furnitures/energy-drink/texture.png b/packages/client/assets/room/furnitures/energy-drink/texture.png
new file mode 100644
index 0000000000..484ca0f96f
Binary files /dev/null and b/packages/client/assets/room/furnitures/energy-drink/texture.png differ
diff --git a/packages/client/assets/room/furnitures/energy-drink/uv.png b/packages/client/assets/room/furnitures/energy-drink/uv.png
new file mode 100644
index 0000000000..2a3f20c999
Binary files /dev/null and b/packages/client/assets/room/furnitures/energy-drink/uv.png differ
diff --git a/packages/client/assets/room/furnitures/eraser/cover.png b/packages/client/assets/room/furnitures/eraser/cover.png
new file mode 100644
index 0000000000..932a3fc62e
Binary files /dev/null and b/packages/client/assets/room/furnitures/eraser/cover.png differ
diff --git a/packages/client/assets/room/furnitures/eraser/cover.psd b/packages/client/assets/room/furnitures/eraser/cover.psd
new file mode 100644
index 0000000000..c393337833
Binary files /dev/null and b/packages/client/assets/room/furnitures/eraser/cover.psd differ
diff --git a/packages/client/assets/room/furnitures/eraser/eraser-uv.png b/packages/client/assets/room/furnitures/eraser/eraser-uv.png
new file mode 100644
index 0000000000..89e4ea4c45
Binary files /dev/null and b/packages/client/assets/room/furnitures/eraser/eraser-uv.png differ
diff --git a/packages/client/assets/room/furnitures/eraser/eraser.blend b/packages/client/assets/room/furnitures/eraser/eraser.blend
new file mode 100644
index 0000000000..103c54fbae
Binary files /dev/null and b/packages/client/assets/room/furnitures/eraser/eraser.blend differ
diff --git a/packages/client/assets/room/furnitures/eraser/eraser.glb b/packages/client/assets/room/furnitures/eraser/eraser.glb
new file mode 100644
index 0000000000..016b60df20
Binary files /dev/null and b/packages/client/assets/room/furnitures/eraser/eraser.glb differ
diff --git a/packages/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png
new file mode 100644
index 0000000000..e3865ad15e
Binary files /dev/null and b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue-uv.png differ
diff --git a/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.blend b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.blend
new file mode 100644
index 0000000000..d59f87c1ee
Binary files /dev/null and b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.blend differ
diff --git a/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.glb b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.glb
new file mode 100644
index 0000000000..48b36ef347
Binary files /dev/null and b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.glb differ
diff --git a/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.png b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.png
new file mode 100644
index 0000000000..7cee4b1859
Binary files /dev/null and b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.png differ
diff --git a/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.psd b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.psd
new file mode 100644
index 0000000000..cd59fc007b
Binary files /dev/null and b/packages/client/assets/room/furnitures/facial-tissue/facial-tissue.psd differ
diff --git a/packages/client/assets/room/furnitures/fan/fan.blend b/packages/client/assets/room/furnitures/fan/fan.blend
new file mode 100644
index 0000000000..8c8106e5fe
Binary files /dev/null and b/packages/client/assets/room/furnitures/fan/fan.blend differ
diff --git a/packages/client/assets/room/furnitures/fan/fan.glb b/packages/client/assets/room/furnitures/fan/fan.glb
new file mode 100644
index 0000000000..d9367f3534
Binary files /dev/null and b/packages/client/assets/room/furnitures/fan/fan.glb differ
diff --git a/packages/client/assets/room/furnitures/holo-display/holo-display.blend b/packages/client/assets/room/furnitures/holo-display/holo-display.blend
new file mode 100644
index 0000000000..56d2e1f819
Binary files /dev/null and b/packages/client/assets/room/furnitures/holo-display/holo-display.blend differ
diff --git a/packages/client/assets/room/furnitures/holo-display/holo-display.glb b/packages/client/assets/room/furnitures/holo-display/holo-display.glb
new file mode 100644
index 0000000000..4d042a59b3
Binary files /dev/null and b/packages/client/assets/room/furnitures/holo-display/holo-display.glb differ
diff --git a/packages/client/assets/room/furnitures/holo-display/ray-uv.png b/packages/client/assets/room/furnitures/holo-display/ray-uv.png
new file mode 100644
index 0000000000..aa7e817e0f
Binary files /dev/null and b/packages/client/assets/room/furnitures/holo-display/ray-uv.png differ
diff --git a/packages/client/assets/room/furnitures/holo-display/ray.png b/packages/client/assets/room/furnitures/holo-display/ray.png
new file mode 100644
index 0000000000..6a5d24e143
Binary files /dev/null and b/packages/client/assets/room/furnitures/holo-display/ray.png differ
diff --git a/packages/client/assets/room/furnitures/keyboard/keyboard.blend b/packages/client/assets/room/furnitures/keyboard/keyboard.blend
new file mode 100644
index 0000000000..ab33d134b3
Binary files /dev/null and b/packages/client/assets/room/furnitures/keyboard/keyboard.blend differ
diff --git a/packages/client/assets/room/furnitures/keyboard/keyboard.glb b/packages/client/assets/room/furnitures/keyboard/keyboard.glb
new file mode 100644
index 0000000000..15dc69f47a
Binary files /dev/null and b/packages/client/assets/room/furnitures/keyboard/keyboard.glb differ
diff --git a/packages/client/assets/room/furnitures/low-table/low-table.blend b/packages/client/assets/room/furnitures/low-table/low-table.blend
new file mode 100644
index 0000000000..e1592174d9
Binary files /dev/null and b/packages/client/assets/room/furnitures/low-table/low-table.blend differ
diff --git a/packages/client/assets/room/furnitures/low-table/low-table.glb b/packages/client/assets/room/furnitures/low-table/low-table.glb
new file mode 100644
index 0000000000..c69bf35d7b
Binary files /dev/null and b/packages/client/assets/room/furnitures/low-table/low-table.glb differ
diff --git a/packages/client/assets/room/furnitures/mat/mat.blend b/packages/client/assets/room/furnitures/mat/mat.blend
new file mode 100644
index 0000000000..a1e1a68c55
Binary files /dev/null and b/packages/client/assets/room/furnitures/mat/mat.blend differ
diff --git a/packages/client/assets/room/furnitures/mat/mat.glb b/packages/client/assets/room/furnitures/mat/mat.glb
new file mode 100644
index 0000000000..87ccd44e1a
Binary files /dev/null and b/packages/client/assets/room/furnitures/mat/mat.glb differ
diff --git a/packages/client/assets/room/furnitures/milk/milk-uv.png b/packages/client/assets/room/furnitures/milk/milk-uv.png
new file mode 100644
index 0000000000..258fd54638
Binary files /dev/null and b/packages/client/assets/room/furnitures/milk/milk-uv.png differ
diff --git a/packages/client/assets/room/furnitures/milk/milk.blend b/packages/client/assets/room/furnitures/milk/milk.blend
new file mode 100644
index 0000000000..2df508d5b9
Binary files /dev/null and b/packages/client/assets/room/furnitures/milk/milk.blend differ
diff --git a/packages/client/assets/room/furnitures/milk/milk.glb b/packages/client/assets/room/furnitures/milk/milk.glb
new file mode 100644
index 0000000000..b335fe3d02
Binary files /dev/null and b/packages/client/assets/room/furnitures/milk/milk.glb differ
diff --git a/packages/client/assets/room/furnitures/milk/milk.png b/packages/client/assets/room/furnitures/milk/milk.png
new file mode 100644
index 0000000000..35181c8c8c
Binary files /dev/null and b/packages/client/assets/room/furnitures/milk/milk.png differ
diff --git a/packages/client/assets/room/furnitures/milk/milk.psd b/packages/client/assets/room/furnitures/milk/milk.psd
new file mode 100644
index 0000000000..f31e439277
Binary files /dev/null and b/packages/client/assets/room/furnitures/milk/milk.psd differ
diff --git a/packages/client/assets/room/furnitures/monitor/monitor.blend b/packages/client/assets/room/furnitures/monitor/monitor.blend
new file mode 100644
index 0000000000..6c042ccdd8
Binary files /dev/null and b/packages/client/assets/room/furnitures/monitor/monitor.blend differ
diff --git a/packages/client/assets/room/furnitures/monitor/monitor.glb b/packages/client/assets/room/furnitures/monitor/monitor.glb
new file mode 100644
index 0000000000..fc33286a15
Binary files /dev/null and b/packages/client/assets/room/furnitures/monitor/monitor.glb differ
diff --git a/packages/client/assets/room/furnitures/monitor/monitor.psd b/packages/client/assets/room/furnitures/monitor/monitor.psd
new file mode 100644
index 0000000000..57afff9cd9
Binary files /dev/null and b/packages/client/assets/room/furnitures/monitor/monitor.psd differ
diff --git a/packages/client/assets/room/furnitures/monitor/screen-uv.png b/packages/client/assets/room/furnitures/monitor/screen-uv.png
new file mode 100644
index 0000000000..35f74de8aa
Binary files /dev/null and b/packages/client/assets/room/furnitures/monitor/screen-uv.png differ
diff --git a/packages/client/assets/room/furnitures/monitor/screen.jpg b/packages/client/assets/room/furnitures/monitor/screen.jpg
new file mode 100644
index 0000000000..4004a1ede9
Binary files /dev/null and b/packages/client/assets/room/furnitures/monitor/screen.jpg differ
diff --git a/packages/client/assets/room/furnitures/moon/moon.blend b/packages/client/assets/room/furnitures/moon/moon.blend
new file mode 100644
index 0000000000..4ff3deab8e
Binary files /dev/null and b/packages/client/assets/room/furnitures/moon/moon.blend differ
diff --git a/packages/client/assets/room/furnitures/moon/moon.glb b/packages/client/assets/room/furnitures/moon/moon.glb
new file mode 100644
index 0000000000..07fa7e4c02
Binary files /dev/null and b/packages/client/assets/room/furnitures/moon/moon.glb differ
diff --git a/packages/client/assets/room/furnitures/moon/moon.jpg b/packages/client/assets/room/furnitures/moon/moon.jpg
new file mode 100644
index 0000000000..8988ac64b9
Binary files /dev/null and b/packages/client/assets/room/furnitures/moon/moon.jpg differ
diff --git a/packages/client/assets/room/furnitures/mousepad/mousepad.blend b/packages/client/assets/room/furnitures/mousepad/mousepad.blend
new file mode 100644
index 0000000000..14bd139c94
Binary files /dev/null and b/packages/client/assets/room/furnitures/mousepad/mousepad.blend differ
diff --git a/packages/client/assets/room/furnitures/mousepad/mousepad.glb b/packages/client/assets/room/furnitures/mousepad/mousepad.glb
new file mode 100644
index 0000000000..681ada49cd
Binary files /dev/null and b/packages/client/assets/room/furnitures/mousepad/mousepad.glb differ
diff --git a/packages/client/assets/room/furnitures/pc/motherboard-uv.png b/packages/client/assets/room/furnitures/pc/motherboard-uv.png
new file mode 100644
index 0000000000..355009fe7c
Binary files /dev/null and b/packages/client/assets/room/furnitures/pc/motherboard-uv.png differ
diff --git a/packages/client/assets/room/furnitures/pc/motherboard-uv.psd b/packages/client/assets/room/furnitures/pc/motherboard-uv.psd
new file mode 100644
index 0000000000..971f33f79e
Binary files /dev/null and b/packages/client/assets/room/furnitures/pc/motherboard-uv.psd differ
diff --git a/packages/client/assets/room/furnitures/pc/motherboard.jpg b/packages/client/assets/room/furnitures/pc/motherboard.jpg
new file mode 100644
index 0000000000..d894e4efcf
Binary files /dev/null and b/packages/client/assets/room/furnitures/pc/motherboard.jpg differ
diff --git a/packages/client/assets/room/furnitures/pc/pc.blend b/packages/client/assets/room/furnitures/pc/pc.blend
new file mode 100644
index 0000000000..13dfec6ccc
Binary files /dev/null and b/packages/client/assets/room/furnitures/pc/pc.blend differ
diff --git a/packages/client/assets/room/furnitures/pc/pc.glb b/packages/client/assets/room/furnitures/pc/pc.glb
new file mode 100644
index 0000000000..44a48b18ae
Binary files /dev/null and b/packages/client/assets/room/furnitures/pc/pc.glb differ
diff --git a/packages/client/assets/room/furnitures/pencil/pencil.blend b/packages/client/assets/room/furnitures/pencil/pencil.blend
new file mode 100644
index 0000000000..0fc6bdd776
Binary files /dev/null and b/packages/client/assets/room/furnitures/pencil/pencil.blend differ
diff --git a/packages/client/assets/room/furnitures/pencil/pencil.glb b/packages/client/assets/room/furnitures/pencil/pencil.glb
new file mode 100644
index 0000000000..a938b5cdcc
Binary files /dev/null and b/packages/client/assets/room/furnitures/pencil/pencil.glb differ
diff --git a/packages/client/assets/room/furnitures/photoframe/photo-uv.png b/packages/client/assets/room/furnitures/photoframe/photo-uv.png
new file mode 100644
index 0000000000..9b94906413
Binary files /dev/null and b/packages/client/assets/room/furnitures/photoframe/photo-uv.png differ
diff --git a/packages/client/assets/room/furnitures/photoframe/photo.jpg b/packages/client/assets/room/furnitures/photoframe/photo.jpg
new file mode 100644
index 0000000000..af14f0f36a
Binary files /dev/null and b/packages/client/assets/room/furnitures/photoframe/photo.jpg differ
diff --git a/packages/client/assets/room/furnitures/photoframe/photoframe.blend b/packages/client/assets/room/furnitures/photoframe/photoframe.blend
new file mode 100644
index 0000000000..4224cde45b
Binary files /dev/null and b/packages/client/assets/room/furnitures/photoframe/photoframe.blend differ
diff --git a/packages/client/assets/room/furnitures/photoframe/photoframe.glb b/packages/client/assets/room/furnitures/photoframe/photoframe.glb
new file mode 100644
index 0000000000..4255a77de6
Binary files /dev/null and b/packages/client/assets/room/furnitures/photoframe/photoframe.glb differ
diff --git a/packages/client/assets/room/furnitures/piano/piano.blend b/packages/client/assets/room/furnitures/piano/piano.blend
new file mode 100644
index 0000000000..7653cdf672
Binary files /dev/null and b/packages/client/assets/room/furnitures/piano/piano.blend differ
diff --git a/packages/client/assets/room/furnitures/piano/piano.glb b/packages/client/assets/room/furnitures/piano/piano.glb
new file mode 100644
index 0000000000..7242e78ceb
Binary files /dev/null and b/packages/client/assets/room/furnitures/piano/piano.glb differ
diff --git a/packages/client/assets/room/furnitures/pinguin/pinguin.blend b/packages/client/assets/room/furnitures/pinguin/pinguin.blend
new file mode 100644
index 0000000000..514c713e4c
Binary files /dev/null and b/packages/client/assets/room/furnitures/pinguin/pinguin.blend differ
diff --git a/packages/client/assets/room/furnitures/pinguin/pinguin.glb b/packages/client/assets/room/furnitures/pinguin/pinguin.glb
new file mode 100644
index 0000000000..6df34c06e9
Binary files /dev/null and b/packages/client/assets/room/furnitures/pinguin/pinguin.glb differ
diff --git a/packages/client/assets/room/furnitures/plant/plant-soil-uv.png b/packages/client/assets/room/furnitures/plant/plant-soil-uv.png
new file mode 100644
index 0000000000..d4971a896c
Binary files /dev/null and b/packages/client/assets/room/furnitures/plant/plant-soil-uv.png differ
diff --git a/packages/client/assets/room/furnitures/plant/plant-soil.png b/packages/client/assets/room/furnitures/plant/plant-soil.png
new file mode 100644
index 0000000000..e79ccd240e
Binary files /dev/null and b/packages/client/assets/room/furnitures/plant/plant-soil.png differ
diff --git a/packages/client/assets/room/furnitures/plant/plant-soil.psd b/packages/client/assets/room/furnitures/plant/plant-soil.psd
new file mode 100644
index 0000000000..1457b7ea5b
Binary files /dev/null and b/packages/client/assets/room/furnitures/plant/plant-soil.psd differ
diff --git a/packages/client/assets/room/furnitures/plant/plant.blend b/packages/client/assets/room/furnitures/plant/plant.blend
new file mode 100644
index 0000000000..aa38c7b54e
Binary files /dev/null and b/packages/client/assets/room/furnitures/plant/plant.blend differ
diff --git a/packages/client/assets/room/furnitures/plant/plant.glb b/packages/client/assets/room/furnitures/plant/plant.glb
new file mode 100644
index 0000000000..38422b4a9b
Binary files /dev/null and b/packages/client/assets/room/furnitures/plant/plant.glb differ
diff --git a/packages/client/assets/room/furnitures/plant2/plant2.blend b/packages/client/assets/room/furnitures/plant2/plant2.blend
new file mode 100644
index 0000000000..6592c5d98d
Binary files /dev/null and b/packages/client/assets/room/furnitures/plant2/plant2.blend differ
diff --git a/packages/client/assets/room/furnitures/plant2/plant2.glb b/packages/client/assets/room/furnitures/plant2/plant2.glb
new file mode 100644
index 0000000000..223e6f5834
Binary files /dev/null and b/packages/client/assets/room/furnitures/plant2/plant2.glb differ
diff --git a/packages/client/assets/room/furnitures/plant2/soil.png b/packages/client/assets/room/furnitures/plant2/soil.png
new file mode 100644
index 0000000000..e79ccd240e
Binary files /dev/null and b/packages/client/assets/room/furnitures/plant2/soil.png differ
diff --git a/packages/client/assets/room/furnitures/poster-h/poster-h.blend b/packages/client/assets/room/furnitures/poster-h/poster-h.blend
new file mode 100644
index 0000000000..40f944f3c1
Binary files /dev/null and b/packages/client/assets/room/furnitures/poster-h/poster-h.blend differ
diff --git a/packages/client/assets/room/furnitures/poster-h/poster-h.glb b/packages/client/assets/room/furnitures/poster-h/poster-h.glb
new file mode 100644
index 0000000000..c6032c1009
Binary files /dev/null and b/packages/client/assets/room/furnitures/poster-h/poster-h.glb differ
diff --git a/packages/client/assets/room/furnitures/poster-h/uv.png b/packages/client/assets/room/furnitures/poster-h/uv.png
new file mode 100644
index 0000000000..f854231e0b
Binary files /dev/null and b/packages/client/assets/room/furnitures/poster-h/uv.png differ
diff --git a/packages/client/assets/room/furnitures/poster-v/poster-v.blend b/packages/client/assets/room/furnitures/poster-v/poster-v.blend
new file mode 100644
index 0000000000..07fe971634
Binary files /dev/null and b/packages/client/assets/room/furnitures/poster-v/poster-v.blend differ
diff --git a/packages/client/assets/room/furnitures/poster-v/poster-v.glb b/packages/client/assets/room/furnitures/poster-v/poster-v.glb
new file mode 100644
index 0000000000..6e3782f193
Binary files /dev/null and b/packages/client/assets/room/furnitures/poster-v/poster-v.glb differ
diff --git a/packages/client/assets/room/furnitures/poster-v/uv.png b/packages/client/assets/room/furnitures/poster-v/uv.png
new file mode 100644
index 0000000000..7bb2bf809e
Binary files /dev/null and b/packages/client/assets/room/furnitures/poster-v/uv.png differ
diff --git a/packages/client/assets/room/furnitures/pudding/pudding.blend b/packages/client/assets/room/furnitures/pudding/pudding.blend
new file mode 100644
index 0000000000..bba40ce161
Binary files /dev/null and b/packages/client/assets/room/furnitures/pudding/pudding.blend differ
diff --git a/packages/client/assets/room/furnitures/pudding/pudding.glb b/packages/client/assets/room/furnitures/pudding/pudding.glb
new file mode 100644
index 0000000000..06c9ed80cc
Binary files /dev/null and b/packages/client/assets/room/furnitures/pudding/pudding.glb differ
diff --git a/packages/client/assets/room/furnitures/rubik-cube/rubik-cube.blend b/packages/client/assets/room/furnitures/rubik-cube/rubik-cube.blend
new file mode 100644
index 0000000000..6c09067e78
Binary files /dev/null and b/packages/client/assets/room/furnitures/rubik-cube/rubik-cube.blend differ
diff --git a/packages/client/assets/room/furnitures/rubik-cube/rubik-cube.glb b/packages/client/assets/room/furnitures/rubik-cube/rubik-cube.glb
new file mode 100644
index 0000000000..d640df9b06
Binary files /dev/null and b/packages/client/assets/room/furnitures/rubik-cube/rubik-cube.glb differ
diff --git a/packages/client/assets/room/furnitures/server/rack-uv.png b/packages/client/assets/room/furnitures/server/rack-uv.png
new file mode 100644
index 0000000000..65bdb0ffd9
Binary files /dev/null and b/packages/client/assets/room/furnitures/server/rack-uv.png differ
diff --git a/packages/client/assets/room/furnitures/server/rack.png b/packages/client/assets/room/furnitures/server/rack.png
new file mode 100644
index 0000000000..b851295cfa
Binary files /dev/null and b/packages/client/assets/room/furnitures/server/rack.png differ
diff --git a/packages/client/assets/room/furnitures/server/server.blend b/packages/client/assets/room/furnitures/server/server.blend
new file mode 100644
index 0000000000..6675dfbdc2
Binary files /dev/null and b/packages/client/assets/room/furnitures/server/server.blend differ
diff --git a/packages/client/assets/room/furnitures/server/server.glb b/packages/client/assets/room/furnitures/server/server.glb
new file mode 100644
index 0000000000..a8b530a2d2
Binary files /dev/null and b/packages/client/assets/room/furnitures/server/server.glb differ
diff --git a/packages/client/assets/room/furnitures/server/server.png b/packages/client/assets/room/furnitures/server/server.png
new file mode 100644
index 0000000000..8e9a0d716c
Binary files /dev/null and b/packages/client/assets/room/furnitures/server/server.png differ
diff --git a/packages/client/assets/room/furnitures/server/uv.png b/packages/client/assets/room/furnitures/server/uv.png
new file mode 100644
index 0000000000..ca2e747d16
Binary files /dev/null and b/packages/client/assets/room/furnitures/server/uv.png differ
diff --git a/packages/client/assets/room/furnitures/sofa/sofa.blend b/packages/client/assets/room/furnitures/sofa/sofa.blend
new file mode 100644
index 0000000000..fb5aa51a2c
Binary files /dev/null and b/packages/client/assets/room/furnitures/sofa/sofa.blend differ
diff --git a/packages/client/assets/room/furnitures/sofa/sofa.glb b/packages/client/assets/room/furnitures/sofa/sofa.glb
new file mode 100644
index 0000000000..6ce77d94ac
Binary files /dev/null and b/packages/client/assets/room/furnitures/sofa/sofa.glb differ
diff --git a/packages/client/assets/room/furnitures/spiral/spiral.blend b/packages/client/assets/room/furnitures/spiral/spiral.blend
new file mode 100644
index 0000000000..9d3be77bce
Binary files /dev/null and b/packages/client/assets/room/furnitures/spiral/spiral.blend differ
diff --git a/packages/client/assets/room/furnitures/spiral/spiral.glb b/packages/client/assets/room/furnitures/spiral/spiral.glb
new file mode 100644
index 0000000000..ee8e3c23b1
Binary files /dev/null and b/packages/client/assets/room/furnitures/spiral/spiral.glb differ
diff --git a/packages/client/assets/room/furnitures/tv/screen-uv.png b/packages/client/assets/room/furnitures/tv/screen-uv.png
new file mode 100644
index 0000000000..4bb74f031f
Binary files /dev/null and b/packages/client/assets/room/furnitures/tv/screen-uv.png differ
diff --git a/packages/client/assets/room/furnitures/tv/tv.blend b/packages/client/assets/room/furnitures/tv/tv.blend
new file mode 100644
index 0000000000..490e298e7b
Binary files /dev/null and b/packages/client/assets/room/furnitures/tv/tv.blend differ
diff --git a/packages/client/assets/room/furnitures/tv/tv.glb b/packages/client/assets/room/furnitures/tv/tv.glb
new file mode 100644
index 0000000000..b9bd23896b
Binary files /dev/null and b/packages/client/assets/room/furnitures/tv/tv.glb differ
diff --git a/packages/client/assets/room/furnitures/wall-clock/wall-clock.blend b/packages/client/assets/room/furnitures/wall-clock/wall-clock.blend
new file mode 100644
index 0000000000..0a61c8f01e
Binary files /dev/null and b/packages/client/assets/room/furnitures/wall-clock/wall-clock.blend differ
diff --git a/packages/client/assets/room/furnitures/wall-clock/wall-clock.glb b/packages/client/assets/room/furnitures/wall-clock/wall-clock.glb
new file mode 100644
index 0000000000..b9f0093a8d
Binary files /dev/null and b/packages/client/assets/room/furnitures/wall-clock/wall-clock.glb differ
diff --git a/packages/client/assets/room/rooms/default/default.blend b/packages/client/assets/room/rooms/default/default.blend
new file mode 100644
index 0000000000..661154724a
Binary files /dev/null and b/packages/client/assets/room/rooms/default/default.blend differ
diff --git a/packages/client/assets/room/rooms/default/default.glb b/packages/client/assets/room/rooms/default/default.glb
new file mode 100644
index 0000000000..3d378deee2
Binary files /dev/null and b/packages/client/assets/room/rooms/default/default.glb differ
diff --git a/packages/client/assets/room/rooms/washitsu/husuma-uv.png b/packages/client/assets/room/rooms/washitsu/husuma-uv.png
new file mode 100644
index 0000000000..ae2fca3911
Binary files /dev/null and b/packages/client/assets/room/rooms/washitsu/husuma-uv.png differ
diff --git a/packages/client/assets/room/rooms/washitsu/husuma.png b/packages/client/assets/room/rooms/washitsu/husuma.png
new file mode 100644
index 0000000000..084cbed67c
Binary files /dev/null and b/packages/client/assets/room/rooms/washitsu/husuma.png differ
diff --git a/packages/client/assets/room/rooms/washitsu/tatami-single1600.png b/packages/client/assets/room/rooms/washitsu/tatami-single1600.png
new file mode 100644
index 0000000000..c0e684d743
Binary files /dev/null and b/packages/client/assets/room/rooms/washitsu/tatami-single1600.png differ
diff --git a/packages/client/assets/room/rooms/washitsu/tatami-uv.png b/packages/client/assets/room/rooms/washitsu/tatami-uv.png
new file mode 100644
index 0000000000..5b16c66091
Binary files /dev/null and b/packages/client/assets/room/rooms/washitsu/tatami-uv.png differ
diff --git a/packages/client/assets/room/rooms/washitsu/tatami.afdesign b/packages/client/assets/room/rooms/washitsu/tatami.afdesign
new file mode 100644
index 0000000000..9300a26950
Binary files /dev/null and b/packages/client/assets/room/rooms/washitsu/tatami.afdesign differ
diff --git a/packages/client/assets/room/rooms/washitsu/tatami.png b/packages/client/assets/room/rooms/washitsu/tatami.png
new file mode 100644
index 0000000000..8894d040ae
Binary files /dev/null and b/packages/client/assets/room/rooms/washitsu/tatami.png differ
diff --git a/packages/client/assets/room/rooms/washitsu/washitsu.blend b/packages/client/assets/room/rooms/washitsu/washitsu.blend
new file mode 100644
index 0000000000..84dc11374d
Binary files /dev/null and b/packages/client/assets/room/rooms/washitsu/washitsu.blend differ
diff --git a/packages/client/assets/room/rooms/washitsu/washitsu.glb b/packages/client/assets/room/rooms/washitsu/washitsu.glb
new file mode 100644
index 0000000000..5b4767bc73
Binary files /dev/null and b/packages/client/assets/room/rooms/washitsu/washitsu.glb differ
diff --git a/packages/client/assets/sounds/aisha/1.mp3 b/packages/client/assets/sounds/aisha/1.mp3
new file mode 100644
index 0000000000..d8e9a2f265
Binary files /dev/null and b/packages/client/assets/sounds/aisha/1.mp3 differ
diff --git a/packages/client/assets/sounds/aisha/2.mp3 b/packages/client/assets/sounds/aisha/2.mp3
new file mode 100644
index 0000000000..477c2eba43
Binary files /dev/null and b/packages/client/assets/sounds/aisha/2.mp3 differ
diff --git a/packages/client/assets/sounds/aisha/3.mp3 b/packages/client/assets/sounds/aisha/3.mp3
new file mode 100644
index 0000000000..fe0d8063df
Binary files /dev/null and b/packages/client/assets/sounds/aisha/3.mp3 differ
diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba.mp3
new file mode 100644
index 0000000000..616b506c4f
Binary files /dev/null and b/packages/client/assets/sounds/noizenecio/kick_gaba.mp3 differ
diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba2.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba2.mp3
new file mode 100644
index 0000000000..33c2837620
Binary files /dev/null and b/packages/client/assets/sounds/noizenecio/kick_gaba2.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/down.mp3 b/packages/client/assets/sounds/syuilo/down.mp3
new file mode 100644
index 0000000000..4cd421139d
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/down.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/kick.mp3 b/packages/client/assets/sounds/syuilo/kick.mp3
new file mode 100644
index 0000000000..4e0e72091c
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/kick.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/pirori-square-wet.mp3 b/packages/client/assets/sounds/syuilo/pirori-square-wet.mp3
new file mode 100644
index 0000000000..babf1fce60
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/pirori-square-wet.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/pirori-wet.mp3 b/packages/client/assets/sounds/syuilo/pirori-wet.mp3
new file mode 100644
index 0000000000..25e2c46a64
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/pirori-wet.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/pirori.mp3 b/packages/client/assets/sounds/syuilo/pirori.mp3
new file mode 100644
index 0000000000..a745415ac0
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/pirori.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/poi1.mp3 b/packages/client/assets/sounds/syuilo/poi1.mp3
new file mode 100644
index 0000000000..59dae90965
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/poi1.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/poi2.mp3 b/packages/client/assets/sounds/syuilo/poi2.mp3
new file mode 100644
index 0000000000..a65c653891
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/poi2.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/pope1.mp3 b/packages/client/assets/sounds/syuilo/pope1.mp3
new file mode 100644
index 0000000000..d6f53cfacc
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/pope1.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/pope2.mp3 b/packages/client/assets/sounds/syuilo/pope2.mp3
new file mode 100644
index 0000000000..fe5d95e292
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/pope2.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/popo.mp3 b/packages/client/assets/sounds/syuilo/popo.mp3
new file mode 100644
index 0000000000..a2a1605bbb
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/popo.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/queue-jammed.mp3 b/packages/client/assets/sounds/syuilo/queue-jammed.mp3
new file mode 100644
index 0000000000..99e0c437fe
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/queue-jammed.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/reverved.mp3 b/packages/client/assets/sounds/syuilo/reverved.mp3
new file mode 100644
index 0000000000..47588ef270
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/reverved.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/ryukyu.mp3 b/packages/client/assets/sounds/syuilo/ryukyu.mp3
new file mode 100644
index 0000000000..9e935e3f37
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/ryukyu.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/snare.mp3 b/packages/client/assets/sounds/syuilo/snare.mp3
new file mode 100644
index 0000000000..9244189c2d
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/snare.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/square-pico.mp3 b/packages/client/assets/sounds/syuilo/square-pico.mp3
new file mode 100644
index 0000000000..c4d8305ae7
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/square-pico.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/triple.mp3 b/packages/client/assets/sounds/syuilo/triple.mp3
new file mode 100644
index 0000000000..54ab974d46
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/triple.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/up.mp3 b/packages/client/assets/sounds/syuilo/up.mp3
new file mode 100644
index 0000000000..3f30867764
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/up.mp3 differ
diff --git a/packages/client/assets/sounds/syuilo/waon.mp3 b/packages/client/assets/sounds/syuilo/waon.mp3
new file mode 100644
index 0000000000..a4af473861
Binary files /dev/null and b/packages/client/assets/sounds/syuilo/waon.mp3 differ
diff --git a/packages/client/assets/sparkle-spritesheet.png b/packages/client/assets/sparkle-spritesheet.png
new file mode 100644
index 0000000000..0defccaf07
Binary files /dev/null and b/packages/client/assets/sparkle-spritesheet.png differ
diff --git a/packages/client/assets/unread.svg b/packages/client/assets/unread.svg
new file mode 100644
index 0000000000..8c3cc9f475
--- /dev/null
+++ b/packages/client/assets/unread.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
+	 y="0px" width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
+<circle fill="#3AA2DC" cx="16.5" cy="16.5" r="6"/>
+</svg>
diff --git a/packages/client/package.json b/packages/client/package.json
new file mode 100644
index 0000000000..24345b969f
--- /dev/null
+++ b/packages/client/package.json
@@ -0,0 +1,163 @@
+{
+	"private": true,
+	"scripts": {
+		"watch": "webpack --watch",
+		"build": "webpack"
+	},
+	"resolutions": {
+		"chokidar": "^3.3.1",
+		"lodash": "^4.17.21"
+	},
+	"dependencies": {
+		"@discordapp/twemoji": "13.1.0",
+		"@elastic/elasticsearch": "7.11.0",
+		"@sentry/browser": "5.29.2",
+		"@sentry/tracing": "5.29.2",
+		"@sinonjs/fake-timers": "7.1.2",
+		"@syuilo/aiscript": "0.11.1",
+		"@types/dateformat": "3.0.1",
+		"@types/escape-regexp": "0.0.0",
+		"@types/glob": "7.2.0",
+		"@types/gulp": "4.0.9",
+		"@types/gulp-rename": "2.0.1",
+		"@types/is-url": "1.2.30",
+		"@types/js-yaml": "4.0.4",
+		"@types/katex": "0.11.1",
+		"@types/matter-js": "0.17.6",
+		"@types/mocha": "8.2.3",
+		"@types/node": "16.11.7",
+		"@types/node-fetch": "2.5.12",
+		"@types/nodemailer": "6.4.4",
+		"@types/nprogress": "0.2.0",
+		"@types/oauth": "0.9.1",
+		"@types/parse5": "6.0.2",
+		"@types/parsimmon": "1.10.6",
+		"@types/portscanner": "2.1.1",
+		"@types/pug": "2.0.5",
+		"@types/punycode": "2.1.0",
+		"@types/qrcode": "1.4.1",
+		"@types/random-seed": "0.3.3",
+		"@types/rename": "1.0.4",
+		"@types/request-stats": "3.0.0",
+		"@types/rimraf": "3.0.2",
+		"@types/seedrandom": "2.4.28",
+		"@types/sinonjs__fake-timers": "6.0.4",
+		"@types/speakeasy": "2.0.6",
+		"@types/throttle-debounce": "2.1.0",
+		"@types/tinycolor2": "1.4.3",
+		"@types/tmp": "0.2.2",
+		"@types/uuid": "8.3.1",
+		"@types/web-push": "3.3.2",
+		"@types/webpack": "5.28.0",
+		"@types/webpack-stream": "3.2.12",
+		"@types/websocket": "1.0.4",
+		"@types/ws": "8.2.0",
+		"@typescript-eslint/parser": "5.1.0",
+		"@vue/compiler-sfc": "3.2.21",
+		"abort-controller": "3.0.0",
+		"autobind-decorator": "2.4.0",
+		"autosize": "4.0.4",
+		"autwh": "0.1.0",
+		"blurhash": "1.1.4",
+		"broadcast-channel": "4.5.0",
+		"chart.js": "3.6.0",
+		"chartjs-adapter-date-fns": "2.0.0",
+		"chartjs-plugin-zoom": "1.1.1",
+		"compare-versions": "3.6.0",
+		"concurrently": "6.3.0",
+		"content-disposition": "0.5.3",
+		"crc-32": "1.2.0",
+		"css-loader": "6.5.1",
+		"cssnano": "5.0.10",
+		"date-fns": "2.25.0",
+		"dateformat": "4.5.1",
+		"deep-email-validator": "0.1.18",
+		"escape-regexp": "0.0.1",
+		"eslint": "8.2.0",
+		"eslint-plugin-vue": "8.0.3",
+		"eventemitter3": "4.0.7",
+		"feed": "4.2.2",
+		"glob": "7.2.0",
+		"got": "11.8.2",
+		"idb-keyval": "5.1.3",
+		"insert-text-at-cursor": "0.3.0",
+		"ip-cidr": "3.0.4",
+		"is-svg": "4.3.1",
+		"js-yaml": "4.1.0",
+		"json5": "2.2.0",
+		"json5-loader": "4.0.1",
+		"katex": "0.13.18",
+		"langmap": "0.0.16",
+		"matter-js": "0.17.1",
+		"mfm-js": "0.20.0",
+		"misskey-js": "0.0.10",
+		"mocha": "8.4.0",
+		"ms": "2.1.3",
+		"nested-property": "4.0.0",
+		"node-fetch": "2.6.1",
+		"parse5": "6.0.1",
+		"photoswipe": "git://github.com/dimsemenov/photoswipe#v5-beta",
+		"portscanner": "2.2.0",
+		"postcss": "8.3.11",
+		"postcss-loader": "6.2.0",
+		"prismjs": "1.25.0",
+		"private-ip": "2.3.3",
+		"probe-image-size": "7.2.1",
+		"promise-limit": "2.7.0",
+		"pug": "3.0.2",
+		"punycode": "2.1.1",
+		"pureimage": "0.3.5",
+		"qrcode": "1.4.4",
+		"random-seed": "0.3.0",
+		"ratelimiter": "3.4.1",
+		"reflect-metadata": "0.1.13",
+		"rename": "1.0.4",
+		"request-stats": "3.0.0",
+		"rimraf": "3.0.2",
+		"rndstr": "1.0.0",
+		"s-age": "1.1.2",
+		"sass": "1.43.4",
+		"sass-loader": "12.3.0",
+		"seedrandom": "3.0.5",
+		"speakeasy": "2.0.0",
+		"strict-event-emitter-types": "2.0.0",
+		"stringz": "2.1.0",
+		"style-loader": "3.3.1",
+		"syuilo-password-strength": "0.0.1",
+		"textarea-caret": "3.1.0",
+		"three": "0.117.1",
+		"throttle-debounce": "3.0.1",
+		"tinycolor2": "1.4.2",
+		"tmp": "0.2.1",
+		"ts-loader": "9.2.6",
+		"ts-node": "10.4.0",
+		"tsc-alias": "1.3.10",
+		"tsconfig-paths": "3.11.0",
+		"tslint": "6.1.3",
+		"tslint-sonarts": "1.9.0",
+		"twemoji-parser": "13.1.0",
+		"typescript": "4.4.4",
+		"uuid": "8.3.2",
+		"v-debounce": "0.1.2",
+		"vanilla-tilt": "1.7.2",
+		"vue": "3.2.21",
+		"vue-loader": "16.7.0",
+		"vue-prism-editor": "2.0.0-alpha.2",
+		"vue-router": "4.0.5",
+		"vue-style-loader": "4.1.3",
+		"vue-svg-loader": "0.17.0-beta.2",
+		"vuedraggable": "4.0.1",
+		"web-push": "3.4.5",
+		"webpack": "5.63.0",
+		"webpack-cli": "4.9.1",
+		"websocket": "1.0.34",
+		"ws": "8.2.3"
+	},
+	"devDependencies": {
+		"@redocly/openapi-core": "1.0.0-beta.54",
+		"@types/fluent-ffmpeg": "2.1.17",
+		"cross-env": "7.0.3",
+		"cypress": "8.5.0",
+		"start-server-and-test": "1.14.0"
+	}
+}
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
new file mode 100644
index 0000000000..ef7eb8f60a
--- /dev/null
+++ b/packages/client/src/account.ts
@@ -0,0 +1,211 @@
+import { del, get, set } from '@/scripts/idb-proxy';
+import { reactive } from 'vue';
+import { apiUrl } from '@/config';
+import { waiting, api, popup, popupMenu, success } from '@/os';
+import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
+import { showSuspendedDialog } from './scripts/show-suspended-dialog';
+import { i18n } from './i18n';
+
+// TODO: 他のタブと永続化されたstateを同期
+
+type Account = {
+	id: string;
+	token: string;
+	isModerator: boolean;
+	isAdmin: boolean;
+	isDeleted: boolean;
+};
+
+const data = localStorage.getItem('account');
+
+// TODO: 外部からはreadonlyに
+export const $i = data ? reactive(JSON.parse(data) as Account) : null;
+
+export async function signout() {
+	waiting();
+	localStorage.removeItem('account');
+
+	//#region Remove account
+	const accounts = await getAccounts();
+	accounts.splice(accounts.findIndex(x => x.id === $i.id), 1);
+
+	if (accounts.length > 0) await set('accounts', accounts);
+	else await del('accounts');
+	//#endregion
+
+	//#region Remove service worker registration
+	try {
+		if (navigator.serviceWorker.controller) {
+			const registration = await navigator.serviceWorker.ready;
+			const push = await registration.pushManager.getSubscription();
+			if (push) {
+				await fetch(`${apiUrl}/sw/unregister`, {
+					method: 'POST',
+					body: JSON.stringify({
+						i: $i.token,
+						endpoint: push.endpoint,
+					}),
+				});
+			}
+		}
+
+		if (accounts.length === 0) {
+			await navigator.serviceWorker.getRegistrations()
+				.then(registrations => {
+					return Promise.all(registrations.map(registration => registration.unregister()));
+				});
+		}
+	} catch (e) {}
+	//#endregion
+
+	document.cookie = `igi=; path=/`;
+
+	if (accounts.length > 0) login(accounts[0].token);
+	else unisonReload('/');
+}
+
+export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
+	return (await get('accounts')) || [];
+}
+
+export async function addAccount(id: Account['id'], token: Account['token']) {
+	const accounts = await getAccounts();
+	if (!accounts.some(x => x.id === id)) {
+		await set('accounts', accounts.concat([{ id, token }]));
+	}
+}
+
+function fetchAccount(token): Promise<Account> {
+	return new Promise((done, fail) => {
+		// Fetch user
+		fetch(`${apiUrl}/i`, {
+			method: 'POST',
+			body: JSON.stringify({
+				i: token
+			})
+		})
+		.then(res => res.json())
+		.then(res => {
+			if (res.error) {
+				if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
+					showSuspendedDialog().then(() => {
+						signout();
+					});
+				} else {
+					signout();
+				}
+			} else {
+				res.token = token;
+				done(res);
+			}
+		})
+		.catch(fail);
+	});
+}
+
+export function updateAccount(data) {
+	for (const [key, value] of Object.entries(data)) {
+		$i[key] = value;
+	}
+	localStorage.setItem('account', JSON.stringify($i));
+}
+
+export function refreshAccount() {
+	return fetchAccount($i.token).then(updateAccount);
+}
+
+export async function login(token: Account['token'], redirect?: string) {
+	waiting();
+	if (_DEV_) console.log('logging as token ', token);
+	const me = await fetchAccount(token);
+	localStorage.setItem('account', JSON.stringify(me));
+	await addAccount(me.id, token);
+
+	if (redirect) {
+		// 他のタブは再読み込みするだけ
+		reloadChannel.postMessage(null);
+		// このページはredirectで指定された先に移動
+		location.href = redirect;
+		return;
+	}
+
+	unisonReload();
+}
+
+export async function openAccountMenu(ev: MouseEvent) {
+	function showSigninDialog() {
+		popup(import('@/components/signin-dialog.vue'), {}, {
+			done: res => {
+				addAccount(res.id, res.i);
+				success();
+			},
+		}, 'closed');
+	}
+
+	function createAccount() {
+		popup(import('@/components/signup-dialog.vue'), {}, {
+			done: res => {
+				addAccount(res.id, res.i);
+				switchAccountWithToken(res.i);
+			},
+		}, 'closed');
+	}
+
+	async function switchAccount(account: any) {
+		const storedAccounts = await getAccounts();
+		const token = storedAccounts.find(x => x.id === account.id).token;
+		switchAccountWithToken(token);
+	}
+
+	function switchAccountWithToken(token: string) {
+		login(token);
+	}
+
+	const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
+	const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
+
+	const accountItemPromises = storedAccounts.map(a => new Promise(res => {
+		accountsPromise.then(accounts => {
+			const account = accounts.find(x => x.id === a.id);
+			if (account == null) return res(null);
+			res({
+				type: 'user',
+				user: account,
+				action: () => { switchAccount(account); }
+			});
+		});
+	}));
+
+	popupMenu([...[{
+		type: 'link',
+		text: i18n.locale.profile,
+		to: `/@${ $i.username }`,
+		avatar: $i,
+	}, null, ...accountItemPromises, {
+		icon: 'fas fa-plus',
+		text: i18n.locale.addAccount,
+		action: () => {
+			popupMenu([{
+				text: i18n.locale.existingAccount,
+				action: () => { showSigninDialog(); },
+			}, {
+				text: i18n.locale.createAccount,
+				action: () => { createAccount(); },
+			}], ev.currentTarget || ev.target);
+		},
+	}, {
+		type: 'link',
+		icon: 'fas fa-users',
+		text: i18n.locale.manageAccounts,
+		to: `/settings/accounts`,
+	}]], ev.currentTarget || ev.target, {
+		align: 'left'
+	});
+}
+
+// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
+declare module '@vue/runtime-core' {
+	interface ComponentCustomProperties {
+		$i: typeof $i;
+	}
+}
diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue
new file mode 100644
index 0000000000..700ce30bb2
--- /dev/null
+++ b/packages/client/src/components/abuse-report-window.vue
@@ -0,0 +1,79 @@
+<template>
+<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')">
+	<template #header>
+		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
+		<I18n :src="$ts.reportAbuseOf" tag="span">
+			<template #name>
+				<b><MkAcct :user="user"/></b>
+			</template>
+		</I18n>
+	</template>
+	<div class="dpvffvvy _monolithic_">
+		<div class="_section">
+			<MkTextarea v-model="comment">
+				<template #label>{{ $ts.details }}</template>
+				<template #caption>{{ $ts.fillAbuseReportDescription }}</template>
+			</MkTextarea>
+		</div>
+		<div class="_section">
+			<MkButton @click="send" primary full :disabled="comment.length === 0">{{ $ts.send }}</MkButton>
+		</div>
+	</div>
+</XWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import XWindow from '@/components/ui/window.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XWindow,
+		MkTextarea,
+		MkButton,
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true,
+		},
+		initialComment: {
+			type: String,
+			required: false,
+		},
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			comment: this.initialComment || '',
+		};
+	},
+
+	methods: {
+		send() {
+			os.apiWithDialog('users/report-abuse', {
+				userId: this.user.id,
+				comment: this.comment,
+			}, undefined, res => {
+				os.dialog({
+					type: 'success',
+					text: this.$ts.abuseReported
+				});
+				this.$refs.window.close();
+			});
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.dpvffvvy {
+	--root-margin: 16px;
+}
+</style>
diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue
new file mode 100644
index 0000000000..bc572e5fff
--- /dev/null
+++ b/packages/client/src/components/analog-clock.vue
@@ -0,0 +1,150 @@
+<template>
+<svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none">
+	<circle v-for="(angle, i) in graduations"
+					:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
+					:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
+					:r="i % 5 == 0 ? 0.125 : 0.05"
+					:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"
+					:key="i"
+	/>
+
+	<line
+		:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))"
+		:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))"
+		:x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
+		:y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
+		:stroke="sHandColor"
+		:stroke-width="thickness / 2"
+		stroke-linecap="round"
+	/>
+
+	<line
+		:x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))"
+		:y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))"
+		:x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
+		:y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
+		:stroke="mHandColor"
+		:stroke-width="thickness"
+		stroke-linecap="round"
+	/>
+
+	<line
+		:x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))"
+		:y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))"
+		:x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
+		:y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
+		:stroke="hHandColor"
+		:stroke-width="thickness"
+		stroke-linecap="round"
+	/>
+</svg>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as tinycolor from 'tinycolor2';
+
+export default defineComponent({
+	props: {
+		thickness: {
+			type: Number,
+			default: 0.1
+		}
+	},
+
+	data() {
+		return {
+			now: new Date(),
+			enabled: true,
+
+			graduationsPadding: 0.5,
+			handsPadding: 1,
+			handsTailLength: 0.7,
+			hHandLengthRatio: 0.75,
+			mHandLengthRatio: 1,
+			sHandLengthRatio: 1,
+
+			computedStyle: getComputedStyle(document.documentElement)
+		};
+	},
+
+	computed: {
+		dark(): boolean {
+			return tinycolor(this.computedStyle.getPropertyValue('--bg')).isDark();
+		},
+
+		majorGraduationColor(): string {
+			return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
+		},
+		minorGraduationColor(): string {
+			return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+		},
+
+		sHandColor(): string {
+			return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
+		},
+		mHandColor(): string {
+			return tinycolor(this.computedStyle.getPropertyValue('--fg')).toHexString();
+		},
+		hHandColor(): string {
+			return tinycolor(this.computedStyle.getPropertyValue('--accent')).toHexString();
+		},
+
+		s(): number {
+			return this.now.getSeconds();
+		},
+		m(): number {
+			return this.now.getMinutes();
+		},
+		h(): number {
+			return this.now.getHours();
+		},
+
+		hAngle(): number {
+			return Math.PI * (this.h % 12 + (this.m + this.s / 60) / 60) / 6;
+		},
+		mAngle(): number {
+			return Math.PI * (this.m + this.s / 60) / 30;
+		},
+		sAngle(): number {
+			return Math.PI * this.s / 30;
+		},
+
+		graduations(): any {
+			const angles = [];
+			for (let i = 0; i < 60; i++) {
+				const angle = Math.PI * i / 30;
+				angles.push(angle);
+			}
+
+			return angles;
+		}
+	},
+
+	mounted() {
+		const update = () => {
+			if (this.enabled) {
+				this.tick();
+				setTimeout(update, 1000);
+			}
+		};
+		update();
+	},
+
+	beforeUnmount() {
+		this.enabled = false;
+	},
+
+	methods: {
+		tick() {
+			this.now = new Date();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mbcofsoe {
+	display: block;
+}
+</style>
diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue
new file mode 100644
index 0000000000..a7d2d507e0
--- /dev/null
+++ b/packages/client/src/components/autocomplete.vue
@@ -0,0 +1,501 @@
+<template>
+<div class="swhvrteh _popup _shadow" @contextmenu.prevent="() => {}">
+	<ol class="users" ref="suggests" v-if="type === 'user'">
+		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user">
+			<img class="avatar" :src="user.avatarUrl"/>
+			<span class="name">
+				<MkUserName :user="user" :key="user.id"/>
+			</span>
+			<span class="username">@{{ acct(user) }}</span>
+		</li>
+		<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li>
+	</ol>
+	<ol class="hashtags" ref="suggests" v-else-if="hashtags.length > 0">
+		<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
+			<span class="name">{{ hashtag }}</span>
+		</li>
+	</ol>
+	<ol class="emojis" ref="suggests" v-else-if="emojis.length > 0">
+		<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
+			<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
+			<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
+			<span class="emoji" v-else>{{ emoji.emoji }}</span>
+			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
+			<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
+		</li>
+	</ol>
+	<ol class="mfmTags" ref="suggests" v-else-if="mfmTags.length > 0">
+		<li v-for="tag in mfmTags" @click="complete(type, tag)" @keydown="onKeydown" tabindex="-1">
+			<span class="tag">{{ tag }}</span>
+		</li>
+	</ol>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import { emojilist } from '@/scripts/emojilist';
+import contains from '@/scripts/contains';
+import { twemojiSvgBase } from '@/scripts/twemoji-base';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+import { acct } from '@/filters/user';
+import * as os from '@/os';
+import { instance } from '@/instance';
+import { MFM_TAGS } from '@/scripts/mfm-tags';
+
+type EmojiDef = {
+	emoji: string;
+	name: string;
+	aliasOf?: string;
+	url?: string;
+	isCustomEmoji?: boolean;
+};
+
+const lib = emojilist.filter(x => x.category !== 'flags');
+
+const char2file = (char: string) => {
+	let codes = Array.from(char).map(x => x.codePointAt(0).toString(16));
+	if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
+	codes = codes.filter(x => x && x.length);
+	return codes.join('-');
+};
+
+const emjdb: EmojiDef[] = lib.map(x => ({
+	emoji: x.char,
+	name: x.name,
+	aliasOf: null,
+	url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
+}));
+
+for (const x of lib) {
+	if (x.keywords) {
+		for (const k of x.keywords) {
+			emjdb.push({
+				emoji: x.char,
+				name: k,
+				aliasOf: x.name,
+				url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
+			});
+		}
+	}
+}
+
+emjdb.sort((a, b) => a.name.length - b.name.length);
+
+//#region Construct Emoji DB
+const customEmojis = instance.emojis;
+const emojiDefinitions: EmojiDef[] = [];
+
+for (const x of customEmojis) {
+	emojiDefinitions.push({
+		name: x.name,
+		emoji: `:${x.name}:`,
+		url: x.url,
+		isCustomEmoji: true
+	});
+
+	if (x.aliases) {
+		for (const alias of x.aliases) {
+			emojiDefinitions.push({
+				name: alias,
+				aliasOf: x.name,
+				emoji: `:${x.name}:`,
+				url: x.url,
+				isCustomEmoji: true
+			});
+		}
+	}
+}
+
+emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
+
+const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
+//#endregion
+
+export default defineComponent({
+	props: {
+		type: {
+			type: String,
+			required: true,
+		},
+
+		q: {
+			type: String,
+			required: false,
+		},
+
+		textarea: {
+			type: HTMLTextAreaElement,
+			required: true,
+		},
+
+		close: {
+			type: Function,
+			required: true,
+		},
+
+		x: {
+			type: Number,
+			required: true,
+		},
+
+		y: {
+			type: Number,
+			required: true,
+		},
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			getStaticImageUrl,
+			fetching: true,
+			users: [],
+			hashtags: [],
+			emojis: [],
+			items: [],
+			mfmTags: [],
+			select: -1,
+		}
+	},
+
+	updated() {
+		this.setPosition();
+		this.items = (this.$refs.suggests as Element | undefined)?.children || [];
+	},
+
+	mounted() {
+		this.setPosition();
+
+		this.textarea.addEventListener('keydown', this.onKeydown);
+
+		for (const el of Array.from(document.querySelectorAll('body *'))) {
+			el.addEventListener('mousedown', this.onMousedown);
+		}
+
+		this.$nextTick(() => {
+			this.exec();
+
+			this.$watch('q', () => {
+				this.$nextTick(() => {
+					this.exec();
+				});
+			});
+		});
+	},
+
+	beforeUnmount() {
+		this.textarea.removeEventListener('keydown', this.onKeydown);
+
+		for (const el of Array.from(document.querySelectorAll('body *'))) {
+			el.removeEventListener('mousedown', this.onMousedown);
+		}
+	},
+
+	methods: {
+		complete(type, value) {
+			this.$emit('done', { type, value });
+			this.$emit('closed');
+
+			if (type === 'emoji') {
+				let recents = this.$store.state.recentlyUsedEmojis;
+				recents = recents.filter((e: any) => e !== value);
+				recents.unshift(value);
+				this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
+			}
+		},
+
+		setPosition() {
+			if (this.x + this.$el.offsetWidth > window.innerWidth) {
+				this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
+			} else {
+				this.$el.style.left = this.x + 'px';
+			}
+
+			if (this.y + this.$el.offsetHeight > window.innerHeight) {
+				this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px';
+				this.$el.style.marginTop = '0';
+			} else {
+				this.$el.style.top = this.y + 'px';
+				this.$el.style.marginTop = 'calc(1em + 8px)';
+			}
+		},
+
+		exec() {
+			this.select = -1;
+			if (this.$refs.suggests) {
+				for (const el of Array.from(this.items)) {
+					el.removeAttribute('data-selected');
+				}
+			}
+
+			if (this.type === 'user') {
+				if (this.q == null) {
+					this.users = [];
+					this.fetching = false;
+					return;
+				}
+
+				const cacheKey = `autocomplete:user:${this.q}`;
+				const cache = sessionStorage.getItem(cacheKey);
+				if (cache) {
+					const users = JSON.parse(cache);
+					this.users = users;
+					this.fetching = false;
+				} else {
+					os.api('users/search-by-username-and-host', {
+						username: this.q,
+						limit: 10,
+						detail: false
+					}).then(users => {
+						this.users = users;
+						this.fetching = false;
+
+						// キャッシュ
+						sessionStorage.setItem(cacheKey, JSON.stringify(users));
+					});
+				}
+			} else if (this.type === 'hashtag') {
+				if (this.q == null || this.q == '') {
+					this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
+					this.fetching = false;
+				} else {
+					const cacheKey = `autocomplete:hashtag:${this.q}`;
+					const cache = sessionStorage.getItem(cacheKey);
+					if (cache) {
+						const hashtags = JSON.parse(cache);
+						this.hashtags = hashtags;
+						this.fetching = false;
+					} else {
+						os.api('hashtags/search', {
+							query: this.q,
+							limit: 30
+						}).then(hashtags => {
+							this.hashtags = hashtags;
+							this.fetching = false;
+
+							// キャッシュ
+							sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
+						});
+					}
+				}
+			} else if (this.type === 'emoji') {
+				if (this.q == null || this.q == '') {
+					// 最近使った絵文字をサジェスト
+					this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
+					return;
+				}
+
+				const matched = [];
+				const max = 30;
+
+				emojiDb.some(x => {
+					if (x.name.startsWith(this.q) && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+					return matched.length == max;
+				});
+				if (matched.length < max) {
+					emojiDb.some(x => {
+						if (x.name.startsWith(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+						return matched.length == max;
+					});
+				}
+				if (matched.length < max) {
+					emojiDb.some(x => {
+						if (x.name.includes(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+						return matched.length == max;
+					});
+				}
+
+				this.emojis = matched;
+			} else if (this.type === 'mfmTag') {
+				if (this.q == null || this.q == '') {
+					this.mfmTags = MFM_TAGS;
+					return;
+				}
+
+				this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
+			}
+		},
+
+		onMousedown(e) {
+			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
+		},
+
+		onKeydown(e) {
+			const cancel = () => {
+				e.preventDefault();
+				e.stopPropagation();
+			};
+
+			switch (e.which) {
+				case 10: // [ENTER]
+				case 13: // [ENTER]
+					if (this.select !== -1) {
+						cancel();
+						(this.items[this.select] as any).click();
+					} else {
+						this.close();
+					}
+					break;
+
+				case 27: // [ESC]
+					cancel();
+					this.close();
+					break;
+
+				case 38: // [↑]
+					if (this.select !== -1) {
+						cancel();
+						this.selectPrev();
+					} else {
+						this.close();
+					}
+					break;
+
+				case 9: // [TAB]
+				case 40: // [↓]
+					cancel();
+					this.selectNext();
+					break;
+
+				default:
+					e.stopPropagation();
+					this.textarea.focus();
+			}
+		},
+
+		selectNext() {
+			if (++this.select >= this.items.length) this.select = 0;
+			if (this.items.length === 0) this.select = -1;
+			this.applySelect();
+		},
+
+		selectPrev() {
+			if (--this.select < 0) this.select = this.items.length - 1;
+			this.applySelect();
+		},
+
+		applySelect() {
+			for (const el of Array.from(this.items)) {
+				el.removeAttribute('data-selected');
+			}
+
+			if (this.select !== -1) {
+				this.items[this.select].setAttribute('data-selected', 'true');
+				(this.items[this.select] as any).focus();
+			}
+		},
+
+		chooseUser() {
+			this.close();
+			os.selectUser().then(user => {
+				this.complete('user', user);
+				this.textarea.focus();
+			});
+		},
+
+		acct
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.swhvrteh {
+	position: fixed;
+	z-index: 65535;
+	max-width: 100%;
+	margin-top: calc(1em + 8px);
+	overflow: hidden;
+	transition: top 0.1s ease, left 0.1s ease;
+
+	> ol {
+		display: block;
+		margin: 0;
+		padding: 4px 0;
+		max-height: 190px;
+		max-width: 500px;
+		overflow: auto;
+		list-style: none;
+
+		> li {
+			display: flex;
+			align-items: center;
+			padding: 4px 12px;
+			white-space: nowrap;
+			overflow: hidden;
+			font-size: 0.9em;
+			cursor: default;
+
+			&, * {
+				user-select: none;
+			}
+
+			* {
+				overflow: hidden;
+				text-overflow: ellipsis;
+			}
+
+			&:hover {
+				background: var(--X3);
+			}
+
+			&[data-selected='true'] {
+				background: var(--accent);
+
+				&, * {
+					color: #fff !important;
+				}
+			}
+
+			&:active {
+				background: var(--accentDarken);
+
+				&, * {
+					color: #fff !important;
+				}
+			}
+		}
+	}
+
+	> .users > li {
+
+		.avatar {
+			min-width: 28px;
+			min-height: 28px;
+			max-width: 28px;
+			max-height: 28px;
+			margin: 0 8px 0 0;
+			border-radius: 100%;
+		}
+
+		.name {
+			margin: 0 8px 0 0;
+		}
+	}
+
+	> .emojis > li {
+
+		.emoji {
+			display: inline-block;
+			margin: 0 4px 0 0;
+			width: 24px;
+
+			> img {
+				width: 24px;
+				vertical-align: bottom;
+			}
+		}
+
+		.alias {
+			margin: 0 0 0 8px;
+		}
+	}
+
+	> .mfmTags > li {
+
+		.name {
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/avatars.vue b/packages/client/src/components/avatars.vue
new file mode 100644
index 0000000000..e843d26daa
--- /dev/null
+++ b/packages/client/src/components/avatars.vue
@@ -0,0 +1,30 @@
+<template>
+<div>
+	<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
+		<MkAvatar :user="user" style="width:32px;height:32px;" :show-indicator="true"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		userIds: {
+			required: true
+		},
+	},
+	data() {
+		return {
+			us: []
+		};
+	},
+	async created() {
+		this.us = await os.api('users/show', {
+			userIds: this.userIds
+		});
+	}
+});
+</script>
diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue
new file mode 100644
index 0000000000..baa922506e
--- /dev/null
+++ b/packages/client/src/components/captcha.vue
@@ -0,0 +1,123 @@
+<template>
+<div>
+	<span v-if="!available">{{ $ts.waiting }}<MkEllipsis/></span>
+	<div ref="captcha"></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+
+type Captcha = {
+	render(container: string | Node, options: {
+		readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
+	}): string;
+	remove(id: string): void;
+	execute(id: string): void;
+	reset(id: string): void;
+	getResponse(id: string): string;
+};
+
+type CaptchaProvider = 'hcaptcha' | 'recaptcha';
+
+type CaptchaContainer = {
+	readonly [_ in CaptchaProvider]?: Captcha;
+};
+
+declare global {
+	interface Window extends CaptchaContainer {
+	}
+}
+
+export default defineComponent({
+	props: {
+		provider: {
+			type: String as PropType<CaptchaProvider>,
+			required: true,
+		},
+		sitekey: {
+			type: String,
+			required: true,
+		},
+		modelValue: {
+			type: String,
+		},
+	},
+
+	data() {
+		return {
+			available: false,
+		};
+	},
+
+	computed: {
+		variable(): string {
+			switch (this.provider) {
+				case 'hcaptcha': return 'hcaptcha';
+				case 'recaptcha': return 'grecaptcha';
+			}
+		},
+		loaded(): boolean {
+			return !!window[this.variable];
+		},
+		src(): string {
+			const endpoint = ({
+				hcaptcha: 'https://hcaptcha.com/1',
+				recaptcha: 'https://www.recaptcha.net/recaptcha',
+			} as Record<CaptchaProvider, string>)[this.provider];
+
+			return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
+		},
+		captcha(): Captcha {
+			return window[this.variable] || {} as unknown as Captcha;
+		},
+	},
+
+	created() {
+		if (this.loaded) {
+			this.available = true;
+		} else {
+			(document.getElementById(this.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
+				async: true,
+				id: this.provider,
+				src: this.src,
+			})))
+				.addEventListener('load', () => this.available = true);
+		}
+	},
+
+	mounted() {
+		if (this.available) {
+			this.requestRender();
+		} else {
+			this.$watch('available', this.requestRender);
+		}
+	},
+
+	beforeUnmount() {
+		this.reset();
+	},
+
+	methods: {
+		reset() {
+			if (this.captcha?.reset) this.captcha.reset();
+		},
+		requestRender() {
+			if (this.captcha.render && this.$refs.captcha instanceof Element) {
+				this.captcha.render(this.$refs.captcha, {
+					sitekey: this.sitekey,
+					theme: this.$store.state.darkMode ? 'dark' : 'light',
+					callback: this.callback,
+					'expired-callback': this.callback,
+					'error-callback': this.callback,
+				});
+			} else {
+				setTimeout(this.requestRender.bind(this), 1);
+			}
+		},
+		callback(response?: string) {
+			this.$emit('update:modelValue', typeof response == 'string' ? response : null);
+		},
+	},
+});
+</script>
diff --git a/packages/client/src/components/channel-follow-button.vue b/packages/client/src/components/channel-follow-button.vue
new file mode 100644
index 0000000000..9af65325bb
--- /dev/null
+++ b/packages/client/src/components/channel-follow-button.vue
@@ -0,0 +1,140 @@
+<template>
+<button class="hdcaacmi _button"
+	:class="{ wait, active: isFollowing, full }"
+	@click="onClick"
+	:disabled="wait"
+>
+	<template v-if="!wait">
+		<template v-if="isFollowing">
+			<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
+		</template>
+		<template v-else>
+			<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
+		</template>
+	</template>
+	<template v-else>
+		<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+	</template>
+</button>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		channel: {
+			type: Object,
+			required: true
+		},
+		full: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+	},
+
+	data() {
+		return {
+			isFollowing: this.channel.isFollowing,
+			wait: false,
+		};
+	},
+
+	methods: {
+		async onClick() {
+			this.wait = true;
+
+			try {
+				if (this.isFollowing) {
+					await os.api('channels/unfollow', {
+						channelId: this.channel.id
+					});
+					this.isFollowing = false;
+				} else {
+					await os.api('channels/follow', {
+						channelId: this.channel.id
+					});
+					this.isFollowing = true;
+				}
+			} catch (e) {
+				console.error(e);
+			} finally {
+				this.wait = false;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hdcaacmi {
+	position: relative;
+	display: inline-block;
+	font-weight: bold;
+	color: var(--accent);
+	background: transparent;
+	border: solid 1px var(--accent);
+	padding: 0;
+	height: 31px;
+	font-size: 16px;
+	border-radius: 32px;
+	background: #fff;
+
+	&.full {
+		padding: 0 8px 0 12px;
+		font-size: 14px;
+	}
+
+	&:not(.full) {
+		width: 31px;
+	}
+
+	&:focus-visible {
+		&:after {
+			content: "";
+			pointer-events: none;
+			position: absolute;
+			top: -5px;
+			right: -5px;
+			bottom: -5px;
+			left: -5px;
+			border: 2px solid var(--focus);
+			border-radius: 32px;
+		}
+	}
+
+	&:hover {
+		//background: mix($primary, #fff, 20);
+	}
+
+	&:active {
+		//background: mix($primary, #fff, 40);
+	}
+
+	&.active {
+		color: #fff;
+		background: var(--accent);
+
+		&:hover {
+			background: var(--accentLighten);
+			border-color: var(--accentLighten);
+		}
+
+		&:active {
+			background: var(--accentDarken);
+			border-color: var(--accentDarken);
+		}
+	}
+
+	&.wait {
+		cursor: wait !important;
+		opacity: 0.7;
+	}
+
+	> span {
+		margin-right: 6px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/channel-preview.vue b/packages/client/src/components/channel-preview.vue
new file mode 100644
index 0000000000..eb00052a78
--- /dev/null
+++ b/packages/client/src/components/channel-preview.vue
@@ -0,0 +1,165 @@
+<template>
+<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
+	<div class="banner" :style="bannerStyle">
+		<div class="fade"></div>
+		<div class="name"><i class="fas fa-satellite-dish"></i> {{ channel.name }}</div>
+		<div class="status">
+			<div>
+				<i class="fas fa-users fa-fw"></i>
+				<I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;">
+					<template #n>
+						<b>{{ channel.usersCount }}</b>
+					</template>
+				</I18n>
+			</div>
+			<div>
+				<i class="fas fa-pencil-alt fa-fw"></i>
+				<I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;">
+					<template #n>
+						<b>{{ channel.notesCount }}</b>
+					</template>
+				</I18n>
+			</div>
+		</div>
+	</div>
+	<article v-if="channel.description">
+		<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
+	</article>
+	<footer>
+		<span v-if="channel.lastNotedAt">
+			{{ $ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
+		</span>
+	</footer>
+</MkA>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		channel: {
+			type: Object,
+			required: true
+		},
+	},
+
+	computed: {
+		bannerStyle() {
+			if (this.channel.bannerUrl) {
+				return { backgroundImage: `url(${this.channel.bannerUrl})` };
+			} else {
+				return { backgroundColor: '#4c5e6d' };
+			}
+		}
+	},
+
+	data() {
+		return {
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.eftoefju {
+	display: block;
+	overflow: hidden;
+	width: 100%;
+
+	&:hover {
+		text-decoration: none;
+	}
+
+	> .banner {
+		position: relative;
+		width: 100%;
+		height: 200px;
+		background-position: center;
+		background-size: cover;
+
+		> .fade {
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			width: 100%;
+			height: 64px;
+			background: linear-gradient(0deg, var(--panel), var(--X15));
+		}
+
+		> .name {
+			position: absolute;
+			top: 16px;
+			left: 16px;
+			padding: 12px 16px;
+			background: rgba(0, 0, 0, 0.7);
+			color: #fff;
+			font-size: 1.2em;
+		}
+
+		> .status {
+			position: absolute;
+			z-index: 1;
+			bottom: 16px;
+			right: 16px;
+			padding: 8px 12px;
+			font-size: 80%;
+			background: rgba(0, 0, 0, 0.7);
+			border-radius: 6px;
+			color: #fff;
+		}
+	}
+
+	> article {
+		padding: 16px;
+
+		> p {
+			margin: 0;
+			font-size: 1em;
+		}
+	}
+
+	> footer {
+		padding: 12px 16px;
+		border-top: solid 0.5px var(--divider);
+
+		> span {
+			opacity: 0.7;
+			font-size: 0.9em;
+		}
+	}
+
+	@media (max-width: 550px) {
+		font-size: 0.9em;
+
+		> .banner {
+			height: 80px;
+
+			> .status {
+				display: none;
+			}
+		}
+
+		> article {
+			padding: 12px;
+		}
+
+		> footer {
+			display: none;
+		}
+	}
+
+	@media (max-width: 500px) {
+		font-size: 0.8em;
+
+		> .banner {
+			height: 70px;
+		}
+
+		> article {
+			padding: 8px;
+		}
+	}
+}
+
+</style>
diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue
new file mode 100644
index 0000000000..c4d0eb85dd
--- /dev/null
+++ b/packages/client/src/components/chart.vue
@@ -0,0 +1,691 @@
+<template>
+<div class="cbbedffa">
+	<canvas ref="chartEl"></canvas>
+	<div v-if="fetching" class="fetching">
+		<MkLoading/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
+import {
+	Chart,
+	ArcElement,
+	LineElement,
+	BarElement,
+	PointElement,
+	BarController,
+	LineController,
+	CategoryScale,
+	LinearScale,
+	TimeScale,
+	Legend,
+	Title,
+	Tooltip,
+	SubTitle,
+	Filler,
+} from 'chart.js';
+import 'chartjs-adapter-date-fns';
+import { enUS } from 'date-fns/locale';
+import zoomPlugin from 'chartjs-plugin-zoom';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+
+Chart.register(
+	ArcElement,
+	LineElement,
+	BarElement,
+	PointElement,
+	BarController,
+	LineController,
+	CategoryScale,
+	LinearScale,
+	TimeScale,
+	Legend,
+	Title,
+	Tooltip,
+	SubTitle,
+	Filler,
+	zoomPlugin,
+);
+
+const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
+const negate = arr => arr.map(x => -x);
+const alpha = (hex, a) => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
+const getColor = (i) => {
+	return colors[i % colors.length];
+};
+
+export default defineComponent({
+	props: {
+		src: {
+			type: String,
+			required: true,
+		},
+		args: {
+			type: Object,
+			required: false,
+		},
+		limit: {
+			type: Number,
+			required: false,
+			default: 90
+		},
+		span: {
+			type: String as PropType<'hour' | 'day'>,
+			required: true,
+		},
+		detailed: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		stacked: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		aspectRatio: {
+			type: Number,
+			required: false,
+			default: null
+		},
+	},
+
+	setup(props) {
+		const now = new Date();
+		let chartInstance: Chart = null;
+		let data: {
+			series: {
+				name: string;
+				type: 'line' | 'area';
+				color?: string;
+				borderDash?: number[];
+				hidden?: boolean;
+				data: {
+					x: number;
+					y: number;
+				}[];
+			}[];
+		} = null;
+
+		const chartEl = ref<HTMLCanvasElement>(null);
+		const fetching = ref(true);
+
+		const getDate = (ago: number) => {
+			const y = now.getFullYear();
+			const m = now.getMonth();
+			const d = now.getDate();
+			const h = now.getHours();
+
+			return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
+		};
+
+		const format = (arr) => {
+			return arr.map((v, i) => ({
+				x: getDate(i).getTime(),
+				y: v
+			}));
+		};
+
+		const render = () => {
+			if (chartInstance) {
+				chartInstance.destroy();
+			}
+
+			const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+
+			// フォントカラー
+			Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+			chartInstance = new Chart(chartEl.value, {
+				type: 'line',
+				data: {
+					labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
+					datasets: data.series.map((x, i) => ({
+						parsing: false,
+						label: x.name,
+						data: x.data.slice().reverse(),
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: x.color ? x.color : getColor(i),
+						borderDash: x.borderDash || [],
+						borderJoinStyle: 'round',
+						backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
+						fill: x.type === 'area',
+						hidden: !!x.hidden,
+					})),
+				},
+				options: {
+					aspectRatio: props.aspectRatio || 2.5,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 8,
+						},
+					},
+					scales: {
+						x: {
+							type: 'time',
+							time: {
+								stepSize: 1,
+								unit: props.span === 'day' ? 'month' : 'day',
+							},
+							grid: {
+								color: gridColor,
+								borderColor: 'rgb(0, 0, 0, 0)',
+							},
+							ticks: {
+								display: props.detailed,
+							},
+							adapters: {
+								date: {
+									locale: enUS,
+								},
+							},
+							min: getDate(props.limit).getTime(),
+						},
+						y: {
+							position: 'left',
+							stacked: props.stacked,
+							grid: {
+								color: gridColor,
+								borderColor: 'rgb(0, 0, 0, 0)',
+							},
+							ticks: {
+								display: props.detailed,
+							},
+						},
+					},
+					interaction: {
+						intersect: false,
+					},
+					plugins: {
+						legend: {
+							display: props.detailed,
+							position: 'bottom',
+							labels: {
+								boxWidth: 16,
+							},
+						},
+						tooltip: {
+							mode: 'index',
+							animation: {
+								duration: 0,
+							},
+						},
+						zoom: {
+							pan: {
+								enabled: true,
+							},
+							zoom: {
+								wheel: {
+									enabled: true,
+								},
+								pinch: {
+									enabled: true,
+								},
+								drag: {
+									enabled: false,
+								},
+								mode: 'x',
+							},
+							limits: {
+								x: {
+									min: 'original',
+									max: 'original',
+								},
+								y: {
+									min: 'original',
+									max: 'original',
+								},
+							}
+						},
+					},
+				},
+			});
+		};
+
+		const exportData = () => {
+			// TODO
+		};
+
+		const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
+			const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Instances',
+					type: 'area',
+					data: format(total
+						? raw.instance.total
+						: sum(raw.instance.inc, negate(raw.instance.dec))
+					),
+				}],
+			};
+		};
+
+		const fetchNotesChart = async (type: string): Promise<typeof data> => {
+			const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'All',
+					type: 'line',
+					borderDash: [5, 5],
+					data: format(type == 'combined'
+						? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
+						: sum(raw[type].inc, negate(raw[type].dec))
+					),
+				}, {
+					name: 'Renotes',
+					type: 'area',
+					data: format(type == 'combined'
+						? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
+						: raw[type].diffs.renote
+					),
+				}, {
+					name: 'Replies',
+					type: 'area',
+					data: format(type == 'combined'
+						? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
+						: raw[type].diffs.reply
+					),
+				}, {
+					name: 'Normal',
+					type: 'area',
+					data: format(type == 'combined'
+						? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
+						: raw[type].diffs.normal
+					),
+				}],
+			};
+		};
+
+		const fetchNotesTotalChart = async (): Promise<typeof data> => {
+			const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Combined',
+					type: 'line',
+					data: format(sum(raw.local.total, raw.remote.total)),
+				}, {
+					name: 'Local',
+					type: 'area',
+					data: format(raw.local.total),
+				}, {
+					name: 'Remote',
+					type: 'area',
+					data: format(raw.remote.total),
+				}],
+			};
+		};
+
+		const fetchUsersChart = async (total: boolean): Promise<typeof data> => {
+			const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Combined',
+					type: 'line',
+					data: format(total
+						? sum(raw.local.total, raw.remote.total)
+						: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
+					),
+				}, {
+					name: 'Local',
+					type: 'area',
+					data: format(total
+						? raw.local.total
+						: sum(raw.local.inc, negate(raw.local.dec))
+					),
+				}, {
+					name: 'Remote',
+					type: 'area',
+					data: format(total
+						? raw.remote.total
+						: sum(raw.remote.inc, negate(raw.remote.dec))
+					),
+				}],
+			};
+		};
+
+		const fetchActiveUsersChart = async (): Promise<typeof data> => {
+			const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Combined',
+					type: 'line',
+					data: format(sum(raw.local.users, raw.remote.users)),
+				}, {
+					name: 'Local',
+					type: 'area',
+					data: format(raw.local.users),
+				}, {
+					name: 'Remote',
+					type: 'area',
+					data: format(raw.remote.users),
+				}],
+			};
+		};
+
+		const fetchDriveChart = async (): Promise<typeof data> => {
+			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+			return {
+				bytes: true,
+				series: [{
+					name: 'All',
+					type: 'line',
+					borderDash: [5, 5],
+					data: format(
+						sum(
+							raw.local.incSize,
+							negate(raw.local.decSize),
+							raw.remote.incSize,
+							negate(raw.remote.decSize)
+						)
+					),
+				}, {
+					name: 'Local +',
+					type: 'area',
+					data: format(raw.local.incSize),
+				}, {
+					name: 'Local -',
+					type: 'area',
+					data: format(negate(raw.local.decSize)),
+				}, {
+					name: 'Remote +',
+					type: 'area',
+					data: format(raw.remote.incSize),
+				}, {
+					name: 'Remote -',
+					type: 'area',
+					data: format(negate(raw.remote.decSize)),
+				}],
+			};
+		};
+
+		const fetchDriveTotalChart = async (): Promise<typeof data> => {
+			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+			return {
+				bytes: true,
+				series: [{
+					name: 'Combined',
+					type: 'line',
+					data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
+				}, {
+					name: 'Local',
+					type: 'area',
+					data: format(raw.local.totalSize),
+				}, {
+					name: 'Remote',
+					type: 'area',
+					data: format(raw.remote.totalSize),
+				}],
+			};
+		};
+
+		const fetchDriveFilesChart = async (): Promise<typeof data> => {
+			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'All',
+					type: 'line',
+					borderDash: [5, 5],
+					data: format(
+						sum(
+							raw.local.incCount,
+							negate(raw.local.decCount),
+							raw.remote.incCount,
+							negate(raw.remote.decCount)
+						)
+					),
+				}, {
+					name: 'Local +',
+					type: 'area',
+					data: format(raw.local.incCount),
+				}, {
+					name: 'Local -',
+					type: 'area',
+					data: format(negate(raw.local.decCount)),
+				}, {
+					name: 'Remote +',
+					type: 'area',
+					data: format(raw.remote.incCount),
+				}, {
+					name: 'Remote -',
+					type: 'area',
+					data: format(negate(raw.remote.decCount)),
+				}],
+			};
+		};
+
+		const fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
+			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Combined',
+					type: 'line',
+					data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
+				}, {
+					name: 'Local',
+					type: 'area',
+					data: format(raw.local.totalCount),
+				}, {
+					name: 'Remote',
+					type: 'area',
+					data: format(raw.remote.totalCount),
+				}],
+			};
+		};
+
+		const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
+			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'In',
+					type: 'area',
+					color: '#008FFB',
+					data: format(raw.requests.received)
+				}, {
+					name: 'Out (succ)',
+					type: 'area',
+					color: '#00E396',
+					data: format(raw.requests.succeeded)
+				}, {
+					name: 'Out (fail)',
+					type: 'area',
+					color: '#FEB019',
+					data: format(raw.requests.failed)
+				}]
+			};
+		};
+
+		const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => {
+			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Users',
+					type: 'area',
+					color: '#008FFB',
+					data: format(total
+						? raw.users.total
+						: sum(raw.users.inc, negate(raw.users.dec))
+					)
+				}]
+			};
+		};
+
+		const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => {
+			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Notes',
+					type: 'area',
+					color: '#008FFB',
+					data: format(total
+						? raw.notes.total
+						: sum(raw.notes.inc, negate(raw.notes.dec))
+					)
+				}]
+			};
+		};
+
+		const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => {
+			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Following',
+					type: 'area',
+					color: '#008FFB',
+					data: format(total
+						? raw.following.total
+						: sum(raw.following.inc, negate(raw.following.dec))
+					)
+				}, {
+					name: 'Followers',
+					type: 'area',
+					color: '#00E396',
+					data: format(total
+						? raw.followers.total
+						: sum(raw.followers.inc, negate(raw.followers.dec))
+					)
+				}]
+			};
+		};
+
+		const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => {
+			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+			return {
+				bytes: true,
+				series: [{
+					name: 'Drive usage',
+					type: 'area',
+					color: '#008FFB',
+					data: format(total
+						? raw.drive.totalUsage
+						: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
+					)
+				}]
+			};
+		};
+
+		const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => {
+			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+			return {
+				series: [{
+					name: 'Drive files',
+					type: 'area',
+					color: '#008FFB',
+					data: format(total
+						? raw.drive.totalFiles
+						: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
+					)
+				}]
+			};
+		};
+
+		const fetchPerUserNotesChart = async (): Promise<typeof data> => {
+			const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
+			return {
+				series: [...(props.args.withoutAll ? [] : [{
+					name: 'All',
+					type: 'line',
+					borderDash: [5, 5],
+					data: format(sum(raw.inc, negate(raw.dec))),
+				}]), {
+					name: 'Renotes',
+					type: 'area',
+					data: format(raw.diffs.renote),
+				}, {
+					name: 'Replies',
+					type: 'area',
+					data: format(raw.diffs.reply),
+				}, {
+					name: 'Normal',
+					type: 'area',
+					data: format(raw.diffs.normal),
+				}],
+			};
+		};
+
+		const fetchAndRender = async () => {
+			const fetchData = () => {
+				switch (props.src) {
+					case 'federation-instances': return fetchFederationInstancesChart(false);
+					case 'federation-instances-total': return fetchFederationInstancesChart(true);
+					case 'users': return fetchUsersChart(false);
+					case 'users-total': return fetchUsersChart(true);
+					case 'active-users': return fetchActiveUsersChart();
+					case 'notes': return fetchNotesChart('combined');
+					case 'local-notes': return fetchNotesChart('local');
+					case 'remote-notes': return fetchNotesChart('remote');
+					case 'notes-total': return fetchNotesTotalChart();
+					case 'drive': return fetchDriveChart();
+					case 'drive-total': return fetchDriveTotalChart();
+					case 'drive-files': return fetchDriveFilesChart();
+					case 'drive-files-total': return fetchDriveFilesTotalChart();
+					
+					case 'instance-requests': return fetchInstanceRequestsChart();
+					case 'instance-users': return fetchInstanceUsersChart(false);
+					case 'instance-users-total': return fetchInstanceUsersChart(true);
+					case 'instance-notes': return fetchInstanceNotesChart(false);
+					case 'instance-notes-total': return fetchInstanceNotesChart(true);
+					case 'instance-ff': return fetchInstanceFfChart(false);
+					case 'instance-ff-total': return fetchInstanceFfChart(true);
+					case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false);
+					case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true);
+					case 'instance-drive-files': return fetchInstanceDriveFilesChart(false);
+					case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
+
+					case 'per-user-notes': return fetchPerUserNotesChart();
+				}
+			};
+			fetching.value = true;
+			data = await fetchData();
+			fetching.value = false;
+			render();
+		};
+
+		watch(() => [props.src, props.span], fetchAndRender);
+
+		onMounted(() => {
+			fetchAndRender();
+		});
+
+		return {
+			chartEl,
+			fetching,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.cbbedffa {
+	position: relative;
+
+	> .fetching {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		-webkit-backdrop-filter: var(--blur, blur(12px));
+		backdrop-filter: var(--blur, blur(12px));
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		cursor: wait;
+	}
+}
+</style>
diff --git a/packages/client/src/components/code-core.vue b/packages/client/src/components/code-core.vue
new file mode 100644
index 0000000000..9cff7b4448
--- /dev/null
+++ b/packages/client/src/components/code-core.vue
@@ -0,0 +1,35 @@
+<template>
+<code v-if="inline" v-html="html" :class="`language-${prismLang}`"></code>
+<pre v-else :class="`language-${prismLang}`"><code v-html="html" :class="`language-${prismLang}`"></code></pre>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import 'prismjs';
+import 'prismjs/themes/prism-okaidia.css';
+
+export default defineComponent({
+	props: {
+		code: {
+			type: String,
+			required: true
+		},
+		lang: {
+			type: String,
+			required: false
+		},
+		inline: {
+			type: Boolean,
+			required: false
+		}
+	},
+	computed: {
+		prismLang() {
+			return Prism.languages[this.lang] ? this.lang : 'js';
+		},
+		html() {
+			return Prism.highlight(this.code, Prism.languages[this.prismLang], this.prismLang);
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/code.vue b/packages/client/src/components/code.vue
new file mode 100644
index 0000000000..f5d6c5673a
--- /dev/null
+++ b/packages/client/src/components/code.vue
@@ -0,0 +1,27 @@
+<template>
+<XCode :code="code" :lang="lang" :inline="inline"/>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+
+export default defineComponent({
+	components: {
+		XCode: defineAsyncComponent(() => import('./code-core.vue'))
+	},
+	props: {
+		code: {
+			type: String,
+			required: true
+		},
+		lang: {
+			type: String,
+			required: false
+		},
+		inline: {
+			type: Boolean,
+			required: false
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue
new file mode 100644
index 0000000000..4bec7b511e
--- /dev/null
+++ b/packages/client/src/components/cw-button.vue
@@ -0,0 +1,70 @@
+<template>
+<button class="nrvgflfu _button" @click="toggle">
+	<b>{{ modelValue ? $ts._cw.hide : $ts._cw.show }}</b>
+	<span v-if="!modelValue">{{ label }}</span>
+</button>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { length } from 'stringz';
+import { concat } from '@/scripts/array';
+
+export default defineComponent({
+	props: {
+		modelValue: {
+			type: Boolean,
+			required: true
+		},
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+
+	computed: {
+		label(): string {
+			return concat([
+				this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [],
+				this.note.files && this.note.files.length !== 0 ? [this.$t('_cw.files', { count: this.note.files.length }) ] : [],
+				this.note.poll != null ? [this.$ts.poll] : []
+			] as string[][]).join(' / ');
+		}
+	},
+
+	methods: {
+		length,
+
+		toggle() {
+			this.$emit('update:modelValue', !this.modelValue);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.nrvgflfu {
+	display: inline-block;
+	padding: 4px 8px;
+	font-size: 0.7em;
+	color: var(--cwFg);
+	background: var(--cwBg);
+	border-radius: 2px;
+
+	&:hover {
+		background: var(--cwHoverBg);
+	}
+
+	> span {
+		margin-left: 4px;
+
+		&:before {
+			content: '(';
+		}
+
+		&:after {
+			content: ')';
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/date-separated-list.vue b/packages/client/src/components/date-separated-list.vue
new file mode 100644
index 0000000000..1aea9fd353
--- /dev/null
+++ b/packages/client/src/components/date-separated-list.vue
@@ -0,0 +1,188 @@
+<script lang="ts">
+import { defineComponent, h, PropType, TransitionGroup } from 'vue';
+import MkAd from '@/components/global/ad.vue';
+
+export default defineComponent({
+	props: {
+		items: {
+			type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
+			required: true,
+		},
+		direction: {
+			type: String,
+			required: false,
+			default: 'down'
+		},
+		reversed: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		noGap: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		ad: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	methods: {
+		focus() {
+			this.$slots.default[0].elm.focus();
+		},
+
+		getDateText(time: string) {
+			const date = new Date(time).getDate();
+			const month = new Date(time).getMonth() + 1;
+			return this.$t('monthAndDay', {
+				month: month.toString(),
+				day: date.toString()
+			});
+		}
+	},
+
+	render() {
+		if (this.items.length === 0) return;
+
+		const renderChildren = () => this.items.map((item, i) => {
+			const el = this.$slots.default({
+				item: item
+			})[0];
+			if (el.key == null && item.id) el.key = item.id;
+
+			if (
+				i != this.items.length - 1 &&
+				new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
+			) {
+				const separator = h('div', {
+					class: 'separator',
+					key: item.id + ':separator',
+				}, h('p', {
+					class: 'date'
+				}, [
+					h('span', [
+						h('i', {
+							class: 'fas fa-angle-up icon',
+						}),
+						this.getDateText(item.createdAt)
+					]),
+					h('span', [
+						this.getDateText(this.items[i + 1].createdAt),
+						h('i', {
+							class: 'fas fa-angle-down icon',
+						})
+					])
+				]));
+
+				return [el, separator];
+			} else {
+				if (this.ad && item._shouldInsertAd_) {
+					return [h(MkAd, {
+						class: 'a', // advertiseの意(ブロッカー対策)
+						key: item.id + ':ad',
+						prefer: ['horizontal', 'horizontal-big'],
+					}), el];
+				} else {
+					return el;
+				}
+			}
+		});
+
+		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? {
+			class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''),
+			name: 'list',
+			tag: 'div',
+			'data-direction': this.direction,
+			'data-reversed': this.reversed ? 'true' : 'false',
+		} : {
+			class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''),
+		}, {
+			default: renderChildren
+		});
+	},
+});
+</script>
+
+<style lang="scss">
+.sqadhkmv {
+	> *:empty {
+		display: none;
+	}
+
+	> *:not(:last-child) {
+		margin-bottom: var(--margin);
+	}
+
+	> .list-move {
+		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
+	}
+
+	> .list-enter-active {
+		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
+	}
+
+	&[data-direction="up"] {
+		> .list-enter-from {
+			opacity: 0;
+			transform: translateY(64px);
+		}
+	}
+
+	&[data-direction="down"] {
+		> .list-enter-from {
+			opacity: 0;
+			transform: translateY(-64px);
+		}
+	}
+
+	> .separator {
+		text-align: center;
+
+		> .date {
+			display: inline-block;
+			position: relative;
+			margin: 0;
+			padding: 0 16px;
+			line-height: 32px;
+			text-align: center;
+			font-size: 12px;
+			color: var(--dateLabelFg);
+
+			> span {
+				&:first-child {
+					margin-right: 8px;
+
+					> .icon {
+						margin-right: 8px;
+					}
+				}
+
+				&:last-child {
+					margin-left: 8px;
+
+					> .icon {
+						margin-left: 8px;
+					}
+				}
+			}
+		}
+	}
+
+	&.noGap {
+		> * {
+			margin: 0 !important;
+			border: none;
+			border-radius: 0;
+			box-shadow: none;
+
+			&:not(:last-child) {
+				border-bottom: solid 0.5px var(--divider);
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/base.vue b/packages/client/src/components/debobigego/base.vue
new file mode 100644
index 0000000000..f551a3478b
--- /dev/null
+++ b/packages/client/src/components/debobigego/base.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="rbusrurv" :class="{ wide: forceWide }" v-size="{ max: [400] }">
+	<slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		forceWide: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rbusrurv {
+	// 他のCSSからも参照されるので消さないように
+	--debobigegoXPadding: 32px;
+	--debobigegoYPadding: 32px;
+
+	--debobigegoContentHMargin: 16px;
+
+	font-size: 95%;
+	line-height: 1.3em;
+	background: var(--bg);
+	padding: var(--debobigegoYPadding) var(--debobigegoXPadding);
+	max-width: 750px;
+	margin: 0 auto;
+
+	&:not(.wide).max-width_400px {
+		--debobigegoXPadding: 0px;
+
+		> ::v-deep(*) {
+			._debobigegoPanel {
+				border: solid 0.5px var(--divider);
+				border-radius: 0;
+				border-left: none;
+				border-right: none;
+			}
+
+			._debobigego_group {
+				> *:not(._debobigegoNoConcat) {
+					&:not(:last-child):not(._debobigegoNoConcatPrev) {
+						&._debobigegoPanel, ._debobigegoPanel {
+							border-bottom: solid 0.5px var(--divider);
+						}
+					}
+
+					&:not(:first-child):not(._debobigegoNoConcatNext) {
+						&._debobigegoPanel, ._debobigegoPanel {
+							border-top: none;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/button.vue b/packages/client/src/components/debobigego/button.vue
new file mode 100644
index 0000000000..b883e817a4
--- /dev/null
+++ b/packages/client/src/components/debobigego/button.vue
@@ -0,0 +1,81 @@
+<template>
+<div class="yzpgjkxe _debobigegoItem">
+	<div class="_debobigegoLabel"><slot name="label"></slot></div>
+	<button class="main _button _debobigegoPanel _debobigegoClickable" :class="{ center, primary, danger }">
+		<slot></slot>
+		<div class="suffix">
+			<slot name="suffix"></slot>
+			<div class="icon">
+				<slot name="suffixIcon"></slot>
+			</div>
+		</div>
+	</button>
+	<div class="_debobigegoCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './debobigego.scss';
+
+export default defineComponent({
+	props: {
+		primary: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		danger: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		disabled: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		center: {
+			type: Boolean,
+			required: false,
+			default: true,
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.yzpgjkxe {
+	> .main {
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		padding: 14px 16px;
+		text-align: left;
+		align-items: center;
+
+		&.center {
+			display: block;
+			text-align: center;
+		}
+
+		&.primary {
+			color: var(--accent);
+		}
+
+		&.danger {
+			color: #ff2a2a;
+		}
+
+		> .suffix {
+			display: inline-flex;
+			margin-left: auto;
+			opacity: 0.7;
+
+			> .icon {
+				margin-left: 1em;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/debobigego.scss b/packages/client/src/components/debobigego/debobigego.scss
new file mode 100644
index 0000000000..833b656b66
--- /dev/null
+++ b/packages/client/src/components/debobigego/debobigego.scss
@@ -0,0 +1,52 @@
+._debobigegoPanel {
+	background: var(--panel);
+	border-radius: var(--radius);
+	transition: background 0.2s ease;
+
+	&._debobigegoClickable {
+		&:hover {
+			//background: var(--panelHighlight);
+		}
+
+		&:active {
+			background: var(--panelHighlight);
+			transition: background 0s;
+		}
+	}
+}
+
+._debobigegoLabel,
+._debobigegoCaption {
+	font-size: 80%;
+	color: var(--fgTransparentWeak);
+
+	&:empty {
+		display: none;
+	}
+}
+
+._debobigegoLabel {
+	position: sticky;
+	top: var(--stickyTop, 0px);
+	z-index: 2;
+	margin: -8px calc(var(--debobigegoXPadding) * -1) 0 calc(var(--debobigegoXPadding) * -1);
+	padding: 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding)) 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding));
+	background: var(--X17);
+	-webkit-backdrop-filter: var(--blur, blur(10px));
+	backdrop-filter: var(--blur, blur(10px));
+}
+
+._themeChanging_ ._debobigegoLabel {
+	transition: none !important;
+	background: transparent;
+}
+
+._debobigegoCaption {
+	padding: 8px var(--debobigegoContentHMargin) 0 var(--debobigegoContentHMargin);
+}
+
+._debobigegoItem {
+	& + ._debobigegoItem {
+		margin-top: 24px;
+	}
+}
diff --git a/packages/client/src/components/debobigego/group.vue b/packages/client/src/components/debobigego/group.vue
new file mode 100644
index 0000000000..cba2c6ec94
--- /dev/null
+++ b/packages/client/src/components/debobigego/group.vue
@@ -0,0 +1,78 @@
+<template>
+<div class="vrtktovg _debobigegoItem _debobigegoNoConcat" v-size="{ max: [500] }" v-sticky-container>
+	<div class="_debobigegoLabel"><slot name="label"></slot></div>
+	<div class="main _debobigego_group" ref="child">
+		<slot></slot>
+	</div>
+	<div class="_debobigegoCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+
+export default defineComponent({
+	setup(props, context) {
+		const child = ref<HTMLElement | null>(null);
+
+		const scanChild = () => {
+			if (child.value == null) return;
+			const els = Array.from(child.value.children);
+			for (let i = 0; i < els.length; i++) {
+				const el = els[i];
+				if (el.classList.contains('_debobigegoNoConcat')) {
+					if (els[i - 1]) els[i - 1].classList.add('_debobigegoNoConcatPrev');
+					if (els[i + 1]) els[i + 1].classList.add('_debobigegoNoConcatNext');
+				}
+			}
+		};
+
+		onMounted(() => {
+			scanChild();
+
+			const observer = new MutationObserver(records => {
+				scanChild();
+			});
+
+			observer.observe(child.value, {
+				childList: true,
+				subtree: false,
+				attributes: false,
+				characterData: false,
+			});
+		});
+
+		return {
+			child
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vrtktovg {
+	> .main {
+		> ::v-deep(*):not(._debobigegoNoConcat) {
+			&:not(._debobigegoNoConcatNext) {
+				margin: 0;
+			}
+
+			&:not(:last-child):not(._debobigegoNoConcatPrev) {
+				&._debobigegoPanel, ._debobigegoPanel {
+					border-bottom: solid 0.5px var(--divider);
+					border-bottom-left-radius: 0;
+					border-bottom-right-radius: 0;
+				}
+			}
+
+			&:not(:first-child):not(._debobigegoNoConcatNext) {
+				&._debobigegoPanel, ._debobigegoPanel {
+					border-top: none;
+					border-top-left-radius: 0;
+					border-top-right-radius: 0;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/info.vue b/packages/client/src/components/debobigego/info.vue
new file mode 100644
index 0000000000..41afb03304
--- /dev/null
+++ b/packages/client/src/components/debobigego/info.vue
@@ -0,0 +1,47 @@
+<template>
+<div class="fzenkabp _debobigegoItem">
+	<div class="_debobigegoPanel" :class="{ warn }">
+		<i v-if="warn" class="fas fa-exclamation-triangle"></i>
+		<i v-else class="fas fa-info-circle"></i>
+		<slot></slot>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		warn: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	data() {
+		return {
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fzenkabp {
+	> div {
+		padding: 14px 16px;
+		font-size: 90%;
+		background: var(--infoBg);
+		color: var(--infoFg);
+
+		&.warn {
+			background: var(--infoWarnBg);
+			color: var(--infoWarnFg);
+		}
+
+		> i {
+			margin-right: 4px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/input.vue b/packages/client/src/components/debobigego/input.vue
new file mode 100644
index 0000000000..d113f04d27
--- /dev/null
+++ b/packages/client/src/components/debobigego/input.vue
@@ -0,0 +1,292 @@
+<template>
+<FormGroup class="_debobigegoItem">
+	<template #label><slot></slot></template>
+	<div class="ztzhwixg _debobigegoItem" :class="{ inline, disabled }">
+		<div class="icon" ref="icon"><slot name="icon"></slot></div>
+		<div class="input _debobigegoPanel">
+			<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+			<input ref="inputEl"
+				:type="type"
+				v-model="v"
+				:disabled="disabled"
+				:required="required"
+				:readonly="readonly"
+				:placeholder="placeholder"
+				:pattern="pattern"
+				:autocomplete="autocomplete"
+				:spellcheck="spellcheck"
+				:step="step"
+				@focus="focused = true"
+				@blur="focused = false"
+				@keydown="onKeydown($event)"
+				@input="onInput"
+				:list="id"
+			>
+			<datalist :id="id" v-if="datalist">
+				<option v-for="data in datalist" :value="data"/>
+			</datalist>
+			<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
+		</div>
+	</div>
+	<template #caption><slot name="desc"></slot></template>
+
+	<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+</FormGroup>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import './debobigego.scss';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
+
+export default defineComponent({
+	components: {
+		FormGroup,
+		FormButton,
+	},
+	props: {
+		modelValue: {
+			required: false
+		},
+		type: {
+			type: String,
+			required: false
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		readonly: {
+			type: Boolean,
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			required: false
+		},
+		pattern: {
+			type: String,
+			required: false
+		},
+		placeholder: {
+			type: String,
+			required: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		autocomplete: {
+			required: false
+		},
+		spellcheck: {
+			required: false
+		},
+		step: {
+			required: false
+		},
+		datalist: {
+			type: Array,
+			required: false,
+		},
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		manualSave: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+	setup(props, context) {
+		const { modelValue, type, autofocus } = toRefs(props);
+		const v = ref(modelValue.value);
+		const id = Math.random().toString(); // TODO: uuid?
+		const focused = ref(false);
+		const changed = ref(false);
+		const invalid = ref(false);
+		const filled = computed(() => v.value !== '' && v.value != null);
+		const inputEl = ref(null);
+		const prefixEl = ref(null);
+		const suffixEl = ref(null);
+
+		const focus = () => inputEl.value.focus();
+		const onInput = (ev) => {
+			changed.value = true;
+			context.emit('change', ev);
+		};
+		const onKeydown = (ev: KeyboardEvent) => {
+			context.emit('keydown', ev);
+
+			if (ev.code === 'Enter') {
+				context.emit('enter');
+			}
+		};
+
+		const updated = () => {
+			changed.value = false;
+			if (type?.value === 'number') {
+				context.emit('update:modelValue', parseFloat(v.value));
+			} else {
+				context.emit('update:modelValue', v.value);
+			}
+		};
+
+		watch(modelValue.value, newValue => {
+			v.value = newValue;
+		});
+
+		watch(v, newValue => {
+			if (!props.manualSave) {
+				updated();
+			}
+
+			invalid.value = inputEl.value.validity.badInput;
+		});
+
+		onMounted(() => {
+			nextTick(() => {
+				if (autofocus.value) {
+					focus();
+				}
+
+				// このコンポーネントが作成された時、非表示状態である場合がある
+				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
+				const clock = setInterval(() => {
+					if (prefixEl.value) {
+						if (prefixEl.value.offsetWidth) {
+							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
+						}
+					}
+					if (suffixEl.value) {
+						if (suffixEl.value.offsetWidth) {
+							inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
+						}
+					}
+				}, 100);
+
+				onUnmounted(() => {
+					clearInterval(clock);
+				});
+			});
+		});
+
+		return {
+			id,
+			v,
+			focused,
+			invalid,
+			changed,
+			filled,
+			inputEl,
+			prefixEl,
+			suffixEl,
+			focus,
+			onInput,
+			onKeydown,
+			updated,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.ztzhwixg {
+	position: relative;
+
+	> .icon {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 24px;
+		text-align: center;
+		line-height: 32px;
+
+		&:not(:empty) + .input {
+			margin-left: 28px;
+		}
+	}
+
+	> .input {
+		$height: 48px;
+		position: relative;
+
+		> input {
+			display: block;
+			height: $height;
+			width: 100%;
+			margin: 0;
+			padding: 0 16px;
+			font: inherit;
+			font-weight: normal;
+			font-size: 1em;
+			line-height: $height;
+			color: var(--inputText);
+			background: transparent;
+			border: none;
+			border-radius: 0;
+			outline: none;
+			box-shadow: none;
+			box-sizing: border-box;
+
+			&[type='file'] {
+				display: none;
+			}
+		}
+
+		> .prefix,
+		> .suffix {
+			display: block;
+			position: absolute;
+			z-index: 1;
+			top: 0;
+			padding: 0 16px;
+			font-size: 1em;
+			line-height: $height;
+			color: var(--inputLabel);
+			pointer-events: none;
+
+			&:empty {
+				display: none;
+			}
+
+			> * {
+				display: inline-block;
+				min-width: 16px;
+				max-width: 150px;
+				overflow: hidden;
+				white-space: nowrap;
+				text-overflow: ellipsis;
+			}
+		}
+
+		> .prefix {
+			left: 0;
+			padding-right: 8px;
+		}
+
+		> .suffix {
+			right: 0;
+			padding-left: 8px;
+		}
+	}
+
+	&.inline {
+		display: inline-block;
+		margin: 0;
+	}
+
+	&.disabled {
+		opacity: 0.7;
+
+		&, * {
+			cursor: not-allowed !important;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/key-value-view.vue b/packages/client/src/components/debobigego/key-value-view.vue
new file mode 100644
index 0000000000..0e034a2d54
--- /dev/null
+++ b/packages/client/src/components/debobigego/key-value-view.vue
@@ -0,0 +1,38 @@
+<template>
+<div class="_debobigegoItem">
+	<div class="_debobigegoPanel anocepby">
+		<span class="key"><slot name="key"></slot></span>
+		<span class="value"><slot name="value"></slot></span>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './debobigego.scss';
+
+export default defineComponent({
+
+});
+</script>
+
+<style lang="scss" scoped>
+.anocepby {
+	display: flex;
+	align-items: center;
+	padding: 14px var(--debobigegoContentHMargin);
+
+	> .key {
+		margin-right: 12px;
+		white-space: nowrap;
+	}
+
+	> .value {
+		margin-left: auto;
+		opacity: 0.7;
+		text-overflow: ellipsis;
+		white-space: nowrap;
+		overflow: hidden;
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/link.vue b/packages/client/src/components/debobigego/link.vue
new file mode 100644
index 0000000000..885579eadf
--- /dev/null
+++ b/packages/client/src/components/debobigego/link.vue
@@ -0,0 +1,103 @@
+<template>
+<div class="qmfkfnzi _debobigegoItem">
+	<a class="main _button _debobigegoPanel _debobigegoClickable" :href="to" target="_blank" v-if="external">
+		<span class="icon"><slot name="icon"></slot></span>
+		<span class="text"><slot></slot></span>
+		<span class="right">
+			<span class="text"><slot name="suffix"></slot></span>
+			<i class="fas fa-external-link-alt icon"></i>
+		</span>
+	</a>
+	<MkA class="main _button _debobigegoPanel _debobigegoClickable" :class="{ active }" :to="to" :behavior="behavior" v-else>
+		<span class="icon"><slot name="icon"></slot></span>
+		<span class="text"><slot></slot></span>
+		<span class="right">
+			<span class="text"><slot name="suffix"></slot></span>
+			<i class="fas fa-chevron-right icon"></i>
+		</span>
+	</MkA>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './debobigego.scss';
+
+export default defineComponent({
+	props: {
+		to: {
+			type: String,
+			required: true
+		},
+		active: {
+			type: Boolean,
+			required: false
+		},
+		external: {
+			type: Boolean,
+			required: false
+		},
+		behavior: {
+			type: String,
+			required: false,
+		},
+	},
+	data() {
+		return {
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qmfkfnzi {
+	> .main {
+		display: flex;
+		align-items: center;
+		width: 100%;
+		box-sizing: border-box;
+		padding: 14px 16px 14px 14px;
+
+		&:hover {
+			text-decoration: none;
+		}
+
+		&.active {
+			color: var(--accent);
+			background: var(--panelHighlight);
+		}
+
+		> .icon {
+			width: 32px;
+			margin-right: 2px;
+			flex-shrink: 0;
+			text-align: center;
+			opacity: 0.8;
+
+			&:empty {
+				display: none;
+
+				& + .text {
+					padding-left: 4px;
+				}
+			}
+		}
+
+		> .text {
+			white-space: nowrap;
+			text-overflow: ellipsis;
+			overflow: hidden;
+			padding-right: 12px;
+		}
+
+		> .right {
+			margin-left: auto;
+			opacity: 0.7;
+
+			> .text:not(:empty) {
+				margin-right: 0.75em;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/object-view.vue b/packages/client/src/components/debobigego/object-view.vue
new file mode 100644
index 0000000000..ea79daa915
--- /dev/null
+++ b/packages/client/src/components/debobigego/object-view.vue
@@ -0,0 +1,102 @@
+<template>
+<FormGroup class="_debobigegoItem">
+	<template #label><slot></slot></template>
+	<div class="drooglns _debobigegoItem" :class="{ tall }">
+		<div class="input _debobigegoPanel">
+			<textarea class="_monospace"
+				v-model="v"
+				readonly
+				:spellcheck="false"
+			></textarea>
+		</div>
+	</div>
+	<template #caption><slot name="desc"></slot></template>
+</FormGroup>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, toRefs, watch } from 'vue';
+import * as JSON5 from 'json5';
+import './debobigego.scss';
+import FormGroup from './group.vue';
+
+export default defineComponent({
+	components: {
+		FormGroup,
+	},
+	props: {
+		value: {
+			required: false
+		},
+		tall: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		pre: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		manualSave: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	setup(props, context) {
+		const { value } = toRefs(props);
+		const v = ref('');
+
+		watch(() => value, newValue => {
+			v.value = JSON5.stringify(newValue.value, null, '\t');
+		}, {
+			immediate: true
+		});
+
+		return {
+			v,
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.drooglns {
+	position: relative;
+
+	> .input {
+		position: relative;
+	
+		> textarea {
+			display: block;
+			width: 100%;
+			min-width: 100%;
+			max-width: 100%;
+			min-height: 130px;
+			margin: 0;
+			padding: 16px var(--debobigegoContentHMargin);
+			box-sizing: border-box;
+			font: inherit;
+			font-weight: normal;
+			font-size: 1em;
+			background: transparent;
+			border: none;
+			border-radius: 0;
+			outline: none;
+			box-shadow: none;
+			color: var(--fg);
+			tab-size: 2;
+			white-space: pre;
+		}
+	}
+
+	&.tall {
+		> .input {
+			> textarea {
+				min-height: 200px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/pagination.vue b/packages/client/src/components/debobigego/pagination.vue
new file mode 100644
index 0000000000..07012cb759
--- /dev/null
+++ b/packages/client/src/components/debobigego/pagination.vue
@@ -0,0 +1,42 @@
+<template>
+<FormGroup class="uljviswt _debobigegoItem">
+	<template #label><slot name="label"></slot></template>
+	<slot :items="items"></slot>
+	<div class="empty" v-if="empty" key="_empty_">
+		<slot name="empty"></slot>
+	</div>
+	<FormButton v-show="more" class="button" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
+		<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+		<template v-if="moreFetching"><MkLoading inline/></template>
+	</FormButton>
+</FormGroup>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
+import paging from '@/scripts/paging';
+
+export default defineComponent({
+	components: {
+		FormButton,
+		FormGroup,
+	},
+
+	mixins: [
+		paging({}),
+	],
+
+	props: {
+		pagination: {
+			required: true
+		},
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.uljviswt {
+}
+</style>
diff --git a/packages/client/src/components/debobigego/radios.vue b/packages/client/src/components/debobigego/radios.vue
new file mode 100644
index 0000000000..b4c5841337
--- /dev/null
+++ b/packages/client/src/components/debobigego/radios.vue
@@ -0,0 +1,112 @@
+<script lang="ts">
+import { defineComponent, h } from 'vue';
+import MkRadio from '@/components/form/radio.vue';
+import './debobigego.scss';
+
+export default defineComponent({
+	components: {
+		MkRadio
+	},
+	props: {
+		modelValue: {
+			required: false
+		},
+	},
+	data() {
+		return {
+			value: this.modelValue,
+		}
+	},
+	watch: {
+		modelValue() {
+			this.value = this.modelValue;
+		},
+		value() {
+			this.$emit('update:modelValue', this.value);
+		}
+	},
+	render() {
+		const label = this.$slots.desc();
+		let options = this.$slots.default();
+
+		// なぜかFragmentになることがあるため
+		if (options.length === 1 && options[0].props == null) options = options[0].children;
+
+		return h('div', {
+			class: 'cnklmpwm _debobigegoItem'
+		}, [
+			h('div', {
+				class: '_debobigegoLabel',
+			}, label),
+			...options.map(option => h('button', {
+				class: '_button _debobigegoPanel _debobigegoClickable',
+				key: option.key,
+				onClick: () => this.value = option.props.value,
+			}, [h('span', {
+				class: ['check', { checked: this.value === option.props.value }],
+			}), option.children]))
+		]);
+	}
+});
+</script>
+
+<style lang="scss">
+.cnklmpwm {
+	> button {
+		display: block;
+		width: 100%;
+		box-sizing: border-box;
+		padding: 14px 18px;
+		text-align: left;
+
+		&:not(:first-of-type) {
+			border-top: none !important;
+			border-top-left-radius: 0;
+			border-top-right-radius: 0;
+		}
+
+		&:not(:last-of-type) {
+			border-bottom: solid 0.5px var(--divider);
+			border-bottom-left-radius: 0;
+			border-bottom-right-radius: 0;
+		}
+
+		> .check {
+			display: inline-block;
+			vertical-align: bottom;
+			position: relative;
+			width: 16px;
+			height: 16px;
+			margin-right: 8px;
+			background: none;
+			border: 2px solid var(--inputBorder);
+			border-radius: 100%;
+			transition: inherit;
+
+			&:after {
+				content: "";
+				display: block;
+				position: absolute;
+				top: 3px;
+				right: 3px;
+				bottom: 3px;
+				left: 3px;
+				border-radius: 100%;
+				opacity: 0;
+				transform: scale(0);
+				transition: .4s cubic-bezier(.25,.8,.25,1);
+			}
+
+			&.checked {
+				border-color: var(--accent);
+
+				&:after {
+					background-color: var(--accent);
+					transform: scale(1);
+					opacity: 1;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/range.vue b/packages/client/src/components/debobigego/range.vue
new file mode 100644
index 0000000000..26fb0f37c6
--- /dev/null
+++ b/packages/client/src/components/debobigego/range.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="ifitouly _debobigegoItem" :class="{ focused, disabled }">
+	<div class="_debobigegoLabel"><slot name="label"></slot></div>
+	<div class="_debobigegoPanel main">
+		<input
+			type="range"
+			ref="input"
+			v-model="v"
+			:disabled="disabled"
+			:min="min"
+			:max="max"
+			:step="step"
+			@focus="focused = true"
+			@blur="focused = false"
+			@input="$emit('update:value', $event.target.value)"
+		/>
+	</div>
+	<div class="_debobigegoCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		value: {
+			type: Number,
+			required: false,
+			default: 0
+		},
+		disabled: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		min: {
+			type: Number,
+			required: false,
+			default: 0
+		},
+		max: {
+			type: Number,
+			required: false,
+			default: 100
+		},
+		step: {
+			type: Number,
+			required: false,
+			default: 1
+		},
+	},
+	data() {
+		return {
+			v: this.value,
+			focused: false
+		};
+	},
+	watch: {
+		value(v) {
+			this.v = parseFloat(v);
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.ifitouly {
+	position: relative;
+
+	> .main {
+		padding: 22px 16px;
+
+		> input {
+			display: block;
+			-webkit-appearance: none;
+			-moz-appearance: none;
+			appearance: none;
+			background: var(--X10);
+			height: 4px;
+			width: 100%;
+			box-sizing: border-box;
+			margin: 0;
+			outline: 0;
+			border: 0;
+			border-radius: 7px;
+
+			&.disabled {
+				opacity: 0.6;
+				cursor: not-allowed;
+			}
+
+			&::-webkit-slider-thumb {
+				-webkit-appearance: none;
+				appearance: none;
+				cursor: pointer;
+				width: 20px;
+				height: 20px;
+				display: block;
+				border-radius: 50%;
+				border: none;
+				background: var(--accent);
+				box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+				box-sizing: content-box;
+			}
+
+			&::-moz-range-thumb {
+				-moz-appearance: none;
+				appearance: none;
+				cursor: pointer;
+				width: 20px;
+				height: 20px;
+				display: block;
+				border-radius: 50%;
+				border: none;
+				background: var(--accent);
+				box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/select.vue b/packages/client/src/components/debobigego/select.vue
new file mode 100644
index 0000000000..7a31371afc
--- /dev/null
+++ b/packages/client/src/components/debobigego/select.vue
@@ -0,0 +1,145 @@
+<template>
+<div class="yrtfrpux _debobigegoItem" :class="{ disabled, inline }">
+	<div class="_debobigegoLabel"><slot name="label"></slot></div>
+	<div class="icon" ref="icon"><slot name="icon"></slot></div>
+	<div class="input _debobigegoPanel _debobigegoClickable" @click="focus">
+		<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
+		<select ref="input"
+			v-model="v"
+			:required="required"
+			:disabled="disabled"
+			@focus="focused = true"
+			@blur="focused = false"
+		>
+			<slot></slot>
+		</select>
+		<div class="suffix">
+			<i class="fas fa-chevron-down"></i>
+		</div>
+	</div>
+	<div class="_debobigegoCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './debobigego.scss';
+
+export default defineComponent({
+	props: {
+		modelValue: {
+			required: false
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			required: false
+		},
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	data() {
+		return {
+		};
+	},
+	computed: {
+		v: {
+			get() {
+				return this.modelValue;
+			},
+			set(v) {
+				this.$emit('update:modelValue', v);
+			}
+		},
+	},
+	methods: {
+		focus() {
+			this.$refs.input.focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.yrtfrpux {
+	position: relative;
+
+	> .icon {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 24px;
+		text-align: center;
+		line-height: 32px;
+
+		&:not(:empty) + .input {
+			margin-left: 28px;
+		}
+	}
+
+	> .input {
+		display: flex;
+		position: relative;
+
+		> select {
+			display: block;
+			flex: 1;
+			width: 100%;
+			padding: 0 16px;
+			font: inherit;
+			font-weight: normal;
+			font-size: 1em;
+			height: 48px;
+			background: none;
+			border: none;
+			border-radius: 0;
+			outline: none;
+			box-shadow: none;
+			appearance: none;
+			-webkit-appearance: none;
+			color: var(--fg);
+
+			option,
+			optgroup {
+				color: var(--fg);
+				background: var(--bg);
+			}
+		}
+
+		> .prefix,
+		> .suffix {
+			display: block;
+			align-self: center;
+			justify-self: center;
+			font-size: 1em;
+			line-height: 32px;
+			color: var(--inputLabel);
+			pointer-events: none;
+
+			&:empty {
+				display: none;
+			}
+
+			> * {
+				display: block;
+				min-width: 16px;
+			}
+		}
+
+		> .prefix {
+			padding-right: 4px;
+		}
+
+		> .suffix {
+			padding: 0 16px 0 0;
+			opacity: 0.7;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/suspense.vue b/packages/client/src/components/debobigego/suspense.vue
new file mode 100644
index 0000000000..b5ba63b4b5
--- /dev/null
+++ b/packages/client/src/components/debobigego/suspense.vue
@@ -0,0 +1,101 @@
+<template>
+<transition name="fade" mode="out-in">
+	<div class="_debobigegoItem" v-if="pending">
+		<div class="_debobigegoPanel">
+			<MkLoading/>
+		</div>
+	</div>
+	<div v-else-if="resolved" class="_debobigegoItem">
+		<slot :result="result"></slot>
+	</div>
+	<div class="_debobigegoItem" v-else>
+		<div class="_debobigegoPanel eiurkvay">
+			<div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
+			<MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
+		</div>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, ref, watch } from 'vue';
+import './debobigego.scss';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+
+	props: {
+		p: {
+			type: Function as PropType<() => Promise<any>>,
+			required: true,
+		}
+	},
+
+	setup(props, context) {
+		const pending = ref(true);
+		const resolved = ref(false);
+		const rejected = ref(false);
+		const result = ref(null);
+
+		const process = () => {
+			if (props.p == null) {
+				return;
+			}
+			const promise = props.p();
+			pending.value = true;
+			resolved.value = false;
+			rejected.value = false;
+			promise.then((_result) => {
+				pending.value = false;
+				resolved.value = true;
+				result.value = _result;
+			});
+			promise.catch(() => {
+				pending.value = false;
+				rejected.value = true;
+			});
+		};
+
+		watch(() => props.p, () => {
+			process();
+		}, {
+			immediate: true
+		});
+
+		const retry = () => {
+			process();
+		};
+
+		return {
+			pending,
+			resolved,
+			rejected,
+			result,
+			retry,
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.eiurkvay {
+	padding: 16px;
+	text-align: center;
+
+	> .retry {
+		margin-top: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/switch.vue b/packages/client/src/components/debobigego/switch.vue
new file mode 100644
index 0000000000..9a69e18302
--- /dev/null
+++ b/packages/client/src/components/debobigego/switch.vue
@@ -0,0 +1,132 @@
+<template>
+<div class="ijnpvmgr _debobigegoItem">
+	<div class="main _debobigegoPanel _debobigegoClickable"
+		:class="{ disabled, checked }"
+		:aria-checked="checked"
+		:aria-disabled="disabled"
+		@click.prevent="toggle"
+	>
+		<input
+			type="checkbox"
+			ref="input"
+			:disabled="disabled"
+			@keydown.enter="toggle"
+		>
+		<span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
+			<span class="handle"></span>
+		</span>
+		<span class="label">
+			<span><slot></slot></span>
+		</span>
+	</div>
+	<div class="_debobigegoCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './debobigego.scss';
+
+export default defineComponent({
+	props: {
+		modelValue: {
+			type: Boolean,
+			default: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		}
+	},
+	computed: {
+		checked(): boolean {
+			return this.modelValue;
+		}
+	},
+	methods: {
+		toggle() {
+			if (this.disabled) return;
+			this.$emit('update:modelValue', !this.checked);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ijnpvmgr {
+	> .main {
+		position: relative;
+		display: flex;
+		padding: 14px 16px;
+		cursor: pointer;
+
+		> * {
+			user-select: none;
+		}
+
+		> input {
+			position: absolute;
+			width: 0;
+			height: 0;
+			opacity: 0;
+			margin: 0;
+		}
+
+		> .button {
+			position: relative;
+			display: inline-block;
+			flex-shrink: 0;
+			margin: 0;
+			width: 34px;
+			height: 22px;
+			background: var(--switchBg);
+			outline: none;
+			border-radius: 999px;
+			transition: all 0.3s;
+			cursor: pointer;
+
+			> .handle {
+				position: absolute;
+				top: 0;
+				left: 3px;
+				bottom: 0;
+				margin: auto 0;
+				border-radius: 100%;
+				transition: background-color 0.3s, transform 0.3s;
+				width: 16px;
+				height: 16px;
+				background-color: #fff;
+				pointer-events: none;
+			}
+		}
+
+		> .label {
+			margin-left: 12px;
+			display: block;
+			transition: inherit;
+			color: var(--fg);
+
+			> span {
+				display: block;
+				line-height: 20px;
+				transition: inherit;
+			}
+		}
+
+		&.disabled {
+			opacity: 0.6;
+			cursor: not-allowed;
+		}
+
+		&.checked {
+			> .button {
+				background-color: var(--accent);
+
+				> .handle {
+					transform: translateX(12px);
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/textarea.vue b/packages/client/src/components/debobigego/textarea.vue
new file mode 100644
index 0000000000..64e8d47126
--- /dev/null
+++ b/packages/client/src/components/debobigego/textarea.vue
@@ -0,0 +1,161 @@
+<template>
+<FormGroup class="_debobigegoItem">
+	<template #label><slot></slot></template>
+	<div class="rivhosbp _debobigegoItem" :class="{ tall, pre }">
+		<div class="input _debobigegoPanel">
+			<textarea ref="input" :class="{ code, _monospace: code }"
+				v-model="v"
+				:required="required"
+				:readonly="readonly"
+				:pattern="pattern"
+				:autocomplete="autocomplete"
+				:spellcheck="!code"
+				@input="onInput"
+				@focus="focused = true"
+				@blur="focused = false"
+			></textarea>
+		</div>
+	</div>
+	<template #caption><slot name="desc"></slot></template>
+
+	<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+</FormGroup>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, toRefs, watch } from 'vue';
+import './debobigego.scss';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
+
+export default defineComponent({
+	components: {
+		FormGroup,
+		FormButton,
+	},
+	props: {
+		modelValue: {
+			required: false
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		readonly: {
+			type: Boolean,
+			required: false
+		},
+		pattern: {
+			type: String,
+			required: false
+		},
+		autocomplete: {
+			type: String,
+			required: false
+		},
+		code: {
+			type: Boolean,
+			required: false
+		},
+		tall: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		pre: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		manualSave: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	setup(props, context) {
+		const { modelValue } = toRefs(props);
+		const v = ref(modelValue.value);
+		const changed = ref(false);
+		const inputEl = ref(null);
+		const focus = () => inputEl.value.focus();
+		const onInput = (ev) => {
+			changed.value = true;
+			context.emit('change', ev);
+		};
+
+		const updated = () => {
+			changed.value = false;
+			context.emit('update:modelValue', v.value);
+		};
+
+		watch(modelValue.value, newValue => {
+			v.value = newValue;
+		});
+
+		watch(v, newValue => {
+			if (!props.manualSave) {
+				updated();
+			}
+		});
+		
+		return {
+			v,
+			updated,
+			changed,
+			focus,
+			onInput,
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rivhosbp {
+	position: relative;
+
+	> .input {
+		position: relative;
+	
+		> textarea {
+			display: block;
+			width: 100%;
+			min-width: 100%;
+			max-width: 100%;
+			min-height: 130px;
+			margin: 0;
+			padding: 16px;
+			box-sizing: border-box;
+			font: inherit;
+			font-weight: normal;
+			font-size: 1em;
+			background: transparent;
+			border: none;
+			border-radius: 0;
+			outline: none;
+			box-shadow: none;
+			color: var(--fg);
+
+			&.code {
+				tab-size: 2;
+			}
+		}
+	}
+
+	&.tall {
+		> .input {
+			> textarea {
+				min-height: 200px;
+			}
+		}
+	}
+
+	&.pre {
+		> .input {
+			> textarea {
+				white-space: pre;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/debobigego/tuple.vue b/packages/client/src/components/debobigego/tuple.vue
new file mode 100644
index 0000000000..8a4599fd64
--- /dev/null
+++ b/packages/client/src/components/debobigego/tuple.vue
@@ -0,0 +1,36 @@
+<template>
+<div class="wthhikgt _debobigegoItem" v-size="{ max: [500] }">
+	<slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+});
+</script>
+
+<style lang="scss" scoped>
+.wthhikgt {
+	position: relative;
+	display: flex;
+
+	> ::v-deep(*) {
+		flex: 1;
+		margin: 0;
+
+		&:not(:last-child) {
+			margin-right: 16px;
+		}
+	}
+
+	&.max-width_500px {
+		display: block;
+
+		> ::v-deep(*) {
+			margin: inherit;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue
new file mode 100644
index 0000000000..90086fd430
--- /dev/null
+++ b/packages/client/src/components/dialog.vue
@@ -0,0 +1,212 @@
+<template>
+<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
+	<div class="mk-dialog">
+		<div class="icon" v-if="icon">
+			<i :class="icon"></i>
+		</div>
+		<div class="icon" v-else-if="!input && !select" :class="type">
+			<i v-if="type === 'success'" class="fas fa-check"></i>
+			<i v-else-if="type === 'error'" class="fas fa-times-circle"></i>
+			<i v-else-if="type === 'warning'" class="fas fa-exclamation-triangle"></i>
+			<i v-else-if="type === 'info'" class="fas fa-info-circle"></i>
+			<i v-else-if="type === 'question'" class="fas fa-question-circle"></i>
+			<i v-else-if="type === 'waiting'" class="fas fa-spinner fa-pulse"></i>
+		</div>
+		<header v-if="title"><Mfm :text="title"/></header>
+		<div class="body" v-if="text"><Mfm :text="text"/></div>
+		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput>
+		<MkSelect v-if="select" v-model="selectedValue" autofocus>
+			<template v-if="select.items">
+				<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
+			</template>
+			<template v-else>
+				<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
+					<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
+				</optgroup>
+			</template>
+		</MkSelect>
+		<div class="buttons" v-if="(showOkButton || showCancelButton) && !actions">
+			<MkButton inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select">{{ (showCancelButton || input || select) ? $ts.ok : $ts.gotIt }}</MkButton>
+			<MkButton inline @click="cancel" v-if="showCancelButton || input || select">{{ $ts.cancel }}</MkButton>
+		</div>
+		<div class="buttons" v-if="actions">
+			<MkButton v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</MkButton>
+		</div>
+	</div>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from '@/components/ui/modal.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSelect from '@/components/form/select.vue';
+
+export default defineComponent({
+	components: {
+		MkModal,
+		MkButton,
+		MkInput,
+		MkSelect,
+	},
+
+	props: {
+		type: {
+			type: String,
+			required: false,
+			default: 'info'
+		},
+		title: {
+			type: String,
+			required: false
+		},
+		text: {
+			type: String,
+			required: false
+		},
+		input: {
+			required: false
+		},
+		select: {
+			required: false
+		},
+		icon: {
+			required: false
+		},
+		actions: {
+			required: false
+		},
+		showOkButton: {
+			type: Boolean,
+			default: true
+		},
+		showCancelButton: {
+			type: Boolean,
+			default: false
+		},
+		cancelableByBgClick: {
+			type: Boolean,
+			default: true
+		},
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			inputValue: this.input && this.input.default ? this.input.default : null,
+			selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
+		};
+	},
+
+	mounted() {
+		document.addEventListener('keydown', this.onKeydown);
+	},
+
+	beforeUnmount() {
+		document.removeEventListener('keydown', this.onKeydown);
+	},
+
+	methods: {
+		done(canceled, result?) {
+			this.$emit('done', { canceled, result });
+			this.$refs.modal.close();
+		},
+
+		async ok() {
+			if (!this.showOkButton) return;
+
+			const result =
+				this.input ? this.inputValue :
+				this.select ? this.selectedValue :
+				true;
+			this.done(false, result);
+		},
+
+		cancel() {
+			this.done(true);
+		},
+
+		onBgClick() {
+			if (this.cancelableByBgClick) {
+				this.cancel();
+			}
+		},
+
+		onKeydown(e) {
+			if (e.which === 27) { // ESC
+				this.cancel();
+			}
+		},
+
+		onInputKeydown(e) {
+			if (e.which === 13) { // Enter
+				e.preventDefault();
+				e.stopPropagation();
+				this.ok();
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-dialog {
+	position: relative;
+	padding: 32px;
+	min-width: 320px;
+	max-width: 480px;
+	box-sizing: border-box;
+	text-align: center;
+	background: var(--panel);
+	border-radius: var(--radius);
+
+	> .icon {
+		font-size: 32px;
+
+		&.success {
+			color: var(--success);
+		}
+
+		&.error {
+			color: var(--error);
+		}
+
+		&.warning {
+			color: var(--warn);
+		}
+
+		> * {
+			display: block;
+			margin: 0 auto;
+		}
+
+		& + header {
+			margin-top: 16px;
+		}
+	}
+
+	> header {
+		margin: 0 0 8px 0;
+		font-weight: bold;
+		font-size: 20px;
+
+		& + .body {
+			margin-top: 8px;
+		}
+	}
+
+	> .body {
+		margin: 16px 0 0 0;
+	}
+
+	> .buttons {
+		margin-top: 16px;
+
+		> * {
+			margin: 0 8px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue
new file mode 100644
index 0000000000..9b6a0c9a0d
--- /dev/null
+++ b/packages/client/src/components/drive-file-thumbnail.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="zdjebgpv" ref="thumbnail">
+	<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
+	<i v-else-if="is === 'image'" class="fas fa-file-image icon"></i>
+	<i v-else-if="is === 'video'" class="fas fa-file-video icon"></i>
+	<i v-else-if="is === 'audio' || is === 'midi'" class="fas fa-music icon"></i>
+	<i v-else-if="is === 'csv'" class="fas fa-file-csv icon"></i>
+	<i v-else-if="is === 'pdf'" class="fas fa-file-pdf icon"></i>
+	<i v-else-if="is === 'textfile'" class="fas fa-file-alt icon"></i>
+	<i v-else-if="is === 'archive'" class="fas fa-file-archive icon"></i>
+	<i v-else class="fas fa-file icon"></i>
+
+	<i v-if="isThumbnailAvailable && is === 'video'" class="fas fa-film icon-sub"></i>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
+import { ColdDeviceStorage } from '@/store';
+
+export default defineComponent({
+	components: {
+		ImgWithBlurhash
+	},
+	props: {
+		file: {
+			type: Object,
+			required: true
+		},
+		fit: {
+			type: String,
+			required: false,
+			default: 'cover'
+		},
+	},
+	data() {
+		return {
+			isContextmenuShowing: false,
+			isDragging: false,
+
+		};
+	},
+	computed: {
+		is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
+			if (this.file.type.startsWith('image/')) return 'image';
+			if (this.file.type.startsWith('video/')) return 'video';
+			if (this.file.type === 'audio/midi') return 'midi';
+			if (this.file.type.startsWith('audio/')) return 'audio';
+			if (this.file.type.endsWith('/csv')) return 'csv';
+			if (this.file.type.endsWith('/pdf')) return 'pdf';
+			if (this.file.type.startsWith('text/')) return 'textfile';
+			if ([
+					"application/zip",
+					"application/x-cpio",
+					"application/x-bzip",
+					"application/x-bzip2",
+					"application/java-archive",
+					"application/x-rar-compressed",
+					"application/x-tar",
+					"application/gzip",
+					"application/x-7z-compressed"
+				].some(e => e === this.file.type)) return 'archive';
+			return 'unknown';
+		},
+		isThumbnailAvailable(): boolean {
+			return this.file.thumbnailUrl
+				? (this.is === 'image' || this.is === 'video')
+				: false;
+		},
+	},
+	mounted() {
+		const audioTag = this.$refs.volumectrl as HTMLAudioElement;
+		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
+	},
+	methods: {
+		volumechange() {
+			const audioTag = this.$refs.volumectrl as HTMLAudioElement;
+			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.zdjebgpv {
+	position: relative;
+
+	> .icon-sub {
+		position: absolute;
+		width: 30%;
+		height: auto;
+		margin: 0;
+		right: 4%;
+		bottom: 4%;
+	}
+
+	> * {
+		margin: auto;
+	}
+
+	> .icon {
+		pointer-events: none;
+		height: 65%;
+		width: 65%;
+	}
+}
+</style>
diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue
new file mode 100644
index 0000000000..f9a4025452
--- /dev/null
+++ b/packages/client/src/components/drive-select-dialog.vue
@@ -0,0 +1,70 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="800"
+	:height="500"
+	:with-ok-button="true"
+	:ok-button-disabled="(type === 'file') && (selected.length === 0)"
+	@click="cancel()"
+	@close="cancel()"
+	@ok="ok()"
+	@closed="$emit('closed')"
+>
+	<template #header>
+		{{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }}
+		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
+	</template>
+	<XDrive :multiple="multiple" @changeSelection="onChangeSelection" @selected="ok()" :select="type"/>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XDrive from './drive.vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import number from '@/filters/number';
+
+export default defineComponent({
+	components: {
+		XDrive,
+		XModalWindow,
+	},
+
+	props: {
+		type: {
+			type: String,
+			required: false,
+			default: 'file'
+		},
+		multiple: {
+			type: Boolean,
+			default: false
+		}
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			selected: []
+		};
+	},
+
+	methods: {
+		ok() {
+			this.$emit('done', this.selected);
+			this.$refs.dialog.close();
+		},
+
+		cancel() {
+			this.$emit('done');
+			this.$refs.dialog.close();
+		},
+
+		onChangeSelection(xs) {
+			this.selected = xs;
+		},
+
+		number
+	}
+});
+</script>
diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue
new file mode 100644
index 0000000000..43f07ebe76
--- /dev/null
+++ b/packages/client/src/components/drive-window.vue
@@ -0,0 +1,44 @@
+<template>
+<XWindow ref="window"
+	:initial-width="800"
+	:initial-height="500"
+	:can-resize="true"
+	@closed="$emit('closed')"
+>
+	<template #header>
+		{{ $ts.drive }}
+	</template>
+	<XDrive :initial-folder="initialFolder"/>
+</XWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XDrive from './drive.vue';
+import XWindow from '@/components/ui/window.vue';
+
+export default defineComponent({
+	components: {
+		XDrive,
+		XWindow,
+	},
+
+	props: {
+		initialFolder: {
+			type: Object,
+			required: false
+		},
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+		};
+	},
+
+	methods: {
+
+	}
+});
+</script>
diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue
new file mode 100644
index 0000000000..86f4ee0f8d
--- /dev/null
+++ b/packages/client/src/components/drive.file.vue
@@ -0,0 +1,374 @@
+<template>
+<div class="ncvczrfv"
+	:class="{ isSelected }"
+	@click="onClick"
+	@contextmenu.stop="onContextmenu"
+	draggable="true"
+	@dragstart="onDragstart"
+	@dragend="onDragend"
+	:title="title"
+>
+	<div class="label" v-if="$i.avatarId == file.id">
+		<img src="/client-assets/label.svg"/>
+		<p>{{ $ts.avatar }}</p>
+	</div>
+	<div class="label" v-if="$i.bannerId == file.id">
+		<img src="/client-assets/label.svg"/>
+		<p>{{ $ts.banner }}</p>
+	</div>
+	<div class="label red" v-if="file.isSensitive">
+		<img src="/client-assets/label-red.svg"/>
+		<p>{{ $ts.nsfw }}</p>
+	</div>
+
+	<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+
+	<p class="name">
+		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
+		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
+	</p>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
+import bytes from '@/filters/bytes';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkDriveFileThumbnail
+	},
+
+	props: {
+		file: {
+			type: Object,
+			required: true,
+		},
+		isSelected: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		selectMode: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	emits: ['chosen'],
+
+	data() {
+		return {
+			isDragging: false
+		};
+	},
+
+	computed: {
+		// TODO: parentへの参照を無くす
+		browser(): any {
+			return this.$parent;
+		},
+		title(): string {
+			return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
+		}
+	},
+
+	methods: {
+		getMenu() {
+			return [{
+				text: this.$ts.rename,
+				icon: 'fas fa-i-cursor',
+				action: this.rename
+			}, {
+				text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
+				icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
+				action: this.toggleSensitive
+			}, {
+				text: this.$ts.describeFile,
+				icon: 'fas fa-i-cursor',
+				action: this.describe
+			}, null, {
+				text: this.$ts.copyUrl,
+				icon: 'fas fa-link',
+				action: this.copyUrl
+			}, {
+				type: 'a',
+				href: this.file.url,
+				target: '_blank',
+				text: this.$ts.download,
+				icon: 'fas fa-download',
+				download: this.file.name
+			}, null, {
+				text: this.$ts.delete,
+				icon: 'fas fa-trash-alt',
+				danger: true,
+				action: this.deleteFile
+			}];
+		},
+
+		onClick(ev) {
+			if (this.selectMode) {
+				this.$emit('chosen', this.file);
+			} else {
+				os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
+			}
+		},
+
+		onContextmenu(e) {
+			os.contextMenu(this.getMenu(), e);
+		},
+
+		onDragstart(e) {
+			e.dataTransfer.effectAllowed = 'move';
+			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
+			this.isDragging = true;
+
+			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
+			// (=あなたの子供が、ドラッグを開始しましたよ)
+			this.browser.isDragSource = true;
+		},
+
+		onDragend(e) {
+			this.isDragging = false;
+			this.browser.isDragSource = false;
+		},
+
+		rename() {
+			os.dialog({
+				title: this.$ts.renameFile,
+				input: {
+					placeholder: this.$ts.inputNewFileName,
+					default: this.file.name,
+					allowEmpty: false
+				}
+			}).then(({ canceled, result: name }) => {
+				if (canceled) return;
+				os.api('drive/files/update', {
+					fileId: this.file.id,
+					name: name
+				});
+			});
+		},
+
+		describe() {
+			os.popup(import('@/components/media-caption.vue'), {
+				title: this.$ts.describeFile,
+				input: {
+					placeholder: this.$ts.inputNewDescription,
+					default: this.file.comment !== null ? this.file.comment : '',
+				},
+				image: this.file
+			}, {
+				done: result => {
+					if (!result || result.canceled) return;
+					let comment = result.result;
+					os.api('drive/files/update', {
+						fileId: this.file.id,
+						comment: comment.length == 0 ? null : comment
+					});
+				}
+			}, 'closed');
+		},
+
+		toggleSensitive() {
+			os.api('drive/files/update', {
+				fileId: this.file.id,
+				isSensitive: !this.file.isSensitive
+			});
+		},
+
+		copyUrl() {
+			copyToClipboard(this.file.url);
+			os.success();
+		},
+
+		setAsAvatar() {
+			os.updateAvatar(this.file);
+		},
+
+		setAsBanner() {
+			os.updateBanner(this.file);
+		},
+
+		addApp() {
+			alert('not implemented yet');
+		},
+
+		async deleteFile() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			os.api('drive/files/delete', {
+				fileId: this.file.id
+			});
+		},
+
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ncvczrfv {
+	position: relative;
+	padding: 8px 0 0 0;
+	min-height: 180px;
+	border-radius: 4px;
+
+	&, * {
+		cursor: pointer;
+	}
+
+	> * {
+		pointer-events: none;
+	}
+
+	&:hover {
+		background: rgba(#000, 0.05);
+
+		> .label {
+			&:before,
+			&:after {
+				background: #0b65a5;
+			}
+
+			&.red {
+				&:before,
+				&:after {
+					background: #c12113;
+				}
+			}
+		}
+	}
+
+	&:active {
+		background: rgba(#000, 0.1);
+
+		> .label {
+			&:before,
+			&:after {
+				background: #0b588c;
+			}
+
+			&.red {
+				&:before,
+				&:after {
+					background: #ce2212;
+				}
+			}
+		}
+	}
+
+	&.isSelected {
+		background: var(--accent);
+
+		&:hover {
+			background: var(--accentLighten);
+		}
+
+		&:active {
+			background: var(--accentDarken);
+		}
+
+		> .label {
+			&:before,
+			&:after {
+				display: none;
+			}
+		}
+
+		> .name {
+			color: #fff;
+		}
+
+		> .thumbnail {
+			color: #fff;
+		}
+	}
+
+	> .label {
+		position: absolute;
+		top: 0;
+		left: 0;
+		pointer-events: none;
+
+		&:before,
+		&:after {
+			content: "";
+			display: block;
+			position: absolute;
+			z-index: 1;
+			background: #0c7ac9;
+		}
+
+		&:before {
+			top: 0;
+			left: 57px;
+			width: 28px;
+			height: 8px;
+		}
+
+		&:after {
+			top: 57px;
+			left: 0;
+			width: 8px;
+			height: 28px;
+		}
+
+		&.red {
+			&:before,
+			&:after {
+				background: #c12113;
+			}
+		}
+
+		> img {
+			position: absolute;
+			z-index: 2;
+			top: 0;
+			left: 0;
+		}
+
+		> p {
+			position: absolute;
+			z-index: 3;
+			top: 19px;
+			left: -28px;
+			width: 120px;
+			margin: 0;
+			text-align: center;
+			line-height: 28px;
+			color: #fff;
+			transform: rotate(-45deg);
+		}
+	}
+
+	> .thumbnail {
+		width: 110px;
+		height: 110px;
+		margin: auto;
+	}
+
+	> .name {
+		display: block;
+		margin: 4px 0 0 0;
+		font-size: 0.8em;
+		text-align: center;
+		word-break: break-all;
+		color: var(--fg);
+		overflow: hidden;
+
+		> .ext {
+			opacity: 0.5;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue
new file mode 100644
index 0000000000..91e27cc8a1
--- /dev/null
+++ b/packages/client/src/components/drive.folder.vue
@@ -0,0 +1,326 @@
+<template>
+<div class="rghtznwe"
+	:class="{ draghover }"
+	@click="onClick"
+	@contextmenu.stop="onContextmenu"
+	@mouseover="onMouseover"
+	@mouseout="onMouseout"
+	@dragover.prevent.stop="onDragover"
+	@dragenter.prevent="onDragenter"
+	@dragleave="onDragleave"
+	@drop.prevent.stop="onDrop"
+	draggable="true"
+	@dragstart="onDragstart"
+	@dragend="onDragend"
+	:title="title"
+>
+	<p class="name">
+		<template v-if="hover"><i class="fas fa-folder-open fa-fw"></i></template>
+		<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
+		{{ folder.name }}
+	</p>
+	<p class="upload" v-if="$store.state.uploadFolder == folder.id">
+		{{ $ts.uploadFolder }}
+	</p>
+	<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		folder: {
+			type: Object,
+			required: true,
+		},
+		isSelected: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		selectMode: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	emits: ['chosen'],
+
+	data() {
+		return {
+			hover: false,
+			draghover: false,
+			isDragging: false,
+		};
+	},
+
+	computed: {
+		browser(): any {
+			return this.$parent;
+		},
+		title(): string {
+			return this.folder.name;
+		}
+	},
+
+	methods: {
+		checkboxClicked(e) {
+			this.$emit('chosen', this.folder);
+		},
+
+		onClick() {
+			this.browser.move(this.folder);
+		},
+
+		onMouseover() {
+			this.hover = true;
+		},
+
+		onMouseout() {
+			this.hover = false
+		},
+
+		onDragover(e) {
+			// 自分自身がドラッグされている場合
+			if (this.isDragging) {
+				// 自分自身にはドロップさせない
+				e.dataTransfer.dropEffect = 'none';
+				return;
+			}
+
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+
+			if (isFile || isDriveFile || isDriveFolder) {
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			} else {
+				e.dataTransfer.dropEffect = 'none';
+			}
+		},
+
+		onDragenter() {
+			if (!this.isDragging) this.draghover = true;
+		},
+
+		onDragleave() {
+			this.draghover = false;
+		},
+
+		onDrop(e) {
+			this.draghover = false;
+
+			// ファイルだったら
+			if (e.dataTransfer.files.length > 0) {
+				for (const file of Array.from(e.dataTransfer.files)) {
+					this.browser.upload(file, this.folder);
+				}
+				return;
+			}
+
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.browser.removeFile(file.id);
+				os.api('drive/files/update', {
+					fileId: file.id,
+					folderId: this.folder.id
+				});
+			}
+			//#endregion
+
+			//#region ドライブのフォルダ
+			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+			if (driveFolder != null && driveFolder != '') {
+				const folder = JSON.parse(driveFolder);
+
+				// 移動先が自分自身ならreject
+				if (folder.id == this.folder.id) return;
+
+				this.browser.removeFolder(folder.id);
+				os.api('drive/folders/update', {
+					folderId: folder.id,
+					parentId: this.folder.id
+				}).then(() => {
+					// noop
+				}).catch(err => {
+					switch (err) {
+						case 'detected-circular-definition':
+							os.dialog({
+								title: this.$ts.unableToProcess,
+								text: this.$ts.circularReferenceFolder
+							});
+							break;
+						default:
+							os.dialog({
+								type: 'error',
+								text: this.$ts.somethingHappened
+							});
+					}
+				});
+			}
+			//#endregion
+		},
+
+		onDragstart(e) {
+			e.dataTransfer.effectAllowed = 'move';
+			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
+			this.isDragging = true;
+
+			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
+			// (=あなたの子供が、ドラッグを開始しましたよ)
+			this.browser.isDragSource = true;
+		},
+
+		onDragend() {
+			this.isDragging = false;
+			this.browser.isDragSource = false;
+		},
+
+		go() {
+			this.browser.move(this.folder.id);
+		},
+
+		newWindow() {
+			this.browser.newWindow(this.folder);
+		},
+
+		rename() {
+			os.dialog({
+				title: this.$ts.renameFolder,
+				input: {
+					placeholder: this.$ts.inputNewFolderName,
+					default: this.folder.name
+				}
+			}).then(({ canceled, result: name }) => {
+				if (canceled) return;
+				os.api('drive/folders/update', {
+					folderId: this.folder.id,
+					name: name
+				});
+			});
+		},
+
+		deleteFolder() {
+			os.api('drive/folders/delete', {
+				folderId: this.folder.id
+			}).then(() => {
+				if (this.$store.state.uploadFolder === this.folder.id) {
+					this.$store.set('uploadFolder', null);
+				}
+			}).catch(err => {
+				switch(err.id) {
+					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
+						os.dialog({
+							type: 'error',
+							title: this.$ts.unableToDelete,
+							text: this.$ts.hasChildFilesOrFolders
+						});
+						break;
+					default:
+						os.dialog({
+							type: 'error',
+							text: this.$ts.unableToDelete
+						});
+				}
+			});
+		},
+
+		setAsUploadFolder() {
+			this.$store.set('uploadFolder', this.folder.id);
+		},
+
+		onContextmenu(e) {
+			os.contextMenu([{
+				text: this.$ts.openInWindow,
+				icon: 'fas fa-window-restore',
+				action: () => {
+					os.popup(import('./drive-window.vue'), {
+						initialFolder: this.folder
+					}, {
+					}, 'closed');
+				}
+			}, null, {
+				text: this.$ts.rename,
+				icon: 'fas fa-i-cursor',
+				action: this.rename
+			}, null, {
+				text: this.$ts.delete,
+				icon: 'fas fa-trash-alt',
+				danger: true,
+				action: this.deleteFolder
+			}], e);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rghtznwe {
+	position: relative;
+	padding: 8px;
+	height: 64px;
+	background: var(--driveFolderBg);
+	border-radius: 4px;
+
+	&, * {
+		cursor: pointer;
+	}
+
+	*:not(.checkbox) {
+		pointer-events: none;
+	}
+
+	> .checkbox {
+		position: absolute;
+		bottom: 8px;
+		right: 8px;
+		width: 16px;
+		height: 16px;
+		background: #fff;
+		border: solid 1px #000;
+
+		&.checked {
+			background: var(--accent);
+		}
+	}
+
+	&.draghover {
+		&:after {
+			content: "";
+			pointer-events: none;
+			position: absolute;
+			top: -4px;
+			right: -4px;
+			bottom: -4px;
+			left: -4px;
+			border: 2px dashed var(--focus);
+			border-radius: 4px;
+		}
+	}
+
+	> .name {
+		margin: 0;
+		font-size: 0.9em;
+		color: var(--desktopDriveFolderFg);
+
+		> i {
+			margin-right: 4px;
+			margin-left: 2px;
+			text-align: left;
+		}
+	}
+
+	> .upload {
+		margin: 4px 4px;
+		font-size: 0.8em;
+		text-align: right;
+		color: var(--desktopDriveFolderFg);
+	}
+}
+</style>
diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue
new file mode 100644
index 0000000000..4f0e6ce0e9
--- /dev/null
+++ b/packages/client/src/components/drive.nav-folder.vue
@@ -0,0 +1,135 @@
+<template>
+<div class="drylbebk"
+	:class="{ draghover }"
+	@click="onClick"
+	@dragover.prevent.stop="onDragover"
+	@dragenter="onDragenter"
+	@dragleave="onDragleave"
+	@drop.stop="onDrop"
+>
+	<i v-if="folder == null" class="fas fa-cloud"></i>
+	<span>{{ folder == null ? $ts.drive : folder.name }}</span>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		folder: {
+			type: Object,
+			required: false,
+		}
+	},
+
+	data() {
+		return {
+			hover: false,
+			draghover: false,
+		};
+	},
+
+	computed: {
+		browser(): any {
+			return this.$parent;
+		}
+	},
+
+	methods: {
+		onClick() {
+			this.browser.move(this.folder);
+		},
+
+		onMouseover() {
+			this.hover = true;
+		},
+
+		onMouseout() {
+			this.hover = false;
+		},
+
+		onDragover(e) {
+			// このフォルダがルートかつカレントディレクトリならドロップ禁止
+			if (this.folder == null && this.browser.folder == null) {
+				e.dataTransfer.dropEffect = 'none';
+			}
+
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+
+			if (isFile || isDriveFile || isDriveFolder) {
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			} else {
+				e.dataTransfer.dropEffect = 'none';
+			}
+
+			return false;
+		},
+
+		onDragenter() {
+			if (this.folder || this.browser.folder) this.draghover = true;
+		},
+
+		onDragleave() {
+			if (this.folder || this.browser.folder) this.draghover = false;
+		},
+
+		onDrop(e) {
+			this.draghover = false;
+
+			// ファイルだったら
+			if (e.dataTransfer.files.length > 0) {
+				for (const file of Array.from(e.dataTransfer.files)) {
+					this.browser.upload(file, this.folder);
+				}
+				return;
+			}
+
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.browser.removeFile(file.id);
+				os.api('drive/files/update', {
+					fileId: file.id,
+					folderId: this.folder ? this.folder.id : null
+				});
+			}
+			//#endregion
+
+			//#region ドライブのフォルダ
+			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+			if (driveFolder != null && driveFolder != '') {
+				const folder = JSON.parse(driveFolder);
+				// 移動先が自分自身ならreject
+				if (this.folder && folder.id == this.folder.id) return;
+				this.browser.removeFolder(folder.id);
+				os.api('drive/folders/update', {
+					folderId: folder.id,
+					parentId: this.folder ? this.folder.id : null
+				});
+			}
+			//#endregion
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.drylbebk {
+	> * {
+		pointer-events: none;
+	}
+
+	&.draghover {
+		background: #eee;
+	}
+
+	> i {
+		margin-right: 4px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue
new file mode 100644
index 0000000000..2b72a0a1c6
--- /dev/null
+++ b/packages/client/src/components/drive.vue
@@ -0,0 +1,784 @@
+<template>
+<div class="yfudmmck">
+	<nav>
+		<div class="path" @contextmenu.prevent.stop="() => {}">
+			<XNavFolder :class="{ current: folder == null }"/>
+			<template v-for="f in hierarchyFolders">
+				<span class="separator"><i class="fas fa-angle-right"></i></span>
+				<XNavFolder :folder="f"/>
+			</template>
+			<span class="separator" v-if="folder != null"><i class="fas fa-angle-right"></i></span>
+			<span class="folder current" v-if="folder != null">{{ folder.name }}</span>
+		</div>
+		<button @click="showMenu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
+	</nav>
+	<div class="main" :class="{ uploading: uploadings.length > 0, fetching }"
+		ref="main"
+		@dragover.prevent.stop="onDragover"
+		@dragenter="onDragenter"
+		@dragleave="onDragleave"
+		@drop.prevent.stop="onDrop"
+		@contextmenu.stop="onContextmenu"
+	>
+		<div class="contents" ref="contents">
+			<div class="folders" ref="foldersContainer" v-show="folders.length > 0">
+				<XFolder v-for="(f, i) in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder" v-anim="i"/>
+				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
+				<div class="padding" v-for="(n, i) in 16" :key="i"></div>
+				<MkButton ref="moreFolders" v-if="moreFolders">{{ $ts.loadMore }}</MkButton>
+			</div>
+			<div class="files" ref="filesContainer" v-show="files.length > 0">
+				<XFile v-for="(file, i) in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile" v-anim="i"/>
+				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
+				<div class="padding" v-for="(n, i) in 16" :key="i"></div>
+				<MkButton ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $ts.loadMore }}</MkButton>
+			</div>
+			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
+				<p v-if="draghover">{{ $t('empty-draghover') }}</p>
+				<p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p>
+				<p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p>
+			</div>
+		</div>
+		<MkLoading v-if="fetching"/>
+	</div>
+	<div class="dropzone" v-if="draghover"></div>
+	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import XNavFolder from './drive.nav-folder.vue';
+import XFolder from './drive.folder.vue';
+import XFile from './drive.file.vue';
+import MkButton from './ui/button.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XNavFolder,
+		XFolder,
+		XFile,
+		MkButton,
+	},
+
+	props: {
+		initialFolder: {
+			type: Object,
+			required: false
+		},
+		type: {
+			type: String,
+			required: false,
+			default: undefined
+		},
+		multiple: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		select: {
+			type: String,
+			required: false,
+			default: null
+		}
+	},
+
+	emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],
+
+	data() {
+		return {
+			/**
+			 * 現在の階層(フォルダ)
+			 * * null でルートを表す
+			 */
+			folder: null,
+
+			files: [],
+			folders: [],
+			moreFiles: false,
+			moreFolders: false,
+			hierarchyFolders: [],
+			selectedFiles: [],
+			selectedFolders: [],
+			uploadings: os.uploads,
+			connection: null,
+
+			/**
+			 * ドロップされようとしているか
+			 */
+			draghover: false,
+
+			/**
+			 * 自信の所有するアイテムがドラッグをスタートさせたか
+			 * (自分自身の階層にドロップできないようにするためのフラグ)
+			 */
+			isDragSource: false,
+
+			fetching: true,
+
+			ilFilesObserver: new IntersectionObserver(
+				(entries) => entries.some((entry) => entry.isIntersecting)
+				&& !this.fetching && this.moreFiles &&
+					this.fetchMoreFiles()
+			),
+			moreFilesElement: null as Element,
+
+		};
+	},
+
+	watch: {
+		folder() {
+			this.$emit('cd', this.folder);
+		}
+	},
+
+	mounted() {
+		if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
+			this.$nextTick(() => {
+				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
+			});
+		}
+
+		this.connection = markRaw(os.stream.useChannel('drive'));
+
+		this.connection.on('fileCreated', this.onStreamDriveFileCreated);
+		this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
+		this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
+		this.connection.on('folderCreated', this.onStreamDriveFolderCreated);
+		this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated);
+		this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted);
+
+		if (this.initialFolder) {
+			this.move(this.initialFolder);
+		} else {
+			this.fetch();
+		}
+	},
+
+	activated() {
+		if (this.$store.state.enableInfiniteScroll) {
+			this.$nextTick(() => {
+				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
+			});
+		}
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+		this.ilFilesObserver.disconnect();
+	},
+
+	methods: {
+		onStreamDriveFileCreated(file) {
+			this.addFile(file, true);
+		},
+
+		onStreamDriveFileUpdated(file) {
+			const current = this.folder ? this.folder.id : null;
+			if (current != file.folderId) {
+				this.removeFile(file);
+			} else {
+				this.addFile(file, true);
+			}
+		},
+
+		onStreamDriveFileDeleted(fileId) {
+			this.removeFile(fileId);
+		},
+
+		onStreamDriveFolderCreated(folder) {
+			this.addFolder(folder, true);
+		},
+
+		onStreamDriveFolderUpdated(folder) {
+			const current = this.folder ? this.folder.id : null;
+			if (current != folder.parentId) {
+				this.removeFolder(folder);
+			} else {
+				this.addFolder(folder, true);
+			}
+		},
+
+		onStreamDriveFolderDeleted(folderId) {
+			this.removeFolder(folderId);
+		},
+
+		onDragover(e): any {
+			// ドラッグ元が自分自身の所有するアイテムだったら
+			if (this.isDragSource) {
+				// 自分自身にはドロップさせない
+				e.dataTransfer.dropEffect = 'none';
+				return;
+			}
+
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+
+			if (isFile || isDriveFile || isDriveFolder) {
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			} else {
+				e.dataTransfer.dropEffect = 'none';
+			}
+
+			return false;
+		},
+
+		onDragenter(e) {
+			if (!this.isDragSource) this.draghover = true;
+		},
+
+		onDragleave(e) {
+			this.draghover = false;
+		},
+
+		onDrop(e): any {
+			this.draghover = false;
+
+			// ドロップされてきたものがファイルだったら
+			if (e.dataTransfer.files.length > 0) {
+				for (const file of Array.from(e.dataTransfer.files)) {
+					this.upload(file, this.folder);
+				}
+				return;
+			}
+
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				if (this.files.some(f => f.id == file.id)) return;
+				this.removeFile(file.id);
+				os.api('drive/files/update', {
+					fileId: file.id,
+					folderId: this.folder ? this.folder.id : null
+				});
+			}
+			//#endregion
+
+			//#region ドライブのフォルダ
+			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+			if (driveFolder != null && driveFolder != '') {
+				const folder = JSON.parse(driveFolder);
+
+				// 移動先が自分自身ならreject
+				if (this.folder && folder.id == this.folder.id) return false;
+				if (this.folders.some(f => f.id == folder.id)) return false;
+				this.removeFolder(folder.id);
+				os.api('drive/folders/update', {
+					folderId: folder.id,
+					parentId: this.folder ? this.folder.id : null
+				}).then(() => {
+					// noop
+				}).catch(err => {
+					switch (err) {
+						case 'detected-circular-definition':
+							os.dialog({
+								title: this.$ts.unableToProcess,
+								text: this.$ts.circularReferenceFolder
+							});
+							break;
+						default:
+							os.dialog({
+								type: 'error',
+								text: this.$ts.somethingHappened
+							});
+					}
+				});
+			}
+			//#endregion
+		},
+
+		selectLocalFile() {
+			(this.$refs.fileInput as any).click();
+		},
+
+		urlUpload() {
+			os.dialog({
+				title: this.$ts.uploadFromUrl,
+				input: {
+					placeholder: this.$ts.uploadFromUrlDescription
+				}
+			}).then(({ canceled, result: url }) => {
+				if (canceled) return;
+				os.api('drive/files/upload-from-url', {
+					url: url,
+					folderId: this.folder ? this.folder.id : undefined
+				});
+
+				os.dialog({
+					title: this.$ts.uploadFromUrlRequested,
+					text: this.$ts.uploadFromUrlMayTakeTime
+				});
+			});
+		},
+
+		createFolder() {
+			os.dialog({
+				title: this.$ts.createFolder,
+				input: {
+					placeholder: this.$ts.folderName
+				}
+			}).then(({ canceled, result: name }) => {
+				if (canceled) return;
+				os.api('drive/folders/create', {
+					name: name,
+					parentId: this.folder ? this.folder.id : undefined
+				}).then(folder => {
+					this.addFolder(folder, true);
+				});
+			});
+		},
+
+		renameFolder(folder) {
+			os.dialog({
+				title: this.$ts.renameFolder,
+				input: {
+					placeholder: this.$ts.inputNewFolderName,
+					default: folder.name
+				}
+			}).then(({ canceled, result: name }) => {
+				if (canceled) return;
+				os.api('drive/folders/update', {
+					folderId: folder.id,
+					name: name
+				}).then(folder => {
+					// FIXME: 画面を更新するために自分自身に移動
+					this.move(folder);
+				});
+			});
+		},
+
+		deleteFolder(folder) {
+			os.api('drive/folders/delete', {
+				folderId: folder.id
+			}).then(() => {
+				// 削除時に親フォルダに移動
+				this.move(folder.parentId);
+			}).catch(err => {
+				switch(err.id) {
+					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
+						os.dialog({
+							type: 'error',
+							title: this.$ts.unableToDelete,
+							text: this.$ts.hasChildFilesOrFolders
+						});
+						break;
+					default:
+						os.dialog({
+							type: 'error',
+							text: this.$ts.unableToDelete
+						});
+					}
+			});
+		},
+
+		onChangeFileInput() {
+			for (const file of Array.from((this.$refs.fileInput as any).files)) {
+				this.upload(file, this.folder);
+			}
+		},
+
+		upload(file, folder) {
+			if (folder && typeof folder == 'object') folder = folder.id;
+			os.upload(file, folder).then(res => {
+				this.addFile(res, true);
+			});
+		},
+
+		chooseFile(file) {
+			const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
+			if (this.multiple) {
+				if (isAlreadySelected) {
+					this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
+				} else {
+					this.selectedFiles.push(file);
+				}
+				this.$emit('change-selection', this.selectedFiles);
+			} else {
+				if (isAlreadySelected) {
+					this.$emit('selected', file);
+				} else {
+					this.selectedFiles = [file];
+					this.$emit('change-selection', [file]);
+				}
+			}
+		},
+
+		chooseFolder(folder) {
+			const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id);
+			if (this.multiple) {
+				if (isAlreadySelected) {
+					this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id);
+				} else {
+					this.selectedFolders.push(folder);
+				}
+				this.$emit('change-selection', this.selectedFolders);
+			} else {
+				if (isAlreadySelected) {
+					this.$emit('selected', folder);
+				} else {
+					this.selectedFolders = [folder];
+					this.$emit('change-selection', [folder]);
+				}
+			}
+		},
+
+		move(target) {
+			if (target == null) {
+				this.goRoot();
+				return;
+			} else if (typeof target == 'object') {
+				target = target.id;
+			}
+
+			this.fetching = true;
+
+			os.api('drive/folders/show', {
+				folderId: target
+			}).then(folder => {
+				this.folder = folder;
+				this.hierarchyFolders = [];
+
+				const dive = folder => {
+					this.hierarchyFolders.unshift(folder);
+					if (folder.parent) dive(folder.parent);
+				};
+
+				if (folder.parent) dive(folder.parent);
+
+				this.$emit('open-folder', folder);
+				this.fetch();
+			});
+		},
+
+		addFolder(folder, unshift = false) {
+			const current = this.folder ? this.folder.id : null;
+			if (current != folder.parentId) return;
+
+			if (this.folders.some(f => f.id == folder.id)) {
+				const exist = this.folders.map(f => f.id).indexOf(folder.id);
+				this.folders[exist] = folder;
+				return;
+			}
+
+			if (unshift) {
+				this.folders.unshift(folder);
+			} else {
+				this.folders.push(folder);
+			}
+		},
+
+		addFile(file, unshift = false) {
+			const current = this.folder ? this.folder.id : null;
+			if (current != file.folderId) return;
+
+			if (this.files.some(f => f.id == file.id)) {
+				const exist = this.files.map(f => f.id).indexOf(file.id);
+				this.files[exist] = file;
+				return;
+			}
+
+			if (unshift) {
+				this.files.unshift(file);
+			} else {
+				this.files.push(file);
+			}
+		},
+
+		removeFolder(folder) {
+			if (typeof folder == 'object') folder = folder.id;
+			this.folders = this.folders.filter(f => f.id != folder);
+		},
+
+		removeFile(file) {
+			if (typeof file == 'object') file = file.id;
+			this.files = this.files.filter(f => f.id != file);
+		},
+
+		appendFile(file) {
+			this.addFile(file);
+		},
+
+		appendFolder(folder) {
+			this.addFolder(folder);
+		},
+
+		prependFile(file) {
+			this.addFile(file, true);
+		},
+
+		prependFolder(folder) {
+			this.addFolder(folder, true);
+		},
+
+		goRoot() {
+			// 既にrootにいるなら何もしない
+			if (this.folder == null) return;
+
+			this.folder = null;
+			this.hierarchyFolders = [];
+			this.$emit('move-root');
+			this.fetch();
+		},
+
+		fetch() {
+			this.folders = [];
+			this.files = [];
+			this.moreFolders = false;
+			this.moreFiles = false;
+			this.fetching = true;
+
+			let fetchedFolders = null;
+			let fetchedFiles = null;
+
+			const foldersMax = 30;
+			const filesMax = 30;
+
+			// フォルダ一覧取得
+			os.api('drive/folders', {
+				folderId: this.folder ? this.folder.id : null,
+				limit: foldersMax + 1
+			}).then(folders => {
+				if (folders.length == foldersMax + 1) {
+					this.moreFolders = true;
+					folders.pop();
+				}
+				fetchedFolders = folders;
+				complete();
+			});
+
+			// ファイル一覧取得
+			os.api('drive/files', {
+				folderId: this.folder ? this.folder.id : null,
+				type: this.type,
+				limit: filesMax + 1
+			}).then(files => {
+				if (files.length == filesMax + 1) {
+					this.moreFiles = true;
+					files.pop();
+				}
+				fetchedFiles = files;
+				complete();
+			});
+
+			let flag = false;
+			const complete = () => {
+				if (flag) {
+					for (const x of fetchedFolders) this.appendFolder(x);
+					for (const x of fetchedFiles) this.appendFile(x);
+					this.fetching = false;
+				} else {
+					flag = true;
+				}
+			};
+		},
+
+		fetchMoreFiles() {
+			this.fetching = true;
+
+			const max = 30;
+
+			// ファイル一覧取得
+			os.api('drive/files', {
+				folderId: this.folder ? this.folder.id : null,
+				type: this.type,
+				untilId: this.files[this.files.length - 1].id,
+				limit: max + 1
+			}).then(files => {
+				if (files.length == max + 1) {
+					this.moreFiles = true;
+					files.pop();
+				} else {
+					this.moreFiles = false;
+				}
+				for (const x of files) this.appendFile(x);
+				this.fetching = false;
+			});
+		},
+
+		getMenu() {
+			return [{
+				text: this.$ts.addFile,
+				type: 'label'
+			}, {
+				text: this.$ts.upload,
+				icon: 'fas fa-upload',
+				action: () => { this.selectLocalFile(); }
+			}, {
+				text: this.$ts.fromUrl,
+				icon: 'fas fa-link',
+				action: () => { this.urlUpload(); }
+			}, null, {
+				text: this.folder ? this.folder.name : this.$ts.drive,
+				type: 'label'
+			}, this.folder ? {
+				text: this.$ts.renameFolder,
+				icon: 'fas fa-i-cursor',
+				action: () => { this.renameFolder(this.folder); }
+			} : undefined, this.folder ? {
+				text: this.$ts.deleteFolder,
+				icon: 'fas fa-trash-alt',
+				action: () => { this.deleteFolder(this.folder); }
+			} : undefined, {
+				text: this.$ts.createFolder,
+				icon: 'fas fa-folder-plus',
+				action: () => { this.createFolder(); }
+			}];
+		},
+
+		showMenu(ev) {
+			os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
+		},
+
+		onContextmenu(ev) {
+			os.contextMenu(this.getMenu(), ev);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.yfudmmck {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+
+	> nav {
+		display: flex;
+		z-index: 2;
+		width: 100%;
+		padding: 0 8px;
+		box-sizing: border-box;
+		overflow: auto;
+		font-size: 0.9em;
+		box-shadow: 0 1px 0 var(--divider);
+
+		&, * {
+			user-select: none;
+		}
+
+		> .path {
+			display: inline-block;
+			vertical-align: bottom;
+			line-height: 38px;
+			white-space: nowrap;
+
+			> * {
+				display: inline-block;
+				margin: 0;
+				padding: 0 8px;
+				line-height: 38px;
+				cursor: pointer;
+
+				* {
+					pointer-events: none;
+				}
+
+				&:hover {
+					text-decoration: underline;
+				}
+
+				&.current {
+					font-weight: bold;
+					cursor: default;
+
+					&:hover {
+						text-decoration: none;
+					}
+				}
+
+				&.separator {
+					margin: 0;
+					padding: 0;
+					opacity: 0.5;
+					cursor: default;
+
+					> i {
+						margin: 0;
+					}
+				}
+			}
+		}
+
+		> .menu {
+			margin-left: auto;
+		}
+	}
+
+	> .main {
+		flex: 1;
+		overflow: auto;
+		padding: var(--margin);
+
+		&, * {
+			user-select: none;
+		}
+
+		&.fetching {
+			cursor: wait !important;
+
+			* {
+				pointer-events: none;
+			}
+
+			> .contents {
+				opacity: 0.5;
+			}
+		}
+
+		&.uploading {
+			height: calc(100% - 38px - 100px);
+		}
+
+		> .contents {
+
+			> .folders,
+			> .files {
+				display: flex;
+				flex-wrap: wrap;
+
+				> .folder,
+				> .file {
+					flex-grow: 1;
+					width: 128px;
+					margin: 4px;
+					box-sizing: border-box;
+				}
+
+				> .padding {
+					flex-grow: 1;
+					pointer-events: none;
+					width: 128px + 8px;
+				}
+			}
+
+			> .empty {
+				padding: 16px;
+				text-align: center;
+				pointer-events: none;
+				opacity: 0.5;
+
+				> p {
+					margin: 0;
+				}
+			}
+		}
+	}
+
+	> .dropzone {
+		position: absolute;
+		left: 0;
+		top: 38px;
+		width: 100%;
+		height: calc(100% - 38px);
+		border: dashed 2px var(--focus);
+		pointer-events: none;
+	}
+
+	> input {
+		display: none;
+	}
+}
+</style>
diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue
new file mode 100644
index 0000000000..1d48bbb8a3
--- /dev/null
+++ b/packages/client/src/components/emoji-picker-dialog.vue
@@ -0,0 +1,76 @@
+<template>
+<MkPopup ref="popup" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.popup.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')" #default="{point}">
+	<MkEmojiPicker class="ryghynhb _popup _shadow" :class="{ pointer: point === 'top' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker"/>
+</MkPopup>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import MkPopup from '@/components/ui/popup.vue';
+import MkEmojiPicker from '@/components/emoji-picker.vue';
+
+export default defineComponent({
+	components: {
+		MkPopup,
+		MkEmojiPicker,
+	},
+
+	props: {
+		manualShowing: {
+			type: Boolean,
+			required: false,
+			default: null,
+		},
+		src: {
+			required: false
+		},
+		showPinned: {
+			required: false,
+			default: true
+		},
+		asReactionPicker: {
+			required: false
+		},
+	},
+
+	emits: ['done', 'close', 'closed'],
+
+	data() {
+		return {
+
+		};
+	},
+
+	methods: {
+		chosen(emoji: any) {
+			this.$emit('done', emoji);
+			this.$refs.popup.close();
+		},
+
+		opening() {
+			this.$refs.picker.reset();
+			this.$refs.picker.focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ryghynhb {
+	&.pointer {
+		&:before {
+			--size: 8px;
+			content: '';
+			display: block;
+			position: absolute;
+			top: calc(0px - (var(--size) * 2));
+			left: 0;
+			right: 0;
+			width: 0;
+			margin: auto;
+			border: solid var(--size) transparent;
+			border-bottom-color: var(--popup);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/emoji-picker-window.vue b/packages/client/src/components/emoji-picker-window.vue
new file mode 100644
index 0000000000..0ffa0c1187
--- /dev/null
+++ b/packages/client/src/components/emoji-picker-window.vue
@@ -0,0 +1,197 @@
+<template>
+<MkWindow ref="window"
+	:initial-width="null"
+	:initial-height="null"
+	:can-resize="false"
+	:mini="true"
+	:front="true"
+	@closed="$emit('closed')"
+>
+	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
+</MkWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import MkWindow from '@/components/ui/window.vue';
+import MkEmojiPicker from '@/components/emoji-picker.vue';
+
+export default defineComponent({
+	components: {
+		MkWindow,
+		MkEmojiPicker,
+	},
+
+	props: {
+		src: {
+			required: false
+		},
+		showPinned: {
+			required: false,
+			default: true
+		},
+		asReactionPicker: {
+			required: false
+		},
+	},
+
+	emits: ['chosen', 'closed'],
+
+	data() {
+		return {
+
+		};
+	},
+
+	methods: {
+		chosen(emoji: any) {
+			this.$emit('chosen', emoji);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.omfetrab {
+	$pad: 8px;
+	--eachSize: 40px;
+
+	display: flex;
+	flex-direction: column;
+	contain: content;
+
+	&.big {
+		--eachSize: 44px;
+	}
+
+	&.w1 {
+		width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
+	}
+
+	&.w2 {
+		width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
+	}
+
+	&.w3 {
+		width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
+	}
+
+	&.h1 {
+		--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
+	}
+
+	&.h2 {
+		--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
+	}
+
+	&.h3 {
+		--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
+	}
+
+	> .search {
+		width: 100%;
+		padding: 12px;
+		box-sizing: border-box;
+		font-size: 1em;
+		outline: none;
+		border: none;
+		background: transparent;
+		color: var(--fg);
+
+		&:not(.filled) {
+			order: 1;
+			z-index: 2;
+			box-shadow: 0px -1px 0 0px var(--divider);
+		}
+	}
+
+	> .emojis {
+		height: var(--height);
+		overflow-y: auto;
+		overflow-x: hidden;
+
+		scrollbar-width: none;
+
+		&::-webkit-scrollbar {
+			display: none;
+		}
+
+		> .index {
+			min-height: var(--height);
+			position: relative;
+			border-bottom: solid 0.5px var(--divider);
+				
+			> .arrow {
+				position: absolute;
+				bottom: 0;
+				left: 0;
+				width: 100%;
+				padding: 16px 0;
+				text-align: center;
+				opacity: 0.5;
+				pointer-events: none;
+			}
+		}
+
+		section {
+			> header {
+				position: sticky;
+				top: 0;
+				left: 0;
+				z-index: 1;
+				padding: 8px;
+				font-size: 12px;
+			}
+
+			> div {
+				padding: $pad;
+
+				> button {
+					position: relative;
+					padding: 0;
+					width: var(--eachSize);
+					height: var(--eachSize);
+					border-radius: 4px;
+
+					&:focus-visible {
+						outline: solid 2px var(--focus);
+						z-index: 1;
+					}
+
+					&:hover {
+						background: rgba(0, 0, 0, 0.05);
+					}
+
+					&:active {
+						background: var(--accent);
+						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
+					}
+
+					> * {
+						font-size: 24px;
+						height: 1.25em;
+						vertical-align: -.25em;
+						pointer-events: none;
+					}
+				}
+			}
+
+			&.result {
+				border-bottom: solid 0.5px var(--divider);
+
+				&:empty {
+					display: none;
+				}
+			}
+
+			&.unicode {
+				min-height: 384px;
+			}
+
+			&.custom {
+				min-height: 64px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/emoji-picker.section.vue b/packages/client/src/components/emoji-picker.section.vue
new file mode 100644
index 0000000000..2401eca2a5
--- /dev/null
+++ b/packages/client/src/components/emoji-picker.section.vue
@@ -0,0 +1,50 @@
+<template>
+<section>
+	<header class="_acrylic" @click="shown = !shown">
+		<i class="toggle fa-fw" :class="shown ? 'fas fa-chevron-down' : 'fas fa-chevron-up'"></i> <slot></slot> ({{ emojis.length }})
+	</header>
+	<div v-if="shown">
+		<button v-for="emoji in emojis"
+			class="_button"
+			@click="chosen(emoji, $event)"
+			:key="emoji"
+		>
+			<MkEmoji :emoji="emoji" :normal="true"/>
+		</button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+
+export default defineComponent({
+	props: {
+		emojis: {
+			required: true,
+		},
+		initialShown: {
+			required: false
+		}
+	},
+
+	emits: ['chosen'],
+
+	data() {
+		return {
+			getStaticImageUrl,
+			shown: this.initialShown,
+		};
+	},
+
+	methods: {
+		chosen(emoji: any, ev) {
+			this.$parent.chosen(emoji, ev);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue
new file mode 100644
index 0000000000..015e201269
--- /dev/null
+++ b/packages/client/src/components/emoji-picker.vue
@@ -0,0 +1,501 @@
+<template>
+<div class="omfetrab" :class="['w' + width, 'h' + height, { big }]">
+	<input ref="search" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" v-model.trim="q" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
+	<div class="emojis" ref="emojis">
+		<section class="result">
+			<div v-if="searchResultCustom.length > 0">
+				<button v-for="emoji in searchResultCustom"
+					class="_button"
+					:title="emoji.name"
+					@click="chosen(emoji, $event)"
+					:key="emoji"
+					tabindex="0"
+				>
+					<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
+					<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+				</button>
+			</div>
+			<div v-if="searchResultUnicode.length > 0">
+				<button v-for="emoji in searchResultUnicode"
+					class="_button"
+					:title="emoji.name"
+					@click="chosen(emoji, $event)"
+					:key="emoji.name"
+					tabindex="0"
+				>
+					<MkEmoji :emoji="emoji.char"/>
+				</button>
+			</div>
+		</section>
+
+		<div class="index" v-if="tab === 'index'">
+			<section v-if="showPinned">
+				<div>
+					<button v-for="emoji in pinned"
+						class="_button"
+						@click="chosen(emoji, $event)"
+						tabindex="0"
+						:key="emoji"
+					>
+						<MkEmoji :emoji="emoji" :normal="true"/>
+					</button>
+				</div>
+			</section>
+
+			<section>
+				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header>
+				<div>
+					<button v-for="emoji in $store.state.recentlyUsedEmojis"
+						class="_button"
+						@click="chosen(emoji, $event)"
+						:key="emoji"
+					>
+						<MkEmoji :emoji="emoji" :normal="true"/>
+					</button>
+				</div>
+			</section>
+		</div>
+		<div>
+			<header class="_acrylic">{{ $ts.customEmojis }}</header>
+			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
+		</div>
+		<div>
+			<header class="_acrylic">{{ $ts.emoji }}</header>
+			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
+		</div>
+	</div>
+	<div class="tabs">
+		<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="fas fa-asterisk fa-fw"></i></button>
+		<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="fas fa-laugh fa-fw"></i></button>
+		<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="fas fa-leaf fa-fw"></i></button>
+		<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="fas fa-hashtag fa-fw"></i></button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import { emojilist } from '@/scripts/emojilist';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+import Particle from '@/components/particle.vue';
+import * as os from '@/os';
+import { isDeviceTouch } from '@/scripts/is-device-touch';
+import { isMobile } from '@/scripts/is-mobile';
+import { emojiCategories } from '@/instance';
+import XSection from './emoji-picker.section.vue';
+
+export default defineComponent({
+	components: {
+		XSection
+	},
+
+	props: {
+		showPinned: {
+			required: false,
+			default: true
+		},
+		asReactionPicker: {
+			required: false
+		},
+	},
+
+	emits: ['chosen'],
+
+	data() {
+		return {
+			emojilist: markRaw(emojilist),
+			getStaticImageUrl,
+			pinned: this.$store.reactiveState.reactions,
+			width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
+			height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
+			big: this.asReactionPicker ? isDeviceTouch : false,
+			customEmojiCategories: emojiCategories,
+			customEmojis: this.$instance.emojis,
+			q: null,
+			searchResultCustom: [],
+			searchResultUnicode: [],
+			tab: 'index',
+			categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
+		};
+	},
+
+	watch: {
+		q() {
+			this.$refs.emojis.scrollTop = 0;
+
+			if (this.q == null || this.q === '') {
+				this.searchResultCustom = [];
+				this.searchResultUnicode = [];
+				return;
+			}
+
+			const q = this.q.replace(/:/g, '');
+
+			const searchCustom = () => {
+				const max = 8;
+				const emojis = this.customEmojis;
+				const matches = new Set();
+
+				const exactMatch = emojis.find(e => e.name === q);
+				if (exactMatch) matches.add(exactMatch);
+
+				if (q.includes(' ')) { // AND検索
+					const keywords = q.split(' ');
+
+					// 名前にキーワードが含まれている
+					for (const emoji of emojis) {
+						if (keywords.every(keyword => emoji.name.includes(keyword))) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+					if (matches.size >= max) return matches;
+
+					// 名前またはエイリアスにキーワードが含まれている
+					for (const emoji of emojis) {
+						if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+				} else {
+					for (const emoji of emojis) {
+						if (emoji.name.startsWith(q)) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+					if (matches.size >= max) return matches;
+
+					for (const emoji of emojis) {
+						if (emoji.aliases.some(alias => alias.startsWith(q))) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+					if (matches.size >= max) return matches;
+
+					for (const emoji of emojis) {
+						if (emoji.name.includes(q)) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+					if (matches.size >= max) return matches;
+
+					for (const emoji of emojis) {
+						if (emoji.aliases.some(alias => alias.includes(q))) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+				}
+
+				return matches;
+			};
+
+			const searchUnicode = () => {
+				const max = 8;
+				const emojis = this.emojilist;
+				const matches = new Set();
+
+				const exactMatch = emojis.find(e => e.name === q);
+				if (exactMatch) matches.add(exactMatch);
+
+				if (q.includes(' ')) { // AND検索
+					const keywords = q.split(' ');
+
+					// 名前にキーワードが含まれている
+					for (const emoji of emojis) {
+						if (keywords.every(keyword => emoji.name.includes(keyword))) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+					if (matches.size >= max) return matches;
+
+					// 名前またはエイリアスにキーワードが含まれている
+					for (const emoji of emojis) {
+						if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+				} else {
+					for (const emoji of emojis) {
+						if (emoji.name.startsWith(q)) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+					if (matches.size >= max) return matches;
+
+					for (const emoji of emojis) {
+						if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+					if (matches.size >= max) return matches;
+
+					for (const emoji of emojis) {
+						if (emoji.name.includes(q)) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+					if (matches.size >= max) return matches;
+
+					for (const emoji of emojis) {
+						if (emoji.keywords.some(keyword => keyword.includes(q))) {
+							matches.add(emoji);
+							if (matches.size >= max) break;
+						}
+					}
+				}
+
+				return matches;
+			};
+
+			this.searchResultCustom = Array.from(searchCustom());
+			this.searchResultUnicode = Array.from(searchUnicode());
+		}
+	},
+
+	mounted() {
+		this.focus();
+	},
+
+	methods: {
+		focus() {
+			if (!isMobile && !isDeviceTouch) {
+				this.$refs.search.focus({
+					preventScroll: true
+				});
+			}
+		},
+
+		reset() {
+			this.$refs.emojis.scrollTop = 0;
+			this.q = '';
+		},
+
+		getKey(emoji: any) {
+			return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
+		},
+
+		chosen(emoji: any, ev) {
+			if (ev) {
+				const el = ev.currentTarget || ev.target;
+				const rect = el.getBoundingClientRect();
+				const x = rect.left + (el.clientWidth / 2);
+				const y = rect.top + (el.clientHeight / 2);
+				os.popup(Particle, { x, y }, {}, 'end');
+			}
+
+			const key = this.getKey(emoji);
+			this.$emit('chosen', key);
+
+			// 最近使った絵文字更新
+			if (!this.pinned.includes(key)) {
+				let recents = this.$store.state.recentlyUsedEmojis;
+				recents = recents.filter((e: any) => e !== key);
+				recents.unshift(key);
+				this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
+			}
+		},
+
+		paste(event) {
+			const paste = (event.clipboardData || window.clipboardData).getData('text');
+			if (this.done(paste)) {
+				event.preventDefault();
+			}
+		},
+
+		done(query) {
+			if (query == null) query = this.q;
+			if (query == null) return;
+			const q = query.replace(/:/g, '');
+			const exactMatchCustom = this.customEmojis.find(e => e.name === q);
+			if (exactMatchCustom) {
+				this.chosen(exactMatchCustom);
+				return true;
+			}
+			const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q);
+			if (exactMatchUnicode) {
+				this.chosen(exactMatchUnicode);
+				return true;
+			}
+			if (this.searchResultCustom.length > 0) {
+				this.chosen(this.searchResultCustom[0]);
+				return true;
+			}
+			if (this.searchResultUnicode.length > 0) {
+				this.chosen(this.searchResultUnicode[0]);
+				return true;
+			}
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.omfetrab {
+	$pad: 8px;
+	--eachSize: 40px;
+
+	display: flex;
+	flex-direction: column;
+
+	&.big {
+		--eachSize: 44px;
+	}
+
+	&.w1 {
+		width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
+	}
+
+	&.w2 {
+		width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
+	}
+
+	&.w3 {
+		width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
+	}
+
+	&.h1 {
+		--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
+	}
+
+	&.h2 {
+		--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
+	}
+
+	&.h3 {
+		--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
+	}
+
+	> .search {
+		width: 100%;
+		padding: 12px;
+		box-sizing: border-box;
+		font-size: 1em;
+		outline: none;
+		border: none;
+		background: transparent;
+		color: var(--fg);
+
+		&:not(.filled) {
+			order: 1;
+			z-index: 2;
+			box-shadow: 0px -1px 0 0px var(--divider);
+		}
+	}
+
+	> .tabs {
+		display: flex;
+		display: none;
+
+		> .tab {
+			flex: 1;
+			height: 38px;
+			border-top: solid 0.5px var(--divider);
+
+			&.active {
+				border-top: solid 1px var(--accent);
+				color: var(--accent);
+			}
+		}
+	}
+
+	> .emojis {
+		height: var(--height);
+		overflow-y: auto;
+		overflow-x: hidden;
+
+		scrollbar-width: none;
+
+		&::-webkit-scrollbar {
+			display: none;
+		}
+
+		> div {
+			&:not(.index) {
+				padding: 4px 0 8px 0;
+				border-top: solid 0.5px var(--divider);
+			}
+
+			> header {
+				/*position: sticky;
+				top: 0;
+				left: 0;*/
+				height: 32px;
+				line-height: 32px;
+				z-index: 2;
+				padding: 0 8px;
+				font-size: 12px;
+			}
+		}
+
+		::v-deep(section) {
+			> header {
+				position: sticky;
+				top: 0;
+				left: 0;
+				height: 32px;
+				line-height: 32px;
+				z-index: 1;
+				padding: 0 8px;
+				font-size: 12px;
+				cursor: pointer;
+
+				&:hover {
+					color: var(--accent);
+				}
+			}
+
+			> div {
+				position: relative;
+				padding: $pad;
+
+				> button {
+					position: relative;
+					padding: 0;
+					width: var(--eachSize);
+					height: var(--eachSize);
+					border-radius: 4px;
+
+					&:focus-visible {
+						outline: solid 2px var(--focus);
+						z-index: 1;
+					}
+
+					&:hover {
+						background: rgba(0, 0, 0, 0.05);
+					}
+
+					&:active {
+						background: var(--accent);
+						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
+					}
+
+					> * {
+						font-size: 24px;
+						height: 1.25em;
+						vertical-align: -.25em;
+						pointer-events: none;
+					}
+				}
+			}
+
+			&.result {
+				border-bottom: solid 0.5px var(--divider);
+
+				&:empty {
+					display: none;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/featured-photos.vue b/packages/client/src/components/featured-photos.vue
new file mode 100644
index 0000000000..276344dfb4
--- /dev/null
+++ b/packages/client/src/components/featured-photos.vue
@@ -0,0 +1,32 @@
+<template>
+<div class="xfbouadm" v-if="meta" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+	},
+
+	data() {
+		return {
+			meta: null,
+		};
+	},
+
+	created() {
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.xfbouadm {
+	background-position: center;
+	background-size: cover;
+}
+</style>
diff --git a/packages/client/src/components/file-type-icon.vue b/packages/client/src/components/file-type-icon.vue
new file mode 100644
index 0000000000..be1af5e501
--- /dev/null
+++ b/packages/client/src/components/file-type-icon.vue
@@ -0,0 +1,28 @@
+<template>
+<span class="mk-file-type-icon">
+	<template v-if="kind == 'image'"><i class="fas fa-file-image"></i></template>
+</span>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		type: {
+			type: String,
+			required: true,
+		}
+	},
+	data() {
+		return {
+		};
+	},
+	computed: {
+		kind(): string {
+			return this.type.split('/')[0];
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue
new file mode 100644
index 0000000000..a96899027f
--- /dev/null
+++ b/packages/client/src/components/follow-button.vue
@@ -0,0 +1,210 @@
+<template>
+<button class="kpoogebi _button"
+	:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
+	@click="onClick"
+	:disabled="wait"
+>
+	<template v-if="!wait">
+		<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
+			<span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
+		</template>
+		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
+			<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
+		</template>
+		<template v-else-if="isFollowing">
+			<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
+		</template>
+		<template v-else-if="!isFollowing && user.isLocked">
+			<span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i>
+		</template>
+		<template v-else-if="!isFollowing && !user.isLocked">
+			<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
+		</template>
+	</template>
+	<template v-else>
+		<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+	</template>
+</button>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+		full: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		large: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+	},
+
+	data() {
+		return {
+			isFollowing: this.user.isFollowing,
+			hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
+			wait: false,
+			connection: null,
+		};
+	},
+
+	created() {
+		// 渡されたユーザー情報が不完全な場合
+		if (this.user.isFollowing == null) {
+			os.api('users/show', {
+				userId: this.user.id
+			}).then(u => {
+				this.isFollowing = u.isFollowing;
+				this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou;
+			});
+		}
+	},
+
+	mounted() {
+		this.connection = markRaw(os.stream.useChannel('main'));
+
+		this.connection.on('follow', this.onFollowChange);
+		this.connection.on('unfollow', this.onFollowChange);
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		onFollowChange(user) {
+			if (user.id == this.user.id) {
+				this.isFollowing = user.isFollowing;
+				this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
+			}
+		},
+
+		async onClick() {
+			this.wait = true;
+
+			try {
+				if (this.isFollowing) {
+					const { canceled } = await os.dialog({
+						type: 'warning',
+						text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
+						showCancelButton: true
+					});
+
+					if (canceled) return;
+
+					await os.api('following/delete', {
+						userId: this.user.id
+					});
+				} else {
+					if (this.hasPendingFollowRequestFromYou) {
+						await os.api('following/requests/cancel', {
+							userId: this.user.id
+						});
+					} else if (this.user.isLocked) {
+						await os.api('following/create', {
+							userId: this.user.id
+						});
+						this.hasPendingFollowRequestFromYou = true;
+					} else {
+						await os.api('following/create', {
+							userId: this.user.id
+						});
+						this.hasPendingFollowRequestFromYou = true;
+					}
+				}
+			} catch (e) {
+				console.error(e);
+			} finally {
+				this.wait = false;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.kpoogebi {
+	position: relative;
+	display: inline-block;
+	font-weight: bold;
+	color: var(--accent);
+	background: transparent;
+	border: solid 1px var(--accent);
+	padding: 0;
+	height: 31px;
+	font-size: 16px;
+	border-radius: 32px;
+	background: #fff;
+
+	&.full {
+		padding: 0 8px 0 12px;
+		font-size: 14px;
+	}
+
+	&.large {
+		font-size: 16px;
+		height: 38px;
+		padding: 0 12px 0 16px;
+	}
+
+	&:not(.full) {
+		width: 31px;
+	}
+
+	&:focus-visible {
+		&:after {
+			content: "";
+			pointer-events: none;
+			position: absolute;
+			top: -5px;
+			right: -5px;
+			bottom: -5px;
+			left: -5px;
+			border: 2px solid var(--focus);
+			border-radius: 32px;
+		}
+	}
+
+	&:hover {
+		//background: mix($primary, #fff, 20);
+	}
+
+	&:active {
+		//background: mix($primary, #fff, 40);
+	}
+
+	&.active {
+		color: #fff;
+		background: var(--accent);
+
+		&:hover {
+			background: var(--accentLighten);
+			border-color: var(--accentLighten);
+		}
+
+		&:active {
+			background: var(--accentDarken);
+			border-color: var(--accentDarken);
+		}
+	}
+
+	&.wait {
+		cursor: wait !important;
+		opacity: 0.7;
+	}
+
+	> span {
+		margin-right: 6px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue
new file mode 100644
index 0000000000..a42ea5864a
--- /dev/null
+++ b/packages/client/src/components/forgot-password.vue
@@ -0,0 +1,84 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="370"
+	:height="400"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+>
+	<template #header>{{ $ts.forgotPassword }}</template>
+
+	<form class="bafeceda" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
+		<div class="main _formRoot">
+			<MkInput class="_formBlock" v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
+				<template #label>{{ $ts.username }}</template>
+				<template #prefix>@</template>
+			</MkInput>
+
+			<MkInput class="_formBlock" v-model="email" type="email" spellcheck="false" required>
+				<template #label>{{ $ts.emailAddress }}</template>
+				<template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
+			</MkInput>
+
+			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
+		</div>
+		<div class="sub">
+			<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
+		</div>
+	</form>
+	<div v-else>
+		{{ $ts._forgotPassword.contactAdmin }}
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XModalWindow,
+		MkButton,
+		MkInput,
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			username: '',
+			email: '',
+			processing: false,
+		};
+	},
+
+	methods: {
+		async onSubmit() {
+			this.processing = true;
+			await os.apiWithDialog('request-reset-password', {
+				username: this.username,
+				email: this.email,
+			});
+
+			this.$emit('done');
+			this.$refs.dialog.close();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.bafeceda {
+	> .main {
+		padding: 24px;
+	}
+
+	> .sub {
+		border-top: solid 0.5px var(--divider);
+		padding: 24px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue
new file mode 100644
index 0000000000..172e6a5138
--- /dev/null
+++ b/packages/client/src/components/form-dialog.vue
@@ -0,0 +1,125 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="450"
+	:can-close="false"
+	:with-ok-button="true"
+	:ok-button-disabled="false"
+	@click="cancel()"
+	@ok="ok()"
+	@close="cancel()"
+	@closed="$emit('closed')"
+>
+	<template #header>
+		{{ title }}
+	</template>
+	<FormBase class="xkpnjxcv">
+		<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
+			<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
+				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</FormInput>
+			<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
+				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</FormInput>
+			<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
+				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</FormTextarea>
+			<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
+				<span v-text="form[item].label || item"></span>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</FormSwitch>
+			<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
+				<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
+				<option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
+			</FormSelect>
+			<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
+				<template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
+				<option v-for="item in form[item].options" :value="item.value" :key="item.value">{{ item.label }}</option>
+			</FormRadios>
+			<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
+				<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</FormRange>
+			<FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
+				<span v-text="form[item].content || item"></span>
+			</FormButton>
+		</template>
+	</FormBase>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import FormBase from './debobigego/base.vue';
+import FormInput from './debobigego/input.vue';
+import FormTextarea from './debobigego/textarea.vue';
+import FormSwitch from './debobigego/switch.vue';
+import FormSelect from './debobigego/select.vue';
+import FormRange from './debobigego/range.vue';
+import FormButton from './debobigego/button.vue';
+import FormRadios from './debobigego/radios.vue';
+
+export default defineComponent({
+	components: {
+		XModalWindow,
+		FormBase,
+		FormInput,
+		FormTextarea,
+		FormSwitch,
+		FormSelect,
+		FormRange,
+		FormButton,
+		FormRadios,
+	},
+
+	props: {
+		title: {
+			type: String,
+			required: true,
+		},
+		form: {
+			type: Object,
+			required: true,
+		},
+	},
+
+	emits: ['done'],
+
+	data() {
+		return {
+			values: {}
+		};
+	},
+
+	created() {
+		for (const item in this.form) {
+			this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null;
+		}
+	},
+
+	methods: {
+		ok() {
+			this.$emit('done', {
+				result: this.values
+			});
+			this.$refs.dialog.close();
+		},
+
+		cancel() {
+			this.$emit('done', {
+				canceled: true
+			});
+			this.$refs.dialog.close();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xkpnjxcv {
+
+}
+</style>
diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue
new file mode 100644
index 0000000000..f2c1ead00c
--- /dev/null
+++ b/packages/client/src/components/form/input.vue
@@ -0,0 +1,315 @@
+<template>
+<div class="matxzzsk">
+	<div class="label" @click="focus"><slot name="label"></slot></div>
+	<div class="input" :class="{ inline, disabled, focused }">
+		<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+		<input ref="inputEl"
+			:type="type"
+			v-model="v"
+			:disabled="disabled"
+			:required="required"
+			:readonly="readonly"
+			:placeholder="placeholder"
+			:pattern="pattern"
+			:autocomplete="autocomplete"
+			:spellcheck="spellcheck"
+			:step="step"
+			@focus="focused = true"
+			@blur="focused = false"
+			@keydown="onKeydown($event)"
+			@input="onInput"
+			:list="id"
+		>
+		<datalist :id="id" v-if="datalist">
+			<option v-for="data in datalist" :value="data"/>
+		</datalist>
+		<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
+	</div>
+	<div class="caption"><slot name="caption"></slot></div>
+
+	<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import { debounce } from 'throttle-debounce';
+
+export default defineComponent({
+	components: {
+		MkButton,
+	},
+
+	props: {
+		modelValue: {
+			required: true
+		},
+		type: {
+			type: String,
+			required: false
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		readonly: {
+			type: Boolean,
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			required: false
+		},
+		pattern: {
+			type: String,
+			required: false
+		},
+		placeholder: {
+			type: String,
+			required: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		autocomplete: {
+			required: false
+		},
+		spellcheck: {
+			required: false
+		},
+		step: {
+			required: false
+		},
+		datalist: {
+			type: Array,
+			required: false,
+		},
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		debounce: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		manualSave: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+
+	setup(props, context) {
+		const { modelValue, type, autofocus } = toRefs(props);
+		const v = ref(modelValue.value);
+		const id = Math.random().toString(); // TODO: uuid?
+		const focused = ref(false);
+		const changed = ref(false);
+		const invalid = ref(false);
+		const filled = computed(() => v.value !== '' && v.value != null);
+		const inputEl = ref(null);
+		const prefixEl = ref(null);
+		const suffixEl = ref(null);
+
+		const focus = () => inputEl.value.focus();
+		const onInput = (ev) => {
+			changed.value = true;
+			context.emit('change', ev);
+		};
+		const onKeydown = (ev: KeyboardEvent) => {
+			context.emit('keydown', ev);
+
+			if (ev.code === 'Enter') {
+				context.emit('enter');
+			}
+		};
+
+		const updated = () => {
+			changed.value = false;
+			if (type?.value === 'number') {
+				context.emit('update:modelValue', parseFloat(v.value));
+			} else {
+				context.emit('update:modelValue', v.value);
+			}
+		};
+
+		const debouncedUpdated = debounce(1000, updated);
+
+		watch(modelValue, newValue => {
+			v.value = newValue;
+		});
+
+		watch(v, newValue => {
+			if (!props.manualSave) {
+				if (props.debounce) {
+					debouncedUpdated();
+				} else {
+					updated();
+				}
+			}
+
+			invalid.value = inputEl.value.validity.badInput;
+		});
+
+		onMounted(() => {
+			nextTick(() => {
+				if (autofocus.value) {
+					focus();
+				}
+
+				// このコンポーネントが作成された時、非表示状態である場合がある
+				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
+				const clock = setInterval(() => {
+					if (prefixEl.value) {
+						if (prefixEl.value.offsetWidth) {
+							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
+						}
+					}
+					if (suffixEl.value) {
+						if (suffixEl.value.offsetWidth) {
+							inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
+						}
+					}
+				}, 100);
+
+				onUnmounted(() => {
+					clearInterval(clock);
+				});
+			});
+		});
+
+		return {
+			id,
+			v,
+			focused,
+			invalid,
+			changed,
+			filled,
+			inputEl,
+			prefixEl,
+			suffixEl,
+			focus,
+			onInput,
+			onKeydown,
+			updated,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.matxzzsk {
+	> .label {
+		font-size: 0.85em;
+		padding: 0 0 8px 12px;
+		user-select: none;
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .caption {
+		font-size: 0.8em;
+		padding: 8px 0 0 12px;
+		color: var(--fgTransparentWeak);
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .input {
+		$height: 42px;
+		position: relative;
+
+		> input {
+			appearance: none;
+			-webkit-appearance: none;
+			display: block;
+			height: $height;
+			width: 100%;
+			margin: 0;
+			padding: 0 12px;
+			font: inherit;
+			font-weight: normal;
+			font-size: 1em;
+			color: var(--fg);
+			background: var(--panel);
+			border: solid 0.5px var(--inputBorder);
+			border-radius: 6px;
+			outline: none;
+			box-shadow: none;
+			box-sizing: border-box;
+			transition: border-color 0.1s ease-out;
+
+			&:hover {
+				border-color: var(--inputBorderHover);
+			}
+		}
+
+		> .prefix,
+		> .suffix {
+			display: flex;
+			align-items: center;
+			position: absolute;
+			z-index: 1;
+			top: 0;
+			padding: 0 12px;
+			font-size: 1em;
+			height: $height;
+			pointer-events: none;
+
+			&:empty {
+				display: none;
+			}
+
+			> * {
+				display: inline-block;
+				min-width: 16px;
+				max-width: 150px;
+				overflow: hidden;
+				white-space: nowrap;
+				text-overflow: ellipsis;
+			}
+		}
+
+		> .prefix {
+			left: 0;
+			padding-right: 6px;
+		}
+
+		> .suffix {
+			right: 0;
+			padding-left: 6px;
+		}
+
+		&.inline {
+			display: inline-block;
+			margin: 0;
+		}
+
+		&.focused {
+			> input {
+				border-color: var(--accent);
+				//box-shadow: 0 0 0 4px var(--focus);
+			}
+		}
+
+		&.disabled {
+			opacity: 0.7;
+
+			&, * {
+				cursor: not-allowed !important;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue
new file mode 100644
index 0000000000..0f31d8fa0a
--- /dev/null
+++ b/packages/client/src/components/form/radio.vue
@@ -0,0 +1,122 @@
+<template>
+<div
+	class="novjtctn"
+	:class="{ disabled, checked }"
+	:aria-checked="checked"
+	:aria-disabled="disabled"
+	@click="toggle"
+>
+	<input type="radio"
+		:disabled="disabled"
+	>
+	<span class="button">
+		<span></span>
+	</span>
+	<span class="label"><slot></slot></span>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		modelValue: {
+			required: false
+		},
+		value: {
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		}
+	},
+	computed: {
+		checked(): boolean {
+			return this.modelValue === this.value;
+		}
+	},
+	methods: {
+		toggle() {
+			if (this.disabled) return;
+			this.$emit('update:modelValue', this.value);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.novjtctn {
+	position: relative;
+	display: inline-block;
+	margin: 8px 20px 0 0;
+	text-align: left;
+	cursor: pointer;
+	transition: all 0.3s;
+
+	> * {
+		user-select: none;
+	}
+
+	&.disabled {
+		opacity: 0.6;
+
+		&, * {
+			cursor: not-allowed !important;
+		}
+	}
+
+	&.checked {
+		> .button {
+			border-color: var(--accent);
+
+			&:after {
+				background-color: var(--accent);
+				transform: scale(1);
+				opacity: 1;
+			}
+		}
+	}
+
+	> input {
+		position: absolute;
+		width: 0;
+		height: 0;
+		opacity: 0;
+		margin: 0;
+	}
+
+	> .button {
+		position: absolute;
+		width: 20px;
+		height: 20px;
+		background: none;
+		border: solid 2px var(--inputBorder);
+		border-radius: 100%;
+		transition: inherit;
+
+		&:after {
+			content: '';
+			display: block;
+			position: absolute;
+			top: 3px;
+			right: 3px;
+			bottom: 3px;
+			left: 3px;
+			border-radius: 100%;
+			opacity: 0;
+			transform: scale(0);
+			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
+		}
+	}
+
+	> .label {
+		margin-left: 28px;
+		display: block;
+		font-size: 16px;
+		line-height: 20px;
+		cursor: pointer;
+	}
+}
+</style>
diff --git a/packages/client/src/components/form/radios.vue b/packages/client/src/components/form/radios.vue
new file mode 100644
index 0000000000..998a738202
--- /dev/null
+++ b/packages/client/src/components/form/radios.vue
@@ -0,0 +1,54 @@
+<script lang="ts">
+import { defineComponent, h } from 'vue';
+import MkRadio from './radio.vue';
+
+export default defineComponent({
+	components: {
+		MkRadio
+	},
+	props: {
+		modelValue: {
+			required: false
+		},
+	},
+	data() {
+		return {
+			value: this.modelValue,
+		}
+	},
+	watch: {
+		value() {
+			this.$emit('update:modelValue', this.value);
+		}
+	},
+	render() {
+		let options = this.$slots.default();
+
+		// なぜかFragmentになることがあるため
+		if (options.length === 1 && options[0].props == null) options = options[0].children;
+
+		return h('div', {
+			class: 'novjtcto'
+		}, [
+			...options.map(option => h(MkRadio, {
+				key: option.key,
+				value: option.props.value,
+				modelValue: this.value,
+				'onUpdate:modelValue': value => this.value = value,
+			}, option.children))
+		]);
+	}
+});
+</script>
+
+<style lang="scss">
+.novjtcto {
+	&:first-child {
+		margin-top: 0;
+	}
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+}
+</style>
diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
new file mode 100644
index 0000000000..4cfe66a8fc
--- /dev/null
+++ b/packages/client/src/components/form/range.vue
@@ -0,0 +1,139 @@
+<template>
+<div class="timctyfi" :class="{ focused, disabled }">
+	<div class="icon"><slot name="icon"></slot></div>
+	<span class="label"><slot name="label"></slot></span>
+	<input
+		type="range"
+		ref="input"
+		v-model="v"
+		:disabled="disabled"
+		:min="min"
+		:max="max"
+		:step="step"
+		:autofocus="autofocus"
+		@focus="focused = true"
+		@blur="focused = false"
+		@input="$emit('update:value', $event.target.value)"
+	/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		value: {
+			type: Number,
+			required: false,
+			default: 0
+		},
+		disabled: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		min: {
+			type: Number,
+			required: false,
+			default: 0
+		},
+		max: {
+			type: Number,
+			required: false,
+			default: 100
+		},
+		step: {
+			type: Number,
+			required: false,
+			default: 1
+		},
+		autofocus: {
+			type: Boolean,
+			required: false
+		}
+	},
+	data() {
+		return {
+			v: this.value,
+			focused: false
+		};
+	},
+	watch: {
+		value(v) {
+			this.v = parseFloat(v);
+		}
+	},
+	mounted() {
+		if (this.autofocus) {
+			this.$nextTick(() => {
+				this.$refs.input.focus();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.timctyfi {
+	position: relative;
+	margin: 8px;
+
+	> .icon {
+		display: inline-block;
+		width: 24px;
+		text-align: center;
+	}
+
+	> .title {
+		pointer-events: none;
+		font-size: 16px;
+		color: var(--inputLabel);
+		overflow: hidden;
+	}
+
+	> input {
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		background: var(--X10);
+		height: 7px;
+		margin: 0 8px;
+		outline: 0;
+		border: 0;
+		border-radius: 7px;
+
+		&.disabled {
+			opacity: 0.6;
+			cursor: not-allowed;
+		}
+
+		&::-webkit-slider-thumb {
+			-webkit-appearance: none;
+			appearance: none;
+			cursor: pointer;
+			width: 20px;
+			height: 20px;
+			display: block;
+			border-radius: 50%;
+			border: none;
+			background: var(--accent);
+			box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+			box-sizing: content-box;
+		}
+
+		&::-moz-range-thumb {
+			-moz-appearance: none;
+			appearance: none;
+			cursor: pointer;
+			width: 20px;
+			height: 20px;
+			display: block;
+			border-radius: 50%;
+			border: none;
+			background: var(--accent);
+			box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/form/section.vue b/packages/client/src/components/form/section.vue
new file mode 100644
index 0000000000..8eac40a0db
--- /dev/null
+++ b/packages/client/src/components/form/section.vue
@@ -0,0 +1,31 @@
+<template>
+<div class="vrtktovh" v-size="{ max: [500] }" v-sticky-container>
+	<div class="label"><slot name="label"></slot></div>
+	<div class="main">
+		<slot></slot>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+
+});
+</script>
+
+<style lang="scss" scoped>
+.vrtktovh {
+	border-top: solid 0.5px var(--divider);
+
+	> .label {
+		font-weight: bold;
+		padding: 24px 0 16px 0;
+	}
+
+	> .main {
+		margin-bottom: 32px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue
new file mode 100644
index 0000000000..f7eb5cd14d
--- /dev/null
+++ b/packages/client/src/components/form/select.vue
@@ -0,0 +1,312 @@
+<template>
+<div class="vblkjoeq">
+	<div class="label" @click="focus"><slot name="label"></slot></div>
+	<div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container">
+		<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+		<select class="select" ref="inputEl"
+			v-model="v"
+			:disabled="disabled"
+			:required="required"
+			:readonly="readonly"
+			:placeholder="placeholder"
+			@focus="focused = true"
+			@blur="focused = false"
+			@input="onInput"
+		>
+			<slot></slot>
+		</select>
+		<div class="suffix" ref="suffixEl"><i class="fas fa-chevron-down"></i></div>
+	</div>
+	<div class="caption"><slot name="caption"></slot></div>
+
+	<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, VNode } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkButton,
+	},
+
+	props: {
+		modelValue: {
+			required: true
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		readonly: {
+			type: Boolean,
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			required: false
+		},
+		placeholder: {
+			type: String,
+			required: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		manualSave: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	emits: ['change', 'update:modelValue'],
+
+	setup(props, context) {
+		const { modelValue, autofocus } = toRefs(props);
+		const v = ref(modelValue.value);
+		const focused = ref(false);
+		const changed = ref(false);
+		const invalid = ref(false);
+		const filled = computed(() => v.value !== '' && v.value != null);
+		const inputEl = ref(null);
+		const prefixEl = ref(null);
+		const suffixEl = ref(null);
+		const container = ref(null);
+
+		const focus = () => inputEl.value.focus();
+		const onInput = (ev) => {
+			changed.value = true;
+			context.emit('change', ev);
+		};
+
+		const updated = () => {
+			changed.value = false;
+			context.emit('update:modelValue', v.value);
+		};
+
+		watch(modelValue, newValue => {
+			v.value = newValue;
+		});
+
+		watch(v, newValue => {
+			if (!props.manualSave) {
+				updated();
+			}
+
+			invalid.value = inputEl.value.validity.badInput;
+		});
+
+		onMounted(() => {
+			nextTick(() => {
+				if (autofocus.value) {
+					focus();
+				}
+
+				// このコンポーネントが作成された時、非表示状態である場合がある
+				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
+				const clock = setInterval(() => {
+					if (prefixEl.value) {
+						if (prefixEl.value.offsetWidth) {
+							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
+						}
+					}
+					if (suffixEl.value) {
+						if (suffixEl.value.offsetWidth) {
+							inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
+						}
+					}
+				}, 100);
+
+				onUnmounted(() => {
+					clearInterval(clock);
+				});
+			});
+		});
+
+		const onClick = (ev: MouseEvent) => {
+			focused.value = true;
+
+			const menu = [];
+			let options = context.slots.default();
+
+			const pushOption = (option: VNode) => {
+				menu.push({
+					text: option.children,
+					active: v.value === option.props.value,
+					action: () => {
+						v.value = option.props.value;
+					},
+				});
+			};
+
+			const scanOptions = (options: VNode[]) => {
+				for (const vnode of options) {
+					if (vnode.type === 'optgroup') {
+						const optgroup = vnode;
+						menu.push({
+							type: 'label',
+							text: optgroup.props.label,
+						});
+						scanOptions(optgroup.children);
+					} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
+						const fragment = vnode;
+						scanOptions(fragment.children);
+					} else {
+						const option = vnode;
+						pushOption(option);
+					}
+				}
+			};
+
+			scanOptions(options);
+
+			os.popupMenu(menu, container.value, {
+				width: container.value.offsetWidth,
+			}).then(() => {
+				focused.value = false;
+			});
+		};
+
+		return {
+			v,
+			focused,
+			invalid,
+			changed,
+			filled,
+			inputEl,
+			prefixEl,
+			suffixEl,
+			container,
+			focus,
+			onInput,
+			onClick,
+			updated,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.vblkjoeq {
+	> .label {
+		font-size: 0.85em;
+		padding: 0 0 8px 12px;
+		user-select: none;
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .caption {
+		font-size: 0.8em;
+		padding: 8px 0 0 12px;
+		color: var(--fgTransparentWeak);
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .input {
+		$height: 42px;
+		position: relative;
+		cursor: pointer;
+
+		&:hover {
+			> .select {
+				border-color: var(--inputBorderHover);
+			}
+		}
+
+		> .select {
+			appearance: none;
+			-webkit-appearance: none;
+			display: block;
+			height: $height;
+			width: 100%;
+			margin: 0;
+			padding: 0 12px;
+			font: inherit;
+			font-weight: normal;
+			font-size: 1em;
+			color: var(--fg);
+			background: var(--panel);
+			border: solid 1px var(--inputBorder);
+			border-radius: 6px;
+			outline: none;
+			box-shadow: none;
+			box-sizing: border-box;
+			cursor: pointer;
+			transition: border-color 0.1s ease-out;
+			pointer-events: none;
+		}
+
+		> .prefix,
+		> .suffix {
+			display: flex;
+			align-items: center;
+			position: absolute;
+			z-index: 1;
+			top: 0;
+			padding: 0 12px;
+			font-size: 1em;
+			height: $height;
+			pointer-events: none;
+
+			&:empty {
+				display: none;
+			}
+
+			> * {
+				display: inline-block;
+				min-width: 16px;
+				max-width: 150px;
+				overflow: hidden;
+				white-space: nowrap;
+				text-overflow: ellipsis;
+			}
+		}
+
+		> .prefix {
+			left: 0;
+			padding-right: 6px;
+		}
+
+		> .suffix {
+			right: 0;
+			padding-left: 6px;
+		}
+
+		&.inline {
+			display: inline-block;
+			margin: 0;
+		}
+
+		&.focused {
+			> select {
+				border-color: var(--accent);
+			}
+		}
+
+		&.disabled {
+			opacity: 0.7;
+
+			&, * {
+				cursor: not-allowed !important;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/form/slot.vue b/packages/client/src/components/form/slot.vue
new file mode 100644
index 0000000000..8580c1307d
--- /dev/null
+++ b/packages/client/src/components/form/slot.vue
@@ -0,0 +1,50 @@
+<template>
+<div class="adhpbeou">
+	<div class="label" @click="focus"><slot name="label"></slot></div>
+	<div class="content">
+		<slot></slot>
+	</div>
+	<div class="caption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+
+});
+</script>
+
+<style lang="scss" scoped>
+.adhpbeou {
+	margin: 1.5em 0;
+
+	> .label {
+		font-size: 0.85em;
+		padding: 0 0 8px 12px;
+		user-select: none;
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .caption {
+		font-size: 0.8em;
+		padding: 8px 0 0 12px;
+		color: var(--fgTransparentWeak);
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .content {
+		position: relative;
+		background: var(--panel);
+		border: solid 0.5px var(--inputBorder);
+		border-radius: 6px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue
new file mode 100644
index 0000000000..85f8b7c870
--- /dev/null
+++ b/packages/client/src/components/form/switch.vue
@@ -0,0 +1,150 @@
+<template>
+<div
+	class="ziffeoms"
+	:class="{ disabled, checked }"
+	role="switch"
+	:aria-checked="checked"
+	:aria-disabled="disabled"
+	@click.prevent="toggle"
+>
+	<input
+		type="checkbox"
+		ref="input"
+		:disabled="disabled"
+		@keydown.enter="toggle"
+	>
+	<span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
+		<span class="handle"></span>
+	</span>
+	<span class="label">
+		<span><slot></slot></span>
+		<p><slot name="caption"></slot></p>
+	</span>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		modelValue: {
+			type: Boolean,
+			default: false
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		}
+	},
+	computed: {
+		checked(): boolean {
+			return this.modelValue;
+		}
+	},
+	methods: {
+		toggle() {
+			if (this.disabled) return;
+			this.$emit('update:modelValue', !this.checked);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ziffeoms {
+	position: relative;
+	display: flex;
+	cursor: pointer;
+	transition: all 0.3s;
+
+	&:first-child {
+		margin-top: 0;
+	}
+
+	&:last-child {
+		margin-bottom: 0;
+	}
+
+	> * {
+		user-select: none;
+	}
+
+	> input {
+		position: absolute;
+		width: 0;
+		height: 0;
+		opacity: 0;
+		margin: 0;
+	}
+
+	> .button {
+		position: relative;
+		display: inline-block;
+		flex-shrink: 0;
+		margin: 0;
+		width: 36px;
+		height: 26px;
+		background: var(--switchBg);
+		outline: none;
+		border-radius: 999px;
+		transition: inherit;
+
+		> .handle {
+			position: absolute;
+			top: 0;
+			bottom: 0;
+			left: 5px;
+			margin: auto 0;
+			border-radius: 100%;
+			transition: background-color 0.3s, transform 0.3s;
+			width: 16px;
+			height: 16px;
+			background-color: #fff;
+		}
+	}
+
+	> .label {
+		margin-left: 16px;
+		margin-top: 2px;
+		display: block;
+		cursor: pointer;
+		transition: inherit;
+		color: var(--fg);
+
+		> span {
+			display: block;
+			line-height: 20px;
+			transition: inherit;
+		}
+
+		> p {
+			margin: 0;
+			color: var(--fgTransparentWeak);
+			font-size: 90%;
+		}
+	}
+
+	&:hover {
+		> .button {
+			background-color: var(--accentedBg);
+		}
+	}
+
+	&.disabled {
+		opacity: 0.6;
+		cursor: not-allowed;
+	}
+
+	&.checked {
+		> .button {
+			background-color: var(--accent);
+			border-color: var(--accent);
+
+			> .handle {
+				transform: translateX(10px);
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/form/textarea.vue b/packages/client/src/components/form/textarea.vue
new file mode 100644
index 0000000000..fdb24f1e2b
--- /dev/null
+++ b/packages/client/src/components/form/textarea.vue
@@ -0,0 +1,252 @@
+<template>
+<div class="adhpbeos">
+	<div class="label" @click="focus"><slot name="label"></slot></div>
+	<div class="input" :class="{ disabled, focused, tall, pre }">
+		<textarea ref="inputEl"
+			:class="{ code, _monospace: code }"
+			v-model="v"
+			:disabled="disabled"
+			:required="required"
+			:readonly="readonly"
+			:placeholder="placeholder"
+			:pattern="pattern"
+			:autocomplete="autocomplete"
+			:spellcheck="spellcheck"
+			@focus="focused = true"
+			@blur="focused = false"
+			@keydown="onKeydown($event)"
+			@input="onInput"
+		></textarea>
+	</div>
+	<div class="caption"><slot name="caption"></slot></div>
+
+	<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import { debounce } from 'throttle-debounce';
+
+export default defineComponent({
+	components: {
+		MkButton,
+	},
+
+	props: {
+		modelValue: {
+			required: true
+		},
+		type: {
+			type: String,
+			required: false
+		},
+		required: {
+			type: Boolean,
+			required: false
+		},
+		readonly: {
+			type: Boolean,
+			required: false
+		},
+		disabled: {
+			type: Boolean,
+			required: false
+		},
+		pattern: {
+			type: String,
+			required: false
+		},
+		placeholder: {
+			type: String,
+			required: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		autocomplete: {
+			required: false
+		},
+		spellcheck: {
+			required: false
+		},
+		code: {
+			type: Boolean,
+			required: false
+		},
+		tall: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		pre: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		debounce: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		manualSave: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+
+	setup(props, context) {
+		const { modelValue, autofocus } = toRefs(props);
+		const v = ref(modelValue.value);
+		const focused = ref(false);
+		const changed = ref(false);
+		const invalid = ref(false);
+		const filled = computed(() => v.value !== '' && v.value != null);
+		const inputEl = ref(null);
+
+		const focus = () => inputEl.value.focus();
+		const onInput = (ev) => {
+			changed.value = true;
+			context.emit('change', ev);
+		};
+		const onKeydown = (ev: KeyboardEvent) => {
+			context.emit('keydown', ev);
+
+			if (ev.code === 'Enter') {
+				context.emit('enter');
+			}
+		};
+
+		const updated = () => {
+			changed.value = false;
+			context.emit('update:modelValue', v.value);
+		};
+
+		const debouncedUpdated = debounce(1000, updated);
+
+		watch(modelValue, newValue => {
+			v.value = newValue;
+		});
+
+		watch(v, newValue => {
+			if (!props.manualSave) {
+				if (props.debounce) {
+					debouncedUpdated();
+				} else {
+					updated();
+				}
+			}
+
+			invalid.value = inputEl.value.validity.badInput;
+		});
+
+		onMounted(() => {
+			nextTick(() => {
+				if (autofocus.value) {
+					focus();
+				}
+			});
+		});
+
+		return {
+			v,
+			focused,
+			invalid,
+			changed,
+			filled,
+			inputEl,
+			focus,
+			onInput,
+			onKeydown,
+			updated,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.adhpbeos {
+	> .label {
+		font-size: 0.85em;
+		padding: 0 0 8px 12px;
+		user-select: none;
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .caption {
+		font-size: 0.8em;
+		padding: 8px 0 0 12px;
+		color: var(--fgTransparentWeak);
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .input {
+		position: relative;
+
+		> textarea {
+			appearance: none;
+			-webkit-appearance: none;
+			display: block;
+			width: 100%;
+			min-width: 100%;
+			max-width: 100%;
+			min-height: 130px;
+			margin: 0;
+			padding: 12px;
+			font: inherit;
+			font-weight: normal;
+			font-size: 1em;
+			color: var(--fg);
+			background: var(--panel);
+			border: solid 0.5px var(--inputBorder);
+			border-radius: 6px;
+			outline: none;
+			box-shadow: none;
+			box-sizing: border-box;
+			transition: border-color 0.1s ease-out;
+
+			&:hover {
+				border-color: var(--inputBorderHover);
+			}
+		}
+
+		&.focused {
+			> textarea {
+				border-color: var(--accent);
+			}
+		}
+
+		&.disabled {
+			opacity: 0.7;
+
+			&, * {
+				cursor: not-allowed !important;
+			}
+		}
+
+		&.tall {
+			> textarea {
+				min-height: 200px;
+			}
+		}
+
+		&.pre {
+			> textarea {
+				white-space: pre;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/formula-core.vue b/packages/client/src/components/formula-core.vue
new file mode 100644
index 0000000000..cf8dee872b
--- /dev/null
+++ b/packages/client/src/components/formula-core.vue
@@ -0,0 +1,34 @@
+
+<template>
+<div v-if="block" v-html="compiledFormula"></div>
+<span v-else v-html="compiledFormula"></span>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as katex from 'katex';import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		formula: {
+			type: String,
+			required: true
+		},
+		block: {
+			type: Boolean,
+			required: true
+		}
+	},
+	computed: {
+		compiledFormula(): any {
+			return katex.renderToString(this.formula, {
+				throwOnError: false
+			} as any);
+		}
+	}
+});
+</script>
+
+<style>
+@import "../../node_modules/katex/dist/katex.min.css";
+</style>
diff --git a/packages/client/src/components/formula.vue b/packages/client/src/components/formula.vue
new file mode 100644
index 0000000000..fbb40bace7
--- /dev/null
+++ b/packages/client/src/components/formula.vue
@@ -0,0 +1,23 @@
+<template>
+<XFormula :formula="formula" :block="block" />
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XFormula: defineAsyncComponent(() => import('./formula-core.vue'))
+	},
+	props: {
+		formula: {
+			type: String,
+			required: true
+		},
+		block: {
+			type: Boolean,
+			required: true
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/gallery-post-preview.vue b/packages/client/src/components/gallery-post-preview.vue
new file mode 100644
index 0000000000..8245902976
--- /dev/null
+++ b/packages/client/src/components/gallery-post-preview.vue
@@ -0,0 +1,126 @@
+<template>
+<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1">
+	<div class="thumbnail">
+		<ImgWithBlurhash class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
+	</div>
+	<article>
+		<header>
+			<MkAvatar :user="post.user" class="avatar"/>
+		</header>
+		<footer>
+			<span class="title">{{ post.title }}</span>
+		</footer>
+	</article>
+</MkA>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { userName } from '@/filters/user';
+import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		ImgWithBlurhash
+	},
+	props: {
+		post: {
+			type: Object,
+			required: true
+		},
+	},
+	methods: {
+		userName
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ttasepnz {
+	display: block;
+	position: relative;
+	height: 200px;
+
+	&:hover {
+		text-decoration: none;
+		color: var(--accent);
+
+		> .thumbnail {
+			transform: scale(1.1);
+		}
+
+		> article {
+			> footer {
+				&:before {
+					opacity: 1;
+				}
+			}
+		}
+	}
+
+	> .thumbnail {
+		width: 100%;
+		height: 100%;
+		position: absolute;
+		transition: all 0.5s ease;
+
+		> .img {
+			width: 100%;
+			height: 100%;
+			object-fit: cover;
+		}
+	}
+
+	> article {
+		position: absolute;
+		z-index: 1;
+		width: 100%;
+		height: 100%;
+
+		> header {
+			position: absolute;
+			top: 0;
+			width: 100%;
+			padding: 12px;
+			box-sizing: border-box;
+			display: flex;
+
+			> .avatar {
+				margin-left: auto;
+				width: 32px;
+				height: 32px;
+			}
+		}
+
+		> footer {
+			position: absolute;
+			bottom: 0;
+			width: 100%;
+			padding: 16px;
+			box-sizing: border-box;
+			color: #fff;
+			text-shadow: 0 0 8px #000;
+			background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
+
+			&:before {
+				content: "";
+				display: block;
+				position: absolute;
+				z-index: -1;
+				top: 0;
+				left: 0;
+				width: 100%;
+				height: 100%;
+				background: linear-gradient(rgba(0, 0, 0, 0.4), transparent);
+				opacity: 0;
+				transition: opacity 0.5s ease;
+			}
+
+			> .title {
+				font-weight: bold;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue
new file mode 100644
index 0000000000..5db61203c6
--- /dev/null
+++ b/packages/client/src/components/global/a.vue
@@ -0,0 +1,138 @@
+<template>
+<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
+	<slot></slot>
+</a>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { router } from '@/router';
+import { url } from '@/config';
+import { popout } from '@/scripts/popout';
+import { ColdDeviceStorage } from '@/store';
+
+export default defineComponent({
+	inject: {
+		navHook: {
+			default: null
+		},
+		sideViewHook: {
+			default: null
+		}
+	},
+
+	props: {
+		to: {
+			type: String,
+			required: true,
+		},
+		activeClass: {
+			type: String,
+			required: false,
+		},
+		behavior: {
+			type: String,
+			required: false,
+		},
+	},
+
+	computed: {
+		active() {
+			if (this.activeClass == null) return false;
+			const resolved = router.resolve(this.to);
+			if (resolved.path == this.$route.path) return true;
+			if (resolved.name == null) return false;
+			if (this.$route.name == null) return false;
+			return resolved.name == this.$route.name;
+		}
+	},
+
+	methods: {
+		onContextmenu(e) {
+			if (window.getSelection().toString() !== '') return;
+			os.contextMenu([{
+				type: 'label',
+				text: this.to,
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(this.to);
+				}
+			}, this.sideViewHook ? {
+				icon: 'fas fa-columns',
+				text: this.$ts.openInSideView,
+				action: () => {
+					this.sideViewHook(this.to);
+				}
+			} : undefined, {
+				icon: 'fas fa-expand-alt',
+				text: this.$ts.showInPage,
+				action: () => {
+					this.$router.push(this.to);
+				}
+			}, null, {
+				icon: 'fas fa-external-link-alt',
+				text: this.$ts.openInNewTab,
+				action: () => {
+					window.open(this.to, '_blank');
+				}
+			}, {
+				icon: 'fas fa-link',
+				text: this.$ts.copyLink,
+				action: () => {
+					copyToClipboard(`${url}${this.to}`);
+				}
+			}], e);
+		},
+
+		window() {
+			os.pageWindow(this.to);
+		},
+
+		modalWindow() {
+			os.modalPageWindow(this.to);
+		},
+
+		popout() {
+			popout(this.to);
+		},
+
+		nav() {
+			if (this.behavior === 'browser') {
+				location.href = this.to;
+				return;
+			}
+
+			if (this.to.startsWith('/my/messaging')) {
+				if (ColdDeviceStorage.get('chatOpenBehavior') === 'window') return this.window();
+				if (ColdDeviceStorage.get('chatOpenBehavior') === 'popout') return this.popout();
+			}
+
+			if (this.behavior) {
+				if (this.behavior === 'window') {
+					return this.window();
+				} else if (this.behavior === 'modalWindow') {
+					return this.modalWindow();
+				}
+			}
+
+			if (this.navHook) {
+				this.navHook(this.to);
+			} else {
+				if (this.$store.state.defaultSideView && this.sideViewHook && this.to !== '/') {
+					return this.sideViewHook(this.to);
+				}
+
+				if (this.$router.currentRoute.value.path === this.to) {
+					window.scroll({ top: 0, behavior: 'smooth' });
+				} else {
+					this.$router.push(this.to);
+				}
+			}
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/global/acct.vue b/packages/client/src/components/global/acct.vue
new file mode 100644
index 0000000000..b0c41c99c0
--- /dev/null
+++ b/packages/client/src/components/global/acct.vue
@@ -0,0 +1,38 @@
+<template>
+<span class="mk-acct">
+	<span class="name">@{{ user.username }}</span>
+	<span class="host" v-if="user.host || detail || $store.state.showFullAcct">@{{ user.host || host }}</span>
+</span>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { toUnicode } from 'punycode/';
+import { host } from '@/config';
+
+export default defineComponent({
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+		detail: {
+			type: Boolean,
+			default: false
+		},
+	},
+	data() {
+		return {
+			host: toUnicode(host),
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-acct {
+	> .host {
+		opacity: 0.5;
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/ad.vue b/packages/client/src/components/global/ad.vue
new file mode 100644
index 0000000000..71cb16740c
--- /dev/null
+++ b/packages/client/src/components/global/ad.vue
@@ -0,0 +1,200 @@
+<template>
+<div class="qiivuoyo" v-if="ad">
+	<div class="main" :class="ad.place" v-if="!showMenu">
+		<a :href="ad.url" target="_blank">
+			<img :src="ad.imageUrl">
+			<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
+		</a>
+	</div>
+	<div class="menu" v-else>
+		<div class="body">
+			<div>Ads by {{ host }}</div>
+			<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
+			<MkButton v-if="ad.ratio !== 0" class="button" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
+			<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
+		</div>
+	</div>
+</div>
+<div v-else></div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+import { Instance, instance } from '@/instance';
+import { host } from '@/config';
+import MkButton from '@/components/ui/button.vue';
+import { defaultStore } from '@/store';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+
+	props: {
+		prefer: {
+			type: Array,
+			required: true
+		},
+		specify: {
+			type: Object,
+			required: false
+		},
+	},
+
+	setup(props) {
+		const showMenu = ref(false);
+		const toggleMenu = () => {
+			showMenu.value = !showMenu.value;
+		};
+
+		const choseAd = (): Instance['ads'][number] | null => {
+			if (props.specify) {
+				return props.specify as Instance['ads'][number];
+			}
+
+			const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
+				...ad,
+				ratio: 0
+			} : ad);
+
+			let ads = allAds.filter(ad => props.prefer.includes(ad.place));
+
+			if (ads.length === 0) {
+				ads = allAds.filter(ad => ad.place === 'square');
+			}
+
+			const lowPriorityAds = ads.filter(ad => ad.ratio === 0);
+			ads = ads.filter(ad => ad.ratio !== 0);
+
+			if (ads.length === 0) {
+				if (lowPriorityAds.length !== 0) {
+					return lowPriorityAds[Math.floor(Math.random() * lowPriorityAds.length)];
+				} else {
+					return null;
+				}
+			}
+
+			const totalFactor = ads.reduce((a, b) => a + b.ratio, 0);
+			const r = Math.random() * totalFactor;
+
+			let stackedFactor = 0;
+			for (const ad of ads) {
+				if (r >= stackedFactor && r <= stackedFactor + ad.ratio) {
+					return ad;
+				} else {
+					stackedFactor += ad.ratio;
+				}
+			}
+
+			return null;
+		};
+
+		const chosen = ref(choseAd());
+
+		const reduceFrequency = () => {
+			if (chosen.value == null) return;
+			if (defaultStore.state.mutedAds.includes(chosen.value.id)) return;
+			defaultStore.push('mutedAds', chosen.value.id);
+			os.success();
+			chosen.value = choseAd();
+			showMenu.value = false;
+		};
+
+		return {
+			ad: chosen,
+			showMenu,
+			toggleMenu,
+			host,
+			reduceFrequency,
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qiivuoyo {
+	background-size: auto auto;
+	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
+
+	> .main {
+		text-align: center;
+
+		> a {
+			display: inline-block;
+			position: relative;
+			vertical-align: bottom;
+
+			&:hover {
+				> img {
+					filter: contrast(120%);
+				}
+			}
+
+			> img {
+				display: block;
+				object-fit: contain;
+				margin: auto;
+			}
+
+			> .menu {
+				position: absolute;
+				top: 0;
+				right: 0;
+				background: var(--panel);
+			}
+		}
+
+		&.square {
+			> a ,
+			> a > img {
+				max-width: min(300px, 100%);
+				max-height: 300px;
+			}
+		}
+
+		&.horizontal {
+			padding: 8px;
+
+			> a ,
+			> a > img {
+				max-width: min(600px, 100%);
+				max-height: 80px;
+			}
+		}
+
+		&.horizontal-big {
+			padding: 8px;
+
+			> a ,
+			> a > img {
+				max-width: min(600px, 100%);
+				max-height: 250px;
+			}
+		}
+
+		&.vertical {
+			> a ,
+			> a > img {
+				max-width: min(100px, 100%);
+			}
+		}
+	}
+
+	> .menu {
+		padding: 8px;
+		text-align: center;
+
+		> .body {
+			padding: 8px;
+			margin: 0 auto;
+			max-width: 400px;
+			border: solid 1px var(--divider);
+
+			> .button {
+				margin: 8px auto;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/avatar.vue b/packages/client/src/components/global/avatar.vue
new file mode 100644
index 0000000000..e509e893da
--- /dev/null
+++ b/packages/client/src/components/global/avatar.vue
@@ -0,0 +1,163 @@
+<template>
+<span class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
+	<img class="inner" :src="url" decoding="async"/>
+	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
+</span>
+<MkA class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
+	<img class="inner" :src="url" decoding="async"/>
+	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
+</MkA>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
+import { acct, userPage } from '@/filters/user';
+import MkUserOnlineIndicator from '@/components/user-online-indicator.vue';
+
+export default defineComponent({
+	components: {
+		MkUserOnlineIndicator
+	},
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+		target: {
+			required: false,
+			default: null
+		},
+		disableLink: {
+			required: false,
+			default: false
+		},
+		disablePreview: {
+			required: false,
+			default: false
+		},
+		showIndicator: {
+			required: false,
+			default: false
+		}
+	},
+	emits: ['click'],
+	computed: {
+		cat(): boolean {
+			return this.user.isCat;
+		},
+		url(): string {
+			return this.$store.state.disableShowingAnimatedImages
+				? getStaticImageUrl(this.user.avatarUrl)
+				: this.user.avatarUrl;
+		},
+	},
+	watch: {
+		'user.avatarBlurhash'() {
+			if (this.$el == null) return;
+			this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
+		}
+	},
+	mounted() {
+		this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
+	},
+	methods: {
+		onClick(e) {
+			this.$emit('click', e);
+		},
+		acct,
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+@keyframes earwiggleleft {
+	from { transform: rotate(37.6deg) skew(30deg); }
+	25% { transform: rotate(10deg) skew(30deg); }
+	50% { transform: rotate(20deg) skew(30deg); }
+	75% { transform: rotate(0deg) skew(30deg); }
+	to { transform: rotate(37.6deg) skew(30deg); }
+}
+
+@keyframes earwiggleright {
+	from { transform: rotate(-37.6deg) skew(-30deg); }
+	30% { transform: rotate(-10deg) skew(-30deg); }
+	55% { transform: rotate(-20deg) skew(-30deg); }
+	75% { transform: rotate(0deg) skew(-30deg); }
+	to { transform: rotate(-37.6deg) skew(-30deg); }
+}
+
+.eiwwqkts {
+	position: relative;
+	display: inline-block;
+	vertical-align: bottom;
+	flex-shrink: 0;
+	border-radius: 100%;
+	line-height: 16px;
+
+	> .inner {
+		position: absolute;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		top: 0;
+		border-radius: 100%;
+		z-index: 1;
+		overflow: hidden;
+		object-fit: cover;
+		width: 100%;
+		height: 100%;
+	}
+
+	> .indicator {
+		position: absolute;
+		z-index: 1;
+		bottom: 0;
+		left: 0;
+		width: 20%;
+		height: 20%;
+	}
+
+	&.square {
+		border-radius: 20%;
+
+		> .inner {
+			border-radius: 20%;
+		}
+	}
+
+	&.cat {
+		&:before, &:after {
+			background: #df548f;
+			border: solid 4px currentColor;
+			box-sizing: border-box;
+			content: '';
+			display: inline-block;
+			height: 50%;
+			width: 50%;
+		}
+
+		&:before {
+			border-radius: 0 75% 75%;
+			transform: rotate(37.5deg) skew(30deg);
+		}
+
+		&:after {
+			border-radius: 75% 0 75% 75%;
+			transform: rotate(-37.5deg) skew(-30deg);
+		}
+
+		&:hover {
+			&:before {
+				animation: earwiggleleft 1s infinite;
+			}
+
+			&:after {
+				animation: earwiggleright 1s infinite;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/ellipsis.vue b/packages/client/src/components/global/ellipsis.vue
new file mode 100644
index 0000000000..0a46f486d6
--- /dev/null
+++ b/packages/client/src/components/global/ellipsis.vue
@@ -0,0 +1,34 @@
+<template>
+	<span class="mk-ellipsis">
+		<span>.</span><span>.</span><span>.</span>
+	</span>
+</template>
+
+<style lang="scss" scoped>
+.mk-ellipsis {
+	> span {
+		animation: ellipsis 1.4s infinite ease-in-out both;
+
+		&:nth-child(1) {
+			animation-delay: 0s;
+		}
+
+		&:nth-child(2) {
+			animation-delay: 0.16s;
+		}
+
+		&:nth-child(3) {
+			animation-delay: 0.32s;
+		}
+	}
+}
+
+@keyframes ellipsis {
+	0%, 80%, 100% {
+		opacity: 1;
+	}
+	40% {
+		opacity: 0;
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/emoji.vue b/packages/client/src/components/global/emoji.vue
new file mode 100644
index 0000000000..67a3dea2c5
--- /dev/null
+++ b/packages/client/src/components/global/emoji.vue
@@ -0,0 +1,125 @@
+<template>
+<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/>
+<img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt" decoding="async"/>
+<span v-else-if="char && useOsNativeEmojis">{{ char }}</span>
+<span v-else>{{ emoji }}</span>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+import { twemojiSvgBase } from '@/scripts/twemoji-base';
+
+export default defineComponent({
+	props: {
+		emoji: {
+			type: String,
+			required: true
+		},
+		normal: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		noStyle: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		customEmojis: {
+			required: false
+		},
+		isReaction: {
+			type: Boolean,
+			default: false
+		},
+	},
+
+	data() {
+		return {
+			url: null,
+			char: null,
+			customEmoji: null
+		}
+	},
+
+	computed: {
+		isCustom(): boolean {
+			return this.emoji.startsWith(':');
+		},
+
+		alt(): string {
+			return this.customEmoji ? `:${this.customEmoji.name}:` : this.char;
+		},
+
+		useOsNativeEmojis(): boolean {
+			return this.$store.state.useOsNativeEmojis && !this.isReaction;
+		},
+
+		ce() {
+			return this.customEmojis || this.$instance?.emojis || [];
+		}
+	},
+
+	watch: {
+		ce: {
+			handler() {
+				if (this.isCustom) {
+					const customEmoji = this.ce.find(x => x.name === this.emoji.substr(1, this.emoji.length - 2));
+					if (customEmoji) {
+						this.customEmoji = customEmoji;
+						this.url = this.$store.state.disableShowingAnimatedImages
+							? getStaticImageUrl(customEmoji.url)
+							: customEmoji.url;
+					}
+				}
+			},
+			immediate: true
+		},
+	},
+
+	created() {
+		if (!this.isCustom) {
+			this.char = this.emoji;
+		}
+
+		if (this.char) {
+			let codes = Array.from(this.char).map(x => x.codePointAt(0).toString(16));
+			if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
+			codes = codes.filter(x => x && x.length);
+
+			this.url = `${twemojiSvgBase}/${codes.join('-')}.svg`;
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-emoji {
+	height: 1.25em;
+	vertical-align: -0.25em;
+
+	&.custom {
+		height: 2.5em;
+		vertical-align: middle;
+		transition: transform 0.2s ease;
+
+		&:hover {
+			transform: scale(1.2);
+		}
+
+		&.normal {
+			height: 1.25em;
+			vertical-align: -0.25em;
+
+			&:hover {
+				transform: none;
+			}
+		}
+	}
+
+	&.noStyle {
+		height: auto !important;
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/error.vue b/packages/client/src/components/global/error.vue
new file mode 100644
index 0000000000..8ce5d16ac6
--- /dev/null
+++ b/packages/client/src/components/global/error.vue
@@ -0,0 +1,46 @@
+<template>
+<transition :name="$store.state.animation ? 'zoom' : ''" appear>
+	<div class="mjndxjcg">
+		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
+		<p><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</p>
+		<MkButton @click="() => $emit('retry')" class="button">{{ $ts.retry }}</MkButton>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+	components: {
+		MkButton,
+	},
+	data() {
+		return {
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mjndxjcg {
+	padding: 32px;
+	text-align: center;
+
+	> p {
+		margin: 0 0 8px 0;
+	}
+
+	> .button {
+		margin: 0 auto;
+	}
+
+	> img {
+		vertical-align: bottom;
+		height: 128px;
+		margin-bottom: 16px;
+		border-radius: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue
new file mode 100644
index 0000000000..7d5e426f2b
--- /dev/null
+++ b/packages/client/src/components/global/header.vue
@@ -0,0 +1,360 @@
+<template>
+<div class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick" ref="el">
+	<template v-if="info">
+		<div class="titleContainer" @click="showTabsPopup" v-if="!hideTitle">
+			<MkAvatar v-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
+			<i v-else-if="info.icon" class="icon" :class="info.icon"></i>
+
+			<div class="title">
+				<MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="title"/>
+				<div v-else-if="info.title" class="title">{{ info.title }}</div>
+				<div class="subtitle" v-if="!narrow && info.subtitle">
+					{{ info.subtitle }}
+				</div>
+				<div class="subtitle activeTab" v-if="narrow && hasTabs">
+					{{ info.tabs.find(tab => tab.active)?.title }}
+					<i class="chevron fas fa-chevron-down"></i>
+				</div>
+			</div>
+		</div>
+		<div class="tabs" v-if="!narrow || hideTitle">
+			<button class="tab _button" v-for="tab in info.tabs" :class="{ active: tab.active }" @click="tab.onClick" v-tooltip="tab.title">
+				<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
+				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
+			</button>
+		</div>
+	</template>
+	<div class="buttons right">
+		<template v-if="info && info.actions && !narrow">
+			<template v-for="action in info.actions">
+				<MkButton class="fullButton" v-if="action.asFullButton" @click.stop="action.handler" primary><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
+				<button v-else class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button>
+			</template>
+		</template>
+		<button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue';
+import * as tinycolor from 'tinycolor2';
+import { popupMenu } from '@/os';
+import { url } from '@/config';
+import { scrollToTop } from '@/scripts/scroll';
+import MkButton from '@/components/ui/button.vue';
+import { i18n } from '@/i18n';
+import { globalEvents } from '@/events';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+
+	props: {
+		info: {
+			type: Object as PropType<{
+				actions?: {}[];
+				tabs?: {}[];
+			}>,
+			required: true
+		},
+		menu: {
+			required: false
+		},
+		thin: {
+			required: false,
+			default: false
+		},
+	},
+
+	setup(props) {
+		const el = ref<HTMLElement>(null);
+		const bg = ref(null);
+		const narrow = ref(false);
+		const height = ref(0);
+		const hasTabs = computed(() => {
+			return props.info.tabs && props.info.tabs.length > 0;
+		});
+		const shouldShowMenu = computed(() => {
+			if (props.info == null) return false;
+			if (props.info.actions != null && narrow.value) return true;
+			if (props.info.menu != null) return true;
+			if (props.info.share != null) return true;
+			if (props.menu != null) return true;
+			return false;
+		});
+
+		const share = () => {
+			navigator.share({
+				url: url + props.info.path,
+				...props.info.share,
+			});
+		};
+
+		const showMenu = (ev: MouseEvent) => {
+			let menu = props.info.menu ? props.info.menu() : [];
+			if (narrow.value && props.info.actions) {
+				menu = [...props.info.actions.map(x => ({
+					text: x.text,
+					icon: x.icon,
+					action: x.handler
+				})), menu.length > 0 ? null : undefined, ...menu];
+			}
+			if (props.info.share) {
+				if (menu.length > 0) menu.push(null);
+				menu.push({
+					text: i18n.locale.share,
+					icon: 'fas fa-share-alt',
+					action: share
+				});
+			}
+			if (props.menu) {
+				if (menu.length > 0) menu.push(null);
+				menu = menu.concat(props.menu);
+			}
+			popupMenu(menu, ev.currentTarget || ev.target);
+		};
+
+		const showTabsPopup = (ev: MouseEvent) => {
+			if (!hasTabs.value) return;
+			if (!narrow.value) return;
+			ev.preventDefault();
+			ev.stopPropagation();
+			const menu = props.info.tabs.map(tab => ({
+				text: tab.title,
+				icon: tab.icon,
+				action: tab.onClick,
+			}));
+			popupMenu(menu, ev.currentTarget || ev.target);
+		};
+
+		const preventDrag = (ev: TouchEvent) => {
+			ev.stopPropagation();
+		};
+
+		const onClick = () => {
+			scrollToTop(el.value, { behavior: 'smooth' });
+		};
+
+		const calcBg = () => {
+			const rawBg = props.info?.bg || 'var(--bg)';
+			const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+			tinyBg.setAlpha(0.85);
+			bg.value = tinyBg.toRgbString();
+		};
+
+		onMounted(() => {
+			calcBg();
+			globalEvents.on('themeChanged', calcBg);
+			onUnmounted(() => {
+				globalEvents.off('themeChanged', calcBg);
+			});
+		
+			if (el.value.parentElement) {
+				narrow.value = el.value.parentElement.offsetWidth < 500;
+				const ro = new ResizeObserver((entries, observer) => {
+					if (el.value) {
+						narrow.value = el.value.parentElement.offsetWidth < 500;
+					}
+				});
+				ro.observe(el.value.parentElement);
+				onUnmounted(() => {
+					ro.disconnect();
+				});
+			}
+		});
+
+		return {
+			el,
+			bg,
+			narrow,
+			height,
+			hasTabs,
+			shouldShowMenu,
+			share,
+			showMenu,
+			showTabsPopup,
+			preventDrag,
+			onClick,
+			hideTitle: inject('shouldOmitHeaderTitle', false),
+			thin_: props.thin || inject('shouldHeaderThin', false)
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.fdidabkb {
+	--height: 60px;
+	display: flex;
+	position: sticky;
+	top: var(--stickyTop, 0);
+	z-index: 1000;
+	width: 100%;
+	-webkit-backdrop-filter: var(--blur, blur(15px));
+	backdrop-filter: var(--blur, blur(15px));
+	border-bottom: solid 0.5px var(--divider);
+
+	&.thin {
+		--height: 50px;
+
+		> .buttons {
+			> .button {
+				font-size: 0.9em;
+			}
+		}
+	}
+
+	&.slim {
+		text-align: center;
+
+		> .titleContainer {
+			flex: 1;
+			margin: 0 auto;
+			margin-left: var(--height);
+
+			> *:first-child {
+				margin-left: auto;
+			}
+
+			> *:last-child {
+				margin-right: auto;
+			}
+		}
+	}
+
+	> .buttons {
+		--margin: 8px;
+		display: flex;
+    align-items: center;
+		height: var(--height);
+		margin: 0 var(--margin);
+
+		&.right {
+			margin-left: auto;
+		}
+
+		&:empty {
+			width: var(--height);
+		}
+
+		> .button {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			height: calc(var(--height) - (var(--margin) * 2));
+			width: calc(var(--height) - (var(--margin) * 2));
+			box-sizing: border-box;
+			position: relative;
+			border-radius: 5px;
+
+			&:hover {
+				background: rgba(0, 0, 0, 0.05);
+			}
+
+			&.highlighted {
+				color: var(--accent);
+			}
+		}
+
+		> .fullButton {
+			& + .fullButton {
+				margin-left: 12px;
+			}
+		}
+	}
+
+	> .titleContainer {
+		display: flex;
+		align-items: center;
+		overflow: auto;
+		white-space: nowrap;
+		text-align: left;
+		font-weight: bold;
+		flex-shrink: 0;
+		margin-left: 24px;
+
+		> .avatar {
+			$size: 32px;
+			display: inline-block;
+			width: $size;
+			height: $size;
+			vertical-align: bottom;
+			margin: 0 8px;
+			pointer-events: none;
+		}
+
+		> .icon {
+			margin-right: 8px;
+		}
+
+		> .title {
+			min-width: 0;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+			line-height: 1.1;
+
+			> .subtitle {
+				opacity: 0.6;
+				font-size: 0.8em;
+				font-weight: normal;
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+
+				&.activeTab {
+					text-align: center;
+
+					> .chevron {
+						display: inline-block;
+						margin-left: 6px;
+					}
+				}
+			}
+		}
+	}
+
+	> .tabs {
+		margin-left: 16px;
+		font-size: 0.8em;
+		overflow: auto;
+		white-space: nowrap;
+
+		> .tab {
+			display: inline-block;
+			position: relative;
+			padding: 0 10px;
+			height: 100%;
+			font-weight: normal;
+			opacity: 0.7;
+
+			&:hover {
+				opacity: 1;
+			}
+
+			&.active {
+				opacity: 1;
+
+				&:after {
+					content: "";
+					display: block;
+					position: absolute;
+					bottom: 0;
+					left: 0;
+					right: 0;
+					margin: 0 auto;
+					width: 100%;
+					height: 3px;
+					background: var(--accent);
+				}
+			}
+
+			> .icon + .title {
+				margin-left: 8px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/i18n.ts b/packages/client/src/components/global/i18n.ts
new file mode 100644
index 0000000000..abf0c96856
--- /dev/null
+++ b/packages/client/src/components/global/i18n.ts
@@ -0,0 +1,42 @@
+import { h, defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		src: {
+			type: String,
+			required: true,
+		},
+		tag: {
+			type: String,
+			required: false,
+			default: 'span',
+		},
+		textTag: {
+			type: String,
+			required: false,
+			default: null,
+		},
+	},
+	render() {
+		let str = this.src;
+		const parsed = [] as (string | { arg: string; })[];
+		while (true) {
+			const nextBracketOpen = str.indexOf('{');
+			const nextBracketClose = str.indexOf('}');
+
+			if (nextBracketOpen === -1) {
+				parsed.push(str);
+				break;
+			} else {
+				if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
+				parsed.push({
+					arg: str.substring(nextBracketOpen + 1, nextBracketClose)
+				});
+			}
+
+			str = str.substr(nextBracketClose + 1);
+		}
+
+		return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]()));
+	}
+});
diff --git a/packages/client/src/components/global/loading.vue b/packages/client/src/components/global/loading.vue
new file mode 100644
index 0000000000..7bde53c12e
--- /dev/null
+++ b/packages/client/src/components/global/loading.vue
@@ -0,0 +1,92 @@
+<template>
+<div class="yxspomdl" :class="{ inline, colored, mini }">
+	<div class="ring"></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		colored: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+		mini: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+@keyframes ring {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
+
+.yxspomdl {
+	padding: 32px;
+	text-align: center;
+	cursor: wait;
+
+	--size: 48px;
+
+	&.colored {
+		color: var(--accent);
+	}
+
+	&.inline {
+		display: inline;
+		padding: 0;
+		--size: 32px;
+	}
+
+	&.mini {
+		padding: 16px;
+		--size: 32px;
+	}
+
+	> .ring {
+		position: relative;
+		display: inline-block;
+		vertical-align: middle;
+
+		&:before,
+		&:after {
+			content: " ";
+			display: block;
+			box-sizing: border-box;
+			width: var(--size);
+			height: var(--size);
+			border-radius: 50%;
+			border: solid 4px;
+		}
+
+		&:before {
+			border-color: currentColor;
+			opacity: 0.3;
+		}
+
+		&:after {
+			position: absolute;
+			top: 0;
+			border-color: currentColor transparent transparent transparent;
+			animation: ring 0.5s linear infinite;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue
new file mode 100644
index 0000000000..ab20404909
--- /dev/null
+++ b/packages/client/src/components/global/misskey-flavored-markdown.vue
@@ -0,0 +1,157 @@
+<template>
+<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MfmCore from '@/components/mfm';
+
+export default defineComponent({
+	components: {
+		MfmCore
+	}
+});
+</script>
+
+<style lang="scss">
+._mfm_blur_ {
+	filter: blur(6px);
+	transition: filter 0.3s;
+
+	&:hover {
+		filter: blur(0px);
+	}
+}
+
+@keyframes mfm-spin {
+	0% { transform: rotate(0deg); }
+	100% { transform: rotate(360deg); }
+}
+
+@keyframes mfm-spinX {
+	0% { transform: perspective(128px) rotateX(0deg); }
+	100% { transform: perspective(128px) rotateX(360deg); }
+}
+
+@keyframes mfm-spinY {
+	0% { transform: perspective(128px) rotateY(0deg); }
+	100% { transform: perspective(128px) rotateY(360deg); }
+}
+
+@keyframes mfm-jump {
+	0% { transform: translateY(0); }
+	25% { transform: translateY(-16px); }
+	50% { transform: translateY(0); }
+	75% { transform: translateY(-8px); }
+	100% { transform: translateY(0); }
+}
+
+@keyframes mfm-bounce {
+	0% { transform: translateY(0) scale(1, 1); }
+	25% { transform: translateY(-16px) scale(1, 1); }
+	50% { transform: translateY(0) scale(1, 1); }
+	75% { transform: translateY(0) scale(1.5, 0.75); }
+	100% { transform: translateY(0) scale(1, 1); }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-twitch {
+	0% { transform: translate(7px, -2px) }
+	5% { transform: translate(-3px, 1px) }
+	10% { transform: translate(-7px, -1px) }
+	15% { transform: translate(0px, -1px) }
+	20% { transform: translate(-8px, 6px) }
+	25% { transform: translate(-4px, -3px) }
+	30% { transform: translate(-4px, -6px) }
+	35% { transform: translate(-8px, -8px) }
+	40% { transform: translate(4px, 6px) }
+	45% { transform: translate(-3px, 1px) }
+	50% { transform: translate(2px, -10px) }
+	55% { transform: translate(-7px, 0px) }
+	60% { transform: translate(-2px, 4px) }
+	65% { transform: translate(3px, -8px) }
+	70% { transform: translate(6px, 7px) }
+	75% { transform: translate(-7px, -2px) }
+	80% { transform: translate(-7px, -8px) }
+	85% { transform: translate(9px, 3px) }
+	90% { transform: translate(-3px, -2px) }
+	95% { transform: translate(-10px, 2px) }
+	100% { transform: translate(-2px, -6px) }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-shake {
+	0% { transform: translate(-3px, -1px) rotate(-8deg) }
+	5% { transform: translate(0px, -1px) rotate(-10deg) }
+	10% { transform: translate(1px, -3px) rotate(0deg) }
+	15% { transform: translate(1px, 1px) rotate(11deg) }
+	20% { transform: translate(-2px, 1px) rotate(1deg) }
+	25% { transform: translate(-1px, -2px) rotate(-2deg) }
+	30% { transform: translate(-1px, 2px) rotate(-3deg) }
+	35% { transform: translate(2px, 1px) rotate(6deg) }
+	40% { transform: translate(-2px, -3px) rotate(-9deg) }
+	45% { transform: translate(0px, -1px) rotate(-12deg) }
+	50% { transform: translate(1px, 2px) rotate(10deg) }
+	55% { transform: translate(0px, -3px) rotate(8deg) }
+	60% { transform: translate(1px, -1px) rotate(8deg) }
+	65% { transform: translate(0px, -1px) rotate(-7deg) }
+	70% { transform: translate(-1px, -3px) rotate(6deg) }
+	75% { transform: translate(0px, -2px) rotate(4deg) }
+	80% { transform: translate(-2px, -1px) rotate(3deg) }
+	85% { transform: translate(1px, -3px) rotate(-10deg) }
+	90% { transform: translate(1px, 0px) rotate(3deg) }
+	95% { transform: translate(-2px, 0px) rotate(-3deg) }
+	100% { transform: translate(2px, 1px) rotate(2deg) }
+}
+
+@keyframes mfm-rubberBand {
+	from { transform: scale3d(1, 1, 1); }
+	30% { transform: scale3d(1.25, 0.75, 1); }
+	40% { transform: scale3d(0.75, 1.25, 1); }
+	50% { transform: scale3d(1.15, 0.85, 1); }
+	65% { transform: scale3d(0.95, 1.05, 1); }
+	75% { transform: scale3d(1.05, 0.95, 1); }
+	to { transform: scale3d(1, 1, 1); }
+}
+
+@keyframes mfm-rainbow {
+	0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+	100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
+</style>
+
+<style lang="scss" scoped>
+.havbbuyv {
+	white-space: pre-wrap;
+
+	&.nowrap {
+		white-space: pre;
+		word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	::v-deep(.quote) {
+		display: block;
+		margin: 8px;
+		padding: 6px 0 6px 12px;
+		color: var(--fg);
+		border-left: solid 3px var(--fg);
+		opacity: 0.7;
+	}
+
+	::v-deep(pre) {
+		font-size: 0.8em;
+	}
+
+	> ::v-deep(code) {
+		font-size: 0.8em;
+		word-break: break-all;
+		padding: 4px 6px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/spacer.vue b/packages/client/src/components/global/spacer.vue
new file mode 100644
index 0000000000..1129d54c71
--- /dev/null
+++ b/packages/client/src/components/global/spacer.vue
@@ -0,0 +1,76 @@
+<template>
+<div ref="root" :class="$style.root" :style="{ padding: margin + 'px' }">
+	<div ref="content" :class="$style.content">
+		<slot></slot>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+
+export default defineComponent({
+	props: {
+		contentMax: {
+			type: Number,
+			required: false,
+			default: null,
+		}
+	},
+
+	setup(props, context) {
+		let ro: ResizeObserver;
+		const root = ref<HTMLElement>(null);
+		const content = ref<HTMLElement>(null);
+		const margin = ref(0);
+		const adjust = (rect: { width: number; height: number; }) => {
+			if (rect.width > (props.contentMax || 500)) {
+				margin.value = 32;
+			} else {
+				margin.value = 12;
+			}
+		};
+
+		onMounted(() => {
+			ro = new ResizeObserver((entries) => {
+				/* iOSが対応していない
+				adjust({
+					width: entries[0].borderBoxSize[0].inlineSize,
+					height: entries[0].borderBoxSize[0].blockSize,
+				});
+				*/
+				adjust({
+					width: root.value.offsetWidth,
+					height: root.value.offsetHeight,
+				});
+			});
+			ro.observe(root.value);
+
+			if (props.contentMax) {
+				content.value.style.maxWidth = `${props.contentMax}px`;
+			}
+		});
+
+		onUnmounted(() => {
+			ro.disconnect();
+		});
+
+		return {
+			root,
+			content,
+			margin,
+		};
+	},
+});
+</script>
+
+<style lang="scss" module>
+.root {
+	box-sizing: border-box;
+	width: 100%;
+}
+
+.content {
+	margin: 0 auto;
+}
+</style>
diff --git a/packages/client/src/components/global/sticky-container.vue b/packages/client/src/components/global/sticky-container.vue
new file mode 100644
index 0000000000..859b2c1d73
--- /dev/null
+++ b/packages/client/src/components/global/sticky-container.vue
@@ -0,0 +1,74 @@
+<template>
+<div ref="rootEl">
+	<slot name="header"></slot>
+	<div ref="bodyEl">
+		<slot></slot>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+
+export default defineComponent({
+	props: {
+		autoSticky: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+	},
+
+	setup(props, context) {
+		const rootEl = ref<HTMLElement>(null);
+		const bodyEl = ref<HTMLElement>(null);
+
+		const calc = () => {
+			const currentStickyTop = getComputedStyle(rootEl.value).getPropertyValue('--stickyTop') || '0px';
+
+			const header = rootEl.value.children[0];
+			if (header === bodyEl.value) {
+				bodyEl.value.style.setProperty('--stickyTop', currentStickyTop);
+			} else {
+				bodyEl.value.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
+
+				if (props.autoSticky) {
+					header.style.setProperty('--stickyTop', currentStickyTop);
+					header.style.position = 'sticky';
+					header.style.top = 'var(--stickyTop)';
+					header.style.zIndex = '1';
+				}
+			}
+		};
+
+		onMounted(() => {
+			calc();
+
+			const observer = new MutationObserver(() => {
+				setTimeout(() => {
+					calc();
+				}, 100);
+			});
+
+			observer.observe(rootEl.value, {
+				attributes: false,
+				childList: true,
+				subtree: false,
+			});
+
+			onUnmounted(() => {
+				observer.disconnect();
+			});
+		});
+
+		return {
+			rootEl,
+			bodyEl,
+		};
+	},
+});
+</script>
+
+<style lang="scss" module>
+
+</style>
diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue
new file mode 100644
index 0000000000..6a330a2307
--- /dev/null
+++ b/packages/client/src/components/global/time.vue
@@ -0,0 +1,73 @@
+<template>
+<time :title="absolute">
+	<template v-if="mode == 'relative'">{{ relative }}</template>
+	<template v-else-if="mode == 'absolute'">{{ absolute }}</template>
+	<template v-else-if="mode == 'detail'">{{ absolute }} ({{ relative }})</template>
+</time>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		time: {
+			type: [Date, String],
+			required: true
+		},
+		mode: {
+			type: String,
+			default: 'relative'
+		}
+	},
+	data() {
+		return {
+			tickId: null,
+			now: new Date()
+		};
+	},
+	computed: {
+		_time(): Date {
+			return typeof this.time == 'string' ? new Date(this.time) : this.time;
+		},
+		absolute(): string {
+			return this._time.toLocaleString();
+		},
+		relative(): string {
+			const time = this._time;
+			const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
+			return (
+				ago >= 31536000 ? this.$t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
+				ago >= 2592000  ? this.$t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
+				ago >= 604800   ? this.$t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
+				ago >= 86400    ? this.$t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
+				ago >= 3600     ? this.$t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
+				ago >= 60       ? this.$t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
+				ago >= 10       ? this.$t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
+				ago >= -1       ? this.$ts._ago.justNow :
+				ago <  -1       ? this.$ts._ago.future :
+				this.$ts._ago.unknown);
+		}
+	},
+	created() {
+		if (this.mode == 'relative' || this.mode == 'detail') {
+			this.tickId = window.requestAnimationFrame(this.tick);
+		}
+	},
+	unmounted() {
+		if (this.mode === 'relative' || this.mode === 'detail') {
+			window.clearTimeout(this.tickId);
+		}
+	},
+	methods: {
+		tick() {
+			// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
+			this.now = new Date();
+
+			this.tickId = setTimeout(() => {
+				window.requestAnimationFrame(this.tick);
+			}, 10000);
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/global/url.vue b/packages/client/src/components/global/url.vue
new file mode 100644
index 0000000000..092fe6620c
--- /dev/null
+++ b/packages/client/src/components/global/url.vue
@@ -0,0 +1,142 @@
+<template>
+<component :is="self ? 'MkA' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
+	@mouseover="onMouseover"
+	@mouseleave="onMouseleave"
+	@contextmenu.stop="() => {}"
+>
+	<template v-if="!self">
+		<span class="schema">{{ schema }}//</span>
+		<span class="hostname">{{ hostname }}</span>
+		<span class="port" v-if="port != ''">:{{ port }}</span>
+	</template>
+	<template v-if="pathname === '/' && self">
+		<span class="self">{{ hostname }}</span>
+	</template>
+	<span class="pathname" v-if="pathname != ''">{{ self ? pathname.substr(1) : pathname }}</span>
+	<span class="query">{{ query }}</span>
+	<span class="hash">{{ hash }}</span>
+	<i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i>
+</component>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { toUnicode as decodePunycode } from 'punycode/';
+import { url as local } from '@/config';
+import { isDeviceTouch } from '@/scripts/is-device-touch';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		url: {
+			type: String,
+			required: true,
+		},
+		rel: {
+			type: String,
+			required: false,
+		}
+	},
+	data() {
+		const self = this.url.startsWith(local);
+		return {
+			local,
+			schema: null as string | null,
+			hostname: null as string | null,
+			port: null as string | null,
+			pathname: null as string | null,
+			query: null as string | null,
+			hash: null as string | null,
+			self: self,
+			attr: self ? 'to' : 'href',
+			target: self ? null : '_blank',
+			showTimer: null,
+			hideTimer: null,
+			checkTimer: null,
+			close: null,
+		};
+	},
+	created() {
+		const url = new URL(this.url);
+		this.schema = url.protocol;
+		this.hostname = decodePunycode(url.hostname);
+		this.port = url.port;
+		this.pathname = decodeURIComponent(url.pathname);
+		this.query = decodeURIComponent(url.search);
+		this.hash = decodeURIComponent(url.hash);
+	},
+	methods: {
+		async showPreview() {
+			if (!document.body.contains(this.$el)) return;
+			if (this.close) return;
+
+			const { dispose } = await os.popup(import('@/components/url-preview-popup.vue'), {
+				url: this.url,
+				source: this.$el
+			});
+
+			this.close = () => {
+				dispose();
+			};
+
+			this.checkTimer = setInterval(() => {
+				if (!document.body.contains(this.$el)) this.closePreview();
+			}, 1000);
+		},
+		closePreview() {
+			if (this.close) {
+				clearInterval(this.checkTimer);
+				this.close();
+				this.close = null;
+			}
+		},
+		onMouseover() {
+			if (isDeviceTouch) return;
+			clearTimeout(this.showTimer);
+			clearTimeout(this.hideTimer);
+			this.showTimer = setTimeout(this.showPreview, 500);
+		},
+		onMouseleave() {
+			if (isDeviceTouch) return;
+			clearTimeout(this.showTimer);
+			clearTimeout(this.hideTimer);
+			this.hideTimer = setTimeout(this.closePreview, 500);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ieqqeuvs {
+	word-break: break-all;
+
+	> .icon {
+		padding-left: 2px;
+		font-size: .9em;
+	}
+
+	> .self {
+		font-weight: bold;
+	}
+
+	> .schema {
+		opacity: 0.5;
+	}
+
+	> .hostname {
+		font-weight: bold;
+	}
+
+	> .pathname {
+		opacity: 0.8;
+	}
+
+	> .query {
+		opacity: 0.5;
+	}
+
+	> .hash {
+		font-style: italic;
+	}
+}
+</style>
diff --git a/packages/client/src/components/global/user-name.vue b/packages/client/src/components/global/user-name.vue
new file mode 100644
index 0000000000..bc93a8ea30
--- /dev/null
+++ b/packages/client/src/components/global/user-name.vue
@@ -0,0 +1,20 @@
+<template>
+<Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+		nowrap: {
+			type: Boolean,
+			default: true
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/components/google.vue b/packages/client/src/components/google.vue
new file mode 100644
index 0000000000..c48feffbf1
--- /dev/null
+++ b/packages/client/src/components/google.vue
@@ -0,0 +1,64 @@
+<template>
+<div class="mk-google">
+	<input type="search" v-model="query" :placeholder="q">
+	<button @click="search"><i class="fas fa-search"></i> {{ $ts.search }}</button>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		q: {
+			type: String,
+			required: true,
+		}
+	},
+	data() {
+		return {
+			query: null,
+		};
+	},
+	mounted() {
+		this.query = this.q;
+	},
+	methods: {
+		search() {
+			window.open(`https://www.google.com/search?q=${this.query}`, '_blank');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-google {
+	display: flex;
+	margin: 8px 0;
+
+	> input {
+		flex-shrink: 1;
+		padding: 10px;
+		width: 100%;
+		height: 40px;
+		font-size: 16px;
+		border: solid 1px var(--divider);
+		border-radius: 4px 0 0 4px;
+		-webkit-appearance: textfield;
+	}
+
+	> button {
+		flex-shrink: 0;
+		margin: 0;
+		padding: 0 16px;
+		border: solid 1px var(--divider);
+		border-left: none;
+		border-radius: 0 4px 4px 0;
+
+		&:active {
+			box-shadow: 0 2px 4px rgba(#000, 0.15) inset;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/image-viewer.vue b/packages/client/src/components/image-viewer.vue
new file mode 100644
index 0000000000..fc28c30b56
--- /dev/null
+++ b/packages/client/src/components/image-viewer.vue
@@ -0,0 +1,85 @@
+<template>
+<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
+	<div class="xubzgfga">
+		<header>{{ image.name }}</header>
+		<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
+		<footer>
+			<span>{{ image.type }}</span>
+			<span>{{ bytes(image.size) }}</span>
+			<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
+		</footer>
+	</div>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import bytes from '@/filters/bytes';
+import number from '@/filters/number';
+import MkModal from '@/components/ui/modal.vue';
+
+export default defineComponent({
+	components: {
+		MkModal,
+	},
+
+	props: {
+		image: {
+			type: Object,
+			required: true
+		},
+	},
+
+	emits: ['closed'],
+
+	methods: {
+		bytes,
+		number,
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xubzgfga {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+
+	> header,
+	> footer {
+		align-self: center;
+		display: inline-block;
+		padding: 6px 9px;
+		font-size: 90%;
+		background: rgba(0, 0, 0, 0.5);
+		border-radius: 6px;
+		color: #fff;
+	}
+
+	> header {
+		margin-bottom: 8px;
+		opacity: 0.9;
+	}
+
+	> img {
+		display: block;
+		flex: 1;
+		min-height: 0;
+		object-fit: contain;
+		width: 100%;
+		cursor: zoom-out;
+		image-orientation: from-image;
+	}
+
+	> footer {
+		margin-top: 8px;
+		opacity: 0.8;
+
+		> span + span {
+			margin-left: 0.5em;
+			padding-left: 0.5em;
+			border-left: solid 1px rgba(255, 255, 255, 0.5);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/img-with-blurhash.vue b/packages/client/src/components/img-with-blurhash.vue
new file mode 100644
index 0000000000..7e80b00208
--- /dev/null
+++ b/packages/client/src/components/img-with-blurhash.vue
@@ -0,0 +1,100 @@
+<template>
+<div class="xubzgfgb" :class="{ cover }" :title="title">
+	<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/>
+	<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { decode } from 'blurhash';
+
+export default defineComponent({
+	props: {
+		src: {
+			type: String,
+			required: false,
+			default: null
+		},
+		hash: {
+			type: String,
+			required: true
+		},
+		alt: {
+			type: String,
+			required: false,
+			default: '',
+		},
+		title: {
+			type: String,
+			required: false,
+			default: null,
+		},
+		size: {
+			type: Number,
+			required: false,
+			default: 64
+		},
+		cover: {
+			type: Boolean,
+			required: false,
+			default: true,
+		}
+	},
+
+	data() {
+		return {
+			loaded: false,
+		};
+	},
+
+	mounted() {
+		this.draw();
+	},
+
+	methods: {
+		draw() {
+			if (this.hash == null) return;
+			const pixels = decode(this.hash, this.size, this.size);
+			const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
+			const imageData = ctx!.createImageData(this.size, this.size);
+			imageData.data.set(pixels);
+			ctx!.putImageData(imageData, 0, 0);
+		},
+
+		onLoad() {
+			this.loaded = true;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xubzgfgb {
+	position: relative;
+	width: 100%;
+	height: 100%;
+
+	> canvas,
+	> img {
+		display: block;
+		width: 100%;
+		height: 100%;
+	}
+
+	> canvas {
+		position: absolute;
+		object-fit: cover;
+	}
+
+	> img {
+		object-fit: contain;
+	}
+
+	&.cover {
+		> img {
+			object-fit: cover;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts
new file mode 100644
index 0000000000..2340b228f8
--- /dev/null
+++ b/packages/client/src/components/index.ts
@@ -0,0 +1,37 @@
+import { App } from 'vue';
+
+import mfm from './global/misskey-flavored-markdown.vue';
+import a from './global/a.vue';
+import acct from './global/acct.vue';
+import avatar from './global/avatar.vue';
+import emoji from './global/emoji.vue';
+import userName from './global/user-name.vue';
+import ellipsis from './global/ellipsis.vue';
+import time from './global/time.vue';
+import url from './global/url.vue';
+import i18n from './global/i18n';
+import loading from './global/loading.vue';
+import error from './global/error.vue';
+import ad from './global/ad.vue';
+import header from './global/header.vue';
+import spacer from './global/spacer.vue';
+import stickyContainer from './global/sticky-container.vue';
+
+export default function(app: App) {
+	app.component('I18n', i18n);
+	app.component('Mfm', mfm);
+	app.component('MkA', a);
+	app.component('MkAcct', acct);
+	app.component('MkAvatar', avatar);
+	app.component('MkEmoji', emoji);
+	app.component('MkUserName', userName);
+	app.component('MkEllipsis', ellipsis);
+	app.component('MkTime', time);
+	app.component('MkUrl', url);
+	app.component('MkLoading', loading);
+	app.component('MkError', error);
+	app.component('MkAd', ad);
+	app.component('MkHeader', header);
+	app.component('MkSpacer', spacer);
+	app.component('MkStickyContainer', stickyContainer);
+}
diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue
new file mode 100644
index 0000000000..bc62998a4a
--- /dev/null
+++ b/packages/client/src/components/instance-stats.vue
@@ -0,0 +1,80 @@
+<template>
+<div class="zbcjwnqg" style="margin-top: -8px;">
+	<div class="selects" style="display: flex;">
+		<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
+			<optgroup :label="$ts.federation">
+				<option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option>
+				<option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option>
+			</optgroup>
+			<optgroup :label="$ts.users">
+				<option value="users">{{ $ts._charts.usersIncDec }}</option>
+				<option value="users-total">{{ $ts._charts.usersTotal }}</option>
+				<option value="active-users">{{ $ts._charts.activeUsers }}</option>
+			</optgroup>
+			<optgroup :label="$ts.notes">
+				<option value="notes">{{ $ts._charts.notesIncDec }}</option>
+				<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
+				<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
+				<option value="notes-total">{{ $ts._charts.notesTotal }}</option>
+			</optgroup>
+			<optgroup :label="$ts.drive">
+				<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
+				<option value="drive-files-total">{{ $ts._charts.filesTotal }}</option>
+				<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
+				<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
+			</optgroup>
+		</MkSelect>
+		<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
+			<option value="hour">{{ $ts.perHour }}</option>
+			<option value="day">{{ $ts.perDay }}</option>
+		</MkSelect>
+	</div>
+	<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, ref, watch } from 'vue';
+import MkSelect from '@/components/form/select.vue';
+import MkChart from '@/components/chart.vue';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+
+export default defineComponent({
+	components: {
+		MkSelect,
+		MkChart,
+	},
+
+	props: {
+		chartLimit: {
+			type: Number,
+			required: false,
+			default: 90
+		},
+		detailed: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	setup() {
+		const chartSpan = ref<'hour' | 'day'>('hour');
+		const chartSrc = ref('notes');
+
+		return {
+			chartSrc,
+			chartSpan,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.zbcjwnqg {
+	> .selects {
+		padding: 8px 16px 0 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue
new file mode 100644
index 0000000000..1ce5a1c2c1
--- /dev/null
+++ b/packages/client/src/components/instance-ticker.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="hpaizdrt" :style="bg">
+	<img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/>
+	<span class="name">{{ info.name }}</span>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { instanceName } from '@/config';
+
+export default defineComponent({
+	props: {
+		instance: {
+			type: Object,
+			required: false
+		},
+	},
+
+	data() {
+		return {
+			info: this.instance || {
+				faviconUrl: '/favicon.ico',
+				name: instanceName,
+				themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
+			}
+		}
+	},
+
+	computed: {
+		bg(): any {
+			const themeColor = this.info.themeColor || '#777777';
+			return {
+				background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
+			};
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hpaizdrt {
+	$height: 1.1rem;
+
+	height: $height;
+	border-radius: 4px 0 0 4px;
+	overflow: hidden;
+	color: #fff;
+
+	> .icon {
+		height: 100%;
+	}
+
+	> .name {
+		margin-left: 4px;
+		line-height: $height;
+		font-size: 0.9em;
+		vertical-align: top;
+		font-weight: bold;
+	}
+}
+</style>
diff --git a/packages/client/src/components/launch-pad.vue b/packages/client/src/components/launch-pad.vue
new file mode 100644
index 0000000000..09f5f89f90
--- /dev/null
+++ b/packages/client/src/components/launch-pad.vue
@@ -0,0 +1,152 @@
+<template>
+<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
+	<div class="szkkfdyq _popup">
+		<div class="main">
+			<template v-for="item in items">
+				<button v-if="item.action" class="_button" @click="$event => { item.action($event); close(); }" v-click-anime>
+					<i class="icon" :class="item.icon"></i>
+					<div class="text">{{ item.text }}</div>
+					<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+				</button>
+				<MkA v-else :to="item.to" @click.passive="close()" v-click-anime>
+					<i class="icon" :class="item.icon"></i>
+					<div class="text">{{ item.text }}</div>
+					<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+				</MkA>
+			</template>
+		</div>
+		<div class="sub">
+			<a href="https://misskey-hub.net/help.html" target="_blank" @click.passive="close()" v-click-anime>
+				<i class="fas fa-question-circle icon"></i>
+				<div class="text">{{ $ts.help }}</div>
+			</a>
+			<MkA to="/about" @click.passive="close()" v-click-anime>
+				<i class="fas fa-info-circle icon"></i>
+				<div class="text">{{ $t('aboutX', { x: instanceName }) }}</div>
+			</MkA>
+			<MkA to="/about-misskey" @click.passive="close()" v-click-anime>
+				<img src="/static-assets/favicon.png" class="icon"/>
+				<div class="text">{{ $ts.aboutMisskey }}</div>
+			</MkA>
+		</div>
+	</div>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from '@/components/ui/modal.vue';
+import { menuDef } from '@/menu';
+import { instanceName } from '@/config';
+
+export default defineComponent({
+	components: {
+		MkModal,
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			menuDef: menuDef,
+			items: [],
+			instanceName,
+		};
+	},
+
+	computed: {
+		menu(): string[] {
+			return this.$store.state.menu;
+		},
+	},
+
+	created() {
+		this.items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
+			type: def.to ? 'link' : 'button',
+			text: this.$ts[def.title],
+			icon: def.icon,
+			to: def.to,
+			action: def.action,
+			indicate: def.indicated,
+		}));
+	},
+
+	methods: {
+		close() {
+			this.$refs.modal.close();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.szkkfdyq {
+	width: 100%;
+	max-height: 100%;
+	max-width: 800px;
+	padding: 32px;
+	box-sizing: border-box;
+	overflow: auto;
+	text-align: center;
+	border-radius: 16px;
+
+	@media (max-width: 500px) {
+		padding: 16px;
+	}
+	
+	> .main, > .sub {
+		> * {
+			position: relative;
+			display: inline-flex;
+			flex-direction: column;
+			align-items: center;
+			justify-content: center;
+			vertical-align: bottom;
+			width: 128px;
+			height: 128px;
+			border-radius: var(--radius);
+
+			@media (max-width: 500px) {
+				width: 100px;
+				height: 100px;
+			}
+
+			&:hover {
+				background: rgba(0, 0, 0, 0.05);
+				text-decoration: none;
+			}
+
+			> .icon {
+				font-size: 26px;
+				height: 32px;
+			}
+
+			> .text {
+				margin-top: 8px;
+				font-size: 0.9em;
+				line-height: 1.5em;
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 32px;
+				left: 32px;
+				color: var(--indicator);
+				font-size: 8px;
+				animation: blink 1s infinite;
+
+				@media (max-width: 500px) {
+					top: 16px;
+					left: 16px;
+				}
+			}
+		}
+	}
+
+	> .sub {
+		margin-top: 8px;
+		padding-top: 8px;
+		border-top: solid 0.5px var(--divider);
+	}
+}
+</style>
diff --git a/packages/client/src/components/link.vue b/packages/client/src/components/link.vue
new file mode 100644
index 0000000000..a8e096e0a0
--- /dev/null
+++ b/packages/client/src/components/link.vue
@@ -0,0 +1,92 @@
+<template>
+<component :is="self ? 'MkA' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
+	@mouseover="onMouseover"
+	@mouseleave="onMouseleave"
+	:title="url"
+>
+	<slot></slot>
+	<i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i>
+</component>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { url as local } from '@/config';
+import { isDeviceTouch } from '@/scripts/is-device-touch';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		url: {
+			type: String,
+			required: true,
+		},
+		rel: {
+			type: String,
+			required: false,
+		}
+	},
+	data() {
+		const self = this.url.startsWith(local);
+		return {
+			local,
+			self: self,
+			attr: self ? 'to' : 'href',
+			target: self ? null : '_blank',
+			showTimer: null,
+			hideTimer: null,
+			checkTimer: null,
+			close: null,
+		};
+	},
+	methods: {
+		async showPreview() {
+			if (!document.body.contains(this.$el)) return;
+			if (this.close) return;
+
+			const { dispose } = await os.popup(import('@/components/url-preview-popup.vue'), {
+				url: this.url,
+				source: this.$el
+			});
+
+			this.close = () => {
+				dispose();
+			};
+
+			this.checkTimer = setInterval(() => {
+				if (!document.body.contains(this.$el)) this.closePreview();
+			}, 1000);
+		},
+		closePreview() {
+			if (this.close) {
+				clearInterval(this.checkTimer);
+				this.close();
+				this.close = null;
+			}
+		},
+		onMouseover() {
+			if (isDeviceTouch) return;
+			clearTimeout(this.showTimer);
+			clearTimeout(this.hideTimer);
+			this.showTimer = setTimeout(this.showPreview, 500);
+		},
+		onMouseleave() {
+			if (isDeviceTouch) return;
+			clearTimeout(this.showTimer);
+			clearTimeout(this.hideTimer);
+			this.hideTimer = setTimeout(this.closePreview, 500);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xlcxczvw {
+	word-break: break-all;
+
+	> .icon {
+		padding-left: 2px;
+		font-size: .9em;
+	}
+}
+</style>
diff --git a/packages/client/src/components/media-banner.vue b/packages/client/src/components/media-banner.vue
new file mode 100644
index 0000000000..2cf8c772e5
--- /dev/null
+++ b/packages/client/src/components/media-banner.vue
@@ -0,0 +1,107 @@
+<template>
+<div class="mk-media-banner">
+	<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
+		<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
+		<b>{{ $ts.sensitive }}</b>
+		<span>{{ $ts.clickToShow }}</span>
+	</div>
+	<div class="audio" v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'">
+		<audio class="audio"
+			:src="media.url"
+			:title="media.name"
+			controls
+			ref="audio"
+			@volumechange="volumechange"
+			preload="metadata" />
+	</div>
+	<a class="download" v-else
+		:href="media.url"
+		:title="media.name"
+		:download="media.name"
+	>
+		<span class="icon"><i class="fas fa-download"></i></span>
+		<b>{{ media.name }}</b>
+	</a>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+
+export default defineComponent({
+	props: {
+		media: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			hide: true,
+		};
+	},
+	mounted() {
+		const audioTag = this.$refs.audio as HTMLAudioElement;
+		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
+	},
+	methods: {
+		volumechange() {
+			const audioTag = this.$refs.audio as HTMLAudioElement;
+			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
+		},
+	},
+})
+</script>
+
+<style lang="scss" scoped>
+.mk-media-banner {
+	width: 100%;
+	border-radius: 4px;
+	margin-top: 4px;
+	overflow: hidden;
+
+	> .download,
+	> .sensitive {
+		display: flex;
+		align-items: center;
+		font-size: 12px;
+		padding: 8px 12px;
+		white-space: nowrap;
+
+		> * {
+			display: block;
+		}
+
+		> b {
+			overflow: hidden;
+			text-overflow: ellipsis;
+		}
+
+		> *:not(:last-child) {
+			margin-right: .2em;
+		}
+
+		> .icon {
+			font-size: 1.6em;
+		}
+	}
+
+	> .download {
+		background: var(--noteAttachedFile);
+	}
+
+	> .sensitive {
+		background: #111;
+		color: #fff;
+	}
+
+	> .audio {
+		.audio {
+			display: block;
+			width: 100%;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/media-caption.vue b/packages/client/src/components/media-caption.vue
new file mode 100644
index 0000000000..08a3ca2b4c
--- /dev/null
+++ b/packages/client/src/components/media-caption.vue
@@ -0,0 +1,259 @@
+<template>
+	<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
+		<div class="container">
+			<div class="fullwidth top-caption">
+				<div class="mk-dialog">
+					<header>
+						<Mfm v-if="title" class="title" :text="title"/>
+						<span class="text-count" :class="{ over: remainingLength < 0 }">{{ remainingLength }}</span>
+					</header>
+					<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
+					<div class="buttons" v-if="(showOkButton || showCancelButton)">
+						<MkButton inline @click="ok" primary :disabled="remainingLength < 0">{{ $ts.ok }}</MkButton>
+						<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
+					</div>
+				</div>
+			</div>
+			<div class="hdrwpsaf fullwidth">
+				<header>{{ image.name }}</header>
+				<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
+				<footer>
+					<span>{{ image.type }}</span>
+					<span>{{ bytes(image.size) }}</span>
+					<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
+				</footer>
+			</div>
+		</div>
+	</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { length } from 'stringz';
+import MkModal from '@/components/ui/modal.vue';
+import MkButton from '@/components/ui/button.vue';
+import bytes from '@/filters/bytes';
+import number from '@/filters/number';
+
+export default defineComponent({
+	components: {
+		MkModal,
+		MkButton,
+	},
+
+	props: {
+		image: {
+			type: Object,
+			required: true,
+		},
+		title: {
+			type: String,
+			required: false
+		},
+		input: {
+			required: true
+		},
+		showOkButton: {
+			type: Boolean,
+			default: true
+		},
+		showCancelButton: {
+			type: Boolean,
+			default: true
+		},
+		cancelableByBgClick: {
+			type: Boolean,
+			default: true
+		},
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			inputValue: this.input.default ? this.input.default : null
+		};
+	},
+
+	computed: {
+		remainingLength(): number {
+			if (typeof this.inputValue != "string") return 512;
+			return 512 - length(this.inputValue);
+		}
+	},
+
+	mounted() {
+		document.addEventListener('keydown', this.onKeydown);
+	},
+
+	beforeUnmount() {
+		document.removeEventListener('keydown', this.onKeydown);
+	},
+
+	methods: {
+		bytes,
+		number,
+
+		done(canceled, result?) {
+			this.$emit('done', { canceled, result });
+			this.$refs.modal.close();
+		},
+
+		async ok() {
+			if (!this.showOkButton) return;
+
+			const result = this.inputValue;
+			this.done(false, result);
+		},
+
+		cancel() {
+			this.done(true);
+		},
+
+		onBgClick() {
+			if (this.cancelableByBgClick) {
+				this.cancel();
+			}
+		},
+
+		onKeydown(e) {
+			if (e.which === 27) { // ESC
+				this.cancel();
+			}
+		},
+
+		onInputKeydown(e) {
+			if (e.which === 13) { // Enter
+				if (e.ctrlKey) {
+					e.preventDefault();
+					e.stopPropagation();
+					this.ok();
+				}
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.container {
+	display: flex;
+	width: 100%;
+	height: 100%;
+	flex-direction: row;
+}
+@media (max-width: 850px) {
+	.container {
+		flex-direction: column;
+	}
+	.top-caption {
+		padding-bottom: 8px;
+	}
+}
+.fullwidth {
+	width: 100%;
+	margin: auto;
+}
+.mk-dialog {
+	position: relative;
+	padding: 32px;
+	min-width: 320px;
+	max-width: 480px;
+	box-sizing: border-box;
+	text-align: center;
+	background: var(--panel);
+	border-radius: var(--radius);
+	margin: auto;
+
+	> header {
+		margin: 0 0 8px 0;
+		position: relative;
+
+		> .title {
+			font-weight: bold;
+			font-size: 20px;
+		}
+
+		> .text-count {
+			opacity: 0.7;
+			position: absolute;
+			right: 0;
+		}
+	}
+
+	> .buttons {
+		margin-top: 16px;
+
+		> * {
+			margin: 0 8px;
+		}
+	}
+
+	> textarea {
+		display: block;
+		box-sizing: border-box;
+		padding: 0 24px;
+		margin: 0;
+		width: 100%;
+		font-size: 16px;
+		border: none;
+		border-radius: 0;
+		background: transparent;
+		color: var(--fg);
+		font-family: inherit;
+		max-width: 100%;
+		min-width: 100%;
+		min-height: 90px;
+
+		&:focus-visible {
+			outline: none;
+		}
+
+		&:disabled {
+			opacity: 0.5;
+		}
+	}
+}
+.hdrwpsaf {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+
+	> header,
+	> footer {
+		align-self: center;
+		display: inline-block;
+		padding: 6px 9px;
+		font-size: 90%;
+		background: rgba(0, 0, 0, 0.5);
+		border-radius: 6px;
+		color: #fff;
+	}
+
+	> header {
+		margin-bottom: 8px;
+		opacity: 0.9;
+	}
+
+	> img {
+		display: block;
+		flex: 1;
+		min-height: 0;
+		object-fit: contain;
+		width: 100%;
+		cursor: zoom-out;
+		image-orientation: from-image;
+	}
+
+	> footer {
+		margin-top: 8px;
+		opacity: 0.8;
+
+		> span + span {
+			margin-left: 0.5em;
+			padding-left: 0.5em;
+			border-left: solid 1px rgba(255, 255, 255, 0.5);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/media-image.vue b/packages/client/src/components/media-image.vue
new file mode 100644
index 0000000000..8843b63207
--- /dev/null
+++ b/packages/client/src/components/media-image.vue
@@ -0,0 +1,155 @@
+<template>
+<div class="qjewsnkg" v-if="hide" @click="hide = false">
+	<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
+	<div class="text">
+		<div>
+			<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
+			<span>{{ $ts.clickToShow }}</span>
+		</div>
+	</div>
+</div>
+<div class="gqnyydlz" :style="{ background: color }" v-else>
+	<a
+		:href="image.url"
+		:title="image.name"
+	>
+		<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/>
+		<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
+	</a>
+	<i class="fas fa-eye-slash" @click="hide = true"></i>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
+import ImageViewer from './image-viewer.vue';
+import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		ImgWithBlurhash
+	},
+	props: {
+		image: {
+			type: Object,
+			required: true
+		},
+		raw: {
+			default: false
+		}
+	},
+	data() {
+		return {
+			hide: true,
+			color: null,
+		};
+	},
+	computed: {
+		url(): any {
+			let url = this.$store.state.disableShowingAnimatedImages
+				? getStaticImageUrl(this.image.thumbnailUrl)
+				: this.image.thumbnailUrl;
+
+			if (this.raw || this.$store.state.loadRawImages) {
+				url = this.image.url;
+			}
+
+			return url;
+		}
+	},
+	created() {
+		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
+		this.$watch('image', () => {
+			this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore');
+			if (this.image.blurhash) {
+				this.color = extractAvgColorFromBlurhash(this.image.blurhash);
+			}
+		}, {
+			deep: true,
+			immediate: true,
+		});
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.qjewsnkg {
+	position: relative;
+
+	> .bg {
+		filter: brightness(0.5);
+	}
+
+	> .text {
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 100%;
+		height: 100%;
+		z-index: 1;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+
+		> div {
+			display: table-cell;
+			text-align: center;
+			font-size: 0.8em;
+			color: #fff;
+
+			> * {
+				display: block;
+			}
+		}
+	}
+}
+
+.gqnyydlz {
+	position: relative;
+	border: solid 0.5px var(--divider);
+
+	> i {
+		display: block;
+		position: absolute;
+		border-radius: 6px;
+		background-color: var(--fg);
+		color: var(--accentLighten);
+		font-size: 14px;
+		opacity: .5;
+		padding: 3px 6px;
+		text-align: center;
+		cursor: pointer;
+		top: 12px;
+		right: 12px;
+	}
+
+	> a {
+		display: block;
+		cursor: zoom-in;
+		overflow: hidden;
+		width: 100%;
+		height: 100%;
+		background-position: center;
+		background-size: contain;
+		background-repeat: no-repeat;
+
+		> .gif {
+			background-color: var(--fg);
+			border-radius: 6px;
+			color: var(--accentLighten);
+			display: inline-block;
+			font-size: 14px;
+			font-weight: bold;
+			left: 12px;
+			opacity: .5;
+			padding: 0 6px;
+			text-align: center;
+			top: 12px;
+			pointer-events: none;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue
new file mode 100644
index 0000000000..51eaa86f35
--- /dev/null
+++ b/packages/client/src/components/media-list.vue
@@ -0,0 +1,167 @@
+<template>
+<div class="hoawjimk">
+	<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :media="media" :key="media.id"/>
+	<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
+		<div :data-count="mediaList.filter(media => previewable(media)).length" ref="gallery">
+			<template v-for="media in mediaList">
+				<XVideo :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
+				<XImage class="image" :data-id="media.id" :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
+			</template>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, PropType, ref } from 'vue';
+import * as misskey from 'misskey-js';
+import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js';
+import PhotoSwipe from 'photoswipe/dist/photoswipe.esm.js';
+import 'photoswipe/dist/photoswipe.css';
+import XBanner from './media-banner.vue';
+import XImage from './media-image.vue';
+import XVideo from './media-video.vue';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+
+export default defineComponent({
+	components: {
+		XBanner,
+		XImage,
+		XVideo,
+	},
+	props: {
+		mediaList: {
+			type: Array as PropType<misskey.entities.DriveFile[]>,
+			required: true,
+		},
+		raw: {
+			default: false
+		},
+	},
+	setup(props) {
+		const gallery = ref(null);
+
+		onMounted(() => {
+			const lightbox = new PhotoSwipeLightbox({
+				dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({
+					src: media.url,
+					w: media.properties.width,
+					h: media.properties.height,
+					alt: media.name,
+				})),
+				gallery: gallery.value,
+				children: '.image',
+				thumbSelector: '.image',
+				pswpModule: PhotoSwipe
+			});
+
+			lightbox.on('itemData', (e) => {
+				const { itemData } = e;
+
+				// element is children
+				const { element } = itemData;
+
+				const id = element.dataset.id;
+				const file = props.mediaList.find(media => media.id === id);
+
+				itemData.src = file.url;
+				itemData.w = Number(file.properties.width);
+				itemData.h = Number(file.properties.height);
+				itemData.msrc = file.thumbnailUrl;
+				itemData.thumbCropped = true;
+			});
+
+			lightbox.init();
+		});
+
+		const previewable = (file: misskey.entities.DriveFile): boolean => {
+			return file.type.startsWith('video') || file.type.startsWith('image');
+		};
+
+		return {
+			previewable,
+			gallery,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.hoawjimk {
+	> .gird-container {
+		position: relative;
+		width: 100%;
+		margin-top: 4px;
+
+		&:before {
+			content: '';
+			display: block;
+			padding-top: 56.25% // 16:9;
+		}
+
+		> div {
+			position: absolute;
+			top: 0;
+			right: 0;
+			bottom: 0;
+			left: 0;
+			display: grid;
+			grid-gap: 4px;
+
+			> * {
+				overflow: hidden;
+				border-radius: 6px;
+			}
+
+			&[data-count="1"] {
+				grid-template-rows: 1fr;
+			}
+
+			&[data-count="2"] {
+				grid-template-columns: 1fr 1fr;
+				grid-template-rows: 1fr;
+			}
+
+			&[data-count="3"] {
+				grid-template-columns: 1fr 0.5fr;
+				grid-template-rows: 1fr 1fr;
+
+				> *:nth-child(1) {
+					grid-row: 1 / 3;
+				}
+
+				> *:nth-child(3) {
+					grid-column: 2 / 3;
+					grid-row: 2 / 3;
+				}
+			}
+
+			&[data-count="4"] {
+				grid-template-columns: 1fr 1fr;
+				grid-template-rows: 1fr 1fr;
+			}
+
+			> *:nth-child(1) {
+				grid-column: 1 / 2;
+				grid-row: 1 / 2;
+			}
+
+			> *:nth-child(2) {
+				grid-column: 2 / 3;
+				grid-row: 1 / 2;
+			}
+
+			> *:nth-child(3) {
+				grid-column: 1 / 2;
+				grid-row: 2 / 3;
+			}
+
+			> *:nth-child(4) {
+				grid-column: 2 / 3;
+				grid-row: 2 / 3;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/media-video.vue b/packages/client/src/components/media-video.vue
new file mode 100644
index 0000000000..aa885bd564
--- /dev/null
+++ b/packages/client/src/components/media-video.vue
@@ -0,0 +1,97 @@
+<template>
+<div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="hide" @click="hide = false">
+	<div>
+		<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
+		<span>{{ $ts.clickToShow }}</span>
+	</div>
+</div>
+<div class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else>
+	<video
+		:poster="video.thumbnailUrl"
+		:title="video.name"
+		preload="none"
+		controls
+		@contextmenu.stop
+	>
+		<source 
+			:src="video.url" 
+			:type="video.type"
+		>
+	</video>
+	<i class="fas fa-eye-slash" @click="hide = true"></i>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		video: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			hide: true,
+		};
+	},
+	created() {
+		this.hide = (this.$store.state.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.nsfw !== 'ignore');
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.kkjnbbplepmiyuadieoenjgutgcmtsvu {
+	position: relative;
+
+	> i {
+		display: block;
+		position: absolute;
+		border-radius: 6px;
+		background-color: var(--fg);
+		color: var(--accentLighten);
+		font-size: 14px;
+		opacity: .5;
+		padding: 3px 6px;
+		text-align: center;
+		cursor: pointer;
+		top: 12px;
+		right: 12px;
+	}
+
+	> video {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+
+		font-size: 3.5em;
+		overflow: hidden;
+		background-position: center;
+		background-size: cover;
+		width: 100%;
+		height: 100%;
+	}
+}
+
+.icozogqfvdetwohsdglrbswgrejoxbdj {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	background: #111;
+	color: #fff;
+
+	> div {
+		display: table-cell;
+		text-align: center;
+		font-size: 12px;
+
+		> b {
+			display: block;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/mention.vue b/packages/client/src/components/mention.vue
new file mode 100644
index 0000000000..a5be3fab22
--- /dev/null
+++ b/packages/client/src/components/mention.vue
@@ -0,0 +1,84 @@
+<template>
+<MkA v-if="url.startsWith('/')" class="ldlomzub" :class="{ isMe }" :to="url" v-user-preview="canonical" :style="{ background: bg }">
+	<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
+	<span class="main">
+		<span class="username">@{{ username }}</span>
+		<span class="host" v-if="(host != localHost) || $store.state.showFullAcct">@{{ toUnicode(host) }}</span>
+	</span>
+</MkA>
+<a v-else class="ldlomzub" :href="url" target="_blank" rel="noopener" :style="{ background: bg }">
+	<span class="main">
+		<span class="username">@{{ username }}</span>
+		<span class="host">@{{ toUnicode(host) }}</span>
+	</span>
+</a>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as tinycolor from 'tinycolor2';
+import { toUnicode } from 'punycode';
+import { host as localHost } from '@/config';
+import { $i } from '@/account';
+
+export default defineComponent({
+	props: {
+		username: {
+			type: String,
+			required: true
+		},
+		host: {
+			type: String,
+			required: true
+		}
+	},
+
+	setup(props) {
+		const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
+
+		const url = `/${canonical}`;
+
+		const isMe = $i && (
+			`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
+		);
+
+		const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
+		bg.setAlpha(0.1);
+
+		return {
+			localHost,
+			isMe,
+			url,
+			canonical,
+			toUnicode,
+			bg: bg.toRgbString(),
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.ldlomzub {
+	display: inline-block;
+	padding: 4px 8px 4px 4px;
+	border-radius: 999px;
+	color: var(--mention);
+
+	&.isMe {
+		color: var(--mentionMe);
+	}
+
+	> .icon {
+		width: 1.5em;
+		margin: 0 0.2em 0 0;
+		vertical-align: bottom;
+		border-radius: 100%;
+	}
+
+	> .main {
+		> .host {
+			opacity: 0.5;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts
new file mode 100644
index 0000000000..d41cf6fc2b
--- /dev/null
+++ b/packages/client/src/components/mfm.ts
@@ -0,0 +1,321 @@
+import { VNode, defineComponent, h } from 'vue';
+import * as mfm from 'mfm-js';
+import MkUrl from '@/components/global/url.vue';
+import MkLink from '@/components/link.vue';
+import MkMention from '@/components/mention.vue';
+import MkEmoji from '@/components/global/emoji.vue';
+import { concat } from '@/scripts/array';
+import MkFormula from '@/components/formula.vue';
+import MkCode from '@/components/code.vue';
+import MkGoogle from '@/components/google.vue';
+import MkSparkle from '@/components/sparkle.vue';
+import MkA from '@/components/global/a.vue';
+import { host } from '@/config';
+import { MFM_TAGS } from '@/scripts/mfm-tags';
+
+export default defineComponent({
+	props: {
+		text: {
+			type: String,
+			required: true
+		},
+		plain: {
+			type: Boolean,
+			default: false
+		},
+		nowrap: {
+			type: Boolean,
+			default: false
+		},
+		author: {
+			type: Object,
+			default: null
+		},
+		i: {
+			type: Object,
+			default: null
+		},
+		customEmojis: {
+			required: false,
+		},
+		isNote: {
+			type: Boolean,
+			default: true
+		},
+	},
+
+	render() {
+		if (this.text == null || this.text == '') return;
+
+		const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text, { fnNameList: MFM_TAGS });
+
+		const validTime = (t: string | null | undefined) => {
+			if (t == null) return null;
+			return t.match(/^[0-9.]+s$/) ? t : null;
+		};
+
+		const genEl = (ast: mfm.MfmNode[]) => concat(ast.map((token): VNode[] => {
+			switch (token.type) {
+				case 'text': {
+					const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+
+					if (!this.plain) {
+						const res = [];
+						for (const t of text.split('\n')) {
+							res.push(h('br'));
+							res.push(t);
+						}
+						res.shift();
+						return res;
+					} else {
+						return [text.replace(/\n/g, ' ')];
+					}
+				}
+
+				case 'bold': {
+					return [h('b', genEl(token.children))];
+				}
+
+				case 'strike': {
+					return [h('del', genEl(token.children))];
+				}
+
+				case 'italic': {
+					return h('i', {
+						style: 'font-style: oblique;'
+					}, genEl(token.children));
+				}
+
+				case 'fn': {
+					// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
+					let style;
+					switch (token.props.name) {
+						case 'tada': {
+							style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : '');
+							break;
+						}
+						case 'jelly': {
+							const speed = validTime(token.props.args.speed) || '1s';
+							style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
+							break;
+						}
+						case 'twitch': {
+							const speed = validTime(token.props.args.speed) || '0.5s';
+							style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : '';
+							break;
+						}
+						case 'shake': {
+							const speed = validTime(token.props.args.speed) || '0.5s';
+							style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : '';
+							break;
+						}
+						case 'spin': {
+							const direction =
+								token.props.args.left ? 'reverse' :
+								token.props.args.alternate ? 'alternate' :
+								'normal';
+							const anime =
+								token.props.args.x ? 'mfm-spinX' :
+								token.props.args.y ? 'mfm-spinY' :
+								'mfm-spin';
+							const speed = validTime(token.props.args.speed) || '1.5s';
+							style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
+							break;
+						}
+						case 'jump': {
+							style = this.$store.state.animatedMfm ? 'animation: mfm-jump 0.75s linear infinite;' : '';
+							break;
+						}
+						case 'bounce': {
+							style = this.$store.state.animatedMfm ? 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;' : '';
+							break;
+						}
+						case 'flip': {
+							const transform =
+								(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
+								token.props.args.v ? 'scaleY(-1)' :
+								'scaleX(-1)';
+							style = `transform: ${transform};`;
+							break;
+						}
+						case 'x2': {
+							style = `font-size: 200%;`;
+							break;
+						}
+						case 'x3': {
+							style = `font-size: 400%;`;
+							break;
+						}
+						case 'x4': {
+							style = `font-size: 600%;`;
+							break;
+						}
+						case 'font': {
+							const family =
+								token.props.args.serif ? 'serif' :
+								token.props.args.monospace ? 'monospace' :
+								token.props.args.cursive ? 'cursive' :
+								token.props.args.fantasy ? 'fantasy' :
+								token.props.args.emoji ? 'emoji' :
+								token.props.args.math ? 'math' :
+								null;
+							if (family) style = `font-family: ${family};`;
+							break;
+						}
+						case 'blur': {
+							return h('span', {
+								class: '_mfm_blur_',
+							}, genEl(token.children));
+						}
+						case 'rainbow': {
+							style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : '';
+							break;
+						}
+						case 'sparkle': {
+							if (!this.$store.state.animatedMfm) {
+								return genEl(token.children);
+							}
+							let count = token.props.args.count ? parseInt(token.props.args.count) : 10;
+							if (count > 100) {
+								count = 100;
+							}
+							const speed = token.props.args.speed ? parseFloat(token.props.args.speed) : 1;
+							return h(MkSparkle, {
+								count, speed,
+							}, genEl(token.children));
+						}
+					}
+					if (style == null) {
+						return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
+					} else {
+						return h('span', {
+							style: 'display: inline-block;' + style,
+						}, genEl(token.children));
+					}
+				}
+
+				case 'small': {
+					return [h('small', {
+						style: 'opacity: 0.7;'
+					}, genEl(token.children))];
+				}
+
+				case 'center': {
+					return [h('div', {
+						style: 'text-align:center;'
+					}, genEl(token.children))];
+				}
+
+				case 'url': {
+					return [h(MkUrl, {
+						key: Math.random(),
+						url: token.props.url,
+						rel: 'nofollow noopener',
+					})];
+				}
+
+				case 'link': {
+					return [h(MkLink, {
+						key: Math.random(),
+						url: token.props.url,
+						rel: 'nofollow noopener',
+					}, genEl(token.children))];
+				}
+
+				case 'mention': {
+					return [h(MkMention, {
+						key: Math.random(),
+						host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
+						username: token.props.username
+					})];
+				}
+
+				case 'hashtag': {
+					return [h(MkA, {
+						key: Math.random(),
+						to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`,
+						style: 'color:var(--hashtag);'
+					}, `#${token.props.hashtag}`)];
+				}
+
+				case 'blockCode': {
+					return [h(MkCode, {
+						key: Math.random(),
+						code: token.props.code,
+						lang: token.props.lang,
+					})];
+				}
+
+				case 'inlineCode': {
+					return [h(MkCode, {
+						key: Math.random(),
+						code: token.props.code,
+						inline: true
+					})];
+				}
+
+				case 'quote': {
+					if (!this.nowrap) {
+						return [h('div', {
+							class: 'quote'
+						}, genEl(token.children))];
+					} else {
+						return [h('span', {
+							class: 'quote'
+						}, genEl(token.children))];
+					}
+				}
+
+				case 'emojiCode': {
+					return [h(MkEmoji, {
+						key: Math.random(),
+						emoji: `:${token.props.name}:`,
+						customEmojis: this.customEmojis,
+						normal: this.plain
+					})];
+				}
+
+				case 'unicodeEmoji': {
+					return [h(MkEmoji, {
+						key: Math.random(),
+						emoji: token.props.emoji,
+						customEmojis: this.customEmojis,
+						normal: this.plain
+					})];
+				}
+
+				case 'mathInline': {
+					return [h(MkFormula, {
+						key: Math.random(),
+						formula: token.props.formula,
+						block: false
+					})];
+				}
+
+				case 'mathBlock': {
+					return [h(MkFormula, {
+						key: Math.random(),
+						formula: token.props.formula,
+						block: true
+					})];
+				}
+
+				case 'search': {
+					return [h(MkGoogle, {
+						key: Math.random(),
+						q: token.props.query
+					})];
+				}
+
+				default: {
+					console.error('unrecognized ast type:', token.type);
+
+					return [];
+				}
+			}
+		}));
+
+		// Parse ast to DOM
+		return h('span', genEl(ast));
+	}
+});
diff --git a/packages/client/src/components/mini-chart.vue b/packages/client/src/components/mini-chart.vue
new file mode 100644
index 0000000000..2eb9ae8cbe
--- /dev/null
+++ b/packages/client/src/components/mini-chart.vue
@@ -0,0 +1,90 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
+	<defs>
+		<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
+			<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
+			<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
+		</linearGradient>
+		<mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+			<polygon
+				:points="polygonPoints"
+				fill="#fff"
+				fill-opacity="0.5"/>
+			<polyline
+				:points="polylinePoints"
+				fill="none"
+				stroke="#fff"
+				stroke-width="2"/>
+			<circle
+				:cx="headX"
+				:cy="headY"
+				r="3"
+				fill="#fff"/>
+		</mask>
+	</defs>
+	<rect
+		x="-10" y="-10"
+		:width="viewBoxX + 20" :height="viewBoxY + 20"
+		:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
+</svg>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		src: {
+			type: Array,
+			required: true
+		}
+	},
+	data() {
+		return {
+			viewBoxX: 50,
+			viewBoxY: 30,
+			gradientId: uuid(),
+			maskId: uuid(),
+			polylinePoints: '',
+			polygonPoints: '',
+			headX: null,
+			headY: null,
+			clock: null
+		};
+	},
+	watch: {
+		src() {
+			this.draw();
+		}
+	},
+	created() {
+		this.draw();
+
+		// Vueが何故かWatchを発動させない場合があるので
+		this.clock = setInterval(this.draw, 1000);
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		draw() {
+			const stats = this.src.slice().reverse();
+			const peak = Math.max.apply(null, stats) || 1;
+
+			const polylinePoints = stats.map((n, i) => [
+				i * (this.viewBoxX / (stats.length - 1)),
+				(1 - (n / peak)) * this.viewBoxY
+			]);
+
+			this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+
+			this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
+
+			this.headX = polylinePoints[polylinePoints.length - 1][0];
+			this.headY = polylinePoints[polylinePoints.length - 1][1];
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/modal-page-window.vue b/packages/client/src/components/modal-page-window.vue
new file mode 100644
index 0000000000..2086736683
--- /dev/null
+++ b/packages/client/src/components/modal-page-window.vue
@@ -0,0 +1,223 @@
+<template>
+<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
+	<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
+		<div class="header" @contextmenu="onContextmenu">
+			<button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
+			<span v-else style="display: inline-block; width: 20px"></span>
+			<span v-if="pageInfo" class="title">
+				<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i>
+				<span>{{ pageInfo.title }}</span>
+			</span>
+			<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
+		</div>
+		<div class="body">
+			<MkStickyContainer>
+				<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
+				<keep-alive>
+					<component :is="component" v-bind="props" :ref="changePage"/>
+				</keep-alive>
+			</MkStickyContainer>
+		</div>
+	</div>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from '@/components/ui/modal.vue';
+import { popout } from '@/scripts/popout';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { resolve } from '@/router';
+import { url } from '@/config';
+import * as symbols from '@/symbols';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkModal,
+	},
+
+	inject: {
+		sideViewHook: {
+			default: null
+		}
+	},
+
+	provide() {
+		return {
+			navHook: (path) => {
+				this.navigate(path);
+			},
+			shouldHeaderThin: true,
+		};
+	},
+
+	props: {
+		initialPath: {
+			type: String,
+			required: true,
+		},
+		initialComponent: {
+			type: Object,
+			required: true,
+		},
+		initialProps: {
+			type: Object,
+			required: false,
+			default: () => {},
+		},
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			width: 860,
+			height: 660,
+			pageInfo: null,
+			path: this.initialPath,
+			component: this.initialComponent,
+			props: this.initialProps,
+			history: [],
+		};
+	},
+
+	computed: {
+		url(): string {
+			return url + this.path;
+		},
+
+		contextmenu() {
+			return [{
+				type: 'label',
+				text: this.path,
+			}, {
+				icon: 'fas fa-expand-alt',
+				text: this.$ts.showInPage,
+				action: this.expand
+			}, this.sideViewHook ? {
+				icon: 'fas fa-columns',
+				text: this.$ts.openInSideView,
+				action: () => {
+					this.sideViewHook(this.path);
+					this.$refs.window.close();
+				}
+			} : undefined, {
+				icon: 'fas fa-external-link-alt',
+				text: this.$ts.popout,
+				action: this.popout
+			}, null, {
+				icon: 'fas fa-external-link-alt',
+				text: this.$ts.openInNewTab,
+				action: () => {
+					window.open(this.url, '_blank');
+					this.$refs.window.close();
+				}
+			}, {
+				icon: 'fas fa-link',
+				text: this.$ts.copyLink,
+				action: () => {
+					copyToClipboard(this.url);
+				}
+			}];
+		},
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+			}
+		},
+
+		navigate(path, record = true) {
+			if (record) this.history.push(this.path);
+			this.path = path;
+			const { component, props } = resolve(path);
+			this.component = component;
+			this.props = props;
+		},
+
+		back() {
+			this.navigate(this.history.pop(), false);
+		},
+
+		expand() {
+			this.$router.push(this.path);
+			this.$refs.window.close();
+		},
+
+		popout() {
+			popout(this.path, this.$el);
+			this.$refs.window.close();
+		},
+
+		onContextmenu(e) {
+			os.contextMenu(this.contextmenu, e);
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.hrmcaedk {
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+	contain: content;
+
+	--root-margin: 24px;
+
+	@media (max-width: 500px) {
+		--root-margin: 16px;
+	}
+
+	> .header {
+		$height: 52px;
+		$height-narrow: 42px;
+		display: flex;
+		flex-shrink: 0;
+		height: $height;
+		line-height: $height;
+		font-weight: bold;
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		box-shadow: 0px 1px var(--divider);
+
+		> button {
+			height: $height;
+			width: $height;
+
+			&:hover {
+				color: var(--fgHighlighted);
+			}
+		}
+
+		@media (max-width: 500px) {
+			height: $height-narrow;
+			line-height: $height-narrow;
+			padding-left: 16px;
+
+			> button {
+				height: $height-narrow;
+				width: $height-narrow;
+			}
+		}
+
+		> .title {
+			flex: 1;
+
+			> .icon {
+				margin-right: 0.5em;
+			}
+		}
+	}
+
+	> .body {
+		overflow: auto;
+		background: var(--bg);
+	}
+}
+</style>
diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue
new file mode 100644
index 0000000000..8b6905a0e4
--- /dev/null
+++ b/packages/client/src/components/note-detailed.vue
@@ -0,0 +1,1229 @@
+<template>
+<div
+	class="lxwezrsl _block"
+	v-if="!muted"
+	v-show="!isDeleted"
+	:tabindex="!isDeleted ? '-1' : null"
+	:class="{ renote: isRenote }"
+	v-hotkey="keymap"
+	v-size="{ max: [500, 450, 350, 300] }"
+>
+	<XSub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/>
+	<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
+	<div class="renote" v-if="isRenote">
+		<MkAvatar class="avatar" :user="note.user"/>
+		<i class="fas fa-retweet"></i>
+		<I18n :src="$ts.renotedBy" tag="span">
+			<template #user>
+				<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
+					<MkUserName :user="note.user"/>
+				</MkA>
+			</template>
+		</I18n>
+		<div class="info">
+			<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
+				<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
+				<MkTime :time="note.createdAt"/>
+			</button>
+			<span class="visibility" v-if="note.visibility !== 'public'">
+				<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
+				<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
+				<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
+			</span>
+			<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
+		</div>
+	</div>
+	<article class="article" @contextmenu.stop="onContextmenu">
+		<header class="header">
+			<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
+			<div class="body">
+				<div class="top">
+					<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.user.id">
+						<MkUserName :user="appearNote.user"/>
+					</MkA>
+					<span class="is-bot" v-if="appearNote.user.isBot">bot</span>
+					<span class="admin" v-if="appearNote.user.isAdmin"><i class="fas fa-bookmark"></i></span>
+					<span class="moderator" v-if="!appearNote.user.isAdmin && appearNote.user.isModerator"><i class="far fa-bookmark"></i></span>
+					<span class="visibility" v-if="appearNote.visibility !== 'public'">
+						<i v-if="appearNote.visibility === 'home'" class="fas fa-home"></i>
+						<i v-else-if="appearNote.visibility === 'followers'" class="fas fa-unlock"></i>
+						<i v-else-if="appearNote.visibility === 'specified'" class="fas fa-envelope"></i>
+					</span>
+					<span class="localOnly" v-if="appearNote.localOnly"><i class="fas fa-biohazard"></i></span>
+				</div>
+				<div class="username"><MkAcct :user="appearNote.user"/></div>
+				<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
+			</div>
+		</header>
+		<div class="main">
+			<div class="body">
+				<p v-if="appearNote.cw != null" class="cw">
+					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
+					<XCwButton v-model="showContent" :note="appearNote"/>
+				</p>
+				<div class="content" v-show="appearNote.cw == null || showContent">
+					<div class="text">
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
+						<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
+						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
+						<a class="rp" v-if="appearNote.renote != null">RN:</a>
+						<div class="translation" v-if="translating || translation">
+							<MkLoading v-if="translating" mini/>
+							<div class="translated" v-else>
+								<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}:</b>
+								{{ translation.text }}
+							</div>
+						</div>
+					</div>
+					<div class="files" v-if="appearNote.files.length > 0">
+						<XMediaList :media-list="appearNote.files"/>
+					</div>
+					<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
+					<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="true" class="url-preview"/>
+					<div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
+				</div>
+				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
+			</div>
+			<footer class="footer">
+				<div class="info">
+					<span class="mobile" v-if="appearNote.viaMobile"><i class="fas fa-mobile-alt"></i></span>
+					<MkTime class="created-at" :time="appearNote.createdAt" mode="detail"/>
+				</div>
+				<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
+				<button @click="reply()" class="button _button">
+					<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
+					<template v-else><i class="fas fa-reply"></i></template>
+					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
+				</button>
+				<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton">
+					<i class="fas fa-retweet"></i><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
+				</button>
+				<button v-else class="button _button">
+					<i class="fas fa-ban"></i>
+				</button>
+				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
+					<i class="fas fa-plus"></i>
+				</button>
+				<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
+					<i class="fas fa-minus"></i>
+				</button>
+				<button class="button _button" @click="menu()" ref="menuButton">
+					<i class="fas fa-ellipsis-h"></i>
+				</button>
+			</footer>
+		</div>
+	</article>
+	<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
+</div>
+<div v-else class="_panel muted" @click="muted = false">
+	<I18n :src="$ts.userSaysSomething" tag="small">
+		<template #name>
+			<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
+				<MkUserName :user="appearNote.user"/>
+			</MkA>
+		</template>
+	</I18n>
+</div>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
+import * as mfm from 'mfm-js';
+import { sum } from '@/scripts/array';
+import XSub from './note.sub.vue';
+import XNoteHeader from './note-header.vue';
+import XNoteSimple from './note-simple.vue';
+import XReactionsViewer from './reactions-viewer.vue';
+import XMediaList from './media-list.vue';
+import XCwButton from './cw-button.vue';
+import XPoll from './poll.vue';
+import { pleaseLogin } from '@/scripts/please-login';
+import { focusPrev, focusNext } from '@/scripts/focus';
+import { url } from '@/config';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { checkWordMute } from '@/scripts/check-word-mute';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+import { noteActions, noteViewInterruptors } from '@/store';
+import { reactionPicker } from '@/scripts/reaction-picker';
+import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
+
+// TODO: note.vueとほぼ同じなので共通化したい
+export default defineComponent({
+	components: {
+		XSub,
+		XNoteHeader,
+		XNoteSimple,
+		XReactionsViewer,
+		XMediaList,
+		XCwButton,
+		XPoll,
+		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
+		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
+	},
+
+	inject: {
+		inChannel: {
+			default: null
+		},
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+	},
+
+	emits: ['update:note'],
+
+	data() {
+		return {
+			connection: null,
+			conversation: [],
+			replies: [],
+			showContent: false,
+			isDeleted: false,
+			muted: false,
+			translation: null,
+			translating: false,
+		};
+	},
+
+	computed: {
+		rs() {
+			return this.$store.state.reactions;
+		},
+		keymap(): any {
+			return {
+				'r': () => this.reply(true),
+				'e|a|plus': () => this.react(true),
+				'q': () => this.renote(true),
+				'f|b': this.favorite,
+				'delete|ctrl+d': this.del,
+				'ctrl+q': this.renoteDirectly,
+				'up|k|shift+tab': this.focusBefore,
+				'down|j|tab': this.focusAfter,
+				'esc': this.blur,
+				'm|o': () => this.menu(true),
+				's': this.toggleShowContent,
+				'1': () => this.reactDirectly(this.rs[0]),
+				'2': () => this.reactDirectly(this.rs[1]),
+				'3': () => this.reactDirectly(this.rs[2]),
+				'4': () => this.reactDirectly(this.rs[3]),
+				'5': () => this.reactDirectly(this.rs[4]),
+				'6': () => this.reactDirectly(this.rs[5]),
+				'7': () => this.reactDirectly(this.rs[6]),
+				'8': () => this.reactDirectly(this.rs[7]),
+				'9': () => this.reactDirectly(this.rs[8]),
+				'0': () => this.reactDirectly(this.rs[9]),
+			};
+		},
+
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.fileIds.length == 0 &&
+				this.note.poll == null);
+		},
+
+		appearNote(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+
+		isMyNote(): boolean {
+			return this.$i && (this.$i.id === this.appearNote.userId);
+		},
+
+		isMyRenote(): boolean {
+			return this.$i && (this.$i.id === this.note.userId);
+		},
+
+		canRenote(): boolean {
+			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
+		},
+
+		reactionsCount(): number {
+			return this.appearNote.reactions
+				? sum(Object.values(this.appearNote.reactions))
+				: 0;
+		},
+
+		urls(): string[] {
+			if (this.appearNote.text) {
+				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
+			} else {
+				return null;
+			}
+		},
+
+		showTicker() {
+			if (this.$store.state.instanceTicker === 'always') return true;
+			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
+			return false;
+		}
+	},
+
+	async created() {
+		if (this.$i) {
+			this.connection = os.stream;
+		}
+
+		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
+
+		// plugin
+		if (noteViewInterruptors.length > 0) {
+			let result = this.note;
+			for (const interruptor of noteViewInterruptors) {
+				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
+			}
+			this.$emit('update:note', Object.freeze(result));
+		}
+
+		os.api('notes/children', {
+			noteId: this.appearNote.id,
+			limit: 30
+		}).then(replies => {
+			this.replies = replies;
+		});
+
+		if (this.appearNote.replyId) {
+			os.api('notes/conversation', {
+				noteId: this.appearNote.replyId
+			}).then(conversation => {
+				this.conversation = conversation.reverse();
+			});
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if (this.$i) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+	},
+
+	beforeUnmount() {
+		this.decapture(true);
+
+		if (this.$i) {
+			this.connection.off('_connected_', this.onStreamConnected);
+		}
+	},
+
+	methods: {
+		updateAppearNote(v) {
+			this.$emit('update:note', Object.freeze(this.isRenote ? {
+				...this.note,
+				renote: {
+					...this.note.renote,
+					...v
+				}
+			} : {
+				...this.note,
+				...v
+			}));
+		},
+
+		readPromo() {
+			os.api('promo/read', {
+				noteId: this.appearNote.id
+			});
+			this.isDeleted = true;
+		},
+
+		capture(withHandler = false) {
+			if (this.$i) {
+				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
+				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		decapture(withHandler = false) {
+			if (this.$i) {
+				this.connection.send('un', {
+					id: this.appearNote.id
+				});
+				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		onStreamConnected() {
+			this.capture();
+		},
+
+		onStreamNoteUpdated(data) {
+			const { type, id, body } = data;
+
+			if (id !== this.appearNote.id) return;
+
+			switch (type) {
+				case 'reacted': {
+					const reaction = body.reaction;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					if (body.emoji) {
+						const emojis = this.appearNote.emojis || [];
+						if (!emojis.includes(body.emoji)) {
+							n.emojis = [...emojis, body.emoji];
+						}
+					}
+
+					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
+					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
+
+					// Increment the count
+					n.reactions = {
+						...this.appearNote.reactions,
+						[reaction]: currentCount + 1
+					};
+
+					if (body.userId === this.$i.id) {
+						n.myReaction = reaction;
+					}
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'unreacted': {
+					const reaction = body.reaction;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
+					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
+
+					// Decrement the count
+					n.reactions = {
+						...this.appearNote.reactions,
+						[reaction]: Math.max(0, currentCount - 1)
+					};
+
+					if (body.userId === this.$i.id) {
+						n.myReaction = null;
+					}
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'pollVoted': {
+					const choice = body.choice;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					const choices = [...this.appearNote.poll.choices];
+					choices[choice] = {
+						...choices[choice],
+						votes: choices[choice].votes + 1,
+						...(body.userId === this.$i.id ? {
+							isVoted: true
+						} : {})
+					};
+
+					n.poll = {
+						...this.appearNote.poll,
+						choices: choices
+					};
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'deleted': {
+					this.isDeleted = true;
+					break;
+				}
+			}
+		},
+
+		reply(viaKeyboard = false) {
+			pleaseLogin();
+			os.post({
+				reply: this.appearNote,
+				animation: !viaKeyboard,
+			}, () => {
+				this.focus();
+			});
+		},
+
+		renote(viaKeyboard = false) {
+			pleaseLogin();
+			this.blur();
+			os.popupMenu([{
+				text: this.$ts.renote,
+				icon: 'fas fa-retweet',
+				action: () => {
+					os.api('notes/create', {
+						renoteId: this.appearNote.id
+					});
+				}
+			}, {
+				text: this.$ts.quote,
+				icon: 'fas fa-quote-right',
+				action: () => {
+					os.post({
+						renote: this.appearNote,
+					});
+				}
+			}], this.$refs.renoteButton, {
+				viaKeyboard
+			});
+		},
+
+		renoteDirectly() {
+			os.apiWithDialog('notes/create', {
+				renoteId: this.appearNote.id
+			}, undefined, (res: any) => {
+				os.dialog({
+					type: 'success',
+					text: this.$ts.renoted,
+				});
+			}, (e: Error) => {
+				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantRenote,
+					});
+				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantReRenote,
+					});
+				}
+			});
+		},
+
+		react(viaKeyboard = false) {
+			pleaseLogin();
+			this.blur();
+			reactionPicker.show(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.focus();
+			});
+		},
+
+		reactDirectly(reaction) {
+			os.api('notes/reactions/create', {
+				noteId: this.appearNote.id,
+				reaction: reaction
+			});
+		},
+
+		undoReact(note) {
+			const oldReaction = note.myReaction;
+			if (!oldReaction) return;
+			os.api('notes/reactions/delete', {
+				noteId: note.id
+			});
+		},
+
+		favorite() {
+			pleaseLogin();
+			os.apiWithDialog('notes/favorites/create', {
+				noteId: this.appearNote.id
+			}, undefined, (res: any) => {
+				os.dialog({
+					type: 'success',
+					text: this.$ts.favorited,
+				});
+			}, (e: Error) => {
+				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.alreadyFavorited,
+					});
+				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantFavorite,
+					});
+				}
+			});
+		},
+
+		del() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.noteDeleteConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.api('notes/delete', {
+					noteId: this.appearNote.id
+				});
+			});
+		},
+
+		delEdit() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.deleteAndEditConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.api('notes/delete', {
+					noteId: this.appearNote.id
+				});
+
+				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
+			});
+		},
+
+		toggleFavorite(favorite: boolean) {
+			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
+		toggleWatch(watch: boolean) {
+			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
+		toggleThreadMute(mute: boolean) {
+			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
+		getMenu() {
+			let menu;
+			if (this.$i) {
+				const statePromise = os.api('notes/state', {
+					noteId: this.appearNote.id
+				});
+
+				menu = [{
+					icon: 'fas fa-copy',
+					text: this.$ts.copyContent,
+					action: this.copyContent
+				}, {
+					icon: 'fas fa-link',
+					text: this.$ts.copyLink,
+					action: this.copyLink
+				}, (this.appearNote.url || this.appearNote.uri) ? {
+					icon: 'fas fa-external-link-square-alt',
+					text: this.$ts.showOnRemote,
+					action: () => {
+						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
+					}
+				} : undefined,
+				{
+					icon: 'fas fa-share-alt',
+					text: this.$ts.share,
+					action: this.share
+				},
+				this.$instance.translatorAvailable ? {
+					icon: 'fas fa-language',
+					text: this.$ts.translate,
+					action: this.translate
+				} : undefined,
+				null,
+				statePromise.then(state => state.isFavorited ? {
+					icon: 'fas fa-star',
+					text: this.$ts.unfavorite,
+					action: () => this.toggleFavorite(false)
+				} : {
+					icon: 'fas fa-star',
+					text: this.$ts.favorite,
+					action: () => this.toggleFavorite(true)
+				}),
+				{
+					icon: 'fas fa-paperclip',
+					text: this.$ts.clip,
+					action: () => this.clip()
+				},
+				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
+					icon: 'fas fa-eye-slash',
+					text: this.$ts.unwatch,
+					action: () => this.toggleWatch(false)
+				} : {
+					icon: 'fas fa-eye',
+					text: this.$ts.watch,
+					action: () => this.toggleWatch(true)
+				}) : undefined,
+				statePromise.then(state => state.isMutedThread ? {
+					icon: 'fas fa-comment-slash',
+					text: this.$ts.unmuteThread,
+					action: () => this.toggleThreadMute(false)
+				} : {
+					icon: 'fas fa-comment-slash',
+					text: this.$ts.muteThread,
+					action: () => this.toggleThreadMute(true)
+				}),
+				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
+					icon: 'fas fa-thumbtack',
+					text: this.$ts.unpin,
+					action: () => this.togglePin(false)
+				} : {
+					icon: 'fas fa-thumbtack',
+					text: this.$ts.pin,
+					action: () => this.togglePin(true)
+				} : undefined,
+				...(this.$i.isModerator || this.$i.isAdmin ? [
+					null,
+					{
+						icon: 'fas fa-bullhorn',
+						text: this.$ts.promote,
+						action: this.promote
+					}]
+					: []
+				),
+				...(this.appearNote.userId != this.$i.id ? [
+					null,
+					{
+						icon: 'fas fa-exclamation-circle',
+						text: this.$ts.reportAbuse,
+						action: () => {
+							const u = `${url}/notes/${this.appearNote.id}`;
+							os.popup(import('@/components/abuse-report-window.vue'), {
+								user: this.appearNote.user,
+								initialComment: `Note: ${u}\n-----\n`
+							}, {}, 'closed');
+						}
+					}]
+					: []
+				),
+				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
+					null,
+					this.appearNote.userId == this.$i.id ? {
+						icon: 'fas fa-edit',
+						text: this.$ts.deleteAndEdit,
+						action: this.delEdit
+					} : undefined,
+					{
+						icon: 'fas fa-trash-alt',
+						text: this.$ts.delete,
+						danger: true,
+						action: this.del
+					}]
+					: []
+				)]
+				.filter(x => x !== undefined);
+			} else {
+				menu = [{
+					icon: 'fas fa-copy',
+					text: this.$ts.copyContent,
+					action: this.copyContent
+				}, {
+					icon: 'fas fa-link',
+					text: this.$ts.copyLink,
+					action: this.copyLink
+				}, (this.appearNote.url || this.appearNote.uri) ? {
+					icon: 'fas fa-external-link-square-alt',
+					text: this.$ts.showOnRemote,
+					action: () => {
+						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
+					}
+				} : undefined]
+				.filter(x => x !== undefined);
+			}
+
+			if (noteActions.length > 0) {
+				menu = menu.concat([null, ...noteActions.map(action => ({
+					icon: 'fas fa-plug',
+					text: action.title,
+					action: () => {
+						action.handler(this.appearNote);
+					}
+				}))]);
+			}
+
+			return menu;
+		},
+
+		onContextmenu(e) {
+			const isLink = (el: HTMLElement) => {
+				if (el.tagName === 'A') return true;
+				if (el.parentElement) {
+					return isLink(el.parentElement);
+				}
+			};
+			if (isLink(e.target)) return;
+			if (window.getSelection().toString() !== '') return;
+
+			if (this.$store.state.useReactionPickerForContextMenu) {
+				e.preventDefault();
+				this.react();
+			} else {
+				os.contextMenu(this.getMenu(), e).then(this.focus);
+			}
+		},
+
+		menu(viaKeyboard = false) {
+			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
+				viaKeyboard
+			}).then(this.focus);
+		},
+
+		showRenoteMenu(viaKeyboard = false) {
+			if (!this.isMyRenote) return;
+			os.popupMenu([{
+				text: this.$ts.unrenote,
+				icon: 'fas fa-trash-alt',
+				danger: true,
+				action: () => {
+					os.api('notes/delete', {
+						noteId: this.note.id
+					});
+					this.isDeleted = true;
+				}
+			}], this.$refs.renoteTime, {
+				viaKeyboard: viaKeyboard
+			});
+		},
+
+		toggleShowContent() {
+			this.showContent = !this.showContent;
+		},
+
+		copyContent() {
+			copyToClipboard(this.appearNote.text);
+			os.success();
+		},
+
+		copyLink() {
+			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
+			os.success();
+		},
+
+		togglePin(pin: boolean) {
+			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
+				noteId: this.appearNote.id
+			}, undefined, null, e => {
+				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.pinLimitExceeded
+					});
+				}
+			});
+		},
+
+		async clip() {
+			const clips = await os.api('clips/list');
+			os.popupMenu([{
+				icon: 'fas fa-plus',
+				text: this.$ts.createNew,
+				action: async () => {
+					const { canceled, result } = await os.form(this.$ts.createNewClip, {
+						name: {
+							type: 'string',
+							label: this.$ts.name
+						},
+						description: {
+							type: 'string',
+							required: false,
+							multiline: true,
+							label: this.$ts.description
+						},
+						isPublic: {
+							type: 'boolean',
+							label: this.$ts.public,
+							default: false
+						}
+					});
+					if (canceled) return;
+
+					const clip = await os.apiWithDialog('clips/create', result);
+
+					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
+				}
+			}, null, ...clips.map(clip => ({
+				text: clip.name,
+				action: () => {
+					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
+				}
+			}))], this.$refs.menuButton, {
+			}).then(this.focus);
+		},
+
+		async promote() {
+			const { canceled, result: days } = await os.dialog({
+				title: this.$ts.numberOfDays,
+				input: { type: 'number' }
+			});
+
+			if (canceled) return;
+
+			os.apiWithDialog('admin/promo/create', {
+				noteId: this.appearNote.id,
+				expiresAt: Date.now() + (86400000 * days)
+			});
+		},
+
+		share() {
+			navigator.share({
+				title: this.$t('noteOf', { user: this.appearNote.user.name }),
+				text: this.appearNote.text,
+				url: `${url}/notes/${this.appearNote.id}`
+			});
+		},
+
+		async translate() {
+			if (this.translation != null) return;
+			this.translating = true;
+			const res = await os.api('notes/translate', {
+				noteId: this.appearNote.id,
+				targetLang: localStorage.getItem('lang') || navigator.language,
+			});
+			this.translating = false;
+			this.translation = res;
+		},
+
+		focus() {
+			this.$el.focus();
+		},
+
+		blur() {
+			this.$el.blur();
+		},
+
+		focusBefore() {
+			focusPrev(this.$el);
+		},
+
+		focusAfter() {
+			focusNext(this.$el);
+		},
+
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.lxwezrsl {
+	position: relative;
+	transition: box-shadow 0.1s ease;
+	overflow: hidden;
+	contain: content;
+
+	&:focus-visible {
+		outline: none;
+
+		&:after {
+			content: "";
+			pointer-events: none;
+			display: block;
+			position: absolute;
+			z-index: 10;
+			top: 0;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			margin: auto;
+			width: calc(100% - 8px);
+			height: calc(100% - 8px);
+			border: dashed 1px var(--focus);
+			border-radius: var(--radius);
+			box-sizing: border-box;
+		}
+	}
+
+	&:hover > .article > .main > .footer > .button {
+		opacity: 1;
+	}
+
+	> .reply-to {
+		opacity: 0.7;
+		padding-bottom: 0;
+	}
+
+	> .reply-to-more {
+		opacity: 0.7;
+	}
+
+	> .renote {
+		display: flex;
+		align-items: center;
+		padding: 16px 32px 8px 32px;
+		line-height: 28px;
+		white-space: pre;
+		color: var(--renote);
+
+		> .avatar {
+			flex-shrink: 0;
+			display: inline-block;
+			width: 28px;
+			height: 28px;
+			margin: 0 8px 0 0;
+			border-radius: 6px;
+		}
+
+		> i {
+			margin-right: 4px;
+		}
+
+		> span {
+			overflow: hidden;
+			flex-shrink: 1;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+
+			> .name {
+				font-weight: bold;
+			}
+		}
+
+		> .info {
+			margin-left: auto;
+			font-size: 0.9em;
+
+			> .time {
+				flex-shrink: 0;
+				color: inherit;
+
+				> .dropdownIcon {
+					margin-right: 4px;
+				}
+			}
+
+			> .visibility {
+				margin-left: 8px;
+			}
+
+			> .localOnly {
+				margin-left: 8px;
+			}
+		}
+	}
+
+	> .renote + .article {
+		padding-top: 8px;
+	}
+
+	> .article {
+		padding: 32px;
+		font-size: 1.1em;
+
+		> .header {
+			display: flex;
+			position: relative;
+			margin-bottom: 16px;
+
+			> .avatar {
+				display: block;
+				flex-shrink: 0;
+				width: 58px;
+				height: 58px;
+			}
+
+			> .body {
+				flex: 1;
+				display: flex;
+				flex-direction: column;
+				justify-content: center;
+				padding-left: 16px;
+				font-size: 0.95em;
+
+				> .top {
+					> .name {
+						font-weight: bold;
+					}
+
+					> .is-bot {
+						flex-shrink: 0;
+						align-self: center;
+						margin: 0 0.5em;
+						padding: 4px 6px;
+						font-size: 80%;
+						border: solid 0.5px var(--divider);
+						border-radius: 4px;
+					}
+
+					> .admin,
+					> .moderator {
+						margin-right: 0.5em;
+						color: var(--badge);
+					}
+				}
+			}
+		}
+
+		> .main {
+			> .body {
+				> .cw {
+					cursor: default;
+					display: block;
+					margin: 0;
+					padding: 0;
+					overflow-wrap: break-word;
+
+					> .text {
+						margin-right: 8px;
+					}
+				}
+
+				> .content {
+					> .text {
+						overflow-wrap: break-word;
+
+						> .reply {
+							color: var(--accent);
+							margin-right: 0.5em;
+						}
+
+						> .rp {
+							margin-left: 4px;
+							font-style: oblique;
+							color: var(--renote);
+						}
+
+						> .translation {
+							border: solid 0.5px var(--divider);
+							border-radius: var(--radius);
+							padding: 12px;
+							margin-top: 8px;
+						}
+					}
+
+					> .url-preview {
+						margin-top: 8px;
+					}
+
+					> .poll {
+						font-size: 80%;
+					}
+
+					> .renote {
+						padding: 8px 0;
+
+						> * {
+							padding: 16px;
+							border: dashed 1px var(--renote);
+							border-radius: 8px;
+						}
+					}
+				}
+
+				> .channel {
+					opacity: 0.7;
+					font-size: 80%;
+				}
+			}
+
+			> .footer {
+				> .info {
+					margin: 16px 0;
+					opacity: 0.7;
+					font-size: 0.9em;
+				}
+
+				> .button {
+					margin: 0;
+					padding: 8px;
+					opacity: 0.7;
+
+					&:not(:last-child) {
+						margin-right: 28px;
+					}
+
+					&:hover {
+						color: var(--fgHighlighted);
+					}
+
+					> .count {
+						display: inline;
+						margin: 0 0 0 8px;
+						opacity: 0.7;
+					}
+
+					&.reacted {
+						color: var(--accent);
+					}
+				}
+			}
+		}
+	}
+
+	> .reply {
+		border-top: solid 0.5px var(--divider);
+	}
+
+	&.max-width_500px {
+		font-size: 0.9em;
+	}
+
+	&.max-width_450px {
+		> .renote {
+			padding: 8px 16px 0 16px;
+		}
+
+		> .article {
+			padding: 16px;
+
+			> .header {
+				> .avatar {
+					width: 50px;
+					height: 50px;
+				}
+			}
+		}
+	}
+
+	&.max-width_350px {
+		> .article {
+			> .main {
+				> .footer {
+					> .button {
+						&:not(:last-child) {
+							margin-right: 18px;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	&.max-width_300px {
+		font-size: 0.825em;
+
+		> .article {
+			> .header {
+				> .avatar {
+					width: 50px;
+					height: 50px;
+				}
+			}
+
+			> .main {
+				> .footer {
+					> .button {
+						&:not(:last-child) {
+							margin-right: 12px;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+
+.muted {
+	padding: 8px;
+	text-align: center;
+	opacity: 0.7;
+}
+</style>
diff --git a/packages/client/src/components/note-header.vue b/packages/client/src/components/note-header.vue
new file mode 100644
index 0000000000..c61ec41dd1
--- /dev/null
+++ b/packages/client/src/components/note-header.vue
@@ -0,0 +1,115 @@
+<template>
+<header class="kkwtjztg">
+	<MkA class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
+		<MkUserName :user="note.user"/>
+	</MkA>
+	<div class="is-bot" v-if="note.user.isBot">bot</div>
+	<div class="username"><MkAcct :user="note.user"/></div>
+	<div class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></div>
+	<div class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></div>
+	<div class="info">
+		<span class="mobile" v-if="note.viaMobile"><i class="fas fa-mobile-alt"></i></span>
+		<MkA class="created-at" :to="notePage(note)">
+			<MkTime :time="note.createdAt"/>
+		</MkA>
+		<span class="visibility" v-if="note.visibility !== 'public'">
+			<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
+			<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
+			<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
+		</span>
+		<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
+	</div>
+</header>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import notePage from '@/filters/note';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	methods: {
+		notePage,
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.kkwtjztg {
+	display: flex;
+	align-items: baseline;
+	white-space: nowrap;
+
+	> .name {
+		flex-shrink: 1;
+		display: block;
+		margin: 0 .5em 0 0;
+		padding: 0;
+		overflow: hidden;
+		font-size: 1em;
+		font-weight: bold;
+		text-decoration: none;
+		text-overflow: ellipsis;
+
+		&:hover {
+			text-decoration: underline;
+		}
+	}
+
+	> .is-bot {
+		flex-shrink: 0;
+		align-self: center;
+		margin: 0 .5em 0 0;
+		padding: 1px 6px;
+		font-size: 80%;
+		border: solid 0.5px var(--divider);
+		border-radius: 3px;
+	}
+
+	> .admin,
+	> .moderator {
+		flex-shrink: 0;
+		margin-right: 0.5em;
+		color: var(--badge);
+	}
+
+	> .username {
+		flex-shrink: 9999999;
+		margin: 0 .5em 0 0;
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	> .info {
+		flex-shrink: 0;
+		margin-left: auto;
+		font-size: 0.9em;
+
+		> .mobile {
+			margin-right: 8px;
+		}
+
+		> .visibility {
+			margin-left: 8px;
+		}
+
+		> .localOnly {
+			margin-left: 8px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/note-preview.vue b/packages/client/src/components/note-preview.vue
new file mode 100644
index 0000000000..a474a01341
--- /dev/null
+++ b/packages/client/src/components/note-preview.vue
@@ -0,0 +1,98 @@
+<template>
+<div class="fefdfafb" v-size="{ min: [350, 500] }">
+	<MkAvatar class="avatar" :user="$i"/>
+	<div class="main">
+		<div class="header">
+			<MkUserName :user="$i"/>
+		</div>
+		<div class="body">
+			<div class="content">
+				<Mfm :text="text" :author="$i" :i="$i"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	components: {
+	},
+
+	props: {
+		text: {
+			type: String,
+			required: true
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.fefdfafb {
+	display: flex;
+	margin: 0;
+	padding: 0;
+	overflow: clip;
+	font-size: 0.95em;
+
+	&.min-width_350px {
+		> .avatar {
+			margin: 0 10px 0 0;
+			width: 44px;
+			height: 44px;
+		}
+	}
+
+	&.min-width_500px {
+		> .avatar {
+			margin: 0 12px 0 0;
+			width: 48px;
+			height: 48px;
+		}
+	}
+
+	> .avatar {
+		flex-shrink: 0;
+		display: block;
+		margin: 0 10px 0 0;
+		width: 40px;
+		height: 40px;
+		border-radius: 8px;
+	}
+
+	> .main {
+		flex: 1;
+		min-width: 0;
+
+		> .header {
+			margin-bottom: 2px;
+		}
+
+		> .body {
+
+			> .cw {
+				cursor: default;
+				display: block;
+				margin: 0;
+				padding: 0;
+				overflow-wrap: break-word;
+
+				> .text {
+					margin-right: 8px;
+				}
+			}
+
+			> .content {
+				> .text {
+					cursor: default;
+					margin: 0;
+					padding: 0;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue
new file mode 100644
index 0000000000..2f19bd6e0b
--- /dev/null
+++ b/packages/client/src/components/note-simple.vue
@@ -0,0 +1,113 @@
+<template>
+<div class="yohlumlk" v-size="{ min: [350, 500] }">
+	<MkAvatar class="avatar" :user="note.user"/>
+	<div class="main">
+		<XNoteHeader class="header" :note="note" :mini="true"/>
+		<div class="body">
+			<p v-if="note.cw != null" class="cw">
+				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+				<XCwButton v-model="showContent" :note="note"/>
+			</p>
+			<div class="content" v-show="note.cw == null || showContent">
+				<XSubNote-content class="text" :note="note"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XNoteHeader from './note-header.vue';
+import XSubNoteContent from './sub-note-content.vue';
+import XCwButton from './cw-button.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XNoteHeader,
+		XSubNoteContent,
+		XCwButton,
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			showContent: false
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.yohlumlk {
+	display: flex;
+	margin: 0;
+	padding: 0;
+	overflow: clip;
+	font-size: 0.95em;
+
+	&.min-width_350px {
+		> .avatar {
+			margin: 0 10px 0 0;
+			width: 44px;
+			height: 44px;
+		}
+	}
+
+	&.min-width_500px {
+		> .avatar {
+			margin: 0 12px 0 0;
+			width: 48px;
+			height: 48px;
+		}
+	}
+
+	> .avatar {
+		flex-shrink: 0;
+		display: block;
+		margin: 0 10px 0 0;
+		width: 40px;
+		height: 40px;
+		border-radius: 8px;
+	}
+
+	> .main {
+		flex: 1;
+		min-width: 0;
+
+		> .header {
+			margin-bottom: 2px;
+		}
+
+		> .body {
+
+			> .cw {
+				cursor: default;
+				display: block;
+				margin: 0;
+				padding: 0;
+				overflow-wrap: break-word;
+
+				> .text {
+					margin-right: 8px;
+				}
+			}
+
+			> .content {
+				> .text {
+					cursor: default;
+					margin: 0;
+					padding: 0;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/note.sub.vue b/packages/client/src/components/note.sub.vue
new file mode 100644
index 0000000000..45204854be
--- /dev/null
+++ b/packages/client/src/components/note.sub.vue
@@ -0,0 +1,146 @@
+<template>
+<div class="wrpstxzv" :class="{ children }" v-size="{ max: [450] }">
+	<div class="main">
+		<MkAvatar class="avatar" :user="note.user"/>
+		<div class="body">
+			<XNoteHeader class="header" :note="note" :mini="true"/>
+			<div class="body">
+				<p v-if="note.cw != null" class="cw">
+					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
+					<XCwButton v-model="showContent" :note="note"/>
+				</p>
+				<div class="content" v-show="note.cw == null || showContent">
+					<XSubNote-content class="text" :note="note"/>
+				</div>
+			</div>
+		</div>
+	</div>
+	<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XNoteHeader from './note-header.vue';
+import XSubNoteContent from './sub-note-content.vue';
+import XCwButton from './cw-button.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	name: 'XSub',
+
+	components: {
+		XNoteHeader,
+		XSubNoteContent,
+		XCwButton,
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		detail: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		children: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		// TODO
+		truncate: {
+			type: Boolean,
+			default: true
+		}
+	},
+
+	data() {
+		return {
+			showContent: false,
+			replies: [],
+		};
+	},
+
+	created() {
+		if (this.detail) {
+			os.api('notes/children', {
+				noteId: this.note.id,
+				limit: 5
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.wrpstxzv {
+	padding: 16px 32px;
+	font-size: 0.9em;
+
+	&.max-width_450px {
+		padding: 14px 16px;
+	}
+
+	&.children {
+		padding: 10px 0 0 16px;
+		font-size: 1em;
+
+		&.max-width_450px {
+			padding: 10px 0 0 8px;
+		}
+	}
+
+	> .main {
+		display: flex;
+
+		> .avatar {
+			flex-shrink: 0;
+			display: block;
+			margin: 0 8px 0 0;
+			width: 38px;
+			height: 38px;
+			border-radius: 8px;
+		}
+
+		> .body {
+			flex: 1;
+			min-width: 0;
+
+			> .header {
+				margin-bottom: 2px;
+			}
+
+			> .body {
+				> .cw {
+					cursor: default;
+					display: block;
+					margin: 0;
+					padding: 0;
+					overflow-wrap: break-word;
+
+					> .text {
+						margin-right: 8px;
+					}
+				}
+
+				> .content {
+					> .text {
+						margin: 0;
+						padding: 0;
+					}
+				}
+			}
+		}
+	}
+
+	> .reply {
+		border-left: solid 0.5px var(--divider);
+		margin-top: 10px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue
new file mode 100644
index 0000000000..b1ec674b67
--- /dev/null
+++ b/packages/client/src/components/note.vue
@@ -0,0 +1,1228 @@
+<template>
+<div
+	class="tkcbzcuz"
+	v-if="!muted"
+	v-show="!isDeleted"
+	:tabindex="!isDeleted ? '-1' : null"
+	:class="{ renote: isRenote }"
+	v-hotkey="keymap"
+	v-size="{ max: [500, 450, 350, 300] }"
+>
+	<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
+	<div class="info" v-if="pinned"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
+	<div class="info" v-if="appearNote._prId_"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
+	<div class="info" v-if="appearNote._featuredId_"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
+	<div class="renote" v-if="isRenote">
+		<MkAvatar class="avatar" :user="note.user"/>
+		<i class="fas fa-retweet"></i>
+		<I18n :src="$ts.renotedBy" tag="span">
+			<template #user>
+				<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
+					<MkUserName :user="note.user"/>
+				</MkA>
+			</template>
+		</I18n>
+		<div class="info">
+			<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
+				<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
+				<MkTime :time="note.createdAt"/>
+			</button>
+			<span class="visibility" v-if="note.visibility !== 'public'">
+				<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
+				<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
+				<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
+			</span>
+			<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
+		</div>
+	</div>
+	<article class="article" @contextmenu.stop="onContextmenu">
+		<MkAvatar class="avatar" :user="appearNote.user"/>
+		<div class="main">
+			<XNoteHeader class="header" :note="appearNote" :mini="true"/>
+			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
+			<div class="body">
+				<p v-if="appearNote.cw != null" class="cw">
+					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
+					<XCwButton v-model="showContent" :note="appearNote"/>
+				</p>
+				<div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent">
+					<div class="text">
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
+						<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
+						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
+						<a class="rp" v-if="appearNote.renote != null">RN:</a>
+						<div class="translation" v-if="translating || translation">
+							<MkLoading v-if="translating" mini/>
+							<div class="translated" v-else>
+								<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}:</b>
+								{{ translation.text }}
+							</div>
+						</div>
+					</div>
+					<div class="files" v-if="appearNote.files.length > 0">
+						<XMediaList :media-list="appearNote.files"/>
+					</div>
+					<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
+					<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
+					<div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
+					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
+						<span>{{ $ts.showMore }}</span>
+					</button>
+				</div>
+				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
+			</div>
+			<footer class="footer">
+				<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
+				<button @click="reply()" class="button _button">
+					<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
+					<template v-else><i class="fas fa-reply"></i></template>
+					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
+				</button>
+				<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton">
+					<i class="fas fa-retweet"></i><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
+				</button>
+				<button v-else class="button _button">
+					<i class="fas fa-ban"></i>
+				</button>
+				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
+					<i class="fas fa-plus"></i>
+				</button>
+				<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
+					<i class="fas fa-minus"></i>
+				</button>
+				<button class="button _button" @click="menu()" ref="menuButton">
+					<i class="fas fa-ellipsis-h"></i>
+				</button>
+			</footer>
+		</div>
+	</article>
+</div>
+<div v-else class="muted" @click="muted = false">
+	<I18n :src="$ts.userSaysSomething" tag="small">
+		<template #name>
+			<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
+				<MkUserName :user="appearNote.user"/>
+			</MkA>
+		</template>
+	</I18n>
+</div>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
+import * as mfm from 'mfm-js';
+import { sum } from '@/scripts/array';
+import XSub from './note.sub.vue';
+import XNoteHeader from './note-header.vue';
+import XNoteSimple from './note-simple.vue';
+import XReactionsViewer from './reactions-viewer.vue';
+import XMediaList from './media-list.vue';
+import XCwButton from './cw-button.vue';
+import XPoll from './poll.vue';
+import { pleaseLogin } from '@/scripts/please-login';
+import { focusPrev, focusNext } from '@/scripts/focus';
+import { url } from '@/config';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { checkWordMute } from '@/scripts/check-word-mute';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+import { noteActions, noteViewInterruptors } from '@/store';
+import { reactionPicker } from '@/scripts/reaction-picker';
+import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
+
+export default defineComponent({
+	components: {
+		XSub,
+		XNoteHeader,
+		XNoteSimple,
+		XReactionsViewer,
+		XMediaList,
+		XCwButton,
+		XPoll,
+		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
+		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
+	},
+
+	inject: {
+		inChannel: {
+			default: null
+		},
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		pinned: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	emits: ['update:note'],
+
+	data() {
+		return {
+			connection: null,
+			replies: [],
+			showContent: false,
+			collapsed: false,
+			isDeleted: false,
+			muted: false,
+			translation: null,
+			translating: false,
+		};
+	},
+
+	computed: {
+		rs() {
+			return this.$store.state.reactions;
+		},
+		keymap(): any {
+			return {
+				'r': () => this.reply(true),
+				'e|a|plus': () => this.react(true),
+				'q': () => this.renote(true),
+				'f|b': this.favorite,
+				'delete|ctrl+d': this.del,
+				'ctrl+q': this.renoteDirectly,
+				'up|k|shift+tab': this.focusBefore,
+				'down|j|tab': this.focusAfter,
+				'esc': this.blur,
+				'm|o': () => this.menu(true),
+				's': this.toggleShowContent,
+				'1': () => this.reactDirectly(this.rs[0]),
+				'2': () => this.reactDirectly(this.rs[1]),
+				'3': () => this.reactDirectly(this.rs[2]),
+				'4': () => this.reactDirectly(this.rs[3]),
+				'5': () => this.reactDirectly(this.rs[4]),
+				'6': () => this.reactDirectly(this.rs[5]),
+				'7': () => this.reactDirectly(this.rs[6]),
+				'8': () => this.reactDirectly(this.rs[7]),
+				'9': () => this.reactDirectly(this.rs[8]),
+				'0': () => this.reactDirectly(this.rs[9]),
+			};
+		},
+
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.fileIds.length == 0 &&
+				this.note.poll == null);
+		},
+
+		appearNote(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+
+		isMyNote(): boolean {
+			return this.$i && (this.$i.id === this.appearNote.userId);
+		},
+
+		isMyRenote(): boolean {
+			return this.$i && (this.$i.id === this.note.userId);
+		},
+
+		canRenote(): boolean {
+			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
+		},
+
+		reactionsCount(): number {
+			return this.appearNote.reactions
+				? sum(Object.values(this.appearNote.reactions))
+				: 0;
+		},
+
+		urls(): string[] {
+			if (this.appearNote.text) {
+				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
+			} else {
+				return null;
+			}
+		},
+
+		showTicker() {
+			if (this.$store.state.instanceTicker === 'always') return true;
+			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
+			return false;
+		}
+	},
+
+	async created() {
+		if (this.$i) {
+			this.connection = os.stream;
+		}
+
+		this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
+			(this.appearNote.text.split('\n').length > 9) ||
+			(this.appearNote.text.length > 500)
+		);
+		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
+
+		// plugin
+		if (noteViewInterruptors.length > 0) {
+			let result = this.note;
+			for (const interruptor of noteViewInterruptors) {
+				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
+			}
+			this.$emit('update:note', Object.freeze(result));
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if (this.$i) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+	},
+
+	beforeUnmount() {
+		this.decapture(true);
+
+		if (this.$i) {
+			this.connection.off('_connected_', this.onStreamConnected);
+		}
+	},
+
+	methods: {
+		updateAppearNote(v) {
+			this.$emit('update:note', Object.freeze(this.isRenote ? {
+				...this.note,
+				renote: {
+					...this.note.renote,
+					...v
+				}
+			} : {
+				...this.note,
+				...v
+			}));
+		},
+
+		readPromo() {
+			os.api('promo/read', {
+				noteId: this.appearNote.id
+			});
+			this.isDeleted = true;
+		},
+
+		capture(withHandler = false) {
+			if (this.$i) {
+				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
+				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		decapture(withHandler = false) {
+			if (this.$i) {
+				this.connection.send('un', {
+					id: this.appearNote.id
+				});
+				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		onStreamConnected() {
+			this.capture();
+		},
+
+		onStreamNoteUpdated(data) {
+			const { type, id, body } = data;
+
+			if (id !== this.appearNote.id) return;
+
+			switch (type) {
+				case 'reacted': {
+					const reaction = body.reaction;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					if (body.emoji) {
+						const emojis = this.appearNote.emojis || [];
+						if (!emojis.includes(body.emoji)) {
+							n.emojis = [...emojis, body.emoji];
+						}
+					}
+
+					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
+					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
+
+					// Increment the count
+					n.reactions = {
+						...this.appearNote.reactions,
+						[reaction]: currentCount + 1
+					};
+
+					if (body.userId === this.$i.id) {
+						n.myReaction = reaction;
+					}
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'unreacted': {
+					const reaction = body.reaction;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
+					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
+
+					// Decrement the count
+					n.reactions = {
+						...this.appearNote.reactions,
+						[reaction]: Math.max(0, currentCount - 1)
+					};
+
+					if (body.userId === this.$i.id) {
+						n.myReaction = null;
+					}
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'pollVoted': {
+					const choice = body.choice;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					const choices = [...this.appearNote.poll.choices];
+					choices[choice] = {
+						...choices[choice],
+						votes: choices[choice].votes + 1,
+						...(body.userId === this.$i.id ? {
+							isVoted: true
+						} : {})
+					};
+
+					n.poll = {
+						...this.appearNote.poll,
+						choices: choices
+					};
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'deleted': {
+					this.isDeleted = true;
+					break;
+				}
+			}
+		},
+
+		reply(viaKeyboard = false) {
+			pleaseLogin();
+			os.post({
+				reply: this.appearNote,
+				animation: !viaKeyboard,
+			}, () => {
+				this.focus();
+			});
+		},
+
+		renote(viaKeyboard = false) {
+			pleaseLogin();
+			this.blur();
+			os.popupMenu([{
+				text: this.$ts.renote,
+				icon: 'fas fa-retweet',
+				action: () => {
+					os.api('notes/create', {
+						renoteId: this.appearNote.id
+					});
+				}
+			}, {
+				text: this.$ts.quote,
+				icon: 'fas fa-quote-right',
+				action: () => {
+					os.post({
+						renote: this.appearNote,
+					});
+				}
+			}], this.$refs.renoteButton, {
+				viaKeyboard
+			});
+		},
+
+		renoteDirectly() {
+			os.apiWithDialog('notes/create', {
+				renoteId: this.appearNote.id
+			}, undefined, (res: any) => {
+				os.dialog({
+					type: 'success',
+					text: this.$ts.renoted,
+				});
+			}, (e: Error) => {
+				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantRenote,
+					});
+				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantReRenote,
+					});
+				}
+			});
+		},
+
+		react(viaKeyboard = false) {
+			pleaseLogin();
+			this.blur();
+			reactionPicker.show(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.focus();
+			});
+		},
+
+		reactDirectly(reaction) {
+			os.api('notes/reactions/create', {
+				noteId: this.appearNote.id,
+				reaction: reaction
+			});
+		},
+
+		undoReact(note) {
+			const oldReaction = note.myReaction;
+			if (!oldReaction) return;
+			os.api('notes/reactions/delete', {
+				noteId: note.id
+			});
+		},
+
+		favorite() {
+			pleaseLogin();
+			os.apiWithDialog('notes/favorites/create', {
+				noteId: this.appearNote.id
+			}, undefined, (res: any) => {
+				os.dialog({
+					type: 'success',
+					text: this.$ts.favorited,
+				});
+			}, (e: Error) => {
+				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.alreadyFavorited,
+					});
+				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantFavorite,
+					});
+				}
+			});
+		},
+
+		del() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.noteDeleteConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.api('notes/delete', {
+					noteId: this.appearNote.id
+				});
+			});
+		},
+
+		delEdit() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.deleteAndEditConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.api('notes/delete', {
+					noteId: this.appearNote.id
+				});
+
+				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
+			});
+		},
+
+		toggleFavorite(favorite: boolean) {
+			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
+		toggleWatch(watch: boolean) {
+			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
+		toggleThreadMute(mute: boolean) {
+			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
+		getMenu() {
+			let menu;
+			if (this.$i) {
+				const statePromise = os.api('notes/state', {
+					noteId: this.appearNote.id
+				});
+
+				menu = [{
+					icon: 'fas fa-copy',
+					text: this.$ts.copyContent,
+					action: this.copyContent
+				}, {
+					icon: 'fas fa-link',
+					text: this.$ts.copyLink,
+					action: this.copyLink
+				}, (this.appearNote.url || this.appearNote.uri) ? {
+					icon: 'fas fa-external-link-square-alt',
+					text: this.$ts.showOnRemote,
+					action: () => {
+						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
+					}
+				} : undefined,
+				{
+					icon: 'fas fa-share-alt',
+					text: this.$ts.share,
+					action: this.share
+				},
+				this.$instance.translatorAvailable ? {
+					icon: 'fas fa-language',
+					text: this.$ts.translate,
+					action: this.translate
+				} : undefined,
+				null,
+				statePromise.then(state => state.isFavorited ? {
+					icon: 'fas fa-star',
+					text: this.$ts.unfavorite,
+					action: () => this.toggleFavorite(false)
+				} : {
+					icon: 'fas fa-star',
+					text: this.$ts.favorite,
+					action: () => this.toggleFavorite(true)
+				}),
+				{
+					icon: 'fas fa-paperclip',
+					text: this.$ts.clip,
+					action: () => this.clip()
+				},
+				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
+					icon: 'fas fa-eye-slash',
+					text: this.$ts.unwatch,
+					action: () => this.toggleWatch(false)
+				} : {
+					icon: 'fas fa-eye',
+					text: this.$ts.watch,
+					action: () => this.toggleWatch(true)
+				}) : undefined,
+				statePromise.then(state => state.isMutedThread ? {
+					icon: 'fas fa-comment-slash',
+					text: this.$ts.unmuteThread,
+					action: () => this.toggleThreadMute(false)
+				} : {
+					icon: 'fas fa-comment-slash',
+					text: this.$ts.muteThread,
+					action: () => this.toggleThreadMute(true)
+				}),
+				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
+					icon: 'fas fa-thumbtack',
+					text: this.$ts.unpin,
+					action: () => this.togglePin(false)
+				} : {
+					icon: 'fas fa-thumbtack',
+					text: this.$ts.pin,
+					action: () => this.togglePin(true)
+				} : undefined,
+				...(this.$i.isModerator || this.$i.isAdmin ? [
+					null,
+					{
+						icon: 'fas fa-bullhorn',
+						text: this.$ts.promote,
+						action: this.promote
+					}]
+					: []
+				),
+				...(this.appearNote.userId != this.$i.id ? [
+					null,
+					{
+						icon: 'fas fa-exclamation-circle',
+						text: this.$ts.reportAbuse,
+						action: () => {
+							const u = `${url}/notes/${this.appearNote.id}`;
+							os.popup(import('@/components/abuse-report-window.vue'), {
+								user: this.appearNote.user,
+								initialComment: `Note: ${u}\n-----\n`
+							}, {}, 'closed');
+						}
+					}]
+					: []
+				),
+				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
+					null,
+					this.appearNote.userId == this.$i.id ? {
+						icon: 'fas fa-edit',
+						text: this.$ts.deleteAndEdit,
+						action: this.delEdit
+					} : undefined,
+					{
+						icon: 'fas fa-trash-alt',
+						text: this.$ts.delete,
+						danger: true,
+						action: this.del
+					}]
+					: []
+				)]
+				.filter(x => x !== undefined);
+			} else {
+				menu = [{
+					icon: 'fas fa-copy',
+					text: this.$ts.copyContent,
+					action: this.copyContent
+				}, {
+					icon: 'fas fa-link',
+					text: this.$ts.copyLink,
+					action: this.copyLink
+				}, (this.appearNote.url || this.appearNote.uri) ? {
+					icon: 'fas fa-external-link-square-alt',
+					text: this.$ts.showOnRemote,
+					action: () => {
+						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
+					}
+				} : undefined]
+				.filter(x => x !== undefined);
+			}
+
+			if (noteActions.length > 0) {
+				menu = menu.concat([null, ...noteActions.map(action => ({
+					icon: 'fas fa-plug',
+					text: action.title,
+					action: () => {
+						action.handler(this.appearNote);
+					}
+				}))]);
+			}
+
+			return menu;
+		},
+
+		onContextmenu(e) {
+			const isLink = (el: HTMLElement) => {
+				if (el.tagName === 'A') return true;
+				if (el.parentElement) {
+					return isLink(el.parentElement);
+				}
+			};
+			if (isLink(e.target)) return;
+			if (window.getSelection().toString() !== '') return;
+
+			if (this.$store.state.useReactionPickerForContextMenu) {
+				e.preventDefault();
+				this.react();
+			} else {
+				os.contextMenu(this.getMenu(), e).then(this.focus);
+			}
+		},
+
+		menu(viaKeyboard = false) {
+			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
+				viaKeyboard
+			}).then(this.focus);
+		},
+
+		showRenoteMenu(viaKeyboard = false) {
+			if (!this.isMyRenote) return;
+			os.popupMenu([{
+				text: this.$ts.unrenote,
+				icon: 'fas fa-trash-alt',
+				danger: true,
+				action: () => {
+					os.api('notes/delete', {
+						noteId: this.note.id
+					});
+					this.isDeleted = true;
+				}
+			}], this.$refs.renoteTime, {
+				viaKeyboard: viaKeyboard
+			});
+		},
+
+		toggleShowContent() {
+			this.showContent = !this.showContent;
+		},
+
+		copyContent() {
+			copyToClipboard(this.appearNote.text);
+			os.success();
+		},
+
+		copyLink() {
+			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
+			os.success();
+		},
+
+		togglePin(pin: boolean) {
+			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
+				noteId: this.appearNote.id
+			}, undefined, null, e => {
+				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.pinLimitExceeded
+					});
+				}
+			});
+		},
+
+		async clip() {
+			const clips = await os.api('clips/list');
+			os.popupMenu([{
+				icon: 'fas fa-plus',
+				text: this.$ts.createNew,
+				action: async () => {
+					const { canceled, result } = await os.form(this.$ts.createNewClip, {
+						name: {
+							type: 'string',
+							label: this.$ts.name
+						},
+						description: {
+							type: 'string',
+							required: false,
+							multiline: true,
+							label: this.$ts.description
+						},
+						isPublic: {
+							type: 'boolean',
+							label: this.$ts.public,
+							default: false
+						}
+					});
+					if (canceled) return;
+
+					const clip = await os.apiWithDialog('clips/create', result);
+
+					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
+				}
+			}, null, ...clips.map(clip => ({
+				text: clip.name,
+				action: () => {
+					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
+				}
+			}))], this.$refs.menuButton, {
+			}).then(this.focus);
+		},
+
+		async promote() {
+			const { canceled, result: days } = await os.dialog({
+				title: this.$ts.numberOfDays,
+				input: { type: 'number' }
+			});
+
+			if (canceled) return;
+
+			os.apiWithDialog('admin/promo/create', {
+				noteId: this.appearNote.id,
+				expiresAt: Date.now() + (86400000 * days)
+			});
+		},
+
+		share() {
+			navigator.share({
+				title: this.$t('noteOf', { user: this.appearNote.user.name }),
+				text: this.appearNote.text,
+				url: `${url}/notes/${this.appearNote.id}`
+			});
+		},
+
+		async translate() {
+			if (this.translation != null) return;
+			this.translating = true;
+			const res = await os.api('notes/translate', {
+				noteId: this.appearNote.id,
+				targetLang: localStorage.getItem('lang') || navigator.language,
+			});
+			this.translating = false;
+			this.translation = res;
+		},
+
+		focus() {
+			this.$el.focus();
+		},
+
+		blur() {
+			this.$el.blur();
+		},
+
+		focusBefore() {
+			focusPrev(this.$el);
+		},
+
+		focusAfter() {
+			focusNext(this.$el);
+		},
+
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.tkcbzcuz {
+	position: relative;
+	transition: box-shadow 0.1s ease;
+	overflow: clip;
+	contain: content;
+
+	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
+	// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
+	// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
+	// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
+	// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか？)
+	//content-visibility: auto;
+  //contain-intrinsic-size: 0 128px;
+
+	&:focus-visible {
+		outline: none;
+
+		&:after {
+			content: "";
+			pointer-events: none;
+			display: block;
+			position: absolute;
+			z-index: 10;
+			top: 0;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			margin: auto;
+			width: calc(100% - 8px);
+			height: calc(100% - 8px);
+			border: dashed 1px var(--focus);
+			border-radius: var(--radius);
+			box-sizing: border-box;
+		}
+	}
+
+	&:hover > .article > .main > .footer > .button {
+		opacity: 1;
+	}
+
+	> .info {
+		display: flex;
+		align-items: center;
+		padding: 16px 32px 8px 32px;
+		line-height: 24px;
+		font-size: 90%;
+		white-space: pre;
+		color: #d28a3f;
+
+		> i {
+			margin-right: 4px;
+		}
+
+		> .hide {
+			margin-left: auto;
+			color: inherit;
+		}
+	}
+
+	> .info + .article {
+		padding-top: 8px;
+	}
+
+	> .reply-to {
+		opacity: 0.7;
+		padding-bottom: 0;
+	}
+
+	> .renote {
+		display: flex;
+		align-items: center;
+		padding: 16px 32px 8px 32px;
+		line-height: 28px;
+		white-space: pre;
+		color: var(--renote);
+
+		> .avatar {
+			flex-shrink: 0;
+			display: inline-block;
+			width: 28px;
+			height: 28px;
+			margin: 0 8px 0 0;
+			border-radius: 6px;
+		}
+
+		> i {
+			margin-right: 4px;
+		}
+
+		> span {
+			overflow: hidden;
+			flex-shrink: 1;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+
+			> .name {
+				font-weight: bold;
+			}
+		}
+
+		> .info {
+			margin-left: auto;
+			font-size: 0.9em;
+
+			> .time {
+				flex-shrink: 0;
+				color: inherit;
+
+				> .dropdownIcon {
+					margin-right: 4px;
+				}
+			}
+
+			> .visibility {
+				margin-left: 8px;
+			}
+
+			> .localOnly {
+				margin-left: 8px;
+			}
+		}
+	}
+
+	> .renote + .article {
+		padding-top: 8px;
+	}
+
+	> .article {
+		display: flex;
+		padding: 28px 32px 18px;
+
+		> .avatar {
+			flex-shrink: 0;
+			display: block;
+			margin: 0 14px 8px 0;
+			width: 58px;
+			height: 58px;
+			position: sticky;
+			top: calc(22px + var(--stickyTop, 0px));
+			left: 0;
+		}
+
+		> .main {
+			flex: 1;
+			min-width: 0;
+
+			> .body {
+				> .cw {
+					cursor: default;
+					display: block;
+					margin: 0;
+					padding: 0;
+					overflow-wrap: break-word;
+
+					> .text {
+						margin-right: 8px;
+					}
+				}
+
+				> .content {
+					&.collapsed {
+						position: relative;
+						max-height: 9em;
+						overflow: hidden;
+
+						> .fade {
+							display: block;
+							position: absolute;
+							bottom: 0;
+							left: 0;
+							width: 100%;
+							height: 64px;
+							background: linear-gradient(0deg, var(--panel), var(--X15));
+
+							> span {
+								display: inline-block;
+								background: var(--panel);
+								padding: 6px 10px;
+								font-size: 0.8em;
+								border-radius: 999px;
+								box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+							}
+
+							&:hover {
+								> span {
+									background: var(--panelHighlight);
+								}
+							}
+						}
+					}
+
+					> .text {
+						overflow-wrap: break-word;
+
+						> .reply {
+							color: var(--accent);
+							margin-right: 0.5em;
+						}
+
+						> .rp {
+							margin-left: 4px;
+							font-style: oblique;
+							color: var(--renote);
+						}
+
+						> .translation {
+							border: solid 0.5px var(--divider);
+							border-radius: var(--radius);
+							padding: 12px;
+							margin-top: 8px;
+						}
+					}
+
+					> .url-preview {
+						margin-top: 8px;
+					}
+
+					> .poll {
+						font-size: 80%;
+					}
+
+					> .renote {
+						padding: 8px 0;
+
+						> * {
+							padding: 16px;
+							border: dashed 1px var(--renote);
+							border-radius: 8px;
+						}
+					}
+				}
+
+				> .channel {
+					opacity: 0.7;
+					font-size: 80%;
+				}
+			}
+
+			> .footer {
+				> .button {
+					margin: 0;
+					padding: 8px;
+					opacity: 0.7;
+
+					&:not(:last-child) {
+						margin-right: 28px;
+					}
+
+					&:hover {
+						color: var(--fgHighlighted);
+					}
+
+					> .count {
+						display: inline;
+						margin: 0 0 0 8px;
+						opacity: 0.7;
+					}
+
+					&.reacted {
+						color: var(--accent);
+					}
+				}
+			}
+		}
+	}
+
+	> .reply {
+		border-top: solid 0.5px var(--divider);
+	}
+
+	&.max-width_500px {
+		font-size: 0.9em;
+	}
+
+	&.max-width_450px {
+		> .renote {
+			padding: 8px 16px 0 16px;
+		}
+
+		> .info {
+			padding: 8px 16px 0 16px;
+		}
+
+		> .article {
+			padding: 14px 16px 9px;
+
+			> .avatar {
+				margin: 0 10px 8px 0;
+				width: 50px;
+				height: 50px;
+				top: calc(14px + var(--stickyTop, 0px));
+			}
+		}
+	}
+
+	&.max-width_350px {
+		> .article {
+			> .main {
+				> .footer {
+					> .button {
+						&:not(:last-child) {
+							margin-right: 18px;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	&.max-width_300px {
+		font-size: 0.825em;
+
+		> .article {
+			> .avatar {
+				width: 44px;
+				height: 44px;
+			}
+
+			> .main {
+				> .footer {
+					> .button {
+						&:not(:last-child) {
+							margin-right: 12px;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+
+.muted {
+	padding: 8px;
+	text-align: center;
+	opacity: 0.7;
+}
+</style>
diff --git a/packages/client/src/components/notes.vue b/packages/client/src/components/notes.vue
new file mode 100644
index 0000000000..1e7da7a2b0
--- /dev/null
+++ b/packages/client/src/components/notes.vue
@@ -0,0 +1,130 @@
+<template>
+<transition name="fade" mode="out-in">
+	<MkLoading v-if="fetching"/>
+
+	<MkError v-else-if="error" @retry="init()"/>
+
+	<div class="_fullinfo" v-else-if="empty">
+		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+		<div>{{ $ts.noNotes }}</div>
+	</div>
+
+	<div v-else class="giivymft" :class="{ noGap }">
+		<div v-show="more && reversed" style="margin-bottom: var(--margin);">
+			<MkButton style="margin: 0 auto;" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+				<template v-if="moreFetching"><MkLoading inline/></template>
+			</MkButton>
+		</div>
+
+		<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true" class="notes">
+			<XNote class="qtqtichx" :note="note" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
+		</XList>
+
+		<div v-show="more && !reversed" style="margin-top: var(--margin);">
+			<MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+				<template v-if="moreFetching"><MkLoading inline/></template>
+			</MkButton>
+		</div>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import paging from '@/scripts/paging';
+import XNote from './note.vue';
+import XList from './date-separated-list.vue';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+	components: {
+		XNote, XList, MkButton,
+	},
+
+	mixins: [
+		paging({
+			before: (self) => {
+				self.$emit('before');
+			},
+
+			after: (self, e) => {
+				self.$emit('after', e);
+			}
+		}),
+	],
+
+	props: {
+		pagination: {
+			required: true
+		},
+		prop: {
+			type: String,
+			required: false
+		},
+		noGap: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	emits: ['before', 'after'],
+
+	computed: {
+		notes(): any[] {
+			return this.prop ? this.items.map(item => item[this.prop]) : this.items;
+		},
+
+		reversed(): boolean {
+			return this.pagination.reversed;
+		}
+	},
+
+	methods: {
+		updated(oldValue, newValue) {
+			const i = this.notes.findIndex(n => n === oldValue);
+			if (this.prop) {
+				this.items[i][this.prop] = newValue;
+			} else {
+				this.items[i] = newValue;
+			}
+		},
+
+		focus() {
+			this.$refs.notes.focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.giivymft {
+	&.noGap {
+		> .notes {
+			background: var(--panel);
+		}
+	}
+
+	&:not(.noGap) {
+		> .notes {
+			background: var(--bg);
+
+			.qtqtichx {
+				background: var(--panel);
+				border-radius: var(--radius);
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/notification-setting-window.vue b/packages/client/src/components/notification-setting-window.vue
new file mode 100644
index 0000000000..ec1efec261
--- /dev/null
+++ b/packages/client/src/components/notification-setting-window.vue
@@ -0,0 +1,99 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="400"
+	:height="450"
+	:with-ok-button="true"
+	:ok-button-disabled="false"
+	@ok="ok()"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+>
+	<template #header>{{ $ts.notificationSetting }}</template>
+	<div class="_monolithic_">
+		<div v-if="showGlobalToggle" class="_section">
+			<MkSwitch v-model="useGlobalSetting">
+				{{ $ts.useGlobalSetting }}
+				<template #caption>{{ $ts.useGlobalSettingDesc }}</template>
+			</MkSwitch>
+		</div>
+		<div v-if="!useGlobalSetting" class="_section">
+			<MkInfo>{{ $ts.notificationSettingDesc }}</MkInfo>
+			<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
+			<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
+			<MkSwitch v-for="type in notificationTypes" :key="type" v-model="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch>
+		</div>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import MkSwitch from './form/switch.vue';
+import MkInfo from './ui/info.vue';
+import MkButton from './ui/button.vue';
+import { notificationTypes } from 'misskey-js';
+
+export default defineComponent({
+	components: {
+		XModalWindow,
+		MkSwitch,
+		MkInfo,
+		MkButton
+	},
+
+	props: {
+		includingTypes: {
+			// TODO: これで型に合わないものを弾いてくれるのかどうか要調査
+			type: Array as PropType<typeof notificationTypes[number][]>,
+			required: false,
+			default: null,
+		},
+		showGlobalToggle: {
+			type: Boolean,
+			required: false,
+			default: true,
+		}
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			typesMap: {} as Record<typeof notificationTypes[number], boolean>,
+			useGlobalSetting: false,
+			notificationTypes,
+		};
+	},
+
+	created() {
+		this.useGlobalSetting = this.includingTypes === null && this.showGlobalToggle;
+
+		for (const type of this.notificationTypes) {
+			this.typesMap[type] = this.includingTypes === null || this.includingTypes.includes(type);
+		}
+	},
+
+	methods: {
+		ok() {
+			const includingTypes = this.useGlobalSetting ? null : (Object.keys(this.typesMap) as typeof notificationTypes[number][])
+				.filter(type => this.typesMap[type]);
+
+			this.$emit('done', { includingTypes });
+			this.$refs.dialog.close();
+		},
+
+		disableAll() {
+			for (const type in this.typesMap) {
+				this.typesMap[type as typeof notificationTypes[number]] = false;
+			}
+		},
+
+		enableAll() {
+			for (const type in this.typesMap) {
+				this.typesMap[type as typeof notificationTypes[number]] = true;
+			}
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue
new file mode 100644
index 0000000000..b629820043
--- /dev/null
+++ b/packages/client/src/components/notification.vue
@@ -0,0 +1,362 @@
+<template>
+<div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }" ref="elRef">
+	<div class="head">
+		<MkAvatar v-if="notification.user" class="icon" :user="notification.user"/>
+		<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
+		<div class="sub-icon" :class="notification.type">
+			<i v-if="notification.type === 'follow'" class="fas fa-plus"></i>
+			<i v-else-if="notification.type === 'receiveFollowRequest'" class="fas fa-clock"></i>
+			<i v-else-if="notification.type === 'followRequestAccepted'" class="fas fa-check"></i>
+			<i v-else-if="notification.type === 'groupInvited'" class="fas fa-id-card-alt"></i>
+			<i v-else-if="notification.type === 'renote'" class="fas fa-retweet"></i>
+			<i v-else-if="notification.type === 'reply'" class="fas fa-reply"></i>
+			<i v-else-if="notification.type === 'mention'" class="fas fa-at"></i>
+			<i v-else-if="notification.type === 'quote'" class="fas fa-quote-left"></i>
+			<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
+			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
+			<XReactionIcon v-else-if="notification.type === 'reaction'"
+				:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
+				:custom-emojis="notification.note.emojis"
+				:no-style="true"
+				@touchstart.passive="onReactionMouseover"
+				@mouseover="onReactionMouseover"
+				@mouseleave="onReactionMouseleave"
+				@touchend="onReactionMouseleave"
+				ref="reactionRef"
+			/>
+		</div>
+	</div>
+	<div class="tail">
+		<header>
+			<MkA v-if="notification.user" class="name" :to="userPage(notification.user)" v-user-preview="notification.user.id"><MkUserName :user="notification.user"/></MkA>
+			<span v-else>{{ notification.header }}</span>
+			<MkTime :time="notification.createdAt" v-if="withTime" class="time"/>
+		</header>
+		<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
+			<i class="fas fa-quote-left"></i>
+			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
+			<i class="fas fa-quote-right"></i>
+		</MkA>
+		<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
+			<i class="fas fa-quote-left"></i>
+			<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
+			<i class="fas fa-quote-right"></i>
+		</MkA>
+		<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
+			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
+		</MkA>
+		<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
+			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
+		</MkA>
+		<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
+			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
+		</MkA>
+		<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
+			<i class="fas fa-quote-left"></i>
+			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
+			<i class="fas fa-quote-right"></i>
+		</MkA>
+		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
+		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $ts.followRequestAccepted }}</span>
+		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $ts.reject }}</button></div></span>
+		<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $ts.reject }}</button></div></span>
+		<span v-if="notification.type === 'app'" class="text">
+			<Mfm :text="notification.body" :nowrap="!full"/>
+		</span>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
+import { getNoteSummary } from '@/scripts/get-note-summary';
+import XReactionIcon from './reaction-icon.vue';
+import MkFollowButton from './follow-button.vue';
+import XReactionTooltip from './reaction-tooltip.vue';
+import notePage from '@/filters/note';
+import { userPage } from '@/filters/user';
+import { i18n } from '@/i18n';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XReactionIcon, MkFollowButton
+	},
+
+	props: {
+		notification: {
+			type: Object,
+			required: true,
+		},
+		withTime: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		full: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+	},
+
+	setup(props) {
+		const elRef = ref<HTMLElement>(null);
+		const reactionRef = ref(null);
+
+		onMounted(() => {
+			let readObserver: IntersectionObserver = null;
+			let connection = null;
+
+			if (!props.notification.isRead) {
+				readObserver = new IntersectionObserver((entries, observer) => {
+					if (!entries.some(entry => entry.isIntersecting)) return;
+					os.stream.send('readNotification', {
+						id: props.notification.id
+					});
+					entries.map(({ target }) => observer.unobserve(target));
+				});
+
+				readObserver.observe(elRef.value);
+
+				connection = os.stream.useChannel('main');
+				connection.on('readAllNotifications', () => readObserver.unobserve(elRef.value));
+			}
+
+			onUnmounted(() => {
+				if (readObserver) readObserver.unobserve(elRef.value);
+				if (connection) connection.dispose();
+			});
+		});
+
+		const followRequestDone = ref(false);
+		const groupInviteDone = ref(false);
+
+		const acceptFollowRequest = () => {
+			followRequestDone.value = true;
+			os.api('following/requests/accept', { userId: props.notification.user.id });
+		};
+
+		const rejectFollowRequest = () => {
+			followRequestDone.value = true;
+			os.api('following/requests/reject', { userId: props.notification.user.id });
+		};
+
+		const acceptGroupInvitation = () => {
+			groupInviteDone.value = true;
+			os.apiWithDialog('users/groups/invitations/accept', { invitationId: props.notification.invitation.id });
+		};
+
+		const rejectGroupInvitation = () => {
+			groupInviteDone.value = true;
+			os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
+		};
+
+		let isReactionHovering = false;
+		let reactionTooltipTimeoutId;
+
+		const onReactionMouseover = () => {
+			if (isReactionHovering) return;
+			isReactionHovering = true;
+			reactionTooltipTimeoutId = setTimeout(openReactionTooltip, 300);
+		};
+
+		const onReactionMouseleave = () => {
+			if (!isReactionHovering) return;
+			isReactionHovering = false;
+			clearTimeout(reactionTooltipTimeoutId);
+			closeReactionTooltip();
+		};
+
+		let changeReactionTooltipShowingState: () => void;
+
+		const openReactionTooltip = () => {
+			closeReactionTooltip();
+			if (!isReactionHovering) return;
+
+			const showing = ref(true);
+			os.popup(XReactionTooltip, {
+				showing,
+				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
+				emojis: props.notification.note.emojis,
+				source: reactionRef.value.$el,
+			}, {}, 'closed');
+
+			changeReactionTooltipShowingState = () => {
+				showing.value = false;
+			};
+		};
+
+		const closeReactionTooltip = () => {
+			if (changeReactionTooltipShowingState != null) {
+				changeReactionTooltipShowingState();
+				changeReactionTooltipShowingState = null;
+			}
+		};
+
+		return {
+			getNoteSummary: (text: string) => getNoteSummary(text, i18n.locale),
+			followRequestDone,
+			groupInviteDone,
+			notePage,
+			userPage,
+			acceptFollowRequest,
+			rejectFollowRequest,
+			acceptGroupInvitation,
+			rejectGroupInvitation,
+			onReactionMouseover,
+			onReactionMouseleave,
+			elRef,
+			reactionRef,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.qglefbjs {
+	position: relative;
+	box-sizing: border-box;
+	padding: 24px 32px;
+	font-size: 0.9em;
+	overflow-wrap: break-word;
+	display: flex;
+	contain: content;
+
+	&.max-width_600px {
+		padding: 16px;
+		font-size: 0.9em;
+	}
+
+	&.max-width_500px {
+		padding: 12px;
+		font-size: 0.8em;
+	}
+
+	&:after {
+		content: "";
+		display: block;
+		clear: both;
+	}
+
+	> .head {
+		position: sticky;
+		top: 0;
+		flex-shrink: 0;
+		width: 42px;
+		height: 42px;
+		margin-right: 8px;
+
+		> .icon {
+			display: block;
+			width: 100%;
+			height: 100%;
+			border-radius: 6px;
+		}
+
+		> .sub-icon {
+			position: absolute;
+			z-index: 1;
+			bottom: -2px;
+			right: -2px;
+			width: 20px;
+			height: 20px;
+			box-sizing: border-box;
+			border-radius: 100%;
+			background: var(--panel);
+			box-shadow: 0 0 0 3px var(--panel);
+			font-size: 12px;
+			text-align: center;
+
+			&:empty {
+				display: none;
+			}
+
+			> * {
+				color: #fff;
+				width: 100%;
+				height: 100%;
+			}
+
+			&.follow, &.followRequestAccepted, &.receiveFollowRequest, &.groupInvited {
+				padding: 3px;
+				background: #36aed2;
+				pointer-events: none;
+			}
+
+			&.renote {
+				padding: 3px;
+				background: #36d298;
+				pointer-events: none;
+			}
+
+			&.quote {
+				padding: 3px;
+				background: #36d298;
+				pointer-events: none;
+			}
+
+			&.reply {
+				padding: 3px;
+				background: #007aff;
+				pointer-events: none;
+			}
+
+			&.mention {
+				padding: 3px;
+				background: #88a6b7;
+				pointer-events: none;
+			}
+
+			&.pollVote {
+				padding: 3px;
+				background: #88a6b7;
+				pointer-events: none;
+			}
+		}
+	}
+
+	> .tail {
+		flex: 1;
+		min-width: 0;
+
+		> header {
+			display: flex;
+			align-items: baseline;
+			white-space: nowrap;
+
+			> .name {
+				text-overflow: ellipsis;
+				white-space: nowrap;
+				min-width: 0;
+				overflow: hidden;
+			}
+
+			> .time {
+				margin-left: auto;
+				font-size: 0.9em;
+			}
+		}
+
+		> .text {
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+
+			> i {
+				vertical-align: super;
+				font-size: 50%;
+				opacity: 0.5;
+			}
+
+			> i:first-child {
+				margin-right: 4px;
+			}
+
+			> i:last-child {
+				margin-left: 4px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue
new file mode 100644
index 0000000000..4ebb12c44b
--- /dev/null
+++ b/packages/client/src/components/notifications.vue
@@ -0,0 +1,159 @@
+<template>
+<transition name="fade" mode="out-in">
+	<MkLoading v-if="fetching"/>
+
+	<MkError v-else-if="error" @retry="init()"/>
+
+	<p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p>
+
+	<div v-else>
+		<XList class="elsfgstc" :items="items" v-slot="{ item: notification }" :no-gap="true">
+			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/>
+			<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
+		</XList>
+
+		<MkButton primary style="margin: var(--margin) auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+			<template v-if="moreFetching"><MkLoading inline/></template>
+		</MkButton>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, markRaw } from 'vue';
+import paging from '@/scripts/paging';
+import XNotification from './notification.vue';
+import XList from './date-separated-list.vue';
+import XNote from './note.vue';
+import { notificationTypes } from 'misskey-js';
+import * as os from '@/os';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+	components: {
+		XNotification,
+		XList,
+		XNote,
+		MkButton,
+	},
+
+	mixins: [
+		paging({}),
+	],
+
+	props: {
+		includeTypes: {
+			type: Array as PropType<typeof notificationTypes[number][]>,
+			required: false,
+			default: null,
+		},
+		unreadOnly: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+	},
+
+	data() {
+		return {
+			connection: null,
+			pagination: {
+				endpoint: 'i/notifications',
+				limit: 10,
+				params: () => ({
+					includeTypes: this.allIncludeTypes || undefined,
+					unreadOnly: this.unreadOnly,
+				})
+			},
+		};
+	},
+
+	computed: {
+		allIncludeTypes() {
+			return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
+		}
+	},
+
+	watch: {
+		includeTypes: {
+			handler() {
+				this.reload();
+			},
+			deep: true
+		},
+		unreadOnly: {
+			handler() {
+				this.reload();
+			},
+		},
+		// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、
+		// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
+		'$i.mutingNotificationTypes': {
+			handler() {
+				if (this.includeTypes === null) {
+					this.reload();
+				}
+			},
+			deep: true
+		}
+	},
+
+	mounted() {
+		this.connection = markRaw(os.stream.useChannel('main'));
+		this.connection.on('notification', this.onNotification);
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		onNotification(notification) {
+			const isMuted = !this.allIncludeTypes.includes(notification.type);
+			if (isMuted || document.visibilityState === 'visible') {
+				os.stream.send('readNotification', {
+					id: notification.id
+				});
+			}
+
+			if (!isMuted) {
+				this.prepend({
+					...notification,
+					isRead: document.visibilityState === 'visible'
+				});
+			}
+		},
+
+		noteUpdated(oldValue, newValue) {
+			const i = this.items.findIndex(n => n.note === oldValue);
+			this.items[i] = {
+				...this.items[i],
+				note: newValue
+			};
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.mfcuwfyp {
+	margin: 0;
+	padding: 16px;
+	text-align: center;
+	color: var(--fg);
+}
+
+.elsfgstc {
+	background: var(--panel);
+}
+</style>
diff --git a/packages/client/src/components/number-diff.vue b/packages/client/src/components/number-diff.vue
new file mode 100644
index 0000000000..9889c97ec3
--- /dev/null
+++ b/packages/client/src/components/number-diff.vue
@@ -0,0 +1,47 @@
+<template>
+<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
+	<slot name="before"></slot>{{ isPlus ? '+' : '' }}{{ number(value) }}<slot name="after"></slot>
+</span>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import number from '@/filters/number';
+
+export default defineComponent({
+	props: {
+		value: {
+			type: Number,
+			required: true
+		},
+	},
+
+	setup(props) {
+		const isPlus = computed(() => props.value > 0);
+		const isMinus = computed(() => props.value < 0);
+		const isZero = computed(() => props.value === 0);
+		return {
+			isPlus,
+			isMinus,
+			isZero,
+			number,
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ceaaebcd {
+	&.isPlus {
+		color: var(--success);
+	}
+
+	&.isMinus {
+		color: var(--error);
+	}
+
+	&.isZero {
+		opacity: 0.5;
+	}
+}
+</style>
diff --git a/packages/client/src/components/page-preview.vue b/packages/client/src/components/page-preview.vue
new file mode 100644
index 0000000000..05df1dc16e
--- /dev/null
+++ b/packages/client/src/components/page-preview.vue
@@ -0,0 +1,162 @@
+<template>
+<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block _isolated" tabindex="-1">
+	<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
+	<article>
+		<header>
+			<h1 :title="page.title">{{ page.title }}</h1>
+		</header>
+		<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
+		<footer>
+			<img class="icon" :src="page.user.avatarUrl"/>
+			<p>{{ userName(page.user) }}</p>
+		</footer>
+	</article>
+</MkA>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { userName } from '@/filters/user';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		page: {
+			type: Object,
+			required: true
+		},
+	},
+	methods: {
+		userName
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vhpxefrj {
+	display: block;
+
+	&:hover {
+		text-decoration: none;
+		color: var(--accent);
+	}
+
+	> .thumbnail {
+		width: 100%;
+		height: 200px;
+		background-position: center;
+		background-size: cover;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+
+		> button {
+			font-size: 3.5em;
+			opacity: 0.7;
+
+			&:hover {
+				font-size: 4em;
+				opacity: 0.9;
+			}
+		}
+
+		& + article {
+			left: 100px;
+			width: calc(100% - 100px);
+		}
+	}
+
+	> article {
+		padding: 16px;
+
+		> header {
+			margin-bottom: 8px;
+
+			> h1 {
+				margin: 0;
+				font-size: 1em;
+				color: var(--urlPreviewTitle);
+			}
+		}
+
+		> p {
+			margin: 0;
+			color: var(--urlPreviewText);
+			font-size: 0.8em;
+		}
+
+		> footer {
+			margin-top: 8px;
+			height: 16px;
+
+			> img {
+				display: inline-block;
+				width: 16px;
+				height: 16px;
+				margin-right: 4px;
+				vertical-align: top;
+			}
+
+			> p {
+				display: inline-block;
+				margin: 0;
+				color: var(--urlPreviewInfo);
+				font-size: 0.8em;
+				line-height: 16px;
+				vertical-align: top;
+			}
+		}
+	}
+
+	@media (max-width: 700px) {
+		> .thumbnail {
+			position: relative;
+			width: 100%;
+			height: 100px;
+
+			& + article {
+				left: 0;
+				width: 100%;
+			}
+		}
+	}
+
+	@media (max-width: 550px) {
+		font-size: 12px;
+
+		> .thumbnail {
+			height: 80px;
+		}
+
+		> article {
+			padding: 12px;
+		}
+	}
+
+	@media (max-width: 500px) {
+		font-size: 10px;
+
+		> .thumbnail {
+			height: 70px;
+		}
+
+		> article {
+			padding: 8px;
+
+			> header {
+				margin-bottom: 4px;
+			}
+
+			> footer {
+				margin-top: 4px;
+
+				> img {
+					width: 12px;
+					height: 12px;
+				}
+			}
+		}
+	}
+}
+
+</style>
diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue
new file mode 100644
index 0000000000..b6be114cd7
--- /dev/null
+++ b/packages/client/src/components/page-window.vue
@@ -0,0 +1,167 @@
+<template>
+<XWindow ref="window"
+	:initial-width="500"
+	:initial-height="500"
+	:can-resize="true"
+	:close-button="true"
+	:contextmenu="contextmenu"
+	@closed="$emit('closed')"
+>
+	<template #header>
+		<template v-if="pageInfo">
+			<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i>
+			<span>{{ pageInfo.title }}</span>
+		</template>
+	</template>
+	<template #headerLeft>
+		<button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
+	</template>
+	<div class="yrolvcoq">
+		<MkStickyContainer>
+			<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
+			<component :is="component" v-bind="props" :ref="changePage"/>
+		</MkStickyContainer>
+	</div>
+</XWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XWindow from '@/components/ui/window.vue';
+import { popout } from '@/scripts/popout';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { resolve } from '@/router';
+import { url } from '@/config';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XWindow,
+	},
+
+	inject: {
+		sideViewHook: {
+			default: null
+		}
+	},
+
+	provide() {
+		return {
+			navHook: (path) => {
+				this.navigate(path);
+			},
+			shouldHeaderThin: true,
+		};
+	},
+
+	props: {
+		initialPath: {
+			type: String,
+			required: true,
+		},
+		initialComponent: {
+			type: Object,
+			required: true,
+		},
+		initialProps: {
+			type: Object,
+			required: false,
+			default: () => {},
+		},
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			pageInfo: null,
+			path: this.initialPath,
+			component: this.initialComponent,
+			props: this.initialProps,
+			history: [],
+		};
+	},
+
+	computed: {
+		url(): string {
+			return url + this.path;
+		},
+
+		contextmenu() {
+			return [{
+				type: 'label',
+				text: this.path,
+			}, {
+				icon: 'fas fa-expand-alt',
+				text: this.$ts.showInPage,
+				action: this.expand
+			}, this.sideViewHook ? {
+				icon: 'fas fa-columns',
+				text: this.$ts.openInSideView,
+				action: () => {
+					this.sideViewHook(this.path);
+					this.$refs.window.close();
+				}
+			} : undefined, {
+				icon: 'fas fa-external-link-alt',
+				text: this.$ts.popout,
+				action: this.popout
+			}, null, {
+				icon: 'fas fa-external-link-alt',
+				text: this.$ts.openInNewTab,
+				action: () => {
+					window.open(this.url, '_blank');
+					this.$refs.window.close();
+				}
+			}, {
+				icon: 'fas fa-link',
+				text: this.$ts.copyLink,
+				action: () => {
+					copyToClipboard(this.url);
+				}
+			}];
+		},
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+			}
+		},
+
+		navigate(path, record = true) {
+			if (record) this.history.push(this.path);
+			this.path = path;
+			const { component, props } = resolve(path);
+			this.component = component;
+			this.props = props;
+		},
+
+		back() {
+			this.navigate(this.history.pop(), false);
+		},
+
+		close() {
+			this.$refs.window.close();
+		},
+
+		expand() {
+			this.$router.push(this.path);
+			this.$refs.window.close();
+		},
+
+		popout() {
+			popout(this.path, this.$el);
+			this.$refs.window.close();
+		},
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.yrolvcoq {
+	min-height: 100%;
+}
+</style>
diff --git a/packages/client/src/components/page/page.block.vue b/packages/client/src/components/page/page.block.vue
new file mode 100644
index 0000000000..54b8b30276
--- /dev/null
+++ b/packages/client/src/components/page/page.block.vue
@@ -0,0 +1,44 @@
+<template>
+<component :is="'x-' + block.type" :block="block" :hpml="hpml" :key="block.id" :h="h"/>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import XText from './page.text.vue';
+import XSection from './page.section.vue';
+import XImage from './page.image.vue';
+import XButton from './page.button.vue';
+import XNumberInput from './page.number-input.vue';
+import XTextInput from './page.text-input.vue';
+import XTextareaInput from './page.textarea-input.vue';
+import XSwitch from './page.switch.vue';
+import XIf from './page.if.vue';
+import XTextarea from './page.textarea.vue';
+import XPost from './page.post.vue';
+import XCounter from './page.counter.vue';
+import XRadioButton from './page.radio-button.vue';
+import XCanvas from './page.canvas.vue';
+import XNote from './page.note.vue';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { Block } from '@/scripts/hpml/block';
+
+export default defineComponent({
+	components: {
+		XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote
+	},
+	props: {
+		block: {
+			type: Object as PropType<Block>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		},
+		h: {
+			type: Number,
+			required: true
+		}
+	},
+});
+</script>
diff --git a/packages/client/src/components/page/page.button.vue b/packages/client/src/components/page/page.button.vue
new file mode 100644
index 0000000000..51da84bd49
--- /dev/null
+++ b/packages/client/src/components/page/page.button.vue
@@ -0,0 +1,66 @@
+<template>
+<div>
+	<MkButton class="kudkigyw" @click="click()" :primary="block.primary">{{ hpml.interpolate(block.text) }}</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, unref } from 'vue';
+import MkButton from '../ui/button.vue';
+import * as os from '@/os';
+import { ButtonBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+	props: {
+		block: {
+			type: Object as PropType<ButtonBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	methods: {
+		click() {
+			if (this.block.action === 'dialog') {
+				this.hpml.eval();
+				os.dialog({
+					text: this.hpml.interpolate(this.block.content)
+				});
+			} else if (this.block.action === 'resetRandom') {
+				this.hpml.updateRandomSeed(Math.random());
+				this.hpml.eval();
+			} else if (this.block.action === 'pushEvent') {
+				os.api('page-push', {
+					pageId: this.hpml.page.id,
+					event: this.block.event,
+					...(this.block.var ? {
+						var: unref(this.hpml.vars)[this.block.var]
+					} : {})
+				});
+
+				os.dialog({
+					type: 'success',
+					text: this.hpml.interpolate(this.block.message)
+				});
+			} else if (this.block.action === 'callAiScript') {
+				this.hpml.callAiScript(this.block.fn);
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.kudkigyw {
+	display: inline-block;
+	min-width: 200px;
+	max-width: 450px;
+	margin: 8px 0;
+}
+</style>
diff --git a/packages/client/src/components/page/page.canvas.vue b/packages/client/src/components/page/page.canvas.vue
new file mode 100644
index 0000000000..8f49b88e5e
--- /dev/null
+++ b/packages/client/src/components/page/page.canvas.vue
@@ -0,0 +1,49 @@
+<template>
+<div class="ysrxegms">
+	<canvas ref="canvas" :width="block.width" :height="block.height"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
+import * as os from '@/os';
+import { CanvasBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+	props: {
+		block: {
+			type: Object as PropType<CanvasBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const canvas: Ref<any> = ref(null);
+
+		onMounted(() => {
+			props.hpml.registerCanvas(props.block.name, canvas.value);
+		});
+
+		return {
+			canvas
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ysrxegms {
+	display: inline-block;
+	vertical-align: bottom;
+	overflow: auto;
+	max-width: 100%;
+
+	> canvas {
+		display: block;
+	}
+}
+</style>
diff --git a/packages/client/src/components/page/page.counter.vue b/packages/client/src/components/page/page.counter.vue
new file mode 100644
index 0000000000..b1af8954b0
--- /dev/null
+++ b/packages/client/src/components/page/page.counter.vue
@@ -0,0 +1,52 @@
+<template>
+<div>
+	<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkButton from '../ui/button.vue';
+import * as os from '@/os';
+import { CounterVarBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+	props: {
+		block: {
+			type: Object as PropType<CounterVarBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const value = computed(() => {
+			return props.hpml.vars.value[props.block.name];
+		});
+
+		function click() {
+			props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1));
+			props.hpml.eval();
+		}
+
+		return {
+			click
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.llumlmnx {
+	display: inline-block;
+	min-width: 300px;
+	max-width: 450px;
+	margin: 8px 0;
+}
+</style>
diff --git a/packages/client/src/components/page/page.if.vue b/packages/client/src/components/page/page.if.vue
new file mode 100644
index 0000000000..ec25332db0
--- /dev/null
+++ b/packages/client/src/components/page/page.if.vue
@@ -0,0 +1,31 @@
+<template>
+<div v-show="hpml.vars.value[block.var]">
+	<XBlock v-for="child in block.children" :block="child" :hpml="hpml" :key="child.id" :h="h"/>
+</div>
+</template>
+
+<script lang="ts">
+import { IfBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { defineComponent, defineAsyncComponent, PropType } from 'vue';
+
+export default defineComponent({
+	components: {
+		XBlock: defineAsyncComponent(() => import('./page.block.vue'))
+	},
+	props: {
+		block: {
+			type: Object as PropType<IfBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		},
+		h: {
+			type: Number,
+			required: true
+		}
+	},
+});
+</script>
diff --git a/packages/client/src/components/page/page.image.vue b/packages/client/src/components/page/page.image.vue
new file mode 100644
index 0000000000..04ce74bd7c
--- /dev/null
+++ b/packages/client/src/components/page/page.image.vue
@@ -0,0 +1,40 @@
+<template>
+<div class="lzyxtsnt">
+	<img v-if="image" :src="image.url"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import * as os from '@/os';
+import { ImageBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+	props: {
+		block: {
+			type: Object as PropType<ImageBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
+
+		return {
+			image
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.lzyxtsnt {
+	> img {
+		max-width: 100%;
+	}
+}
+</style>
diff --git a/packages/client/src/components/page/page.note.vue b/packages/client/src/components/page/page.note.vue
new file mode 100644
index 0000000000..925844c1bd
--- /dev/null
+++ b/packages/client/src/components/page/page.note.vue
@@ -0,0 +1,47 @@
+<template>
+<div class="voxdxuby">
+	<XNote v-if="note && !block.detailed" v-model:note="note" :key="note.id + ':normal'"/>
+	<XNoteDetailed v-if="note && block.detailed" v-model:note="note" :key="note.id + ':detail'"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
+import XNote from '@/components/note.vue';
+import XNoteDetailed from '@/components/note-detailed.vue';
+import * as os from '@/os';
+import { NoteBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+	components: {
+		XNote,
+		XNoteDetailed,
+	},
+	props: {
+		block: {
+			type: Object as PropType<NoteBlock>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const note: Ref<Record<string, any> | null> = ref(null);
+
+		onMounted(() => {
+			os.api('notes/show', { noteId: props.block.note })
+			.then(result => {
+				note.value = result;
+			});
+		});
+
+		return {
+			note
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.voxdxuby {
+	margin: 1em 0;
+}
+</style>
diff --git a/packages/client/src/components/page/page.number-input.vue b/packages/client/src/components/page/page.number-input.vue
new file mode 100644
index 0000000000..b5120d0f85
--- /dev/null
+++ b/packages/client/src/components/page/page.number-input.vue
@@ -0,0 +1,55 @@
+<template>
+<div>
+	<MkInput class="kudkigyw" :model-value="value" @update:modelValue="updateValue($event)" type="number">
+		<template #label>{{ hpml.interpolate(block.text) }}</template>
+	</MkInput>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkInput from '../form/input.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { NumberInputVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+	components: {
+		MkInput
+	},
+	props: {
+		block: {
+			type: Object as PropType<NumberInputVarBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const value = computed(() => {
+			return props.hpml.vars.value[props.block.name];
+		});
+
+		function updateValue(newValue) {
+			props.hpml.updatePageVar(props.block.name, newValue);
+			props.hpml.eval();
+		}
+
+		return {
+			value,
+			updateValue
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.kudkigyw {
+	display: inline-block;
+	min-width: 300px;
+	max-width: 450px;
+	margin: 8px 0;
+}
+</style>
diff --git a/packages/client/src/components/page/page.post.vue b/packages/client/src/components/page/page.post.vue
new file mode 100644
index 0000000000..1b86ea1ab9
--- /dev/null
+++ b/packages/client/src/components/page/page.post.vue
@@ -0,0 +1,109 @@
+<template>
+<div class="ngbfujlo">
+	<MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea>
+	<MkButton class="button" primary @click="post()" :disabled="posting || posted">
+		<i v-if="posted" class="fas fa-check"></i>
+		<i v-else class="fas fa-paper-plane"></i>
+	</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import MkTextarea from '../form/textarea.vue';
+import MkButton from '../ui/button.vue';
+import { apiUrl } from '@/config';
+import * as os from '@/os';
+import { PostBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+	components: {
+		MkTextarea,
+		MkButton,
+	},
+	props: {
+		block: {
+			type: Object as PropType<PostBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	data() {
+		return {
+			text: this.hpml.interpolate(this.block.text),
+			posted: false,
+			posting: false,
+		};
+	},
+	watch: {
+		'hpml.vars': {
+			handler() {
+				this.text = this.hpml.interpolate(this.block.text);
+			},
+			deep: true
+		}
+	},
+	methods: {
+		upload() {
+			const promise = new Promise((ok) => {
+				const canvas = this.hpml.canvases[this.block.canvasId];
+				canvas.toBlob(blob => {
+					const data = new FormData();
+					data.append('file', blob);
+					data.append('i', this.$i.token);
+					if (this.$store.state.uploadFolder) {
+						data.append('folderId', this.$store.state.uploadFolder);
+					}
+
+					fetch(apiUrl + '/drive/files/create', {
+						method: 'POST',
+						body: data
+					})
+					.then(response => response.json())
+					.then(f => {
+						ok(f);
+					})
+				});
+			});
+			os.promiseDialog(promise);
+			return promise;
+		},
+		async post() {
+			this.posting = true;
+			const file = this.block.attachCanvasImage ? await this.upload() : null;
+			os.apiWithDialog('notes/create', {
+				text: this.text === '' ? null : this.text,
+				fileIds: file ? [file.id] : undefined,
+			}).then(() => {
+				this.posted = true;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ngbfujlo {
+	position: relative;
+	padding: 32px;
+	border-radius: 6px;
+	box-shadow: 0 2px 8px var(--shadow);
+	z-index: 1;
+
+	> .button {
+		margin-top: 32px;
+	}
+
+	@media (max-width: 600px) {
+		padding: 16px;
+
+		> .button {
+			margin-top: 16px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/page/page.radio-button.vue b/packages/client/src/components/page/page.radio-button.vue
new file mode 100644
index 0000000000..4d3c03291e
--- /dev/null
+++ b/packages/client/src/components/page/page.radio-button.vue
@@ -0,0 +1,45 @@
+<template>
+<div>
+	<div>{{ hpml.interpolate(block.title) }}</div>
+	<MkRadio v-for="item in block.values" :modelValue="value" @update:modelValue="updateValue($event)" :value="item" :key="item">{{ item }}</MkRadio>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkRadio from '../form/radio.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { RadioButtonVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+	components: {
+		MkRadio
+	},
+	props: {
+		block: {
+			type: Object as PropType<RadioButtonVarBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const value = computed(() => {
+			return props.hpml.vars.value[props.block.name];
+		});
+
+		function updateValue(newValue: string) {
+			props.hpml.updatePageVar(props.block.name, newValue);
+			props.hpml.eval();
+		}
+
+		return {
+			value,
+			updateValue
+		};
+	}
+});
+</script>
diff --git a/packages/client/src/components/page/page.section.vue b/packages/client/src/components/page/page.section.vue
new file mode 100644
index 0000000000..d32f5dc732
--- /dev/null
+++ b/packages/client/src/components/page/page.section.vue
@@ -0,0 +1,60 @@
+<template>
+<section class="sdgxphyu">
+	<component :is="'h' + h">{{ block.title }}</component>
+
+	<div class="children">
+		<XBlock v-for="child in block.children" :block="child" :hpml="hpml" :key="child.id" :h="h + 1"/>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, PropType } from 'vue';
+import * as os from '@/os';
+import { SectionBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+	components: {
+		XBlock: defineAsyncComponent(() => import('./page.block.vue'))
+	},
+	props: {
+		block: {
+			type: Object as PropType<SectionBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		},
+		h: {
+			required: true
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.sdgxphyu {
+	margin: 1.5em 0;
+
+	> h2 {
+		font-size: 1.35em;
+		margin: 0 0 0.5em 0;
+	}
+
+	> h3 {
+		font-size: 1em;
+		margin: 0 0 0.5em 0;
+	}
+
+	> h4 {
+		font-size: 1em;
+		margin: 0 0 0.5em 0;
+	}
+
+	> .children {
+		//padding 16px
+	}
+}
+</style>
diff --git a/packages/client/src/components/page/page.switch.vue b/packages/client/src/components/page/page.switch.vue
new file mode 100644
index 0000000000..1ece88157f
--- /dev/null
+++ b/packages/client/src/components/page/page.switch.vue
@@ -0,0 +1,55 @@
+<template>
+<div class="hkcxmtwj">
+	<MkSwitch :model-value="value" @update:modelValue="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkSwitch from '../form/switch.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { SwitchVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+	components: {
+		MkSwitch
+	},
+	props: {
+		block: {
+			type: Object as PropType<SwitchVarBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const value = computed(() => {
+			return props.hpml.vars.value[props.block.name];
+		});
+
+		function updateValue(newValue: boolean) {
+			props.hpml.updatePageVar(props.block.name, newValue);
+			props.hpml.eval();
+		}
+
+		return {
+			value,
+			updateValue
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hkcxmtwj {
+	display: inline-block;
+	margin: 16px auto;
+
+	& + .hkcxmtwj {
+		margin-left: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/page/page.text-input.vue b/packages/client/src/components/page/page.text-input.vue
new file mode 100644
index 0000000000..e4d3f6039a
--- /dev/null
+++ b/packages/client/src/components/page/page.text-input.vue
@@ -0,0 +1,55 @@
+<template>
+<div>
+	<MkInput class="kudkigyw" :model-value="value" @update:modelValue="updateValue($event)" type="text">
+		<template #label>{{ hpml.interpolate(block.text) }}</template>
+	</MkInput>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkInput from '../form/input.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { TextInputVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+	components: {
+		MkInput
+	},
+	props: {
+		block: {
+			type: Object as PropType<TextInputVarBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const value = computed(() => {
+			return props.hpml.vars.value[props.block.name];
+		});
+
+		function updateValue(newValue) {
+			props.hpml.updatePageVar(props.block.name, newValue);
+			props.hpml.eval();
+		}
+
+		return {
+			value,
+			updateValue
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.kudkigyw {
+	display: inline-block;
+	min-width: 300px;
+	max-width: 450px;
+	margin: 8px 0;
+}
+</style>
diff --git a/packages/client/src/components/page/page.text.vue b/packages/client/src/components/page/page.text.vue
new file mode 100644
index 0000000000..7dd41ed869
--- /dev/null
+++ b/packages/client/src/components/page/page.text.vue
@@ -0,0 +1,68 @@
+<template>
+<div class="mrdgzndn">
+	<Mfm :text="text" :is-note="false" :i="$i" :key="text"/>
+	<MkUrlPreview v-for="url in urls" :url="url" :key="url" class="url"/>
+</div>
+</template>
+
+<script lang="ts">
+import { TextBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { defineAsyncComponent, defineComponent, PropType } from 'vue';
+import * as mfm from 'mfm-js';
+import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
+
+export default defineComponent({
+	components: {
+		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
+	},
+	props: {
+		block: {
+			type: Object as PropType<TextBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	data() {
+		return {
+			text: this.hpml.interpolate(this.block.text),
+		};
+	},
+	computed: {
+		urls(): string[] {
+			if (this.text) {
+				return extractUrlFromMfm(mfm.parse(this.text));
+			} else {
+				return [];
+			}
+		}
+	},
+	watch: {
+		'hpml.vars': {
+			handler() {
+				this.text = this.hpml.interpolate(this.block.text);
+			},
+			deep: true
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mrdgzndn {
+	&:not(:first-child) {
+		margin-top: 0.5em;
+	}
+
+	&:not(:last-child) {
+		margin-bottom: 0.5em;
+	}
+
+	> .url {
+		margin: 0.5em 0;
+	}
+}
+</style>
diff --git a/packages/client/src/components/page/page.textarea-input.vue b/packages/client/src/components/page/page.textarea-input.vue
new file mode 100644
index 0000000000..6e082b2bef
--- /dev/null
+++ b/packages/client/src/components/page/page.textarea-input.vue
@@ -0,0 +1,47 @@
+<template>
+<div>
+	<MkTextarea :model-value="value" @update:modelValue="updateValue($event)">
+		<template #label>{{ hpml.interpolate(block.text) }}</template>
+	</MkTextarea>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkTextarea from '../form/textarea.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { HpmlTextInput } from '@/scripts/hpml';
+import { TextInputVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+	components: {
+		MkTextarea
+	},
+	props: {
+		block: {
+			type: Object as PropType<TextInputVarBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	setup(props, ctx) {
+		const value = computed(() => {
+			return props.hpml.vars.value[props.block.name];
+		});
+
+		function updateValue(newValue) {
+			props.hpml.updatePageVar(props.block.name, newValue);
+			props.hpml.eval();
+		}
+
+		return {
+			value,
+			updateValue
+		};
+	}
+});
+</script>
diff --git a/packages/client/src/components/page/page.textarea.vue b/packages/client/src/components/page/page.textarea.vue
new file mode 100644
index 0000000000..5b4ee2b452
--- /dev/null
+++ b/packages/client/src/components/page/page.textarea.vue
@@ -0,0 +1,39 @@
+<template>
+<MkTextarea :model-value="text" readonly></MkTextarea>
+</template>
+
+<script lang="ts">
+import { TextBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { defineComponent, PropType } from 'vue';
+import MkTextarea from '../form/textarea.vue';
+
+export default defineComponent({
+	components: {
+		MkTextarea
+	},
+	props: {
+		block: {
+			type: Object as PropType<TextBlock>,
+			required: true
+		},
+		hpml: {
+			type: Object as PropType<Hpml>,
+			required: true
+		}
+	},
+	data() {
+		return {
+			text: this.hpml.interpolate(this.block.text),
+		};
+	},
+	watch: {
+		'hpml.vars': {
+			handler() {
+				this.text = this.hpml.interpolate(this.block.text);
+			},
+			deep: true
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/page/page.vue b/packages/client/src/components/page/page.vue
new file mode 100644
index 0000000000..6d1c419a40
--- /dev/null
+++ b/packages/client/src/components/page/page.vue
@@ -0,0 +1,86 @@
+<template>
+<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }" v-if="hpml">
+	<XBlock v-for="child in page.content" :block="child" :hpml="hpml" :key="child.id" :h="2"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, nextTick, onUnmounted, PropType } from 'vue';
+import { parse } from '@syuilo/aiscript';
+import XBlock from './page.block.vue';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { url } from '@/config';
+import { $i } from '@/account';
+import { defaultStore } from '@/store';
+
+export default defineComponent({
+	components: {
+		XBlock
+	},
+	props: {
+		page: {
+			type: Object as PropType<Record<string, any>>,
+			required: true
+		},
+	},
+	setup(props, ctx) {
+
+		const hpml = new Hpml(props.page, {
+			randomSeed: Math.random(),
+			visitor: $i,
+			url: url,
+			enableAiScript: !defaultStore.state.disablePagesScript
+		});
+
+		onMounted(() => {
+			nextTick(() => {
+				if (props.page.script && hpml.aiscript) {
+					let ast;
+					try {
+						ast = parse(props.page.script);
+					} catch (e) {
+						console.error(e);
+						/*os.dialog({
+							type: 'error',
+							text: 'Syntax error :('
+						});*/
+						return;
+					}
+					hpml.aiscript.exec(ast).then(() => {
+						hpml.eval();
+					}).catch(e => {
+						console.error(e);
+						/*os.dialog({
+							type: 'error',
+							text: e
+						});*/
+					});
+				} else {
+					hpml.eval();
+				}
+			});
+			onUnmounted(() => {
+				if (hpml.aiscript) hpml.aiscript.abort();
+			});
+		});
+
+		return {
+			hpml,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.iroscrza {
+	&.serif {
+		> div {
+			font-family: serif;
+		}
+	}
+
+	&.center {
+		text-align: center;
+	}
+}
+</style>
diff --git a/packages/client/src/components/particle.vue b/packages/client/src/components/particle.vue
new file mode 100644
index 0000000000..d82705c1e8
--- /dev/null
+++ b/packages/client/src/components/particle.vue
@@ -0,0 +1,114 @@
+<template>
+<div class="vswabwbm" :style="{ top: `${y - 64}px`, left: `${x - 64}px` }" :class="{ active }">
+	<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
+		<circle fill="none" cx="64" cy="64">
+			<animate attributeName="r"
+				begin="0s" dur="0.5s"
+				values="4; 32"
+				calcMode="spline"
+				keyTimes="0; 1"
+				keySplines="0.165, 0.84, 0.44, 1"
+				repeatCount="1"
+			/>
+			<animate attributeName="stroke-width"
+				begin="0s" dur="0.5s"
+				values="16; 0"
+				calcMode="spline"
+				keyTimes="0; 1"
+				keySplines="0.3, 0.61, 0.355, 1"
+				repeatCount="1"
+			/>
+		</circle>
+		<g fill="none" fill-rule="evenodd">
+			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
+				<animate attributeName="r"
+					begin="0s" dur="0.8s"
+					:values="`${particle.size}; 0`"
+					calcMode="spline"
+					keyTimes="0; 1"
+					keySplines="0.165, 0.84, 0.44, 1"
+					repeatCount="1"
+				/>
+				<animate attributeName="cx"
+					begin="0s" dur="0.8s"
+					:values="`${particle.xA}; ${particle.xB}`"
+					calcMode="spline"
+					keyTimes="0; 1"
+					keySplines="0.3, 0.61, 0.355, 1"
+					repeatCount="1"
+				/>
+				<animate attributeName="cy"
+					begin="0s" dur="0.8s"
+					:values="`${particle.yA}; ${particle.yB}`"
+					calcMode="spline"
+					keyTimes="0; 1"
+					keySplines="0.3, 0.61, 0.355, 1"
+					repeatCount="1"
+				/>
+			</circle>
+		</g>
+	</svg>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		x: {
+			type: Number,
+			required: true
+		},
+		y: {
+			type: Number,
+			required: true
+		}
+	},
+	emits: ['end'],
+	data() {
+		const particles = [];
+		const origin = 64;
+		const colors = ['#FF1493', '#00FFFF', '#FFE202'];
+
+		for (let i = 0; i < 12; i++) {
+			const angle = Math.random() * (Math.PI * 2);
+			const pos = Math.random() * 16;
+			const velocity = 16 + (Math.random() * 48);
+			particles.push({
+				size: 4 + (Math.random() * 8),
+				xA: origin + (Math.sin(angle) * pos),
+				yA: origin + (Math.cos(angle) * pos),
+				xB: origin + (Math.sin(angle) * (pos + velocity)),
+				yB: origin + (Math.cos(angle) * (pos + velocity)),
+				color: colors[Math.floor(Math.random() * colors.length)]
+			});
+		}
+
+		return {
+			particles
+		};
+	},
+	mounted() {
+		setTimeout(() => {
+			this.$emit('end');
+		}, 1100);
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vswabwbm {
+	pointer-events: none;
+	position: fixed;
+	z-index: 1000000;
+	width: 128px;
+	height: 128px;
+
+	> svg {
+		> circle {
+			stroke: var(--accent);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue
new file mode 100644
index 0000000000..aa213cfe49
--- /dev/null
+++ b/packages/client/src/components/poll-editor.vue
@@ -0,0 +1,251 @@
+<template>
+<div class="zmdxowus">
+	<p class="caution" v-if="choices.length < 2">
+		<i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }}
+	</p>
+	<ul ref="choices">
+		<li v-for="(choice, i) in choices" :key="i">
+			<MkInput class="input" :model-value="choice" @update:modelValue="onInput(i, $event)" :placeholder="$t('_poll.choiceN', { n: i + 1 })">
+			</MkInput>
+			<button @click="remove(i)" class="_button">
+				<i class="fas fa-times"></i>
+			</button>
+		</li>
+	</ul>
+	<MkButton class="add" v-if="choices.length < 10" @click="add">{{ $ts.add }}</MkButton>
+	<MkButton class="add" v-else disabled>{{ $ts._poll.noMore }}</MkButton>
+	<section>
+		<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
+		<div>
+			<MkSelect v-model="expiration">
+				<template #label>{{ $ts._poll.expiration }}</template>
+				<option value="infinite">{{ $ts._poll.infinite }}</option>
+				<option value="at">{{ $ts._poll.at }}</option>
+				<option value="after">{{ $ts._poll.after }}</option>
+			</MkSelect>
+			<section v-if="expiration === 'at'">
+				<MkInput v-model="atDate" type="date" class="input">
+					<template #label>{{ $ts._poll.deadlineDate }}</template>
+				</MkInput>
+				<MkInput v-model="atTime" type="time" class="input">
+					<template #label>{{ $ts._poll.deadlineTime }}</template>
+				</MkInput>
+			</section>
+			<section v-if="expiration === 'after'">
+				<MkInput v-model="after" type="number" class="input">
+					<template #label>{{ $ts._poll.duration }}</template>
+				</MkInput>
+				<MkSelect v-model="unit">
+					<option value="second">{{ $ts._time.second }}</option>
+					<option value="minute">{{ $ts._time.minute }}</option>
+					<option value="hour">{{ $ts._time.hour }}</option>
+					<option value="day">{{ $ts._time.day }}</option>
+				</MkSelect>
+			</section>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { addTime } from '@/scripts/time';
+import { formatDateTimeString } from '@/scripts/format-time-string';
+import MkInput from './form/input.vue';
+import MkSelect from './form/select.vue';
+import MkSwitch from './form/switch.vue';
+import MkButton from './ui/button.vue';
+
+export default defineComponent({
+	components: {
+		MkInput,
+		MkSelect,
+		MkSwitch,
+		MkButton,
+	},
+
+	props: {
+		poll: {
+			type: Object,
+			required: true
+		}
+	},
+
+	emits: ['updated'],
+
+	data() {
+		return {
+			choices: this.poll.choices,
+			multiple: this.poll.multiple,
+			expiration: 'infinite',
+			atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'),
+			atTime: '00:00',
+			after: 0,
+			unit: 'second',
+		};
+	},
+
+	watch: {
+		choices: {
+			handler() {
+				this.$emit('updated', this.get());
+			},
+			deep: true
+		},
+		multiple: {
+			handler() {
+				this.$emit('updated', this.get());
+			},
+		},
+		expiration: {
+			handler() {
+				this.$emit('updated', this.get());
+			},
+		},
+		atDate: {
+			handler() {
+				this.$emit('updated', this.get());
+			},
+		},
+		after: {
+			handler() {
+				this.$emit('updated', this.get());
+			},
+		},
+		unit: {
+			handler() {
+				this.$emit('updated', this.get());
+			},
+		},
+	},
+
+	created() {
+		const poll = this.poll;
+		if (poll.expiresAt) {
+			this.expiration = 'at';
+			this.atDate = this.atTime = poll.expiresAt;
+		} else if (typeof poll.expiredAfter === 'number') {
+			this.expiration = 'after';
+			this.after = poll.expiredAfter / 1000;
+		} else {
+			this.expiration = 'infinite';
+		}
+	},
+
+	methods: {
+		onInput(i, e) {
+			this.choices[i] = e;
+		},
+
+		add() {
+			this.choices.push('');
+			this.$nextTick(() => {
+				// TODO
+				//(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
+			});
+		},
+
+		remove(i) {
+			this.choices = this.choices.filter((_, _i) => _i != i);
+		},
+
+		get() {
+			const at = () => {
+				return new Date(`${this.atDate} ${this.atTime}`).getTime();
+			};
+
+			const after = () => {
+				let base = parseInt(this.after);
+				switch (this.unit) {
+					case 'day': base *= 24;
+					case 'hour': base *= 60;
+					case 'minute': base *= 60;
+					case 'second': return base *= 1000;
+					default: return null;
+				}
+			};
+
+			return {
+				choices: this.choices,
+				multiple: this.multiple,
+				...(
+					this.expiration === 'at' ? { expiresAt: at() } :
+					this.expiration === 'after' ? { expiredAfter: after() } : {}
+				)
+			};
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.zmdxowus {
+	padding: 8px;
+
+	> .caution {
+		margin: 0 0 8px 0;
+		font-size: 0.8em;
+		color: #f00;
+
+		> i {
+			margin-right: 4px;
+		}
+	}
+
+	> ul {
+		display: block;
+		margin: 0;
+		padding: 0;
+		list-style: none;
+
+		> li {
+			display: flex;
+			margin: 8px 0;
+			padding: 0;
+			width: 100%;
+
+			> .input {
+				flex: 1;
+				margin-top: 16px;
+				margin-bottom: 0;
+			}
+
+			> button {
+				width: 32px;
+				padding: 4px 0;
+			}
+		}
+	}
+
+	> .add {
+		margin: 8px 0 0 0;
+		z-index: 1;
+	}
+
+	> section {
+		margin: 16px 0 -16px 0;
+
+		> div {
+			margin: 0 8px;
+
+			&:last-child {
+				flex: 1 0 auto;
+
+				> section {
+					align-items: center;
+					display: flex;
+					margin: -32px 0 0;
+
+					> &:first-child {
+						margin-right: 16px;
+					}
+
+					> .input {
+						flex: 1 0 auto;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/poll.vue b/packages/client/src/components/poll.vue
new file mode 100644
index 0000000000..049fe3a435
--- /dev/null
+++ b/packages/client/src/components/poll.vue
@@ -0,0 +1,174 @@
+<template>
+<div class="tivcixzd" :class="{ done: closed || isVoted }">
+	<ul>
+		<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }">
+			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
+			<span>
+				<template v-if="choice.isVoted"><i class="fas fa-check"></i></template>
+				<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
+				<span class="votes" v-if="showResult">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span>
+			</span>
+		</li>
+	</ul>
+	<p v-if="!readOnly">
+		<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
+		<span> · </span>
+		<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a>
+		<span v-if="isVoted">{{ $ts._poll.voted }}</span>
+		<span v-else-if="closed">{{ $ts._poll.closed }}</span>
+		<span v-if="remaining > 0"> · {{ timer }}</span>
+	</p>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { sum } from '@/scripts/array';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		readOnly: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+	data() {
+		return {
+			remaining: -1,
+			showResult: false,
+		};
+	},
+	computed: {
+		poll(): any {
+			return this.note.poll;
+		},
+		total(): number {
+			return sum(this.poll.choices.map(x => x.votes));
+		},
+		closed(): boolean {
+			return !this.remaining;
+		},
+		timer(): string {
+			return this.$t(
+				this.remaining >= 86400 ? '_poll.remainingDays' :
+				this.remaining >= 3600 ? '_poll.remainingHours' :
+				this.remaining >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
+					s: Math.floor(this.remaining % 60),
+					m: Math.floor(this.remaining / 60) % 60,
+					h: Math.floor(this.remaining / 3600) % 24,
+					d: Math.floor(this.remaining / 86400)
+				});
+		},
+		isVoted(): boolean {
+			return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
+		}
+	},
+	created() {
+		this.showResult = this.readOnly || this.isVoted;
+
+		if (this.note.poll.expiresAt) {
+			const update = () => {
+				if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
+					requestAnimationFrame(update);
+				else
+					this.showResult = true;
+			};
+
+			update();
+		}
+	},
+	methods: {
+		toggleShowResult() {
+			this.showResult = !this.showResult;
+		},
+		vote(id) {
+			if (this.readOnly || this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
+			os.api('notes/polls/vote', {
+				noteId: this.note.id,
+				choice: id
+			}).then(() => {
+				if (!this.showResult) this.showResult = !this.poll.multiple;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.tivcixzd {
+	> ul {
+		display: block;
+		margin: 0;
+		padding: 0;
+		list-style: none;
+
+		> li {
+			display: block;
+			position: relative;
+			margin: 4px 0;
+			padding: 4px 8px;
+			border: solid 0.5px var(--divider);
+			border-radius: 4px;
+			overflow: hidden;
+			cursor: pointer;
+
+			&:hover {
+				background: rgba(#000, 0.05);
+			}
+
+			&:active {
+				background: rgba(#000, 0.1);
+			}
+
+			> .backdrop {
+				position: absolute;
+				top: 0;
+				left: 0;
+				height: 100%;
+				background: var(--accent);
+				transition: width 1s ease;
+			}
+
+			> span {
+				position: relative;
+
+				> i {
+					margin-right: 4px;
+				}
+
+				> .votes {
+					margin-left: 4px;
+				}
+			}
+		}
+	}
+
+	> p {
+		color: var(--fg);
+
+		a {
+			color: inherit;
+		}
+	}
+
+	&.done {
+		> ul > li {
+			cursor: default;
+
+			&:hover {
+				background: transparent;
+			}
+
+			&:active {
+				background: transparent;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/post-form-attaches.vue b/packages/client/src/components/post-form-attaches.vue
new file mode 100644
index 0000000000..dff0dec21e
--- /dev/null
+++ b/packages/client/src/components/post-form-attaches.vue
@@ -0,0 +1,193 @@
+<template>
+<div class="skeikyzd" v-show="files.length != 0">
+	<XDraggable class="files" v-model="_files" item-key="id" animation="150" delay="100" delay-on-touch-only="true">
+		<template #item="{element}">
+			<div @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
+				<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
+				<div class="sensitive" v-if="element.isSensitive">
+					<i class="fas fa-exclamation-triangle icon"></i>
+				</div>
+			</div>
+		</template>
+	</XDraggable>
+	<p class="remain">{{ 4 - files.length }}/4</p>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import MkDriveFileThumbnail from './drive-file-thumbnail.vue'
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
+		MkDriveFileThumbnail
+	},
+
+	props: {
+		files: {
+			type: Array,
+			required: true
+		},
+		detachMediaFn: {
+			type: Function,
+			required: false
+		}
+	},
+
+	emits: ['updated', 'detach', 'changeSensitive', 'changeName'],
+
+	data() {
+		return {
+			menu: null as Promise<null> | null,
+
+		};
+	},
+
+	computed: {
+		_files: {
+			get() {
+				return this.files;
+			},
+			set(value) {
+				this.$emit('updated', value);
+			}
+		}
+	},
+
+	methods: {
+		detachMedia(id) {
+			if (this.detachMediaFn) {
+				this.detachMediaFn(id);
+			} else {
+				this.$emit('detach', id);
+			}
+		},
+		toggleSensitive(file) {
+			os.api('drive/files/update', {
+				fileId: file.id,
+				isSensitive: !file.isSensitive
+			}).then(() => {
+				this.$emit('changeSensitive', file, !file.isSensitive);
+			});
+		},
+		async rename(file) {
+			const { canceled, result } = await os.dialog({
+				title: this.$ts.enterFileName,
+				input: {
+					default: file.name
+				},
+				allowEmpty: false
+			});
+			if (canceled) return;
+			os.api('drive/files/update', {
+				fileId: file.id,
+				name: result
+			}).then(() => {
+				this.$emit('changeName', file, result);
+				file.name = result;
+			});
+		},
+
+		async describe(file) {
+			os.popup(import("@/components/media-caption.vue"), {
+				title: this.$ts.describeFile,
+				input: {
+					placeholder: this.$ts.inputNewDescription,
+					default: file.comment !== null ? file.comment : "",
+				},
+				image: file
+			}, {
+				done: result => {
+					if (!result || result.canceled) return;
+					let comment = result.result;
+					os.api('drive/files/update', {
+						fileId: file.id,
+						comment: comment.length == 0 ? null : comment
+					});
+				}
+			}, 'closed');
+		},
+
+		showFileMenu(file, ev: MouseEvent) {
+			if (this.menu) return;
+			this.menu = os.popupMenu([{
+				text: this.$ts.renameFile,
+				icon: 'fas fa-i-cursor',
+				action: () => { this.rename(file) }
+			}, {
+				text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
+				icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
+				action: () => { this.toggleSensitive(file) }
+			}, {
+				text: this.$ts.describeFile,
+				icon: 'fas fa-i-cursor',
+				action: () => { this.describe(file) }
+			}, {
+				text: this.$ts.attachCancel,
+				icon: 'fas fa-times-circle',
+				action: () => { this.detachMedia(file.id) }
+			}], ev.currentTarget || ev.target).then(() => this.menu = null);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.skeikyzd {
+	padding: 8px 16px;
+	position: relative;
+
+	> .files {
+		display: flex;
+		flex-wrap: wrap;
+
+		> div {
+			position: relative;
+			width: 64px;
+			height: 64px;
+			margin-right: 4px;
+			border-radius: 4px;
+			overflow: hidden;
+			cursor: move;
+
+			&:hover > .remove {
+				display: block;
+			}
+
+			> .thumbnail {
+				width: 100%;
+				height: 100%;
+				z-index: 1;
+				color: var(--fg);
+			}
+
+			> .sensitive {
+				display: flex;
+				position: absolute;
+				width: 64px;
+				height: 64px;
+				top: 0;
+				left: 0;
+				z-index: 2;
+				background: rgba(17, 17, 17, .7);
+				color: #fff;
+
+				> .icon {
+					margin: auto;
+				}
+			}
+		}
+	}
+
+	> .remain {
+		display: block;
+		position: absolute;
+		top: 8px;
+		right: 8px;
+		margin: 0;
+		padding: 0;
+	}
+}
+</style>
diff --git a/packages/client/src/components/post-form-dialog.vue b/packages/client/src/components/post-form-dialog.vue
new file mode 100644
index 0000000000..ae1cd7f01e
--- /dev/null
+++ b/packages/client/src/components/post-form-dialog.vue
@@ -0,0 +1,19 @@
+<template>
+<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')" :position="'top'">
+	<MkPostForm @posted="$refs.modal.close()" @cancel="$refs.modal.close()" @esc="$refs.modal.close()" v-bind="$attrs"/>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from '@/components/ui/modal.vue';
+import MkPostForm from '@/components/post-form.vue';
+
+export default defineComponent({
+	components: {
+		MkModal,
+		MkPostForm,
+	},
+	emits: ['closed'],
+});
+</script>
diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
new file mode 100644
index 0000000000..ce6b7db3ee
--- /dev/null
+++ b/packages/client/src/components/post-form.vue
@@ -0,0 +1,980 @@
+<template>
+<div class="gafaadew" :class="{ modal, _popup: modal }"
+	v-size="{ max: [310, 500] }"
+	@dragover.stop="onDragover"
+	@dragenter="onDragenter"
+	@dragleave="onDragleave"
+	@drop.stop="onDrop"
+>
+	<header>
+		<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
+		<div>
+			<span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
+			<span class="local-only" v-if="localOnly"><i class="fas fa-biohazard"></i></span>
+			<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$ts.visibility" :disabled="channel != null">
+				<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
+				<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
+				<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
+				<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
+			</button>
+			<button class="_button preview" @click="showPreview = !showPreview" :class="{ active: showPreview }" v-tooltip="$ts.previewNoteText"><i class="fas fa-file-code"></i></button>
+			<button class="submit _buttonGradate" :disabled="!canPost" @click="post" data-cy-open-post-form-submit>{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
+		</div>
+	</header>
+	<div class="form" :class="{ fixed }">
+		<XNoteSimple class="preview" v-if="reply" :note="reply"/>
+		<XNoteSimple class="preview" v-if="renote" :note="renote"/>
+		<div class="with-quote" v-if="quoteId"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
+		<div v-if="visibility === 'specified'" class="to-specified">
+			<span style="margin-right: 8px;">{{ $ts.recipient }}</span>
+			<div class="visibleUsers">
+				<span v-for="u in visibleUsers" :key="u.id">
+					<MkAcct :user="u"/>
+					<button class="_button" @click="removeVisibleUser(u)"><i class="fas fa-times"></i></button>
+				</span>
+				<button @click="addVisibleUser" class="_buttonPrimary"><i class="fas fa-plus fa-fw"></i></button>
+			</div>
+		</div>
+		<MkInfo warn v-if="hasNotSpecifiedMentions" class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo>
+		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
+		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd" data-cy-post-form-text/>
+		<input v-show="withHashtags" ref="hashtags" class="hashtags" v-model="hashtags" :placeholder="$ts.hashtags" list="hashtags">
+		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
+		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
+		<XNotePreview class="preview" v-if="showPreview" :text="text"/>
+		<footer>
+			<button class="_button" @click="chooseFileFrom" v-tooltip="$ts.attachFile"><i class="fas fa-photo-video"></i></button>
+			<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$ts.poll"><i class="fas fa-poll-h"></i></button>
+			<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$ts.useCw"><i class="fas fa-eye-slash"></i></button>
+			<button class="_button" @click="insertMention" v-tooltip="$ts.mention"><i class="fas fa-at"></i></button>
+			<button class="_button" @click="withHashtags = !withHashtags" :class="{ active: withHashtags }" v-tooltip="$ts.hashtags"><i class="fas fa-hashtag"></i></button>
+			<button class="_button" @click="insertEmoji" v-tooltip="$ts.emoji"><i class="fas fa-laugh-squint"></i></button>
+			<button class="_button" @click="showActions" v-tooltip="$ts.plugin" v-if="postFormActions.length > 0"><i class="fas fa-plug"></i></button>
+		</footer>
+		<datalist id="hashtags">
+			<option v-for="hashtag in recentHashtags" :value="hashtag" :key="hashtag"/>
+		</datalist>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import insertTextAtCursor from 'insert-text-at-cursor';
+import { length } from 'stringz';
+import { toASCII } from 'punycode/';
+import XNoteSimple from './note-simple.vue';
+import XNotePreview from './note-preview.vue';
+import * as mfm from 'mfm-js';
+import { host, url } from '@/config';
+import { erase, unique } from '@/scripts/array';
+import { extractMentions } from '@/scripts/extract-mentions';
+import * as Acct from 'misskey-js/built/acct';
+import { formatTimeString } from '@/scripts/format-time-string';
+import { Autocomplete } from '@/scripts/autocomplete';
+import { noteVisibilities } from 'misskey-js';
+import * as os from '@/os';
+import { selectFile } from '@/scripts/select-file';
+import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
+import { isMobile } from '@/scripts/is-mobile';
+import { throttle } from 'throttle-debounce';
+import MkInfo from '@/components/ui/info.vue';
+import { defaultStore } from '@/store';
+
+export default defineComponent({
+	components: {
+		XNoteSimple,
+		XNotePreview,
+		XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
+		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')),
+		MkInfo,
+	},
+
+	inject: ['modal'],
+
+	props: {
+		reply: {
+			type: Object,
+			required: false
+		},
+		renote: {
+			type: Object,
+			required: false
+		},
+		channel: {
+			type: Object,
+			required: false
+		},
+		mention: {
+			type: Object,
+			required: false
+		},
+		specified: {
+			type: Object,
+			required: false
+		},
+		initialText: {
+			type: String,
+			required: false
+		},
+		initialVisibility: {
+			type: String,
+			required: false
+		},
+		initialFiles: {
+			type: Array,
+			required: false
+		},
+		initialLocalOnly: {
+			type: Boolean,
+			required: false
+		},
+		visibleUsers: {
+			type: Array,
+			required: false,
+			default: () => []
+		},
+		initialNote: {
+			type: Object,
+			required: false
+		},
+		share: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		fixed: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+	},
+
+	emits: ['posted', 'cancel', 'esc'],
+
+	data() {
+		return {
+			posting: false,
+			text: '',
+			files: [],
+			poll: null,
+			useCw: false,
+			showPreview: false,
+			cw: null,
+			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
+			visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
+			autocomplete: null,
+			draghover: false,
+			quoteId: null,
+			hasNotSpecifiedMentions: false,
+			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
+			imeText: '',
+			typing: throttle(3000, () => {
+				if (this.channel) {
+					os.stream.send('typingOnChannel', { channel: this.channel.id });
+				}
+			}),
+			postFormActions,
+		};
+	},
+
+	computed: {
+		draftKey(): string {
+			let key = this.channel ? `channel:${this.channel.id}` : '';
+
+			if (this.renote) {
+				key += `renote:${this.renote.id}`;
+			} else if (this.reply) {
+				key += `reply:${this.reply.id}`;
+			} else {
+				key += 'note';
+			}
+
+			return key;
+		},
+
+		placeholder(): string {
+			if (this.renote) {
+				return this.$ts._postForm.quotePlaceholder;
+			} else if (this.reply) {
+				return this.$ts._postForm.replyPlaceholder;
+			} else if (this.channel) {
+				return this.$ts._postForm.channelPlaceholder;
+			} else {
+				const xs = [
+					this.$ts._postForm._placeholders.a,
+					this.$ts._postForm._placeholders.b,
+					this.$ts._postForm._placeholders.c,
+					this.$ts._postForm._placeholders.d,
+					this.$ts._postForm._placeholders.e,
+					this.$ts._postForm._placeholders.f
+				];
+				return xs[Math.floor(Math.random() * xs.length)];
+			}
+		},
+
+		submitText(): string {
+			return this.renote
+				? this.$ts.quote
+				: this.reply
+					? this.$ts.reply
+					: this.$ts.note;
+		},
+
+		textLength(): number {
+			return length((this.text + this.imeText).trim());
+		},
+
+		canPost(): boolean {
+			return !this.posting &&
+				(1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
+				(this.textLength <= this.max) &&
+				(!this.poll || this.poll.choices.length >= 2);
+		},
+
+		max(): number {
+			return this.$instance ? this.$instance.maxNoteTextLength : 1000;
+		},
+
+		withHashtags: defaultStore.makeGetterSetter('postFormWithHashtags'),
+		hashtags: defaultStore.makeGetterSetter('postFormHashtags'),
+	},
+
+	watch: {
+		text() {
+			this.checkMissingMention();
+		},
+		visibleUsers: {
+			handler() {
+				this.checkMissingMention();
+			},
+			deep: true
+		}
+	},
+
+	mounted() {
+		if (this.initialText) {
+			this.text = this.initialText;
+		}
+
+		if (this.initialVisibility) {
+			this.visibility = this.initialVisibility;
+		}
+
+		if (this.initialFiles) {
+			this.files = this.initialFiles;
+		}
+
+		if (typeof this.initialLocalOnly === 'boolean') {
+			this.localOnly = this.initialLocalOnly;
+		}
+
+		if (this.mention) {
+			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
+			this.text += ' ';
+		}
+
+		if (this.reply && this.reply.user.host != null) {
+			this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
+		}
+
+		if (this.reply && this.reply.text != null) {
+			const ast = mfm.parse(this.reply.text);
+
+			for (const x of extractMentions(ast)) {
+				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
+
+				// 自分は除外
+				if (this.$i.username == x.username && x.host == null) continue;
+				if (this.$i.username == x.username && x.host == host) continue;
+
+				// 重複は除外
+				if (this.text.indexOf(`${mention} `) != -1) continue;
+
+				this.text += `${mention} `;
+			}
+		}
+
+		if (this.channel) {
+			this.visibility = 'public';
+			this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+		}
+
+		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
+		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
+			this.visibility = this.reply.visibility;
+			if (this.reply.visibility === 'specified') {
+				os.api('users/show', {
+					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
+				}).then(users => {
+					this.visibleUsers.push(...users);
+				});
+
+				if (this.reply.userId !== this.$i.id) {
+					os.api('users/show', { userId: this.reply.userId }).then(user => {
+						this.visibleUsers.push(user);
+					});
+				}
+			}
+		}
+
+		if (this.specified) {
+			this.visibility = 'specified';
+			this.visibleUsers.push(this.specified);
+		}
+
+		// keep cw when reply
+		if (this.$store.state.keepCw && this.reply && this.reply.cw) {
+			this.useCw = true;
+			this.cw = this.reply.cw;
+		}
+
+		if (this.autofocus) {
+			this.focus();
+
+			this.$nextTick(() => {
+				this.focus();
+			});
+		}
+
+		// TODO: detach when unmount
+		new Autocomplete(this.$refs.text, this, { model: 'text' });
+		new Autocomplete(this.$refs.cw, this, { model: 'cw' });
+		new Autocomplete(this.$refs.hashtags, this, { model: 'hashtags' });
+
+		this.$nextTick(() => {
+			// 書きかけの投稿を復元
+			if (!this.share && !this.mention && !this.specified) {
+				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
+				if (draft) {
+					this.text = draft.data.text;
+					this.useCw = draft.data.useCw;
+					this.cw = draft.data.cw;
+					this.visibility = draft.data.visibility;
+					this.localOnly = draft.data.localOnly;
+					this.files = (draft.data.files || []).filter(e => e);
+					if (draft.data.poll) {
+						this.poll = draft.data.poll;
+					}
+				}
+			}
+
+			// 削除して編集
+			if (this.initialNote) {
+				const init = this.initialNote;
+				this.text = init.text ? init.text : '';
+				this.files = init.files;
+				this.cw = init.cw;
+				this.useCw = init.cw != null;
+				if (init.poll) {
+					this.poll = {
+						choices: init.poll.choices.map(x => x.text),
+						multiple: init.poll.multiple,
+						expiresAt: init.poll.expiresAt,
+						expiredAfter: init.poll.expiredAfter,
+					};
+				}
+				this.visibility = init.visibility;
+				this.localOnly = init.localOnly;
+				this.quoteId = init.renote ? init.renote.id : null;
+			}
+
+			this.$nextTick(() => this.watch());
+		});
+	},
+
+	methods: {
+		watch() {
+			this.$watch('text', () => this.saveDraft());
+			this.$watch('useCw', () => this.saveDraft());
+			this.$watch('cw', () => this.saveDraft());
+			this.$watch('poll', () => this.saveDraft());
+			this.$watch('files', () => this.saveDraft(), { deep: true });
+			this.$watch('visibility', () => this.saveDraft());
+			this.$watch('localOnly', () => this.saveDraft());
+		},
+
+		checkMissingMention() {
+			if (this.visibility === 'specified') {
+				const ast = mfm.parse(this.text);
+
+				for (const x of extractMentions(ast)) {
+					if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
+						this.hasNotSpecifiedMentions = true;
+						return;
+					}
+				}
+				this.hasNotSpecifiedMentions = false;
+			}
+		},
+
+		addMissingMention() {
+			const ast = mfm.parse(this.text);
+
+			for (const x of extractMentions(ast)) {
+				if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
+					os.api('users/show', { username: x.username, host: x.host }).then(user => {
+						this.visibleUsers.push(user);
+					});
+				}
+			}
+		},
+
+		togglePoll() {
+			if (this.poll) {
+				this.poll = null;
+			} else {
+				this.poll = {
+					choices: ['', ''],
+					multiple: false,
+					expiresAt: null,
+					expiredAfter: null,
+				};
+			}
+		},
+
+		addTag(tag: string) {
+			insertTextAtCursor(this.$refs.text, ` #${tag} `);
+		},
+
+		focus() {
+			(this.$refs.text as any).focus();
+		},
+
+		chooseFileFrom(ev) {
+			selectFile(ev.currentTarget || ev.target, this.$ts.attachFile, true).then(files => {
+				for (const file of files) {
+					this.files.push(file);
+				}
+			});
+		},
+
+		detachFile(id) {
+			this.files = this.files.filter(x => x.id != id);
+		},
+
+		updateFiles(files) {
+			this.files = files;
+		},
+
+		updateFileSensitive(file, sensitive) {
+			this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
+		},
+
+		updateFileName(file, name) {
+			this.files[this.files.findIndex(x => x.id === file.id)].name = name;
+		},
+
+		upload(file: File, name?: string) {
+			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
+				this.files.push(res);
+			});
+		},
+
+		onPollUpdate(poll) {
+			this.poll = poll;
+			this.saveDraft();
+		},
+
+		setVisibility() {
+			if (this.channel) {
+				// TODO: information dialog
+				return;
+			}
+
+			os.popup(import('./visibility-picker.vue'), {
+				currentVisibility: this.visibility,
+				currentLocalOnly: this.localOnly,
+				src: this.$refs.visibilityButton
+			}, {
+				changeVisibility: visibility => {
+					this.visibility = visibility;
+					if (this.$store.state.rememberNoteVisibility) {
+						this.$store.set('visibility', visibility);
+					}
+				},
+				changeLocalOnly: localOnly => {
+					this.localOnly = localOnly;
+					if (this.$store.state.rememberNoteVisibility) {
+						this.$store.set('localOnly', localOnly);
+					}
+				}
+			}, 'closed');
+		},
+
+		addVisibleUser() {
+			os.selectUser().then(user => {
+				this.visibleUsers.push(user);
+			});
+		},
+
+		removeVisibleUser(user) {
+			this.visibleUsers = erase(user, this.visibleUsers);
+		},
+
+		clear() {
+			this.text = '';
+			this.files = [];
+			this.poll = null;
+			this.quoteId = null;
+		},
+
+		onKeydown(e: KeyboardEvent) {
+			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
+			if (e.which === 27) this.$emit('esc');
+			this.typing();
+		},
+
+		onCompositionUpdate(e: CompositionEvent) {
+			this.imeText = e.data;
+			this.typing();
+		},
+
+		onCompositionEnd(e: CompositionEvent) {
+			this.imeText = '';
+		},
+
+		async onPaste(e: ClipboardEvent) {
+			for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
+				if (item.kind == 'file') {
+					const file = item.getAsFile();
+					const lio = file.name.lastIndexOf('.');
+					const ext = lio >= 0 ? file.name.slice(lio) : '';
+					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+					this.upload(file, formatted);
+				}
+			}
+
+			const paste = e.clipboardData.getData('text');
+
+			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
+				e.preventDefault();
+
+				os.dialog({
+					type: 'info',
+					text: this.$ts.quoteQuestion,
+					showCancelButton: true
+				}).then(({ canceled }) => {
+					if (canceled) {
+						insertTextAtCursor(this.$refs.text, paste);
+						return;
+					}
+
+					this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+				});
+			}
+		},
+
+		onDragover(e) {
+			if (!e.dataTransfer.items[0]) return;
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+			if (isFile || isDriveFile) {
+				e.preventDefault();
+				this.draghover = true;
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			}
+		},
+
+		onDragenter(e) {
+			this.draghover = true;
+		},
+
+		onDragleave(e) {
+			this.draghover = false;
+		},
+
+		onDrop(e): void {
+			this.draghover = false;
+
+			// ファイルだったら
+			if (e.dataTransfer.files.length > 0) {
+				e.preventDefault();
+				for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
+				return;
+			}
+
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.files.push(file);
+				e.preventDefault();
+			}
+			//#endregion
+		},
+
+		saveDraft() {
+			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+
+			data[this.draftKey] = {
+				updatedAt: new Date(),
+				data: {
+					text: this.text,
+					useCw: this.useCw,
+					cw: this.cw,
+					visibility: this.visibility,
+					localOnly: this.localOnly,
+					files: this.files,
+					poll: this.poll
+				}
+			};
+
+			localStorage.setItem('drafts', JSON.stringify(data));
+		},
+
+		deleteDraft() {
+			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+
+			delete data[this.draftKey];
+
+			localStorage.setItem('drafts', JSON.stringify(data));
+		},
+
+		async post() {
+			let data = {
+				text: this.text == '' ? undefined : this.text,
+				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+				replyId: this.reply ? this.reply.id : undefined,
+				renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
+				channelId: this.channel ? this.channel.id : undefined,
+				poll: this.poll,
+				cw: this.useCw ? this.cw || '' : undefined,
+				localOnly: this.localOnly,
+				visibility: this.visibility,
+				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
+				viaMobile: isMobile
+			};
+
+			if (this.withHashtags && this.hashtags && this.hashtags.trim() !== '') {
+				const hashtags = this.hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
+				data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
+			}
+
+			// plugin
+			if (notePostInterruptors.length > 0) {
+				for (const interruptor of notePostInterruptors) {
+					data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
+				}
+			}
+
+			this.posting = true;
+			os.api('notes/create', data).then(() => {
+				this.clear();
+				this.$nextTick(() => {
+					this.deleteDraft();
+					this.$emit('posted');
+					if (data.text && data.text != '') {
+						const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
+						const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
+						localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
+					}
+					this.posting = false;
+				});
+			}).catch(err => {
+				this.posting = false;
+				os.dialog({
+					type: 'error',
+					text: err.message + '\n' + (err as any).id,
+				});
+			});
+		},
+
+		cancel() {
+			this.$emit('cancel');
+		},
+
+		insertMention() {
+			os.selectUser().then(user => {
+				insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' ');
+			});
+		},
+
+		async insertEmoji(ev) {
+			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
+		},
+
+		showActions(ev) {
+			os.popupMenu(postFormActions.map(action => ({
+				text: action.title,
+				action: () => {
+					action.handler({
+						text: this.text
+					}, (key, value) => {
+						if (key === 'text') { this.text = value; }
+					});
+				}
+			})), ev.currentTarget || ev.target);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.gafaadew {
+	position: relative;
+
+	&.modal {
+		width: 100%;
+		max-width: 520px;
+	}
+
+	> header {
+		z-index: 1000;
+		height: 66px;
+
+		> .cancel {
+			padding: 0;
+			font-size: 20px;
+			width: 64px;
+			line-height: 66px;
+		}
+
+		> div {
+			position: absolute;
+			top: 0;
+			right: 0;
+
+			> .text-count {
+				opacity: 0.7;
+				line-height: 66px;
+			}
+
+			> .visibility {
+				height: 34px;
+				width: 34px;
+				margin: 0 0 0 8px;
+
+				& + .localOnly {
+					margin-left: 0 !important;
+				}
+			}
+			
+			> .local-only {
+				margin: 0 0 0 12px;
+				opacity: 0.7;
+			}
+
+			> .preview {
+				display: inline-block;
+				padding: 0;
+				margin: 0 8px 0 0;
+				font-size: 16px;
+				width: 34px;
+				height: 34px;
+				border-radius: 6px;
+
+				&:hover {
+					background: var(--X5);
+				}
+
+				&.active {
+					color: var(--accent);
+				}
+			}
+
+			> .submit {
+				margin: 16px 16px 16px 0;
+				padding: 0 12px;
+				line-height: 34px;
+				font-weight: bold;
+				vertical-align: bottom;
+				border-radius: 4px;
+				font-size: 0.9em;
+
+				&:disabled {
+					opacity: 0.7;
+				}
+
+				> i {
+					margin-left: 6px;
+				}
+			}
+		}
+	}
+
+	> .form {
+		> .preview {
+			padding: 16px;
+		}
+
+		> .with-quote {
+			margin: 0 0 8px 0;
+			color: var(--accent);
+
+			> button {
+				padding: 4px 8px;
+				color: var(--accentAlpha04);
+
+				&:hover {
+					color: var(--accentAlpha06);
+				}
+
+				&:active {
+					color: var(--accentDarken30);
+				}
+			}
+		}
+
+		> .to-specified {
+			padding: 6px 24px;
+			margin-bottom: 8px;
+			overflow: auto;
+			white-space: nowrap;
+
+			> .visibleUsers {
+				display: inline;
+				top: -1px;
+				font-size: 14px;
+
+				> button {
+					padding: 4px;
+					border-radius: 8px;
+				}
+
+				> span {
+					margin-right: 14px;
+					padding: 8px 0 8px 8px;
+					border-radius: 8px;
+					background: var(--X4);
+
+					> button {
+						padding: 4px 8px;
+					}
+				}
+			}
+		}
+
+		> .hasNotSpecifiedMentions {
+			margin: 0 20px 16px 20px;
+		}
+
+		> .cw,
+		> .hashtags,
+		> .text {
+			display: block;
+			box-sizing: border-box;
+			padding: 0 24px;
+			margin: 0;
+			width: 100%;
+			font-size: 16px;
+			border: none;
+			border-radius: 0;
+			background: transparent;
+			color: var(--fg);
+			font-family: inherit;
+
+			&:focus {
+				outline: none;
+			}
+
+			&:disabled {
+				opacity: 0.5;
+			}
+		}
+
+		> .cw {
+			z-index: 1;
+			padding-bottom: 8px;
+			border-bottom: solid 0.5px var(--divider);
+		}
+
+		> .hashtags {
+			z-index: 1;
+			padding-top: 8px;
+			padding-bottom: 8px;
+			border-top: solid 0.5px var(--divider);
+		}
+
+		> .text {
+			max-width: 100%;
+			min-width: 100%;
+			min-height: 90px;
+
+			&.withCw {
+				padding-top: 8px;
+			}
+		}
+
+		> footer {
+			padding: 0 16px 16px 16px;
+
+			> button {
+				display: inline-block;
+				padding: 0;
+				margin: 0;
+				font-size: 16px;
+				width: 48px;
+				height: 48px;
+				border-radius: 6px;
+
+				&:hover {
+					background: var(--X5);
+				}
+
+				&.active {
+					color: var(--accent);
+				}
+			}
+		}
+	}
+
+	&.max-width_500px {
+		> header {
+			height: 50px;
+
+			> .cancel {
+				width: 50px;
+				line-height: 50px;
+			}
+
+			> div {
+				> .text-count {
+					line-height: 50px;
+				}
+
+				> .submit {
+					margin: 8px;
+				}
+			}
+		}
+
+		> .form {
+			> .to-specified {
+				padding: 6px 16px;
+			}
+
+			> .cw,
+			> .hashtags,
+			> .text {
+				padding: 0 16px;
+			}
+
+			> .text {
+				min-height: 80px;
+			}
+
+			> footer {
+				padding: 0 8px 8px 8px;
+			}
+		}
+	}
+
+	&.max-width_310px {
+		> .form {
+			> footer {
+				> button {
+					font-size: 14px;
+					width: 44px;
+				height: 44px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/queue-chart.vue b/packages/client/src/components/queue-chart.vue
new file mode 100644
index 0000000000..7e0ed58cbd
--- /dev/null
+++ b/packages/client/src/components/queue-chart.vue
@@ -0,0 +1,232 @@
+<template>
+<canvas ref="chartEl"></canvas>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+import {
+	Chart,
+	ArcElement,
+	LineElement,
+	BarElement,
+	PointElement,
+	BarController,
+	LineController,
+	CategoryScale,
+	LinearScale,
+	TimeScale,
+	Legend,
+	Title,
+	Tooltip,
+	SubTitle,
+	Filler,
+} from 'chart.js';
+import number from '@/filters/number';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+
+Chart.register(
+	ArcElement,
+	LineElement,
+	BarElement,
+	PointElement,
+	BarController,
+	LineController,
+	CategoryScale,
+	LinearScale,
+	TimeScale,
+	Legend,
+	Title,
+	Tooltip,
+	SubTitle,
+	Filler,
+);
+
+const alpha = (hex, a) => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+export default defineComponent({
+	props: {
+		domain: {
+			type: String,
+			required: true,
+		},
+		connection: {
+			required: true,
+		},
+	},
+
+	setup(props) {
+		const chartEl = ref<HTMLCanvasElement>(null);
+
+		const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+
+		// フォントカラー
+		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+		onMounted(() => {
+			const chartInstance = new Chart(chartEl.value, {
+				type: 'line',
+				data: {
+					labels: [],
+					datasets: [{
+						label: 'Process',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderJoinStyle: 'round',
+						borderColor: '#00E396',
+						backgroundColor: alpha('#00E396', 0.1),
+						data: []
+					}, {
+						label: 'Active',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderJoinStyle: 'round',
+						borderColor: '#00BCD4',
+						backgroundColor: alpha('#00BCD4', 0.1),
+						data: []
+					}, {
+						label: 'Waiting',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderJoinStyle: 'round',
+						borderColor: '#FFB300',
+						backgroundColor: alpha('#FFB300', 0.1),
+						yAxisID: 'y2',
+						data: []
+					}, {
+						label: 'Delayed',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderJoinStyle: 'round',
+						borderColor: '#E53935',
+						borderDash: [5, 5],
+						fill: false,
+						yAxisID: 'y2',
+						data: []
+					}],
+				},
+				options: {
+					aspectRatio: 2.5,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 8,
+						},
+					},
+					scales: {
+						x: {
+							grid: {
+								display: true,
+								color: gridColor,
+								borderColor: 'rgb(0, 0, 0, 0)',
+							},
+							ticks: {
+								display: false,
+								maxTicksLimit: 10
+							},
+						},
+						y: {
+							min: 0,
+							stack: 'queue',
+							stackWeight: 2,
+							grid: {
+								color: gridColor,
+								borderColor: 'rgb(0, 0, 0, 0)',
+							},
+						},
+						y2: {
+							min: 0,
+							offset: true,
+							stack: 'queue',
+							stackWeight: 1,
+							grid: {
+								color: gridColor,
+								borderColor: 'rgb(0, 0, 0, 0)',
+							},
+						},
+					},
+					interaction: {
+						intersect: false,
+					},
+					plugins: {
+						legend: {
+							position: 'bottom',
+							labels: {
+								boxWidth: 16,
+							},
+						},
+						tooltip: {
+							mode: 'index',
+							animation: {
+								duration: 0,
+							},
+						},
+					},
+				},
+			});
+
+			const onStats = (stats) => {
+				chartInstance.data.labels.push('');
+				chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
+				chartInstance.data.datasets[1].data.push(stats[props.domain].active);
+				chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
+				chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+				if (chartInstance.data.datasets[0].data.length > 200) {
+					chartInstance.data.labels.shift();
+					chartInstance.data.datasets[0].data.shift();
+					chartInstance.data.datasets[1].data.shift();
+					chartInstance.data.datasets[2].data.shift();
+					chartInstance.data.datasets[3].data.shift();
+				}
+				chartInstance.update();
+			};
+
+			const onStatsLog = (statsLog) => {
+				for (const stats of [...statsLog].reverse()) {
+					chartInstance.data.labels.push('');
+					chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
+					chartInstance.data.datasets[1].data.push(stats[props.domain].active);
+					chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
+					chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+					if (chartInstance.data.datasets[0].data.length > 200) {
+						chartInstance.data.labels.shift();
+						chartInstance.data.datasets[0].data.shift();
+						chartInstance.data.datasets[1].data.shift();
+						chartInstance.data.datasets[2].data.shift();
+						chartInstance.data.datasets[3].data.shift();
+					}
+				}
+				chartInstance.update();
+			};
+
+			props.connection.on('stats', onStats);
+			props.connection.on('statsLog', onStatsLog);
+
+			onUnmounted(() => {
+				props.connection.off('stats', onStats);
+				props.connection.off('statsLog', onStatsLog);
+			});
+		});
+
+		return {
+			chartEl,
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/components/reaction-icon.vue b/packages/client/src/components/reaction-icon.vue
new file mode 100644
index 0000000000..c0ec955e32
--- /dev/null
+++ b/packages/client/src/components/reaction-icon.vue
@@ -0,0 +1,25 @@
+<template>
+<MkEmoji :emoji="reaction" :custom-emojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		reaction: {
+			type: String,
+			required: true
+		},
+		customEmojis: {
+			required: false,
+			default: () => []
+		},
+		noStyle: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+});
+</script>
diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue
new file mode 100644
index 0000000000..93143cbe81
--- /dev/null
+++ b/packages/client/src/components/reaction-tooltip.vue
@@ -0,0 +1,51 @@
+<template>
+<MkTooltip :source="source" ref="tooltip" @closed="$emit('closed')" :max-width="340">
+	<div class="beeadbfb">
+		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
+		<div class="name">{{ reaction.replace('@.', '') }}</div>
+	</div>
+</MkTooltip>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkTooltip from './ui/tooltip.vue';
+import XReactionIcon from './reaction-icon.vue';
+
+export default defineComponent({
+	components: {
+		MkTooltip,
+		XReactionIcon,
+	},
+	props: {
+		reaction: {
+			type: String,
+			required: true,
+		},
+		emojis: {
+			type: Array,
+			required: true,
+		},
+		source: {
+			required: true,
+		}
+	},
+	emits: ['closed'],
+})
+</script>
+
+<style lang="scss" scoped>
+.beeadbfb {
+	text-align: center;
+
+	> .icon {
+		display: block;
+		width: 60px;
+		margin: 0 auto;
+	}
+
+	> .name {
+		font-size: 0.9em;
+	}
+}
+</style>
diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue
new file mode 100644
index 0000000000..7c49bd1d9c
--- /dev/null
+++ b/packages/client/src/components/reactions-viewer.details.vue
@@ -0,0 +1,91 @@
+<template>
+<MkTooltip :source="source" ref="tooltip" @closed="$emit('closed')" :max-width="340">
+	<div class="bqxuuuey">
+		<div class="reaction">
+			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
+			<div class="name">{{ reaction.replace('@.', '') }}</div>
+		</div>
+		<div class="users">
+			<template v-if="users.length <= 10">
+				<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
+					<MkAvatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
+					<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/>
+				</b>
+			</template>
+			<template v-if="10 < users.length">
+				<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
+					<MkAvatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
+					<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/>
+				</b>
+				<span slot="omitted">+{{ count - 10 }}</span>
+			</template>
+		</div>
+	</div>
+</MkTooltip>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkTooltip from './ui/tooltip.vue';
+import XReactionIcon from './reaction-icon.vue';
+
+export default defineComponent({
+	components: {
+		MkTooltip,
+		XReactionIcon
+	},
+	props: {
+		reaction: {
+			type: String,
+			required: true,
+		},
+		users: {
+			type: Array,
+			required: true,
+		},
+		count: {
+			type: Number,
+			required: true,
+		},
+		emojis: {
+			type: Array,
+			required: true,
+		},
+		source: {
+			required: true,
+		}
+	},
+	emits: ['closed'],
+})
+</script>
+
+<style lang="scss" scoped>
+.bqxuuuey {
+	display: flex;
+
+	> .reaction {
+		flex: 1;
+		max-width: 100px;
+		text-align: center;
+
+		> .icon {
+			display: block;
+			width: 60px;
+			margin: 0 auto;
+		}
+
+		> .name {
+			font-size: 0.9em;
+		}
+	}
+
+	> .users {
+		flex: 1;
+		min-width: 0;
+		font-size: 0.9em;
+		border-left: solid 0.5px var(--divider);
+		padding-left: 10px;
+    margin-left: 10px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/reactions-viewer.reaction.vue b/packages/client/src/components/reactions-viewer.reaction.vue
new file mode 100644
index 0000000000..47a3bb9720
--- /dev/null
+++ b/packages/client/src/components/reactions-viewer.reaction.vue
@@ -0,0 +1,183 @@
+<template>
+<button
+	class="hkzvhatu _button"
+	:class="{ reacted: note.myReaction == reaction, canToggle }"
+	@click="toggleReaction(reaction)"
+	v-if="count > 0"
+	@touchstart.passive="onMouseover"
+	@mouseover="onMouseover"
+	@mouseleave="onMouseleave"
+	@touchend="onMouseleave"
+	ref="reaction"
+	v-particle="canToggle"
+>
+	<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
+	<span>{{ count }}</span>
+</button>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+import XDetails from '@/components/reactions-viewer.details.vue';
+import XReactionIcon from '@/components/reaction-icon.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XReactionIcon
+	},
+	props: {
+		reaction: {
+			type: String,
+			required: true,
+		},
+		count: {
+			type: Number,
+			required: true,
+		},
+		isInitial: {
+			type: Boolean,
+			required: true,
+		},
+		note: {
+			type: Object,
+			required: true,
+		},
+	},
+	data() {
+		return {
+			close: null,
+			detailsTimeoutId: null,
+			isHovering: false
+		};
+	},
+	computed: {
+		canToggle(): boolean {
+			return !this.reaction.match(/@\w/) && this.$i;
+		},
+	},
+	watch: {
+		count(newCount, oldCount) {
+			if (oldCount < newCount) this.anime();
+			if (this.close != null) this.openDetails();
+		},
+	},
+	mounted() {
+		if (!this.isInitial) this.anime();
+	},
+	methods: {
+		toggleReaction() {
+			if (!this.canToggle) return;
+
+			const oldReaction = this.note.myReaction;
+			if (oldReaction) {
+				os.api('notes/reactions/delete', {
+					noteId: this.note.id
+				}).then(() => {
+					if (oldReaction !== this.reaction) {
+						os.api('notes/reactions/create', {
+							noteId: this.note.id,
+							reaction: this.reaction
+						});
+					}
+				});
+			} else {
+				os.api('notes/reactions/create', {
+					noteId: this.note.id,
+					reaction: this.reaction
+				});
+			}
+		},
+		onMouseover() {
+			if (this.isHovering) return;
+			this.isHovering = true;
+			this.detailsTimeoutId = setTimeout(this.openDetails, 300);
+		},
+		onMouseleave() {
+			if (!this.isHovering) return;
+			this.isHovering = false;
+			clearTimeout(this.detailsTimeoutId);
+			this.closeDetails();
+		},
+		openDetails() {
+			os.api('notes/reactions', {
+				noteId: this.note.id,
+				type: this.reaction,
+				limit: 11
+			}).then((reactions: any[]) => {
+				const users = reactions
+					.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
+					.map(x => x.user);
+
+				this.closeDetails();
+				if (!this.isHovering) return;
+
+				const showing = ref(true);
+				os.popup(XDetails, {
+					showing,
+					reaction: this.reaction,
+					emojis: this.note.emojis,
+					users,
+					count: this.count,
+					source: this.$refs.reaction
+				}, {}, 'closed');
+
+				this.close = () => {
+					showing.value = false;
+				};
+			});
+		},
+		closeDetails() {
+			if (this.close != null) {
+				this.close();
+				this.close = null;
+			}
+		},
+		anime() {
+			if (document.hidden) return;
+
+			// TODO
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hkzvhatu {
+	display: inline-block;
+	height: 32px;
+	margin: 2px;
+	padding: 0 6px;
+	border-radius: 4px;
+
+	&.canToggle {
+		background: rgba(0, 0, 0, 0.05);
+
+		&:hover {
+			background: rgba(0, 0, 0, 0.1);
+		}
+	}
+
+	&:not(.canToggle) {
+		cursor: default;
+	}
+
+	&.reacted {
+		background: var(--accent);
+
+		&:hover {
+			background: var(--accent);
+		}
+
+		> span {
+			color: var(--fgOnAccent);
+		}
+	}
+
+	> span {
+		font-size: 0.9em;
+		line-height: 32px;
+		margin: 0 0 0 4px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/reactions-viewer.vue b/packages/client/src/components/reactions-viewer.vue
new file mode 100644
index 0000000000..94a0318734
--- /dev/null
+++ b/packages/client/src/components/reactions-viewer.vue
@@ -0,0 +1,48 @@
+<template>
+<div class="tdflqwzn" :class="{ isMe }">
+	<XReaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XReaction from './reactions-viewer.reaction.vue';
+
+export default defineComponent({
+	components: {
+		XReaction
+	},
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+	},
+	data() {
+		return {
+			initialReactions: new Set(Object.keys(this.note.reactions))
+		};
+	},
+	computed: {
+		isMe(): boolean {
+			return this.$i && this.$i.id === this.note.userId;
+		},
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.tdflqwzn {
+	margin: 4px -2px 0 -2px;
+
+	&:empty {
+		display: none;
+	}
+
+	&.isMe {
+		> span {
+			cursor: default !important;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/remote-caution.vue b/packages/client/src/components/remote-caution.vue
new file mode 100644
index 0000000000..c496ea8f48
--- /dev/null
+++ b/packages/client/src/components/remote-caution.vue
@@ -0,0 +1,35 @@
+<template>
+<div class="jmgmzlwq _block"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ $ts.remoteUserCaution }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		href: {
+			type: String,
+			required: true
+		},
+	},
+	data() {
+		return {
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.jmgmzlwq {
+	font-size: 0.8em;
+	padding: 16px;
+	background: var(--infoWarnBg);
+	color: var(--infoWarnFg);
+
+	> a {
+		margin-left: 4px;
+		color: var(--accent);
+	}
+}
+</style>
diff --git a/packages/client/src/components/sample.vue b/packages/client/src/components/sample.vue
new file mode 100644
index 0000000000..ba6c682c44
--- /dev/null
+++ b/packages/client/src/components/sample.vue
@@ -0,0 +1,116 @@
+<template>
+<div class="_card">
+	<div class="_content">
+		<MkInput v-model="text">
+			<template #label>Text</template>
+		</MkInput>
+		<MkSwitch v-model="flag">
+			<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
+		</MkSwitch>
+		<div style="margin: 32px 0;">
+			<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
+			<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
+			<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
+		</div>
+		<MkButton inline>This is</MkButton>
+		<MkButton inline primary>the button</MkButton>
+	</div>
+	<div class="_content" style="pointer-events: none;">
+		<Mfm :text="mfm"/>
+	</div>
+	<div class="_content">
+		<MkButton inline primary @click="openMenu">Open menu</MkButton>
+		<MkButton inline primary @click="openDialog">Open dialog</MkButton>
+		<MkButton inline primary @click="openForm">Open form</MkButton>
+		<MkButton inline primary @click="openDrive">Open drive</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkRadio from '@/components/form/radio.vue';
+import * as os from '@/os';
+import * as config from '@/config';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSwitch,
+		MkTextarea,
+		MkRadio,
+	},
+
+	data() {
+		return {
+			text: '',
+			flag: true,
+			radio: 'misskey',
+			mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
+		}
+	},
+
+	methods: {
+		async openDialog() {
+			os.dialog({
+				type: 'warning',
+				title: 'Oh my Aichan',
+				text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+			});
+		},
+
+		async openForm() {
+			os.form('Example form', {
+				foo: {
+					type: 'boolean',
+					default: true,
+					label: 'This is a boolean property'
+				},
+				bar: {
+					type: 'number',
+					default: 300,
+					label: 'This is a number property'
+				},
+				baz: {
+					type: 'string',
+					default: 'Misskey makes you happy.',
+					label: 'This is a string property'
+				},
+			});
+		},
+
+		async openDrive() {
+			os.selectDriveFile();
+		},
+
+		async selectUser() {
+			os.selectUser();
+		},
+
+		async openMenu(ev) {
+			os.popupMenu([{
+				type: 'label',
+				text: 'Fruits'
+			}, {
+				text: 'Create some apples',
+				action: () => {},
+			}, {
+				text: 'Read some oranges',
+				action: () => {},
+			}, {
+				text: 'Update some melons',
+				action: () => {},
+			}, null, {
+				text: 'Delete some bananas',
+				danger: true,
+				action: () => {},
+			}], ev.currentTarget || ev.target);
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/components/signin-dialog.vue b/packages/client/src/components/signin-dialog.vue
new file mode 100644
index 0000000000..2edd10f539
--- /dev/null
+++ b/packages/client/src/components/signin-dialog.vue
@@ -0,0 +1,42 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="370"
+	:height="400"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+>
+	<template #header>{{ $ts.login }}</template>
+
+	<MkSignin :auto-set="autoSet" @login="onLogin"/>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import MkSignin from './signin.vue';
+
+export default defineComponent({
+	components: {
+		MkSignin,
+		XModalWindow,
+	},
+
+	props: {
+		autoSet: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	emits: ['done', 'closed'],
+
+	methods: {
+		onLogin(res) {
+			this.$emit('done', res);
+			this.$refs.dialog.close();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/signin.vue b/packages/client/src/components/signin.vue
new file mode 100644
index 0000000000..68bbd5368e
--- /dev/null
+++ b/packages/client/src/components/signin.vue
@@ -0,0 +1,240 @@
+<template>
+<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
+	<div class="auth _section _formRoot">
+		<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
+		<div class="normal-signin" v-if="!totpLogin">
+			<MkInput class="_formBlock" v-model="username" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @update:modelValue="onUsernameChange" data-cy-signin-username>
+				<template #prefix>@</template>
+				<template #suffix>@{{ host }}</template>
+			</MkInput>
+			<MkInput class="_formBlock" v-model="password" :placeholder="$ts.password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required data-cy-signin-password>
+				<template #prefix><i class="fas fa-lock"></i></template>
+				<template #caption><button class="_textButton" @click="resetPassword" type="button">{{ $ts.forgotPassword }}</button></template>
+			</MkInput>
+			<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
+		</div>
+		<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
+			<div v-if="user && user.securityKeys" class="twofa-group tap-group">
+				<p>{{ $ts.tapSecurityKey }}</p>
+				<MkButton @click="queryKey" v-if="!queryingKey">
+					{{ $ts.retry }}
+				</MkButton>
+			</div>
+			<div class="or-hr" v-if="user && user.securityKeys">
+				<p class="or-msg">{{ $ts.or }}</p>
+			</div>
+			<div class="twofa-group totp-group">
+				<p style="margin-bottom:0;">{{ $ts.twoStepAuthentication }}</p>
+				<MkInput v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
+					<template #label>{{ $ts.password }}</template>
+					<template #prefix><i class="fas fa-lock"></i></template>
+				</MkInput>
+				<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
+					<template #label>{{ $ts.token }}</template>
+					<template #prefix><i class="fas fa-gavel"></i></template>
+				</MkInput>
+				<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
+			</div>
+		</div>
+	</div>
+	<div class="social _section">
+		<a class="_borderButton _gap" v-if="meta && meta.enableTwitterIntegration" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
+		<a class="_borderButton _gap" v-if="meta && meta.enableGithubIntegration" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
+		<a class="_borderButton _gap" v-if="meta && meta.enableDiscordIntegration" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
+	</div>
+</form>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { toUnicode } from 'punycode/';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import { apiUrl, host } from '@/config';
+import { byteify, hexify } from '@/scripts/2fa';
+import * as os from '@/os';
+import { login } from '@/account';
+import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+	},
+
+	props: {
+		withAvatar: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+		autoSet: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	emits: ['login'],
+
+	data() {
+		return {
+			signing: false,
+			user: null,
+			username: '',
+			password: '',
+			token: '',
+			apiUrl,
+			host: toUnicode(host),
+			totpLogin: false,
+			credential: null,
+			challengeData: null,
+			queryingKey: false,
+		};
+	},
+
+	computed: {
+		meta() {
+			return this.$instance;
+		},
+	},
+
+	methods: {
+		onUsernameChange() {
+			os.api('users/show', {
+				username: this.username
+			}).then(user => {
+				this.user = user;
+			}, () => {
+				this.user = null;
+			});
+		},
+
+		onLogin(res) {
+			if (this.autoSet) {
+				return login(res.i);
+			} else {
+				return;
+			}
+		},
+
+		queryKey() {
+			this.queryingKey = true;
+			return navigator.credentials.get({
+				publicKey: {
+					challenge: byteify(this.challengeData.challenge, 'base64'),
+					allowCredentials: this.challengeData.securityKeys.map(key => ({
+						id: byteify(key.id, 'hex'),
+						type: 'public-key',
+						transports: ['usb', 'nfc', 'ble', 'internal']
+					})),
+					timeout: 60 * 1000
+				}
+			}).catch(() => {
+				this.queryingKey = false;
+				return Promise.reject(null);
+			}).then(credential => {
+				this.queryingKey = false;
+				this.signing = true;
+				return os.api('signin', {
+					username: this.username,
+					password: this.password,
+					signature: hexify(credential.response.signature),
+					authenticatorData: hexify(credential.response.authenticatorData),
+					clientDataJSON: hexify(credential.response.clientDataJSON),
+					credentialId: credential.id,
+					challengeId: this.challengeData.challengeId
+				});
+			}).then(res => {
+				this.$emit('login', res);
+				return this.onLogin(res);
+			}).catch(err => {
+				if (err === null) return;
+				os.dialog({
+					type: 'error',
+					text: this.$ts.signinFailed
+				});
+				this.signing = false;
+			});
+		},
+
+		onSubmit() {
+			this.signing = true;
+			if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
+				if (window.PublicKeyCredential && this.user.securityKeys) {
+					os.api('signin', {
+						username: this.username,
+						password: this.password
+					}).then(res => {
+						this.totpLogin = true;
+						this.signing = false;
+						this.challengeData = res;
+						return this.queryKey();
+					}).catch(this.loginFailed);
+				} else {
+					this.totpLogin = true;
+					this.signing = false;
+				}
+			} else {
+				os.api('signin', {
+					username: this.username,
+					password: this.password,
+					token: this.user && this.user.twoFactorEnabled ? this.token : undefined
+				}).then(res => {
+					this.$emit('login', res);
+					this.onLogin(res);
+				}).catch(this.loginFailed);
+			}
+		},
+
+		loginFailed(err) {
+			switch (err.id) {
+				case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
+					os.dialog({
+						type: 'error',
+						title: this.$ts.loginFailed,
+						text: this.$ts.noSuchUser
+					});
+					break;
+				}
+				case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
+					showSuspendedDialog();
+					break;
+				}
+				default: {
+					os.dialog({
+						type: 'error',
+						title: this.$ts.loginFailed,
+						text: JSON.stringify(err)
+					});
+				}
+			}
+
+			this.challengeData = null;
+			this.totpLogin = false;
+			this.signing = false;
+		},
+
+		resetPassword() {
+			os.popup(import('@/components/forgot-password.vue'), {}, {
+			}, 'closed');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.eppvobhk {
+	> .auth {
+		> .avatar {
+			margin: 0 auto 0 auto;
+			width: 64px;
+			height: 64px;
+			background: #ddd;
+			background-position: center;
+			background-size: cover;
+			border-radius: 100%;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/signup-dialog.vue b/packages/client/src/components/signup-dialog.vue
new file mode 100644
index 0000000000..30fe3bf7d3
--- /dev/null
+++ b/packages/client/src/components/signup-dialog.vue
@@ -0,0 +1,50 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="366"
+	:height="500"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+>
+	<template #header>{{ $ts.signup }}</template>
+
+	<div class="_monolithic_">
+		<div class="_section">
+			<XSignup :auto-set="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
+		</div>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import XSignup from './signup.vue';
+
+export default defineComponent({
+	components: {
+		XSignup,
+		XModalWindow,
+	},
+
+	props: {
+		autoSet: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	emits: ['done', 'closed'],
+
+	methods: {
+		onSignup(res) {
+			this.$emit('done', res);
+			this.$refs.dialog.close();
+		},
+
+		onSignupEmailPending() {
+			this.$refs.dialog.close();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/signup.vue b/packages/client/src/components/signup.vue
new file mode 100644
index 0000000000..621f30486f
--- /dev/null
+++ b/packages/client/src/components/signup.vue
@@ -0,0 +1,268 @@
+<template>
+<form class="qlvuhzng _formRoot" @submit.prevent="onSubmit" :autocomplete="Math.random()">
+	<template v-if="meta">
+		<MkInput class="_formBlock" v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
+			<template #label>{{ $ts.invitationCode }}</template>
+			<template #prefix><i class="fas fa-key"></i></template>
+		</MkInput>
+		<MkInput class="_formBlock" v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
+			<template #label>{{ $ts.username }} <div class="_button _help" v-tooltip:dialog="$ts.usernameInfo"><i class="far fa-question-circle"></i></div></template>
+			<template #prefix>@</template>
+			<template #suffix>@{{ host }}</template>
+			<template #caption>
+				<span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
+				<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
+				<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
+				<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
+				<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
+				<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
+				<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
+			</template>
+		</MkInput>
+		<MkInput v-if="meta.emailRequiredForSignup" class="_formBlock" v-model="email" :debounce="true" type="email" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeEmail" data-cy-signup-email>
+			<template #label>{{ $ts.emailAddress }} <div class="_button _help" v-tooltip:dialog="$ts._signup.emailAddressInfo"><i class="far fa-question-circle"></i></div></template>
+			<template #prefix><i class="fas fa-envelope"></i></template>
+			<template #caption>
+				<span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
+				<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
+				<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
+				<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
+				<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
+				<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
+				<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
+				<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
+				<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
+			</template>
+		</MkInput>
+		<MkInput class="_formBlock" v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword" data-cy-signup-password>
+			<template #label>{{ $ts.password }}</template>
+			<template #prefix><i class="fas fa-lock"></i></template>
+			<template #caption>
+				<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
+				<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
+				<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
+			</template>
+		</MkInput>
+		<MkInput class="_formBlock" v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePasswordRetype" data-cy-signup-password-retype>
+			<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
+			<template #prefix><i class="fas fa-lock"></i></template>
+			<template #caption>
+				<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
+				<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
+			</template>
+		</MkInput>
+		<label v-if="meta.tosUrl" class="_formBlock tou">
+			<input type="checkbox" v-model="ToSAgreement">
+			<I18n :src="$ts.agreeTo">
+				<template #0>
+					<a :href="meta.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a>
+				</template>
+			</I18n>
+		</label>
+		<captcha v-if="meta.enableHcaptcha" class="_formBlock captcha" provider="hcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
+		<captcha v-if="meta.enableRecaptcha" class="_formBlock captcha" provider="recaptcha" ref="recaptcha" v-model="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
+		<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
+	</template>
+</form>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+const getPasswordStrength = require('syuilo-password-strength');
+import { toUnicode } from 'punycode/';
+import { host, url } from '@/config';
+import MkButton from './ui/button.vue';
+import MkInput from './form/input.vue';
+import MkSwitch from './form/switch.vue';
+import * as os from '@/os';
+import { login } from '@/account';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSwitch,
+		captcha: defineAsyncComponent(() => import('./captcha.vue')),
+	},
+
+	props: {
+		autoSet: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	emits: ['signup'],
+
+	data() {
+		return {
+			host: toUnicode(host),
+			username: '',
+			password: '',
+			retypedPassword: '',
+			invitationCode: '',
+			email: '',
+			url,
+			usernameState: null,
+			emailState: null,
+			passwordStrength: '',
+			passwordRetypeState: null,
+			submitting: false,
+			ToSAgreement: false,
+			hCaptchaResponse: null,
+			reCaptchaResponse: null,
+		}
+	},
+
+	computed: {
+		meta() {
+			return this.$instance;
+		},
+
+		shouldDisableSubmitting(): boolean {
+			return this.submitting ||
+				this.meta.tosUrl && !this.ToSAgreement ||
+				this.meta.enableHcaptcha && !this.hCaptchaResponse ||
+				this.meta.enableRecaptcha && !this.reCaptchaResponse ||
+				this.passwordRetypeState == 'not-match';
+		},
+
+		shouldShowProfileUrl(): boolean {
+			return (this.username != '' &&
+				this.usernameState != 'invalid-format' &&
+				this.usernameState != 'min-range' &&
+				this.usernameState != 'max-range');
+		}
+	},
+
+	methods: {
+		onChangeUsername() {
+			if (this.username == '') {
+				this.usernameState = null;
+				return;
+			}
+
+			const err =
+				!this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
+				this.username.length < 1 ? 'min-range' :
+				this.username.length > 20 ? 'max-range' :
+				null;
+
+			if (err) {
+				this.usernameState = err;
+				return;
+			}
+
+			this.usernameState = 'wait';
+
+			os.api('username/available', {
+				username: this.username
+			}).then(result => {
+				this.usernameState = result.available ? 'ok' : 'unavailable';
+			}).catch(err => {
+				this.usernameState = 'error';
+			});
+		},
+
+		onChangeEmail() {
+			if (this.email == '') {
+				this.emailState = null;
+				return;
+			}
+
+			this.emailState = 'wait';
+
+			os.api('email-address/available', {
+				emailAddress: this.email
+			}).then(result => {
+				this.emailState = result.available ? 'ok' :
+					result.reason === 'used' ? 'unavailable:used' :
+					result.reason === 'format' ? 'unavailable:format' :
+					result.reason === 'disposable' ? 'unavailable:disposable' :
+					result.reason === 'mx' ? 'unavailable:mx' :
+					result.reason === 'smtp' ? 'unavailable:smtp' :
+					'unavailable';
+			}).catch(err => {
+				this.emailState = 'error';
+			});
+		},
+
+		onChangePassword() {
+			if (this.password == '') {
+				this.passwordStrength = '';
+				return;
+			}
+
+			const strength = getPasswordStrength(this.password);
+			this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
+		},
+
+		onChangePasswordRetype() {
+			if (this.retypedPassword == '') {
+				this.passwordRetypeState = null;
+				return;
+			}
+
+			this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
+		},
+
+		onSubmit() {
+			if (this.submitting) return;
+			this.submitting = true;
+
+			os.api('signup', {
+				username: this.username,
+				password: this.password,
+				emailAddress: this.email,
+				invitationCode: this.invitationCode,
+				'hcaptcha-response': this.hCaptchaResponse,
+				'g-recaptcha-response': this.reCaptchaResponse,
+			}).then(() => {
+				if (this.meta.emailRequiredForSignup) {
+					os.dialog({
+						type: 'success',
+						title: this.$ts._signup.almostThere,
+						text: this.$t('_signup.emailSent', { email: this.email }),
+					});
+					this.$emit('signupEmailPending');
+				} else {
+					os.api('signin', {
+						username: this.username,
+						password: this.password
+					}).then(res => {
+						this.$emit('signup', res);
+
+						if (this.autoSet) {
+							login(res.i);
+						}
+					});
+				}
+			}).catch(() => {
+				this.submitting = false;
+				this.$refs.hcaptcha?.reset?.();
+				this.$refs.recaptcha?.reset?.();
+
+				os.dialog({
+					type: 'error',
+					text: this.$ts.somethingHappened
+				});
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qlvuhzng {
+	.captcha {
+		margin: 16px 0;
+	}
+
+	> .tou {
+		display: block;
+		margin: 16px 0;
+		cursor: pointer;
+	}
+}
+</style>
diff --git a/packages/client/src/components/sparkle.vue b/packages/client/src/components/sparkle.vue
new file mode 100644
index 0000000000..3aaf03995d
--- /dev/null
+++ b/packages/client/src/components/sparkle.vue
@@ -0,0 +1,179 @@
+<template>
+<span class="mk-sparkle">
+	<span ref="content">
+		<slot></slot>
+	</span>
+	<canvas ref="canvas"></canvas>
+</span>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+const sprite = new Image();
+sprite.src = '/client-assets/sparkle-spritesheet.png';
+
+export default defineComponent({
+	props: {
+		count: {
+			type: Number,
+			required: true,
+		},
+		speed: {
+			type: Number,
+			required: true,
+		},
+	},
+	data() {
+		return {
+			sprites: [0,6,13,20],
+			particles: [],
+			anim: null,
+			ctx: null,
+		};
+	},
+	methods: {
+		createSparkles(w, h, count) {
+			var holder = [];
+
+			for (var i = 0; i < count; i++) {
+
+				const color = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
+
+				holder[i] = {
+					position: {
+						x: Math.floor(Math.random() * w),
+						y: Math.floor(Math.random() * h)
+					},
+					style: this.sprites[ Math.floor(Math.random() * 4) ],
+					delta: {
+						x: Math.floor(Math.random() * 1000) - 500,
+						y: Math.floor(Math.random() * 1000) - 500
+					},
+					color: color,
+					opacity: Math.random(),
+				};
+
+			}
+
+			return holder;
+		},
+		draw(time) {
+			this.ctx.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
+			this.ctx.beginPath();
+
+			const particleSize = Math.floor(this.fontSize / 2);
+			this.particles.forEach((particle) => {
+				var modulus = Math.floor(Math.random()*7);
+
+				if (Math.floor(time) % modulus === 0) {
+					particle.style = this.sprites[ Math.floor(Math.random()*4) ];
+				}
+
+				this.ctx.save();
+				this.ctx.globalAlpha = particle.opacity;
+				this.ctx.drawImage(sprite, particle.style, 0, 7, 7, particle.position.x, particle.position.y, particleSize, particleSize);
+
+				this.ctx.globalCompositeOperation = "source-atop";
+				this.ctx.globalAlpha = 0.5;
+				this.ctx.fillStyle = particle.color;
+				this.ctx.fillRect(particle.position.x, particle.position.y, particleSize, particleSize);
+
+				this.ctx.restore();
+			});
+			this.ctx.stroke();
+		},
+		tick() {
+			this.anim = window.requestAnimationFrame((time) => {
+				if (!this.$refs.canvas) {
+					return;
+				}
+				this.particles.forEach((particle) => {
+					if (!particle) {
+						return;
+					}
+					var randX = Math.random() > Math.random() * 2;
+					var randY = Math.random() > Math.random() * 3;
+
+					if (randX) {
+						particle.position.x += (particle.delta.x * this.speed) / 1500;
+					}
+
+					if (!randY) {
+						particle.position.y -= (particle.delta.y * this.speed) / 800;
+					}
+
+					if( particle.position.x > this.$refs.canvas.width ) {
+						particle.position.x = -7;
+					} else if (particle.position.x < -7) {
+						particle.position.x = this.$refs.canvas.width;
+					}
+
+					if (particle.position.y > this.$refs.canvas.height) {
+						particle.position.y = -7;
+						particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width);
+					} else if (particle.position.y < -7) {
+						particle.position.y = this.$refs.canvas.height;
+						particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width);
+					}
+
+					particle.opacity -= 0.005;
+
+					if (particle.opacity <= 0) {
+						particle.opacity = 1;
+					}
+				});
+
+				this.draw(time);
+
+				this.tick();
+			});
+		},
+		resize() {
+			if (this.$refs.content) {
+				const contentRect = this.$refs.content.getBoundingClientRect();
+				this.fontSize = parseFloat(getComputedStyle(this.$refs.content).fontSize);
+				const padding = this.fontSize * 0.2;
+
+				this.$refs.canvas.width = parseInt(contentRect.width + padding);
+				this.$refs.canvas.height = parseInt(contentRect.height + padding);
+
+				this.particles = this.createSparkles(this.$refs.canvas.width, this.$refs.canvas.height, this.count);
+			}
+		},
+	},
+	mounted() {
+		this.ctx = this.$refs.canvas.getContext('2d');
+
+		new ResizeObserver(this.resize).observe(this.$refs.content);
+
+		this.resize();
+		this.tick();
+	},
+	updated() {
+		this.resize();
+	},
+	destroyed() {
+		window.cancelAnimationFrame(this.anim);
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-sparkle {
+	position: relative;
+	display: inline-block;
+
+	> span {
+		display: inline-block;
+	}
+
+	> canvas {
+		position: absolute;
+		top: -0.1em;
+		left: -0.1em;
+		pointer-events: none;
+	}
+}
+</style>
diff --git a/packages/client/src/components/sub-note-content.vue b/packages/client/src/components/sub-note-content.vue
new file mode 100644
index 0000000000..3f03f021cd
--- /dev/null
+++ b/packages/client/src/components/sub-note-content.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="wrmlmaau">
+	<div class="body">
+		<span v-if="note.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
+		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $ts.deleted }})</span>
+		<MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
+		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
+		<MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
+	</div>
+	<details v-if="note.files.length > 0">
+		<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
+		<XMediaList :media-list="note.files"/>
+	</details>
+	<details v-if="note.poll">
+		<summary>{{ $ts.poll }}</summary>
+		<XPoll :note="note"/>
+	</details>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XPoll from './poll.vue';
+import XMediaList from './media-list.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XPoll,
+		XMediaList,
+	},
+	props: {
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wrmlmaau {
+	overflow-wrap: break-word;
+
+	> .body {
+		> .reply {
+			margin-right: 6px;
+			color: var(--accent);
+		}
+
+		> .rp {
+			margin-left: 4px;
+			font-style: oblique;
+			color: var(--renote);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/tab.vue b/packages/client/src/components/tab.vue
new file mode 100644
index 0000000000..c629727358
--- /dev/null
+++ b/packages/client/src/components/tab.vue
@@ -0,0 +1,73 @@
+<script lang="ts">
+import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
+
+export default defineComponent({
+	props: {
+		modelValue: {
+			required: true,
+		},
+	},
+	render() {
+		const options = this.$slots.default();
+
+		return withDirectives(h('div', {
+			class: 'pxhvhrfw',
+		}, options.map(option => withDirectives(h('button', {
+			class: ['_button', { active: this.modelValue === option.props.value }],
+			key: option.key,
+			disabled: this.modelValue === option.props.value,
+			onClick: () => {
+				this.$emit('update:modelValue', option.props.value);
+			}
+		}, option.children), [
+			[resolveDirective('click-anime')]
+		]))), [
+			[resolveDirective('size'), { max: [500] }]
+		]);
+	}
+});
+</script>
+
+<style lang="scss">
+.pxhvhrfw {
+	display: flex;
+	font-size: 90%;
+
+	> button {
+		flex: 1;
+		padding: 10px 8px;
+		border-radius: var(--radius);
+
+		&:disabled {
+			opacity: 1 !important;
+			cursor: default;
+		}
+
+		&.active {
+			color: var(--accent);
+			background: var(--accentedBg);
+		}
+
+		&:not(.active):hover {
+			color: var(--fgHighlighted);
+			background: var(--panelHighlight);
+		}
+
+		&:not(:first-child) {
+			margin-left: 8px;
+		}
+
+		> .icon {
+			margin-right: 6px;
+		}
+	}
+
+	&.max-width_500px {
+		font-size: 80%;
+
+		> button {
+			padding: 11px 8px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/taskmanager.api-window.vue b/packages/client/src/components/taskmanager.api-window.vue
new file mode 100644
index 0000000000..6ec4da3a59
--- /dev/null
+++ b/packages/client/src/components/taskmanager.api-window.vue
@@ -0,0 +1,72 @@
+<template>
+<XWindow ref="window"
+	:initial-width="370"
+	:initial-height="450"
+	:can-resize="true"
+	@close="$refs.window.close()"
+	@closed="$emit('closed')"
+>
+	<template #header>Req Viewer</template>
+
+	<div class="rlkneywz">
+		<MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
+			<option value="req">Request</option>
+			<option value="res">Response</option>
+		</MkTab>
+
+		<code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code>
+		<code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code>
+	</div>
+</XWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as JSON5 from 'json5';
+import XWindow from '@/components/ui/window.vue';
+import MkTab from '@/components/tab.vue';
+
+export default defineComponent({
+	components: {
+		XWindow,
+		MkTab,
+	},
+
+	props: {
+		req: {
+			required: true,
+		}
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			tab: 'req',
+			reqStr: JSON5.stringify(this.req.req, null, '\t'),
+			resStr: JSON5.stringify(this.req.res, null, '\t'),
+		}
+	},
+
+	methods: {
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rlkneywz {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+
+	> code {
+		display: block;
+		flex: 1;
+		padding: 8px;
+		overflow: auto;
+		font-size: 0.9em;
+		tab-size: 2;
+		white-space: pre;
+	}
+}
+</style>
diff --git a/packages/client/src/components/taskmanager.vue b/packages/client/src/components/taskmanager.vue
new file mode 100644
index 0000000000..6efbf286e6
--- /dev/null
+++ b/packages/client/src/components/taskmanager.vue
@@ -0,0 +1,233 @@
+<template>
+<XWindow ref="window" :initial-width="650" :initial-height="420" :can-resize="true" @closed="$emit('closed')">
+	<template #header>
+		<i class="fas fa-terminal" style="margin-right: 0.5em;"></i>Task Manager
+	</template>
+	<div class="qljqmnzj _monospace">
+		<MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
+			<option value="windows">Windows</option>
+			<option value="stream">Stream</option>
+			<option value="streamPool">Stream (Pool)</option>
+			<option value="api">API</option>
+		</MkTab>
+
+		<div class="content">
+			<div v-if="tab === 'windows'" class="windows" v-follow>
+				<div class="header">
+					<div>#ID</div>
+					<div>Component</div>
+					<div>Action</div>
+				</div>
+				<div v-for="p in popups">
+					<div>#{{ p.id }}</div>
+					<div>{{ p.component.name ? p.component.name : '<anonymous>' }}</div>
+					<div><button class="_textButton" @click="killPopup(p)">Kill</button></div>
+				</div>
+			</div>
+			<div v-if="tab === 'stream'" class="stream" v-follow>
+				<div class="header">
+					<div>#ID</div>
+					<div>Ch</div>
+					<div>Handle</div>
+					<div>In</div>
+					<div>Out</div>
+				</div>
+				<div v-for="c in connections">
+					<div>#{{ c.id }}</div>
+					<div>{{ c.channel }}</div>
+					<div v-if="c.users !== null">(shared)<span v-if="c.name">{{ ' ' + c.name }}</span></div>
+					<div v-else>{{ c.name ? c.name : '<anonymous>' }}</div>
+					<div>{{ c.in }}</div>
+					<div>{{ c.out }}</div>
+				</div>
+			</div>
+			<div v-if="tab === 'streamPool'" class="streamPool" v-follow>
+				<div class="header">
+					<div>#ID</div>
+					<div>Ch</div>
+					<div>Users</div>
+				</div>
+				<div v-for="p in pools">
+					<div>#{{ p.id }}</div>
+					<div>{{ p.channel }}</div>
+					<div>{{ p.users }}</div>
+				</div>
+			</div>
+			<div v-if="tab === 'api'" class="api" v-follow>
+				<div class="header">
+					<div>#ID</div>
+					<div>Endpoint</div>
+					<div>State</div>
+				</div>
+				<div v-for="req in apiRequests" @click="showReq(req)">
+					<div>#{{ req.id }}</div>
+					<div>{{ req.endpoint }}</div>
+					<div class="state" :class="req.state">{{ req.state }}</div>
+				</div>
+			</div>
+		</div>
+
+		<footer>
+			<div><span class="label">Windows</span>{{ popups.length }}</div>
+			<div><span class="label">Stream</span>{{ connections.length }}</div>
+			<div><span class="label">Stream (Pool)</span>{{ pools.length }}</div>
+		</footer>
+	</div>
+</XWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw, onBeforeUnmount, ref, shallowRef } from 'vue';
+import XWindow from '@/components/ui/window.vue';
+import MkTab from '@/components/tab.vue';
+import MkButton from '@/components/ui/button.vue';
+import follow from '@/directives/follow-append';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XWindow,
+		MkTab,
+		MkButton,
+	},
+
+	directives: {
+		follow
+	},
+
+	props: {
+	},
+
+	emits: ['closed'],
+
+	setup() {
+		const connections = shallowRef([]);
+		const pools = shallowRef([]);
+		const refreshStreamInfo = () => {
+			console.log(os.stream.sharedConnectionPools, os.stream.sharedConnections, os.stream.nonSharedConnections);
+			const conn = os.stream.sharedConnections.map(c => ({
+				id: c.id, name: c.name, channel: c.channel, users: c.pool.users, in: c.inCount, out: c.outCount,
+			})).concat(os.stream.nonSharedConnections.map(c => ({
+				id: c.id, name: c.name, channel: c.channel, users: null, in: c.inCount, out: c.outCount,
+			})));
+			conn.sort((a, b) => (a.id > b.id) ? 1 : -1);
+			connections.value = conn;
+			pools.value = os.stream.sharedConnectionPools;
+		};
+		const interval = setInterval(refreshStreamInfo, 1000);
+		onBeforeUnmount(() => {
+			clearInterval(interval);
+		});
+
+		const killPopup = p => {
+			os.popups.value = os.popups.value.filter(x => x !== p);
+		};
+
+		const showReq = req => {
+			os.popup(import('./taskmanager.api-window.vue'), {
+				req: req
+			}, {
+			}, 'closed');
+		};
+
+		return {
+			tab: ref('stream'),
+			popups: os.popups,
+			apiRequests: os.apiRequests,
+			connections,
+			pools,
+			killPopup,
+			showReq,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.qljqmnzj {
+	display: flex;
+	flex-direction: column;
+	height: 100%;
+
+	> .content {
+		flex: 1;
+		overflow: auto;
+
+		> div {
+			display: table;
+			width: 100%;
+			padding: 16px;
+			box-sizing: border-box;
+
+			> div {
+				display: table-row;
+
+				&:nth-child(even) {
+					//background: rgba(0, 0, 0, 0.1);
+				}
+
+				&.header {
+					opacity: 0.7;
+				}
+
+				> div {
+					display: table-cell;
+					white-space: nowrap;
+
+					&:not(:last-child) {
+						padding-right: 8px;
+					}
+				}
+			}
+
+			&.api {
+				> div {
+					&:not(.header) {
+						cursor: pointer;
+
+						&:hover {
+							color: var(--accent);
+						}
+					}
+
+					> .state {
+						&.pending {
+							color: var(--warn);
+						}
+
+						&.success {
+							color: var(--success);
+						}
+
+						&.failed {
+							color: var(--error);
+						}
+					}
+				}
+			}
+		}
+	}
+
+	> footer {
+		display: flex;
+		width: 100%;
+		padding: 8px 16px;
+		box-sizing: border-box;
+		border-top: solid 0.5px var(--divider);
+		font-size: 0.9em;
+
+		> div {
+			flex: 1;
+
+			> .label {
+				opacity: 0.7;
+				margin-right: 0.5em;
+
+				&:after {
+					content: ":";
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/timeline.vue b/packages/client/src/components/timeline.vue
new file mode 100644
index 0000000000..fa7f4e7f4d
--- /dev/null
+++ b/packages/client/src/components/timeline.vue
@@ -0,0 +1,183 @@
+<template>
+<XNotes :no-gap="!$store.state.showGapBetweenNotesInTimeline" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import XNotes from './notes.vue';
+import * as os from '@/os';
+import * as sound from '@/scripts/sound';
+
+export default defineComponent({
+	components: {
+		XNotes
+	},
+
+	provide() {
+		return {
+			inChannel: this.src === 'channel'
+		};
+	},
+
+	props: {
+		src: {
+			type: String,
+			required: true
+		},
+		list: {
+			type: String,
+			required: false
+		},
+		antenna: {
+			type: String,
+			required: false
+		},
+		channel: {
+			type: String,
+			required: false
+		},
+		sound: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	emits: ['note', 'queue', 'before', 'after'],
+
+	data() {
+		return {
+			connection: null,
+			connection2: null,
+			pagination: null,
+			baseQuery: {
+				includeMyRenotes: this.$store.state.showMyRenotes,
+				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
+				includeLocalRenotes: this.$store.state.showLocalRenotes
+			},
+			query: {},
+			date: null
+		};
+	},
+
+	created() {
+		const prepend = note => {
+			(this.$refs.tl as any).prepend(note);
+
+			this.$emit('note');
+
+			if (this.sound) {
+				sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
+			}
+		};
+
+		const onUserAdded = () => {
+			(this.$refs.tl as any).reload();
+		};
+
+		const onUserRemoved = () => {
+			(this.$refs.tl as any).reload();
+		};
+
+		const onChangeFollowing = () => {
+			if (!this.$refs.tl.backed) {
+				this.$refs.tl.reload();
+			}
+		};
+
+		let endpoint;
+
+		if (this.src == 'antenna') {
+			endpoint = 'antennas/notes';
+			this.query = {
+				antennaId: this.antenna
+			};
+			this.connection = markRaw(os.stream.useChannel('antenna', {
+				antennaId: this.antenna
+			}));
+			this.connection.on('note', prepend);
+		} else if (this.src == 'home') {
+			endpoint = 'notes/timeline';
+			this.connection = markRaw(os.stream.useChannel('homeTimeline'));
+			this.connection.on('note', prepend);
+
+			this.connection2 = markRaw(os.stream.useChannel('main'));
+			this.connection2.on('follow', onChangeFollowing);
+			this.connection2.on('unfollow', onChangeFollowing);
+		} else if (this.src == 'local') {
+			endpoint = 'notes/local-timeline';
+			this.connection = markRaw(os.stream.useChannel('localTimeline'));
+			this.connection.on('note', prepend);
+		} else if (this.src == 'social') {
+			endpoint = 'notes/hybrid-timeline';
+			this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
+			this.connection.on('note', prepend);
+		} else if (this.src == 'global') {
+			endpoint = 'notes/global-timeline';
+			this.connection = markRaw(os.stream.useChannel('globalTimeline'));
+			this.connection.on('note', prepend);
+		} else if (this.src == 'mentions') {
+			endpoint = 'notes/mentions';
+			this.connection = markRaw(os.stream.useChannel('main'));
+			this.connection.on('mention', prepend);
+		} else if (this.src == 'directs') {
+			endpoint = 'notes/mentions';
+			this.query = {
+				visibility: 'specified'
+			};
+			const onNote = note => {
+				if (note.visibility == 'specified') {
+					prepend(note);
+				}
+			};
+			this.connection = markRaw(os.stream.useChannel('main'));
+			this.connection.on('mention', onNote);
+		} else if (this.src == 'list') {
+			endpoint = 'notes/user-list-timeline';
+			this.query = {
+				listId: this.list
+			};
+			this.connection = markRaw(os.stream.useChannel('userList', {
+				listId: this.list
+			}));
+			this.connection.on('note', prepend);
+			this.connection.on('userAdded', onUserAdded);
+			this.connection.on('userRemoved', onUserRemoved);
+		} else if (this.src == 'channel') {
+			endpoint = 'channels/timeline';
+			this.query = {
+				channelId: this.channel
+			};
+			this.connection = markRaw(os.stream.useChannel('channel', {
+				channelId: this.channel
+			}));
+			this.connection.on('note', prepend);
+		}
+
+		this.pagination = {
+			endpoint: endpoint,
+			limit: 10,
+			params: init => ({
+				untilDate: this.date?.getTime(),
+				...this.baseQuery, ...this.query
+			})
+		};
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+		if (this.connection2) this.connection2.dispose();
+	},
+
+	methods: {
+		focus() {
+			this.$refs.tl.focus();
+		},
+
+		timetravel(date?: Date) {
+			this.date = date;
+			this.$refs.tl.reload();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue
new file mode 100644
index 0000000000..fb0de68092
--- /dev/null
+++ b/packages/client/src/components/toast.vue
@@ -0,0 +1,73 @@
+<template>
+<div class="mk-toast">
+	<transition name="notification-slide" appear @after-leave="$emit('closed')">
+		<XNotification :notification="notification" class="notification _acrylic" v-if="showing"/>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XNotification from './notification.vue';
+
+export default defineComponent({
+	components: {
+		XNotification
+	},
+	props: {
+		notification: {
+			type: Object,
+			required: true
+		}
+	},
+	emits: ['closed'],
+	data() {
+		return {
+			showing: true
+		};
+	},
+	mounted() {
+		setTimeout(() => {
+			this.showing = false;
+		}, 6000);
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.notification-slide-enter-active, .notification-slide-leave-active {
+	transition: opacity 0.3s, transform 0.3s !important;
+}
+.notification-slide-enter-from, .notification-slide-leave-to {
+	opacity: 0;
+	transform: translateX(-250px);
+}
+
+.mk-toast {
+	position: fixed;
+	z-index: 10000;
+	left: 0;
+	width: 250px;
+	top: 32px;
+	padding: 0 32px;
+	pointer-events: none;
+
+	@media (max-width: 700px) {
+		top: initial;
+		bottom: 112px;
+		padding: 0 16px;
+	}
+
+	@media (max-width: 500px) {
+		bottom: 92px;
+		padding: 0 8px;
+	}
+
+	> .notification {
+		height: 100%;
+		box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+		border-radius: 8px;
+		overflow: hidden;
+	}
+}
+</style>
diff --git a/packages/client/src/components/token-generate-window.vue b/packages/client/src/components/token-generate-window.vue
new file mode 100644
index 0000000000..bf5775d4d8
--- /dev/null
+++ b/packages/client/src/components/token-generate-window.vue
@@ -0,0 +1,117 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="400"
+	:height="450"
+	:with-ok-button="true"
+	:ok-button-disabled="false"
+	:can-close="false"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+	@ok="ok()"
+>
+	<template #header>{{ title || $ts.generateAccessToken }}</template>
+	<div v-if="information" class="_section">
+		<MkInfo warn>{{ information }}</MkInfo>
+	</div>
+	<div class="_section">
+		<MkInput v-model="name">
+			<template #label>{{ $ts.name }}</template>
+		</MkInput>
+	</div>
+	<div class="_section">
+		<div style="margin-bottom: 16px;"><b>{{ $ts.permission }}</b></div>
+		<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
+		<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
+		<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { permissions } from 'misskey-js';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import MkInput from './form/input.vue';
+import MkTextarea from './form/textarea.vue';
+import MkSwitch from './form/switch.vue';
+import MkButton from './ui/button.vue';
+import MkInfo from './ui/info.vue';
+
+export default defineComponent({
+	components: {
+		XModalWindow,
+		MkInput,
+		MkTextarea,
+		MkSwitch,
+		MkButton,
+		MkInfo,
+	},
+
+	props: {
+		title: {
+			type: String,
+			required: false,
+			default: null
+		},
+		information: {
+			type: String,
+			required: false,
+			default: null
+		},
+		initialName: {
+			type: String,
+			required: false,
+			default: null
+		},
+		initialPermissions: {
+			type: Array,
+			required: false,
+			default: null
+		}
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			name: this.initialName,
+			permissions: {},
+			kinds: permissions
+		};
+	},
+
+	created() {
+		if (this.initialPermissions) {
+			for (const kind of this.initialPermissions) {
+				this.permissions[kind] = true;
+			}
+		} else {
+			for (const kind of this.kinds) {
+				this.permissions[kind] = false;
+			}
+		}
+	},
+
+	methods: {
+		ok() {
+			this.$emit('done', {
+				name: this.name,
+				permissions: Object.keys(this.permissions).filter(p => this.permissions[p])
+			});
+			this.$refs.dialog.close();
+		},
+
+		disableAll() {
+			for (const p in this.permissions) {
+				this.permissions[p] = false;
+			}
+		},
+
+		enableAll() {
+			for (const p in this.permissions) {
+				this.permissions[p] = true;
+			}
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue
new file mode 100644
index 0000000000..b5f4547c84
--- /dev/null
+++ b/packages/client/src/components/ui/button.vue
@@ -0,0 +1,262 @@
+<template>
+<button v-if="!link" class="bghgjjyj _button"
+	:class="{ inline, primary, gradate, danger, rounded, full }"
+	:type="type"
+	@click="$emit('click', $event)"
+	@mousedown="onMousedown"
+>
+	<div ref="ripples" class="ripples"></div>
+	<div class="content">
+		<slot></slot>
+	</div>
+</button>
+<MkA v-else class="bghgjjyj _button"
+	:class="{ inline, primary, gradate, danger, rounded, full }"
+	:to="to"
+	@mousedown="onMousedown"
+>
+	<div ref="ripples" class="ripples"></div>
+	<div class="content">
+		<slot></slot>
+	</div>
+</MkA>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		type: {
+			type: String,
+			required: false
+		},
+		primary: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		gradate: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		rounded: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		inline: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		link: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		to: {
+			type: String,
+			required: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		wait: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		danger: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		full: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	emits: ['click'],
+	mounted() {
+		if (this.autofocus) {
+			this.$nextTick(() => {
+				this.$el.focus();
+			});
+		}
+	},
+	methods: {
+		onMousedown(e: MouseEvent) {
+			function distance(p, q) {
+				return Math.hypot(p.x - q.x, p.y - q.y);
+			}
+
+			function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) {
+				const origin = {x: circleCenterX, y: circleCenterY};
+				const dist1 = distance({x: 0, y: 0}, origin);
+				const dist2 = distance({x: boxW, y: 0}, origin);
+				const dist3 = distance({x: 0, y: boxH}, origin);
+				const dist4 = distance({x: boxW, y: boxH }, origin);
+				return Math.max(dist1, dist2, dist3, dist4) * 2;
+			}
+
+			const rect = e.target.getBoundingClientRect();
+
+			const ripple = document.createElement('div');
+			ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px';
+			ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px';
+
+			this.$refs.ripples.appendChild(ripple);
+
+			const circleCenterX = e.clientX - rect.left;
+			const circleCenterY = e.clientY - rect.top;
+
+			const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
+
+			setTimeout(() => {
+				ripple.style.transform = 'scale(' + (scale / 2) + ')';
+			}, 1);
+			setTimeout(() => {
+				ripple.style.transition = 'all 1s ease';
+				ripple.style.opacity = '0';
+			}, 1000);
+			setTimeout(() => {
+				if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
+			}, 2000);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.bghgjjyj {
+	position: relative;
+	z-index: 1; // 他コンポーネントのbox-shadowに隠されないようにするため
+	display: block;
+	min-width: 100px;
+	width: max-content;
+	padding: 8px 14px;
+	text-align: center;
+	font-weight: normal;
+	font-size: 0.8em;
+	line-height: 22px;
+	box-shadow: none;
+	text-decoration: none;
+	background: var(--buttonBg);
+	border-radius: 4px;
+	overflow: clip;
+	box-sizing: border-box;
+	transition: background 0.1s ease;
+
+	&:not(:disabled):hover {
+		background: var(--buttonHoverBg);
+	}
+
+	&:not(:disabled):active {
+		background: var(--buttonHoverBg);
+	}
+
+	&.full {
+		width: 100%;
+	}
+
+	&.rounded {
+		border-radius: 999px;
+	}
+
+	&.primary {
+		font-weight: bold;
+		color: var(--fgOnAccent) !important;
+		background: var(--accent);
+
+		&:not(:disabled):hover {
+			background: var(--X8);
+		}
+
+		&:not(:disabled):active {
+			background: var(--X8);
+		}
+	}
+
+	&.gradate {
+		font-weight: bold;
+		color: var(--fgOnAccent) !important;
+		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+
+		&:not(:disabled):hover {
+			background: linear-gradient(90deg, var(--X8), var(--X8));
+		}
+
+		&:not(:disabled):active {
+			background: linear-gradient(90deg, var(--X8), var(--X8));
+		}
+	}
+
+	&.danger {
+		color: #ff2a2a;
+
+		&.primary {
+			color: #fff;
+			background: #ff2a2a;
+
+			&:not(:disabled):hover {
+				background: #ff4242;
+			}
+
+			&:not(:disabled):active {
+				background: #d42e2e;
+			}
+		}
+	}
+
+	&:disabled {
+		opacity: 0.7;
+	}
+
+	&:focus-visible {
+		outline: solid 2px var(--focus);
+		outline-offset: 2px;
+	}
+
+	&.inline {
+		display: inline-block;
+		width: auto;
+		min-width: 100px;
+	}
+
+	> .ripples {
+		position: absolute;
+		z-index: 0;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		border-radius: 6px;
+		overflow: hidden;
+
+		::v-deep(div) {
+			position: absolute;
+			width: 2px;
+			height: 2px;
+			border-radius: 100%;
+			background: rgba(0, 0, 0, 0.1);
+			opacity: 1;
+			transform: scale(1);
+			transition: all 0.5s cubic-bezier(0,.5,0,1);
+		}
+	}
+
+	&.primary > .ripples ::v-deep(div) {
+		background: rgba(0, 0, 0, 0.15);
+	}
+
+	> .content {
+		position: relative;
+		z-index: 1;
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/container.vue b/packages/client/src/components/ui/container.vue
new file mode 100644
index 0000000000..14673dfcd7
--- /dev/null
+++ b/packages/client/src/components/ui/container.vue
@@ -0,0 +1,262 @@
+<template>
+<div class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }">
+	<header v-if="showHeader" ref="header">
+		<div class="title"><slot name="header"></slot></div>
+		<div class="sub">
+			<slot name="func"></slot>
+			<button class="_button" v-if="foldable" @click="() => showBody = !showBody">
+				<template v-if="showBody"><i class="fas fa-angle-up"></i></template>
+				<template v-else><i class="fas fa-angle-down"></i></template>
+			</button>
+		</div>
+	</header>
+	<transition name="container-toggle"
+		@enter="enter"
+		@after-enter="afterEnter"
+		@leave="leave"
+		@after-leave="afterLeave"
+	>
+		<div v-show="showBody" class="content" :class="{ omitted }" ref="content">
+			<slot></slot>
+			<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }">
+				<span>{{ $ts.showMore }}</span>
+			</button>
+		</div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		showHeader: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+		thin: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		naked: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		foldable: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		expanded: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+		scrollable: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		maxHeight: {
+			type: Number,
+			required: false,
+			default: null
+		},
+	},
+	data() {
+		return {
+			showBody: this.expanded,
+			omitted: null,
+			ignoreOmit: false,
+		};
+	},
+	mounted() {
+		this.$watch('showBody', showBody => {
+			const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0;
+			this.$el.style.minHeight = `${headerHeight}px`;
+			if (showBody) {
+				this.$el.style.flexBasis = `auto`;
+			} else {
+				this.$el.style.flexBasis = `${headerHeight}px`;
+			}
+		}, {
+			immediate: true
+		});
+
+		this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px');
+
+		const calcOmit = () => {
+			if (this.omitted || this.ignoreOmit || this.maxHeight == null) return;
+			const height = this.$refs.content.offsetHeight;
+			this.omitted = height > this.maxHeight;
+		};
+
+		calcOmit();
+		new ResizeObserver((entries, observer) => {
+			calcOmit();
+		}).observe(this.$refs.content);
+	},
+	methods: {
+		toggleContent(show: boolean) {
+			if (!this.foldable) return;
+			this.showBody = show;
+		},
+
+		enter(el) {
+			const elementHeight = el.getBoundingClientRect().height;
+			el.style.height = 0;
+			el.offsetHeight; // reflow
+			el.style.height = elementHeight + 'px';
+		},
+		afterEnter(el) {
+			el.style.height = null;
+		},
+		leave(el) {
+			const elementHeight = el.getBoundingClientRect().height;
+			el.style.height = elementHeight + 'px';
+			el.offsetHeight; // reflow
+			el.style.height = 0;
+		},
+		afterLeave(el) {
+			el.style.height = null;
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.container-toggle-enter-active, .container-toggle-leave-active {
+	overflow-y: hidden;
+	transition: opacity 0.5s, height 0.5s !important;
+}
+.container-toggle-enter-from {
+	opacity: 0;
+}
+.container-toggle-leave-to {
+	opacity: 0;
+}
+
+.ukygtjoj {
+	position: relative;
+	overflow: clip;
+
+	&.naked {
+		background: transparent !important;
+		box-shadow: none !important;
+	}
+
+	&.scrollable {
+		display: flex;
+		flex-direction: column;
+
+		> .content {
+			overflow: auto;
+		}
+	}
+
+	> header {
+		position: sticky;
+		top: var(--stickyTop, 0px);
+		left: 0;
+		color: var(--panelHeaderFg);
+		background: var(--panelHeaderBg);
+		border-bottom: solid 0.5px var(--panelHeaderDivider);
+		z-index: 2;
+		line-height: 1.4em;
+
+		> .title {
+			margin: 0;
+			padding: 12px 16px;
+
+			> ::v-deep(i) {
+				margin-right: 6px;
+			}
+
+			&:empty {
+				display: none;
+			}
+		}
+
+		> .sub {
+			position: absolute;
+			z-index: 2;
+			top: 0;
+			right: 0;
+			height: 100%;
+
+			> ::v-deep(button) {
+				width: 42px;
+				height: 100%;
+			}
+		}
+	}
+
+	> .content {
+		--stickyTop: 0px;
+
+		&.omitted {
+			position: relative;
+			max-height: var(--maxHeight);
+			overflow: hidden;
+
+			> .fade {
+				display: block;
+				position: absolute;
+				z-index: 10;
+				bottom: 0;
+				left: 0;
+				width: 100%;
+				height: 64px;
+				background: linear-gradient(0deg, var(--panel), var(--X15));
+
+				> span {
+					display: inline-block;
+					background: var(--panel);
+					padding: 6px 10px;
+					font-size: 0.8em;
+					border-radius: 999px;
+					box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+				}
+
+				&:hover {
+					> span {
+						background: var(--panelHighlight);
+					}
+				}
+			}
+		}
+	}
+
+	&.max-width_380px, &.thin {
+		> header {
+			> .title {
+				padding: 8px 10px;
+				font-size: 0.9em;
+			}
+		}
+
+		> .content {
+		}
+	}
+}
+
+._forceContainerFull_ .ukygtjoj {
+	> header {
+		> .title {
+			padding: 12px 16px !important;
+		}
+	}
+}
+
+._forceContainerFull_.ukygtjoj {
+	> header {
+		> .title {
+			padding: 12px 16px !important;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/context-menu.vue b/packages/client/src/components/ui/context-menu.vue
new file mode 100644
index 0000000000..561099cbe0
--- /dev/null
+++ b/packages/client/src/components/ui/context-menu.vue
@@ -0,0 +1,97 @@
+<template>
+<transition :name="$store.state.animation ? 'fade' : ''" appear>
+	<div class="nvlagfpb" @contextmenu.prevent.stop="() => {}">
+		<MkMenu :items="items" @close="$emit('closed')" class="_popup _shadow" :align="'left'"/>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import contains from '@/scripts/contains';
+import MkMenu from './menu.vue';
+
+export default defineComponent({
+	components: {
+		MkMenu,
+	},
+	props: {
+		items: {
+			type: Array,
+			required: true
+		},
+		ev: {
+			required: true
+		},
+		viaKeyboard: {
+			type: Boolean,
+			required: false
+		},
+	},
+	emits: ['closed'],
+	computed: {
+		keymap(): any {
+			return {
+				'esc': () => this.$emit('closed'),
+			};
+		},
+	},
+	mounted() {
+		let left = this.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
+		let top = this.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
+
+		const width = this.$el.offsetWidth;
+		const height = this.$el.offsetHeight;
+
+		if (left + width - window.pageXOffset > window.innerWidth) {
+			left = window.innerWidth - width + window.pageXOffset;
+		}
+
+		if (top + height - window.pageYOffset > window.innerHeight) {
+			top = window.innerHeight - height + window.pageYOffset;
+		}
+
+		if (top < 0) {
+			top = 0;
+		}
+
+		if (left < 0) {
+			left = 0;
+		}
+
+		this.$el.style.top = top + 'px';
+		this.$el.style.left = left + 'px';
+
+		for (const el of Array.from(document.querySelectorAll('body *'))) {
+			el.addEventListener('mousedown', this.onMousedown);
+		}
+	},
+	beforeUnmount() {
+		for (const el of Array.from(document.querySelectorAll('body *'))) {
+			el.removeEventListener('mousedown', this.onMousedown);
+		}
+	},
+	methods: {
+		onMousedown(e) {
+			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed');
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.nvlagfpb {
+	position: absolute;
+	z-index: 65535;
+}
+
+.fade-enter-active, .fade-leave-active {
+	transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
+	transform-origin: left top;
+}
+
+.fade-enter-from, .fade-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+</style>
diff --git a/packages/client/src/components/ui/folder.vue b/packages/client/src/components/ui/folder.vue
new file mode 100644
index 0000000000..3997421d08
--- /dev/null
+++ b/packages/client/src/components/ui/folder.vue
@@ -0,0 +1,156 @@
+<template>
+<div class="ssazuxis" v-size="{ max: [500] }">
+	<header @click="showBody = !showBody" class="_button" :style="{ background: bg }">
+		<div class="title"><slot name="header"></slot></div>
+		<div class="divider"></div>
+		<button class="_button">
+			<template v-if="showBody"><i class="fas fa-angle-up"></i></template>
+			<template v-else><i class="fas fa-angle-down"></i></template>
+		</button>
+	</header>
+	<transition name="folder-toggle"
+		@enter="enter"
+		@after-enter="afterEnter"
+		@leave="leave"
+		@after-leave="afterLeave"
+	>
+		<div v-show="showBody">
+			<slot></slot>
+		</div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as tinycolor from 'tinycolor2';
+
+const localStoragePrefix = 'ui:folder:';
+
+export default defineComponent({
+	props: {
+		expanded: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+		persistKey: {
+			type: String,
+			required: false,
+			default: null
+		},
+	},
+	data() {
+		return {
+			bg: null,
+			showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
+		};
+	},
+	watch: {
+		showBody() {
+			if (this.persistKey) {
+				localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
+			}
+		}
+	},
+	mounted() {
+		function getParentBg(el: Element | null): string {
+			if (el == null || el.tagName === 'BODY') return 'var(--bg)';
+			const bg = el.style.background || el.style.backgroundColor;
+			if (bg) {
+				return bg;
+			} else {
+				return getParentBg(el.parentElement);
+			}
+		}
+		const rawBg = getParentBg(this.$el);
+		const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+		bg.setAlpha(0.85);
+		this.bg = bg.toRgbString();
+	},
+	methods: {
+		toggleContent(show: boolean) {
+			this.showBody = show;
+		},
+
+		enter(el) {
+			const elementHeight = el.getBoundingClientRect().height;
+			el.style.height = 0;
+			el.offsetHeight; // reflow
+			el.style.height = elementHeight + 'px';
+		},
+		afterEnter(el) {
+			el.style.height = null;
+		},
+		leave(el) {
+			const elementHeight = el.getBoundingClientRect().height;
+			el.style.height = elementHeight + 'px';
+			el.offsetHeight; // reflow
+			el.style.height = 0;
+		},
+		afterLeave(el) {
+			el.style.height = null;
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.folder-toggle-enter-active, .folder-toggle-leave-active {
+	overflow-y: hidden;
+	transition: opacity 0.5s, height 0.5s !important;
+}
+.folder-toggle-enter-from {
+	opacity: 0;
+}
+.folder-toggle-leave-to {
+	opacity: 0;
+}
+
+.ssazuxis {
+	position: relative;
+
+	> header {
+		display: flex;
+		position: relative;
+		z-index: 10;
+		position: sticky;
+		top: var(--stickyTop, 0px);
+		padding: var(--x-padding);
+		-webkit-backdrop-filter: var(--blur, blur(8px));
+		backdrop-filter: var(--blur, blur(20px));
+
+		> .title {
+			margin: 0;
+			padding: 12px 16px 12px 0;
+
+			> i {
+				margin-right: 6px;
+			}
+
+			&:empty {
+				display: none;
+			}
+		}
+
+		> .divider {
+			flex: 1;
+			margin: auto;
+			height: 1px;
+			background: var(--divider);
+		}
+
+		> button {
+			padding: 12px 0 12px 16px;
+		}
+	}
+
+	&.max-width_500px {
+		> header {
+			> .title {
+				padding: 8px 10px 8px 0;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/hr.vue b/packages/client/src/components/ui/hr.vue
new file mode 100644
index 0000000000..6b075cb440
--- /dev/null
+++ b/packages/client/src/components/ui/hr.vue
@@ -0,0 +1,16 @@
+<template>
+<div class="evrzpitu"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';import * as os from '@/os';
+
+export default defineComponent({});
+</script>
+
+<style lang="scss" scoped>
+.evrzpitu
+	margin 16px 0
+	border-bottom solid var(--lineWidth) var(--faceDivider)
+
+</style>
diff --git a/packages/client/src/components/ui/info.vue b/packages/client/src/components/ui/info.vue
new file mode 100644
index 0000000000..8f5986baf7
--- /dev/null
+++ b/packages/client/src/components/ui/info.vue
@@ -0,0 +1,45 @@
+<template>
+<div class="fpezltsf" :class="{ warn }">
+	<i v-if="warn" class="fas fa-exclamation-triangle"></i>
+	<i v-else class="fas fa-info-circle"></i>
+	<slot></slot>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		warn: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	data() {
+		return {
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fpezltsf {
+	padding: 16px;
+	font-size: 90%;
+	background: var(--infoBg);
+	color: var(--infoFg);
+	border-radius: var(--radius);
+
+	&.warn {
+		background: var(--infoWarnBg);
+		color: var(--infoWarnFg);
+	}
+
+	> i {
+		margin-right: 4px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
new file mode 100644
index 0000000000..5938fb00a1
--- /dev/null
+++ b/packages/client/src/components/ui/menu.vue
@@ -0,0 +1,278 @@
+<template>
+<div class="rrevdjwt" :class="{ center: align === 'center' }"
+	:style="{ width: width ? width + 'px' : null }"
+	ref="items"
+	@contextmenu.self="e => e.preventDefault()"
+	v-hotkey="keymap"
+>
+	<template v-for="(item, i) in _items">
+		<div v-if="item === null" class="divider"></div>
+		<span v-else-if="item.type === 'label'" class="label item">
+			<span>{{ item.text }}</span>
+		</span>
+		<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
+			<span><MkEllipsis/></span>
+		</span>
+		<MkA v-else-if="item.type === 'link'" :to="item.to" @click.passive="close()" :tabindex="i" class="_button item">
+			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
+			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+			<span>{{ item.text }}</span>
+			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+		</MkA>
+		<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item">
+			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
+			<span>{{ item.text }}</span>
+			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+		</a>
+		<button v-else-if="item.type === 'user'" @click="clicked(item.action, $event)" :tabindex="i" class="_button item">
+			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
+			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+		</button>
+		<button v-else @click="clicked(item.action, $event)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active">
+			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
+			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
+			<span>{{ item.text }}</span>
+			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
+		</button>
+	</template>
+	<span v-if="_items.length === 0" class="none item">
+		<span>{{ $ts.none }}</span>
+	</span>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, unref } from 'vue';
+import { focusPrev, focusNext } from '@/scripts/focus';
+import contains from '@/scripts/contains';
+
+export default defineComponent({
+	props: {
+		items: {
+			type: Array,
+			required: true
+		},
+		viaKeyboard: {
+			type: Boolean,
+			required: false
+		},
+		align: {
+			type: String,
+			requried: false
+		},
+		width: {
+			type: Number,
+			required: false
+		},
+	},
+	emits: ['close'],
+	data() {
+		return {
+			_items: [],
+		};
+	},
+	computed: {
+		keymap(): any {
+			return {
+				'up|k|shift+tab': this.focusUp,
+				'down|j|tab': this.focusDown,
+				'esc': this.close,
+			};
+		},
+	},
+	watch: {
+		items: {
+			handler() {
+				const items = ref(unref(this.items).filter(item => item !== undefined));
+
+				for (let i = 0; i < items.value.length; i++) {
+					const item = items.value[i];
+					
+					if (item && item.then) { // if item is Promise
+						items.value[i] = { type: 'pending' };
+						item.then(actualItem => {
+							items.value[i] = actualItem;
+						});
+					}
+				}
+
+				this._items = items;
+			},
+			immediate: true
+		}
+	},
+	mounted() {
+		if (this.viaKeyboard) {
+			this.$nextTick(() => {
+				focusNext(this.$refs.items.children[0], true, false);
+			});
+		}
+
+		if (this.contextmenuEvent) {
+			this.$el.style.top = this.contextmenuEvent.pageY + 'px';
+			this.$el.style.left = this.contextmenuEvent.pageX + 'px';
+
+			for (const el of Array.from(document.querySelectorAll('body *'))) {
+				el.addEventListener('mousedown', this.onMousedown);
+			}
+		}
+	},
+	beforeUnmount() {
+		for (const el of Array.from(document.querySelectorAll('body *'))) {
+			el.removeEventListener('mousedown', this.onMousedown);
+		}
+	},
+	methods: {
+		clicked(fn, ev) {
+			fn(ev);
+			this.close();
+		},
+		close() {
+			this.$emit('close');
+		},
+		focusUp() {
+			focusPrev(document.activeElement);
+		},
+		focusDown() {
+			focusNext(document.activeElement);
+		},
+		onMousedown(e) {
+			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rrevdjwt {
+	padding: 8px 0;
+	min-width: 200px;
+	max-height: 90vh;
+	overflow: auto;
+
+	&.center {
+		> .item {
+			text-align: center;
+		}
+	}
+
+	> .item {
+		display: block;
+		position: relative;
+		padding: 8px 18px;
+		width: 100%;
+		box-sizing: border-box;
+		white-space: nowrap;
+		font-size: 0.9em;
+		line-height: 20px;
+		text-align: left;
+		overflow: hidden;
+		text-overflow: ellipsis;
+
+		&:before {
+			content: "";
+			display: block;
+			position: absolute;
+			top: 0;
+			left: 0;
+			right: 0;
+			margin: auto;
+			width: calc(100% - 16px);
+			height: 100%;
+			border-radius: 6px;
+		}
+
+		> * {
+			position: relative;
+		}
+
+		&.danger {
+			color: #ff2a2a;
+
+			&:hover {
+				color: #fff;
+
+				&:before {
+					background: #ff4242;
+				}
+			}
+
+			&:active {
+				color: #fff;
+
+				&:before {
+					background: #d42e2e;
+				}
+			}
+		}
+
+		&.active {
+			color: var(--fgOnAccent);
+			opacity: 1;
+
+			&:before {
+				background: var(--accent);
+			}
+		}
+
+		&:not(:disabled):hover {
+			color: var(--accent);
+			text-decoration: none;
+
+			&:before {
+				background: var(--accentedBg);
+			}
+		}
+
+		&:not(:active):focus-visible {
+			box-shadow: 0 0 0 2px var(--focus) inset;
+		}
+
+		&.label {
+			pointer-events: none;
+			font-size: 0.7em;
+			padding-bottom: 4px;
+
+			> span {
+				opacity: 0.7;
+			}
+		}
+
+		&.pending {
+			pointer-events: none;
+			opacity: 0.7;
+		}
+
+		&.none {
+			pointer-events: none;
+			opacity: 0.7;
+		}
+
+		> i {
+			margin-right: 5px;
+			width: 20px;
+		}
+
+		> .avatar {
+			margin-right: 5px;
+			width: 20px;
+			height: 20px;
+		}
+
+		> .indicator {
+			position: absolute;
+			top: 5px;
+			left: 13px;
+			color: var(--indicator);
+			font-size: 12px;
+			animation: blink 1s infinite;
+		}
+	}
+
+	> .divider {
+		margin: 8px 0;
+		height: 1px;
+		background: var(--divider);
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/modal-window.vue b/packages/client/src/components/ui/modal-window.vue
new file mode 100644
index 0000000000..da98192b87
--- /dev/null
+++ b/packages/client/src/components/ui/modal-window.vue
@@ -0,0 +1,148 @@
+<template>
+<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
+	<div class="ebkgoccj _window _narrow_" @keydown="onKeydown" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) :  (height ? `min(${height}px, 100%)` : '100%') }">
+		<div class="header">
+			<button class="_button" v-if="withOkButton" @click="$emit('close')"><i class="fas fa-times"></i></button>
+			<span class="title">
+				<slot name="header"></slot>
+			</span>
+			<button class="_button" v-if="!withOkButton" @click="$emit('close')"><i class="fas fa-times"></i></button>
+			<button class="_button" v-if="withOkButton" @click="$emit('ok')" :disabled="okButtonDisabled"><i class="fas fa-check"></i></button>
+		</div>
+		<div class="body" v-if="padding">
+			<div class="_section">
+				<slot></slot>
+			</div>
+		</div>
+		<div class="body" v-else>
+			<slot></slot>
+		</div>
+	</div>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from './modal.vue';
+
+export default defineComponent({
+	components: {
+		MkModal
+	},
+	props: {
+		withOkButton: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		okButtonDisabled: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		padding: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		width: {
+			type: Number,
+			required: false,
+			default: 400
+		},
+		height: {
+			type: Number,
+			required: false,
+			default: null
+		},
+		canClose: {
+			type: Boolean,
+			required: false,
+			default: true,
+		},
+		scroll: {
+			type: Boolean,
+			required: false,
+			default: true,
+		},
+	},
+
+	emits: ['click', 'close', 'closed', 'ok'],
+
+	data() {
+		return {
+		};
+	},
+
+	methods: {
+		close() {
+			this.$refs.modal.close();
+		},
+
+		onKeydown(e) {
+			if (e.which === 27) { // Esc
+				e.preventDefault();
+				e.stopPropagation();
+				this.close();
+			}
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ebkgoccj {
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+	contain: content;
+
+	--root-margin: 24px;
+
+	@media (max-width: 500px) {
+		--root-margin: 16px;
+	}
+
+	> .header {
+		$height: 58px;
+		$height-narrow: 42px;
+		display: flex;
+		flex-shrink: 0;
+		box-shadow: 0px 1px var(--divider);
+
+		> button {
+			height: $height;
+			width: $height;
+
+			@media (max-width: 500px) {
+				height: $height-narrow;
+				width: $height-narrow;
+			}
+		}
+
+		> .title {
+			flex: 1;
+			line-height: $height;
+			padding-left: 32px;
+			font-weight: bold;
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			pointer-events: none;
+
+			@media (max-width: 500px) {
+				line-height: $height-narrow;
+				padding-left: 16px;
+			}
+		}
+
+		> button + .title {
+			padding-left: 0;
+		}
+	}
+
+	> .body {
+		overflow: auto;
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue
new file mode 100644
index 0000000000..33fcdb687f
--- /dev/null
+++ b/packages/client/src/components/ui/modal.vue
@@ -0,0 +1,292 @@
+<template>
+<transition :name="$store.state.animation ? popup ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? popup ? 500 : 300 : 0" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered">
+	<div v-show="manualShowing != null ? manualShowing : showing" class="qzhlnise" :class="{ front }" v-hotkey.global="keymap" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+		<div class="bg _modalBg" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
+		<div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content">
+			<slot></slot>
+		</div>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+function getFixedContainer(el: Element | null): Element | null {
+	if (el == null || el.tagName === 'BODY') return null;
+	const position = window.getComputedStyle(el).getPropertyValue('position');
+	if (position === 'fixed') {
+		return el;
+	} else {
+		return getFixedContainer(el.parentElement);
+	}
+}
+
+export default defineComponent({
+	provide: {
+		modal: true
+	},
+	props: {
+		manualShowing: {
+			type: Boolean,
+			required: false,
+			default: null,
+		},
+		srcCenter: {
+			type: Boolean,
+			required: false
+		},
+		src: {
+			required: false,
+		},
+		position: {
+			required: false
+		},
+		front: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+	emits: ['opening', 'click', 'esc', 'close', 'closed'],
+	data() {
+		return {
+			showing: true,
+			fixed: false,
+			transformOrigin: 'center',
+			contentClicking: false,
+		};
+	},
+	computed: {
+		keymap(): any {
+			return {
+				'esc': () => this.$emit('esc'),
+			};
+		},
+		popup(): boolean {
+			return this.src != null;
+		}
+	},
+	mounted() {
+		this.$watch('src', () => {
+			this.fixed = getFixedContainer(this.src) != null;
+			this.$nextTick(() => {
+				this.align();
+			});
+		}, { immediate: true });
+
+		this.$nextTick(() => {
+			const popover = this.$refs.content as any;
+			new ResizeObserver((entries, observer) => {
+				this.align();
+			}).observe(popover);
+		});
+	},
+	methods: {
+		align() {
+			if (!this.popup) return;
+
+			const popover = this.$refs.content as any;
+
+			if (popover == null) return;
+
+			const rect = this.src.getBoundingClientRect();
+			
+			const width = popover.offsetWidth;
+			const height = popover.offsetHeight;
+
+			let left;
+			let top;
+
+			if (this.srcCenter) {
+				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
+				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2);
+				left = (x - (width / 2));
+				top = (y - (height / 2));
+			} else {
+				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
+				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight;
+				left = (x - (width / 2));
+				top = y;
+			}
+
+			if (this.fixed) {
+				if (left + width > window.innerWidth) {
+					left = window.innerWidth - width;
+				}
+
+				if (top + height > window.innerHeight) {
+					top = window.innerHeight - height;
+				}
+			} else {
+				if (left + width - window.pageXOffset > window.innerWidth) {
+					left = window.innerWidth - width + window.pageXOffset - 1;
+				}
+
+				if (top + height - window.pageYOffset > window.innerHeight) {
+					top = window.innerHeight - height + window.pageYOffset - 1;
+				}
+			}
+
+			if (top < 0) {
+				top = 0;
+			}
+
+			if (left < 0) {
+				left = 0;
+			}
+
+			if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) {
+				this.transformOrigin = 'center top';
+			} else {
+				this.transformOrigin = 'center';
+			}
+
+			popover.style.left = left + 'px';
+			popover.style.top = top + 'px';
+		},
+
+		childRendered() {
+			// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
+			const content = this.$refs.content.children[0];
+			content.addEventListener('mousedown', e => {
+				this.contentClicking = true;
+				window.addEventListener('mouseup', e => {
+					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
+					setTimeout(() => {
+						this.contentClicking = false;
+					}, 100);
+				}, { passive: true, once: true });
+			}, { passive: true });
+		},
+
+		close() {
+			this.showing = false;
+			this.$emit('close');
+		},
+
+		onBgClick() {
+			if (this.contentClicking) return;
+			this.$emit('click');
+		},
+
+		onClosed() {
+			this.$emit('closed');
+		}
+	}
+});
+</script>
+
+<style lang="scss">
+.modal-popup-enter-active, .modal-popup-leave-active,
+.modal-enter-from, .modal-leave-to {
+	> .content {
+		transform-origin: var(--transformOrigin);
+	}
+}
+</style>
+
+<style lang="scss" scoped>
+.modal-enter-active, .modal-leave-active {
+	> .bg {
+		transition: opacity 0.3s !important;
+	}
+
+	> .content {
+		transition: opacity 0.3s, transform 0.3s !important;
+	}
+}
+.modal-enter-from, .modal-leave-to {
+	> .bg {
+		opacity: 0;
+	}
+
+	> .content {
+		pointer-events: none;
+		opacity: 0;
+		transform: scale(0.9);
+	}
+}
+
+.modal-popup-enter-active, .modal-popup-leave-active {
+	> .bg {
+		transition: opacity 0.3s !important;
+	}
+
+	> .content {
+		transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1) !important;
+	}
+}
+.modal-popup-enter-from, .modal-popup-leave-to {
+	> .bg {
+		opacity: 0;
+	}
+
+	> .content {
+		pointer-events: none;
+		opacity: 0;
+		transform: scale(0.9);
+	}
+}
+
+.qzhlnise {
+	> .bg {
+		z-index: 10000;
+	}
+
+	> .content:not(.popup) {
+		position: fixed;
+		z-index: 10000;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		margin: auto;
+		padding: 32px;
+		// TODO: mask-imageはiOSだとやたら重い。なんとかしたい
+		-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
+		mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
+		overflow: auto;
+		display: flex;
+
+		@media (max-width: 500px) {
+			padding: 16px;
+			-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
+			mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
+		}
+
+		> ::v-deep(*) {
+			margin: auto;
+		}
+
+		&.top {
+			> ::v-deep(*) {
+				margin-top: 0;
+			}
+		}
+	}
+
+	> .content.popup {
+		position: absolute;
+		z-index: 10000;
+
+		&.fixed {
+			position: fixed;
+		}
+	}
+
+	&.front {
+		> .bg {
+			z-index: 20000;
+		}
+
+		> .content:not(.popup) {
+			z-index: 20000;
+		}
+
+		> .content.popup {
+			z-index: 20000;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue
new file mode 100644
index 0000000000..f6a457d88f
--- /dev/null
+++ b/packages/client/src/components/ui/pagination.vue
@@ -0,0 +1,69 @@
+<template>
+<transition name="fade" mode="out-in">
+	<MkLoading v-if="fetching"/>
+
+	<MkError v-else-if="error" @retry="init()"/>
+
+	<div class="empty" v-else-if="empty" key="_empty_">
+		<slot name="empty"></slot>
+	</div>
+
+	<div v-else class="cxiknjgy">
+		<slot :items="items"></slot>
+		<div class="more _gap" v-show="more" key="_more_">
+			<MkButton class="button" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
+				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+				<template v-if="moreFetching"><MkLoading inline/></template>
+			</MkButton>
+		</div>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from './button.vue';
+import paging from '@/scripts/paging';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+
+	mixins: [
+		paging({}),
+	],
+
+	props: {
+		pagination: {
+			required: true
+		},
+
+		disableAutoLoad: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.cxiknjgy {
+	> .more > .button {
+		margin-left: auto;
+		margin-right: auto;
+		height: 48px;
+		min-width: 150px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/popup-menu.vue b/packages/client/src/components/ui/popup-menu.vue
new file mode 100644
index 0000000000..3ff4c658b1
--- /dev/null
+++ b/packages/client/src/components/ui/popup-menu.vue
@@ -0,0 +1,42 @@
+<template>
+<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
+	<MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/>
+</MkPopup>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPopup from './popup.vue';
+import MkMenu from './menu.vue';
+
+export default defineComponent({
+	components: {
+		MkPopup,
+		MkMenu,
+	},
+
+	props: {
+		items: {
+			type: Array,
+			required: true
+		},
+		align: {
+			type: String,
+			required: false
+		},
+		width: {
+			type: Number,
+			required: false
+		},
+		viaKeyboard: {
+			type: Boolean,
+			required: false
+		},
+		src: {
+			required: false
+		},
+	},
+
+	emits: ['close', 'closed'],
+});
+</script>
diff --git a/packages/client/src/components/ui/popup.vue b/packages/client/src/components/ui/popup.vue
new file mode 100644
index 0000000000..0fb1780cc5
--- /dev/null
+++ b/packages/client/src/components/ui/popup.vue
@@ -0,0 +1,213 @@
+<template>
+<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered">
+	<div v-show="manualShowing != null ? manualShowing : showing" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" ref="content" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+		<slot></slot>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+
+function getFixedContainer(el: Element | null): Element | null {
+	if (el == null || el.tagName === 'BODY') return null;
+	const position = window.getComputedStyle(el).getPropertyValue('position');
+	if (position === 'fixed') {
+		return el;
+	} else {
+		return getFixedContainer(el.parentElement);
+	}
+}
+
+export default defineComponent({
+	props: {
+		manualShowing: {
+			type: Boolean,
+			required: false,
+			default: null,
+		},
+		srcCenter: {
+			type: Boolean,
+			required: false
+		},
+		src: {
+			type: Object as PropType<HTMLElement>,
+			required: false,
+		},
+		position: {
+			required: false
+		},
+		front: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	emits: ['opening', 'click', 'esc', 'close', 'closed'],
+
+	data() {
+		return {
+			showing: true,
+			fixed: false,
+			transformOrigin: 'center',
+			contentClicking: false,
+		};
+	},
+
+	mounted() {
+		this.$watch('src', () => {
+			if (this.src) {
+				this.src.style.pointerEvents = 'none';
+			}
+			this.fixed = getFixedContainer(this.src) != null;
+			this.$nextTick(() => {
+				this.align();
+			});
+		}, { immediate: true });
+
+		this.$nextTick(() => {
+			const popover = this.$refs.content as any;
+			new ResizeObserver((entries, observer) => {
+				this.align();
+			}).observe(popover);
+		});
+
+		document.addEventListener('mousedown', this.onDocumentClick, { passive: true });
+	},
+
+	beforeUnmount() {
+		document.removeEventListener('mousedown', this.onDocumentClick);
+	},
+
+	methods: {
+		align() {
+			if (this.src == null) return;
+
+			const popover = this.$refs.content as any;
+
+			if (popover == null) return;
+
+			const rect = this.src.getBoundingClientRect();
+			
+			const width = popover.offsetWidth;
+			const height = popover.offsetHeight;
+
+			let left;
+			let top;
+
+			if (this.srcCenter) {
+				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
+				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2);
+				left = (x - (width / 2));
+				top = (y - (height / 2));
+			} else {
+				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
+				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight;
+				left = (x - (width / 2));
+				top = y;
+			}
+
+			if (this.fixed) {
+				if (left + width > window.innerWidth) {
+					left = window.innerWidth - width;
+				}
+
+				if (top + height > window.innerHeight) {
+					top = window.innerHeight - height;
+				}
+			} else {
+				if (left + width - window.pageXOffset > window.innerWidth) {
+					left = window.innerWidth - width + window.pageXOffset - 1;
+				}
+
+				if (top + height - window.pageYOffset > window.innerHeight) {
+					top = window.innerHeight - height + window.pageYOffset - 1;
+				}
+			}
+
+			if (top < 0) {
+				top = 0;
+			}
+
+			if (left < 0) {
+				left = 0;
+			}
+
+			if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) {
+				this.transformOrigin = 'center top';
+			} else {
+				this.transformOrigin = 'center';
+			}
+
+			popover.style.left = left + 'px';
+			popover.style.top = top + 'px';
+		},
+
+		childRendered() {
+			// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
+			const content = this.$refs.content.children[0];
+			content.addEventListener('mousedown', e => {
+				this.contentClicking = true;
+				window.addEventListener('mouseup', e => {
+					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
+					setTimeout(() => {
+						this.contentClicking = false;
+					}, 100);
+				}, { passive: true, once: true });
+			}, { passive: true });
+		},
+
+		close() {
+			if (this.src) this.src.style.pointerEvents = 'auto';
+			this.showing = false;
+			this.$emit('close');
+		},
+
+		onClosed() {
+			this.$emit('closed');
+		},
+
+		onDocumentClick(ev) {
+			const flyoutElement = this.$refs.content;
+			let targetElement = ev.target;
+			do {
+				if (targetElement === flyoutElement) {
+					return;
+				}
+				targetElement = targetElement.parentNode;
+			} while (targetElement);
+			this.close();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.popup-menu-enter-active {
+	transform-origin: var(--transformOrigin);
+	transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
+}
+.popup-menu-leave-active {
+	transform-origin: var(--transformOrigin);
+	transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), transform 0.2s cubic-bezier(0.4, 0, 1, 1) !important;
+}
+.popup-menu-enter-from, .popup-menu-leave-to {
+	pointer-events: none;
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.ccczpooj {
+	position: absolute;
+	z-index: 10000;
+
+	&.fixed {
+		position: fixed;
+	}
+
+	&.front {
+		z-index: 20000;
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/super-menu.vue b/packages/client/src/components/ui/super-menu.vue
new file mode 100644
index 0000000000..195cc57326
--- /dev/null
+++ b/packages/client/src/components/ui/super-menu.vue
@@ -0,0 +1,148 @@
+<template>
+<div class="rrevdjwu" :class="{ grid }">
+	<div class="group" v-for="group in def">
+		<div class="title" v-if="group.title">{{ group.title }}</div>
+
+		<div class="items">
+			<template v-for="(item, i) in group.items">
+				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+					<i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
+					<span class="text">{{ item.text }}</span>
+				</a>
+				<button v-else-if="item.type === 'button'" @click="ev => item.action(ev)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active">
+					<i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
+					<span class="text">{{ item.text }}</span>
+				</button>
+				<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+					<i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
+					<span class="text">{{ item.text }}</span>
+				</MkA>
+			</template>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, unref } from 'vue';
+
+export default defineComponent({
+	props: {
+		def: {
+			type: Array,
+			required: true
+		},
+		grid: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.rrevdjwu {
+	> .group {
+		& + .group {
+			margin-top: 16px;
+			padding-top: 16px;
+			border-top: solid 0.5px var(--divider);
+		}
+
+		> .title {
+			font-size: 0.9em;
+			opacity: 0.7;
+			margin: 0 0 8px 12px;
+		}
+	
+		> .items {
+			> .item {
+				display: flex;
+				align-items: center;
+				width: 100%;
+				box-sizing: border-box;
+				padding: 10px 16px 10px 8px;
+				border-radius: 9px;
+				font-size: 0.9em;
+
+				&:hover {
+					text-decoration: none;
+					background: var(--panelHighlight);
+				}
+
+				&.active {
+					color: var(--accent);
+					background: var(--accentedBg);
+				}
+
+				&.danger {
+					color: var(--error);
+				}
+
+				> .icon {
+					width: 32px;
+					margin-right: 2px;
+					flex-shrink: 0;
+					text-align: center;
+					opacity: 0.8;
+				}
+
+				> .text {
+					white-space: nowrap;
+					text-overflow: ellipsis;
+					overflow: hidden;
+					padding-right: 12px;
+				}
+
+			}
+		}
+	}
+
+	&.grid {
+		> .group {
+			& + .group {
+				padding-top: 0;
+				border-top: none;
+			}
+
+			margin-left: 0;
+			margin-right: 0;
+
+			> .title {
+				font-size: 1em;
+				opacity: 0.7;
+				margin: 0 0 8px 16px;
+			}
+
+			> .items {
+				display: grid;
+				grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
+				grid-gap: 8px;
+				padding: 0 16px;
+
+				> .item {
+					flex-direction: column;
+					padding: 18px 16px 16px 16px;
+					background: var(--panel);
+					border-radius: 8px;
+					text-align: center;
+
+					> .icon {
+						display: block;
+						margin-right: 0;
+						margin-bottom: 12px;
+						font-size: 1.5em;
+					}
+
+					> .text {
+						padding-right: 0;
+						width: 100%;
+						font-size: 0.8em;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue
new file mode 100644
index 0000000000..c003895c14
--- /dev/null
+++ b/packages/client/src/components/ui/tooltip.vue
@@ -0,0 +1,92 @@
+<template>
+<transition name="tooltip" appear @after-leave="$emit('closed')">
+	<div class="buebdbiu _acrylic _shadow" v-show="showing" ref="content" :style="{ maxWidth: maxWidth + 'px' }">
+		<slot>{{ text }}</slot>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		showing: {
+			type: Boolean,
+			required: true,
+		},
+		source: {
+			required: true,
+		},
+		text: {
+			type: String,
+			required: false
+		},
+		maxWidth: {
+			type: Number,
+			required: false,
+			default: 250,
+		},
+	},
+
+	emits: ['closed'],
+
+	mounted() {
+		this.$nextTick(() => {
+			if (this.source == null) {
+				this.$emit('closed');
+				return;
+			}
+
+			const rect = this.source.getBoundingClientRect();
+
+			const contentWidth = this.$refs.content.offsetWidth;
+			const contentHeight = this.$refs.content.offsetHeight;
+
+			let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
+			let top = rect.top + window.pageYOffset - contentHeight;
+
+			left -= (this.$el.offsetWidth / 2);
+
+			if (left + contentWidth - window.pageXOffset > window.innerWidth) {
+				left = window.innerWidth - contentWidth + window.pageXOffset - 1;
+			}
+
+			if (top - window.pageYOffset < 0) {
+				top = rect.top + window.pageYOffset + this.source.offsetHeight;
+				this.$refs.content.style.transformOrigin = 'center top';
+			}
+
+			this.$el.style.left = left + 'px';
+			this.$el.style.top = top + 'px';
+		});
+	},
+})
+</script>
+
+<style lang="scss" scoped>
+.tooltip-enter-active,
+.tooltip-leave-active {
+	opacity: 1;
+	transform: scale(1);
+	transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), opacity 200ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.tooltip-enter-from,
+.tooltip-leave-active {
+	opacity: 0;
+	transform: scale(0.75);
+}
+
+.buebdbiu {
+	position: absolute;
+	z-index: 11000;
+	font-size: 0.8em;
+	padding: 8px 12px;
+	box-sizing: border-box;
+	text-align: center;
+	border-radius: 4px;
+	border: solid 0.5px var(--divider);
+	pointer-events: none;
+	transform-origin: center bottom;
+}
+</style>
diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
new file mode 100644
index 0000000000..b7093b6641
--- /dev/null
+++ b/packages/client/src/components/ui/window.vue
@@ -0,0 +1,525 @@
+<template>
+<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
+	<div class="ebkgocck" :class="{ front }" v-if="showing">
+		<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
+			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
+				<span class="left">
+					<slot name="headerLeft"></slot>
+				</span>
+				<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
+					<slot name="header"></slot>
+				</span>
+				<span class="right">
+					<slot name="headerRight"></slot>
+					<button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button>
+				</span>
+			</div>
+			<div class="body" v-if="padding">
+				<div class="_section">
+					<slot></slot>
+				</div>
+			</div>
+			<div class="body" v-else>
+				<slot></slot>
+			</div>
+		</div>
+		<template v-if="canResize">
+			<div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div>
+			<div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div>
+			<div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div>
+			<div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div>
+			<div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div>
+			<div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div>
+			<div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div>
+			<div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
+		</template>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import contains from '@/scripts/contains';
+import * as os from '@/os';
+
+const minHeight = 50;
+const minWidth = 250;
+
+function dragListen(fn) {
+	window.addEventListener('mousemove',  fn);
+	window.addEventListener('touchmove',  fn);
+	window.addEventListener('mouseleave', dragClear.bind(null, fn));
+	window.addEventListener('mouseup',    dragClear.bind(null, fn));
+	window.addEventListener('touchend',   dragClear.bind(null, fn));
+}
+
+function dragClear(fn) {
+	window.removeEventListener('mousemove',  fn);
+	window.removeEventListener('touchmove',  fn);
+	window.removeEventListener('mouseleave', dragClear);
+	window.removeEventListener('mouseup',    dragClear);
+	window.removeEventListener('touchend',   dragClear);
+}
+
+export default defineComponent({
+	provide: {
+		inWindow: true
+	},
+
+	props: {
+		padding: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		initialWidth: {
+			type: Number,
+			required: false,
+			default: 400
+		},
+		initialHeight: {
+			type: Number,
+			required: false,
+			default: null
+		},
+		canResize: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		closeButton: {
+			type: Boolean,
+			required: false,
+			default: true,
+		},
+		mini: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		front: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		contextmenu: {
+			type: Array,
+			required: false,
+		}
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			showing: true,
+			id: Math.random().toString(), // TODO: UUIDとかにする
+		};
+	},
+
+	mounted() {
+		if (this.initialWidth) this.applyTransformWidth(this.initialWidth);
+		if (this.initialHeight) this.applyTransformHeight(this.initialHeight);
+
+		this.applyTransformTop((window.innerHeight / 2) - (this.$el.offsetHeight / 2));
+		this.applyTransformLeft((window.innerWidth / 2) - (this.$el.offsetWidth / 2));
+
+		os.windows.set(this.id, {
+			z: Number(document.defaultView.getComputedStyle(this.$el, null).zIndex)
+		});
+
+		// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
+		this.top();
+
+		window.addEventListener('resize', this.onBrowserResize);
+	},
+
+	unmounted() {
+		os.windows.delete(this.id);
+		window.removeEventListener('resize', this.onBrowserResize);
+	},
+
+	methods: {
+		close() {
+			this.showing = false;
+		},
+
+		onKeydown(e) {
+			if (e.which === 27) { // Esc
+				e.preventDefault();
+				e.stopPropagation();
+				this.close();
+			}
+		},
+
+		onContextmenu(e) {
+			if (this.contextmenu) {
+				os.contextMenu(this.contextmenu, e);
+			}
+		},
+
+		// 最前面へ移動
+		top() {
+			let z = 0;
+			const ws = Array.from(os.windows.entries()).filter(([k, v]) => k !== this.id).map(([k, v]) => v);
+			for (const w of ws) {
+				if (w.z > z) z = w.z;
+			}
+			if (z > 0) {
+				(this.$el as any).style.zIndex = z + 1;
+				os.windows.set(this.id, {
+					z: z + 1
+				});
+			}
+		},
+
+		onBodyMousedown() {
+			this.top();
+		},
+
+		onHeaderMousedown(e) {
+			const main = this.$el as any;
+
+			if (!contains(main, document.activeElement)) main.focus();
+
+			const position = main.getBoundingClientRect();
+
+			const clickX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX;
+			const clickY = e.touches && e.touches.length > 0 ? e.touches[0].clientY : e.clientY;
+			const moveBaseX = clickX - position.left;
+			const moveBaseY = clickY - position.top;
+			const browserWidth = window.innerWidth;
+			const browserHeight = window.innerHeight;
+			const windowWidth = main.offsetWidth;
+			const windowHeight = main.offsetHeight;
+
+			// 動かした時
+			dragListen(me => {
+				const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
+				const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
+
+				let moveLeft = x - moveBaseX;
+				let moveTop = y - moveBaseY;
+
+				// 下はみ出し
+				if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
+
+				// 左はみ出し
+				if (moveLeft < 0) moveLeft = 0;
+
+				// 上はみ出し
+				if (moveTop < 0) moveTop = 0;
+
+				// 右はみ出し
+				if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
+
+				this.$el.style.left = moveLeft + 'px';
+				this.$el.style.top = moveTop + 'px';
+			});
+		},
+
+		// 上ハンドル掴み時
+		onTopHandleMousedown(e) {
+			const main = this.$el as any;
+
+			const base = e.clientY;
+			const height = parseInt(getComputedStyle(main, '').height, 10);
+			const top = parseInt(getComputedStyle(main, '').top, 10);
+
+			// 動かした時
+			dragListen(me => {
+				const move = me.clientY - base;
+				if (top + move > 0) {
+					if (height + -move > minHeight) {
+						this.applyTransformHeight(height + -move);
+						this.applyTransformTop(top + move);
+					} else { // 最小の高さより小さくなろうとした時
+						this.applyTransformHeight(minHeight);
+						this.applyTransformTop(top + (height - minHeight));
+					}
+				} else { // 上のはみ出し時
+					this.applyTransformHeight(top + height);
+					this.applyTransformTop(0);
+				}
+			});
+		},
+
+		// 右ハンドル掴み時
+		onRightHandleMousedown(e) {
+			const main = this.$el as any;
+
+			const base = e.clientX;
+			const width = parseInt(getComputedStyle(main, '').width, 10);
+			const left = parseInt(getComputedStyle(main, '').left, 10);
+			const browserWidth = window.innerWidth;
+
+			// 動かした時
+			dragListen(me => {
+				const move = me.clientX - base;
+				if (left + width + move < browserWidth) {
+					if (width + move > minWidth) {
+						this.applyTransformWidth(width + move);
+					} else { // 最小の幅より小さくなろうとした時
+						this.applyTransformWidth(minWidth);
+					}
+				} else { // 右のはみ出し時
+					this.applyTransformWidth(browserWidth - left);
+				}
+			});
+		},
+
+		// 下ハンドル掴み時
+		onBottomHandleMousedown(e) {
+			const main = this.$el as any;
+
+			const base = e.clientY;
+			const height = parseInt(getComputedStyle(main, '').height, 10);
+			const top = parseInt(getComputedStyle(main, '').top, 10);
+			const browserHeight = window.innerHeight;
+
+			// 動かした時
+			dragListen(me => {
+				const move = me.clientY - base;
+				if (top + height + move < browserHeight) {
+					if (height + move > minHeight) {
+						this.applyTransformHeight(height + move);
+					} else { // 最小の高さより小さくなろうとした時
+						this.applyTransformHeight(minHeight);
+					}
+				} else { // 下のはみ出し時
+					this.applyTransformHeight(browserHeight - top);
+				}
+			});
+		},
+
+		// 左ハンドル掴み時
+		onLeftHandleMousedown(e) {
+			const main = this.$el as any;
+
+			const base = e.clientX;
+			const width = parseInt(getComputedStyle(main, '').width, 10);
+			const left = parseInt(getComputedStyle(main, '').left, 10);
+
+			// 動かした時
+			dragListen(me => {
+				const move = me.clientX - base;
+				if (left + move > 0) {
+					if (width + -move > minWidth) {
+						this.applyTransformWidth(width + -move);
+						this.applyTransformLeft(left + move);
+					} else { // 最小の幅より小さくなろうとした時
+						this.applyTransformWidth(minWidth);
+						this.applyTransformLeft(left + (width - minWidth));
+					}
+				} else { // 左のはみ出し時
+					this.applyTransformWidth(left + width);
+					this.applyTransformLeft(0);
+				}
+			});
+		},
+
+		// 左上ハンドル掴み時
+		onTopLeftHandleMousedown(e) {
+			this.onTopHandleMousedown(e);
+			this.onLeftHandleMousedown(e);
+		},
+
+		// 右上ハンドル掴み時
+		onTopRightHandleMousedown(e) {
+			this.onTopHandleMousedown(e);
+			this.onRightHandleMousedown(e);
+		},
+
+		// 右下ハンドル掴み時
+		onBottomRightHandleMousedown(e) {
+			this.onBottomHandleMousedown(e);
+			this.onRightHandleMousedown(e);
+		},
+
+		// 左下ハンドル掴み時
+		onBottomLeftHandleMousedown(e) {
+			this.onBottomHandleMousedown(e);
+			this.onLeftHandleMousedown(e);
+		},
+
+		// 高さを適用
+		applyTransformHeight(height) {
+			if (height > window.innerHeight) height = window.innerHeight;
+			(this.$el as any).style.height = height + 'px';
+		},
+
+		// 幅を適用
+		applyTransformWidth(width) {
+			if (width > window.innerWidth) width = window.innerWidth;
+			(this.$el as any).style.width = width + 'px';
+		},
+
+		// Y座標を適用
+		applyTransformTop(top) {
+			(this.$el as any).style.top = top + 'px';
+		},
+
+		// X座標を適用
+		applyTransformLeft(left) {
+			(this.$el as any).style.left = left + 'px';
+		},
+
+		onBrowserResize() {
+			const main = this.$el as any;
+			const position = main.getBoundingClientRect();
+			const browserWidth = window.innerWidth;
+			const browserHeight = window.innerHeight;
+			const windowWidth = main.offsetWidth;
+			const windowHeight = main.offsetHeight;
+			if (position.left < 0) main.style.left = 0;     // 左はみ出し
+			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px';  // 下はみ出し
+			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px';    // 右はみ出し
+			if (position.top < 0) main.style.top = 0;       // 上はみ出し
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.window-enter-active, .window-leave-active {
+	transition: opacity 0.2s, transform 0.2s !important;
+}
+.window-enter-from, .window-leave-to {
+	pointer-events: none;
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.ebkgocck {
+	position: fixed;
+	top: 0;
+	left: 0;
+	z-index: 10000; // mk-modalのと同じでなければならない
+
+	&.front {
+		z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない
+	}
+
+	> .body {
+		overflow: hidden;
+		display: flex;
+		flex-direction: column;
+		contain: content;
+		width: 100%;
+    height: 100%;
+
+		> .header {
+			--height: 50px;
+
+			&.mini {
+				--height: 38px;
+			}
+
+			display: flex;
+			position: relative;
+			z-index: 1;
+			flex-shrink: 0;
+			user-select: none;
+			height: var(--height);
+			border-bottom: solid 1px var(--divider);
+
+			> .left, > .right {
+				> ::v-deep(button) {
+					height: var(--height);
+					width: var(--height);
+
+					&:hover {
+						color: var(--fgHighlighted);
+					}
+				}
+			}
+
+			> .title {
+				flex: 1;
+				position: relative;
+				line-height: var(--height);
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				text-align: center;
+				cursor: move;
+			}
+		}
+
+		> .body {
+			flex: 1;
+			overflow: auto;
+		}
+	}
+
+	> .handle {
+		$size: 8px;
+
+		position: absolute;
+
+		&.top {
+			top: -($size);
+			left: 0;
+			width: 100%;
+			height: $size;
+			cursor: ns-resize;
+		}
+
+		&.right {
+			top: 0;
+			right: -($size);
+			width: $size;
+			height: 100%;
+			cursor: ew-resize;
+		}
+
+		&.bottom {
+			bottom: -($size);
+			left: 0;
+			width: 100%;
+			height: $size;
+			cursor: ns-resize;
+		}
+
+		&.left {
+			top: 0;
+			left: -($size);
+			width: $size;
+			height: 100%;
+			cursor: ew-resize;
+		}
+
+		&.top-left {
+			top: -($size);
+			left: -($size);
+			width: $size * 2;
+			height: $size * 2;
+			cursor: nwse-resize;
+		}
+
+		&.top-right {
+			top: -($size);
+			right: -($size);
+			width: $size * 2;
+			height: $size * 2;
+			cursor: nesw-resize;
+		}
+
+		&.bottom-right {
+			bottom: -($size);
+			right: -($size);
+			width: $size * 2;
+			height: $size * 2;
+			cursor: nwse-resize;
+		}
+
+		&.bottom-left {
+			bottom: -($size);
+			left: -($size);
+			width: $size * 2;
+			height: $size * 2;
+			cursor: nesw-resize;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/updated.vue b/packages/client/src/components/updated.vue
new file mode 100644
index 0000000000..c021c60669
--- /dev/null
+++ b/packages/client/src/components/updated.vue
@@ -0,0 +1,62 @@
+<template>
+<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
+	<div class="ewlycnyt">
+		<div class="title">{{ $ts.misskeyUpdated }}</div>
+		<div class="version">✨{{ version }}🚀</div>
+		<MkButton full @click="whatIsNew">{{ $ts.whatIsNew }}</MkButton>
+		<MkButton class="gotIt" primary full @click="$refs.modal.close()">{{ $ts.gotIt }}</MkButton>
+	</div>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from '@/components/ui/modal.vue';
+import MkButton from '@/components/ui/button.vue';
+import { version } from '@/config';
+
+export default defineComponent({
+	components: {
+		MkModal,
+		MkButton,
+	},
+
+	data() {
+		return {
+			version: version,
+		};
+	},
+
+	methods: {
+		whatIsNew() {
+			this.$refs.modal.close();
+			window.open(`https://misskey-hub.net/docs/releases.html#_${version.replace(/\./g, '-')}`, '_blank');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ewlycnyt {
+	position: relative;
+	padding: 32px;
+	min-width: 320px;
+	max-width: 480px;
+	box-sizing: border-box;
+	text-align: center;
+	background: var(--panel);
+	border-radius: var(--radius);
+
+	> .title {
+		font-weight: bold;
+	}
+
+	> .version {
+		margin: 1em 0;
+	}
+
+	> .gotIt {
+		margin: 8px 0 0 0;
+	}
+}
+</style>
diff --git a/packages/client/src/components/url-preview-popup.vue b/packages/client/src/components/url-preview-popup.vue
new file mode 100644
index 0000000000..0a402f793f
--- /dev/null
+++ b/packages/client/src/components/url-preview-popup.vue
@@ -0,0 +1,60 @@
+<template>
+<div class="fgmtyycl" :style="{ top: top + 'px', left: left + 'px' }">
+	<transition name="zoom" @after-leave="$emit('closed')">
+		<MkUrlPreview class="_popup _shadow" :url="url" v-if="showing"/>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkUrlPreview from './url-preview.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkUrlPreview
+	},
+
+	props: {
+		url: {
+			type: String,
+			required: true
+		},
+		source: {
+			required: true
+		},
+		showing: {
+			type: Boolean,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			u: null,
+			top: 0,
+			left: 0,
+		};
+	},
+
+	mounted() {
+		const rect = this.source.getBoundingClientRect();
+		const x = Math.max((rect.left + (this.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset;
+		const y = rect.top + this.source.offsetHeight + window.pageYOffset;
+
+		this.top = y;
+		this.left = x;
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.fgmtyycl {
+	position: absolute;
+	z-index: 11000;
+	width: 500px;
+	max-width: calc(90vw - 12px);
+	pointer-events: none;
+}
+</style>
diff --git a/packages/client/src/components/url-preview.vue b/packages/client/src/components/url-preview.vue
new file mode 100644
index 0000000000..0826ba5ccf
--- /dev/null
+++ b/packages/client/src/components/url-preview.vue
@@ -0,0 +1,334 @@
+<template>
+<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
+	<button class="disablePlayer" @click="playerEnabled = false" :title="$ts.disablePlayer"><i class="fas fa-times"></i></button>
+	<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
+</div>
+<div v-else-if="tweetId && tweetExpanded" class="twitter" ref="twitter">
+	<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
+</div>
+<div v-else class="mk-url-preview" v-size="{ max: [400, 350] }">
+	<transition name="zoom" mode="out-in">
+		<component :is="self ? 'MkA' : 'a'" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
+			<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
+				<button class="_button" v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$ts.enablePlayer"><i class="fas fa-play-circle"></i></button>
+			</div>
+			<article>
+				<header>
+					<h1 :title="title">{{ title }}</h1>
+				</header>
+				<p v-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
+				<footer>
+					<img class="icon" v-if="icon" :src="icon"/>
+					<p :title="sitename">{{ sitename }}</p>
+				</footer>
+			</article>
+		</component>
+	</transition>
+	<div class="expandTweet" v-if="tweetId">
+		<a @click="tweetExpanded = true">
+			<i class="fab fa-twitter"></i> {{ $ts.expandTweet }}
+		</a>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { url as local, lang } from '@/config';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		url: {
+			type: String,
+			require: true
+		},
+
+		detail: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+
+		compact: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	data() {
+		const self = this.url.startsWith(local);
+		return {
+			local,
+			fetching: true,
+			title: null,
+			description: null,
+			thumbnail: null,
+			icon: null,
+			sitename: null,
+			player: {
+				url: null,
+				width: null,
+				height: null
+			},
+			tweetId: null,
+			tweetExpanded: this.detail,
+			embedId: `embed${Math.random().toString().replace(/\D/,'')}`,
+			tweetHeight: 150,
+ 			tweetLeft: 0,
+			playerEnabled: false,
+			self: self,
+			attr: self ? 'to' : 'href',
+			target: self ? null : '_blank',
+		};
+	},
+
+	created() {
+		const requestUrl = new URL(this.url);
+
+		if (requestUrl.hostname == 'twitter.com') {
+			const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
+			if (m) this.tweetId = m[1];
+		}
+
+		if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
+			requestUrl.hostname = 'www.youtube.com';
+		}
+
+		const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
+
+		requestUrl.hash = '';
+
+		fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
+			res.json().then(info => {
+				if (info.url == null) return;
+				this.title = info.title;
+				this.description = info.description;
+				this.thumbnail = info.thumbnail;
+				this.icon = info.icon;
+				this.sitename = info.sitename;
+				this.fetching = false;
+				this.player = info.player;
+			})
+		});
+
+		(window as any).addEventListener('message', this.adjustTweetHeight);
+	},
+
+	mounted() {
+		// 300pxないと絶対右にはみ出るので左に移動してしまう
+		const areaWidth = (this.$el as any)?.clientWidth;
+		if (areaWidth && areaWidth < 300) this.tweetLeft = areaWidth - 241;
+	},
+
+	methods: {
+		adjustTweetHeight(message: any) {
+			if (message.origin !== 'https://platform.twitter.com') return;
+			const embed = message.data?.['twttr.embed'];
+			if (embed?.method !== 'twttr.private.resize') return;
+			if (embed?.id !== this.embedId) return;
+			const height = embed?.params[0]?.height;
+			if (height) this.tweetHeight = height;
+ 		},
+	},
+
+	beforeUnmount() {
+		(window as any).removeEventListener('message', this.adjustTweetHeight);
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.player {
+	position: relative;
+	width: 100%;
+
+	> button {
+		position: absolute;
+		top: -1.5em;
+		right: 0;
+		font-size: 1em;
+		width: 1.5em;
+		height: 1.5em;
+		padding: 0;
+		margin: 0;
+		color: var(--fg);
+		background: rgba(128, 128, 128, 0.2);
+		opacity: 0.7;
+
+		&:hover {
+			opacity: 0.9;
+		}
+	}
+
+	> iframe {
+		height: 100%;
+		left: 0;
+		position: absolute;
+		top: 0;
+		width: 100%;
+	}
+}
+
+.mk-url-preview {
+	&.max-width_400px {
+		> a {
+			font-size: 12px;
+
+			> .thumbnail {
+				height: 80px;
+			}
+
+			> article {
+				padding: 12px;
+			}
+		}
+	}
+
+	&.max-width_350px {
+		> a {
+			font-size: 10px;
+
+			> .thumbnail {
+				height: 70px;
+			}
+
+			> article {
+				padding: 8px;
+
+				> header {
+					margin-bottom: 4px;
+				}
+
+				> footer {
+					margin-top: 4px;
+
+					> img {
+						width: 12px;
+						height: 12px;
+					}
+				}
+			}
+
+			&.compact {
+				> .thumbnail {
+					position: absolute;
+					width: 56px;
+					height: 100%;
+				}
+
+				> article {
+					left: 56px;
+					width: calc(100% - 56px);
+					padding: 4px;
+
+					> header {
+						margin-bottom: 2px;
+					}
+
+					> footer {
+						margin-top: 2px;
+					}
+				}
+			}
+		}
+	}
+
+	> a {
+		position: relative;
+		display: block;
+		font-size: 14px;
+		box-shadow: 0 0 0 1px var(--divider);
+		border-radius: 8px;
+		overflow: hidden;
+
+		&:hover {
+			text-decoration: none;
+			border-color: rgba(0, 0, 0, 0.2);
+
+			> article > header > h1 {
+				text-decoration: underline;
+			}
+		}
+
+		> .thumbnail {
+			position: absolute;
+			width: 100px;
+			height: 100%;
+			background-position: center;
+			background-size: cover;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+
+			> button {
+				font-size: 3.5em;
+				opacity: 0.7;
+
+				&:hover {
+					font-size: 4em;
+					opacity: 0.9;
+				}
+			}
+
+			& + article {
+				left: 100px;
+				width: calc(100% - 100px);
+			}
+		}
+
+		> article {
+			position: relative;
+			box-sizing: border-box;
+			padding: 16px;
+
+			> header {
+				margin-bottom: 8px;
+
+				> h1 {
+					margin: 0;
+					font-size: 1em;
+				}
+			}
+
+			> p {
+				margin: 0;
+				font-size: 0.8em;
+			}
+
+			> footer {
+				margin-top: 8px;
+				height: 16px;
+
+				> img {
+					display: inline-block;
+					width: 16px;
+					height: 16px;
+					margin-right: 4px;
+					vertical-align: top;
+				}
+
+				> p {
+					display: inline-block;
+					margin: 0;
+					color: var(--urlPreviewInfo);
+					font-size: 0.8em;
+					line-height: 16px;
+					vertical-align: top;
+				}
+			}
+		}
+
+		&.compact {
+			> article {
+				> header h1, p, footer {
+					overflow: hidden;
+					white-space: nowrap;
+					text-overflow: ellipsis;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/user-info.vue b/packages/client/src/components/user-info.vue
new file mode 100644
index 0000000000..ce82443b84
--- /dev/null
+++ b/packages/client/src/components/user-info.vue
@@ -0,0 +1,142 @@
+<template>
+<div class="_panel vjnjpkug">
+	<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
+	<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
+	<div class="title">
+		<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
+		<p class="username"><MkAcct :user="user"/></p>
+	</div>
+	<div class="description">
+		<div class="mfm" v-if="user.description">
+			<Mfm :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+		</div>
+		<span v-else style="opacity: 0.7;">{{ $ts.noAccountDescription }}</span>
+	</div>
+	<div class="status">
+		<div>
+			<p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span>
+		</div>
+		<div>
+			<p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span>
+		</div>
+		<div>
+			<p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span>
+		</div>
+	</div>
+	<MkFollowButton class="koudoku-button" v-if="$i && user.id != $i.id" :user="user" mini/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkFollowButton from './follow-button.vue';
+import { userPage } from '@/filters/user';
+
+export default defineComponent({
+	components: {
+		MkFollowButton
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	methods: {
+		userPage,
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vjnjpkug {
+	position: relative;
+
+	> .banner {
+		height: 84px;
+		background-color: rgba(0, 0, 0, 0.1);
+		background-size: cover;
+		background-position: center;
+	}
+
+	> .avatar {
+		display: block;
+		position: absolute;
+		top: 62px;
+		left: 13px;
+		z-index: 2;
+		width: 58px;
+		height: 58px;
+		border: solid 4px var(--panel);
+	}
+
+	> .title {
+		display: block;
+		padding: 10px 0 10px 88px;
+
+		> .name {
+			display: inline-block;
+			margin: 0;
+			font-weight: bold;
+			line-height: 16px;
+			word-break: break-all;
+		}
+
+		> .username {
+			display: block;
+			margin: 0;
+			line-height: 16px;
+			font-size: 0.8em;
+			color: var(--fg);
+			opacity: 0.7;
+		}
+	}
+
+	> .description {
+		padding: 16px;
+		font-size: 0.8em;
+		border-top: solid 0.5px var(--divider);
+
+		> .mfm {
+			display: -webkit-box;
+			-webkit-line-clamp: 3;
+			-webkit-box-orient: vertical;  
+			overflow: hidden;
+		}
+	}
+
+	> .status {
+		padding: 10px 16px;
+		border-top: solid 0.5px var(--divider);
+
+		> div {
+			display: inline-block;
+			width: 33%;
+
+			> p {
+				margin: 0;
+				font-size: 0.7em;
+				color: var(--fg);
+			}
+
+			> span {
+				font-size: 1em;
+				color: var(--accent);
+			}
+		}
+	}
+
+	> .koudoku-button {
+		position: absolute;
+		top: 8px;
+		right: 8px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/user-list.vue b/packages/client/src/components/user-list.vue
new file mode 100644
index 0000000000..733dbe0ad7
--- /dev/null
+++ b/packages/client/src/components/user-list.vue
@@ -0,0 +1,91 @@
+<template>
+<MkError v-if="error" @retry="init()"/>
+
+<div v-else class="efvhhmdq _isolated">
+	<div class="no-users" v-if="empty">
+		<p>{{ $ts.noUsers }}</p>
+	</div>
+	<div class="users">
+		<MkUserInfo class="user" v-for="user in users" :user="user" :key="user.id"/>
+	</div>
+	<button class="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :class="{ fetching: moreFetching }" v-show="more" :disabled="moreFetching">
+		<template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ moreFetching ? $ts.loading : $ts.loadMore }}
+	</button>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import paging from '@/scripts/paging';
+import MkUserInfo from './user-info.vue';
+import { userPage } from '@/filters/user';
+
+export default defineComponent({
+	components: {
+		MkUserInfo,
+	},
+
+	mixins: [
+		paging({}),
+	],
+
+	props: {
+		pagination: {
+			required: true
+		},
+		extract: {
+			required: false
+		},
+		expanded: {
+			type: Boolean,
+			default: true
+		},
+	},
+
+	computed: {
+		users() {
+			return this.extract ? this.extract(this.items) : this.items;
+		}
+	},
+
+	methods: {
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.efvhhmdq {
+	> .no-users {
+		text-align: center;
+	}
+
+	> .users {
+		display: grid;
+		grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+		grid-gap: var(--margin);
+	}
+
+	> .more {
+		display: block;
+		width: 100%;
+		padding: 16px;
+
+		&:hover {
+			background: rgba(#000, 0.025);
+		}
+
+		&:active {
+			background: rgba(#000, 0.05);
+		}
+
+		&.fetching {
+			cursor: wait;
+		}
+
+		> i {
+			margin-right: 4px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/user-online-indicator.vue b/packages/client/src/components/user-online-indicator.vue
new file mode 100644
index 0000000000..afaf0e8736
--- /dev/null
+++ b/packages/client/src/components/user-online-indicator.vue
@@ -0,0 +1,50 @@
+<template>
+<div class="fzgwjkgc" :class="user.onlineStatus" v-tooltip="text"></div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+	},
+
+	computed: {
+		text(): string {
+			switch (this.user.onlineStatus) {
+				case 'online': return this.$ts.online;
+				case 'active': return this.$ts.active;
+				case 'offline': return this.$ts.offline;
+				case 'unknown': return this.$ts.unknown;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fzgwjkgc {
+	box-shadow: 0 0 0 3px var(--panel);
+	border-radius: 120%; // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる
+
+	&.online {
+		background: #58d4c9;
+	}
+
+	&.active {
+		background: #e4bc48;
+	}
+
+	&.offline {
+		background: #ea5353;
+	}
+
+	&.unknown {
+		background: #888;
+	}
+}
+</style>
diff --git a/packages/client/src/components/user-preview.vue b/packages/client/src/components/user-preview.vue
new file mode 100644
index 0000000000..f7fd3f6b64
--- /dev/null
+++ b/packages/client/src/components/user-preview.vue
@@ -0,0 +1,192 @@
+<template>
+<transition name="popup" appear @after-leave="$emit('closed')">
+	<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }">
+		<div v-if="fetched" class="info">
+			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
+			<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
+			<div class="title">
+				<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
+				<p class="username"><MkAcct :user="user"/></p>
+			</div>
+			<div class="description">
+				<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+			</div>
+			<div class="status">
+				<div>
+					<p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span>
+				</div>
+				<div>
+					<p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span>
+				</div>
+				<div>
+					<p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span>
+				</div>
+			</div>
+			<MkFollowButton class="koudoku-button" v-if="$i && user.id != $i.id" :user="user" mini/>
+		</div>
+		<div v-else>
+			<MkLoading/>
+		</div>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
+import MkFollowButton from './follow-button.vue';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkFollowButton
+	},
+
+	props: {
+		showing: {
+			type: Boolean,
+			required: true
+		},
+		q: {
+			type: String,
+			required: true
+		},
+		source: {
+			required: true
+		}
+	},
+
+	emits: ['closed', 'mouseover', 'mouseleave'],
+
+	data() {
+		return {
+			user: null,
+			fetched: false,
+			top: 0,
+			left: 0,
+		};
+	},
+
+	mounted() {
+		if (typeof this.q == 'object') {
+			this.user = this.q;
+			this.fetched = true;
+		} else {
+			const query = this.q.startsWith('@') ?
+				Acct.parse(this.q.substr(1)) :
+				{ userId: this.q };
+
+			os.api('users/show', query).then(user => {
+				if (!this.showing) return;
+				this.user = user;
+				this.fetched = true;
+			});
+		}
+
+		const rect = this.source.getBoundingClientRect();
+		const x = ((rect.left + (this.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
+		const y = rect.top + this.source.offsetHeight + window.pageYOffset;
+
+		this.top = y;
+		this.left = x;
+	},
+
+	methods: {
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.popup-enter-active, .popup-leave-active {
+	transition: opacity 0.3s, transform 0.3s !important;
+}
+.popup-enter-from, .popup-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+.fxxzrfni {
+	position: absolute;
+	z-index: 11000;
+	width: 300px;
+	overflow: hidden;
+	transform-origin: center top;
+
+	> .info {
+		> .banner {
+			height: 84px;
+			background-color: rgba(0, 0, 0, 0.1);
+			background-size: cover;
+			background-position: center;
+		}
+
+		> .avatar {
+			display: block;
+			position: absolute;
+			top: 62px;
+			left: 13px;
+			z-index: 2;
+			width: 58px;
+			height: 58px;
+			border: solid 3px var(--face);
+			border-radius: 8px;
+		}
+
+		> .title {
+			display: block;
+			padding: 8px 0 8px 82px;
+
+			> .name {
+				display: inline-block;
+				margin: 0;
+				font-weight: bold;
+				line-height: 16px;
+				word-break: break-all;
+			}
+
+			> .username {
+				display: block;
+				margin: 0;
+				line-height: 16px;
+				font-size: 0.8em;
+				color: var(--fg);
+				opacity: 0.7;
+			}
+		}
+
+		> .description {
+			padding: 0 16px;
+			font-size: 0.8em;
+			color: var(--fg);
+		}
+
+		> .status {
+			padding: 8px 16px;
+
+			> div {
+				display: inline-block;
+				width: 33%;
+
+				> p {
+					margin: 0;
+					font-size: 0.7em;
+					color: var(--fg);
+				}
+
+				> span {
+					font-size: 1em;
+					color: var(--accent);
+				}
+			}
+		}
+
+		> .koudoku-button {
+			position: absolute;
+			top: 8px;
+			right: 8px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/user-select-dialog.vue b/packages/client/src/components/user-select-dialog.vue
new file mode 100644
index 0000000000..80f6293563
--- /dev/null
+++ b/packages/client/src/components/user-select-dialog.vue
@@ -0,0 +1,199 @@
+<template>
+<XModalWindow ref="dialog"
+	:with-ok-button="true"
+	:ok-button-disabled="selected == null"
+	@click="cancel()"
+	@close="cancel()"
+	@ok="ok()"
+	@closed="$emit('closed')"
+>
+	<template #header>{{ $ts.selectUser }}</template>
+	<div class="tbhwbxda _monolithic_">
+		<div class="_section">
+			<div class="_inputSplit">
+				<MkInput v-model="username" class="input" @update:modelValue="search" ref="username">
+					<template #label>{{ $ts.username }}</template>
+					<template #prefix>@</template>
+				</MkInput>
+				<MkInput v-model="host" class="input" @update:modelValue="search">
+					<template #label>{{ $ts.host }}</template>
+					<template #prefix>@</template>
+				</MkInput>
+			</div>
+		</div>
+		<div class="_section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }">
+			<div class="users" v-if="users.length > 0">
+				<div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
+					<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
+					<div class="body">
+						<MkUserName :user="user" class="name"/>
+						<MkAcct :user="user" class="acct"/>
+					</div>
+				</div>
+			</div>
+			<div v-else class="empty">
+				<span>{{ $ts.noUsers }}</span>
+			</div>
+		</div>
+		<div class="_section recent" v-if="username == '' && host == ''">
+			<div class="users">
+				<div class="user" v-for="user in recentUsers" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
+					<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
+					<div class="body">
+						<MkUserName :user="user" class="name"/>
+						<MkAcct :user="user" class="acct"/>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkInput from './form/input.vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkInput,
+		XModalWindow,
+	},
+
+	props: {
+	},
+
+	emits: ['ok', 'cancel', 'closed'],
+
+	data() {
+		return {
+			username: '',
+			host: '',
+			recentUsers: [],
+			users: [],
+			selected: null,
+		};
+	},
+
+	async mounted() {
+		this.focus();
+
+		this.$nextTick(() => {
+			this.focus();
+		});
+
+		this.recentUsers = await os.api('users/show', {
+			userIds: this.$store.state.recentlyUsedUsers
+		});
+	},
+
+	methods: {
+		search() {
+			if (this.username == '' && this.host == '') {
+				this.users = [];
+				return;
+			}
+			os.api('users/search-by-username-and-host', {
+				username: this.username,
+				host: this.host,
+				limit: 10,
+				detail: false
+			}).then(users => {
+				this.users = users;
+			});
+		},
+
+		focus() {
+			this.$refs.username.focus();
+		},
+
+		ok() {
+			this.$emit('ok', this.selected);
+			this.$refs.dialog.close();
+
+			// 最近使ったユーザー更新
+			let recents = this.$store.state.recentlyUsedUsers;
+			recents = recents.filter(x => x !== this.selected.id);
+			recents.unshift(this.selected.id);
+			this.$store.set('recentlyUsedUsers', recents.splice(0, 16));
+		},
+
+		cancel() {
+			this.$emit('cancel');
+			this.$refs.dialog.close();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.tbhwbxda {
+	> ._section {
+		display: flex;
+		flex-direction: column;
+		overflow: auto;
+		height: 100%;
+
+		&.result.hit {
+			padding: 0;
+		}
+
+		&.recent {
+			padding: 0;
+		}
+
+		> .users {
+			flex: 1;
+			overflow: auto;
+			padding: 8px 0;
+
+			> .user {
+				display: flex;
+				align-items: center;
+				padding: 8px var(--root-margin);
+				font-size: 14px;
+
+				&:hover {
+					background: var(--X7);
+				}
+
+				&.selected {
+					background: var(--accent);
+					color: #fff;
+				}
+
+				> * {
+					pointer-events: none;
+					user-select: none;
+				}
+
+				> .avatar {
+					width: 45px;
+					height: 45px;
+				}
+
+				> .body {
+					padding: 0 8px;
+					min-width: 0;
+
+					> .name {
+						display: block;
+						font-weight: bold;
+					}
+
+					> .acct {
+						opacity: 0.5;
+					}
+				}
+			}
+		}
+
+		> .empty {
+			opacity: 0.7;
+			text-align: center;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/users-dialog.vue b/packages/client/src/components/users-dialog.vue
new file mode 100644
index 0000000000..6eec5289b3
--- /dev/null
+++ b/packages/client/src/components/users-dialog.vue
@@ -0,0 +1,147 @@
+<template>
+<div class="mk-users-dialog">
+	<div class="header">
+		<span>{{ title }}</span>
+		<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
+	</div>
+
+	<div class="users">
+		<MkA v-for="item in items" class="user" :key="item.id" :to="userPage(extract ? extract(item) : item)">
+			<MkAvatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true" :show-indicator="true"/>
+			<div class="body">
+				<MkUserName :user="extract ? extract(item) : item" class="name"/>
+				<MkAcct :user="extract ? extract(item) : item" class="acct"/>
+			</div>
+		</MkA>
+	</div>
+	<button class="more _button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching">
+		<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+		<template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>
+	</button>
+
+	<p class="empty" v-if="empty">{{ $ts.noUsers }}</p>
+
+	<MkError v-if="error" @retry="init()"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import paging from '@/scripts/paging';
+import { userPage } from '@/filters/user';
+
+export default defineComponent({
+	mixins: [
+		paging({}),
+	],
+
+	props: {
+		title: {
+			required: true
+		},
+		pagination: {
+			required: true
+		},
+		extract: {
+			required: false
+		}
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	methods: {
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-users-dialog {
+	width: 350px;
+	height: 350px;
+	background: var(--panel);
+	border-radius: var(--radius);
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+
+	> .header {
+		display: flex;
+		flex-shrink: 0;
+
+		> button {
+			height: 58px;
+			width: 58px;
+
+			@media (max-width: 500px) {
+				height: 42px;
+				width: 42px;
+			}
+		}
+
+		> span {
+			flex: 1;
+			line-height: 58px;
+			padding-left: 32px;
+			font-weight: bold;
+
+			@media (max-width: 500px) {
+				line-height: 42px;
+				padding-left: 16px;
+			}
+		}
+	}
+
+	> .users {
+		flex: 1;
+		overflow: auto;
+
+		&:empty {
+			display: none;
+		}
+
+		> .user {
+			display: flex;
+			align-items: center;
+			font-size: 14px;
+			padding: 8px 32px;
+
+			@media (max-width: 500px) {
+				padding: 8px 16px;
+			}
+
+			> * {
+				pointer-events: none;
+				user-select: none;
+			}
+
+			> .avatar {
+				width: 45px;
+				height: 45px;
+			}
+
+			> .body {
+				padding: 0 8px;
+				overflow: hidden;
+
+				> .name {
+					display: block;
+					font-weight: bold;
+				}
+
+				> .acct {
+					opacity: 0.5;
+				}
+			}
+		}
+	}
+
+	> .empty {
+		text-align: center;
+		opacity: 0.5;
+	}
+}
+</style>
diff --git a/packages/client/src/components/visibility-picker.vue b/packages/client/src/components/visibility-picker.vue
new file mode 100644
index 0000000000..7a811b42f7
--- /dev/null
+++ b/packages/client/src/components/visibility-picker.vue
@@ -0,0 +1,167 @@
+<template>
+<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
+	<div class="gqyayizv _popup">
+		<button class="_button" @click="choose('public')" :class="{ active: v == 'public' }" data-index="1" key="public">
+			<div><i class="fas fa-globe"></i></div>
+			<div>
+				<span>{{ $ts._visibility.public }}</span>
+				<span>{{ $ts._visibility.publicDescription }}</span>
+			</div>
+		</button>
+		<button class="_button" @click="choose('home')" :class="{ active: v == 'home' }" data-index="2" key="home">
+			<div><i class="fas fa-home"></i></div>
+			<div>
+				<span>{{ $ts._visibility.home }}</span>
+				<span>{{ $ts._visibility.homeDescription }}</span>
+			</div>
+		</button>
+		<button class="_button" @click="choose('followers')" :class="{ active: v == 'followers' }" data-index="3" key="followers">
+			<div><i class="fas fa-unlock"></i></div>
+			<div>
+				<span>{{ $ts._visibility.followers }}</span>
+				<span>{{ $ts._visibility.followersDescription }}</span>
+			</div>
+		</button>
+		<button :disabled="localOnly" class="_button" @click="choose('specified')" :class="{ active: v == 'specified' }" data-index="4" key="specified">
+			<div><i class="fas fa-envelope"></i></div>
+			<div>
+				<span>{{ $ts._visibility.specified }}</span>
+				<span>{{ $ts._visibility.specifiedDescription }}</span>
+			</div>
+		</button>
+		<div class="divider"></div>
+		<button class="_button localOnly" @click="localOnly = !localOnly" :class="{ active: localOnly }" data-index="5" key="localOnly">
+			<div><i class="fas fa-biohazard"></i></div>
+			<div>
+				<span>{{ $ts._visibility.localOnly }}</span>
+				<span>{{ $ts._visibility.localOnlyDescription }}</span>
+			</div>
+			<div><i :class="localOnly ? 'fas fa-toggle-on' : 'fas fa-toggle-off'"></i></div>
+		</button>
+	</div>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from '@/components/ui/modal.vue';
+
+export default defineComponent({
+	components: {
+		MkModal,
+	},
+	props: {
+		currentVisibility: {
+			type: String,
+			required: true
+		},
+		currentLocalOnly: {
+			type: Boolean,
+			required: true
+		},
+		src: {
+			required: false
+		},
+	},
+	emits: ['change-visibility', 'change-local-only', 'closed'],
+	data() {
+		return {
+			v: this.currentVisibility,
+			localOnly: this.currentLocalOnly,
+		}
+	},
+	watch: {
+		localOnly() {
+			this.$emit('change-local-only', this.localOnly);
+		}
+	},
+	methods: {
+		choose(visibility) {
+			this.v = visibility;
+			this.$emit('change-visibility', visibility);
+			this.$nextTick(() => {
+				this.$refs.modal.close();
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.gqyayizv {
+	width: 240px;
+	padding: 8px 0;
+
+	> .divider {
+		margin: 8px 0;
+		border-top: solid 0.5px var(--divider);
+	}
+
+	> button {
+		display: flex;
+		padding: 8px 14px;
+		font-size: 12px;
+		text-align: left;
+		width: 100%;
+		box-sizing: border-box;
+
+		&:hover {
+			background: rgba(0, 0, 0, 0.05);
+		}
+
+		&:active {
+			background: rgba(0, 0, 0, 0.1);
+		}
+
+		&.active {
+			color: #fff;
+			background: var(--accent);
+		}
+
+		&.localOnly.active {
+			color: var(--accent);
+			background: inherit;
+		}
+
+		> *:nth-child(1) {
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			margin-right: 10px;
+			width: 16px;
+			top: 0;
+			bottom: 0;
+			margin-top: auto;
+			margin-bottom: auto;
+		}
+
+		> *:nth-child(2) {
+			flex: 1 1 auto;
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+
+			> span:first-child {
+				display: block;
+				font-weight: bold;
+			}
+
+			> span:last-child:not(:first-child) {
+				opacity: 0.6;
+			}
+		}
+
+		> *:nth-child(3) {
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			margin-left: 10px;
+			width: 16px;
+			top: 0;
+			bottom: 0;
+			margin-top: auto;
+			margin-bottom: auto;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/components/waiting-dialog.vue b/packages/client/src/components/waiting-dialog.vue
new file mode 100644
index 0000000000..35a760ea41
--- /dev/null
+++ b/packages/client/src/components/waiting-dialog.vue
@@ -0,0 +1,92 @@
+<template>
+<MkModal ref="modal" @click="success ? done() : () => {}" @closed="$emit('closed')">
+	<div class="iuyakobc" :class="{ iconOnly: (text == null) || success }">
+		<i v-if="success" class="fas fa-check icon success"></i>
+		<i v-else class="fas fa-spinner fa-pulse icon waiting"></i>
+		<div class="text" v-if="text && !success">{{ text }}<MkEllipsis/></div>
+	</div>
+</MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from '@/components/ui/modal.vue';
+
+export default defineComponent({
+	components: {
+		MkModal,
+	},
+
+	props: {
+		success: {
+			type: Boolean,
+			required: true,
+		},
+		showing: {
+			type: Boolean,
+			required: true,
+		},
+		text: {
+			type: String,
+			required: false,
+		},
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+		};
+	},
+
+	watch: {
+		showing() {
+			if (!this.showing) this.done();
+		}
+	},
+
+	methods: {
+		done() {
+			this.$emit('done');
+			this.$refs.modal.close();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.iuyakobc {
+	position: relative;
+	padding: 32px;
+	box-sizing: border-box;
+	text-align: center;
+	background: var(--panel);
+	border-radius: var(--radius);
+	width: 250px;
+
+	&.iconOnly {
+		padding: 0;
+		width: 96px;
+		height: 96px;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	> .icon {
+		font-size: 32px;
+
+		&.success {
+			color: var(--accent);
+		}
+
+		&.waiting {
+			opacity: 0.7;
+		}
+	}
+
+	> .text {
+		margin-top: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/components/widgets.vue b/packages/client/src/components/widgets.vue
new file mode 100644
index 0000000000..8aec77796d
--- /dev/null
+++ b/packages/client/src/components/widgets.vue
@@ -0,0 +1,152 @@
+<template>
+<div class="vjoppmmu">
+	<template v-if="edit">
+		<header>
+			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
+				<template #label>{{ $ts.selectWidget }}</template>
+				<option v-for="widget in widgetDefs" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
+			</MkSelect>
+			<MkButton inline @click="addWidget" primary><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
+			<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
+		</header>
+		<XDraggable
+			v-model="_widgets"
+			item-key="id"
+			animation="150"
+		>
+			<template #item="{element}">
+				<div class="customize-container">
+					<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
+					<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
+					<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" @updateProps="updateWidget(element.id, $event)"/>
+				</div>
+			</template>
+		</XDraggable>
+	</template>
+	<component v-else class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" @updateProps="updateWidget(widget.id, $event)"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import MkSelect from '@/components/form/select.vue';
+import MkButton from '@/components/ui/button.vue';
+import { widgets as widgetDefs } from '@/widgets';
+
+export default defineComponent({
+	components: {
+		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
+		MkSelect,
+		MkButton,
+	},
+
+	props: {
+		widgets: {
+			type: Array,
+			required: true,
+		},
+		edit: {
+			type: Boolean,
+			required: true,
+		},
+	},
+
+	emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
+
+	data() {
+		return {
+			widgetAdderSelected: null,
+			widgetDefs,
+			settings: {},
+		};
+	},
+
+	computed: {
+		_widgets: {
+			get() {
+				return this.widgets;
+			},
+			set(value) {
+				this.$emit('updateWidgets', value);
+			}
+		}
+	},
+
+	methods: {
+		configWidget(id) {
+			this.settings[id]();
+		},
+
+		addWidget() {
+			if (this.widgetAdderSelected == null) return;
+
+			this.$emit('addWidget', {
+				name: this.widgetAdderSelected,
+				id: uuid(),
+				data: {}
+			});
+
+			this.widgetAdderSelected = null;
+		},
+
+		removeWidget(widget) {
+			this.$emit('removeWidget', widget);
+		},
+
+		updateWidget(id, data) {
+			this.$emit('updateWidget', { id, data });
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vjoppmmu {
+	> header {
+		margin: 16px 0;
+
+		> * {
+			width: 100%;
+			padding: 4px;
+		}
+	}
+
+	> .widget, .customize-container {
+		margin: var(--margin) 0;
+
+		&:first-of-type {
+			margin-top: 0;
+		}
+	}
+
+	.customize-container {
+		position: relative;
+		cursor: move;
+
+		> *:not(.remove):not(.config) {
+			pointer-events: none;
+		}
+
+		> .config,
+		> .remove {
+			position: absolute;
+			z-index: 10000;
+			top: 8px;
+			width: 32px;
+			height: 32px;
+			color: #fff;
+			background: rgba(#000, 0.7);
+			border-radius: 4px;
+		}
+
+		> .config {
+			right: 8px + 8px + 32px;
+		}
+
+		> .remove {
+			right: 8px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts
new file mode 100644
index 0000000000..f2022b0f02
--- /dev/null
+++ b/packages/client/src/config.ts
@@ -0,0 +1,15 @@
+const address = new URL(location.href);
+const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content;
+
+export const host = address.host;
+export const hostname = address.hostname;
+export const url = address.origin;
+export const apiUrl = url + '/api';
+export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
+export const lang = localStorage.getItem('lang');
+export const langs = _LANGS_;
+export const locale = JSON.parse(localStorage.getItem('locale'));
+export const version = _VERSION_;
+export const instanceName = siteName === 'Misskey' ? host : siteName;
+export const ui = localStorage.getItem('ui');
+export const debug = localStorage.getItem('debug') === 'true';
diff --git a/packages/client/src/directives/anim.ts b/packages/client/src/directives/anim.ts
new file mode 100644
index 0000000000..1ceef984d8
--- /dev/null
+++ b/packages/client/src/directives/anim.ts
@@ -0,0 +1,18 @@
+import { Directive } from 'vue';
+
+export default {
+	beforeMount(src, binding, vn) {
+		src.style.opacity = '0';
+		src.style.transform = 'scale(0.9)';
+		// ページネーションと相性が悪いので
+		//if (typeof binding.value === 'number') src.style.transitionDelay = `${binding.value * 30}ms`;
+		src.classList.add('_zoom');
+	},
+
+	mounted(src, binding, vn) {
+		setTimeout(() => {
+			src.style.opacity = '1';
+			src.style.transform = 'none';
+		}, 1);
+	},
+} as Directive;
diff --git a/packages/client/src/directives/appear.ts b/packages/client/src/directives/appear.ts
new file mode 100644
index 0000000000..a504d11ef9
--- /dev/null
+++ b/packages/client/src/directives/appear.ts
@@ -0,0 +1,22 @@
+import { Directive } from 'vue';
+
+export default {
+	mounted(src, binding, vn) {
+		const fn = binding.value;
+		if (fn == null) return;
+
+		const observer = new IntersectionObserver(entries => {
+			if (entries.some(entry => entry.isIntersecting)) {
+				fn();
+			}
+		});
+
+		observer.observe(src);
+
+		src._observer_ = observer;
+	},
+
+	unmounted(src, binding, vn) {
+		if (src._observer_) src._observer_.disconnect();
+	}
+} as Directive;
diff --git a/packages/client/src/directives/click-anime.ts b/packages/client/src/directives/click-anime.ts
new file mode 100644
index 0000000000..001dcca46e
--- /dev/null
+++ b/packages/client/src/directives/click-anime.ts
@@ -0,0 +1,29 @@
+import { Directive } from 'vue';
+import { defaultStore } from '@/store';
+
+export default {
+	mounted(el, binding, vn) {
+		if (!defaultStore.state.animation) return;
+
+		el.classList.add('_anime_bounce_standBy');
+
+		el.addEventListener('mousedown', () => {
+			el.classList.add('_anime_bounce_standBy');
+			el.classList.add('_anime_bounce_ready');
+
+			el.addEventListener('mouseleave', () => {
+				el.classList.remove('_anime_bounce_ready');
+			});
+		});
+
+		el.addEventListener('click', () => {
+			el.classList.add('_anime_bounce');
+		});
+
+		el.addEventListener('animationend', () => {
+			el.classList.remove('_anime_bounce_ready');
+			el.classList.remove('_anime_bounce');
+			el.classList.add('_anime_bounce_standBy');
+		});
+	}
+} as Directive;
diff --git a/packages/client/src/directives/follow-append.ts b/packages/client/src/directives/follow-append.ts
new file mode 100644
index 0000000000..b0e99628b0
--- /dev/null
+++ b/packages/client/src/directives/follow-append.ts
@@ -0,0 +1,35 @@
+import { Directive } from 'vue';
+import { getScrollContainer, getScrollPosition } from '@/scripts/scroll';
+
+export default {
+	mounted(src, binding, vn) {
+		if (binding.value === false) return;
+
+		let isBottom = true;
+
+		const container = getScrollContainer(src)!;
+		container.addEventListener('scroll', () => {
+			const pos = getScrollPosition(container);
+			const viewHeight = container.clientHeight;
+			const height = container.scrollHeight;
+			isBottom = (pos + viewHeight > height - 32);
+		}, { passive: true });
+		container.scrollTop = container.scrollHeight;
+
+		const ro = new ResizeObserver((entries, observer) => {
+			if (isBottom) {
+				const height = container.scrollHeight;
+				container.scrollTop = height;
+			}
+		});
+
+		ro.observe(src);
+
+		// TODO: 新たにプロパティを作るのをやめMapを使う
+		src._ro_ = ro;
+	},
+
+	unmounted(src, binding, vn) {
+		if (src._ro_) src._ro_.unobserve(src);
+	}
+} as Directive;
diff --git a/packages/client/src/directives/get-size.ts b/packages/client/src/directives/get-size.ts
new file mode 100644
index 0000000000..e3b5dea0f3
--- /dev/null
+++ b/packages/client/src/directives/get-size.ts
@@ -0,0 +1,34 @@
+import { Directive } from 'vue';
+
+export default {
+	mounted(src, binding, vn) {
+		const calc = () => {
+			const height = src.clientHeight;
+			const width = src.clientWidth;
+
+			// 要素が(一時的に)DOMに存在しないときは計算スキップ
+			if (height === 0) return;
+
+			binding.value(width, height);
+		};
+
+		calc();
+
+		// Vue3では使えなくなった
+		// 無くても大丈夫か...？
+		// TODO: ↑大丈夫じゃなかったので解決策を探す
+		//vn.context.$on('hook:activated', calc);
+
+		const ro = new ResizeObserver((entries, observer) => {
+			calc();
+		});
+		ro.observe(src);
+
+		src._get_size_ro_ = ro;
+	},
+
+	unmounted(src, binding, vn) {
+		binding.value(0, 0);
+		src._get_size_ro_.unobserve(src);
+	}
+} as Directive;
diff --git a/packages/client/src/directives/hotkey.ts b/packages/client/src/directives/hotkey.ts
new file mode 100644
index 0000000000..d813a95074
--- /dev/null
+++ b/packages/client/src/directives/hotkey.ts
@@ -0,0 +1,24 @@
+import { Directive } from 'vue';
+import { makeHotkey } from '../scripts/hotkey';
+
+export default {
+	mounted(el, binding) {
+		el._hotkey_global = binding.modifiers.global === true;
+
+		el._keyHandler = makeHotkey(binding.value);
+
+		if (el._hotkey_global) {
+			document.addEventListener('keydown', el._keyHandler);
+		} else {
+			el.addEventListener('keydown', el._keyHandler);
+		}
+	},
+
+	unmounted(el) {
+		if (el._hotkey_global) {
+			document.removeEventListener('keydown', el._keyHandler);
+		} else {
+			el.removeEventListener('keydown', el._keyHandler);
+		}
+	}
+} as Directive;
diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts
new file mode 100644
index 0000000000..cd71bc26d3
--- /dev/null
+++ b/packages/client/src/directives/index.ts
@@ -0,0 +1,26 @@
+import { App } from 'vue';
+
+import userPreview from './user-preview';
+import size from './size';
+import getSize from './get-size';
+import particle from './particle';
+import tooltip from './tooltip';
+import hotkey from './hotkey';
+import appear from './appear';
+import anim from './anim';
+import stickyContainer from './sticky-container';
+import clickAnime from './click-anime';
+
+export default function(app: App) {
+	app.directive('userPreview', userPreview);
+	app.directive('user-preview', userPreview);
+	app.directive('size', size);
+	app.directive('get-size', getSize);
+	app.directive('particle', particle);
+	app.directive('tooltip', tooltip);
+	app.directive('hotkey', hotkey);
+	app.directive('appear', appear);
+	app.directive('anim', anim);
+	app.directive('click-anime', clickAnime);
+	app.directive('sticky-container', stickyContainer);
+}
diff --git a/packages/client/src/directives/particle.ts b/packages/client/src/directives/particle.ts
new file mode 100644
index 0000000000..c90df89a5e
--- /dev/null
+++ b/packages/client/src/directives/particle.ts
@@ -0,0 +1,18 @@
+import Particle from '@/components/particle.vue';
+import { popup } from '@/os';
+
+export default {
+	mounted(el, binding, vn) {
+		// 明示的に false であればバインドしない
+		if (binding.value === false) return;
+
+		el.addEventListener('click', () => {
+			const rect = el.getBoundingClientRect();
+
+			const x = rect.left + (el.clientWidth / 2);
+			const y = rect.top + (el.clientHeight / 2);
+
+			popup(Particle, { x, y }, {}, 'end');
+		});
+	}
+};
diff --git a/packages/client/src/directives/size.ts b/packages/client/src/directives/size.ts
new file mode 100644
index 0000000000..a72a97abcc
--- /dev/null
+++ b/packages/client/src/directives/size.ts
@@ -0,0 +1,68 @@
+import { Directive } from 'vue';
+
+//const observers = new Map<Element, ResizeObserver>();
+
+export default {
+	mounted(src, binding, vn) {
+		const query = binding.value;
+
+		const addClass = (el: Element, cls: string) => {
+			el.classList.add(cls);
+		};
+
+		const removeClass = (el: Element, cls: string) => {
+			el.classList.remove(cls);
+		};
+
+		const calc = () => {
+			const width = src.clientWidth;
+
+			// 要素が(一時的に)DOMに存在しないときは計算スキップ
+			if (width === 0) return;
+
+			if (query.max) {
+				for (const v of query.max) {
+					if (width <= v) {
+						addClass(src, 'max-width_' + v + 'px');
+					} else {
+						removeClass(src, 'max-width_' + v + 'px');
+					}
+				}
+			}
+			if (query.min) {
+				for (const v of query.min) {
+					if (width >= v) {
+						addClass(src, 'min-width_' + v + 'px');
+					} else {
+						removeClass(src, 'min-width_' + v + 'px');
+					}
+				}
+			}
+		};
+
+		calc();
+
+		window.addEventListener('resize', calc);
+
+		// Vue3では使えなくなった
+		// 無くても大丈夫か...？
+		// TODO: ↑大丈夫じゃなかったので解決策を探す
+		//vn.context.$on('hook:activated', calc);
+
+		//const ro = new ResizeObserver((entries, observer) => {
+		//	calc();
+		//});
+
+		//ro.observe(el);
+
+		// TODO: 新たにプロパティを作るのをやめMapを使う
+		// ただメモリ的には↓の方が省メモリかもしれないので検討中
+		//el._ro_ = ro;
+		src._calc_ = calc;
+	},
+
+	unmounted(src, binding, vn) {
+		//el._ro_.unobserve(el);
+		window.removeEventListener('resize', src._calc_);
+	}
+} as Directive;
diff --git a/packages/client/src/directives/sticky-container.ts b/packages/client/src/directives/sticky-container.ts
new file mode 100644
index 0000000000..9610eba4da
--- /dev/null
+++ b/packages/client/src/directives/sticky-container.ts
@@ -0,0 +1,15 @@
+import { Directive } from 'vue';
+
+export default {
+	mounted(src, binding, vn) {
+		//const query = binding.value;
+
+		const header = src.children[0];
+		const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px';
+		src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
+		header.style.setProperty('--stickyTop', currentStickyTop);
+		header.style.position = 'sticky';
+		header.style.top = 'var(--stickyTop)';
+		header.style.zIndex = '1';
+	},
+} as Directive;
diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
new file mode 100644
index 0000000000..1294f6b063
--- /dev/null
+++ b/packages/client/src/directives/tooltip.ts
@@ -0,0 +1,87 @@
+import { Directive, ref } from 'vue';
+import { isDeviceTouch } from '@/scripts/is-device-touch';
+import { popup, dialog } from '@/os';
+
+const start = isDeviceTouch ? 'touchstart' : 'mouseover';
+const end = isDeviceTouch ? 'touchend' : 'mouseleave';
+const delay = 100;
+
+export default {
+	mounted(el: HTMLElement, binding, vn) {
+		const self = (el as any)._tooltipDirective_ = {} as any;
+
+		self.text = binding.value as string;
+		self._close = null;
+		self.showTimer = null;
+		self.hideTimer = null;
+		self.checkTimer = null;
+
+		self.close = () => {
+			if (self._close) {
+				clearInterval(self.checkTimer);
+				self._close();
+				self._close = null;
+			}
+		};
+
+		if (binding.arg === 'dialog') {
+			el.addEventListener('click', (ev) => {
+				ev.preventDefault();
+				ev.stopPropagation();
+				dialog({
+					type: 'info',
+					text: binding.value,
+				});
+				return false;
+			});
+		}
+
+		self.show = () => {
+			if (!document.body.contains(el)) return;
+			if (self._close) return;
+			if (self.text == null) return;
+
+			const showing = ref(true);
+			popup(import('@/components/ui/tooltip.vue'), {
+				showing,
+				text: self.text,
+				source: el
+			}, {}, 'closed');
+
+			self._close = () => {
+				showing.value = false;
+			};
+		};
+
+		el.addEventListener('selectstart', e => {
+			e.preventDefault();
+		});
+
+		el.addEventListener(start, () => {
+			clearTimeout(self.showTimer);
+			clearTimeout(self.hideTimer);
+			self.showTimer = setTimeout(self.show, delay);
+		}, { passive: true });
+
+		el.addEventListener(end, () => {
+			clearTimeout(self.showTimer);
+			clearTimeout(self.hideTimer);
+			self.hideTimer = setTimeout(self.close, delay);
+		}, { passive: true });
+
+		el.addEventListener('click', () => {
+			clearTimeout(self.showTimer);
+			self.close();
+		});
+	},
+
+	updated(el, binding) {
+		const self = el._tooltipDirective_;
+		self.text = binding.value as string;
+	},
+
+	unmounted(el, binding, vn) {
+		const self = el._tooltipDirective_;
+		clearInterval(self.checkTimer);
+	},
+} as Directive;
diff --git a/packages/client/src/directives/user-preview.ts b/packages/client/src/directives/user-preview.ts
new file mode 100644
index 0000000000..68d9e2816c
--- /dev/null
+++ b/packages/client/src/directives/user-preview.ts
@@ -0,0 +1,118 @@
+import { Directive, ref } from 'vue';
+import autobind from 'autobind-decorator';
+import { popup } from '@/os';
+
+export class UserPreview {
+	private el;
+	private user;
+	private showTimer;
+	private hideTimer;
+	private checkTimer;
+	private promise;
+
+	constructor(el, user) {
+		this.el = el;
+		this.user = user;
+
+		this.attach();
+	}
+
+	@autobind
+	private show() {
+		if (!document.body.contains(this.el)) return;
+		if (this.promise) return;
+
+		const showing = ref(true);
+
+		popup(import('@/components/user-preview.vue'), {
+			showing,
+			q: this.user,
+			source: this.el
+		}, {
+			mouseover: () => {
+				clearTimeout(this.hideTimer);
+			},
+			mouseleave: () => {
+				clearTimeout(this.showTimer);
+				this.hideTimer = setTimeout(this.close, 500);
+			},
+		}, 'closed');
+
+		this.promise = {
+			cancel: () => {
+				showing.value = false;
+			}
+		};
+
+		this.checkTimer = setInterval(() => {
+			if (!document.body.contains(this.el)) {
+				clearTimeout(this.showTimer);
+				clearTimeout(this.hideTimer);
+				this.close();
+			}
+		}, 1000);
+	}
+
+	@autobind
+	private close() {
+		if (this.promise) {
+			clearInterval(this.checkTimer);
+			this.promise.cancel();
+			this.promise = null;
+		}
+	}
+
+	@autobind
+	private onMouseover() {
+		clearTimeout(this.showTimer);
+		clearTimeout(this.hideTimer);
+		this.showTimer = setTimeout(this.show, 500);
+	}
+
+	@autobind
+	private onMouseleave() {
+		clearTimeout(this.showTimer);
+		clearTimeout(this.hideTimer);
+		this.hideTimer = setTimeout(this.close, 500);
+	}
+
+	@autobind
+	private onClick() {
+		clearTimeout(this.showTimer);
+		this.close();
+	}
+
+	@autobind
+	public attach() {
+		this.el.addEventListener('mouseover', this.onMouseover);
+		this.el.addEventListener('mouseleave', this.onMouseleave);
+		this.el.addEventListener('click', this.onClick);
+	}
+
+	@autobind
+	public detach() {
+		this.el.removeEventListener('mouseover', this.onMouseover);
+		this.el.removeEventListener('mouseleave', this.onMouseleave);
+		this.el.removeEventListener('click', this.onClick);
+		clearInterval(this.checkTimer);
+	}
+}
+
+export default {
+	mounted(el: HTMLElement, binding, vn) {
+		if (binding.value == null) return;
+
+		// TODO: 新たにプロパティを作るのをやめMapを使う
+		// ただメモリ的には↓の方が省メモリかもしれないので検討中
+		const self = (el as any)._userPreviewDirective_ = {} as any;
+
+		self.preview = new UserPreview(el, binding.value);
+	},
+
+	unmounted(el, binding, vn) {
+		if (binding.value == null) return;
+
+		const self = el._userPreviewDirective_;
+		self.preview.detach();
+	}
+} as Directive;
diff --git a/packages/client/src/emojilist.json b/packages/client/src/emojilist.json
new file mode 100644
index 0000000000..75c424ab4b
--- /dev/null
+++ b/packages/client/src/emojilist.json
@@ -0,0 +1,1749 @@
+[
+	{ "category": "face", "char": "😀", "name": "grinning", "keywords": ["face", "smile", "happy", "joy", ": D", "grin"] },
+	{ "category": "face", "char": "😬", "name": "grimacing", "keywords": ["face", "grimace", "teeth"] },
+	{ "category": "face", "char": "😁", "name": "grin", "keywords": ["face", "happy", "smile", "joy", "kawaii"] },
+	{ "category": "face", "char": "😂", "name": "joy", "keywords": ["face", "cry", "tears", "weep", "happy", "happytears", "haha"] },
+	{ "category": "face", "char": "🤣", "name": "rofl", "keywords": ["face", "rolling", "floor", "laughing", "lol", "haha"] },
+	{ "category": "face", "char": "🥳", "name": "partying", "keywords": ["face", "celebration", "woohoo"] },
+	{ "category": "face", "char": "😃", "name": "smiley", "keywords": ["face", "happy", "joy", "haha", ": D", ": )", "smile", "funny"] },
+	{ "category": "face", "char": "😄", "name": "smile", "keywords": ["face", "happy", "joy", "funny", "haha", "laugh", "like", ": D", ": )"] },
+	{ "category": "face", "char": "😅", "name": "sweat_smile", "keywords": ["face", "hot", "happy", "laugh", "sweat", "smile", "relief"] },
+	{ "category": "face", "char": "🥲", "name": "smiling_face_with_tear", "keywords": ["face"] },
+	{ "category": "face", "char": "😆", "name": "laughing", "keywords": ["happy", "joy", "lol", "satisfied", "haha", "face", "glad", "XD", "laugh"] },
+	{ "category": "face", "char": "😇", "name": "innocent", "keywords": ["face", "angel", "heaven", "halo"] },
+	{ "category": "face", "char": "😉", "name": "wink", "keywords": ["face", "happy", "mischievous", "secret", ";)", "smile", "eye"] },
+	{ "category": "face", "char": "😊", "name": "blush", "keywords": ["face", "smile", "happy", "flushed", "crush", "embarrassed", "shy", "joy"] },
+	{ "category": "face", "char": "🙂", "name": "slightly_smiling_face", "keywords": ["face", "smile"] },
+	{ "category": "face", "char": "🙃", "name": "upside_down_face", "keywords": ["face", "flipped", "silly", "smile"] },
+	{ "category": "face", "char": "☺️", "name": "relaxed", "keywords": ["face", "blush", "massage", "happiness"] },
+	{ "category": "face", "char": "😋", "name": "yum", "keywords": ["happy", "joy", "tongue", "smile", "face", "silly", "yummy", "nom", "delicious", "savouring"] },
+	{ "category": "face", "char": "😌", "name": "relieved", "keywords": ["face", "relaxed", "phew", "massage", "happiness"] },
+	{ "category": "face", "char": "😍", "name": "heart_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "heart"] },
+	{ "category": "face", "char": "🥰", "name": "smiling_face_with_three_hearts", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "hearts", "adore"] },
+	{ "category": "face", "char": "😘", "name": "kissing_heart", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] },
+	{ "category": "face", "char": "😗", "name": "kissing", "keywords": ["love", "like", "face", "3", "valentines", "infatuation", "kiss"] },
+	{ "category": "face", "char": "😙", "name": "kissing_smiling_eyes", "keywords": ["face", "affection", "valentines", "infatuation", "kiss"] },
+	{ "category": "face", "char": "😚", "name": "kissing_closed_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] },
+	{ "category": "face", "char": "😜", "name": "stuck_out_tongue_winking_eye", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "wink", "tongue"] },
+	{ "category": "face", "char": "🤪", "name": "zany", "keywords": ["face", "goofy", "crazy"] },
+	{ "category": "face", "char": "🤨", "name": "raised_eyebrow", "keywords": ["face", "distrust", "scepticism", "disapproval", "disbelief", "surprise"] },
+	{ "category": "face", "char": "🧐", "name": "monocle", "keywords": ["face", "stuffy", "wealthy"] },
+	{ "category": "face", "char": "😝", "name": "stuck_out_tongue_closed_eyes", "keywords": ["face", "prank", "playful", "mischievous", "smile", "tongue"] },
+	{ "category": "face", "char": "😛", "name": "stuck_out_tongue", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "tongue"] },
+	{ "category": "face", "char": "🤑", "name": "money_mouth_face", "keywords": ["face", "rich", "dollar", "money"] },
+	{ "category": "face", "char": "🤓", "name": "nerd_face", "keywords": ["face", "nerdy", "geek", "dork"] },
+	{ "category": "face", "char": "🥸", "name": "disguised_face", "keywords": ["face", "nose", "glasses", "incognito"] },
+	{ "category": "face", "char": "😎", "name": "sunglasses", "keywords": ["face", "cool", "smile", "summer", "beach", "sunglass"] },
+	{ "category": "face", "char": "🤩", "name": "star_struck", "keywords": ["face", "smile", "starry", "eyes", "grinning"] },
+	{ "category": "face", "char": "🤡", "name": "clown_face", "keywords": ["face"] },
+	{ "category": "face", "char": "🤠", "name": "cowboy_hat_face", "keywords": ["face", "cowgirl", "hat"] },
+	{ "category": "face", "char": "🤗", "name": "hugs", "keywords": ["face", "smile", "hug"] },
+	{ "category": "face", "char": "😏", "name": "smirk", "keywords": ["face", "smile", "mean", "prank", "smug", "sarcasm"] },
+	{ "category": "face", "char": "😶", "name": "no_mouth", "keywords": ["face", "hellokitty"] },
+	{ "category": "face", "char": "😐", "name": "neutral_face", "keywords": ["indifference", "meh", ": |", "neutral"] },
+	{ "category": "face", "char": "😑", "name": "expressionless", "keywords": ["face", "indifferent", "-_-", "meh", "deadpan"] },
+	{ "category": "face", "char": "😒", "name": "unamused", "keywords": ["indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"] },
+	{ "category": "face", "char": "🙄", "name": "roll_eyes", "keywords": ["face", "eyeroll", "frustrated"] },
+	{ "category": "face", "char": "🤔", "name": "thinking", "keywords": ["face", "hmmm", "think", "consider"] },
+	{ "category": "face", "char": "🤥", "name": "lying_face", "keywords": ["face", "lie", "pinocchio"] },
+	{ "category": "face", "char": "🤭", "name": "hand_over_mouth", "keywords": ["face", "whoops", "shock", "surprise"] },
+	{ "category": "face", "char": "🤫", "name": "shushing", "keywords": ["face", "quiet", "shhh"] },
+	{ "category": "face", "char": "🤬", "name": "symbols_over_mouth", "keywords": ["face", "swearing", "cursing", "cussing", "profanity", "expletive"] },
+	{ "category": "face", "char": "🤯", "name": "exploding_head", "keywords": ["face", "shocked", "mind", "blown"] },
+	{ "category": "face", "char": "😳", "name": "flushed", "keywords": ["face", "blush", "shy", "flattered"] },
+	{ "category": "face", "char": "😞", "name": "disappointed", "keywords": ["face", "sad", "upset", "depressed", ": ("] },
+	{ "category": "face", "char": "😟", "name": "worried", "keywords": ["face", "concern", "nervous", ": ("] },
+	{ "category": "face", "char": "😠", "name": "angry", "keywords": ["mad", "face", "annoyed", "frustrated"] },
+	{ "category": "face", "char": "😡", "name": "rage", "keywords": ["angry", "mad", "hate", "despise"] },
+	{ "category": "face", "char": "😔", "name": "pensive", "keywords": ["face", "sad", "depressed", "upset"] },
+	{ "category": "face", "char": "😕", "name": "confused", "keywords": ["face", "indifference", "huh", "weird", "hmmm", ": /"] },
+	{ "category": "face", "char": "🙁", "name": "slightly_frowning_face", "keywords": ["face", "frowning", "disappointed", "sad", "upset"] },
+	{ "category": "face", "char": "☹", "name": "frowning_face", "keywords": ["face", "sad", "upset", "frown"] },
+	{ "category": "face", "char": "😣", "name": "persevere", "keywords": ["face", "sick", "no", "upset", "oops"] },
+	{ "category": "face", "char": "😖", "name": "confounded", "keywords": ["face", "confused", "sick", "unwell", "oops", ": S"] },
+	{ "category": "face", "char": "😫", "name": "tired_face", "keywords": ["sick", "whine", "upset", "frustrated"] },
+	{ "category": "face", "char": "😩", "name": "weary", "keywords": ["face", "tired", "sleepy", "sad", "frustrated", "upset"] },
+	{ "category": "face", "char": "🥺", "name": "pleading", "keywords": ["face", "begging", "mercy"] },
+	{ "category": "face", "char": "😤", "name": "triumph", "keywords": ["face", "gas", "phew", "proud", "pride"] },
+	{ "category": "face", "char": "😮", "name": "open_mouth", "keywords": ["face", "surprise", "impressed", "wow", "whoa", ": O"] },
+	{ "category": "face", "char": "😱", "name": "scream", "keywords": ["face", "munch", "scared", "omg"] },
+	{ "category": "face", "char": "😨", "name": "fearful", "keywords": ["face", "scared", "terrified", "nervous", "oops", "huh"] },
+	{ "category": "face", "char": "😰", "name": "cold_sweat", "keywords": ["face", "nervous", "sweat"] },
+	{ "category": "face", "char": "😯", "name": "hushed", "keywords": ["face", "woo", "shh"] },
+	{ "category": "face", "char": "😦", "name": "frowning", "keywords": ["face", "aw", "what"] },
+	{ "category": "face", "char": "😧", "name": "anguished", "keywords": ["face", "stunned", "nervous"] },
+	{ "category": "face", "char": "😢", "name": "cry", "keywords": ["face", "tears", "sad", "depressed", "upset", ": '("] },
+	{ "category": "face", "char": "😥", "name": "disappointed_relieved", "keywords": ["face", "phew", "sweat", "nervous"] },
+	{ "category": "face", "char": "🤤", "name": "drooling_face", "keywords": ["face"] },
+	{ "category": "face", "char": "😪", "name": "sleepy", "keywords": ["face", "tired", "rest", "nap"] },
+	{ "category": "face", "char": "😓", "name": "sweat", "keywords": ["face", "hot", "sad", "tired", "exercise"] },
+	{ "category": "face", "char": "🥵", "name": "hot", "keywords": ["face", "feverish", "heat", "red", "sweating"] },
+	{ "category": "face", "char": "🥶", "name": "cold", "keywords": ["face", "blue", "freezing", "frozen", "frostbite", "icicles"] },
+	{ "category": "face", "char": "😭", "name": "sob", "keywords": ["face", "cry", "tears", "sad", "upset", "depressed"] },
+	{ "category": "face", "char": "😵", "name": "dizzy_face", "keywords": ["spent", "unconscious", "xox", "dizzy"] },
+	{ "category": "face", "char": "😲", "name": "astonished", "keywords": ["face", "xox", "surprised", "poisoned"] },
+	{ "category": "face", "char": "🤐", "name": "zipper_mouth_face", "keywords": ["face", "sealed", "zipper", "secret"] },
+	{ "category": "face", "char": "🤢", "name": "nauseated_face", "keywords": ["face", "vomit", "gross", "green", "sick", "throw up", "ill"] },
+	{ "category": "face", "char": "🤧", "name": "sneezing_face", "keywords": ["face", "gesundheit", "sneeze", "sick", "allergy"] },
+	{ "category": "face", "char": "🤮", "name": "vomiting", "keywords": ["face", "sick"] },
+	{ "category": "face", "char": "😷", "name": "mask", "keywords": ["face", "sick", "ill", "disease"] },
+	{ "category": "face", "char": "🤒", "name": "face_with_thermometer", "keywords": ["sick", "temperature", "thermometer", "cold", "fever"] },
+	{ "category": "face", "char": "🤕", "name": "face_with_head_bandage", "keywords": ["injured", "clumsy", "bandage", "hurt"] },
+	{ "category": "face", "char": "🥴", "name": "woozy", "keywords": ["face", "dizzy", "intoxicated", "tipsy", "wavy"] },
+	{ "category": "face", "char": "🥱", "name": "yawning", "keywords": ["face", "tired", "yawning"] },
+	{ "category": "face", "char": "😴", "name": "sleeping", "keywords": ["face", "tired", "sleepy", "night", "zzz"] },
+	{ "category": "face", "char": "💤", "name": "zzz", "keywords": ["sleepy", "tired", "dream"] },
+	{ "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] },
+	{ "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] },
+	{ "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] },
+	{ "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] },
+	{ "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] },
+	{ "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] },
+	{ "category": "face", "char": "👹", "name": "japanese_ogre", "keywords": ["monster", "red", "mask", "halloween", "scary", "creepy", "devil", "demon", "japanese", "ogre"] },
+	{ "category": "face", "char": "👺", "name": "japanese_goblin", "keywords": ["red", "evil", "mask", "monster", "scary", "creepy", "japanese", "goblin"] },
+	{ "category": "face", "char": "💀", "name": "skull", "keywords": ["dead", "skeleton", "creepy", "death"] },
+	{ "category": "face", "char": "👻", "name": "ghost", "keywords": ["halloween", "spooky", "scary"] },
+	{ "category": "face", "char": "👽", "name": "alien", "keywords": ["UFO", "paul", "weird", "outer_space"] },
+	{ "category": "face", "char": "🤖", "name": "robot", "keywords": ["computer", "machine", "bot"] },
+	{ "category": "face", "char": "😺", "name": "smiley_cat", "keywords": ["animal", "cats", "happy", "smile"] },
+	{ "category": "face", "char": "😸", "name": "smile_cat", "keywords": ["animal", "cats", "smile"] },
+	{ "category": "face", "char": "😹", "name": "joy_cat", "keywords": ["animal", "cats", "haha", "happy", "tears"] },
+	{ "category": "face", "char": "😻", "name": "heart_eyes_cat", "keywords": ["animal", "love", "like", "affection", "cats", "valentines", "heart"] },
+	{ "category": "face", "char": "😼", "name": "smirk_cat", "keywords": ["animal", "cats", "smirk"] },
+	{ "category": "face", "char": "😽", "name": "kissing_cat", "keywords": ["animal", "cats", "kiss"] },
+	{ "category": "face", "char": "🙀", "name": "scream_cat", "keywords": ["animal", "cats", "munch", "scared", "scream"] },
+	{ "category": "face", "char": "😿", "name": "crying_cat_face", "keywords": ["animal", "tears", "weep", "sad", "cats", "upset", "cry"] },
+	{ "category": "face", "char": "😾", "name": "pouting_cat", "keywords": ["animal", "cats"] },
+	{ "category": "people", "char": "🤲", "name": "palms_up", "keywords": ["hands", "gesture", "cupped", "prayer"] },
+	{ "category": "people", "char": "🙌", "name": "raised_hands", "keywords": ["gesture", "hooray", "yea", "celebration", "hands"] },
+	{ "category": "people", "char": "👏", "name": "clap", "keywords": ["hands", "praise", "applause", "congrats", "yay"] },
+	{ "category": "people", "char": "👋", "name": "wave", "keywords": ["hands", "gesture", "goodbye", "solong", "farewell", "hello", "hi", "palm"] },
+	{ "category": "people", "char": "🤙", "name": "call_me_hand", "keywords": ["hands", "gesture"] },
+	{ "category": "people", "char": "👍", "name": "+1", "keywords": ["thumbsup", "yes", "awesome", "good", "agree", "accept", "cool", "hand", "like"] },
+	{ "category": "people", "char": "👎", "name": "-1", "keywords": ["thumbsdown", "no", "dislike", "hand"] },
+	{ "category": "people", "char": "👊", "name": "facepunch", "keywords": ["angry", "violence", "fist", "hit", "attack", "hand"] },
+	{ "category": "people", "char": "✊", "name": "fist", "keywords": ["fingers", "hand", "grasp"] },
+	{ "category": "people", "char": "🤛", "name": "fist_left", "keywords": ["hand", "fistbump"] },
+	{ "category": "people", "char": "🤜", "name": "fist_right", "keywords": ["hand", "fistbump"] },
+	{ "category": "people", "char": "✌", "name": "v", "keywords": ["fingers", "ohyeah", "hand", "peace", "victory", "two"] },
+	{ "category": "people", "char": "👌", "name": "ok_hand", "keywords": ["fingers", "limbs", "perfect", "ok", "okay"] },
+	{ "category": "people", "char": "✋", "name": "raised_hand", "keywords": ["fingers", "stop", "highfive", "palm", "ban"] },
+	{ "category": "people", "char": "🤚", "name": "raised_back_of_hand", "keywords": ["fingers", "raised", "backhand"] },
+	{ "category": "people", "char": "👐", "name": "open_hands", "keywords": ["fingers", "butterfly", "hands", "open"] },
+	{ "category": "people", "char": "💪", "name": "muscle", "keywords": ["arm", "flex", "hand", "summer", "strong", "biceps"] },
+	{ "category": "people", "char": "🦾", "name": "mechanical_arm", "keywords": ["flex", "hand", "strong", "biceps"] },
+	{ "category": "people", "char": "🙏", "name": "pray", "keywords": ["please", "hope", "wish", "namaste", "highfive"] },
+	{ "category": "people", "char": "🦶", "name": "foot", "keywords": ["kick", "stomp"] },
+	{ "category": "people", "char": "🦵", "name": "leg", "keywords": ["kick", "limb"] },
+	{ "category": "people", "char": "🦿", "name": "mechanical_leg", "keywords": ["kick", "limb"] },
+	{ "category": "people", "char": "🤝", "name": "handshake", "keywords": ["agreement", "shake"] },
+	{ "category": "people", "char": "☝", "name": "point_up", "keywords": ["hand", "fingers", "direction", "up"] },
+	{ "category": "people", "char": "👆", "name": "point_up_2", "keywords": ["fingers", "hand", "direction", "up"] },
+	{ "category": "people", "char": "👇", "name": "point_down", "keywords": ["fingers", "hand", "direction", "down"] },
+	{ "category": "people", "char": "👈", "name": "point_left", "keywords": ["direction", "fingers", "hand", "left"] },
+	{ "category": "people", "char": "👉", "name": "point_right", "keywords": ["fingers", "hand", "direction", "right"] },
+	{ "category": "people", "char": "🖕", "name": "fu", "keywords": ["hand", "fingers", "rude", "middle", "flipping"] },
+	{ "category": "people", "char": "🖐", "name": "raised_hand_with_fingers_splayed", "keywords": ["hand", "fingers", "palm"] },
+	{ "category": "people", "char": "🤟", "name": "love_you", "keywords": ["hand", "fingers", "gesture"] },
+	{ "category": "people", "char": "🤘", "name": "metal", "keywords": ["hand", "fingers", "evil_eye", "sign_of_horns", "rock_on"] },
+	{ "category": "people", "char": "🤞", "name": "crossed_fingers", "keywords": ["good", "lucky"] },
+	{ "category": "people", "char": "🖖", "name": "vulcan_salute", "keywords": ["hand", "fingers", "spock", "star trek"] },
+	{ "category": "people", "char": "✍", "name": "writing_hand", "keywords": ["lower_left_ballpoint_pen", "stationery", "write", "compose"] },
+	{ "category": "people", "char": "🤏", "name": "pinching_hand", "keywords": ["hand", "fingers"] },
+	{ "category": "people", "char": "🤌", "name": "pinched_fingers", "keywords": ["hand", "fingers"] },
+	{ "category": "people", "char": "🤳", "name": "selfie", "keywords": ["camera", "phone"] },
+	{ "category": "people", "char": "💅", "name": "nail_care", "keywords": ["beauty", "manicure", "finger", "fashion", "nail"] },
+	{ "category": "people", "char": "👄", "name": "lips", "keywords": ["mouth", "kiss"] },
+	{ "category": "people", "char": "🦷", "name": "tooth", "keywords": ["teeth", "dentist"] },
+	{ "category": "people", "char": "👅", "name": "tongue", "keywords": ["mouth", "playful"] },
+	{ "category": "people", "char": "👂", "name": "ear", "keywords": ["face", "hear", "sound", "listen"] },
+	{ "category": "people", "char": "🦻", "name": "ear_with_hearing_aid", "keywords": ["face", "hear", "sound", "listen"] },
+	{ "category": "people", "char": "👃", "name": "nose", "keywords": ["smell", "sniff"] },
+	{ "category": "people", "char": "👁", "name": "eye", "keywords": ["face", "look", "see", "watch", "stare"] },
+	{ "category": "people", "char": "👀", "name": "eyes", "keywords": ["look", "watch", "stalk", "peek", "see"] },
+	{ "category": "people", "char": "🧠", "name": "brain", "keywords": ["smart", "intelligent"] },
+	{ "category": "people", "char": "🫀", "name": "anatomical_heart", "keywords": [] },
+	{ "category": "people", "char": "🫁", "name": "lungs", "keywords": [] },
+	{ "category": "people", "char": "👤", "name": "bust_in_silhouette", "keywords": ["user", "person", "human"] },
+	{ "category": "people", "char": "👥", "name": "busts_in_silhouette", "keywords": ["user", "person", "human", "group", "team"] },
+	{ "category": "people", "char": "🗣", "name": "speaking_head", "keywords": ["user", "person", "human", "sing", "say", "talk"] },
+	{ "category": "people", "char": "👶", "name": "baby", "keywords": ["child", "boy", "girl", "toddler"] },
+	{ "category": "people", "char": "🧒", "name": "child", "keywords": ["gender-neutral", "young"] },
+	{ "category": "people", "char": "👦", "name": "boy", "keywords": ["man", "male", "guy", "teenager"] },
+	{ "category": "people", "char": "👧", "name": "girl", "keywords": ["female", "woman", "teenager"] },
+	{ "category": "people", "char": "🧑", "name": "adult", "keywords": ["gender-neutral", "person"] },
+	{ "category": "people", "char": "👨", "name": "man", "keywords": ["mustache", "father", "dad", "guy", "classy", "sir", "moustache"] },
+	{ "category": "people", "char": "👩", "name": "woman", "keywords": ["female", "girls", "lady"] },
+	{ "category": "people", "char": "🧑‍🦱", "name": "curly_hair", "keywords": ["curly", "afro", "braids", "ringlets"] },
+	{ "category": "people", "char": "👩‍🦱", "name": "curly_hair_woman", "keywords": ["woman", "female", "girl", "curly", "afro", "braids", "ringlets"] },
+	{ "category": "people", "char": "👨‍🦱", "name": "curly_hair_man", "keywords": ["man", "male", "boy", "guy", "curly", "afro", "braids", "ringlets"] },
+	{ "category": "people", "char": "🧑‍🦰", "name": "red_hair", "keywords": ["redhead"] },
+	{ "category": "people", "char": "👩‍🦰", "name": "red_hair_woman", "keywords": ["woman", "female", "girl", "ginger", "redhead"] },
+	{ "category": "people", "char": "👨‍🦰", "name": "red_hair_man", "keywords": ["man", "male", "boy", "guy", "ginger", "redhead"] },
+	{ "category": "people", "char": "👱‍♀️", "name": "blonde_woman", "keywords": ["woman", "female", "girl", "blonde", "person"] },
+	{ "category": "people", "char": "👱", "name": "blonde_man", "keywords": ["man", "male", "boy", "blonde", "guy", "person"] },
+	{ "category": "people", "char": "🧑‍🦳", "name": "white_hair", "keywords": ["gray", "old", "white"] },
+	{ "category": "people", "char": "👩‍🦳", "name": "white_hair_woman", "keywords": ["woman", "female", "girl", "gray", "old", "white"] },
+	{ "category": "people", "char": "👨‍🦳", "name": "white_hair_man", "keywords": ["man", "male", "boy", "guy", "gray", "old", "white"] },
+	{ "category": "people", "char": "🧑‍🦲", "name": "bald", "keywords": ["bald", "chemotherapy", "hairless", "shaven"] },
+	{ "category": "people", "char": "👩‍🦲", "name": "bald_woman", "keywords": ["woman", "female", "girl", "bald", "chemotherapy", "hairless", "shaven"] },
+	{ "category": "people", "char": "👨‍🦲", "name": "bald_man", "keywords": ["man", "male", "boy", "guy", "bald", "chemotherapy", "hairless", "shaven"] },
+	{ "category": "people", "char": "🧔", "name": "bearded_person", "keywords": ["person", "bewhiskered"] },
+	{ "category": "people", "char": "🧓", "name": "older_adult", "keywords": ["human", "elder", "senior", "gender-neutral"] },
+	{ "category": "people", "char": "👴", "name": "older_man", "keywords": ["human", "male", "men", "old", "elder", "senior"] },
+	{ "category": "people", "char": "👵", "name": "older_woman", "keywords": ["human", "female", "women", "lady", "old", "elder", "senior"] },
+	{ "category": "people", "char": "👲", "name": "man_with_gua_pi_mao", "keywords": ["male", "boy", "chinese"] },
+	{ "category": "people", "char": "🧕", "name": "woman_with_headscarf", "keywords": ["female", "hijab", "mantilla", "tichel"] },
+	{ "category": "people", "char": "👳‍♀️", "name": "woman_with_turban", "keywords": ["female", "indian", "hinduism", "arabs", "woman"] },
+	{ "category": "people", "char": "👳", "name": "man_with_turban", "keywords": ["male", "indian", "hinduism", "arabs"] },
+	{ "category": "people", "char": "👮‍♀️", "name": "policewoman", "keywords": ["woman", "police", "law", "legal", "enforcement", "arrest", "911", "female"] },
+	{ "category": "people", "char": "👮", "name": "policeman", "keywords": ["man", "police", "law", "legal", "enforcement", "arrest", "911"] },
+	{ "category": "people", "char": "👷‍♀️", "name": "construction_worker_woman", "keywords": ["female", "human", "wip", "build", "construction", "worker", "labor", "woman"] },
+	{ "category": "people", "char": "👷", "name": "construction_worker_man", "keywords": ["male", "human", "wip", "guy", "build", "construction", "worker", "labor"] },
+	{ "category": "people", "char": "💂‍♀️", "name": "guardswoman", "keywords": ["uk", "gb", "british", "female", "royal", "woman"] },
+	{ "category": "people", "char": "💂", "name": "guardsman", "keywords": ["uk", "gb", "british", "male", "guy", "royal"] },
+	{ "category": "people", "char": "🕵️‍♀️", "name": "female_detective", "keywords": ["human", "spy", "detective", "female", "woman"] },
+	{ "category": "people", "char": "🕵", "name": "male_detective", "keywords": ["human", "spy", "detective"] },
+	{ "category": "people", "char": "🧑‍⚕️", "name": "health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "human"] },
+	{ "category": "people", "char": "👩‍⚕️", "name": "woman_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "woman", "human"] },
+	{ "category": "people", "char": "👨‍⚕️", "name": "man_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🌾", "name": "farmer", "keywords": ["rancher", "gardener", "human"] },
+	{ "category": "people", "char": "👩‍🌾", "name": "woman_farmer", "keywords": ["rancher", "gardener", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🌾", "name": "man_farmer", "keywords": ["rancher", "gardener", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🍳", "name": "cook", "keywords": ["chef", "human"] },
+	{ "category": "people", "char": "👩‍🍳", "name": "woman_cook", "keywords": ["chef", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🍳", "name": "man_cook", "keywords": ["chef", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🎓", "name": "student", "keywords": ["graduate", "human"] },
+	{ "category": "people", "char": "👩‍🎓", "name": "woman_student", "keywords": ["graduate", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🎓", "name": "man_student", "keywords": ["graduate", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🎤", "name": "singer", "keywords": ["rockstar", "entertainer", "human"] },
+	{ "category": "people", "char": "👩‍🎤", "name": "woman_singer", "keywords": ["rockstar", "entertainer", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🎤", "name": "man_singer", "keywords": ["rockstar", "entertainer", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🏫", "name": "teacher", "keywords": ["instructor", "professor", "human"] },
+	{ "category": "people", "char": "👩‍🏫", "name": "woman_teacher", "keywords": ["instructor", "professor", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🏫", "name": "man_teacher", "keywords": ["instructor", "professor", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🏭", "name": "factory_worker", "keywords": ["assembly", "industrial", "human"] },
+	{ "category": "people", "char": "👩‍🏭", "name": "woman_factory_worker", "keywords": ["assembly", "industrial", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🏭", "name": "man_factory_worker", "keywords": ["assembly", "industrial", "man", "human"] },
+	{ "category": "people", "char": "🧑‍💻", "name": "technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "human", "laptop", "computer"] },
+	{ "category": "people", "char": "👩‍💻", "name": "woman_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "woman", "human", "laptop", "computer"] },
+	{ "category": "people", "char": "👨‍💻", "name": "man_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "man", "human", "laptop", "computer"] },
+	{ "category": "people", "char": "🧑‍💼", "name": "office_worker", "keywords": ["business", "manager", "human"] },
+	{ "category": "people", "char": "👩‍💼", "name": "woman_office_worker", "keywords": ["business", "manager", "woman", "human"] },
+	{ "category": "people", "char": "👨‍💼", "name": "man_office_worker", "keywords": ["business", "manager", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🔧", "name": "mechanic", "keywords": ["plumber", "human", "wrench"] },
+	{ "category": "people", "char": "👩‍🔧", "name": "woman_mechanic", "keywords": ["plumber", "woman", "human", "wrench"] },
+	{ "category": "people", "char": "👨‍🔧", "name": "man_mechanic", "keywords": ["plumber", "man", "human", "wrench"] },
+	{ "category": "people", "char": "🧑‍🔬", "name": "scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "human"] },
+	{ "category": "people", "char": "👩‍🔬", "name": "woman_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🔬", "name": "man_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🎨", "name": "artist", "keywords": ["painter", "human"] },
+	{ "category": "people", "char": "👩‍🎨", "name": "woman_artist", "keywords": ["painter", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🎨", "name": "man_artist", "keywords": ["painter", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🚒", "name": "firefighter", "keywords": ["fireman", "human"] },
+	{ "category": "people", "char": "👩‍🚒", "name": "woman_firefighter", "keywords": ["fireman", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🚒", "name": "man_firefighter", "keywords": ["fireman", "man", "human"] },
+	{ "category": "people", "char": "🧑‍✈️", "name": "pilot", "keywords": ["aviator", "plane", "human"] },
+	{ "category": "people", "char": "👩‍✈️", "name": "woman_pilot", "keywords": ["aviator", "plane", "woman", "human"] },
+	{ "category": "people", "char": "👨‍✈️", "name": "man_pilot", "keywords": ["aviator", "plane", "man", "human"] },
+	{ "category": "people", "char": "🧑‍🚀", "name": "astronaut", "keywords": ["space", "rocket", "human"] },
+	{ "category": "people", "char": "👩‍🚀", "name": "woman_astronaut", "keywords": ["space", "rocket", "woman", "human"] },
+	{ "category": "people", "char": "👨‍🚀", "name": "man_astronaut", "keywords": ["space", "rocket", "man", "human"] },
+	{ "category": "people", "char": "🧑‍⚖️", "name": "judge", "keywords": ["justice", "court", "human"] },
+	{ "category": "people", "char": "👩‍⚖️", "name": "woman_judge", "keywords": ["justice", "court", "woman", "human"] },
+	{ "category": "people", "char": "👨‍⚖️", "name": "man_judge", "keywords": ["justice", "court", "man", "human"] },
+	{ "category": "people", "char": "🦸‍♀️", "name": "woman_superhero", "keywords": ["woman", "female", "good", "heroine", "superpowers"] },
+	{ "category": "people", "char": "🦸‍♂️", "name": "man_superhero", "keywords": ["man", "male", "good", "hero", "superpowers"] },
+	{ "category": "people", "char": "🦹‍♀️", "name": "woman_supervillain", "keywords": ["woman", "female", "evil", "bad", "criminal", "heroine", "superpowers"] },
+	{ "category": "people", "char": "🦹‍♂️", "name": "man_supervillain", "keywords": ["man", "male", "evil", "bad", "criminal", "hero", "superpowers"] },
+	{ "category": "people", "char": "🤶", "name": "mrs_claus", "keywords": ["woman", "female", "xmas", "mother christmas"] },
+	{ "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF84", "name": "mx_claus", "keywords": ["xmas", "christmas"] },
+	{ "category": "people", "char": "🎅", "name": "santa", "keywords": ["festival", "man", "male", "xmas", "father christmas"] },
+	{ "category": "people", "char": "🥷", "name": "ninja", "keywords": [] },
+	{ "category": "people", "char": "🧙‍♀️", "name": "sorceress", "keywords": ["woman", "female", "mage", "witch"] },
+	{ "category": "people", "char": "🧙‍♂️", "name": "wizard", "keywords": ["man", "male", "mage", "sorcerer"] },
+	{ "category": "people", "char": "🧝‍♀️", "name": "woman_elf", "keywords": ["woman", "female"] },
+	{ "category": "people", "char": "🧝‍♂️", "name": "man_elf", "keywords": ["man", "male"] },
+	{ "category": "people", "char": "🧛‍♀️", "name": "woman_vampire", "keywords": ["woman", "female"] },
+	{ "category": "people", "char": "🧛‍♂️", "name": "man_vampire", "keywords": ["man", "male", "dracula"] },
+	{ "category": "people", "char": "🧟‍♀️", "name": "woman_zombie", "keywords": ["woman", "female", "undead", "walking dead"] },
+	{ "category": "people", "char": "🧟‍♂️", "name": "man_zombie", "keywords": ["man", "male", "dracula", "undead", "walking dead"] },
+	{ "category": "people", "char": "🧞‍♀️", "name": "woman_genie", "keywords": ["woman", "female"] },
+	{ "category": "people", "char": "🧞‍♂️", "name": "man_genie", "keywords": ["man", "male"] },
+	{ "category": "people", "char": "🧜‍♀️", "name": "mermaid", "keywords": ["woman", "female", "merwoman", "ariel"] },
+	{ "category": "people", "char": "🧜‍♂️", "name": "merman", "keywords": ["man", "male", "triton"] },
+	{ "category": "people", "char": "🧚‍♀️", "name": "woman_fairy", "keywords": ["woman", "female"] },
+	{ "category": "people", "char": "🧚‍♂️", "name": "man_fairy", "keywords": ["man", "male"] },
+	{ "category": "people", "char": "👼", "name": "angel", "keywords": ["heaven", "wings", "halo"] },
+	{ "category": "people", "char": "🤰", "name": "pregnant_woman", "keywords": ["baby"] },
+	{ "category": "people", "char": "🤱", "name": "breastfeeding", "keywords": ["nursing", "baby"] },
+	{ "category": "people", "char": "\uD83D\uDC69\u200D\uD83C\uDF7C", "name": "woman_feeding_baby", "keywords": [] },
+	{ "category": "people", "char": "\uD83D\uDC68\u200D\uD83C\uDF7C", "name": "man_feeding_baby", "keywords": [] },
+	{ "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF7C", "name": "person_feeding_baby", "keywords": [] },
+	{ "category": "people", "char": "👸", "name": "princess", "keywords": ["girl", "woman", "female", "blond", "crown", "royal", "queen"] },
+	{ "category": "people", "char": "🤴", "name": "prince", "keywords": ["boy", "man", "male", "crown", "royal", "king"] },
+	{ "category": "people", "char": "👰", "name": "person_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] },
+	{ "category": "people", "char": "👰", "name": "bride_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] },
+	{ "category": "people", "char": "🤵", "name": "person_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] },
+	{ "category": "people", "char": "🤵", "name": "man_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] },
+	{ "category": "people", "char": "🏃‍♀️", "name": "running_woman", "keywords": ["woman", "walking", "exercise", "race", "running", "female"] },
+	{ "category": "people", "char": "🏃", "name": "running_man", "keywords": ["man", "walking", "exercise", "race", "running"] },
+	{ "category": "people", "char": "🚶‍♀️", "name": "walking_woman", "keywords": ["human", "feet", "steps", "woman", "female"] },
+	{ "category": "people", "char": "🚶", "name": "walking_man", "keywords": ["human", "feet", "steps"] },
+	{ "category": "people", "char": "💃", "name": "dancer", "keywords": ["female", "girl", "woman", "fun"] },
+	{ "category": "people", "char": "🕺", "name": "man_dancing", "keywords": ["male", "boy", "fun", "dancer"] },
+	{ "category": "people", "char": "👯", "name": "dancing_women", "keywords": ["female", "bunny", "women", "girls"] },
+	{ "category": "people", "char": "👯‍♂️", "name": "dancing_men", "keywords": ["male", "bunny", "men", "boys"] },
+	{ "category": "people", "char": "👫", "name": "couple", "keywords": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"] },
+	{ "category": "people", "char": "\uD83E\uDDD1\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1", "name": "people_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"] },
+	{ "category": "people", "char": "👬", "name": "two_men_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "man", "human"] },
+	{ "category": "people", "char": "👭", "name": "two_women_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "female", "human"] },
+	{ "category": "people", "char": "🫂", "name": "people_hugging", "keywords": [] },
+	{ "category": "people", "char": "🙇‍♀️", "name": "bowing_woman", "keywords": ["woman", "female", "girl"] },
+	{ "category": "people", "char": "🙇", "name": "bowing_man", "keywords": ["man", "male", "boy"] },
+	{ "category": "people", "char": "🤦‍♂️", "name": "man_facepalming", "keywords": ["man", "male", "boy", "disbelief"] },
+	{ "category": "people", "char": "🤦‍♀️", "name": "woman_facepalming", "keywords": ["woman", "female", "girl", "disbelief"] },
+	{ "category": "people", "char": "🤷", "name": "woman_shrugging", "keywords": ["woman", "female", "girl", "confused", "indifferent", "doubt"] },
+	{ "category": "people", "char": "🤷‍♂️", "name": "man_shrugging", "keywords": ["man", "male", "boy", "confused", "indifferent", "doubt"] },
+	{ "category": "people", "char": "💁", "name": "tipping_hand_woman", "keywords": ["female", "girl", "woman", "human", "information"] },
+	{ "category": "people", "char": "💁‍♂️", "name": "tipping_hand_man", "keywords": ["male", "boy", "man", "human", "information"] },
+	{ "category": "people", "char": "🙅", "name": "no_good_woman", "keywords": ["female", "girl", "woman", "nope"] },
+	{ "category": "people", "char": "🙅‍♂️", "name": "no_good_man", "keywords": ["male", "boy", "man", "nope"] },
+	{ "category": "people", "char": "🙆", "name": "ok_woman", "keywords": ["women", "girl", "female", "pink", "human", "woman"] },
+	{ "category": "people", "char": "🙆‍♂️", "name": "ok_man", "keywords": ["men", "boy", "male", "blue", "human", "man"] },
+	{ "category": "people", "char": "🙋", "name": "raising_hand_woman", "keywords": ["female", "girl", "woman"] },
+	{ "category": "people", "char": "🙋‍♂️", "name": "raising_hand_man", "keywords": ["male", "boy", "man"] },
+	{ "category": "people", "char": "🙎", "name": "pouting_woman", "keywords": ["female", "girl", "woman"] },
+	{ "category": "people", "char": "🙎‍♂️", "name": "pouting_man", "keywords": ["male", "boy", "man"] },
+	{ "category": "people", "char": "🙍", "name": "frowning_woman", "keywords": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"] },
+	{ "category": "people", "char": "🙍‍♂️", "name": "frowning_man", "keywords": ["male", "boy", "man", "sad", "depressed", "discouraged", "unhappy"] },
+	{ "category": "people", "char": "💇", "name": "haircut_woman", "keywords": ["female", "girl", "woman"] },
+	{ "category": "people", "char": "💇‍♂️", "name": "haircut_man", "keywords": ["male", "boy", "man"] },
+	{ "category": "people", "char": "💆", "name": "massage_woman", "keywords": ["female", "girl", "woman", "head"] },
+	{ "category": "people", "char": "💆‍♂️", "name": "massage_man", "keywords": ["male", "boy", "man", "head"] },
+	{ "category": "people", "char": "🧖‍♀️", "name": "woman_in_steamy_room", "keywords": ["female", "woman", "spa", "steamroom", "sauna"] },
+	{ "category": "people", "char": "🧖‍♂️", "name": "man_in_steamy_room", "keywords": ["male", "man", "spa", "steamroom", "sauna"] },
+	{ "category": "people", "char": "🧏‍♀️", "name": "woman_deaf", "keywords": ["woman", "female"] },
+	{ "category": "people", "char": "🧏‍♂️", "name": "man_deaf", "keywords": ["man", "male"] },
+	{ "category": "people", "char": "🧍‍♀️", "name": "woman_standing", "keywords": ["woman", "female"] },
+	{ "category": "people", "char": "🧍‍♂️", "name": "man_standing", "keywords": ["man", "male"] },
+	{ "category": "people", "char": "🧎‍♀️", "name": "woman_kneeling", "keywords": ["woman", "female"] },
+	{ "category": "people", "char": "🧎‍♂️", "name": "man_kneeling", "keywords": ["man", "male"] },
+	{ "category": "people", "char": "🧑‍🦯", "name": "person_with_probing_cane", "keywords": ["accessibility", "blind"] },
+	{ "category": "people", "char": "👩‍🦯", "name": "woman_with_probing_cane", "keywords": ["woman", "female", "accessibility", "blind"] },
+	{ "category": "people", "char": "👨‍🦯", "name": "man_with_probing_cane", "keywords": ["man", "male", "accessibility", "blind"] },
+	{ "category": "people", "char": "🧑‍🦼", "name": "person_in_motorized_wheelchair", "keywords": ["accessibility"] },
+	{ "category": "people", "char": "👩‍🦼", "name": "woman_in_motorized_wheelchair", "keywords": ["woman", "female", "accessibility"] },
+	{ "category": "people", "char": "👨‍🦼", "name": "man_in_motorized_wheelchair", "keywords": ["man", "male", "accessibility"] },
+	{ "category": "people", "char": "🧑‍🦽", "name": "person_in_manual_wheelchair", "keywords": ["accessibility"] },
+	{ "category": "people", "char": "👩‍🦽", "name": "woman_in_manual_wheelchair", "keywords": ["woman", "female", "accessibility"] },
+	{ "category": "people", "char": "👨‍🦽", "name": "man_in_manual_wheelchair", "keywords": ["man", "male", "accessibility"] },
+	{ "category": "people", "char": "💑", "name": "couple_with_heart_woman_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] },
+	{ "category": "people", "char": "👩‍❤️‍👩", "name": "couple_with_heart_woman_woman", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] },
+	{ "category": "people", "char": "👨‍❤️‍👨", "name": "couple_with_heart_man_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] },
+	{ "category": "people", "char": "💏", "name": "couplekiss_man_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] },
+	{ "category": "people", "char": "👩‍❤️‍💋‍👩", "name": "couplekiss_woman_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] },
+	{ "category": "people", "char": "👨‍❤️‍💋‍👨", "name": "couplekiss_man_man", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] },
+	{ "category": "people", "char": "👪", "name": "family_man_woman_boy", "keywords": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"] },
+	{ "category": "people", "char": "👨‍👩‍👧", "name": "family_man_woman_girl", "keywords": ["home", "parents", "people", "human", "child"] },
+	{ "category": "people", "char": "👨‍👩‍👧‍👦", "name": "family_man_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👩‍👦‍👦", "name": "family_man_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👩‍👧‍👧", "name": "family_man_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👩‍👩‍👦", "name": "family_woman_woman_boy", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👩‍👩‍👧", "name": "family_woman_woman_girl", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👩‍👩‍👧‍👦", "name": "family_woman_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👩‍👩‍👦‍👦", "name": "family_woman_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👩‍👩‍👧‍👧", "name": "family_woman_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👨‍👦", "name": "family_man_man_boy", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👨‍👧", "name": "family_man_man_girl", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👨‍👧‍👦", "name": "family_man_man_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👨‍👦‍👦", "name": "family_man_man_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👨‍👧‍👧", "name": "family_man_man_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] },
+	{ "category": "people", "char": "👩‍👦", "name": "family_woman_boy", "keywords": ["home", "parent", "people", "human", "child"] },
+	{ "category": "people", "char": "👩‍👧", "name": "family_woman_girl", "keywords": ["home", "parent", "people", "human", "child"] },
+	{ "category": "people", "char": "👩‍👧‍👦", "name": "family_woman_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] },
+	{ "category": "people", "char": "👩‍👦‍👦", "name": "family_woman_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] },
+	{ "category": "people", "char": "👩‍👧‍👧", "name": "family_woman_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👦", "name": "family_man_boy", "keywords": ["home", "parent", "people", "human", "child"] },
+	{ "category": "people", "char": "👨‍👧", "name": "family_man_girl", "keywords": ["home", "parent", "people", "human", "child"] },
+	{ "category": "people", "char": "👨‍👧‍👦", "name": "family_man_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👦‍👦", "name": "family_man_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] },
+	{ "category": "people", "char": "👨‍👧‍👧", "name": "family_man_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] },
+	{ "category": "people", "char": "🧶", "name": "yarn", "keywords": ["ball", "crochet", "knit"] },
+	{ "category": "people", "char": "🧵", "name": "thread", "keywords": ["needle", "sewing", "spool", "string"] },
+	{ "category": "people", "char": "🧥", "name": "coat", "keywords": ["jacket"] },
+	{ "category": "people", "char": "🥼", "name": "labcoat", "keywords": ["doctor", "experiment", "scientist", "chemist"] },
+	{ "category": "people", "char": "👚", "name": "womans_clothes", "keywords": ["fashion", "shopping_bags", "female"] },
+	{ "category": "people", "char": "👕", "name": "tshirt", "keywords": ["fashion", "cloth", "casual", "shirt", "tee"] },
+	{ "category": "people", "char": "👖", "name": "jeans", "keywords": ["fashion", "shopping"] },
+	{ "category": "people", "char": "👔", "name": "necktie", "keywords": ["shirt", "suitup", "formal", "fashion", "cloth", "business"] },
+	{ "category": "people", "char": "👗", "name": "dress", "keywords": ["clothes", "fashion", "shopping"] },
+	{ "category": "people", "char": "👙", "name": "bikini", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] },
+	{ "category": "people", "char": "🩱", "name": "one_piece_swimsuit", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] },
+	{ "category": "people", "char": "👘", "name": "kimono", "keywords": ["dress", "fashion", "women", "female", "japanese"] },
+	{ "category": "people", "char": "🥻", "name": "sari", "keywords": ["dress", "fashion", "women", "female"] },
+	{ "category": "people", "char": "🩲", "name": "briefs", "keywords": ["dress", "fashion"] },
+	{ "category": "people", "char": "🩳", "name": "shorts", "keywords": ["dress", "fashion"] },
+	{ "category": "people", "char": "💄", "name": "lipstick", "keywords": ["female", "girl", "fashion", "woman"] },
+	{ "category": "people", "char": "💋", "name": "kiss", "keywords": ["face", "lips", "love", "like", "affection", "valentines"] },
+	{ "category": "people", "char": "👣", "name": "footprints", "keywords": ["feet", "tracking", "walking", "beach"] },
+	{ "category": "people", "char": "🥿", "name": "flat_shoe", "keywords": ["ballet", "slip-on", "slipper"] },
+	{ "category": "people", "char": "👠", "name": "high_heel", "keywords": ["fashion", "shoes", "female", "pumps", "stiletto"] },
+	{ "category": "people", "char": "👡", "name": "sandal", "keywords": ["shoes", "fashion", "flip flops"] },
+	{ "category": "people", "char": "👢", "name": "boot", "keywords": ["shoes", "fashion"] },
+	{ "category": "people", "char": "👞", "name": "mans_shoe", "keywords": ["fashion", "male"] },
+	{ "category": "people", "char": "👟", "name": "athletic_shoe", "keywords": ["shoes", "sports", "sneakers"] },
+	{ "category": "people", "char": "🩴", "name": "thong_sandal", "keywords": [] },
+	{ "category": "people", "char": "🩰", "name": "ballet_shoes", "keywords": ["shoes", "sports"] },
+	{ "category": "people", "char": "🧦", "name": "socks", "keywords": ["stockings", "clothes"] },
+	{ "category": "people", "char": "🧤", "name": "gloves", "keywords": ["hands", "winter", "clothes"] },
+	{ "category": "people", "char": "🧣", "name": "scarf", "keywords": ["neck", "winter", "clothes"] },
+	{ "category": "people", "char": "👒", "name": "womans_hat", "keywords": ["fashion", "accessories", "female", "lady", "spring"] },
+	{ "category": "people", "char": "🎩", "name": "tophat", "keywords": ["magic", "gentleman", "classy", "circus"] },
+	{ "category": "people", "char": "🧢", "name": "billed_hat", "keywords": ["cap", "baseball"] },
+	{ "category": "people", "char": "⛑", "name": "rescue_worker_helmet", "keywords": ["construction", "build"] },
+	{ "category": "people", "char": "🪖", "name": "military_helmet", "keywords": [] },
+	{ "category": "people", "char": "🎓", "name": "mortar_board", "keywords": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"] },
+	{ "category": "people", "char": "👑", "name": "crown", "keywords": ["king", "kod", "leader", "royalty", "lord"] },
+	{ "category": "people", "char": "🎒", "name": "school_satchel", "keywords": ["student", "education", "bag", "backpack"] },
+	{ "category": "people", "char": "🧳", "name": "luggage", "keywords": ["packing", "travel"] },
+	{ "category": "people", "char": "👝", "name": "pouch", "keywords": ["bag", "accessories", "shopping"] },
+	{ "category": "people", "char": "👛", "name": "purse", "keywords": ["fashion", "accessories", "money", "sales", "shopping"] },
+	{ "category": "people", "char": "👜", "name": "handbag", "keywords": ["fashion", "accessory", "accessories", "shopping"] },
+	{ "category": "people", "char": "💼", "name": "briefcase", "keywords": ["business", "documents", "work", "law", "legal", "job", "career"] },
+	{ "category": "people", "char": "👓", "name": "eyeglasses", "keywords": ["fashion", "accessories", "eyesight", "nerdy", "dork", "geek"] },
+	{ "category": "people", "char": "🕶", "name": "dark_sunglasses", "keywords": ["face", "cool", "accessories"] },
+	{ "category": "people", "char": "🥽", "name": "goggles", "keywords": ["eyes", "protection", "safety"] },
+	{ "category": "people", "char": "💍", "name": "ring", "keywords": ["wedding", "propose", "marriage", "valentines", "diamond", "fashion", "jewelry", "gem", "engagement"] },
+	{ "category": "people", "char": "🌂", "name": "closed_umbrella", "keywords": ["weather", "rain", "drizzle"] },
+	{ "category": "animals_and_nature", "char": "🐶", "name": "dog", "keywords": ["animal", "friend", "nature", "woof", "puppy", "pet", "faithful"] },
+	{ "category": "animals_and_nature", "char": "🐱", "name": "cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] },
+	{ "category": "animals_and_nature", "char": "🐈‍⬛", "name": "black_cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] },
+	{ "category": "animals_and_nature", "char": "🐭", "name": "mouse", "keywords": ["animal", "nature", "cheese_wedge", "rodent"] },
+	{ "category": "animals_and_nature", "char": "🐹", "name": "hamster", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐰", "name": "rabbit", "keywords": ["animal", "nature", "pet", "spring", "magic", "bunny"] },
+	{ "category": "animals_and_nature", "char": "🦊", "name": "fox_face", "keywords": ["animal", "nature", "face"] },
+	{ "category": "animals_and_nature", "char": "🐻", "name": "bear", "keywords": ["animal", "nature", "wild"] },
+	{ "category": "animals_and_nature", "char": "🐼", "name": "panda_face", "keywords": ["animal", "nature", "panda"] },
+	{ "category": "animals_and_nature", "char": "🐨", "name": "koala", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐯", "name": "tiger", "keywords": ["animal", "cat", "danger", "wild", "nature", "roar"] },
+	{ "category": "animals_and_nature", "char": "🦁", "name": "lion", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐮", "name": "cow", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] },
+	{ "category": "animals_and_nature", "char": "🐷", "name": "pig", "keywords": ["animal", "oink", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐽", "name": "pig_nose", "keywords": ["animal", "oink"] },
+	{ "category": "animals_and_nature", "char": "🐸", "name": "frog", "keywords": ["animal", "nature", "croak", "toad"] },
+	{ "category": "animals_and_nature", "char": "🦑", "name": "squid", "keywords": ["animal", "nature", "ocean", "sea"] },
+	{ "category": "animals_and_nature", "char": "🐙", "name": "octopus", "keywords": ["animal", "creature", "ocean", "sea", "nature", "beach"] },
+	{ "category": "animals_and_nature", "char": "🦐", "name": "shrimp", "keywords": ["animal", "ocean", "nature", "seafood"] },
+	{ "category": "animals_and_nature", "char": "🐵", "name": "monkey_face", "keywords": ["animal", "nature", "circus"] },
+	{ "category": "animals_and_nature", "char": "🦍", "name": "gorilla", "keywords": ["animal", "nature", "circus"] },
+	{ "category": "animals_and_nature", "char": "🙈", "name": "see_no_evil", "keywords": ["monkey", "animal", "nature", "haha"] },
+	{ "category": "animals_and_nature", "char": "🙉", "name": "hear_no_evil", "keywords": ["animal", "monkey", "nature"] },
+	{ "category": "animals_and_nature", "char": "🙊", "name": "speak_no_evil", "keywords": ["monkey", "animal", "nature", "omg"] },
+	{ "category": "animals_and_nature", "char": "🐒", "name": "monkey", "keywords": ["animal", "nature", "banana", "circus"] },
+	{ "category": "animals_and_nature", "char": "🐔", "name": "chicken", "keywords": ["animal", "cluck", "nature", "bird"] },
+	{ "category": "animals_and_nature", "char": "🐧", "name": "penguin", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐦", "name": "bird", "keywords": ["animal", "nature", "fly", "tweet", "spring"] },
+	{ "category": "animals_and_nature", "char": "🐤", "name": "baby_chick", "keywords": ["animal", "chicken", "bird"] },
+	{ "category": "animals_and_nature", "char": "🐣", "name": "hatching_chick", "keywords": ["animal", "chicken", "egg", "born", "baby", "bird"] },
+	{ "category": "animals_and_nature", "char": "🐥", "name": "hatched_chick", "keywords": ["animal", "chicken", "baby", "bird"] },
+	{ "category": "animals_and_nature", "char": "🦆", "name": "duck", "keywords": ["animal", "nature", "bird", "mallard"] },
+	{ "category": "animals_and_nature", "char": "🦅", "name": "eagle", "keywords": ["animal", "nature", "bird"] },
+	{ "category": "animals_and_nature", "char": "🦉", "name": "owl", "keywords": ["animal", "nature", "bird", "hoot"] },
+	{ "category": "animals_and_nature", "char": "🦇", "name": "bat", "keywords": ["animal", "nature", "blind", "vampire"] },
+	{ "category": "animals_and_nature", "char": "🐺", "name": "wolf", "keywords": ["animal", "nature", "wild"] },
+	{ "category": "animals_and_nature", "char": "🐗", "name": "boar", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐴", "name": "horse", "keywords": ["animal", "brown", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦄", "name": "unicorn", "keywords": ["animal", "nature", "mystical"] },
+	{ "category": "animals_and_nature", "char": "🐝", "name": "honeybee", "keywords": ["animal", "insect", "nature", "bug", "spring", "honey"] },
+	{ "category": "animals_and_nature", "char": "🐛", "name": "bug", "keywords": ["animal", "insect", "nature", "worm"] },
+	{ "category": "animals_and_nature", "char": "🦋", "name": "butterfly", "keywords": ["animal", "insect", "nature", "caterpillar"] },
+	{ "category": "animals_and_nature", "char": "🐌", "name": "snail", "keywords": ["slow", "animal", "shell"] },
+	{ "category": "animals_and_nature", "char": "🐞", "name": "beetle", "keywords": ["animal", "insect", "nature", "ladybug"] },
+	{ "category": "animals_and_nature", "char": "🐜", "name": "ant", "keywords": ["animal", "insect", "nature", "bug"] },
+	{ "category": "animals_and_nature", "char": "🦗", "name": "grasshopper", "keywords": ["animal", "cricket", "chirp"] },
+	{ "category": "animals_and_nature", "char": "🕷", "name": "spider", "keywords": ["animal", "arachnid"] },
+	{ "category": "animals_and_nature", "char": "🪲", "name": "beetle", "keywords": ["animal"] },
+	{ "category": "animals_and_nature", "char": "🪳", "name": "cockroach", "keywords": ["animal"] },
+	{ "category": "animals_and_nature", "char": "🪰", "name": "fly", "keywords": ["animal"] },
+	{ "category": "animals_and_nature", "char": "🪱", "name": "worm", "keywords": ["animal"] },
+	{ "category": "animals_and_nature", "char": "🦂", "name": "scorpion", "keywords": ["animal", "arachnid"] },
+	{ "category": "animals_and_nature", "char": "🦀", "name": "crab", "keywords": ["animal", "crustacean"] },
+	{ "category": "animals_and_nature", "char": "🐍", "name": "snake", "keywords": ["animal", "evil", "nature", "hiss", "python"] },
+	{ "category": "animals_and_nature", "char": "🦎", "name": "lizard", "keywords": ["animal", "nature", "reptile"] },
+	{ "category": "animals_and_nature", "char": "🦖", "name": "t-rex", "keywords": ["animal", "nature", "dinosaur", "tyrannosaurus", "extinct"] },
+	{ "category": "animals_and_nature", "char": "🦕", "name": "sauropod", "keywords": ["animal", "nature", "dinosaur", "brachiosaurus", "brontosaurus", "diplodocus", "extinct"] },
+	{ "category": "animals_and_nature", "char": "🐢", "name": "turtle", "keywords": ["animal", "slow", "nature", "tortoise"] },
+	{ "category": "animals_and_nature", "char": "🐠", "name": "tropical_fish", "keywords": ["animal", "swim", "ocean", "beach", "nemo"] },
+	{ "category": "animals_and_nature", "char": "🐟", "name": "fish", "keywords": ["animal", "food", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐡", "name": "blowfish", "keywords": ["animal", "nature", "food", "sea", "ocean"] },
+	{ "category": "animals_and_nature", "char": "🐬", "name": "dolphin", "keywords": ["animal", "nature", "fish", "sea", "ocean", "flipper", "fins", "beach"] },
+	{ "category": "animals_and_nature", "char": "🦈", "name": "shark", "keywords": ["animal", "nature", "fish", "sea", "ocean", "jaws", "fins", "beach"] },
+	{ "category": "animals_and_nature", "char": "🐳", "name": "whale", "keywords": ["animal", "nature", "sea", "ocean"] },
+	{ "category": "animals_and_nature", "char": "🐋", "name": "whale2", "keywords": ["animal", "nature", "sea", "ocean"] },
+	{ "category": "animals_and_nature", "char": "🐊", "name": "crocodile", "keywords": ["animal", "nature", "reptile", "lizard", "alligator"] },
+	{ "category": "animals_and_nature", "char": "🐆", "name": "leopard", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦓", "name": "zebra", "keywords": ["animal", "nature", "stripes", "safari"] },
+	{ "category": "animals_and_nature", "char": "🐅", "name": "tiger2", "keywords": ["animal", "nature", "roar"] },
+	{ "category": "animals_and_nature", "char": "🐃", "name": "water_buffalo", "keywords": ["animal", "nature", "ox", "cow"] },
+	{ "category": "animals_and_nature", "char": "🐂", "name": "ox", "keywords": ["animal", "cow", "beef"] },
+	{ "category": "animals_and_nature", "char": "🐄", "name": "cow2", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] },
+	{ "category": "animals_and_nature", "char": "🦌", "name": "deer", "keywords": ["animal", "nature", "horns", "venison"] },
+	{ "category": "animals_and_nature", "char": "🐪", "name": "dromedary_camel", "keywords": ["animal", "hot", "desert", "hump"] },
+	{ "category": "animals_and_nature", "char": "🐫", "name": "camel", "keywords": ["animal", "nature", "hot", "desert", "hump"] },
+	{ "category": "animals_and_nature", "char": "🦒", "name": "giraffe", "keywords": ["animal", "nature", "spots", "safari"] },
+	{ "category": "animals_and_nature", "char": "🐘", "name": "elephant", "keywords": ["animal", "nature", "nose", "th", "circus"] },
+	{ "category": "animals_and_nature", "char": "🦏", "name": "rhinoceros", "keywords": ["animal", "nature", "horn"] },
+	{ "category": "animals_and_nature", "char": "🐐", "name": "goat", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐏", "name": "ram", "keywords": ["animal", "sheep", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐑", "name": "sheep", "keywords": ["animal", "nature", "wool", "shipit"] },
+	{ "category": "animals_and_nature", "char": "🐎", "name": "racehorse", "keywords": ["animal", "gamble", "luck"] },
+	{ "category": "animals_and_nature", "char": "🐖", "name": "pig2", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐀", "name": "rat", "keywords": ["animal", "mouse", "rodent"] },
+	{ "category": "animals_and_nature", "char": "🐁", "name": "mouse2", "keywords": ["animal", "nature", "rodent"] },
+	{ "category": "animals_and_nature", "char": "🐓", "name": "rooster", "keywords": ["animal", "nature", "chicken"] },
+	{ "category": "animals_and_nature", "char": "🦃", "name": "turkey", "keywords": ["animal", "bird"] },
+	{ "category": "animals_and_nature", "char": "🕊", "name": "dove", "keywords": ["animal", "bird"] },
+	{ "category": "animals_and_nature", "char": "🐕", "name": "dog2", "keywords": ["animal", "nature", "friend", "doge", "pet", "faithful"] },
+	{ "category": "animals_and_nature", "char": "🐩", "name": "poodle", "keywords": ["dog", "animal", "101", "nature", "pet"] },
+	{ "category": "animals_and_nature", "char": "🐈", "name": "cat2", "keywords": ["animal", "meow", "pet", "cats"] },
+	{ "category": "animals_and_nature", "char": "🐇", "name": "rabbit2", "keywords": ["animal", "nature", "pet", "magic", "spring"] },
+	{ "category": "animals_and_nature", "char": "🐿", "name": "chipmunk", "keywords": ["animal", "nature", "rodent", "squirrel"] },
+	{ "category": "animals_and_nature", "char": "🦔", "name": "hedgehog", "keywords": ["animal", "nature", "spiny"] },
+	{ "category": "animals_and_nature", "char": "🦝", "name": "raccoon", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦙", "name": "llama", "keywords": ["animal", "nature", "alpaca"] },
+	{ "category": "animals_and_nature", "char": "🦛", "name": "hippopotamus", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦘", "name": "kangaroo", "keywords": ["animal", "nature", "australia", "joey", "hop", "marsupial"] },
+	{ "category": "animals_and_nature", "char": "🦡", "name": "badger", "keywords": ["animal", "nature", "honey"] },
+	{ "category": "animals_and_nature", "char": "🦢", "name": "swan", "keywords": ["animal", "nature", "bird"] },
+	{ "category": "animals_and_nature", "char": "🦚", "name": "peacock", "keywords": ["animal", "nature", "peahen", "bird"] },
+	{ "category": "animals_and_nature", "char": "🦜", "name": "parrot", "keywords": ["animal", "nature", "bird", "pirate", "talk"] },
+	{ "category": "animals_and_nature", "char": "🦞", "name": "lobster", "keywords": ["animal", "nature", "bisque", "claws", "seafood"] },
+	{ "category": "animals_and_nature", "char": "🦠", "name": "microbe", "keywords": ["amoeba", "bacteria", "germs"] },
+	{ "category": "animals_and_nature", "char": "🦟", "name": "mosquito", "keywords": ["animal", "nature", "insect", "malaria"] },
+	{ "category": "animals_and_nature", "char": "🦬", "name": "bison", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦣", "name": "mammoth", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦫", "name": "beaver", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐻‍❄️", "name": "polar_bear", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦤", "name": "dodo", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🪶", "name": "feather", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦭", "name": "seal", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐾", "name": "paw_prints", "keywords": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"] },
+	{ "category": "animals_and_nature", "char": "🐉", "name": "dragon", "keywords": ["animal", "myth", "nature", "chinese", "green"] },
+	{ "category": "animals_and_nature", "char": "🐲", "name": "dragon_face", "keywords": ["animal", "myth", "nature", "chinese", "green"] },
+	{ "category": "animals_and_nature", "char": "🦧", "name": "orangutan", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦮", "name": "guide_dog", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🐕‍🦺", "name": "service_dog", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦥", "name": "sloth", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦦", "name": "otter", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦨", "name": "skunk", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🦩", "name": "flamingo", "keywords": ["animal", "nature"] },
+	{ "category": "animals_and_nature", "char": "🌵", "name": "cactus", "keywords": ["vegetable", "plant", "nature"] },
+	{ "category": "animals_and_nature", "char": "🎄", "name": "christmas_tree", "keywords": ["festival", "vacation", "december", "xmas", "celebration"] },
+	{ "category": "animals_and_nature", "char": "🌲", "name": "evergreen_tree", "keywords": ["plant", "nature"] },
+	{ "category": "animals_and_nature", "char": "🌳", "name": "deciduous_tree", "keywords": ["plant", "nature"] },
+	{ "category": "animals_and_nature", "char": "🌴", "name": "palm_tree", "keywords": ["plant", "vegetable", "nature", "summer", "beach", "mojito", "tropical"] },
+	{ "category": "animals_and_nature", "char": "🌱", "name": "seedling", "keywords": ["plant", "nature", "grass", "lawn", "spring"] },
+	{ "category": "animals_and_nature", "char": "🌿", "name": "herb", "keywords": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"] },
+	{ "category": "animals_and_nature", "char": "☘", "name": "shamrock", "keywords": ["vegetable", "plant", "nature", "irish", "clover"] },
+	{ "category": "animals_and_nature", "char": "🍀", "name": "four_leaf_clover", "keywords": ["vegetable", "plant", "nature", "lucky", "irish"] },
+	{ "category": "animals_and_nature", "char": "🎍", "name": "bamboo", "keywords": ["plant", "nature", "vegetable", "panda", "pine_decoration"] },
+	{ "category": "animals_and_nature", "char": "🎋", "name": "tanabata_tree", "keywords": ["plant", "nature", "branch", "summer"] },
+	{ "category": "animals_and_nature", "char": "🍃", "name": "leaves", "keywords": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"] },
+	{ "category": "animals_and_nature", "char": "🍂", "name": "fallen_leaf", "keywords": ["nature", "plant", "vegetable", "leaves"] },
+	{ "category": "animals_and_nature", "char": "🍁", "name": "maple_leaf", "keywords": ["nature", "plant", "vegetable", "ca", "fall"] },
+	{ "category": "animals_and_nature", "char": "🌾", "name": "ear_of_rice", "keywords": ["nature", "plant"] },
+	{ "category": "animals_and_nature", "char": "🌺", "name": "hibiscus", "keywords": ["plant", "vegetable", "flowers", "beach"] },
+	{ "category": "animals_and_nature", "char": "🌻", "name": "sunflower", "keywords": ["nature", "plant", "fall"] },
+	{ "category": "animals_and_nature", "char": "🌹", "name": "rose", "keywords": ["flowers", "valentines", "love", "spring"] },
+	{ "category": "animals_and_nature", "char": "🥀", "name": "wilted_flower", "keywords": ["plant", "nature", "flower"] },
+	{ "category": "animals_and_nature", "char": "🌷", "name": "tulip", "keywords": ["flowers", "plant", "nature", "summer", "spring"] },
+	{ "category": "animals_and_nature", "char": "🌼", "name": "blossom", "keywords": ["nature", "flowers", "yellow"] },
+	{ "category": "animals_and_nature", "char": "🌸", "name": "cherry_blossom", "keywords": ["nature", "plant", "spring", "flower"] },
+	{ "category": "animals_and_nature", "char": "💐", "name": "bouquet", "keywords": ["flowers", "nature", "spring"] },
+	{ "category": "animals_and_nature", "char": "🍄", "name": "mushroom", "keywords": ["plant", "vegetable"] },
+	{ "category": "animals_and_nature", "char": "🪴", "name": "potted_plant", "keywords": ["plant"] },
+	{ "category": "animals_and_nature", "char": "🌰", "name": "chestnut", "keywords": ["food", "squirrel"] },
+	{ "category": "animals_and_nature", "char": "🎃", "name": "jack_o_lantern", "keywords": ["halloween", "light", "pumpkin", "creepy", "fall"] },
+	{ "category": "animals_and_nature", "char": "🐚", "name": "shell", "keywords": ["nature", "sea", "beach"] },
+	{ "category": "animals_and_nature", "char": "🕸", "name": "spider_web", "keywords": ["animal", "insect", "arachnid", "silk"] },
+	{ "category": "animals_and_nature", "char": "🌎", "name": "earth_americas", "keywords": ["globe", "world", "USA", "international"] },
+	{ "category": "animals_and_nature", "char": "🌍", "name": "earth_africa", "keywords": ["globe", "world", "international"] },
+	{ "category": "animals_and_nature", "char": "🌏", "name": "earth_asia", "keywords": ["globe", "world", "east", "international"] },
+	{ "category": "animals_and_nature", "char": "🪐", "name": "ringed_planet", "keywords": ["saturn"] },
+	{ "category": "animals_and_nature", "char": "🌕", "name": "full_moon", "keywords": ["nature", "yellow", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌖", "name": "waning_gibbous_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep", "waxing_gibbous_moon"] },
+	{ "category": "animals_and_nature", "char": "🌗", "name": "last_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌘", "name": "waning_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌑", "name": "new_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌒", "name": "waxing_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌓", "name": "first_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌔", "name": "waxing_gibbous_moon", "keywords": ["nature", "night", "sky", "gray", "twilight", "planet", "space", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌚", "name": "new_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌝", "name": "full_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌛", "name": "first_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌜", "name": "last_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
+	{ "category": "animals_and_nature", "char": "🌞", "name": "sun_with_face", "keywords": ["nature", "morning", "sky"] },
+	{ "category": "animals_and_nature", "char": "🌙", "name": "crescent_moon", "keywords": ["night", "sleep", "sky", "evening", "magic"] },
+	{ "category": "animals_and_nature", "char": "⭐", "name": "star", "keywords": ["night", "yellow"] },
+	{ "category": "animals_and_nature", "char": "🌟", "name": "star2", "keywords": ["night", "sparkle", "awesome", "good", "magic"] },
+	{ "category": "animals_and_nature", "char": "💫", "name": "dizzy", "keywords": ["star", "sparkle", "shoot", "magic"] },
+	{ "category": "animals_and_nature", "char": "✨", "name": "sparkles", "keywords": ["stars", "shine", "shiny", "cool", "awesome", "good", "magic"] },
+	{ "category": "animals_and_nature", "char": "☄", "name": "comet", "keywords": ["space"] },
+	{ "category": "animals_and_nature", "char": "☀️", "name": "sunny", "keywords": ["weather", "nature", "brightness", "summer", "beach", "spring"] },
+	{ "category": "animals_and_nature", "char": "🌤", "name": "sun_behind_small_cloud", "keywords": ["weather"] },
+	{ "category": "animals_and_nature", "char": "⛅", "name": "partly_sunny", "keywords": ["weather", "nature", "cloudy", "morning", "fall", "spring"] },
+	{ "category": "animals_and_nature", "char": "🌥", "name": "sun_behind_large_cloud", "keywords": ["weather"] },
+	{ "category": "animals_and_nature", "char": "🌦", "name": "sun_behind_rain_cloud", "keywords": ["weather"] },
+	{ "category": "animals_and_nature", "char": "☁️", "name": "cloud", "keywords": ["weather", "sky"] },
+	{ "category": "animals_and_nature", "char": "🌧", "name": "cloud_with_rain", "keywords": ["weather"] },
+	{ "category": "animals_and_nature", "char": "⛈", "name": "cloud_with_lightning_and_rain", "keywords": ["weather", "lightning"] },
+	{ "category": "animals_and_nature", "char": "🌩", "name": "cloud_with_lightning", "keywords": ["weather", "thunder"] },
+	{ "category": "animals_and_nature", "char": "⚡", "name": "zap", "keywords": ["thunder", "weather", "lightning bolt", "fast"] },
+	{ "category": "animals_and_nature", "char": "🔥", "name": "fire", "keywords": ["hot", "cook", "flame"] },
+	{ "category": "animals_and_nature", "char": "💥", "name": "boom", "keywords": ["bomb", "explode", "explosion", "collision", "blown"] },
+	{ "category": "animals_and_nature", "char": "❄️", "name": "snowflake", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas"] },
+	{ "category": "animals_and_nature", "char": "🌨", "name": "cloud_with_snow", "keywords": ["weather"] },
+	{ "category": "animals_and_nature", "char": "⛄", "name": "snowman", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen", "without_snow"] },
+	{ "category": "animals_and_nature", "char": "☃", "name": "snowman_with_snow", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen"] },
+	{ "category": "animals_and_nature", "char": "🌬", "name": "wind_face", "keywords": ["gust", "air"] },
+	{ "category": "animals_and_nature", "char": "💨", "name": "dash", "keywords": ["wind", "air", "fast", "shoo", "fart", "smoke", "puff"] },
+	{ "category": "animals_and_nature", "char": "🌪", "name": "tornado", "keywords": ["weather", "cyclone", "twister"] },
+	{ "category": "animals_and_nature", "char": "🌫", "name": "fog", "keywords": ["weather"] },
+	{ "category": "animals_and_nature", "char": "☂", "name": "open_umbrella", "keywords": ["weather", "spring"] },
+	{ "category": "animals_and_nature", "char": "☔", "name": "umbrella", "keywords": ["rainy", "weather", "spring"] },
+	{ "category": "animals_and_nature", "char": "💧", "name": "droplet", "keywords": ["water", "drip", "faucet", "spring"] },
+	{ "category": "animals_and_nature", "char": "💦", "name": "sweat_drops", "keywords": ["water", "drip", "oops"] },
+	{ "category": "animals_and_nature", "char": "🌊", "name": "ocean", "keywords": ["sea", "water", "wave", "nature", "tsunami", "disaster"] },
+	{ "category": "food_and_drink", "char": "🍏", "name": "green_apple", "keywords": ["fruit", "nature"] },
+	{ "category": "food_and_drink", "char": "🍎", "name": "apple", "keywords": ["fruit", "mac", "school"] },
+	{ "category": "food_and_drink", "char": "🍐", "name": "pear", "keywords": ["fruit", "nature", "food"] },
+	{ "category": "food_and_drink", "char": "🍊", "name": "tangerine", "keywords": ["food", "fruit", "nature", "orange"] },
+	{ "category": "food_and_drink", "char": "🍋", "name": "lemon", "keywords": ["fruit", "nature"] },
+	{ "category": "food_and_drink", "char": "🍌", "name": "banana", "keywords": ["fruit", "food", "monkey"] },
+	{ "category": "food_and_drink", "char": "🍉", "name": "watermelon", "keywords": ["fruit", "food", "picnic", "summer"] },
+	{ "category": "food_and_drink", "char": "🍇", "name": "grapes", "keywords": ["fruit", "food", "wine"] },
+	{ "category": "food_and_drink", "char": "🍓", "name": "strawberry", "keywords": ["fruit", "food", "nature"] },
+	{ "category": "food_and_drink", "char": "🍈", "name": "melon", "keywords": ["fruit", "nature", "food"] },
+	{ "category": "food_and_drink", "char": "🍒", "name": "cherries", "keywords": ["food", "fruit"] },
+	{ "category": "food_and_drink", "char": "🍑", "name": "peach", "keywords": ["fruit", "nature", "food"] },
+	{ "category": "food_and_drink", "char": "🍍", "name": "pineapple", "keywords": ["fruit", "nature", "food"] },
+	{ "category": "food_and_drink", "char": "🥥", "name": "coconut", "keywords": ["fruit", "nature", "food", "palm"] },
+	{ "category": "food_and_drink", "char": "🥝", "name": "kiwi_fruit", "keywords": ["fruit", "food"] },
+	{ "category": "food_and_drink", "char": "🥭", "name": "mango", "keywords": ["fruit", "food", "tropical"] },
+	{ "category": "food_and_drink", "char": "🥑", "name": "avocado", "keywords": ["fruit", "food"] },
+	{ "category": "food_and_drink", "char": "🥦", "name": "broccoli", "keywords": ["fruit", "food", "vegetable"] },
+	{ "category": "food_and_drink", "char": "🍅", "name": "tomato", "keywords": ["fruit", "vegetable", "nature", "food"] },
+	{ "category": "food_and_drink", "char": "🍆", "name": "eggplant", "keywords": ["vegetable", "nature", "food", "aubergine"] },
+	{ "category": "food_and_drink", "char": "🥒", "name": "cucumber", "keywords": ["fruit", "food", "pickle"] },
+	{ "category": "food_and_drink", "char": "🫐", "name": "blueberries", "keywords": ["fruit", "food"] },
+	{ "category": "food_and_drink", "char": "🫒", "name": "olive", "keywords": ["fruit", "food"] },
+	{ "category": "food_and_drink", "char": "🫑", "name": "bell_pepper", "keywords": ["fruit", "food"] },
+	{ "category": "food_and_drink", "char": "🥕", "name": "carrot", "keywords": ["vegetable", "food", "orange"] },
+	{ "category": "food_and_drink", "char": "🌶", "name": "hot_pepper", "keywords": ["food", "spicy", "chilli", "chili"] },
+	{ "category": "food_and_drink", "char": "🥔", "name": "potato", "keywords": ["food", "tuber", "vegatable", "starch"] },
+	{ "category": "food_and_drink", "char": "🌽", "name": "corn", "keywords": ["food", "vegetable", "plant"] },
+	{ "category": "food_and_drink", "char": "🥬", "name": "leafy_greens", "keywords": ["food", "vegetable", "plant", "bok choy", "cabbage", "kale", "lettuce"] },
+	{ "category": "food_and_drink", "char": "🍠", "name": "sweet_potato", "keywords": ["food", "nature"] },
+	{ "category": "food_and_drink", "char": "🥜", "name": "peanuts", "keywords": ["food", "nut"] },
+	{ "category": "food_and_drink", "char": "🧄", "name": "garlic", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🧅", "name": "onion", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🍯", "name": "honey_pot", "keywords": ["bees", "sweet", "kitchen"] },
+	{ "category": "food_and_drink", "char": "🥐", "name": "croissant", "keywords": ["food", "bread", "french"] },
+	{ "category": "food_and_drink", "char": "🍞", "name": "bread", "keywords": ["food", "wheat", "breakfast", "toast"] },
+	{ "category": "food_and_drink", "char": "🥖", "name": "baguette_bread", "keywords": ["food", "bread", "french"] },
+	{ "category": "food_and_drink", "char": "🥯", "name": "bagel", "keywords": ["food", "bread", "bakery", "schmear"] },
+	{ "category": "food_and_drink", "char": "🥨", "name": "pretzel", "keywords": ["food", "bread", "twisted"] },
+	{ "category": "food_and_drink", "char": "🧀", "name": "cheese", "keywords": ["food", "chadder"] },
+	{ "category": "food_and_drink", "char": "🥚", "name": "egg", "keywords": ["food", "chicken", "breakfast"] },
+	{ "category": "food_and_drink", "char": "🥓", "name": "bacon", "keywords": ["food", "breakfast", "pork", "pig", "meat"] },
+	{ "category": "food_and_drink", "char": "🥩", "name": "steak", "keywords": ["food", "cow", "meat", "cut", "chop", "lambchop", "porkchop"] },
+	{ "category": "food_and_drink", "char": "🥞", "name": "pancakes", "keywords": ["food", "breakfast", "flapjacks", "hotcakes"] },
+	{ "category": "food_and_drink", "char": "🍗", "name": "poultry_leg", "keywords": ["food", "meat", "drumstick", "bird", "chicken", "turkey"] },
+	{ "category": "food_and_drink", "char": "🍖", "name": "meat_on_bone", "keywords": ["good", "food", "drumstick"] },
+	{ "category": "food_and_drink", "char": "🦴", "name": "bone", "keywords": ["skeleton"] },
+	{ "category": "food_and_drink", "char": "🍤", "name": "fried_shrimp", "keywords": ["food", "animal", "appetizer", "summer"] },
+	{ "category": "food_and_drink", "char": "🍳", "name": "fried_egg", "keywords": ["food", "breakfast", "kitchen", "egg"] },
+	{ "category": "food_and_drink", "char": "🍔", "name": "hamburger", "keywords": ["meat", "fast food", "beef", "cheeseburger", "mcdonalds", "burger king"] },
+	{ "category": "food_and_drink", "char": "🍟", "name": "fries", "keywords": ["chips", "snack", "fast food"] },
+	{ "category": "food_and_drink", "char": "🥙", "name": "stuffed_flatbread", "keywords": ["food", "flatbread", "stuffed", "gyro"] },
+	{ "category": "food_and_drink", "char": "🌭", "name": "hotdog", "keywords": ["food", "frankfurter"] },
+	{ "category": "food_and_drink", "char": "🍕", "name": "pizza", "keywords": ["food", "party"] },
+	{ "category": "food_and_drink", "char": "🥪", "name": "sandwich", "keywords": ["food", "lunch", "bread"] },
+	{ "category": "food_and_drink", "char": "🥫", "name": "canned_food", "keywords": ["food", "soup"] },
+	{ "category": "food_and_drink", "char": "🍝", "name": "spaghetti", "keywords": ["food", "italian", "noodle"] },
+	{ "category": "food_and_drink", "char": "🌮", "name": "taco", "keywords": ["food", "mexican"] },
+	{ "category": "food_and_drink", "char": "🌯", "name": "burrito", "keywords": ["food", "mexican"] },
+	{ "category": "food_and_drink", "char": "🥗", "name": "green_salad", "keywords": ["food", "healthy", "lettuce"] },
+	{ "category": "food_and_drink", "char": "🥘", "name": "shallow_pan_of_food", "keywords": ["food", "cooking", "casserole", "paella"] },
+	{ "category": "food_and_drink", "char": "🍜", "name": "ramen", "keywords": ["food", "japanese", "noodle", "chopsticks"] },
+	{ "category": "food_and_drink", "char": "🍲", "name": "stew", "keywords": ["food", "meat", "soup"] },
+	{ "category": "food_and_drink", "char": "🍥", "name": "fish_cake", "keywords": ["food", "japan", "sea", "beach", "narutomaki", "pink", "swirl", "kamaboko", "surimi", "ramen"] },
+	{ "category": "food_and_drink", "char": "🥠", "name": "fortune_cookie", "keywords": ["food", "prophecy"] },
+	{ "category": "food_and_drink", "char": "🍣", "name": "sushi", "keywords": ["food", "fish", "japanese", "rice"] },
+	{ "category": "food_and_drink", "char": "🍱", "name": "bento", "keywords": ["food", "japanese", "box"] },
+	{ "category": "food_and_drink", "char": "🍛", "name": "curry", "keywords": ["food", "spicy", "hot", "indian"] },
+	{ "category": "food_and_drink", "char": "🍙", "name": "rice_ball", "keywords": ["food", "japanese"] },
+	{ "category": "food_and_drink", "char": "🍚", "name": "rice", "keywords": ["food", "china", "asian"] },
+	{ "category": "food_and_drink", "char": "🍘", "name": "rice_cracker", "keywords": ["food", "japanese"] },
+	{ "category": "food_and_drink", "char": "🍢", "name": "oden", "keywords": ["food", "japanese"] },
+	{ "category": "food_and_drink", "char": "🍡", "name": "dango", "keywords": ["food", "dessert", "sweet", "japanese", "barbecue", "meat"] },
+	{ "category": "food_and_drink", "char": "🍧", "name": "shaved_ice", "keywords": ["hot", "dessert", "summer"] },
+	{ "category": "food_and_drink", "char": "🍨", "name": "ice_cream", "keywords": ["food", "hot", "dessert"] },
+	{ "category": "food_and_drink", "char": "🍦", "name": "icecream", "keywords": ["food", "hot", "dessert", "summer"] },
+	{ "category": "food_and_drink", "char": "🥧", "name": "pie", "keywords": ["food", "dessert", "pastry"] },
+	{ "category": "food_and_drink", "char": "🍰", "name": "cake", "keywords": ["food", "dessert"] },
+	{ "category": "food_and_drink", "char": "🧁", "name": "cupcake", "keywords": ["food", "dessert", "bakery", "sweet"] },
+	{ "category": "food_and_drink", "char": "🥮", "name": "moon_cake", "keywords": ["food", "autumn"] },
+	{ "category": "food_and_drink", "char": "🎂", "name": "birthday", "keywords": ["food", "dessert", "cake"] },
+	{ "category": "food_and_drink", "char": "🍮", "name": "custard", "keywords": ["dessert", "food"] },
+	{ "category": "food_and_drink", "char": "🍬", "name": "candy", "keywords": ["snack", "dessert", "sweet", "lolly"] },
+	{ "category": "food_and_drink", "char": "🍭", "name": "lollipop", "keywords": ["food", "snack", "candy", "sweet"] },
+	{ "category": "food_and_drink", "char": "🍫", "name": "chocolate_bar", "keywords": ["food", "snack", "dessert", "sweet"] },
+	{ "category": "food_and_drink", "char": "🍿", "name": "popcorn", "keywords": ["food", "movie theater", "films", "snack"] },
+	{ "category": "food_and_drink", "char": "🥟", "name": "dumpling", "keywords": ["food", "empanada", "pierogi", "potsticker"] },
+	{ "category": "food_and_drink", "char": "🍩", "name": "doughnut", "keywords": ["food", "dessert", "snack", "sweet", "donut"] },
+	{ "category": "food_and_drink", "char": "🍪", "name": "cookie", "keywords": ["food", "snack", "oreo", "chocolate", "sweet", "dessert"] },
+	{ "category": "food_and_drink", "char": "🧇", "name": "waffle", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🧆", "name": "falafel", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🧈", "name": "butter", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🦪", "name": "oyster", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🫓", "name": "flatbread", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🫔", "name": "tamale", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🫕", "name": "fondue", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🥛", "name": "milk_glass", "keywords": ["beverage", "drink", "cow"] },
+	{ "category": "food_and_drink", "char": "🍺", "name": "beer", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] },
+	{ "category": "food_and_drink", "char": "🍻", "name": "beers", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] },
+	{ "category": "food_and_drink", "char": "🥂", "name": "clinking_glasses", "keywords": ["beverage", "drink", "party", "alcohol", "celebrate", "cheers", "wine", "champagne", "toast"] },
+	{ "category": "food_and_drink", "char": "🍷", "name": "wine_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "booze"] },
+	{ "category": "food_and_drink", "char": "🥃", "name": "tumbler_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "liquor", "booze", "bourbon", "scotch", "whisky", "glass", "shot"] },
+	{ "category": "food_and_drink", "char": "🍸", "name": "cocktail", "keywords": ["drink", "drunk", "alcohol", "beverage", "booze", "mojito"] },
+	{ "category": "food_and_drink", "char": "🍹", "name": "tropical_drink", "keywords": ["beverage", "cocktail", "summer", "beach", "alcohol", "booze", "mojito"] },
+	{ "category": "food_and_drink", "char": "🍾", "name": "champagne", "keywords": ["drink", "wine", "bottle", "celebration"] },
+	{ "category": "food_and_drink", "char": "🍶", "name": "sake", "keywords": ["wine", "drink", "drunk", "beverage", "japanese", "alcohol", "booze"] },
+	{ "category": "food_and_drink", "char": "🍵", "name": "tea", "keywords": ["drink", "bowl", "breakfast", "green", "british"] },
+	{ "category": "food_and_drink", "char": "🥤", "name": "cup_with_straw", "keywords": ["drink", "soda"] },
+	{ "category": "food_and_drink", "char": "☕", "name": "coffee", "keywords": ["beverage", "caffeine", "latte", "espresso"] },
+	{ "category": "food_and_drink", "char": "🫖", "name": "teapot", "keywords": [] },
+	{ "category": "food_and_drink", "char": "🧋", "name": "bubble_tea", "keywords": ["tapioca"] },
+	{ "category": "food_and_drink", "char": "🍼", "name": "baby_bottle", "keywords": ["food", "container", "milk"] },
+	{ "category": "food_and_drink", "char": "🧃", "name": "beverage_box", "keywords": ["food", "drink"] },
+	{ "category": "food_and_drink", "char": "🧉", "name": "mate", "keywords": ["food", "drink"] },
+	{ "category": "food_and_drink", "char": "🧊", "name": "ice_cube", "keywords": ["food"] },
+	{ "category": "food_and_drink", "char": "🧂", "name": "salt", "keywords": ["condiment", "shaker"] },
+	{ "category": "food_and_drink", "char": "🥄", "name": "spoon", "keywords": ["cutlery", "kitchen", "tableware"] },
+	{ "category": "food_and_drink", "char": "🍴", "name": "fork_and_knife", "keywords": ["cutlery", "kitchen"] },
+	{ "category": "food_and_drink", "char": "🍽", "name": "plate_with_cutlery", "keywords": ["food", "eat", "meal", "lunch", "dinner", "restaurant"] },
+	{ "category": "food_and_drink", "char": "🥣", "name": "bowl_with_spoon", "keywords": ["food", "breakfast", "cereal", "oatmeal", "porridge"] },
+	{ "category": "food_and_drink", "char": "🥡", "name": "takeout_box", "keywords": ["food", "leftovers"] },
+	{ "category": "food_and_drink", "char": "🥢", "name": "chopsticks", "keywords": ["food"] },
+	{ "category": "activity", "char": "⚽", "name": "soccer", "keywords": ["sports", "football"] },
+	{ "category": "activity", "char": "🏀", "name": "basketball", "keywords": ["sports", "balls", "NBA"] },
+	{ "category": "activity", "char": "🏈", "name": "football", "keywords": ["sports", "balls", "NFL"] },
+	{ "category": "activity", "char": "⚾", "name": "baseball", "keywords": ["sports", "balls"] },
+	{ "category": "activity", "char": "🥎", "name": "softball", "keywords": ["sports", "balls"] },
+	{ "category": "activity", "char": "🎾", "name": "tennis", "keywords": ["sports", "balls", "green"] },
+	{ "category": "activity", "char": "🏐", "name": "volleyball", "keywords": ["sports", "balls"] },
+	{ "category": "activity", "char": "🏉", "name": "rugby_football", "keywords": ["sports", "team"] },
+	{ "category": "activity", "char": "🥏", "name": "flying_disc", "keywords": ["sports", "frisbee", "ultimate"] },
+	{ "category": "activity", "char": "🎱", "name": "8ball", "keywords": ["pool", "hobby", "game", "luck", "magic"] },
+	{ "category": "activity", "char": "⛳", "name": "golf", "keywords": ["sports", "business", "flag", "hole", "summer"] },
+	{ "category": "activity", "char": "🏌️‍♀️", "name": "golfing_woman", "keywords": ["sports", "business", "woman", "female"] },
+	{ "category": "activity", "char": "🏌", "name": "golfing_man", "keywords": ["sports", "business"] },
+	{ "category": "activity", "char": "🏓", "name": "ping_pong", "keywords": ["sports", "pingpong"] },
+	{ "category": "activity", "char": "🏸", "name": "badminton", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🥅", "name": "goal_net", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🏒", "name": "ice_hockey", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🏑", "name": "field_hockey", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🥍", "name": "lacrosse", "keywords": ["sports", "ball", "stick"] },
+	{ "category": "activity", "char": "🏏", "name": "cricket", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🎿", "name": "ski", "keywords": ["sports", "winter", "cold", "snow"] },
+	{ "category": "activity", "char": "⛷", "name": "skier", "keywords": ["sports", "winter", "snow"] },
+	{ "category": "activity", "char": "🏂", "name": "snowboarder", "keywords": ["sports", "winter"] },
+	{ "category": "activity", "char": "🤺", "name": "person_fencing", "keywords": ["sports", "fencing", "sword"] },
+	{ "category": "activity", "char": "🤼‍♀️", "name": "women_wrestling", "keywords": ["sports", "wrestlers"] },
+	{ "category": "activity", "char": "🤼‍♂️", "name": "men_wrestling", "keywords": ["sports", "wrestlers"] },
+	{ "category": "activity", "char": "🤸‍♀️", "name": "woman_cartwheeling", "keywords": ["gymnastics"] },
+	{ "category": "activity", "char": "🤸‍♂️", "name": "man_cartwheeling", "keywords": ["gymnastics"] },
+	{ "category": "activity", "char": "🤾‍♀️", "name": "woman_playing_handball", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🤾‍♂️", "name": "man_playing_handball", "keywords": ["sports"] },
+	{ "category": "activity", "char": "⛸", "name": "ice_skate", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🥌", "name": "curling_stone", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🛹", "name": "skateboard", "keywords": ["board"] },
+	{ "category": "activity", "char": "🛷", "name": "sled", "keywords": ["sleigh", "luge", "toboggan"] },
+	{ "category": "activity", "char": "🏹", "name": "bow_and_arrow", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🎣", "name": "fishing_pole_and_fish", "keywords": ["food", "hobby", "summer"] },
+	{ "category": "activity", "char": "🥊", "name": "boxing_glove", "keywords": ["sports", "fighting"] },
+	{ "category": "activity", "char": "🥋", "name": "martial_arts_uniform", "keywords": ["judo", "karate", "taekwondo"] },
+	{ "category": "activity", "char": "🚣‍♀️", "name": "rowing_woman", "keywords": ["sports", "hobby", "water", "ship", "woman", "female"] },
+	{ "category": "activity", "char": "🚣", "name": "rowing_man", "keywords": ["sports", "hobby", "water", "ship"] },
+	{ "category": "activity", "char": "🧗‍♀️", "name": "climbing_woman", "keywords": ["sports", "hobby", "woman", "female", "rock"] },
+	{ "category": "activity", "char": "🧗‍♂️", "name": "climbing_man", "keywords": ["sports", "hobby", "man", "male", "rock"] },
+	{ "category": "activity", "char": "🏊‍♀️", "name": "swimming_woman", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer", "woman", "female"] },
+	{ "category": "activity", "char": "🏊", "name": "swimming_man", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer"] },
+	{ "category": "activity", "char": "🤽‍♀️", "name": "woman_playing_water_polo", "keywords": ["sports", "pool"] },
+	{ "category": "activity", "char": "🤽‍♂️", "name": "man_playing_water_polo", "keywords": ["sports", "pool"] },
+	{ "category": "activity", "char": "🧘‍♀️", "name": "woman_in_lotus_position", "keywords": ["woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"] },
+	{ "category": "activity", "char": "🧘‍♂️", "name": "man_in_lotus_position", "keywords": ["man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"] },
+	{ "category": "activity", "char": "🏄‍♀️", "name": "surfing_woman", "keywords": ["sports", "ocean", "sea", "summer", "beach", "woman", "female"] },
+	{ "category": "activity", "char": "🏄", "name": "surfing_man", "keywords": ["sports", "ocean", "sea", "summer", "beach"] },
+	{ "category": "activity", "char": "🛀", "name": "bath", "keywords": ["clean", "shower", "bathroom"] },
+	{ "category": "activity", "char": "⛹️‍♀️", "name": "basketball_woman", "keywords": ["sports", "human", "woman", "female"] },
+	{ "category": "activity", "char": "⛹", "name": "basketball_man", "keywords": ["sports", "human"] },
+	{ "category": "activity", "char": "🏋️‍♀️", "name": "weight_lifting_woman", "keywords": ["sports", "training", "exercise", "woman", "female"] },
+	{ "category": "activity", "char": "🏋", "name": "weight_lifting_man", "keywords": ["sports", "training", "exercise"] },
+	{ "category": "activity", "char": "🚴‍♀️", "name": "biking_woman", "keywords": ["sports", "bike", "exercise", "hipster", "woman", "female"] },
+	{ "category": "activity", "char": "🚴", "name": "biking_man", "keywords": ["sports", "bike", "exercise", "hipster"] },
+	{ "category": "activity", "char": "🚵‍♀️", "name": "mountain_biking_woman", "keywords": ["transportation", "sports", "human", "race", "bike", "woman", "female"] },
+	{ "category": "activity", "char": "🚵", "name": "mountain_biking_man", "keywords": ["transportation", "sports", "human", "race", "bike"] },
+	{ "category": "activity", "char": "🏇", "name": "horse_racing", "keywords": ["animal", "betting", "competition", "gambling", "luck"] },
+	{ "category": "activity", "char": "🤿", "name": "diving_mask", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🪀", "name": "yo_yo", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🪁", "name": "kite", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🦺", "name": "safety_vest", "keywords": ["sports"] },
+	{ "category": "activity", "char": "🪡", "name": "sewing_needle", "keywords": [] },
+	{ "category": "activity", "char": "🪢", "name": "knot", "keywords": [] },
+	{ "category": "activity", "char": "🕴", "name": "business_suit_levitating", "keywords": ["suit", "business", "levitate", "hover", "jump"] },
+	{ "category": "activity", "char": "🏆", "name": "trophy", "keywords": ["win", "award", "contest", "place", "ftw", "ceremony"] },
+	{ "category": "activity", "char": "🎽", "name": "running_shirt_with_sash", "keywords": ["play", "pageant"] },
+	{ "category": "activity", "char": "🏅", "name": "medal_sports", "keywords": ["award", "winning"] },
+	{ "category": "activity", "char": "🎖", "name": "medal_military", "keywords": ["award", "winning", "army"] },
+	{ "category": "activity", "char": "🥇", "name": "1st_place_medal", "keywords": ["award", "winning", "first"] },
+	{ "category": "activity", "char": "🥈", "name": "2nd_place_medal", "keywords": ["award", "second"] },
+	{ "category": "activity", "char": "🥉", "name": "3rd_place_medal", "keywords": ["award", "third"] },
+	{ "category": "activity", "char": "🎗", "name": "reminder_ribbon", "keywords": ["sports", "cause", "support", "awareness"] },
+	{ "category": "activity", "char": "🏵", "name": "rosette", "keywords": ["flower", "decoration", "military"] },
+	{ "category": "activity", "char": "🎫", "name": "ticket", "keywords": ["event", "concert", "pass"] },
+	{ "category": "activity", "char": "🎟", "name": "tickets", "keywords": ["sports", "concert", "entrance"] },
+	{ "category": "activity", "char": "🎭", "name": "performing_arts", "keywords": ["acting", "theater", "drama"] },
+	{ "category": "activity", "char": "🎨", "name": "art", "keywords": ["design", "paint", "draw", "colors"] },
+	{ "category": "activity", "char": "🎪", "name": "circus_tent", "keywords": ["festival", "carnival", "party"] },
+	{ "category": "activity", "char": "🤹‍♀️", "name": "woman_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] },
+	{ "category": "activity", "char": "🤹‍♂️", "name": "man_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] },
+	{ "category": "activity", "char": "🎤", "name": "microphone", "keywords": ["sound", "music", "PA", "sing", "talkshow"] },
+	{ "category": "activity", "char": "🎧", "name": "headphones", "keywords": ["music", "score", "gadgets"] },
+	{ "category": "activity", "char": "🎼", "name": "musical_score", "keywords": ["treble", "clef", "compose"] },
+	{ "category": "activity", "char": "🎹", "name": "musical_keyboard", "keywords": ["piano", "instrument", "compose"] },
+	{ "category": "activity", "char": "🥁", "name": "drum", "keywords": ["music", "instrument", "drumsticks", "snare"] },
+	{ "category": "activity", "char": "🎷", "name": "saxophone", "keywords": ["music", "instrument", "jazz", "blues"] },
+	{ "category": "activity", "char": "🎺", "name": "trumpet", "keywords": ["music", "brass"] },
+	{ "category": "activity", "char": "🎸", "name": "guitar", "keywords": ["music", "instrument"] },
+	{ "category": "activity", "char": "🎻", "name": "violin", "keywords": ["music", "instrument", "orchestra", "symphony"] },
+	{ "category": "activity", "char": "🪕", "name": "banjo", "keywords": ["music", "instrument"] },
+	{ "category": "activity", "char": "🪗", "name": "accordion", "keywords": ["music", "instrument"] },
+	{ "category": "activity", "char": "🪘", "name": "long_drum", "keywords": ["music", "instrument"] },
+	{ "category": "activity", "char": "🎬", "name": "clapper", "keywords": ["movie", "film", "record"] },
+	{ "category": "activity", "char": "🎮", "name": "video_game", "keywords": ["play", "console", "PS4", "controller"] },
+	{ "category": "activity", "char": "👾", "name": "space_invader", "keywords": ["game", "arcade", "play"] },
+	{ "category": "activity", "char": "🎯", "name": "dart", "keywords": ["game", "play", "bar", "target", "bullseye"] },
+	{ "category": "activity", "char": "🎲", "name": "game_die", "keywords": ["dice", "random", "tabletop", "play", "luck"] },
+	{ "category": "activity", "char": "♟️", "name": "chess_pawn", "keywords": ["expendable"] },
+	{ "category": "activity", "char": "🎰", "name": "slot_machine", "keywords": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"] },
+	{ "category": "activity", "char": "🧩", "name": "jigsaw", "keywords": ["interlocking", "puzzle", "piece"] },
+	{ "category": "activity", "char": "🎳", "name": "bowling", "keywords": ["sports", "fun", "play"] },
+	{ "category": "activity", "char": "🪄", "name": "magic_wand", "keywords": [] },
+	{ "category": "activity", "char": "🪅", "name": "pinata", "keywords": [] },
+	{ "category": "activity", "char": "🪆", "name": "nesting_dolls", "keywords": [] },
+	{ "category": "travel_and_places", "char": "🚗", "name": "red_car", "keywords": ["red", "transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚕", "name": "taxi", "keywords": ["uber", "vehicle", "cars", "transportation"] },
+	{ "category": "travel_and_places", "char": "🚙", "name": "blue_car", "keywords": ["transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚌", "name": "bus", "keywords": ["car", "vehicle", "transportation"] },
+	{ "category": "travel_and_places", "char": "🚎", "name": "trolleybus", "keywords": ["bart", "transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🏎", "name": "racing_car", "keywords": ["sports", "race", "fast", "formula", "f1"] },
+	{ "category": "travel_and_places", "char": "🚓", "name": "police_car", "keywords": ["vehicle", "cars", "transportation", "law", "legal", "enforcement"] },
+	{ "category": "travel_and_places", "char": "🚑", "name": "ambulance", "keywords": ["health", "911", "hospital"] },
+	{ "category": "travel_and_places", "char": "🚒", "name": "fire_engine", "keywords": ["transportation", "cars", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚐", "name": "minibus", "keywords": ["vehicle", "car", "transportation"] },
+	{ "category": "travel_and_places", "char": "🚚", "name": "truck", "keywords": ["cars", "transportation"] },
+	{ "category": "travel_and_places", "char": "🚛", "name": "articulated_lorry", "keywords": ["vehicle", "cars", "transportation", "express"] },
+	{ "category": "travel_and_places", "char": "🚜", "name": "tractor", "keywords": ["vehicle", "car", "farming", "agriculture"] },
+	{ "category": "travel_and_places", "char": "🛴", "name": "kick_scooter", "keywords": ["vehicle", "kick", "razor"] },
+	{ "category": "travel_and_places", "char": "🏍", "name": "motorcycle", "keywords": ["race", "sports", "fast"] },
+	{ "category": "travel_and_places", "char": "🚲", "name": "bike", "keywords": ["sports", "bicycle", "exercise", "hipster"] },
+	{ "category": "travel_and_places", "char": "🛵", "name": "motor_scooter", "keywords": ["vehicle", "vespa", "sasha"] },
+	{ "category": "travel_and_places", "char": "🦽", "name": "manual_wheelchair", "keywords": ["vehicle"] },
+	{ "category": "travel_and_places", "char": "🦼", "name": "motorized_wheelchair", "keywords": ["vehicle"] },
+	{ "category": "travel_and_places", "char": "🛺", "name": "auto_rickshaw", "keywords": ["vehicle"] },
+	{ "category": "travel_and_places", "char": "🪂", "name": "parachute", "keywords": ["vehicle"] },
+	{ "category": "travel_and_places", "char": "🚨", "name": "rotating_light", "keywords": ["police", "ambulance", "911", "emergency", "alert", "error", "pinged", "law", "legal"] },
+	{ "category": "travel_and_places", "char": "🚔", "name": "oncoming_police_car", "keywords": ["vehicle", "law", "legal", "enforcement", "911"] },
+	{ "category": "travel_and_places", "char": "🚍", "name": "oncoming_bus", "keywords": ["vehicle", "transportation"] },
+	{ "category": "travel_and_places", "char": "🚘", "name": "oncoming_automobile", "keywords": ["car", "vehicle", "transportation"] },
+	{ "category": "travel_and_places", "char": "🚖", "name": "oncoming_taxi", "keywords": ["vehicle", "cars", "uber"] },
+	{ "category": "travel_and_places", "char": "🚡", "name": "aerial_tramway", "keywords": ["transportation", "vehicle", "ski"] },
+	{ "category": "travel_and_places", "char": "🚠", "name": "mountain_cableway", "keywords": ["transportation", "vehicle", "ski"] },
+	{ "category": "travel_and_places", "char": "🚟", "name": "suspension_railway", "keywords": ["vehicle", "transportation"] },
+	{ "category": "travel_and_places", "char": "🚃", "name": "railway_car", "keywords": ["transportation", "vehicle", "train"] },
+	{ "category": "travel_and_places", "char": "🚋", "name": "train", "keywords": ["transportation", "vehicle", "carriage", "public", "travel"] },
+	{ "category": "travel_and_places", "char": "🚝", "name": "monorail", "keywords": ["transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚄", "name": "bullettrain_side", "keywords": ["transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚅", "name": "bullettrain_front", "keywords": ["transportation", "vehicle", "speed", "fast", "public", "travel"] },
+	{ "category": "travel_and_places", "char": "🚈", "name": "light_rail", "keywords": ["transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚞", "name": "mountain_railway", "keywords": ["transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚂", "name": "steam_locomotive", "keywords": ["transportation", "vehicle", "train"] },
+	{ "category": "travel_and_places", "char": "🚆", "name": "train2", "keywords": ["transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚇", "name": "metro", "keywords": ["transportation", "blue-square", "mrt", "underground", "tube"] },
+	{ "category": "travel_and_places", "char": "🚊", "name": "tram", "keywords": ["transportation", "vehicle"] },
+	{ "category": "travel_and_places", "char": "🚉", "name": "station", "keywords": ["transportation", "vehicle", "public"] },
+	{ "category": "travel_and_places", "char": "🛸", "name": "flying_saucer", "keywords": ["transportation", "vehicle", "ufo"] },
+	{ "category": "travel_and_places", "char": "🚁", "name": "helicopter", "keywords": ["transportation", "vehicle", "fly"] },
+	{ "category": "travel_and_places", "char": "🛩", "name": "small_airplane", "keywords": ["flight", "transportation", "fly", "vehicle"] },
+	{ "category": "travel_and_places", "char": "✈️", "name": "airplane", "keywords": ["vehicle", "transportation", "flight", "fly"] },
+	{ "category": "travel_and_places", "char": "🛫", "name": "flight_departure", "keywords": ["airport", "flight", "landing"] },
+	{ "category": "travel_and_places", "char": "🛬", "name": "flight_arrival", "keywords": ["airport", "flight", "boarding"] },
+	{ "category": "travel_and_places", "char": "⛵", "name": "sailboat", "keywords": ["ship", "summer", "transportation", "water", "sailing"] },
+	{ "category": "travel_and_places", "char": "🛥", "name": "motor_boat", "keywords": ["ship"] },
+	{ "category": "travel_and_places", "char": "🚤", "name": "speedboat", "keywords": ["ship", "transportation", "vehicle", "summer"] },
+	{ "category": "travel_and_places", "char": "⛴", "name": "ferry", "keywords": ["boat", "ship", "yacht"] },
+	{ "category": "travel_and_places", "char": "🛳", "name": "passenger_ship", "keywords": ["yacht", "cruise", "ferry"] },
+	{ "category": "travel_and_places", "char": "🚀", "name": "rocket", "keywords": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"] },
+	{ "category": "travel_and_places", "char": "🛰", "name": "artificial_satellite", "keywords": ["communication", "gps", "orbit", "spaceflight", "NASA", "ISS"] },
+	{ "category": "travel_and_places", "char": "🛻", "name": "pickup_truck", "keywords": ["car"] },
+	{ "category": "travel_and_places", "char": "🛼", "name": "roller_skate", "keywords": [] },
+	{ "category": "travel_and_places", "char": "💺", "name": "seat", "keywords": ["sit", "airplane", "transport", "bus", "flight", "fly"] },
+	{ "category": "travel_and_places", "char": "🛶", "name": "canoe", "keywords": ["boat", "paddle", "water", "ship"] },
+	{ "category": "travel_and_places", "char": "⚓", "name": "anchor", "keywords": ["ship", "ferry", "sea", "boat"] },
+	{ "category": "travel_and_places", "char": "🚧", "name": "construction", "keywords": ["wip", "progress", "caution", "warning"] },
+	{ "category": "travel_and_places", "char": "⛽", "name": "fuelpump", "keywords": ["gas station", "petroleum"] },
+	{ "category": "travel_and_places", "char": "🚏", "name": "busstop", "keywords": ["transportation", "wait"] },
+	{ "category": "travel_and_places", "char": "🚦", "name": "vertical_traffic_light", "keywords": ["transportation", "driving"] },
+	{ "category": "travel_and_places", "char": "🚥", "name": "traffic_light", "keywords": ["transportation", "signal"] },
+	{ "category": "travel_and_places", "char": "🏁", "name": "checkered_flag", "keywords": ["contest", "finishline", "race", "gokart"] },
+	{ "category": "travel_and_places", "char": "🚢", "name": "ship", "keywords": ["transportation", "titanic", "deploy"] },
+	{ "category": "travel_and_places", "char": "🎡", "name": "ferris_wheel", "keywords": ["photo", "carnival", "londoneye"] },
+	{ "category": "travel_and_places", "char": "🎢", "name": "roller_coaster", "keywords": ["carnival", "playground", "photo", "fun"] },
+	{ "category": "travel_and_places", "char": "🎠", "name": "carousel_horse", "keywords": ["photo", "carnival"] },
+	{ "category": "travel_and_places", "char": "🏗", "name": "building_construction", "keywords": ["wip", "working", "progress"] },
+	{ "category": "travel_and_places", "char": "🌁", "name": "foggy", "keywords": ["photo", "mountain"] },
+	{ "category": "travel_and_places", "char": "🏭", "name": "factory", "keywords": ["building", "industry", "pollution", "smoke"] },
+	{ "category": "travel_and_places", "char": "⛲", "name": "fountain", "keywords": ["photo", "summer", "water", "fresh"] },
+	{ "category": "travel_and_places", "char": "🎑", "name": "rice_scene", "keywords": ["photo", "japan", "asia", "tsukimi"] },
+	{ "category": "travel_and_places", "char": "⛰", "name": "mountain", "keywords": ["photo", "nature", "environment"] },
+	{ "category": "travel_and_places", "char": "🏔", "name": "mountain_snow", "keywords": ["photo", "nature", "environment", "winter", "cold"] },
+	{ "category": "travel_and_places", "char": "🗻", "name": "mount_fuji", "keywords": ["photo", "mountain", "nature", "japanese"] },
+	{ "category": "travel_and_places", "char": "🌋", "name": "volcano", "keywords": ["photo", "nature", "disaster"] },
+	{ "category": "travel_and_places", "char": "🗾", "name": "japan", "keywords": ["nation", "country", "japanese", "asia"] },
+	{ "category": "travel_and_places", "char": "🏕", "name": "camping", "keywords": ["photo", "outdoors", "tent"] },
+	{ "category": "travel_and_places", "char": "⛺", "name": "tent", "keywords": ["photo", "camping", "outdoors"] },
+	{ "category": "travel_and_places", "char": "🏞", "name": "national_park", "keywords": ["photo", "environment", "nature"] },
+	{ "category": "travel_and_places", "char": "🛣", "name": "motorway", "keywords": ["road", "cupertino", "interstate", "highway"] },
+	{ "category": "travel_and_places", "char": "🛤", "name": "railway_track", "keywords": ["train", "transportation"] },
+	{ "category": "travel_and_places", "char": "🌅", "name": "sunrise", "keywords": ["morning", "view", "vacation", "photo"] },
+	{ "category": "travel_and_places", "char": "🌄", "name": "sunrise_over_mountains", "keywords": ["view", "vacation", "photo"] },
+	{ "category": "travel_and_places", "char": "🏜", "name": "desert", "keywords": ["photo", "warm", "saharah"] },
+	{ "category": "travel_and_places", "char": "🏖", "name": "beach_umbrella", "keywords": ["weather", "summer", "sunny", "sand", "mojito"] },
+	{ "category": "travel_and_places", "char": "🏝", "name": "desert_island", "keywords": ["photo", "tropical", "mojito"] },
+	{ "category": "travel_and_places", "char": "🌇", "name": "city_sunrise", "keywords": ["photo", "good morning", "dawn"] },
+	{ "category": "travel_and_places", "char": "🌆", "name": "city_sunset", "keywords": ["photo", "evening", "sky", "buildings"] },
+	{ "category": "travel_and_places", "char": "🏙", "name": "cityscape", "keywords": ["photo", "night life", "urban"] },
+	{ "category": "travel_and_places", "char": "🌃", "name": "night_with_stars", "keywords": ["evening", "city", "downtown"] },
+	{ "category": "travel_and_places", "char": "🌉", "name": "bridge_at_night", "keywords": ["photo", "sanfrancisco"] },
+	{ "category": "travel_and_places", "char": "🌌", "name": "milky_way", "keywords": ["photo", "space", "stars"] },
+	{ "category": "travel_and_places", "char": "🌠", "name": "stars", "keywords": ["night", "photo"] },
+	{ "category": "travel_and_places", "char": "🎇", "name": "sparkler", "keywords": ["stars", "night", "shine"] },
+	{ "category": "travel_and_places", "char": "🎆", "name": "fireworks", "keywords": ["photo", "festival", "carnival", "congratulations"] },
+	{ "category": "travel_and_places", "char": "🌈", "name": "rainbow", "keywords": ["nature", "happy", "unicorn_face", "photo", "sky", "spring"] },
+	{ "category": "travel_and_places", "char": "🏘", "name": "houses", "keywords": ["buildings", "photo"] },
+	{ "category": "travel_and_places", "char": "🏰", "name": "european_castle", "keywords": ["building", "royalty", "history"] },
+	{ "category": "travel_and_places", "char": "🏯", "name": "japanese_castle", "keywords": ["photo", "building"] },
+	{ "category": "travel_and_places", "char": "🗼", "name": "tokyo_tower", "keywords": ["photo", "japanese"] },
+	{ "category": "travel_and_places", "char": "", "name": "shibuya_109", "keywords": ["photo", "japanese"] },
+	{ "category": "travel_and_places", "char": "🏟", "name": "stadium", "keywords": ["photo", "place", "sports", "concert", "venue"] },
+	{ "category": "travel_and_places", "char": "🗽", "name": "statue_of_liberty", "keywords": ["american", "newyork"] },
+	{ "category": "travel_and_places", "char": "🏠", "name": "house", "keywords": ["building", "home"] },
+	{ "category": "travel_and_places", "char": "🏡", "name": "house_with_garden", "keywords": ["home", "plant", "nature"] },
+	{ "category": "travel_and_places", "char": "🏚", "name": "derelict_house", "keywords": ["abandon", "evict", "broken", "building"] },
+	{ "category": "travel_and_places", "char": "🏢", "name": "office", "keywords": ["building", "bureau", "work"] },
+	{ "category": "travel_and_places", "char": "🏬", "name": "department_store", "keywords": ["building", "shopping", "mall"] },
+	{ "category": "travel_and_places", "char": "🏣", "name": "post_office", "keywords": ["building", "envelope", "communication"] },
+	{ "category": "travel_and_places", "char": "🏤", "name": "european_post_office", "keywords": ["building", "email"] },
+	{ "category": "travel_and_places", "char": "🏥", "name": "hospital", "keywords": ["building", "health", "surgery", "doctor"] },
+	{ "category": "travel_and_places", "char": "🏦", "name": "bank", "keywords": ["building", "money", "sales", "cash", "business", "enterprise"] },
+	{ "category": "travel_and_places", "char": "🏨", "name": "hotel", "keywords": ["building", "accomodation", "checkin"] },
+	{ "category": "travel_and_places", "char": "🏪", "name": "convenience_store", "keywords": ["building", "shopping", "groceries"] },
+	{ "category": "travel_and_places", "char": "🏫", "name": "school", "keywords": ["building", "student", "education", "learn", "teach"] },
+	{ "category": "travel_and_places", "char": "🏩", "name": "love_hotel", "keywords": ["like", "affection", "dating"] },
+	{ "category": "travel_and_places", "char": "💒", "name": "wedding", "keywords": ["love", "like", "affection", "couple", "marriage", "bride", "groom"] },
+	{ "category": "travel_and_places", "char": "🏛", "name": "classical_building", "keywords": ["art", "culture", "history"] },
+	{ "category": "travel_and_places", "char": "⛪", "name": "church", "keywords": ["building", "religion", "christ"] },
+	{ "category": "travel_and_places", "char": "🕌", "name": "mosque", "keywords": ["islam", "worship", "minaret"] },
+	{ "category": "travel_and_places", "char": "🕍", "name": "synagogue", "keywords": ["judaism", "worship", "temple", "jewish"] },
+	{ "category": "travel_and_places", "char": "🕋", "name": "kaaba", "keywords": ["mecca", "mosque", "islam"] },
+	{ "category": "travel_and_places", "char": "⛩", "name": "shinto_shrine", "keywords": ["temple", "japan", "kyoto"] },
+	{ "category": "travel_and_places", "char": "🛕", "name": "hindu_temple", "keywords": ["temple"] },
+
+	{ "category": "travel_and_places", "char": "🪨", "name": "rock", "keywords": [] },
+	{ "category": "travel_and_places", "char": "🪵", "name": "wood", "keywords": [] },
+	{ "category": "travel_and_places", "char": "🛖", "name": "hut", "keywords": [] },
+
+	{ "category": "objects", "char": "⌚", "name": "watch", "keywords": ["time", "accessories"] },
+	{ "category": "objects", "char": "📱", "name": "iphone", "keywords": ["technology", "apple", "gadgets", "dial"] },
+	{ "category": "objects", "char": "📲", "name": "calling", "keywords": ["iphone", "incoming"] },
+	{ "category": "objects", "char": "💻", "name": "computer", "keywords": ["technology", "laptop", "screen", "display", "monitor"] },
+	{ "category": "objects", "char": "⌨", "name": "keyboard", "keywords": ["technology", "computer", "type", "input", "text"] },
+	{ "category": "objects", "char": "🖥", "name": "desktop_computer", "keywords": ["technology", "computing", "screen"] },
+	{ "category": "objects", "char": "🖨", "name": "printer", "keywords": ["paper", "ink"] },
+	{ "category": "objects", "char": "🖱", "name": "computer_mouse", "keywords": ["click"] },
+	{ "category": "objects", "char": "🖲", "name": "trackball", "keywords": ["technology", "trackpad"] },
+	{ "category": "objects", "char": "🕹", "name": "joystick", "keywords": ["game", "play"] },
+	{ "category": "objects", "char": "🗜", "name": "clamp", "keywords": ["tool"] },
+	{ "category": "objects", "char": "💽", "name": "minidisc", "keywords": ["technology", "record", "data", "disk", "90s"] },
+	{ "category": "objects", "char": "💾", "name": "floppy_disk", "keywords": ["oldschool", "technology", "save", "90s", "80s"] },
+	{ "category": "objects", "char": "💿", "name": "cd", "keywords": ["technology", "dvd", "disk", "disc", "90s"] },
+	{ "category": "objects", "char": "📀", "name": "dvd", "keywords": ["cd", "disk", "disc"] },
+	{ "category": "objects", "char": "📼", "name": "vhs", "keywords": ["record", "video", "oldschool", "90s", "80s"] },
+	{ "category": "objects", "char": "📷", "name": "camera", "keywords": ["gadgets", "photography"] },
+	{ "category": "objects", "char": "📸", "name": "camera_flash", "keywords": ["photography", "gadgets"] },
+	{ "category": "objects", "char": "📹", "name": "video_camera", "keywords": ["film", "record"] },
+	{ "category": "objects", "char": "🎥", "name": "movie_camera", "keywords": ["film", "record"] },
+	{ "category": "objects", "char": "📽", "name": "film_projector", "keywords": ["video", "tape", "record", "movie"] },
+	{ "category": "objects", "char": "🎞", "name": "film_strip", "keywords": ["movie"] },
+	{ "category": "objects", "char": "📞", "name": "telephone_receiver", "keywords": ["technology", "communication", "dial"] },
+	{ "category": "objects", "char": "☎️", "name": "phone", "keywords": ["technology", "communication", "dial", "telephone"] },
+	{ "category": "objects", "char": "📟", "name": "pager", "keywords": ["bbcall", "oldschool", "90s"] },
+	{ "category": "objects", "char": "📠", "name": "fax", "keywords": ["communication", "technology"] },
+	{ "category": "objects", "char": "📺", "name": "tv", "keywords": ["technology", "program", "oldschool", "show", "television"] },
+	{ "category": "objects", "char": "📻", "name": "radio", "keywords": ["communication", "music", "podcast", "program"] },
+	{ "category": "objects", "char": "🎙", "name": "studio_microphone", "keywords": ["sing", "recording", "artist", "talkshow"] },
+	{ "category": "objects", "char": "🎚", "name": "level_slider", "keywords": ["scale"] },
+	{ "category": "objects", "char": "🎛", "name": "control_knobs", "keywords": ["dial"] },
+	{ "category": "objects", "char": "🧭", "name": "compass", "keywords": ["magnetic", "navigation", "orienteering"] },
+	{ "category": "objects", "char": "⏱", "name": "stopwatch", "keywords": ["time", "deadline"] },
+	{ "category": "objects", "char": "⏲", "name": "timer_clock", "keywords": ["alarm"] },
+	{ "category": "objects", "char": "⏰", "name": "alarm_clock", "keywords": ["time", "wake"] },
+	{ "category": "objects", "char": "🕰", "name": "mantelpiece_clock", "keywords": ["time"] },
+	{ "category": "objects", "char": "⏳", "name": "hourglass_flowing_sand", "keywords": ["oldschool", "time", "countdown"] },
+	{ "category": "objects", "char": "⌛", "name": "hourglass", "keywords": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"] },
+	{ "category": "objects", "char": "📡", "name": "satellite", "keywords": ["communication", "future", "radio", "space"] },
+	{ "category": "objects", "char": "🔋", "name": "battery", "keywords": ["power", "energy", "sustain"] },
+	{ "category": "objects", "char": "🔌", "name": "electric_plug", "keywords": ["charger", "power"] },
+	{ "category": "objects", "char": "💡", "name": "bulb", "keywords": ["light", "electricity", "idea"] },
+	{ "category": "objects", "char": "🔦", "name": "flashlight", "keywords": ["dark", "camping", "sight", "night"] },
+	{ "category": "objects", "char": "🕯", "name": "candle", "keywords": ["fire", "wax"] },
+	{ "category": "objects", "char": "🧯", "name": "fire_extinguisher", "keywords": ["quench"] },
+	{ "category": "objects", "char": "🗑", "name": "wastebasket", "keywords": ["bin", "trash", "rubbish", "garbage", "toss"] },
+	{ "category": "objects", "char": "🛢", "name": "oil_drum", "keywords": ["barrell"] },
+	{ "category": "objects", "char": "💸", "name": "money_with_wings", "keywords": ["dollar", "bills", "payment", "sale"] },
+	{ "category": "objects", "char": "💵", "name": "dollar", "keywords": ["money", "sales", "bill", "currency"] },
+	{ "category": "objects", "char": "💴", "name": "yen", "keywords": ["money", "sales", "japanese", "dollar", "currency"] },
+	{ "category": "objects", "char": "💶", "name": "euro", "keywords": ["money", "sales", "dollar", "currency"] },
+	{ "category": "objects", "char": "💷", "name": "pound", "keywords": ["british", "sterling", "money", "sales", "bills", "uk", "england", "currency"] },
+	{ "category": "objects", "char": "💰", "name": "moneybag", "keywords": ["dollar", "payment", "coins", "sale"] },
+	{ "category": "objects", "char": "🪙", "name": "coin", "keywords": ["dollar", "payment", "coins", "sale"] },
+	{ "category": "objects", "char": "💳", "name": "credit_card", "keywords": ["money", "sales", "dollar", "bill", "payment", "shopping"] },
+	{ "category": "objects", "char": "💎", "name": "gem", "keywords": ["blue", "ruby", "diamond", "jewelry"] },
+	{ "category": "objects", "char": "⚖", "name": "balance_scale", "keywords": ["law", "fairness", "weight"] },
+	{ "category": "objects", "char": "🧰", "name": "toolbox", "keywords": ["tools", "diy", "fix", "maintainer", "mechanic"] },
+	{ "category": "objects", "char": "🔧", "name": "wrench", "keywords": ["tools", "diy", "ikea", "fix", "maintainer"] },
+	{ "category": "objects", "char": "🔨", "name": "hammer", "keywords": ["tools", "build", "create"] },
+	{ "category": "objects", "char": "⚒", "name": "hammer_and_pick", "keywords": ["tools", "build", "create"] },
+	{ "category": "objects", "char": "🛠", "name": "hammer_and_wrench", "keywords": ["tools", "build", "create"] },
+	{ "category": "objects", "char": "⛏", "name": "pick", "keywords": ["tools", "dig"] },
+	{ "category": "objects", "char": "🪓", "name": "axe", "keywords": ["tools"] },
+	{ "category": "objects", "char": "🦯", "name": "probing_cane", "keywords": ["tools"] },
+	{ "category": "objects", "char": "🔩", "name": "nut_and_bolt", "keywords": ["handy", "tools", "fix"] },
+	{ "category": "objects", "char": "⚙", "name": "gear", "keywords": ["cog"] },
+	{ "category": "objects", "char": "🪃", "name": "boomerang", "keywords": ["tool"] },
+	{ "category": "objects", "char": "🪚", "name": "carpentry_saw", "keywords": ["tool"] },
+	{ "category": "objects", "char": "🪛", "name": "screwdriver", "keywords": ["tool"] },
+	{ "category": "objects", "char": "🪝", "name": "hook", "keywords": ["tool"] },
+	{ "category": "objects", "char": "🪜", "name": "ladder", "keywords": ["tool"] },
+	{ "category": "objects", "char": "🧱", "name": "brick", "keywords": ["bricks"] },
+	{ "category": "objects", "char": "⛓", "name": "chains", "keywords": ["lock", "arrest"] },
+	{ "category": "objects", "char": "🧲", "name": "magnet", "keywords": ["attraction", "magnetic"] },
+	{ "category": "objects", "char": "🔫", "name": "gun", "keywords": ["violence", "weapon", "pistol", "revolver"] },
+	{ "category": "objects", "char": "💣", "name": "bomb", "keywords": ["boom", "explode", "explosion", "terrorism"] },
+	{ "category": "objects", "char": "🧨", "name": "firecracker", "keywords": ["dynamite", "boom", "explode", "explosion", "explosive"] },
+	{ "category": "objects", "char": "🔪", "name": "hocho", "keywords": ["knife", "blade", "cutlery", "kitchen", "weapon"] },
+	{ "category": "objects", "char": "🗡", "name": "dagger", "keywords": ["weapon"] },
+	{ "category": "objects", "char": "⚔", "name": "crossed_swords", "keywords": ["weapon"] },
+	{ "category": "objects", "char": "🛡", "name": "shield", "keywords": ["protection", "security"] },
+	{ "category": "objects", "char": "🚬", "name": "smoking", "keywords": ["kills", "tobacco", "cigarette", "joint", "smoke"] },
+	{ "category": "objects", "char": "☠", "name": "skull_and_crossbones", "keywords": ["poison", "danger", "deadly", "scary", "death", "pirate", "evil"] },
+	{ "category": "objects", "char": "⚰", "name": "coffin", "keywords": ["vampire", "dead", "die", "death", "rip", "graveyard", "cemetery", "casket", "funeral", "box"] },
+	{ "category": "objects", "char": "⚱", "name": "funeral_urn", "keywords": ["dead", "die", "death", "rip", "ashes"] },
+	{ "category": "objects", "char": "🏺", "name": "amphora", "keywords": ["vase", "jar"] },
+	{ "category": "objects", "char": "🔮", "name": "crystal_ball", "keywords": ["disco", "party", "magic", "circus", "fortune_teller"] },
+	{ "category": "objects", "char": "📿", "name": "prayer_beads", "keywords": ["dhikr", "religious"] },
+	{ "category": "objects", "char": "🧿", "name": "nazar_amulet", "keywords": ["bead", "charm"] },
+	{ "category": "objects", "char": "💈", "name": "barber", "keywords": ["hair", "salon", "style"] },
+	{ "category": "objects", "char": "⚗", "name": "alembic", "keywords": ["distilling", "science", "experiment", "chemistry"] },
+	{ "category": "objects", "char": "🔭", "name": "telescope", "keywords": ["stars", "space", "zoom", "science", "astronomy"] },
+	{ "category": "objects", "char": "🔬", "name": "microscope", "keywords": ["laboratory", "experiment", "zoomin", "science", "study"] },
+	{ "category": "objects", "char": "🕳", "name": "hole", "keywords": ["embarrassing"] },
+	{ "category": "objects", "char": "💊", "name": "pill", "keywords": ["health", "medicine", "doctor", "pharmacy", "drug"] },
+	{ "category": "objects", "char": "💉", "name": "syringe", "keywords": ["health", "hospital", "drugs", "blood", "medicine", "needle", "doctor", "nurse"] },
+	{ "category": "objects", "char": "🩸", "name": "drop_of_blood", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
+	{ "category": "objects", "char": "🩹", "name": "adhesive_bandage", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
+	{ "category": "objects", "char": "🩺", "name": "stethoscope", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
+	{ "category": "objects", "char": "🪒", "name": "razor", "keywords": ["health"] },
+	{ "category": "objects", "char": "🧬", "name": "dna", "keywords": ["biologist", "genetics", "life"] },
+	{ "category": "objects", "char": "🧫", "name": "petri_dish", "keywords": ["bacteria", "biology", "culture", "lab"] },
+	{ "category": "objects", "char": "🧪", "name": "test_tube", "keywords": ["chemistry", "experiment", "lab", "science"] },
+	{ "category": "objects", "char": "🌡", "name": "thermometer", "keywords": ["weather", "temperature", "hot", "cold"] },
+	{ "category": "objects", "char": "🧹", "name": "broom", "keywords": ["cleaning", "sweeping", "witch"] },
+	{ "category": "objects", "char": "🧺", "name": "basket", "keywords": ["laundry"] },
+	{ "category": "objects", "char": "🧻", "name": "toilet_paper", "keywords": ["roll"] },
+	{ "category": "objects", "char": "🏷", "name": "label", "keywords": ["sale", "tag"] },
+	{ "category": "objects", "char": "🔖", "name": "bookmark", "keywords": ["favorite", "label", "save"] },
+	{ "category": "objects", "char": "🚽", "name": "toilet", "keywords": ["restroom", "wc", "washroom", "bathroom", "potty"] },
+	{ "category": "objects", "char": "🚿", "name": "shower", "keywords": ["clean", "water", "bathroom"] },
+	{ "category": "objects", "char": "🛁", "name": "bathtub", "keywords": ["clean", "shower", "bathroom"] },
+	{ "category": "objects", "char": "🧼", "name": "soap", "keywords": ["bar", "bathing", "cleaning", "lather"] },
+	{ "category": "objects", "char": "🧽", "name": "sponge", "keywords": ["absorbing", "cleaning", "porous"] },
+	{ "category": "objects", "char": "🧴", "name": "lotion_bottle", "keywords": ["moisturizer", "sunscreen"] },
+	{ "category": "objects", "char": "🔑", "name": "key", "keywords": ["lock", "door", "password"] },
+	{ "category": "objects", "char": "🗝", "name": "old_key", "keywords": ["lock", "door", "password"] },
+	{ "category": "objects", "char": "🛋", "name": "couch_and_lamp", "keywords": ["read", "chill"] },
+	{ "category": "objects", "char": "🪔", "name": "diya_Lamp", "keywords": ["light", "oil"] },
+	{ "category": "objects", "char": "🛌", "name": "sleeping_bed", "keywords": ["bed", "rest"] },
+	{ "category": "objects", "char": "🛏", "name": "bed", "keywords": ["sleep", "rest"] },
+	{ "category": "objects", "char": "🚪", "name": "door", "keywords": ["house", "entry", "exit"] },
+	{ "category": "objects", "char": "🪑", "name": "chair", "keywords": ["house", "desk"] },
+	{ "category": "objects", "char": "🛎", "name": "bellhop_bell", "keywords": ["service"] },
+	{ "category": "objects", "char": "🧸", "name": "teddy_bear", "keywords": ["plush", "stuffed"] },
+	{ "category": "objects", "char": "🖼", "name": "framed_picture", "keywords": ["photography"] },
+	{ "category": "objects", "char": "🗺", "name": "world_map", "keywords": ["location", "direction"] },
+	{ "category": "objects", "char": "🛗", "name": "elevator", "keywords": ["household"] },
+	{ "category": "objects", "char": "🪞", "name": "mirror", "keywords": ["household"] },
+	{ "category": "objects", "char": "🪟", "name": "window", "keywords": ["household"] },
+	{ "category": "objects", "char": "🪠", "name": "plunger", "keywords": ["household"] },
+	{ "category": "objects", "char": "🪤", "name": "mouse_trap", "keywords": ["household"] },
+	{ "category": "objects", "char": "🪣", "name": "bucket", "keywords": ["household"] },
+	{ "category": "objects", "char": "🪥", "name": "toothbrush", "keywords": ["household"] },
+	{ "category": "objects", "char": "⛱", "name": "parasol_on_ground", "keywords": ["weather", "summer"] },
+	{ "category": "objects", "char": "🗿", "name": "moyai", "keywords": ["rock", "easter island", "moai"] },
+	{ "category": "objects", "char": "🛍", "name": "shopping", "keywords": ["mall", "buy", "purchase"] },
+	{ "category": "objects", "char": "🛒", "name": "shopping_cart", "keywords": ["trolley"] },
+	{ "category": "objects", "char": "🎈", "name": "balloon", "keywords": ["party", "celebration", "birthday", "circus"] },
+	{ "category": "objects", "char": "🎏", "name": "flags", "keywords": ["fish", "japanese", "koinobori", "carp", "banner"] },
+	{ "category": "objects", "char": "🎀", "name": "ribbon", "keywords": ["decoration", "pink", "girl", "bowtie"] },
+	{ "category": "objects", "char": "🎁", "name": "gift", "keywords": ["present", "birthday", "christmas", "xmas"] },
+	{ "category": "objects", "char": "🎊", "name": "confetti_ball", "keywords": ["festival", "party", "birthday", "circus"] },
+	{ "category": "objects", "char": "🎉", "name": "tada", "keywords": ["party", "congratulations", "birthday", "magic", "circus", "celebration"] },
+	{ "category": "objects", "char": "🎎", "name": "dolls", "keywords": ["japanese", "toy", "kimono"] },
+	{ "category": "objects", "char": "🎐", "name": "wind_chime", "keywords": ["nature", "ding", "spring", "bell"] },
+	{ "category": "objects", "char": "🎌", "name": "crossed_flags", "keywords": ["japanese", "nation", "country", "border"] },
+	{ "category": "objects", "char": "🏮", "name": "izakaya_lantern", "keywords": ["light", "paper", "halloween", "spooky"] },
+	{ "category": "objects", "char": "🧧", "name": "red_envelope", "keywords": ["gift"] },
+	{ "category": "objects", "char": "✉️", "name": "email", "keywords": ["letter", "postal", "inbox", "communication"] },
+	{ "category": "objects", "char": "📩", "name": "envelope_with_arrow", "keywords": ["email", "communication"] },
+	{ "category": "objects", "char": "📨", "name": "incoming_envelope", "keywords": ["email", "inbox"] },
+	{ "category": "objects", "char": "📧", "name": "e-mail", "keywords": ["communication", "inbox"] },
+	{ "category": "objects", "char": "💌", "name": "love_letter", "keywords": ["email", "like", "affection", "envelope", "valentines"] },
+	{ "category": "objects", "char": "📮", "name": "postbox", "keywords": ["email", "letter", "envelope"] },
+	{ "category": "objects", "char": "📪", "name": "mailbox_closed", "keywords": ["email", "communication", "inbox"] },
+	{ "category": "objects", "char": "📫", "name": "mailbox", "keywords": ["email", "inbox", "communication"] },
+	{ "category": "objects", "char": "📬", "name": "mailbox_with_mail", "keywords": ["email", "inbox", "communication"] },
+	{ "category": "objects", "char": "📭", "name": "mailbox_with_no_mail", "keywords": ["email", "inbox"] },
+	{ "category": "objects", "char": "📦", "name": "package", "keywords": ["mail", "gift", "cardboard", "box", "moving"] },
+	{ "category": "objects", "char": "📯", "name": "postal_horn", "keywords": ["instrument", "music"] },
+	{ "category": "objects", "char": "📥", "name": "inbox_tray", "keywords": ["email", "documents"] },
+	{ "category": "objects", "char": "📤", "name": "outbox_tray", "keywords": ["inbox", "email"] },
+	{ "category": "objects", "char": "📜", "name": "scroll", "keywords": ["documents", "ancient", "history", "paper"] },
+	{ "category": "objects", "char": "📃", "name": "page_with_curl", "keywords": ["documents", "office", "paper"] },
+	{ "category": "objects", "char": "📑", "name": "bookmark_tabs", "keywords": ["favorite", "save", "order", "tidy"] },
+	{ "category": "objects", "char": "🧾", "name": "receipt", "keywords": ["accounting", "expenses"] },
+	{ "category": "objects", "char": "📊", "name": "bar_chart", "keywords": ["graph", "presentation", "stats"] },
+	{ "category": "objects", "char": "📈", "name": "chart_with_upwards_trend", "keywords": ["graph", "presentation", "stats", "recovery", "business", "economics", "money", "sales", "good", "success"] },
+	{ "category": "objects", "char": "📉", "name": "chart_with_downwards_trend", "keywords": ["graph", "presentation", "stats", "recession", "business", "economics", "money", "sales", "bad", "failure"] },
+	{ "category": "objects", "char": "📄", "name": "page_facing_up", "keywords": ["documents", "office", "paper", "information"] },
+	{ "category": "objects", "char": "📅", "name": "date", "keywords": ["calendar", "schedule"] },
+	{ "category": "objects", "char": "📆", "name": "calendar", "keywords": ["schedule", "date", "planning"] },
+	{ "category": "objects", "char": "🗓", "name": "spiral_calendar", "keywords": ["date", "schedule", "planning"] },
+	{ "category": "objects", "char": "📇", "name": "card_index", "keywords": ["business", "stationery"] },
+	{ "category": "objects", "char": "🗃", "name": "card_file_box", "keywords": ["business", "stationery"] },
+	{ "category": "objects", "char": "🗳", "name": "ballot_box", "keywords": ["election", "vote"] },
+	{ "category": "objects", "char": "🗄", "name": "file_cabinet", "keywords": ["filing", "organizing"] },
+	{ "category": "objects", "char": "📋", "name": "clipboard", "keywords": ["stationery", "documents"] },
+	{ "category": "objects", "char": "🗒", "name": "spiral_notepad", "keywords": ["memo", "stationery"] },
+	{ "category": "objects", "char": "📁", "name": "file_folder", "keywords": ["documents", "business", "office"] },
+	{ "category": "objects", "char": "📂", "name": "open_file_folder", "keywords": ["documents", "load"] },
+	{ "category": "objects", "char": "🗂", "name": "card_index_dividers", "keywords": ["organizing", "business", "stationery"] },
+	{ "category": "objects", "char": "🗞", "name": "newspaper_roll", "keywords": ["press", "headline"] },
+	{ "category": "objects", "char": "📰", "name": "newspaper", "keywords": ["press", "headline"] },
+	{ "category": "objects", "char": "📓", "name": "notebook", "keywords": ["stationery", "record", "notes", "paper", "study"] },
+	{ "category": "objects", "char": "📕", "name": "closed_book", "keywords": ["read", "library", "knowledge", "textbook", "learn"] },
+	{ "category": "objects", "char": "📗", "name": "green_book", "keywords": ["read", "library", "knowledge", "study"] },
+	{ "category": "objects", "char": "📘", "name": "blue_book", "keywords": ["read", "library", "knowledge", "learn", "study"] },
+	{ "category": "objects", "char": "📙", "name": "orange_book", "keywords": ["read", "library", "knowledge", "textbook", "study"] },
+	{ "category": "objects", "char": "📔", "name": "notebook_with_decorative_cover", "keywords": ["classroom", "notes", "record", "paper", "study"] },
+	{ "category": "objects", "char": "📒", "name": "ledger", "keywords": ["notes", "paper"] },
+	{ "category": "objects", "char": "📚", "name": "books", "keywords": ["literature", "library", "study"] },
+	{ "category": "objects", "char": "📖", "name": "open_book", "keywords": ["book", "read", "library", "knowledge", "literature", "learn", "study"] },
+	{ "category": "objects", "char": "🧷", "name": "safety_pin", "keywords": ["diaper"] },
+	{ "category": "objects", "char": "🔗", "name": "link", "keywords": ["rings", "url"] },
+	{ "category": "objects", "char": "📎", "name": "paperclip", "keywords": ["documents", "stationery"] },
+	{ "category": "objects", "char": "🖇", "name": "paperclips", "keywords": ["documents", "stationery"] },
+	{ "category": "objects", "char": "✂️", "name": "scissors", "keywords": ["stationery", "cut"] },
+	{ "category": "objects", "char": "📐", "name": "triangular_ruler", "keywords": ["stationery", "math", "architect", "sketch"] },
+	{ "category": "objects", "char": "📏", "name": "straight_ruler", "keywords": ["stationery", "calculate", "length", "math", "school", "drawing", "architect", "sketch"] },
+	{ "category": "objects", "char": "🧮", "name": "abacus", "keywords": ["calculation"] },
+	{ "category": "objects", "char": "📌", "name": "pushpin", "keywords": ["stationery", "mark", "here"] },
+	{ "category": "objects", "char": "📍", "name": "round_pushpin", "keywords": ["stationery", "location", "map", "here"] },
+	{ "category": "objects", "char": "🚩", "name": "triangular_flag_on_post", "keywords": ["mark", "milestone", "place"] },
+	{ "category": "objects", "char": "🏳", "name": "white_flag", "keywords": ["losing", "loser", "lost", "surrender", "give up", "fail"] },
+	{ "category": "objects", "char": "🏴", "name": "black_flag", "keywords": ["pirate"] },
+	{ "category": "objects", "char": "🏳️‍🌈", "name": "rainbow_flag", "keywords": ["flag", "rainbow", "pride", "gay", "lgbt", "glbt", "queer", "homosexual", "lesbian", "bisexual", "transgender"] },
+	{ "category": "objects", "char": "🏳️‍⚧️", "name": "transgender_flag", "keywords": ["flag", "transgender"] },
+	{ "category": "objects", "char": "🔐", "name": "closed_lock_with_key", "keywords": ["security", "privacy"] },
+	{ "category": "objects", "char": "🔒", "name": "lock", "keywords": ["security", "password", "padlock"] },
+	{ "category": "objects", "char": "🔓", "name": "unlock", "keywords": ["privacy", "security"] },
+	{ "category": "objects", "char": "🔏", "name": "lock_with_ink_pen", "keywords": ["security", "secret"] },
+	{ "category": "objects", "char": "🖊", "name": "pen", "keywords": ["stationery", "writing", "write"] },
+	{ "category": "objects", "char": "🖋", "name": "fountain_pen", "keywords": ["stationery", "writing", "write"] },
+	{ "category": "objects", "char": "✒️", "name": "black_nib", "keywords": ["pen", "stationery", "writing", "write"] },
+	{ "category": "objects", "char": "📝", "name": "memo", "keywords": ["write", "documents", "stationery", "pencil", "paper", "writing", "legal", "exam", "quiz", "test", "study", "compose"] },
+	{ "category": "objects", "char": "✏️", "name": "pencil2", "keywords": ["stationery", "write", "paper", "writing", "school", "study"] },
+	{ "category": "objects", "char": "🖍", "name": "crayon", "keywords": ["drawing", "creativity"] },
+	{ "category": "objects", "char": "🖌", "name": "paintbrush", "keywords": ["drawing", "creativity", "art"] },
+	{ "category": "objects", "char": "🔍", "name": "mag", "keywords": ["search", "zoom", "find", "detective"] },
+	{ "category": "objects", "char": "🔎", "name": "mag_right", "keywords": ["search", "zoom", "find", "detective"] },
+	{ "category": "objects", "char": "🪦", "name": "headstone", "keywords": [] },
+	{ "category": "objects", "char": "🪧", "name": "placard", "keywords": [] },
+	{ "category": "symbols", "char": "💯", "name": "100", "keywords": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"] },
+	{ "category": "symbols", "char": "🔢", "name": "1234", "keywords": ["numbers", "blue-square"] },
+	{ "category": "symbols", "char": "❤️", "name": "heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "🧡", "name": "orange_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "💛", "name": "yellow_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "💚", "name": "green_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "💙", "name": "blue_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "💜", "name": "purple_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "🤎", "name": "brown_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "🖤", "name": "black_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "🤍", "name": "white_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "💔", "name": "broken_heart", "keywords": ["sad", "sorry", "break", "heart", "heartbreak"] },
+	{ "category": "symbols", "char": "❣", "name": "heavy_heart_exclamation", "keywords": ["decoration", "love"] },
+	{ "category": "symbols", "char": "💕", "name": "two_hearts", "keywords": ["love", "like", "affection", "valentines", "heart"] },
+	{ "category": "symbols", "char": "💞", "name": "revolving_hearts", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "💓", "name": "heartbeat", "keywords": ["love", "like", "affection", "valentines", "pink", "heart"] },
+	{ "category": "symbols", "char": "💗", "name": "heartpulse", "keywords": ["like", "love", "affection", "valentines", "pink"] },
+	{ "category": "symbols", "char": "💖", "name": "sparkling_heart", "keywords": ["love", "like", "affection", "valentines"] },
+	{ "category": "symbols", "char": "💘", "name": "cupid", "keywords": ["love", "like", "heart", "affection", "valentines"] },
+	{ "category": "symbols", "char": "💝", "name": "gift_heart", "keywords": ["love", "valentines"] },
+	{ "category": "symbols", "char": "💟", "name": "heart_decoration", "keywords": ["purple-square", "love", "like"] },
+	{ "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83D\uDD25", "name": "heart_on_fire", "keywords": [] },
+	{ "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83E\uDE79", "name": "mending_heart", "keywords": [] },
+	{ "category": "symbols", "char": "☮", "name": "peace_symbol", "keywords": ["hippie"] },
+	{ "category": "symbols", "char": "✝", "name": "latin_cross", "keywords": ["christianity"] },
+	{ "category": "symbols", "char": "☪", "name": "star_and_crescent", "keywords": ["islam"] },
+	{ "category": "symbols", "char": "🕉", "name": "om", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] },
+	{ "category": "symbols", "char": "☸", "name": "wheel_of_dharma", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] },
+	{ "category": "symbols", "char": "✡", "name": "star_of_david", "keywords": ["judaism"] },
+	{ "category": "symbols", "char": "🔯", "name": "six_pointed_star", "keywords": ["purple-square", "religion", "jewish", "hexagram"] },
+	{ "category": "symbols", "char": "🕎", "name": "menorah", "keywords": ["hanukkah", "candles", "jewish"] },
+	{ "category": "symbols", "char": "☯", "name": "yin_yang", "keywords": ["balance"] },
+	{ "category": "symbols", "char": "☦", "name": "orthodox_cross", "keywords": ["suppedaneum", "religion"] },
+	{ "category": "symbols", "char": "🛐", "name": "place_of_worship", "keywords": ["religion", "church", "temple", "prayer"] },
+	{ "category": "symbols", "char": "⛎", "name": "ophiuchus", "keywords": ["sign", "purple-square", "constellation", "astrology"] },
+	{ "category": "symbols", "char": "♈", "name": "aries", "keywords": ["sign", "purple-square", "zodiac", "astrology"] },
+	{ "category": "symbols", "char": "♉", "name": "taurus", "keywords": ["purple-square", "sign", "zodiac", "astrology"] },
+	{ "category": "symbols", "char": "♊", "name": "gemini", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
+	{ "category": "symbols", "char": "♋", "name": "cancer", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
+	{ "category": "symbols", "char": "♌", "name": "leo", "keywords": ["sign", "purple-square", "zodiac", "astrology"] },
+	{ "category": "symbols", "char": "♍", "name": "virgo", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
+	{ "category": "symbols", "char": "♎", "name": "libra", "keywords": ["sign", "purple-square", "zodiac", "astrology"] },
+	{ "category": "symbols", "char": "♏", "name": "scorpius", "keywords": ["sign", "zodiac", "purple-square", "astrology", "scorpio"] },
+	{ "category": "symbols", "char": "♐", "name": "sagittarius", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
+	{ "category": "symbols", "char": "♑", "name": "capricorn", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
+	{ "category": "symbols", "char": "♒", "name": "aquarius", "keywords": ["sign", "purple-square", "zodiac", "astrology"] },
+	{ "category": "symbols", "char": "♓", "name": "pisces", "keywords": ["purple-square", "sign", "zodiac", "astrology"] },
+	{ "category": "symbols", "char": "🆔", "name": "id", "keywords": ["purple-square", "words"] },
+	{ "category": "symbols", "char": "⚛", "name": "atom_symbol", "keywords": ["science", "physics", "chemistry"] },
+	{ "category": "symbols", "char": "⚧️", "name": "transgender_symbol", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] },
+	{ "category": "symbols", "char": "🈳", "name": "u7a7a", "keywords": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square", "aki"] },
+	{ "category": "symbols", "char": "🈹", "name": "u5272", "keywords": ["cut", "divide", "chinese", "kanji", "pink-square", "waribiki"] },
+	{ "category": "symbols", "char": "☢", "name": "radioactive", "keywords": ["nuclear", "danger"] },
+	{ "category": "symbols", "char": "☣", "name": "biohazard", "keywords": ["danger"] },
+	{ "category": "symbols", "char": "📴", "name": "mobile_phone_off", "keywords": ["mute", "orange-square", "silence", "quiet"] },
+	{ "category": "symbols", "char": "📳", "name": "vibration_mode", "keywords": ["orange-square", "phone"] },
+	{ "category": "symbols", "char": "🈶", "name": "u6709", "keywords": ["orange-square", "chinese", "have", "kanji", "ari"] },
+	{ "category": "symbols", "char": "🈚", "name": "u7121", "keywords": ["nothing", "chinese", "kanji", "japanese", "orange-square", "nashi"] },
+	{ "category": "symbols", "char": "🈸", "name": "u7533", "keywords": ["chinese", "japanese", "kanji", "orange-square", "moushikomi"] },
+	{ "category": "symbols", "char": "🈺", "name": "u55b6", "keywords": ["japanese", "opening hours", "orange-square", "eigyo"] },
+	{ "category": "symbols", "char": "🈷️", "name": "u6708", "keywords": ["chinese", "month", "moon", "japanese", "orange-square", "kanji", "tsuki", "tsukigime", "getsugaku"] },
+	{ "category": "symbols", "char": "✴️", "name": "eight_pointed_black_star", "keywords": ["orange-square", "shape", "polygon"] },
+	{ "category": "symbols", "char": "🆚", "name": "vs", "keywords": ["words", "orange-square"] },
+	{ "category": "symbols", "char": "🉑", "name": "accept", "keywords": ["ok", "good", "chinese", "kanji", "agree", "yes", "orange-circle"] },
+	{ "category": "symbols", "char": "💮", "name": "white_flower", "keywords": ["japanese", "spring"] },
+	{ "category": "symbols", "char": "🉐", "name": "ideograph_advantage", "keywords": ["chinese", "kanji", "obtain", "get", "circle"] },
+	{ "category": "symbols", "char": "㊙️", "name": "secret", "keywords": ["privacy", "chinese", "sshh", "kanji", "red-circle"] },
+	{ "category": "symbols", "char": "㊗️", "name": "congratulations", "keywords": ["chinese", "kanji", "japanese", "red-circle"] },
+	{ "category": "symbols", "char": "🈴", "name": "u5408", "keywords": ["japanese", "chinese", "join", "kanji", "red-square", "goukaku", "pass"] },
+	{ "category": "symbols", "char": "🈵", "name": "u6e80", "keywords": ["full", "chinese", "japanese", "red-square", "kanji", "man"] },
+	{ "category": "symbols", "char": "🈲", "name": "u7981", "keywords": ["kanji", "japanese", "chinese", "forbidden", "limit", "restricted", "red-square", "kinshi"] },
+	{ "category": "symbols", "char": "🅰️", "name": "a", "keywords": ["red-square", "alphabet", "letter"] },
+	{ "category": "symbols", "char": "🅱️", "name": "b", "keywords": ["red-square", "alphabet", "letter"] },
+	{ "category": "symbols", "char": "🆎", "name": "ab", "keywords": ["red-square", "alphabet"] },
+	{ "category": "symbols", "char": "🆑", "name": "cl", "keywords": ["alphabet", "words", "red-square"] },
+	{ "category": "symbols", "char": "🅾️", "name": "o2", "keywords": ["alphabet", "red-square", "letter"] },
+	{ "category": "symbols", "char": "🆘", "name": "sos", "keywords": ["help", "red-square", "words", "emergency", "911"] },
+	{ "category": "symbols", "char": "⛔", "name": "no_entry", "keywords": ["limit", "security", "privacy", "bad", "denied", "stop", "circle"] },
+	{ "category": "symbols", "char": "📛", "name": "name_badge", "keywords": ["fire", "forbid"] },
+	{ "category": "symbols", "char": "🚫", "name": "no_entry_sign", "keywords": ["forbid", "stop", "limit", "denied", "disallow", "circle"] },
+	{ "category": "symbols", "char": "❌", "name": "x", "keywords": ["no", "delete", "remove", "cancel", "red"] },
+	{ "category": "symbols", "char": "⭕", "name": "o", "keywords": ["circle", "round"] },
+	{ "category": "symbols", "char": "🛑", "name": "stop_sign", "keywords": ["stop"] },
+	{ "category": "symbols", "char": "💢", "name": "anger", "keywords": ["angry", "mad"] },
+	{ "category": "symbols", "char": "♨️", "name": "hotsprings", "keywords": ["bath", "warm", "relax"] },
+	{ "category": "symbols", "char": "🚷", "name": "no_pedestrians", "keywords": ["rules", "crossing", "walking", "circle"] },
+	{ "category": "symbols", "char": "🚯", "name": "do_not_litter", "keywords": ["trash", "bin", "garbage", "circle"] },
+	{ "category": "symbols", "char": "🚳", "name": "no_bicycles", "keywords": ["cyclist", "prohibited", "circle"] },
+	{ "category": "symbols", "char": "🚱", "name": "non-potable_water", "keywords": ["drink", "faucet", "tap", "circle"] },
+	{ "category": "symbols", "char": "🔞", "name": "underage", "keywords": ["18", "drink", "pub", "night", "minor", "circle"] },
+	{ "category": "symbols", "char": "📵", "name": "no_mobile_phones", "keywords": ["iphone", "mute", "circle"] },
+	{ "category": "symbols", "char": "❗", "name": "exclamation", "keywords": ["heavy_exclamation_mark", "danger", "surprise", "punctuation", "wow", "warning"] },
+	{ "category": "symbols", "char": "❕", "name": "grey_exclamation", "keywords": ["surprise", "punctuation", "gray", "wow", "warning"] },
+	{ "category": "symbols", "char": "❓", "name": "question", "keywords": ["doubt", "confused"] },
+	{ "category": "symbols", "char": "❔", "name": "grey_question", "keywords": ["doubts", "gray", "huh", "confused"] },
+	{ "category": "symbols", "char": "‼️", "name": "bangbang", "keywords": ["exclamation", "surprise"] },
+	{ "category": "symbols", "char": "⁉️", "name": "interrobang", "keywords": ["wat", "punctuation", "surprise"] },
+	{ "category": "symbols", "char": "🔅", "name": "low_brightness", "keywords": ["sun", "afternoon", "warm", "summer"] },
+	{ "category": "symbols", "char": "🔆", "name": "high_brightness", "keywords": ["sun", "light"] },
+	{ "category": "symbols", "char": "🔱", "name": "trident", "keywords": ["weapon", "spear"] },
+	{ "category": "symbols", "char": "⚜", "name": "fleur_de_lis", "keywords": ["decorative", "scout"] },
+	{ "category": "symbols", "char": "〽️", "name": "part_alternation_mark", "keywords": ["graph", "presentation", "stats", "business", "economics", "bad"] },
+	{ "category": "symbols", "char": "⚠️", "name": "warning", "keywords": ["exclamation", "wip", "alert", "error", "problem", "issue"] },
+	{ "category": "symbols", "char": "🚸", "name": "children_crossing", "keywords": ["school", "warning", "danger", "sign", "driving", "yellow-diamond"] },
+	{ "category": "symbols", "char": "🔰", "name": "beginner", "keywords": ["badge", "shield"] },
+	{ "category": "symbols", "char": "♻️", "name": "recycle", "keywords": ["arrow", "environment", "garbage", "trash"] },
+	{ "category": "symbols", "char": "🈯", "name": "u6307", "keywords": ["chinese", "point", "green-square", "kanji", "reserved", "shiteiseki"] },
+	{ "category": "symbols", "char": "💹", "name": "chart", "keywords": ["green-square", "graph", "presentation", "stats"] },
+	{ "category": "symbols", "char": "❇️", "name": "sparkle", "keywords": ["stars", "green-square", "awesome", "good", "fireworks"] },
+	{ "category": "symbols", "char": "✳️", "name": "eight_spoked_asterisk", "keywords": ["star", "sparkle", "green-square"] },
+	{ "category": "symbols", "char": "❎", "name": "negative_squared_cross_mark", "keywords": ["x", "green-square", "no", "deny"] },
+	{ "category": "symbols", "char": "✅", "name": "white_check_mark", "keywords": ["green-square", "ok", "agree", "vote", "election", "answer", "tick"] },
+	{ "category": "symbols", "char": "💠", "name": "diamond_shape_with_a_dot_inside", "keywords": ["jewel", "blue", "gem", "crystal", "fancy"] },
+	{ "category": "symbols", "char": "🌀", "name": "cyclone", "keywords": ["weather", "swirl", "blue", "cloud", "vortex", "spiral", "whirlpool", "spin", "tornado", "hurricane", "typhoon"] },
+	{ "category": "symbols", "char": "➿", "name": "loop", "keywords": ["tape", "cassette"] },
+	{ "category": "symbols", "char": "🌐", "name": "globe_with_meridians", "keywords": ["earth", "international", "world", "internet", "interweb", "i18n"] },
+	{ "category": "symbols", "char": "Ⓜ️", "name": "m", "keywords": ["alphabet", "blue-circle", "letter"] },
+	{ "category": "symbols", "char": "🏧", "name": "atm", "keywords": ["money", "sales", "cash", "blue-square", "payment", "bank"] },
+	{ "category": "symbols", "char": "🈂️", "name": "sa", "keywords": ["japanese", "blue-square", "katakana"] },
+	{ "category": "symbols", "char": "🛂", "name": "passport_control", "keywords": ["custom", "blue-square"] },
+	{ "category": "symbols", "char": "🛃", "name": "customs", "keywords": ["passport", "border", "blue-square"] },
+	{ "category": "symbols", "char": "🛄", "name": "baggage_claim", "keywords": ["blue-square", "airport", "transport"] },
+	{ "category": "symbols", "char": "🛅", "name": "left_luggage", "keywords": ["blue-square", "travel"] },
+	{ "category": "symbols", "char": "♿", "name": "wheelchair", "keywords": ["blue-square", "disabled", "a11y", "accessibility"] },
+	{ "category": "symbols", "char": "🚭", "name": "no_smoking", "keywords": ["cigarette", "blue-square", "smell", "smoke"] },
+	{ "category": "symbols", "char": "🚾", "name": "wc", "keywords": ["toilet", "restroom", "blue-square"] },
+	{ "category": "symbols", "char": "🅿️", "name": "parking", "keywords": ["cars", "blue-square", "alphabet", "letter"] },
+	{ "category": "symbols", "char": "🚰", "name": "potable_water", "keywords": ["blue-square", "liquid", "restroom", "cleaning", "faucet"] },
+	{ "category": "symbols", "char": "🚹", "name": "mens", "keywords": ["toilet", "restroom", "wc", "blue-square", "gender", "male"] },
+	{ "category": "symbols", "char": "🚺", "name": "womens", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] },
+	{ "category": "symbols", "char": "🚼", "name": "baby_symbol", "keywords": ["orange-square", "child"] },
+	{ "category": "symbols", "char": "🚻", "name": "restroom", "keywords": ["blue-square", "toilet", "refresh", "wc", "gender"] },
+	{ "category": "symbols", "char": "🚮", "name": "put_litter_in_its_place", "keywords": ["blue-square", "sign", "human", "info"] },
+	{ "category": "symbols", "char": "🎦", "name": "cinema", "keywords": ["blue-square", "record", "film", "movie", "curtain", "stage", "theater"] },
+	{ "category": "symbols", "char": "📶", "name": "signal_strength", "keywords": ["blue-square", "reception", "phone", "internet", "connection", "wifi", "bluetooth", "bars"] },
+	{ "category": "symbols", "char": "🈁", "name": "koko", "keywords": ["blue-square", "here", "katakana", "japanese", "destination"] },
+	{ "category": "symbols", "char": "🆖", "name": "ng", "keywords": ["blue-square", "words", "shape", "icon"] },
+	{ "category": "symbols", "char": "🆗", "name": "ok", "keywords": ["good", "agree", "yes", "blue-square"] },
+	{ "category": "symbols", "char": "🆙", "name": "up", "keywords": ["blue-square", "above", "high"] },
+	{ "category": "symbols", "char": "🆒", "name": "cool", "keywords": ["words", "blue-square"] },
+	{ "category": "symbols", "char": "🆕", "name": "new", "keywords": ["blue-square", "words", "start"] },
+	{ "category": "symbols", "char": "🆓", "name": "free", "keywords": ["blue-square", "words"] },
+	{ "category": "symbols", "char": "0️⃣", "name": "zero", "keywords": ["0", "numbers", "blue-square", "null"] },
+	{ "category": "symbols", "char": "1️⃣", "name": "one", "keywords": ["blue-square", "numbers", "1"] },
+	{ "category": "symbols", "char": "2️⃣", "name": "two", "keywords": ["numbers", "2", "prime", "blue-square"] },
+	{ "category": "symbols", "char": "3️⃣", "name": "three", "keywords": ["3", "numbers", "prime", "blue-square"] },
+	{ "category": "symbols", "char": "4️⃣", "name": "four", "keywords": ["4", "numbers", "blue-square"] },
+	{ "category": "symbols", "char": "5️⃣", "name": "five", "keywords": ["5", "numbers", "blue-square", "prime"] },
+	{ "category": "symbols", "char": "6️⃣", "name": "six", "keywords": ["6", "numbers", "blue-square"] },
+	{ "category": "symbols", "char": "7️⃣", "name": "seven", "keywords": ["7", "numbers", "blue-square", "prime"] },
+	{ "category": "symbols", "char": "8️⃣", "name": "eight", "keywords": ["8", "blue-square", "numbers"] },
+	{ "category": "symbols", "char": "9️⃣", "name": "nine", "keywords": ["blue-square", "numbers", "9"] },
+	{ "category": "symbols", "char": "🔟", "name": "keycap_ten", "keywords": ["numbers", "10", "blue-square"] },
+	{ "category": "symbols", "char": "*⃣", "name": "asterisk", "keywords": ["star", "keycap"] },
+	{ "category": "symbols", "char": "⏏️", "name": "eject_button", "keywords": ["blue-square"] },
+	{ "category": "symbols", "char": "▶️", "name": "arrow_forward", "keywords": ["blue-square", "right", "direction", "play"] },
+	{ "category": "symbols", "char": "⏸", "name": "pause_button", "keywords": ["pause", "blue-square"] },
+	{ "category": "symbols", "char": "⏭", "name": "next_track_button", "keywords": ["forward", "next", "blue-square"] },
+	{ "category": "symbols", "char": "⏹", "name": "stop_button", "keywords": ["blue-square"] },
+	{ "category": "symbols", "char": "⏺", "name": "record_button", "keywords": ["blue-square"] },
+	{ "category": "symbols", "char": "⏯", "name": "play_or_pause_button", "keywords": ["blue-square", "play", "pause"] },
+	{ "category": "symbols", "char": "⏮", "name": "previous_track_button", "keywords": ["backward"] },
+	{ "category": "symbols", "char": "⏩", "name": "fast_forward", "keywords": ["blue-square", "play", "speed", "continue"] },
+	{ "category": "symbols", "char": "⏪", "name": "rewind", "keywords": ["play", "blue-square"] },
+	{ "category": "symbols", "char": "🔀", "name": "twisted_rightwards_arrows", "keywords": ["blue-square", "shuffle", "music", "random"] },
+	{ "category": "symbols", "char": "🔁", "name": "repeat", "keywords": ["loop", "record"] },
+	{ "category": "symbols", "char": "🔂", "name": "repeat_one", "keywords": ["blue-square", "loop"] },
+	{ "category": "symbols", "char": "◀️", "name": "arrow_backward", "keywords": ["blue-square", "left", "direction"] },
+	{ "category": "symbols", "char": "🔼", "name": "arrow_up_small", "keywords": ["blue-square", "triangle", "direction", "point", "forward", "top"] },
+	{ "category": "symbols", "char": "🔽", "name": "arrow_down_small", "keywords": ["blue-square", "direction", "bottom"] },
+	{ "category": "symbols", "char": "⏫", "name": "arrow_double_up", "keywords": ["blue-square", "direction", "top"] },
+	{ "category": "symbols", "char": "⏬", "name": "arrow_double_down", "keywords": ["blue-square", "direction", "bottom"] },
+	{ "category": "symbols", "char": "➡️", "name": "arrow_right", "keywords": ["blue-square", "next"] },
+	{ "category": "symbols", "char": "⬅️", "name": "arrow_left", "keywords": ["blue-square", "previous", "back"] },
+	{ "category": "symbols", "char": "⬆️", "name": "arrow_up", "keywords": ["blue-square", "continue", "top", "direction"] },
+	{ "category": "symbols", "char": "⬇️", "name": "arrow_down", "keywords": ["blue-square", "direction", "bottom"] },
+	{ "category": "symbols", "char": "↗️", "name": "arrow_upper_right", "keywords": ["blue-square", "point", "direction", "diagonal", "northeast"] },
+	{ "category": "symbols", "char": "↘️", "name": "arrow_lower_right", "keywords": ["blue-square", "direction", "diagonal", "southeast"] },
+	{ "category": "symbols", "char": "↙️", "name": "arrow_lower_left", "keywords": ["blue-square", "direction", "diagonal", "southwest"] },
+	{ "category": "symbols", "char": "↖️", "name": "arrow_upper_left", "keywords": ["blue-square", "point", "direction", "diagonal", "northwest"] },
+	{ "category": "symbols", "char": "↕️", "name": "arrow_up_down", "keywords": ["blue-square", "direction", "way", "vertical"] },
+	{ "category": "symbols", "char": "↔️", "name": "left_right_arrow", "keywords": ["shape", "direction", "horizontal", "sideways"] },
+	{ "category": "symbols", "char": "🔄", "name": "arrows_counterclockwise", "keywords": ["blue-square", "sync", "cycle"] },
+	{ "category": "symbols", "char": "↪️", "name": "arrow_right_hook", "keywords": ["blue-square", "return", "rotate", "direction"] },
+	{ "category": "symbols", "char": "↩️", "name": "leftwards_arrow_with_hook", "keywords": ["back", "return", "blue-square", "undo", "enter"] },
+	{ "category": "symbols", "char": "⤴️", "name": "arrow_heading_up", "keywords": ["blue-square", "direction", "top"] },
+	{ "category": "symbols", "char": "⤵️", "name": "arrow_heading_down", "keywords": ["blue-square", "direction", "bottom"] },
+	{ "category": "symbols", "char": "#️⃣", "name": "hash", "keywords": ["symbol", "blue-square", "twitter"] },
+	{ "category": "symbols", "char": "ℹ️", "name": "information_source", "keywords": ["blue-square", "alphabet", "letter"] },
+	{ "category": "symbols", "char": "🔤", "name": "abc", "keywords": ["blue-square", "alphabet"] },
+	{ "category": "symbols", "char": "🔡", "name": "abcd", "keywords": ["blue-square", "alphabet"] },
+	{ "category": "symbols", "char": "🔠", "name": "capital_abcd", "keywords": ["alphabet", "words", "blue-square"] },
+	{ "category": "symbols", "char": "🔣", "name": "symbols", "keywords": ["blue-square", "music", "note", "ampersand", "percent", "glyphs", "characters"] },
+	{ "category": "symbols", "char": "🎵", "name": "musical_note", "keywords": ["score", "tone", "sound"] },
+	{ "category": "symbols", "char": "🎶", "name": "notes", "keywords": ["music", "score"] },
+	{ "category": "symbols", "char": "〰️", "name": "wavy_dash", "keywords": ["draw", "line", "moustache", "mustache", "squiggle", "scribble"] },
+	{ "category": "symbols", "char": "➰", "name": "curly_loop", "keywords": ["scribble", "draw", "shape", "squiggle"] },
+	{ "category": "symbols", "char": "✔️", "name": "heavy_check_mark", "keywords": ["ok", "nike", "answer", "yes", "tick"] },
+	{ "category": "symbols", "char": "🔃", "name": "arrows_clockwise", "keywords": ["sync", "cycle", "round", "repeat"] },
+	{ "category": "symbols", "char": "➕", "name": "heavy_plus_sign", "keywords": ["math", "calculation", "addition", "more", "increase"] },
+	{ "category": "symbols", "char": "➖", "name": "heavy_minus_sign", "keywords": ["math", "calculation", "subtract", "less"] },
+	{ "category": "symbols", "char": "➗", "name": "heavy_division_sign", "keywords": ["divide", "math", "calculation"] },
+	{ "category": "symbols", "char": "✖️", "name": "heavy_multiplication_x", "keywords": ["math", "calculation"] },
+	{ "category": "symbols", "char": "♾", "name": "infinity", "keywords": ["forever"] },
+	{ "category": "symbols", "char": "💲", "name": "heavy_dollar_sign", "keywords": ["money", "sales", "payment", "currency", "buck"] },
+	{ "category": "symbols", "char": "💱", "name": "currency_exchange", "keywords": ["money", "sales", "dollar", "travel"] },
+	{ "category": "symbols", "char": "©️", "name": "copyright", "keywords": ["ip", "license", "circle", "law", "legal"] },
+	{ "category": "symbols", "char": "®️", "name": "registered", "keywords": ["alphabet", "circle"] },
+	{ "category": "symbols", "char": "™️", "name": "tm", "keywords": ["trademark", "brand", "law", "legal"] },
+	{ "category": "symbols", "char": "🔚", "name": "end", "keywords": ["words", "arrow"] },
+	{ "category": "symbols", "char": "🔙", "name": "back", "keywords": ["arrow", "words", "return"] },
+	{ "category": "symbols", "char": "🔛", "name": "on", "keywords": ["arrow", "words"] },
+	{ "category": "symbols", "char": "🔝", "name": "top", "keywords": ["words", "blue-square"] },
+	{ "category": "symbols", "char": "🔜", "name": "soon", "keywords": ["arrow", "words"] },
+	{ "category": "symbols", "char": "☑️", "name": "ballot_box_with_check", "keywords": ["ok", "agree", "confirm", "black-square", "vote", "election", "yes", "tick"] },
+	{ "category": "symbols", "char": "🔘", "name": "radio_button", "keywords": ["input", "old", "music", "circle"] },
+	{ "category": "symbols", "char": "⚫", "name": "black_circle", "keywords": ["shape", "button", "round"] },
+	{ "category": "symbols", "char": "⚪", "name": "white_circle", "keywords": ["shape", "round"] },
+	{ "category": "symbols", "char": "🔴", "name": "red_circle", "keywords": ["shape", "error", "danger"] },
+	{ "category": "symbols", "char": "🟠", "name": "orange_circle", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟡", "name": "yellow_circle", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟢", "name": "green_circle", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🔵", "name": "large_blue_circle", "keywords": ["shape", "icon", "button"] },
+	{ "category": "symbols", "char": "🟣", "name": "purple_circle", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟤", "name": "brown_circle", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🔸", "name": "small_orange_diamond", "keywords": ["shape", "jewel", "gem"] },
+	{ "category": "symbols", "char": "🔹", "name": "small_blue_diamond", "keywords": ["shape", "jewel", "gem"] },
+	{ "category": "symbols", "char": "🔶", "name": "large_orange_diamond", "keywords": ["shape", "jewel", "gem"] },
+	{ "category": "symbols", "char": "🔷", "name": "large_blue_diamond", "keywords": ["shape", "jewel", "gem"] },
+	{ "category": "symbols", "char": "🔺", "name": "small_red_triangle", "keywords": ["shape", "direction", "up", "top"] },
+	{ "category": "symbols", "char": "▪️", "name": "black_small_square", "keywords": ["shape", "icon"] },
+	{ "category": "symbols", "char": "▫️", "name": "white_small_square", "keywords": ["shape", "icon"] },
+	{ "category": "symbols", "char": "⬛", "name": "black_large_square", "keywords": ["shape", "icon", "button"] },
+	{ "category": "symbols", "char": "⬜", "name": "white_large_square", "keywords": ["shape", "icon", "stone", "button"] },
+	{ "category": "symbols", "char": "🟥", "name": "red_square", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟧", "name": "orange_square", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟨", "name": "yellow_square", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟩", "name": "green_square", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟦", "name": "blue_square", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟪", "name": "purple_square", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🟫", "name": "brown_square", "keywords": ["shape"] },
+	{ "category": "symbols", "char": "🔻", "name": "small_red_triangle_down", "keywords": ["shape", "direction", "bottom"] },
+	{ "category": "symbols", "char": "◼️", "name": "black_medium_square", "keywords": ["shape", "button", "icon"] },
+	{ "category": "symbols", "char": "◻️", "name": "white_medium_square", "keywords": ["shape", "stone", "icon"] },
+	{ "category": "symbols", "char": "◾", "name": "black_medium_small_square", "keywords": ["icon", "shape", "button"] },
+	{ "category": "symbols", "char": "◽", "name": "white_medium_small_square", "keywords": ["shape", "stone", "icon", "button"] },
+	{ "category": "symbols", "char": "🔲", "name": "black_square_button", "keywords": ["shape", "input", "frame"] },
+	{ "category": "symbols", "char": "🔳", "name": "white_square_button", "keywords": ["shape", "input"] },
+	{ "category": "symbols", "char": "🔈", "name": "speaker", "keywords": ["sound", "volume", "silence", "broadcast"] },
+	{ "category": "symbols", "char": "🔉", "name": "sound", "keywords": ["volume", "speaker", "broadcast"] },
+	{ "category": "symbols", "char": "🔊", "name": "loud_sound", "keywords": ["volume", "noise", "noisy", "speaker", "broadcast"] },
+	{ "category": "symbols", "char": "🔇", "name": "mute", "keywords": ["sound", "volume", "silence", "quiet"] },
+	{ "category": "symbols", "char": "📣", "name": "mega", "keywords": ["sound", "speaker", "volume"] },
+	{ "category": "symbols", "char": "📢", "name": "loudspeaker", "keywords": ["volume", "sound"] },
+	{ "category": "symbols", "char": "🔔", "name": "bell", "keywords": ["sound", "notification", "christmas", "xmas", "chime"] },
+	{ "category": "symbols", "char": "🔕", "name": "no_bell", "keywords": ["sound", "volume", "mute", "quiet", "silent"] },
+	{ "category": "symbols", "char": "🃏", "name": "black_joker", "keywords": ["poker", "cards", "game", "play", "magic"] },
+	{ "category": "symbols", "char": "🀄", "name": "mahjong", "keywords": ["game", "play", "chinese", "kanji"] },
+	{ "category": "symbols", "char": "♠️", "name": "spades", "keywords": ["poker", "cards", "suits", "magic"] },
+	{ "category": "symbols", "char": "♣️", "name": "clubs", "keywords": ["poker", "cards", "magic", "suits"] },
+	{ "category": "symbols", "char": "♥️", "name": "hearts", "keywords": ["poker", "cards", "magic", "suits"] },
+	{ "category": "symbols", "char": "♦️", "name": "diamonds", "keywords": ["poker", "cards", "magic", "suits"] },
+	{ "category": "symbols", "char": "🎴", "name": "flower_playing_cards", "keywords": ["game", "sunset", "red"] },
+	{ "category": "symbols", "char": "💭", "name": "thought_balloon", "keywords": ["bubble", "cloud", "speech", "thinking", "dream"] },
+	{ "category": "symbols", "char": "🗯", "name": "right_anger_bubble", "keywords": ["caption", "speech", "thinking", "mad"] },
+	{ "category": "symbols", "char": "💬", "name": "speech_balloon", "keywords": ["bubble", "words", "message", "talk", "chatting"] },
+	{ "category": "symbols", "char": "🗨", "name": "left_speech_bubble", "keywords": ["words", "message", "talk", "chatting"] },
+	{ "category": "symbols", "char": "🕐", "name": "clock1", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕑", "name": "clock2", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕒", "name": "clock3", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕓", "name": "clock4", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕔", "name": "clock5", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕕", "name": "clock6", "keywords": ["time", "late", "early", "schedule", "dawn", "dusk"] },
+	{ "category": "symbols", "char": "🕖", "name": "clock7", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕗", "name": "clock8", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕘", "name": "clock9", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕙", "name": "clock10", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕚", "name": "clock11", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕛", "name": "clock12", "keywords": ["time", "noon", "midnight", "midday", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕜", "name": "clock130", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕝", "name": "clock230", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕞", "name": "clock330", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕟", "name": "clock430", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕠", "name": "clock530", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕡", "name": "clock630", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕢", "name": "clock730", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕣", "name": "clock830", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕤", "name": "clock930", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕥", "name": "clock1030", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕦", "name": "clock1130", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "symbols", "char": "🕧", "name": "clock1230", "keywords": ["time", "late", "early", "schedule"] },
+	{ "category": "flags", "char": "🇦🇫", "name": "afghanistan", "keywords": ["af", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇽", "name": "aland_islands", "keywords": ["Åland", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇱", "name": "albania", "keywords": ["al", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇩🇿", "name": "algeria", "keywords": ["dz", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇸", "name": "american_samoa", "keywords": ["american", "ws", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇩", "name": "andorra", "keywords": ["ad", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇴", "name": "angola", "keywords": ["ao", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇮", "name": "anguilla", "keywords": ["ai", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇶", "name": "antarctica", "keywords": ["aq", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇬", "name": "antigua_barbuda", "keywords": ["antigua", "barbuda", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇷", "name": "argentina", "keywords": ["ar", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇲", "name": "armenia", "keywords": ["am", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇼", "name": "aruba", "keywords": ["aw", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇨", "name": "ascension_island", "keywords": ["flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇺", "name": "australia", "keywords": ["au", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇹", "name": "austria", "keywords": ["at", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇿", "name": "azerbaijan", "keywords": ["az", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇸", "name": "bahamas", "keywords": ["bs", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇭", "name": "bahrain", "keywords": ["bh", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇩", "name": "bangladesh", "keywords": ["bd", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇧", "name": "barbados", "keywords": ["bb", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇾", "name": "belarus", "keywords": ["by", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇪", "name": "belgium", "keywords": ["be", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇿", "name": "belize", "keywords": ["bz", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇯", "name": "benin", "keywords": ["bj", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇲", "name": "bermuda", "keywords": ["bm", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇹", "name": "bhutan", "keywords": ["bt", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇴", "name": "bolivia", "keywords": ["bo", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇶", "name": "caribbean_netherlands", "keywords": ["bonaire", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇦", "name": "bosnia_herzegovina", "keywords": ["bosnia", "herzegovina", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇼", "name": "botswana", "keywords": ["bw", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇷", "name": "brazil", "keywords": ["br", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇴", "name": "british_indian_ocean_territory", "keywords": ["british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇻🇬", "name": "british_virgin_islands", "keywords": ["british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇳", "name": "brunei", "keywords": ["bn", "darussalam", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇬", "name": "bulgaria", "keywords": ["bg", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇫", "name": "burkina_faso", "keywords": ["burkina", "faso", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇮", "name": "burundi", "keywords": ["bi", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇻", "name": "cape_verde", "keywords": ["cabo", "verde", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇭", "name": "cambodia", "keywords": ["kh", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇲", "name": "cameroon", "keywords": ["cm", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇦", "name": "canada", "keywords": ["ca", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇨", "name": "canary_islands", "keywords": ["canary", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇾", "name": "cayman_islands", "keywords": ["cayman", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇫", "name": "central_african_republic", "keywords": ["central", "african", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇩", "name": "chad", "keywords": ["td", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇱", "name": "chile", "keywords": ["flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇳", "name": "cn", "keywords": ["china", "chinese", "prc", "flag", "country", "nation", "banner"] },
+	{ "category": "flags", "char": "🇨🇽", "name": "christmas_island", "keywords": ["christmas", "island", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇨", "name": "cocos_islands", "keywords": ["cocos", "keeling", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇴", "name": "colombia", "keywords": ["co", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇲", "name": "comoros", "keywords": ["km", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇬", "name": "congo_brazzaville", "keywords": ["congo", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇩", "name": "congo_kinshasa", "keywords": ["congo", "democratic", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇰", "name": "cook_islands", "keywords": ["cook", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇷", "name": "costa_rica", "keywords": ["costa", "rica", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇭🇷", "name": "croatia", "keywords": ["hr", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇺", "name": "cuba", "keywords": ["cu", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇼", "name": "curacao", "keywords": ["curaçao", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇾", "name": "cyprus", "keywords": ["cy", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇿", "name": "czech_republic", "keywords": ["cz", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇩🇰", "name": "denmark", "keywords": ["dk", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇩🇯", "name": "djibouti", "keywords": ["dj", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇩🇲", "name": "dominica", "keywords": ["dm", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇩🇴", "name": "dominican_republic", "keywords": ["dominican", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇪🇨", "name": "ecuador", "keywords": ["ec", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇪🇬", "name": "egypt", "keywords": ["eg", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇻", "name": "el_salvador", "keywords": ["el", "salvador", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇶", "name": "equatorial_guinea", "keywords": ["equatorial", "gn", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇪🇷", "name": "eritrea", "keywords": ["er", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇪🇪", "name": "estonia", "keywords": ["ee", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇪🇹", "name": "ethiopia", "keywords": ["et", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇪🇺", "name": "eu", "keywords": ["european", "union", "flag", "banner"] },
+	{ "category": "flags", "char": "🇫🇰", "name": "falkland_islands", "keywords": ["falkland", "islands", "malvinas", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇫🇴", "name": "faroe_islands", "keywords": ["faroe", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇫🇯", "name": "fiji", "keywords": ["fj", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇫🇮", "name": "finland", "keywords": ["fi", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇫🇷", "name": "fr", "keywords": ["banner", "flag", "nation", "france", "french", "country"] },
+	{ "category": "flags", "char": "🇬🇫", "name": "french_guiana", "keywords": ["french", "guiana", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇫", "name": "french_polynesia", "keywords": ["french", "polynesia", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇫", "name": "french_southern_territories", "keywords": ["french", "southern", "territories", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇦", "name": "gabon", "keywords": ["ga", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇲", "name": "gambia", "keywords": ["gm", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇪", "name": "georgia", "keywords": ["ge", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇩🇪", "name": "de", "keywords": ["german", "nation", "flag", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇭", "name": "ghana", "keywords": ["gh", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇮", "name": "gibraltar", "keywords": ["gi", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇷", "name": "greece", "keywords": ["gr", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇱", "name": "greenland", "keywords": ["gl", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇩", "name": "grenada", "keywords": ["gd", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇵", "name": "guadeloupe", "keywords": ["gp", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇺", "name": "guam", "keywords": ["gu", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇹", "name": "guatemala", "keywords": ["gt", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇬", "name": "guernsey", "keywords": ["gg", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇳", "name": "guinea", "keywords": ["gn", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇼", "name": "guinea_bissau", "keywords": ["gw", "bissau", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇾", "name": "guyana", "keywords": ["gy", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇭🇹", "name": "haiti", "keywords": ["ht", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇭🇳", "name": "honduras", "keywords": ["hn", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇭🇰", "name": "hong_kong", "keywords": ["hong", "kong", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇭🇺", "name": "hungary", "keywords": ["hu", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇸", "name": "iceland", "keywords": ["is", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇳", "name": "india", "keywords": ["in", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇩", "name": "indonesia", "keywords": ["flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇷", "name": "iran", "keywords": ["iran, ", "islamic", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇶", "name": "iraq", "keywords": ["iq", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇪", "name": "ireland", "keywords": ["ie", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇲", "name": "isle_of_man", "keywords": ["isle", "man", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇱", "name": "israel", "keywords": ["il", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇮🇹", "name": "it", "keywords": ["italy", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇮", "name": "cote_divoire", "keywords": ["ivory", "coast", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇯🇲", "name": "jamaica", "keywords": ["jm", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇯🇵", "name": "jp", "keywords": ["japanese", "nation", "flag", "country", "banner"] },
+	{ "category": "flags", "char": "🇯🇪", "name": "jersey", "keywords": ["je", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇯🇴", "name": "jordan", "keywords": ["jo", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇿", "name": "kazakhstan", "keywords": ["kz", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇪", "name": "kenya", "keywords": ["ke", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇮", "name": "kiribati", "keywords": ["ki", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇽🇰", "name": "kosovo", "keywords": ["xk", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇼", "name": "kuwait", "keywords": ["kw", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇬", "name": "kyrgyzstan", "keywords": ["kg", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇦", "name": "laos", "keywords": ["lao", "democratic", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇻", "name": "latvia", "keywords": ["lv", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇧", "name": "lebanon", "keywords": ["lb", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇸", "name": "lesotho", "keywords": ["ls", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇷", "name": "liberia", "keywords": ["lr", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇾", "name": "libya", "keywords": ["ly", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇮", "name": "liechtenstein", "keywords": ["li", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇹", "name": "lithuania", "keywords": ["lt", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇺", "name": "luxembourg", "keywords": ["lu", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇴", "name": "macau", "keywords": ["macao", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇰", "name": "macedonia", "keywords": ["macedonia, ", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇬", "name": "madagascar", "keywords": ["mg", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇼", "name": "malawi", "keywords": ["mw", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇾", "name": "malaysia", "keywords": ["my", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇻", "name": "maldives", "keywords": ["mv", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇱", "name": "mali", "keywords": ["ml", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇹", "name": "malta", "keywords": ["mt", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇭", "name": "marshall_islands", "keywords": ["marshall", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇶", "name": "martinique", "keywords": ["mq", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇷", "name": "mauritania", "keywords": ["mr", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇺", "name": "mauritius", "keywords": ["mu", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇾🇹", "name": "mayotte", "keywords": ["yt", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇽", "name": "mexico", "keywords": ["mx", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇫🇲", "name": "micronesia", "keywords": ["micronesia, ", "federated", "states", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇩", "name": "moldova", "keywords": ["moldova, ", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇨", "name": "monaco", "keywords": ["mc", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇳", "name": "mongolia", "keywords": ["mn", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇪", "name": "montenegro", "keywords": ["me", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇸", "name": "montserrat", "keywords": ["ms", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇦", "name": "morocco", "keywords": ["ma", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇿", "name": "mozambique", "keywords": ["mz", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇲", "name": "myanmar", "keywords": ["mm", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇦", "name": "namibia", "keywords": ["na", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇷", "name": "nauru", "keywords": ["nr", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇵", "name": "nepal", "keywords": ["np", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇱", "name": "netherlands", "keywords": ["nl", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇨", "name": "new_caledonia", "keywords": ["new", "caledonia", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇿", "name": "new_zealand", "keywords": ["new", "zealand", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇮", "name": "nicaragua", "keywords": ["ni", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇪", "name": "niger", "keywords": ["ne", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇬", "name": "nigeria", "keywords": ["flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇺", "name": "niue", "keywords": ["nu", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇫", "name": "norfolk_island", "keywords": ["norfolk", "island", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇲🇵", "name": "northern_mariana_islands", "keywords": ["northern", "mariana", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇵", "name": "north_korea", "keywords": ["north", "korea", "nation", "flag", "country", "banner"] },
+	{ "category": "flags", "char": "🇳🇴", "name": "norway", "keywords": ["no", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇴🇲", "name": "oman", "keywords": ["om_symbol", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇰", "name": "pakistan", "keywords": ["pk", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇼", "name": "palau", "keywords": ["pw", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇸", "name": "palestinian_territories", "keywords": ["palestine", "palestinian", "territories", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇦", "name": "panama", "keywords": ["pa", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇬", "name": "papua_new_guinea", "keywords": ["papua", "new", "guinea", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇾", "name": "paraguay", "keywords": ["py", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇪", "name": "peru", "keywords": ["pe", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇭", "name": "philippines", "keywords": ["ph", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇳", "name": "pitcairn_islands", "keywords": ["pitcairn", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇱", "name": "poland", "keywords": ["pl", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇹", "name": "portugal", "keywords": ["pt", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇷", "name": "puerto_rico", "keywords": ["puerto", "rico", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇶🇦", "name": "qatar", "keywords": ["qa", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇷🇪", "name": "reunion", "keywords": ["réunion", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇷🇴", "name": "romania", "keywords": ["ro", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇷🇺", "name": "ru", "keywords": ["russian", "federation", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇷🇼", "name": "rwanda", "keywords": ["rw", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇧🇱", "name": "st_barthelemy", "keywords": ["saint", "barthélemy", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇭", "name": "st_helena", "keywords": ["saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇳", "name": "st_kitts_nevis", "keywords": ["saint", "kitts", "nevis", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇨", "name": "st_lucia", "keywords": ["saint", "lucia", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇵🇲", "name": "st_pierre_miquelon", "keywords": ["saint", "pierre", "miquelon", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇻🇨", "name": "st_vincent_grenadines", "keywords": ["saint", "vincent", "grenadines", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇼🇸", "name": "samoa", "keywords": ["ws", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇲", "name": "san_marino", "keywords": ["san", "marino", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇹", "name": "sao_tome_principe", "keywords": ["sao", "tome", "principe", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇦", "name": "saudi_arabia", "keywords": ["flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇳", "name": "senegal", "keywords": ["sn", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇷🇸", "name": "serbia", "keywords": ["rs", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇨", "name": "seychelles", "keywords": ["sc", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇱", "name": "sierra_leone", "keywords": ["sierra", "leone", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇬", "name": "singapore", "keywords": ["sg", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇽", "name": "sint_maarten", "keywords": ["sint", "maarten", "dutch", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇰", "name": "slovakia", "keywords": ["sk", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇮", "name": "slovenia", "keywords": ["si", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇧", "name": "solomon_islands", "keywords": ["solomon", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇴", "name": "somalia", "keywords": ["so", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇿🇦", "name": "south_africa", "keywords": ["south", "africa", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇸", "name": "south_georgia_south_sandwich_islands", "keywords": ["south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇰🇷", "name": "kr", "keywords": ["south", "korea", "nation", "flag", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇸", "name": "south_sudan", "keywords": ["south", "sd", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇪🇸", "name": "es", "keywords": ["spain", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇱🇰", "name": "sri_lanka", "keywords": ["sri", "lanka", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇩", "name": "sudan", "keywords": ["sd", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇷", "name": "suriname", "keywords": ["sr", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇿", "name": "swaziland", "keywords": ["sz", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇪", "name": "sweden", "keywords": ["se", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇨🇭", "name": "switzerland", "keywords": ["ch", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇸🇾", "name": "syria", "keywords": ["syrian", "arab", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇼", "name": "taiwan", "keywords": ["tw", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇯", "name": "tajikistan", "keywords": ["tj", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇿", "name": "tanzania", "keywords": ["tanzania, ", "united", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇭", "name": "thailand", "keywords": ["th", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇱", "name": "timor_leste", "keywords": ["timor", "leste", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇬", "name": "togo", "keywords": ["tg", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇰", "name": "tokelau", "keywords": ["tk", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇴", "name": "tonga", "keywords": ["to", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇹", "name": "trinidad_tobago", "keywords": ["trinidad", "tobago", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇦", "name": "tristan_da_cunha", "keywords": ["flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇳", "name": "tunisia", "keywords": ["tn", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇷", "name": "tr", "keywords": ["turkey", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇲", "name": "turkmenistan", "keywords": ["flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇨", "name": "turks_caicos_islands", "keywords": ["turks", "caicos", "islands", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇹🇻", "name": "tuvalu", "keywords": ["flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇺🇬", "name": "uganda", "keywords": ["ug", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇺🇦", "name": "ukraine", "keywords": ["ua", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇦🇪", "name": "united_arab_emirates", "keywords": ["united", "arab", "emirates", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇬🇧", "name": "uk", "keywords": ["united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "UK", "english", "england", "union jack"] },
+	{ "category": "flags", "char": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", "name": "england", "keywords": ["flag", "english"] },
+	{ "category": "flags", "char": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "name": "scotland", "keywords": ["flag", "scottish"] },
+	{ "category": "flags", "char": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", "name": "wales", "keywords": ["flag", "welsh"] },
+	{ "category": "flags", "char": "🇺🇸", "name": "us", "keywords": ["united", "states", "america", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇻🇮", "name": "us_virgin_islands", "keywords": ["virgin", "islands", "us", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇺🇾", "name": "uruguay", "keywords": ["uy", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇺🇿", "name": "uzbekistan", "keywords": ["uz", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇻🇺", "name": "vanuatu", "keywords": ["vu", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇻🇦", "name": "vatican_city", "keywords": ["vatican", "city", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇻🇪", "name": "venezuela", "keywords": ["ve", "bolivarian", "republic", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇻🇳", "name": "vietnam", "keywords": ["viet", "nam", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇼🇫", "name": "wallis_futuna", "keywords": ["wallis", "futuna", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇪🇭", "name": "western_sahara", "keywords": ["western", "sahara", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇾🇪", "name": "yemen", "keywords": ["ye", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇿🇲", "name": "zambia", "keywords": ["zm", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇿🇼", "name": "zimbabwe", "keywords": ["zw", "flag", "nation", "country", "banner"] },
+	{ "category": "flags", "char": "🇺🇳", "name": "united_nations", "keywords": ["un", "flag", "banner"] },
+	{ "category": "flags", "char": "🏴‍☠️", "name": "pirate_flag", "keywords": ["skull", "crossbones", "flag", "banner"] }
+]
diff --git a/packages/client/src/events.ts b/packages/client/src/events.ts
new file mode 100644
index 0000000000..dbbd908b8f
--- /dev/null
+++ b/packages/client/src/events.ts
@@ -0,0 +1,4 @@
+import { EventEmitter } from 'eventemitter3';
+
+// TODO: 型付け
+export const globalEvents = new EventEmitter();
diff --git a/packages/client/src/filters/bytes.ts b/packages/client/src/filters/bytes.ts
new file mode 100644
index 0000000000..50e63534b6
--- /dev/null
+++ b/packages/client/src/filters/bytes.ts
@@ -0,0 +1,9 @@
+export default (v, digits = 0) => {
+	if (v == null) return '?';
+	const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+	if (v == 0) return '0';
+	const isMinus = v < 0;
+	if (isMinus) v = -v;
+	const i = Math.floor(Math.log(v) / Math.log(1024));
+	return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
+};
diff --git a/packages/client/src/filters/note.ts b/packages/client/src/filters/note.ts
new file mode 100644
index 0000000000..5c000cf83b
--- /dev/null
+++ b/packages/client/src/filters/note.ts
@@ -0,0 +1,3 @@
+export default note => {
+	return `/notes/${note.id}`;
+};
diff --git a/packages/client/src/filters/number.ts b/packages/client/src/filters/number.ts
new file mode 100644
index 0000000000..880a848ca4
--- /dev/null
+++ b/packages/client/src/filters/number.ts
@@ -0,0 +1 @@
+export default n => n == null ? 'N/A' : n.toLocaleString();
diff --git a/packages/client/src/filters/user.ts b/packages/client/src/filters/user.ts
new file mode 100644
index 0000000000..ff2f7e2dae
--- /dev/null
+++ b/packages/client/src/filters/user.ts
@@ -0,0 +1,15 @@
+import * as misskey from 'misskey-js';
+import * as Acct from 'misskey-js/built/acct';
+import { url } from '@/config';
+
+export const acct = (user: misskey.Acct) => {
+	return Acct.toString(user);
+};
+
+export const userName = (user: misskey.entities.User) => {
+	return user.name || user.username;
+};
+
+export const userPage = (user: misskey.Acct, path?, absolute = false) => {
+	return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
+};
diff --git a/packages/client/src/i18n.ts b/packages/client/src/i18n.ts
new file mode 100644
index 0000000000..fbc10a0bad
--- /dev/null
+++ b/packages/client/src/i18n.ts
@@ -0,0 +1,13 @@
+import { markRaw } from 'vue';
+import { locale } from '@/config';
+import { I18n } from '@/scripts/i18n';
+
+export const i18n = markRaw(new I18n(locale));
+
+// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
+declare module '@vue/runtime-core' {
+	interface ComponentCustomProperties {
+		$t: typeof i18n['t'];
+		$ts: typeof i18n['locale'];
+	}
+}
diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts
new file mode 100644
index 0000000000..da5b0489ab
--- /dev/null
+++ b/packages/client/src/init.ts
@@ -0,0 +1,420 @@
+/**
+ * Client entry point
+ */
+
+import '@/style.scss';
+
+//#region account indexedDB migration
+import { set } from '@/scripts/idb-proxy';
+
+if (localStorage.getItem('accounts') != null) {
+	set('accounts', JSON.parse(localStorage.getItem('accounts')));
+	localStorage.removeItem('accounts');
+}
+//#endregion
+
+import * as Sentry from '@sentry/browser';
+import { Integrations } from '@sentry/tracing';
+import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue';
+import compareVersions from 'compare-versions';
+
+import widgets from '@/widgets';
+import directives from '@/directives';
+import components from '@/components';
+import { version, ui, lang, host } from '@/config';
+import { router } from '@/router';
+import { applyTheme } from '@/scripts/theme';
+import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
+import { i18n } from '@/i18n';
+import { stream, dialog, post, popup } from '@/os';
+import * as sound from '@/scripts/sound';
+import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
+import { defaultStore, ColdDeviceStorage } from '@/store';
+import { fetchInstance, instance } from '@/instance';
+import { makeHotkey } from '@/scripts/hotkey';
+import { search } from '@/scripts/search';
+import { isMobile } from '@/scripts/is-mobile';
+import { initializeSw } from '@/scripts/initialize-sw';
+import { reloadChannel } from '@/scripts/unison-reload';
+import { reactionPicker } from '@/scripts/reaction-picker';
+import { getUrlWithoutLoginId } from '@/scripts/login-id';
+import { getAccountFromId } from '@/scripts/get-account-from-id';
+
+console.info(`Misskey v${version}`);
+
+// boot.jsのやつを解除
+window.onerror = null;
+window.onunhandledrejection = null;
+
+if (_DEV_) {
+	console.warn('Development mode!!!');
+
+	console.info(`vue ${vueVersion}`);
+
+	(window as any).$i = $i;
+	(window as any).$store = defaultStore;
+
+	window.addEventListener('error', event => {
+		console.error(event);
+		/*
+		dialog({
+			type: 'error',
+			title: 'DEV: Unhandled error',
+			text: event.message
+		});
+		*/
+	});
+
+	window.addEventListener('unhandledrejection', event => {
+		console.error(event);
+		/*
+		dialog({
+			type: 'error',
+			title: 'DEV: Unhandled promise rejection',
+			text: event.reason
+		});
+		*/
+	});
+}
+
+if (defaultStore.state.reportError && !_DEV_) {
+	Sentry.init({
+		dsn: 'https://fd273254a07a4b61857607a9ea05d629@o501808.ingest.sentry.io/5583438',
+		tracesSampleRate: 1.0,
+	});
+
+	Sentry.setTag('misskey_version', version);
+	Sentry.setTag('ui', ui);
+	Sentry.setTag('lang', lang);
+	Sentry.setTag('host', host);
+}
+
+// タッチデバイスでCSSの:hoverを機能させる
+document.addEventListener('touchend', () => {}, { passive: true });
+
+// 一斉リロード
+reloadChannel.addEventListener('message', path => {
+	if (path !== null) location.href = path;
+	else location.reload();
+});
+
+//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+// TODO: いつの日にか消したい
+const vh = window.innerHeight * 0.01;
+document.documentElement.style.setProperty('--vh', `${vh}px`);
+window.addEventListener('resize', () => {
+	const vh = window.innerHeight * 0.01;
+	document.documentElement.style.setProperty('--vh', `${vh}px`);
+});
+//#endregion
+
+// If mobile, insert the viewport meta tag
+if (isMobile || window.innerWidth <= 1024) {
+	const viewport = document.getElementsByName('viewport').item(0);
+	viewport.setAttribute('content',
+		`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
+	document.head.appendChild(viewport);
+}
+
+//#region Set lang attr
+const html = document.documentElement;
+html.setAttribute('lang', lang);
+//#endregion
+
+//#region loginId
+const params = new URLSearchParams(location.search);
+const loginId = params.get('loginId');
+
+if (loginId) {
+	const target = getUrlWithoutLoginId(location.href);
+
+	if (!$i || $i.id !== loginId) {
+		const account = await getAccountFromId(loginId);
+		if (account) {
+			await login(account.token, target);
+		}
+	}
+
+	history.replaceState({ misskey: 'loginId' }, '', target);
+}
+
+//#endregion
+
+//#region Fetch user
+if ($i && $i.token) {
+	if (_DEV_) {
+		console.log('account cache found. refreshing...');
+	}
+
+	refreshAccount();
+} else {
+	if (_DEV_) {
+		console.log('no account cache found.');
+	}
+
+	// 連携ログインの場合用にCookieを参照する
+	const i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1];
+
+	if (i != null && i !== 'null') {
+		if (_DEV_) {
+			console.log('signing...');
+		}
+
+		try {
+			document.body.innerHTML = '<div>Please wait...</div>';
+			await login(i);
+			location.reload();
+		} catch (e) {
+			// Render the error screen
+			// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
+			document.body.innerHTML = '<div id="err">Oops!</div>';
+		}
+	} else {
+		if (_DEV_) {
+			console.log('not signed in');
+		}
+	}
+}
+//#endregion
+
+fetchInstance().then(() => {
+	localStorage.setItem('v', instance.version);
+
+	// Init service worker
+	initializeSw();
+});
+
+const app = createApp(await (
+	window.location.search === '?zen' ? import('@/ui/zen.vue') :
+	!$i                               ? import('@/ui/visitor.vue') :
+	ui === 'deck'                     ? import('@/ui/deck.vue') :
+	ui === 'desktop'                  ? import('@/ui/desktop.vue') :
+	ui === 'chat'                     ? import('@/ui/chat/index.vue') :
+	ui === 'classic'                  ? import('@/ui/classic.vue') :
+	import('@/ui/universal.vue')
+).then(x => x.default));
+
+if (_DEV_) {
+	app.config.performance = true;
+}
+
+app.config.globalProperties = {
+	$i,
+	$store: defaultStore,
+	$instance: instance,
+	$t: i18n.t,
+	$ts: i18n.locale,
+};
+
+app.use(router);
+
+widgets(app);
+directives(app);
+components(app);
+
+await router.isReady();
+
+const splash = document.getElementById('splash');
+// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
+if (splash) splash.addEventListener('transitionend', () => {
+	splash.remove();
+});
+
+const rootEl = document.createElement('div');
+document.body.appendChild(rootEl);
+app.mount(rootEl);
+
+reactionPicker.init();
+
+if (splash) {
+	splash.style.opacity = '0';
+	splash.style.pointerEvents = 'none';
+}
+
+// クライアントが更新されたか？
+const lastVersion = localStorage.getItem('lastVersion');
+if (lastVersion !== version) {
+	localStorage.setItem('lastVersion', version);
+
+	// テーマリビルドするため
+	localStorage.removeItem('theme');
+
+	try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
+		if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
+			// ログインしてる場合だけ
+			if ($i) {
+				popup(import('@/components/updated.vue'), {}, {}, 'closed');
+			}
+		}
+	} catch (e) {
+	}
+}
+
+// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
+watch(defaultStore.reactiveState.darkMode, (darkMode) => {
+	applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
+}, { immediate: localStorage.theme == null });
+
+const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
+const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
+
+watch(darkTheme, (theme) => {
+	if (defaultStore.state.darkMode) {
+		applyTheme(theme);
+	}
+});
+
+watch(lightTheme, (theme) => {
+	if (!defaultStore.state.darkMode) {
+		applyTheme(theme);
+	}
+});
+
+//#region Sync dark mode
+if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
+	defaultStore.set('darkMode', isDeviceDarkmode());
+}
+
+window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
+	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
+		defaultStore.set('darkMode', mql.matches);
+	}
+});
+//#endregion
+
+// shortcut
+document.addEventListener('keydown', makeHotkey({
+	'd': () => {
+		defaultStore.set('darkMode', !defaultStore.state.darkMode);
+	},
+	'p|n': post,
+	's': search,
+	//TODO: 'h|/': help
+}));
+
+watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
+	document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
+}, { immediate: true });
+
+watch(defaultStore.reactiveState.useBlurEffect, v => {
+	if (v) {
+		document.documentElement.style.removeProperty('--blur');
+	} else {
+		document.documentElement.style.setProperty('--blur', 'none');
+	}
+}, { immediate: true });
+
+let reloadDialogShowing = false;
+stream.on('_disconnected_', async () => {
+	if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
+		location.reload();
+	} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
+		if (reloadDialogShowing) return;
+		reloadDialogShowing = true;
+		const { canceled } = await dialog({
+			type: 'warning',
+			title: i18n.locale.disconnectedFromServer,
+			text: i18n.locale.reloadConfirm,
+			showCancelButton: true
+		});
+		reloadDialogShowing = false;
+		if (!canceled) {
+			location.reload();
+		}
+	}
+});
+
+stream.on('emojiAdded', data => {
+	// TODO
+	//store.commit('instance/set', );
+});
+
+for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
+	import('./plugin').then(({ install }) => {
+		install(plugin);
+	});
+}
+
+if ($i) {
+	if ($i.isDeleted) {
+		dialog({
+			type: 'warning',
+			text: i18n.locale.accountDeletionInProgress,
+		});
+	}
+
+	if ('Notification' in window) {
+		// 許可を得ていなかったらリクエスト
+		if (Notification.permission === 'default') {
+			Notification.requestPermission();
+		}
+	}
+
+	const main = markRaw(stream.useChannel('main', null, 'System'));
+
+	// 自分の情報が更新されたとき
+	main.on('meUpdated', i => {
+		updateAccount(i);
+	});
+
+	main.on('readAllNotifications', () => {
+		updateAccount({ hasUnreadNotification: false });
+	});
+
+	main.on('unreadNotification', () => {
+		updateAccount({ hasUnreadNotification: true });
+	});
+
+	main.on('unreadMention', () => {
+		updateAccount({ hasUnreadMentions: true });
+	});
+
+	main.on('readAllUnreadMentions', () => {
+		updateAccount({ hasUnreadMentions: false });
+	});
+
+	main.on('unreadSpecifiedNote', () => {
+		updateAccount({ hasUnreadSpecifiedNotes: true });
+	});
+
+	main.on('readAllUnreadSpecifiedNotes', () => {
+		updateAccount({ hasUnreadSpecifiedNotes: false });
+	});
+
+	main.on('readAllMessagingMessages', () => {
+		updateAccount({ hasUnreadMessagingMessage: false });
+	});
+
+	main.on('unreadMessagingMessage', () => {
+		updateAccount({ hasUnreadMessagingMessage: true });
+		sound.play('chatBg');
+	});
+
+	main.on('readAllAntennas', () => {
+		updateAccount({ hasUnreadAntenna: false });
+	});
+
+	main.on('unreadAntenna', () => {
+		updateAccount({ hasUnreadAntenna: true });
+		sound.play('antenna');
+	});
+
+	main.on('readAllAnnouncements', () => {
+		updateAccount({ hasUnreadAnnouncement: false });
+	});
+
+	main.on('readAllChannels', () => {
+		updateAccount({ hasUnreadChannel: false });
+	});
+
+	main.on('unreadChannel', () => {
+		updateAccount({ hasUnreadChannel: true });
+		sound.play('channel');
+	});
+
+	// トークンが再生成されたとき
+	// このままではMisskeyが利用できないので強制的にサインアウトさせる
+	main.on('myTokenRegenerated', () => {
+		signout();
+	});
+}
diff --git a/packages/client/src/instance.ts b/packages/client/src/instance.ts
new file mode 100644
index 0000000000..6e912aa2e5
--- /dev/null
+++ b/packages/client/src/instance.ts
@@ -0,0 +1,52 @@
+import { computed, reactive } from 'vue';
+import * as Misskey from 'misskey-js';
+import { api } from './os';
+
+// TODO: 他のタブと永続化されたstateを同期
+
+const data = localStorage.getItem('instance');
+
+// TODO: instanceをリアクティブにするかは再考の余地あり
+
+export const instance: Misskey.entities.InstanceMetadata = reactive(data ? JSON.parse(data) : {
+	// TODO: set default values
+});
+
+export async function fetchInstance() {
+	const meta = await api('meta', {
+		detail: false
+	});
+
+	for (const [k, v] of Object.entries(meta)) {
+		instance[k] = v;
+	}
+
+	localStorage.setItem('instance', JSON.stringify(instance));
+}
+
+export const emojiCategories = computed(() => {
+	if (instance.emojis == null) return [];
+	const categories = new Set();
+	for (const emoji of instance.emojis) {
+		categories.add(emoji.category);
+	}
+	return Array.from(categories);
+});
+
+export const emojiTags = computed(() => {
+	if (instance.emojis == null) return [];
+	const tags = new Set();
+	for (const emoji of instance.emojis) {
+		for (const tag of emoji.aliases) {
+			tags.add(tag);
+		}
+	}
+	return Array.from(tags);
+});
+
+// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
+declare module '@vue/runtime-core' {
+	interface ComponentCustomProperties {
+		$instance: typeof instance;
+	}
+}
diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
new file mode 100644
index 0000000000..ae74740bb8
--- /dev/null
+++ b/packages/client/src/menu.ts
@@ -0,0 +1,224 @@
+import { computed, ref } from 'vue';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { ui } from '@/config';
+import { $i } from './account';
+import { unisonReload } from '@/scripts/unison-reload';
+import { router } from './router';
+
+export const menuDef = {
+	notifications: {
+		title: 'notifications',
+		icon: 'fas fa-bell',
+		show: computed(() => $i != null),
+		indicated: computed(() => $i != null && $i.hasUnreadNotification),
+		to: '/my/notifications',
+	},
+	messaging: {
+		title: 'messaging',
+		icon: 'fas fa-comments',
+		show: computed(() => $i != null),
+		indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage),
+		to: '/my/messaging',
+	},
+	drive: {
+		title: 'drive',
+		icon: 'fas fa-cloud',
+		show: computed(() => $i != null),
+		to: '/my/drive',
+	},
+	followRequests: {
+		title: 'followRequests',
+		icon: 'fas fa-user-clock',
+		show: computed(() => $i != null && $i.isLocked),
+		indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
+		to: '/my/follow-requests',
+	},
+	featured: {
+		title: 'featured',
+		icon: 'fas fa-fire-alt',
+		to: '/featured',
+	},
+	explore: {
+		title: 'explore',
+		icon: 'fas fa-hashtag',
+		to: '/explore',
+	},
+	announcements: {
+		title: 'announcements',
+		icon: 'fas fa-broadcast-tower',
+		indicated: computed(() => $i != null && $i.hasUnreadAnnouncement),
+		to: '/announcements',
+	},
+	search: {
+		title: 'search',
+		icon: 'fas fa-search',
+		action: () => search(),
+	},
+	lists: {
+		title: 'lists',
+		icon: 'fas fa-list-ul',
+		show: computed(() => $i != null),
+		active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')),
+		action: (ev) => {
+			const items = ref([{
+				type: 'pending'
+			}]);
+			os.api('users/lists/list').then(lists => {
+				const _items = [...lists.map(list => ({
+					type: 'link',
+					text: list.name,
+					to: `/timeline/list/${list.id}`
+				})), null, {
+					type: 'link',
+					to: '/my/lists',
+					text: i18n.locale.manageLists,
+					icon: 'fas fa-cog',
+				}];
+				items.value = _items;
+			});
+			os.popupMenu(items, ev.currentTarget || ev.target);
+		},
+	},
+	groups: {
+		title: 'groups',
+		icon: 'fas fa-users',
+		show: computed(() => $i != null),
+		to: '/my/groups',
+	},
+	antennas: {
+		title: 'antennas',
+		icon: 'fas fa-satellite',
+		show: computed(() => $i != null),
+		active: computed(() => router.currentRoute.value.path.startsWith('/timeline/antenna/') || router.currentRoute.value.path === '/my/antennas' || router.currentRoute.value.path.startsWith('/my/antennas/')),
+		action: (ev) => {
+			const items = ref([{
+				type: 'pending'
+			}]);
+			os.api('antennas/list').then(antennas => {
+				const _items = [...antennas.map(antenna => ({
+					type: 'link',
+					text: antenna.name,
+					to: `/timeline/antenna/${antenna.id}`
+				})), null, {
+					type: 'link',
+					to: '/my/antennas',
+					text: i18n.locale.manageAntennas,
+					icon: 'fas fa-cog',
+				}];
+				items.value = _items;
+			});
+			os.popupMenu(items, ev.currentTarget || ev.target);
+		},
+	},
+	mentions: {
+		title: 'mentions',
+		icon: 'fas fa-at',
+		show: computed(() => $i != null),
+		indicated: computed(() => $i != null && $i.hasUnreadMentions),
+		to: '/my/mentions',
+	},
+	messages: {
+		title: 'directNotes',
+		icon: 'fas fa-envelope',
+		show: computed(() => $i != null),
+		indicated: computed(() => $i != null && $i.hasUnreadSpecifiedNotes),
+		to: '/my/messages',
+	},
+	favorites: {
+		title: 'favorites',
+		icon: 'fas fa-star',
+		show: computed(() => $i != null),
+		to: '/my/favorites',
+	},
+	pages: {
+		title: 'pages',
+		icon: 'fas fa-file-alt',
+		to: '/pages',
+	},
+	gallery: {
+		title: 'gallery',
+		icon: 'fas fa-icons',
+		to: '/gallery',
+	},
+	clips: {
+		title: 'clip',
+		icon: 'fas fa-paperclip',
+		show: computed(() => $i != null),
+		to: '/my/clips',
+	},
+	channels: {
+		title: 'channel',
+		icon: 'fas fa-satellite-dish',
+		to: '/channels',
+	},
+	federation: {
+		title: 'federation',
+		icon: 'fas fa-globe',
+		to: '/federation',
+	},
+	emojis: {
+		title: 'emojis',
+		icon: 'fas fa-laugh',
+		to: '/emojis',
+	},
+	games: {
+		title: 'games',
+		icon: 'fas fa-gamepad',
+		to: '/games/reversi',
+	},
+	scratchpad: {
+		title: 'scratchpad',
+		icon: 'fas fa-terminal',
+		to: '/scratchpad',
+	},
+	rooms: {
+		title: 'rooms',
+		icon: 'fas fa-door-closed',
+		show: computed(() => $i != null),
+		to: computed(() => `/@${$i.username}/room`),
+	},
+	ui: {
+		title: 'switchUi',
+		icon: 'fas fa-columns',
+		action: (ev) => {
+			os.popupMenu([{
+				text: i18n.locale.default,
+				active: ui === 'default' || ui === null,
+				action: () => {
+					localStorage.setItem('ui', 'default');
+					unisonReload();
+				}
+			}, {
+				text: i18n.locale.deck,
+				active: ui === 'deck',
+				action: () => {
+					localStorage.setItem('ui', 'deck');
+					unisonReload();
+				}
+			}, {
+				text: i18n.locale.classic,
+				active: ui === 'classic',
+				action: () => {
+					localStorage.setItem('ui', 'classic');
+					unisonReload();
+				}
+			}, {
+				text: 'Chat (β)',
+				active: ui === 'chat',
+				action: () => {
+					localStorage.setItem('ui', 'chat');
+					unisonReload();
+				}
+			}, /*{
+				text: i18n.locale.desktop + ' (β)',
+				active: ui === 'desktop',
+				action: () => {
+					localStorage.setItem('ui', 'desktop');
+					unisonReload();
+				}
+			}*/], ev.currentTarget || ev.target);
+		},
+	},
+};
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
new file mode 100644
index 0000000000..a570ffc9ed
--- /dev/null
+++ b/packages/client/src/os.ts
@@ -0,0 +1,501 @@
+// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
+
+import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
+import { EventEmitter } from 'eventemitter3';
+import insertTextAtCursor from 'insert-text-at-cursor';
+import * as Misskey from 'misskey-js';
+import * as Sentry from '@sentry/browser';
+import { apiUrl, debug, url } from '@/config';
+import MkPostFormDialog from '@/components/post-form-dialog.vue';
+import MkWaitingDialog from '@/components/waiting-dialog.vue';
+import { resolve } from '@/router';
+import { $i } from '@/account';
+import { defaultStore } from '@/store';
+
+export const stream = markRaw(new Misskey.Stream(url, $i));
+
+export const pendingApiRequestsCount = ref(0);
+let apiRequestsCount = 0; // for debug
+export const apiRequests = ref([]); // for debug
+
+export const windows = new Map();
+
+const apiClient = new Misskey.api.APIClient({
+	origin: url,
+});
+
+export const api = ((endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) => {
+	pendingApiRequestsCount.value++;
+
+	const onFinally = () => {
+		pendingApiRequestsCount.value--;
+	};
+
+	const log = debug ? reactive({
+		id: ++apiRequestsCount,
+		endpoint,
+		req: markRaw(data),
+		res: null,
+		state: 'pending',
+	}) : null;
+	if (debug) {
+		apiRequests.value.push(log);
+		if (apiRequests.value.length > 128) apiRequests.value.shift();
+	}
+
+	const promise = new Promise((resolve, reject) => {
+		// Append a credential
+		if ($i) (data as any).i = $i.token;
+		if (token !== undefined) (data as any).i = token;
+
+		// Send request
+		fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
+			method: 'POST',
+			body: JSON.stringify(data),
+			credentials: 'omit',
+			cache: 'no-cache'
+		}).then(async (res) => {
+			const body = res.status === 204 ? null : await res.json();
+
+			if (res.status === 200) {
+				resolve(body);
+				if (debug) {
+					log!.res = markRaw(JSON.parse(JSON.stringify(body)));
+					log!.state = 'success';
+				}
+			} else if (res.status === 204) {
+				resolve();
+				if (debug) {
+					log!.state = 'success';
+				}
+			} else {
+				reject(body.error);
+				if (debug) {
+					log!.res = markRaw(body.error);
+					log!.state = 'failed';
+				}
+
+				if (defaultStore.state.reportError && !_DEV_) {
+					Sentry.withScope((scope) => {
+						scope.setTag('api_endpoint', endpoint);
+						scope.setContext('api params', data);
+						scope.setContext('api error info', body.info);
+						scope.setTag('api_error_id', body.id);
+						scope.setTag('api_error_code', body.code);
+						scope.setTag('api_error_kind', body.kind);
+						scope.setLevel(Sentry.Severity.Error);
+						Sentry.captureMessage('API error');
+					});
+				}
+			}
+		}).catch(reject);
+	});
+
+	promise.then(onFinally, onFinally);
+
+	return promise;
+}) as typeof apiClient.request;
+
+export const apiWithDialog = ((
+	endpoint: string,
+	data: Record<string, any> = {},
+	token?: string | null | undefined,
+) => {
+	const promise = api(endpoint, data, token);
+	promiseDialog(promise, null, (e) => {
+		dialog({
+			type: 'error',
+			text: e.message + '\n' + (e as any).id,
+		});
+	});
+
+	return promise;
+}) as typeof api;
+
+export function promiseDialog<T extends Promise<any>>(
+	promise: T,
+	onSuccess?: ((res: any) => void) | null,
+	onFailure?: ((e: Error) => void) | null,
+	text?: string,
+): T {
+	const showing = ref(true);
+	const success = ref(false);
+
+	promise.then(res => {
+		if (onSuccess) {
+			showing.value = false;
+			onSuccess(res);
+		} else {
+			success.value = true;
+			setTimeout(() => {
+				showing.value = false;
+			}, 1000);
+		}
+	}).catch(e => {
+		showing.value = false;
+		if (onFailure) {
+			onFailure(e);
+		} else {
+			dialog({
+				type: 'error',
+				text: e
+			});
+		}
+	});
+
+	// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
+	popup(MkWaitingDialog, {
+		success: success,
+		showing: showing,
+		text: text,
+	}, {}, 'closed');
+
+	return promise;
+}
+
+function isModule(x: any): x is typeof import('*.vue') {
+	return x.default != null;
+}
+
+let popupIdCount = 0;
+export const popups = ref([]) as Ref<{
+	id: any;
+	component: any;
+	props: Record<string, any>;
+}[]>;
+
+export async function popup(component: Component | typeof import('*.vue') | Promise<Component | typeof import('*.vue')>, props: Record<string, any>, events = {}, disposeEvent?: string) {
+	if (component.then) component = await component;
+
+	if (isModule(component)) component = component.default;
+	markRaw(component);
+
+	const id = ++popupIdCount;
+	const dispose = () => {
+		if (_DEV_) console.log('os:popup close', id, component, props, events);
+		// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ？
+		setTimeout(() => {
+			popups.value = popups.value.filter(popup => popup.id !== id);
+		}, 0);
+	};
+	const state = {
+		component,
+		props,
+		events: disposeEvent ? {
+			...events,
+			[disposeEvent]: dispose
+		} : events,
+		id,
+	};
+
+	if (_DEV_) console.log('os:popup open', id, component, props, events);
+	popups.value.push(state);
+
+	return {
+		dispose,
+	};
+}
+
+export function pageWindow(path: string) {
+	const { component, props } = resolve(path);
+	popup(import('@/components/page-window.vue'), {
+		initialPath: path,
+		initialComponent: markRaw(component),
+		initialProps: props,
+	}, {}, 'closed');
+}
+
+export function modalPageWindow(path: string) {
+	const { component, props } = resolve(path);
+	popup(import('@/components/modal-page-window.vue'), {
+		initialPath: path,
+		initialComponent: markRaw(component),
+		initialProps: props,
+	}, {}, 'closed');
+}
+
+export function dialog(props: {
+	type: 'error' | 'info' | 'success' | 'warning' | 'waiting';
+	title?: string | null;
+	text?: string | null;
+}) {
+	return new Promise((resolve, reject) => {
+		popup(import('@/components/dialog.vue'), props, {
+			done: result => {
+				resolve(result ? result : { canceled: true });
+			},
+		}, 'closed');
+	});
+}
+
+export function success() {
+	return new Promise((resolve, reject) => {
+		const showing = ref(true);
+		setTimeout(() => {
+			showing.value = false;
+		}, 1000);
+		popup(import('@/components/waiting-dialog.vue'), {
+			success: true,
+			showing: showing
+		}, {
+			done: () => resolve(),
+		}, 'closed');
+	});
+}
+
+export function waiting() {
+	return new Promise((resolve, reject) => {
+		const showing = ref(true);
+		popup(import('@/components/waiting-dialog.vue'), {
+			success: false,
+			showing: showing
+		}, {
+			done: () => resolve(),
+		}, 'closed');
+	});
+}
+
+export function form(title, form) {
+	return new Promise((resolve, reject) => {
+		popup(import('@/components/form-dialog.vue'), { title, form }, {
+			done: result => {
+				resolve(result);
+			},
+		}, 'closed');
+	});
+}
+
+export async function selectUser() {
+	return new Promise((resolve, reject) => {
+		popup(import('@/components/user-select-dialog.vue'), {}, {
+			ok: user => {
+				resolve(user);
+			},
+		}, 'closed');
+	});
+}
+
+export async function selectDriveFile(multiple: boolean) {
+	return new Promise((resolve, reject) => {
+		popup(import('@/components/drive-select-dialog.vue'), {
+			type: 'file',
+			multiple
+		}, {
+			done: files => {
+				if (files) {
+					resolve(multiple ? files : files[0]);
+				}
+			},
+		}, 'closed');
+	});
+}
+
+export async function selectDriveFolder(multiple: boolean) {
+	return new Promise((resolve, reject) => {
+		popup(import('@/components/drive-select-dialog.vue'), {
+			type: 'folder',
+			multiple
+		}, {
+			done: folders => {
+				if (folders) {
+					resolve(multiple ? folders : folders[0]);
+				}
+			},
+		}, 'closed');
+	});
+}
+
+export async function pickEmoji(src?: HTMLElement, opts) {
+	return new Promise((resolve, reject) => {
+		popup(import('@/components/emoji-picker-dialog.vue'), {
+			src,
+			...opts
+		}, {
+			done: emoji => {
+				resolve(emoji);
+			},
+		}, 'closed');
+	});
+}
+
+type AwaitType<T> =
+	T extends Promise<infer U> ? U :
+	T extends (...args: any[]) => Promise<infer V> ? V :
+	T;
+let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
+let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
+export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
+	if (openingEmojiPicker) return;
+
+	activeTextarea = initialTextarea;
+
+	const textareas = document.querySelectorAll('textarea, input');
+	for (const textarea of Array.from(textareas)) {
+		textarea.addEventListener('focus', () => {
+			activeTextarea = textarea;
+		});
+	}
+
+	const observer = new MutationObserver(records => {
+		for (const record of records) {
+			for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) {
+				const textareas = node.querySelectorAll('textarea, input') as NodeListOf<NonNullable<typeof activeTextarea>>;
+				for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) {
+					if (document.activeElement === textarea) activeTextarea = textarea;
+					textarea.addEventListener('focus', () => {
+						activeTextarea = textarea;
+					});
+				}
+			}
+		}
+	});
+
+	observer.observe(document.body, {
+		childList: true,
+		subtree: true,
+		attributes: false,
+		characterData: false,
+	});
+
+	openingEmojiPicker = await popup(import('@/components/emoji-picker-window.vue'), {
+		src,
+		...opts
+	}, {
+		chosen: emoji => {
+			insertTextAtCursor(activeTextarea, emoji);
+		},
+		closed: () => {
+			openingEmojiPicker!.dispose();
+			openingEmojiPicker = null;
+			observer.disconnect();
+		}
+	});
+}
+
+export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: {
+	align?: string;
+	width?: number;
+	viaKeyboard?: boolean;
+}) {
+	return new Promise((resolve, reject) => {
+		let dispose;
+		popup(import('@/components/ui/popup-menu.vue'), {
+			items,
+			src,
+			width: options?.width,
+			align: options?.align,
+			viaKeyboard: options?.viaKeyboard
+		}, {
+			closed: () => {
+				resolve();
+				dispose();
+			},
+		}).then(res => {
+			dispose = res.dispose;
+		});
+	});
+}
+
+export function contextMenu(items: any[], ev: MouseEvent) {
+	ev.preventDefault();
+	return new Promise((resolve, reject) => {
+		let dispose;
+		popup(import('@/components/ui/context-menu.vue'), {
+			items,
+			ev,
+		}, {
+			closed: () => {
+				resolve();
+				dispose();
+			},
+		}).then(res => {
+			dispose = res.dispose;
+		});
+	});
+}
+
+export function post(props: Record<string, any>) {
+	return new Promise((resolve, reject) => {
+		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
+		// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
+		//       Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
+		//       複数のpost formを開いたときに場合によってはエラーになる
+		//       もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
+		let dispose;
+		popup(MkPostFormDialog, props, {
+			closed: () => {
+				resolve();
+				dispose();
+			},
+		}).then(res => {
+			dispose = res.dispose;
+		});
+	});
+}
+
+export const deckGlobalEvents = new EventEmitter();
+
+export const uploads = ref([]);
+
+export function upload(file: File, folder?: any, name?: string) {
+	if (folder && typeof folder == 'object') folder = folder.id;
+
+	return new Promise((resolve, reject) => {
+		const id = Math.random();
+
+		const reader = new FileReader();
+		reader.onload = (e) => {
+			const ctx = reactive({
+				id: id,
+				name: name || file.name || 'untitled',
+				progressMax: undefined,
+				progressValue: undefined,
+				img: window.URL.createObjectURL(file)
+			});
+
+			uploads.value.push(ctx);
+
+			const data = new FormData();
+			data.append('i', $i.token);
+			data.append('force', 'true');
+			data.append('file', file);
+
+			if (folder) data.append('folderId', folder);
+			if (name) data.append('name', name);
+
+			const xhr = new XMLHttpRequest();
+			xhr.open('POST', apiUrl + '/drive/files/create', true);
+			xhr.onload = (e: any) => {
+				const driveFile = JSON.parse(e.target.response);
+
+				resolve(driveFile);
+
+				uploads.value = uploads.value.filter(x => x.id != id);
+			};
+
+			xhr.upload.onprogress = e => {
+				if (e.lengthComputable) {
+					ctx.progressMax = e.total;
+					ctx.progressValue = e.loaded;
+				}
+			};
+
+			xhr.send(data);
+		};
+		reader.readAsArrayBuffer(file);
+	});
+}
+
+/*
+export function checkExistence(fileData: ArrayBuffer): Promise<any> {
+	return new Promise((resolve, reject) => {
+		const data = new FormData();
+		data.append('md5', getMD5(fileData));
+
+		os.api('drive/files/find-by-hash', {
+			md5: getMD5(fileData)
+		}).then(resp => {
+			resolve(resp.length > 0 ? resp[0] : null);
+		});
+	});
+}*/
diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue
new file mode 100644
index 0000000000..c549751a27
--- /dev/null
+++ b/packages/client/src/pages/_error_.vue
@@ -0,0 +1,94 @@
+<template>
+<MkLoading v-if="!loaded" />
+<transition :name="$store.state.animation ? 'zoom' : ''" appear>
+	<div class="mjndxjch" v-show="loaded">
+		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
+		<p><b><i class="fas fa-exclamation-triangle"></i> {{ $ts.pageLoadError }}</b></p>
+		<p v-if="version === meta.version">{{ $ts.pageLoadErrorDescription }}</p>
+		<p v-else-if="serverIsDead">{{ $ts.serverIsDead }}</p>
+		<template v-else>
+			<p>{{ $ts.newVersionOfClientAvailable }}</p>
+			<p>{{ $ts.youShouldUpgradeClient }}</p>
+			<MkButton @click="reload" class="button primary">{{ $ts.reload }}</MkButton>
+		</template>
+		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p>
+		<p v-if="error" class="error">ERROR: {{ error }}</p>
+	</div>
+</transition>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import * as symbols from '@/symbols';
+import { version } from '@/config';
+import * as os from '@/os';
+import { unisonReload } from '@/scripts/unison-reload';
+
+export default defineComponent({
+	components: {
+		MkButton,
+	},
+	props: {
+		error: {
+			required: false,
+		}
+	},
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.error,
+				icon: 'fas fa-exclamation-triangle'
+			},
+			loaded: false,
+			serverIsDead: false,
+			meta: {} as any,
+			version,
+		};
+	},
+	created() {
+		os.api('meta', {
+			detail: false
+		}).then(meta => {
+			this.loaded = true;
+			this.serverIsDead = false;
+			this.meta = meta;
+			localStorage.setItem('v', meta.version);
+		}, () => {
+			this.loaded = true;
+			this.serverIsDead = true;
+		});
+	},
+	methods: {
+		reload() {
+			unisonReload();
+		},
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mjndxjch {
+	padding: 32px;
+	text-align: center;
+
+	> p {
+		margin: 0 0 12px 0;
+	}
+
+	> .button {
+		margin: 8px auto;
+	}
+
+	> img {
+		vertical-align: bottom;
+		height: 128px;
+		margin-bottom: 24px;
+		border-radius: 16px;
+	}
+
+	> .error {
+		opacity: 0.7;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/_loading_.vue b/packages/client/src/pages/_loading_.vue
new file mode 100644
index 0000000000..05c6af1cd7
--- /dev/null
+++ b/packages/client/src/pages/_loading_.vue
@@ -0,0 +1,10 @@
+<template>
+<MkLoading/>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({});
+</script>
diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue
new file mode 100644
index 0000000000..c428c1ad83
--- /dev/null
+++ b/packages/client/src/pages/about-misskey.vue
@@ -0,0 +1,238 @@
+<template>
+<div style="overflow: clip;">
+	<FormBase class="znqjceqz">
+		<div id="debug"></div>
+		<section class="_debobigegoItem about">
+			<div class="_debobigegoPanel panel" :class="{ playing: easterEggEngine != null }" ref="about">
+				<img src="/client-assets/about-icon.png" alt="" class="icon" @load="iconLoaded" draggable="false" @click="gravity"/>
+				<div class="misskey">Misskey</div>
+				<div class="version">v{{ version }}</div>
+				<span class="emoji" v-for="emoji in easterEggEmojis" :key="emoji.id" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
+			</div>
+		</section>
+		<section class="_debobigegoItem" style="text-align: center; padding: 0 16px;">
+			{{ $ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ $ts.learnMore }}</a>
+		</section>
+		<FormGroup>
+			<FormLink to="https://github.com/misskey-dev/misskey" external>
+				<template #icon><i class="fas fa-code"></i></template>
+				{{ $ts._aboutMisskey.source }}
+				<template #suffix>GitHub</template>
+			</FormLink>
+			<FormLink to="https://crowdin.com/project/misskey" external>
+				<template #icon><i class="fas fa-language"></i></template>
+				{{ $ts._aboutMisskey.translation }}
+				<template #suffix>Crowdin</template>
+			</FormLink>
+			<FormLink to="https://www.patreon.com/syuilo" external>
+				<template #icon><i class="fas fa-hand-holding-medical"></i></template>
+				{{ $ts._aboutMisskey.donate }}
+				<template #suffix>Patreon</template>
+			</FormLink>
+		</FormGroup>
+		<FormGroup>
+			<template #label>{{ $ts._aboutMisskey.contributors }}</template>
+			<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
+			<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
+			<FormLink to="https://github.com/mei23" external>@mei23</FormLink>
+			<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink>
+			<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink>
+			<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
+			<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
+			<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
+			<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
+			<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template>
+		</FormGroup>
+		<FormGroup>
+			<template #label><Mfm text="[jelly ❤]"/> {{ $ts._aboutMisskey.patrons }}</template>
+			<FormKeyValueView v-for="patron in patrons" :key="patron"><template #key>{{ patron }}</template></FormKeyValueView>
+			<template #caption>{{ $ts._aboutMisskey.morePatrons }}</template>
+		</FormGroup>
+	</FormBase>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { version } from '@/config';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import MkLink from '@/components/link.vue';
+import { physics } from '@/scripts/physics';
+import * as symbols from '@/symbols';
+
+const patrons = [
+	'Satsuki Yanagi',
+	'noellabo',
+	'mametsuko',
+	'AureoleArk',
+	'Gargron',
+	'Nokotaro Takeda',
+	'Suji Yan',
+	'Hekovic',
+	'Gitmo Life Services',
+	'nenohi',
+	'naga_rus',
+	'Melilot',
+	'Efertone',
+	'oi_yekssim',
+	'nanami kan',
+	'motcha',
+	'dansup',
+	'Quinton Macejkovic',
+	'YUKIMOCHI',
+	'mewl hayabusa',
+	'makokunsan',
+	'Peter G.',
+	'Nesakko',
+	'regtan',
+	'見当かなみ',
+	'natalie',
+	'Jerry',
+	'takimura',
+	'sikyosyounin',
+	'YuzuRyo61',
+	'sheeta.s',
+	'osapon',
+	'mkatze',
+	'CG',
+	'nafuchoco',
+	'Takumi Sugita',
+	'chidori ninokura',
+	'mydarkstar',
+	'kiritan',
+	'kabo2468y',
+	'weepjp',
+	'Liaizon Wakest',
+	'Steffen K9',
+	'Roujo',
+	'uroco @99',
+	'totokoro',
+	'public_yusuke',
+	'wara',
+	'S Y',
+	'Denshi',
+	'Osushimaru',
+	'吴浥',
+	'DignifiedSilence',
+	't_w',
+];
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormGroup,
+		FormLink,
+		FormKeyValueView,
+		MkLink,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.aboutMisskey,
+				icon: null
+			},
+			version,
+			patrons,
+			easterEggReady: false,
+			easterEggEmojis: [],
+			easterEggEngine: null,
+		}
+	},
+
+	beforeUnmount() {
+		if (this.easterEggEngine) {
+			this.easterEggEngine.stop();
+		}
+	},
+
+	methods: {
+		iconLoaded() {
+			const emojis = this.$store.state.reactions;
+			const containerWidth = this.$refs.about.offsetWidth;
+			for (let i = 0; i < 32; i++) {
+				this.easterEggEmojis.push({
+					id: i.toString(),
+					top: -(128 + (Math.random() * 256)),
+					left: (Math.random() * containerWidth),
+					emoji: emojis[Math.floor(Math.random() * emojis.length)],
+				});
+			}
+
+			this.$nextTick(() => {
+				this.easterEggReady = true;
+			});
+		},
+
+		gravity() {
+			if (!this.easterEggReady) return;
+			this.easterEggReady = false;
+			this.easterEggEngine = physics(this.$refs.about);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.znqjceqz {
+	max-width: 800px;
+	box-sizing: border-box;
+	margin: 0 auto;
+
+	> .about {
+		> .panel {
+			position: relative;
+			text-align: center;
+			padding: 16px;
+
+			&.playing {
+				&, * {
+					user-select: none;
+				}
+
+				* {
+					will-change: transform;
+				}
+
+				> .emoji {
+					visibility: visible;
+				}
+			}
+
+			> .icon {
+				display: block;
+				width: 100px;
+				margin: 0 auto;
+				border-radius: 16px;
+			}
+
+			> .misskey {
+				margin: 0.75em auto 0 auto;
+				width: max-content;
+			}
+
+			> .version {
+				margin: 0 auto;
+				width: max-content;
+				opacity: 0.5;
+			}
+
+			> .emoji {
+				position: absolute;
+				top: 0;
+				left: 0;
+				visibility: hidden;
+
+				> .emoji {
+					pointer-events: none;
+					font-size: 24px;
+					width: 24px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
new file mode 100644
index 0000000000..dbdf0f6d91
--- /dev/null
+++ b/packages/client/src/pages/about.vue
@@ -0,0 +1,123 @@
+<template>
+<FormBase>
+	<div class="_debobigegoItem">
+		<div class="_debobigegoPanel fwhjspax">
+			<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
+			<span class="name">{{ $instance.name || host }}</span>
+		</div>
+	</div>
+
+	<FormTextarea readonly :value="$instance.description">
+	</FormTextarea>
+
+	<FormGroup>
+		<FormKeyValueView>
+			<template #key>Misskey</template>
+			<template #value>v{{ version }}</template>
+		</FormKeyValueView>
+		<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink>
+	</FormGroup>
+
+	<FormGroup>
+		<FormKeyValueView>
+			<template #key>{{ $ts.administrator }}</template>
+			<template #value>{{ $instance.maintainerName }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.contact }}</template>
+			<template #value>{{ $instance.maintainerEmail }}</template>
+		</FormKeyValueView>
+	</FormGroup>
+
+	<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" external>{{ $ts.tos }}</FormLink>
+
+	<FormSuspense :p="initStats">
+		<FormGroup>
+			<template #label>{{ $ts.statistics }}</template>
+			<FormKeyValueView>
+				<template #key>{{ $ts.users }}</template>
+				<template #value>{{ number(stats.originalUsersCount) }}</template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>{{ $ts.notes }}</template>
+				<template #value>{{ number(stats.originalNotesCount) }}</template>
+			</FormKeyValueView>
+		</FormGroup>
+	</FormSuspense>
+
+	<FormGroup>
+		<template #label>Well-known resources</template>
+		<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
+		<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
+		<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
+		<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
+		<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { version, instanceName } from '@/config';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import * as symbols from '@/symbols';
+import { host } from '@/config';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormGroup,
+		FormLink,
+		FormKeyValueView,
+		FormTextarea,
+		FormSuspense,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.instanceInfo,
+				icon: 'fas fa-info-circle'
+			},
+			host,
+			version,
+			instanceName,
+			stats: null,
+			initStats: () => os.api('stats', {
+			}).then((stats) => {
+				this.stats = stats;
+			})
+		}
+	},
+
+	methods: {
+		number
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fwhjspax {
+	padding: 16px;
+	text-align: center;
+
+	> .icon {
+		display: block;
+		margin: auto;
+		height: 64px;
+		border-radius: 8px;
+	}
+
+	> .name {
+		display: block;
+		margin-top: 12px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
new file mode 100644
index 0000000000..ca94737781
--- /dev/null
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -0,0 +1,170 @@
+<template>
+<div class="lcixvhis">
+	<div class="_section reports">
+		<div class="_content">
+			<div class="inputs" style="display: flex;">
+				<MkSelect v-model="state" style="margin: 0; flex: 1;">
+					<template #label>{{ $ts.state }}</template>
+					<option value="all">{{ $ts.all }}</option>
+					<option value="unresolved">{{ $ts.unresolved }}</option>
+					<option value="resolved">{{ $ts.resolved }}</option>
+				</MkSelect>
+				<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
+					<template #label>{{ $ts.reporteeOrigin }}</template>
+					<option value="combined">{{ $ts.all }}</option>
+					<option value="local">{{ $ts.local }}</option>
+					<option value="remote">{{ $ts.remote }}</option>
+				</MkSelect>
+				<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
+					<template #label>{{ $ts.reporterOrigin }}</template>
+					<option value="combined">{{ $ts.all }}</option>
+					<option value="local">{{ $ts.local }}</option>
+					<option value="remote">{{ $ts.remote }}</option>
+				</MkSelect>
+			</div>
+			<!-- TODO
+			<div class="inputs" style="display: flex; padding-top: 1.2em;">
+				<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()">
+					<span>{{ $ts.username }}</span>
+				</MkInput>
+				<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
+					<span>{{ $ts.host }}</span>
+				</MkInput>
+			</div>
+			-->
+
+			<MkPagination :pagination="pagination" #default="{items}" ref="reports" style="margin-top: var(--margin);">
+				<div class="bcekxzvu _card _gap" v-for="report in items" :key="report.id">
+					<div class="_content target">
+						<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
+						<div class="info">
+							<MkUserName class="name" :user="report.targetUser"/>
+							<div class="acct">@{{ acct(report.targetUser) }}</div>
+						</div>
+					</div>
+					<div class="_content">
+						<div>
+							<Mfm :text="report.comment"/>
+						</div>
+						<hr>
+						<div>Reporter: <MkAcct :user="report.reporter"/></div>
+						<div><MkTime :time="report.createdAt"/></div>
+					</div>
+					<div class="_footer">
+						<div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div>
+						<MkButton @click="resolve(report)" primary v-if="!report.resolved">{{ $ts.abuseMarkAsResolved }}</MkButton>
+					</div>
+				</div>
+			</MkPagination>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import { acct } from '@/filters/user';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkPagination,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.abuseReports,
+				icon: 'fas fa-exclamation-circle',
+				bg: 'var(--bg)',
+			},
+			searchUsername: '',
+			searchHost: '',
+			state: 'unresolved',
+			reporterOrigin: 'combined',
+			targetUserOrigin: 'combined',
+			pagination: {
+				endpoint: 'admin/abuse-user-reports',
+				limit: 10,
+				params: () => ({
+					state: this.state,
+					reporterOrigin: this.reporterOrigin,
+					targetUserOrigin: this.targetUserOrigin,
+				}),
+			},
+		}
+	},
+
+	watch: {
+		state() {
+			this.$refs.reports.reload();
+		},
+
+		reporterOrigin() {
+			this.$refs.reports.reload();
+		},
+
+		targetUserOrigin() {
+			this.$refs.reports.reload();
+		},
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		acct,
+
+		resolve(report) {
+			os.apiWithDialog('admin/resolve-abuse-user-report', {
+				reportId: report.id,
+			}).then(() => {
+				this.$refs.reports.removeItem(item => item.id === report.id);
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.lcixvhis {
+	margin: var(--margin);
+}
+
+.bcekxzvu {
+	> .target {
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		text-align: left;
+		align-items: center;
+					
+		> .avatar {
+			width: 42px;
+			height: 42px;
+		}
+
+		> .info {
+			margin-left: 0.3em;
+			padding: 0 8px;
+			flex: 1;
+
+			> .name {
+				font-weight: bold;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue
new file mode 100644
index 0000000000..df6c9d5d00
--- /dev/null
+++ b/packages/client/src/pages/admin/ads.vue
@@ -0,0 +1,138 @@
+<template>
+<div class="uqshojas">
+	<section class="_card _gap ads" v-for="ad in ads">
+		<div class="_content ad">
+			<MkAd v-if="ad.url" :specify="ad"/>
+			<MkInput v-model="ad.url" type="url">
+				<template #label>URL</template>
+			</MkInput>
+			<MkInput v-model="ad.imageUrl">
+				<template #label>{{ $ts.imageUrl }}</template>
+			</MkInput>
+			<div style="margin: 32px 0;">
+				<MkRadio v-model="ad.place" value="square">square</MkRadio>
+				<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
+				<MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
+			</div>
+			<!--
+			<div style="margin: 32px 0;">
+				{{ $ts.priority }}
+				<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
+				<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
+				<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
+			</div>
+			-->
+			<MkInput v-model="ad.ratio" type="number">
+				<template #label>{{ $ts.ratio }}</template>
+			</MkInput>
+			<MkInput v-model="ad.expiresAt" type="date">
+				<template #label>{{ $ts.expiration }}</template>
+			</MkInput>
+			<MkTextarea v-model="ad.memo">
+				<template #label>{{ $ts.memo }}</template>
+			</MkTextarea>
+			<div class="buttons">
+				<MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+				<MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+			</div>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkRadio from '@/components/form/radio.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkTextarea,
+		MkRadio,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.ads,
+				icon: 'fas fa-audio-description',
+				bg: 'var(--bg)',
+				actions: [{
+					asFullButton: true,
+					icon: 'fas fa-plus',
+					text: this.$ts.add,
+					handler: this.add,
+				}],
+			},
+			ads: [],
+		}
+	},
+
+	created() {
+		os.api('admin/ad/list').then(ads => {
+			this.ads = ads;
+		});
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		add() {
+			this.ads.unshift({
+				id: null,
+				memo: '',
+				place: 'square',
+				priority: 'middle',
+				ratio: 1,
+				url: '',
+				imageUrl: null,
+				expiresAt: null,
+			});
+		},
+
+		remove(ad) {
+			os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: ad.url }),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				this.ads = this.ads.filter(x => x != ad);
+				os.apiWithDialog('admin/ad/delete', {
+					id: ad.id
+				});
+			});
+		},
+
+		save(ad) {
+			if (ad.id == null) {
+				os.apiWithDialog('admin/ad/create', {
+					...ad,
+					expiresAt: new Date(ad.expiresAt).getTime()
+				});
+			} else {
+				os.apiWithDialog('admin/ad/update', {
+					...ad,
+					expiresAt: new Date(ad.expiresAt).getTime()
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.uqshojas {
+	margin: var(--margin);
+}
+</style>
diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue
new file mode 100644
index 0000000000..a64008967f
--- /dev/null
+++ b/packages/client/src/pages/admin/announcements.vue
@@ -0,0 +1,125 @@
+<template>
+<div class="ztgjmzrw">
+	<section class="_card _gap announcements" v-for="announcement in announcements">
+		<div class="_content announcement">
+			<MkInput v-model="announcement.title">
+				<template #label>{{ $ts.title }}</template>
+			</MkInput>
+			<MkTextarea v-model="announcement.text">
+				<template #label>{{ $ts.text }}</template>
+			</MkTextarea>
+			<MkInput v-model="announcement.imageUrl">
+				<template #label>{{ $ts.imageUrl }}</template>
+			</MkInput>
+			<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
+			<div class="buttons">
+				<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+				<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+			</div>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkTextarea,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.announcements,
+				icon: 'fas fa-broadcast-tower',
+				bg: 'var(--bg)',
+				actions: [{
+					asFullButton: true,
+					icon: 'fas fa-plus',
+					text: this.$ts.add,
+					handler: this.add,
+				}],
+			},
+			announcements: [],
+		}
+	},
+
+	created() {
+		os.api('admin/announcements/list').then(announcements => {
+			this.announcements = announcements;
+		});
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		add() {
+			this.announcements.unshift({
+				id: null,
+				title: '',
+				text: '',
+				imageUrl: null
+			});
+		},
+
+		remove(announcement) {
+			os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: announcement.title }),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				this.announcements = this.announcements.filter(x => x != announcement);
+				os.api('admin/announcements/delete', announcement);
+			});
+		},
+
+		save(announcement) {
+			if (announcement.id == null) {
+				os.api('admin/announcements/create', announcement).then(() => {
+					os.dialog({
+						type: 'success',
+						text: this.$ts.saved
+					});
+				}).catch(e => {
+					os.dialog({
+						type: 'error',
+						text: e
+					});
+				});
+			} else {
+				os.api('admin/announcements/update', announcement).then(() => {
+					os.dialog({
+						type: 'success',
+						text: this.$ts.saved
+					});
+				}).catch(e => {
+					os.dialog({
+						type: 'error',
+						text: e
+					});
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ztgjmzrw {
+	margin: var(--margin);
+}
+</style>
diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue
new file mode 100644
index 0000000000..8f7873baa3
--- /dev/null
+++ b/packages/client/src/pages/admin/bot-protection.vue
@@ -0,0 +1,138 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormRadios v-model="provider">
+			<template #desc><i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}</template>
+			<option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
+			<option value="hcaptcha">hCaptcha</option>
+			<option value="recaptcha">reCAPTCHA</option>
+		</FormRadios>
+
+		<template v-if="provider === 'hcaptcha'">
+			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">hCaptcha</div>
+				<div class="main">
+					<FormInput v-model="hcaptchaSiteKey">
+						<template #prefix><i class="fas fa-key"></i></template>
+						<span>{{ $ts.hcaptchaSiteKey }}</span>
+					</FormInput>
+					<FormInput v-model="hcaptchaSecretKey">
+						<template #prefix><i class="fas fa-key"></i></template>
+						<span>{{ $ts.hcaptchaSecretKey }}</span>
+					</FormInput>
+				</div>
+			</div>
+			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">{{ $ts.preview }}</div>
+				<div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
+					<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
+				</div>
+			</div>
+		</template>
+		<template v-else-if="provider === 'recaptcha'">
+			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">reCAPTCHA</div>
+				<div class="main">
+					<FormInput v-model="recaptchaSiteKey">
+						<template #prefix><i class="fas fa-key"></i></template>
+						<span>{{ $ts.recaptchaSiteKey }}</span>
+					</FormInput>
+					<FormInput v-model="recaptchaSecretKey">
+						<template #prefix><i class="fas fa-key"></i></template>
+						<span>{{ $ts.recaptchaSecretKey }}</span>
+					</FormInput>
+				</div>
+			</div>
+			<div v-if="recaptchaSiteKey" class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">{{ $ts.preview }}</div>
+				<div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
+					<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
+				</div>
+			</div>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormRadios from '@/components/debobigego/radios.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormRadios,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormInfo,
+		FormSuspense,
+		MkCaptcha: defineAsyncComponent(() => import('@/components/captcha.vue')),
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.botProtection,
+				icon: 'fas fa-shield-alt'
+			},
+			provider: null,
+			enableHcaptcha: false,
+			hcaptchaSiteKey: null,
+			hcaptchaSecretKey: null,
+			enableRecaptcha: false,
+			recaptchaSiteKey: null,
+			recaptchaSecretKey: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableHcaptcha = meta.enableHcaptcha;
+			this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
+			this.hcaptchaSecretKey = meta.hcaptchaSecretKey;
+			this.enableRecaptcha = meta.enableRecaptcha;
+			this.recaptchaSiteKey = meta.recaptchaSiteKey;
+			this.recaptchaSecretKey = meta.recaptchaSecretKey;
+
+			this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null;
+
+			this.$watch(() => this.provider, () => {
+				this.enableHcaptcha = this.provider === 'hcaptcha';
+				this.enableRecaptcha = this.provider === 'recaptcha';
+			});
+		},
+	
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableHcaptcha: this.enableHcaptcha,
+				hcaptchaSiteKey: this.hcaptchaSiteKey,
+				hcaptchaSecretKey: this.hcaptchaSecretKey,
+				enableRecaptcha: this.enableRecaptcha,
+				recaptchaSiteKey: this.recaptchaSiteKey,
+				recaptchaSecretKey: this.recaptchaSecretKey,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue
new file mode 100644
index 0000000000..b550831e02
--- /dev/null
+++ b/packages/client/src/pages/admin/database.vue
@@ -0,0 +1,61 @@
+<template>
+<FormBase>
+	<FormSuspense :p="databasePromiseFactory" v-slot="{ result: database }">
+		<FormGroup v-for="table in database" :key="table[0]">
+			<template #label>{{ table[0] }}</template>
+			<FormKeyValueView>
+				<template #key>Size</template>
+				<template #value>{{ bytes(table[1].size) }}</template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>Records</template>
+				<template #value>{{ number(table[1].count) }}</template>
+			</FormKeyValueView>
+		</FormGroup>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import bytes from '@/filters/bytes';
+import number from '@/filters/number';
+
+export default defineComponent({
+	components: {
+		FormSuspense,
+		FormKeyValueView,
+		FormBase,
+		FormGroup,
+		FormLink,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.database,
+				icon: 'fas fa-database',
+				bg: 'var(--bg)',
+			},
+			databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		bytes, number,
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue
new file mode 100644
index 0000000000..3733f53a23
--- /dev/null
+++ b/packages/client/src/pages/admin/email-settings.vue
@@ -0,0 +1,128 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
+
+		<template v-if="enableEmail">
+			<FormInput v-model="email" type="email">
+				<span>{{ $ts.emailAddress }}</span>
+			</FormInput>
+
+			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+				<div class="_debobigegoLabel">{{ $ts.smtpConfig }}</div>
+				<div class="main">
+					<FormInput v-model="smtpHost">
+						<span>{{ $ts.smtpHost }}</span>
+					</FormInput>
+					<FormInput v-model="smtpPort" type="number">
+						<span>{{ $ts.smtpPort }}</span>
+					</FormInput>
+					<FormInput v-model="smtpUser">
+						<span>{{ $ts.smtpUser }}</span>
+					</FormInput>
+					<FormInput v-model="smtpPass" type="password">
+						<span>{{ $ts.smtpPass }}</span>
+					</FormInput>
+					<FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
+					<FormSwitch v-model="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
+				</div>
+			</div>
+
+			<FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.emailServer,
+				icon: 'fas fa-envelope',
+				bg: 'var(--bg)',
+			},
+			enableEmail: false,
+			email: null,
+			smtpSecure: false,
+			smtpHost: '',
+			smtpPort: 0,
+			smtpUser: '',
+			smtpPass: '',
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableEmail = meta.enableEmail;
+			this.email = meta.email;
+			this.smtpSecure = meta.smtpSecure;
+			this.smtpHost = meta.smtpHost;
+			this.smtpPort = meta.smtpPort;
+			this.smtpUser = meta.smtpUser;
+			this.smtpPass = meta.smtpPass;
+		},
+
+		async testEmail() {
+			const { canceled, result: destination } = await os.dialog({
+				title: this.$ts.destination,
+				input: {
+					placeholder: this.$instance.maintainerEmail
+				}
+			});
+			if (canceled) return;
+			os.apiWithDialog('admin/send-email', {
+				to: destination,
+				subject: 'Test email',
+				text: 'Yo'
+			});
+		},
+
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableEmail: this.enableEmail,
+				email: this.email,
+				smtpSecure: this.smtpSecure,
+				smtpHost: this.smtpHost,
+				smtpPort: this.smtpPort,
+				smtpUser: this.smtpUser,
+				smtpPass: this.smtpPass,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/emoji-edit-dialog.vue b/packages/client/src/pages/admin/emoji-edit-dialog.vue
new file mode 100644
index 0000000000..e612855105
--- /dev/null
+++ b/packages/client/src/pages/admin/emoji-edit-dialog.vue
@@ -0,0 +1,120 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="370"
+	:with-ok-button="true"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+	@ok="ok()"
+>
+	<template #header>:{{ emoji.name }}:</template>
+
+	<div class="_monolithic_">
+		<div class="yigymqpb _section">
+			<img :src="emoji.url" class="img"/>
+			<MkInput class="_formBlock" v-model="name">
+				<template #label>{{ $ts.name }}</template>
+			</MkInput>
+			<MkInput class="_formBlock" v-model="category" :datalist="categories">
+				<template #label>{{ $ts.category }}</template>
+			</MkInput>
+			<MkInput class="_formBlock" v-model="aliases">
+				<template #label>{{ $ts.tags }}</template>
+				<template #caption>{{ $ts.setMultipleBySeparatingWithSpace }}</template>
+			</MkInput>
+			<MkButton danger @click="del()"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
+		</div>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+import { unique } from '@/scripts/array';
+
+export default defineComponent({
+	components: {
+		XModalWindow,
+		MkButton,
+		MkInput,
+	},
+
+	props: {
+		emoji: {
+			required: true,
+		}
+	},
+
+	emits: ['done', 'closed'],
+
+	data() {
+		return {
+			name: this.emoji.name,
+			category: this.emoji.category,
+			aliases: this.emoji.aliases?.join(' '),
+			categories: [],
+		}
+	},
+
+	created() {
+		os.api('meta', { detail: false }).then(({ emojis }) => {
+			this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
+		});
+	},
+
+	methods: {
+		ok() {
+			this.update();
+		},
+
+		async update() {
+			await os.apiWithDialog('admin/emoji/update', {
+				id: this.emoji.id,
+				name: this.name,
+				category: this.category,
+				aliases: this.aliases.split(' '),
+			});
+
+			this.$emit('done', {
+				updated: {
+					name: this.name,
+					category: this.category,
+					aliases: this.aliases.split(' '),
+				}
+			});
+			this.$refs.dialog.close();
+		},
+
+		async del() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.emoji.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			os.api('admin/emoji/remove', {
+				id: this.emoji.id
+			}).then(() => {
+				this.$emit('done', {
+					deleted: true
+				});
+				this.$refs.dialog.close();
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.yigymqpb {
+	> .img {
+		display: block;
+		height: 64px;
+		margin: 0 auto;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
new file mode 100644
index 0000000000..c9ba193dd1
--- /dev/null
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -0,0 +1,263 @@
+<template>
+<div class="ogwlenmc">
+	<div class="local" v-if="tab === 'local'">
+		<MkInput v-model="query" :debounce="true" type="search" style="margin: var(--margin);">
+			<template #prefix><i class="fas fa-search"></i></template>
+			<template #label>{{ $ts.search }}</template>
+		</MkInput>
+		<MkPagination :pagination="pagination" ref="emojis">
+			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
+			<template #default="{items}">
+				<div class="ldhfsamy">
+					<button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)">
+						<img :src="emoji.url" class="img" :alt="emoji.name"/>
+						<div class="body">
+							<div class="name _monospace">{{ emoji.name }}</div>
+							<div class="info">{{ emoji.category }}</div>
+						</div>
+					</button>
+				</div>
+			</template>
+		</MkPagination>
+	</div>
+
+	<div class="remote" v-else-if="tab === 'remote'">
+		<MkInput v-model="queryRemote" :debounce="true" type="search" style="margin: var(--margin);">
+			<template #prefix><i class="fas fa-search"></i></template>
+			<template #label>{{ $ts.search }}</template>
+		</MkInput>
+		<MkInput v-model="host" :debounce="true" style="margin: var(--margin);">
+			<template #label>{{ $ts.host }}</template>
+		</MkInput>
+		<MkPagination :pagination="remotePagination" ref="remoteEmojis">
+			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
+			<template #default="{items}">
+				<div class="ldhfsamy">
+					<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
+						<img :src="emoji.url" class="img" :alt="emoji.name"/>
+						<div class="body">
+							<div class="name _monospace">{{ emoji.name }}</div>
+							<div class="info">{{ emoji.host }}</div>
+						</div>
+					</div>
+				</div>
+			</template>
+		</MkPagination>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, toRef } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkTab from '@/components/tab.vue';
+import { selectFile } from '@/scripts/select-file';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkTab,
+		MkButton,
+		MkInput,
+		MkPagination,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.$ts.customEmojis,
+				icon: 'fas fa-laugh',
+				bg: 'var(--bg)',
+				actions: [{
+					asFullButton: true,
+					icon: 'fas fa-plus',
+					text: this.$ts.addEmoji,
+					handler: this.add,
+				}],
+				tabs: [{
+					active: this.tab === 'local',
+					title: this.$ts.local,
+					onClick: () => { this.tab = 'local'; },
+				}, {
+					active: this.tab === 'remote',
+					title: this.$ts.remote,
+					onClick: () => { this.tab = 'remote'; },
+				},]
+			})),
+			tab: 'local',
+			query: null,
+			queryRemote: null,
+			host: '',
+			pagination: {
+				endpoint: 'admin/emoji/list',
+				limit: 30,
+				params: computed(() => ({
+					query: (this.query && this.query !== '') ? this.query : null
+				}))
+			},
+			remotePagination: {
+				endpoint: 'admin/emoji/list-remote',
+				limit: 30,
+				params: computed(() => ({
+					query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
+					host: (this.host && this.host !== '') ? this.host : null
+				}))
+			},
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', toRef(this, symbols.PAGE_INFO));
+	},
+
+	methods: {
+		async add(e) {
+			const files = await selectFile(e.currentTarget || e.target, null, true);
+
+			const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
+				fileId: file.id,
+			})));
+			promise.then(() => {
+				this.$refs.emojis.reload();
+			});
+			os.promiseDialog(promise);
+		},
+
+		edit(emoji) {
+			os.popup(import('./emoji-edit-dialog.vue'), {
+				emoji: emoji
+			}, {
+				done: result => {
+					if (result.updated) {
+						this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
+							...emoji,
+							...result.updated
+						});
+					} else if (result.deleted) {
+						this.$refs.emojis.removeItem(item => item.id === emoji.id);
+					}
+				},
+			}, 'closed');
+		},
+
+		im(emoji) {
+			os.apiWithDialog('admin/emoji/copy', {
+				emojiId: emoji.id,
+			});
+		},
+
+		remoteMenu(emoji, ev) {
+			os.popupMenu([{
+				type: 'label',
+				text: ':' + emoji.name + ':',
+			}, {
+				text: this.$ts.import,
+				icon: 'fas fa-plus',
+				action: () => { this.im(emoji) }
+			}], ev.currentTarget || ev.target);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ogwlenmc {
+	> .local {
+	  .empty {
+    	margin: var(--margin);
+		}
+		
+		.ldhfsamy {
+			display: grid;
+			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+			grid-gap: 12px;
+			margin: var(--margin);
+	
+			> .emoji {
+				display: flex;
+				align-items: center;
+				padding: 12px;
+				text-align: left;
+
+				&:hover {
+					color: var(--accent);
+				}
+
+				> .img {
+					width: 42px;
+					height: 42px;
+				}
+
+				> .body {
+					padding: 0 0 0 8px;
+					white-space: nowrap;
+					overflow: hidden;
+
+					> .name {
+						text-overflow: ellipsis;
+						overflow: hidden;
+					}
+
+					> .info {
+						opacity: 0.5;
+						text-overflow: ellipsis;
+						overflow: hidden;
+					}
+				}
+			}
+		}
+	}
+
+	> .remote {
+	  .empty {
+      margin: var(--margin);
+    }
+								
+		.ldhfsamy {
+			display: grid;
+			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+			grid-gap: 12px;
+			margin: var(--margin);
+
+			> .emoji {
+				display: flex;
+				align-items: center;
+				padding: 12px;
+				text-align: left;
+
+				&:hover {
+					color: var(--accent);
+				}
+
+				> .img {
+					width: 32px;
+					height: 32px;
+				}
+
+				> .body {
+					padding: 0 0 0 8px;
+					white-space: nowrap;
+					overflow: hidden;
+
+					> .name {
+						text-overflow: ellipsis;
+						overflow: hidden;
+					}
+
+					> .info {
+						opacity: 0.5;
+						font-size: 90%;
+						text-overflow: ellipsis;
+						overflow: hidden;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/file-dialog.vue b/packages/client/src/pages/admin/file-dialog.vue
new file mode 100644
index 0000000000..016a012ea5
--- /dev/null
+++ b/packages/client/src/pages/admin/file-dialog.vue
@@ -0,0 +1,129 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="370"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+>
+	<template #header v-if="file">{{ file.name }}</template>
+	<div class="cxqhhsmd" v-if="file">
+		<div class="_section">
+			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+			<div class="info">
+				<span style="margin-right: 1em;">{{ file.type }}</span>
+				<span>{{ bytes(file.size) }}</span>
+				<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
+			</div>
+		</div>
+		<div class="_section">
+			<div class="_content">
+				<MkSwitch @update:modelValue="toggleIsSensitive" v-model="isSensitive">NSFW</MkSwitch>
+			</div>
+		</div>
+		<div class="_section">
+			<div class="_content">
+				<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton>
+				<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
+			</div>
+		</div>
+		<div class="_section" v-if="info">
+			<details class="_content rawdata">
+				<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
+			</details>
+		</div>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
+import Progress from '@/scripts/loading';
+import bytes from '@/filters/bytes';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkSwitch,
+		XModalWindow,
+		MkDriveFileThumbnail,
+	},
+
+	props: {
+		fileId: {
+			required: true,
+		}
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			file: null,
+			info: null,
+			isSensitive: false,
+		};
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		async fetch() {
+			Progress.start();
+			this.file = await os.api('drive/files/show', { fileId: this.fileId });
+			this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
+			this.isSensitive = this.file.isSensitive;
+			Progress.done();
+		},
+
+		showUser() {
+			os.pageWindow(`/user-info/${this.file.userId}`);
+		},
+
+		async del() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.file.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			os.apiWithDialog('drive/files/delete', {
+				fileId: this.file.id
+			});
+		},
+
+		async toggleIsSensitive(v) {
+			await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
+			this.isSensitive = v;
+		},
+
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.cxqhhsmd {
+	> ._section {
+		> .thumbnail {
+			height: 150px;
+			max-width: 100%;
+		}
+
+		> .info {
+			text-align: center;
+			margin-top: 8px;
+		}
+		
+		> .rawdata {
+			overflow: auto;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/files-settings.vue b/packages/client/src/pages/admin/files-settings.vue
new file mode 100644
index 0000000000..03d8f3de1f
--- /dev/null
+++ b/packages/client/src/pages/admin/files-settings.vue
@@ -0,0 +1,93 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="cacheRemoteFiles">
+			{{ $ts.cacheRemoteFiles }}
+			<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
+		</FormSwitch>
+
+		<FormSwitch v-model="proxyRemoteFiles">
+			{{ $ts.proxyRemoteFiles }}
+			<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
+		</FormSwitch>
+
+		<FormInput v-model="localDriveCapacityMb" type="number">
+			<span>{{ $ts.driveCapacityPerLocalAccount }}</span>
+			<template #suffix>MB</template>
+			<template #desc>{{ $ts.inMb }}</template>
+		</FormInput>
+
+		<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
+			<span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
+			<template #suffix>MB</template>
+			<template #desc>{{ $ts.inMb }}</template>
+		</FormInput>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.files,
+				icon: 'fas fa-cloud',
+				bg: 'var(--bg)',
+			},
+			cacheRemoteFiles: false,
+			proxyRemoteFiles: false,
+			localDriveCapacityMb: 0,
+			remoteDriveCapacityMb: 0,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.cacheRemoteFiles = meta.cacheRemoteFiles;
+			this.proxyRemoteFiles = meta.proxyRemoteFiles;
+			this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
+			this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				cacheRemoteFiles: this.cacheRemoteFiles,
+				proxyRemoteFiles: this.proxyRemoteFiles,
+				localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
+				remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue
new file mode 100644
index 0000000000..e291d97bbc
--- /dev/null
+++ b/packages/client/src/pages/admin/files.vue
@@ -0,0 +1,209 @@
+<template>
+<div class="xrmjdkdw">
+	<MkContainer :foldable="true" class="lookup">
+		<template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template>
+		<div class="xrmjdkdw-lookup">
+			<MkInput class="item" v-model="q" type="text" @enter="find()">
+				<template #label>{{ $ts.fileIdOrUrl }}</template>
+			</MkInput>
+			<MkButton @click="find()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
+		</div>
+	</MkContainer>
+
+	<div class="_section">
+		<div class="_content">
+			<div class="inputs" style="display: flex;">
+				<MkSelect v-model="origin" style="margin: 0; flex: 1;">
+					<template #label>{{ $ts.instance }}</template>
+					<option value="combined">{{ $ts.all }}</option>
+					<option value="local">{{ $ts.local }}</option>
+					<option value="remote">{{ $ts.remote }}</option>
+				</MkSelect>
+				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
+					<template #label>{{ $ts.host }}</template>
+				</MkInput>
+			</div>
+			<div class="inputs" style="display: flex; padding-top: 1.2em;">
+				<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
+					<template #label>MIME type</template>
+				</MkInput>
+			</div>
+			<MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files">
+				<button class="file _panel _button _gap" v-for="file in items" :key="file.id" @click="show(file, $event)">
+					<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+					<div class="body">
+						<div>
+							<small style="opacity: 0.7;">{{ file.name }}</small>
+						</div>
+						<div>
+							<MkAcct v-if="file.user" :user="file.user"/>
+							<div v-else>{{ $ts.system }}</div>
+						</div>
+						<div>
+							<span style="margin-right: 1em;">{{ file.type }}</span>
+							<span>{{ bytes(file.size) }}</span>
+						</div>
+						<div>
+							<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
+						</div>
+					</div>
+				</button>
+			</MkPagination>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
+import bytes from '@/filters/bytes';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkPagination,
+		MkContainer,
+		MkDriveFileThumbnail,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.files,
+				icon: 'fas fa-cloud',
+				bg: 'var(--bg)',
+				actions: [{
+					text: this.$ts.clearCachedFiles,
+					icon: 'fas fa-trash-alt',
+					handler: this.clear
+				}]
+			},
+			q: null,
+			origin: 'local',
+			type: null,
+			searchHost: '',
+			pagination: {
+				endpoint: 'admin/drive/files',
+				limit: 10,
+				params: () => ({
+					type: (this.type && this.type !== '') ? this.type : null,
+					origin: this.origin,
+					hostname: (this.hostname && this.hostname !== '') ? this.hostname : null,
+				}),
+			},
+		}
+	},
+
+	watch: {
+		type() {
+			this.$refs.files.reload();
+		},
+		origin() {
+			this.$refs.files.reload();
+		},
+		searchHost() {
+			this.$refs.files.reload();
+		},
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		clear() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.clearCachedFilesConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.apiWithDialog('admin/drive/clean-remote-files', {});
+			});
+		},
+
+		show(file, ev) {
+			os.popup(import('./file-dialog.vue'), {
+				fileId: file.id
+			}, {}, 'closed');
+		},
+
+		find() {
+			os.api('admin/drive/show-file', this.q.startsWith('http://') || this.q.startsWith('https://') ? { url: this.q.trim() } : { fileId: this.q.trim() }).then(file => {
+				this.show(file);
+			}).catch(e => {
+				if (e.code === 'NO_SUCH_FILE') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.notFound
+					});
+				}
+			});
+		},
+
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xrmjdkdw {
+	margin: var(--margin);
+
+	> .lookup {
+		margin-bottom: 16px;
+	}
+
+	.urempief {
+		margin-top: var(--margin);
+
+		> .file {
+			display: flex;
+			width: 100%;
+			box-sizing: border-box;
+			text-align: left;
+			align-items: center;
+
+			&:hover {
+				color: var(--accent);
+			}
+
+			> .thumbnail {
+				width: 128px;
+				height: 128px;
+			}
+
+			> .body {
+				margin-left: 0.3em;
+				padding: 8px;
+				flex: 1;
+
+				@media (max-width: 500px) {
+					font-size: 14px;
+				}
+			}
+		}
+	}
+}
+
+.xrmjdkdw-lookup {
+	padding: 16px;
+
+	> .item {
+		margin-bottom: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
new file mode 100644
index 0000000000..d3f9406db7
--- /dev/null
+++ b/packages/client/src/pages/admin/index.vue
@@ -0,0 +1,388 @@
+<template>
+<div class="hiyeyicy" :class="{ wide: !narrow }" ref="el">
+	<div class="nav" v-if="!narrow || page == null">
+		<MkHeader :info="header"></MkHeader>
+	
+		<MkSpacer :content-max="700">
+			<div class="lxpfedzu">
+				<div class="banner">
+					<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
+				</div>
+
+				<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+				<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+
+				<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
+			</div>
+		</MkSpacer>
+	</div>
+	<div class="main">
+		<MkStickyContainer>
+			<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
+			<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
+		</MkStickyContainer>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue';
+import { i18n } from '@/i18n';
+import MkSuperMenu from '@/components/ui/super-menu.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import MkInfo from '@/components/ui/info.vue';
+import { scroll } from '@/scripts/scroll';
+import { instance } from '@/instance';
+import * as symbols from '@/symbols';
+import * as os from '@/os';
+import { lookupUser } from '@/scripts/lookup-user';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		MkSuperMenu,
+		FormGroup,
+		FormButton,
+		MkInfo,
+	},
+
+	provide: {
+		shouldOmitHeaderTitle: false,
+	},
+
+	props: {
+		initialPage: {
+			type: String,
+			required: false
+		}
+	},
+
+	setup(props, context) {
+		const indexInfo = {
+			title: i18n.locale.controlPanel,
+			icon: 'fas fa-cog',
+			bg: 'var(--bg)',
+			hideHeader: true,
+		};
+		const INFO = ref(indexInfo);
+		const childInfo = ref(null);
+		const page = ref(props.initialPage);
+		const narrow = ref(false);
+		const view = ref(null);
+		const el = ref(null);
+		const onInfo = (viewInfo) => {
+			if (isRef(viewInfo)) {
+				watch(viewInfo, () => {
+					childInfo.value = viewInfo.value;
+				}, { immediate: true });
+			} else {
+				childInfo.value = viewInfo;
+			}
+		};
+		const pageProps = ref({});
+
+		const isEmpty = (x: any) => x == null || x == '';
+
+		const noMaintainerInformation = ref(false);
+		const noBotProtection = ref(false);
+
+		os.api('meta', { detail: true }).then(meta => {
+			// TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する
+			noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
+			noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
+		});
+
+		const menuDef = computed(() => [{
+			title: i18n.locale.quickAction,
+			items: [{
+				type: 'button',
+				icon: 'fas fa-search',
+				text: i18n.locale.lookup,
+				action: lookup,
+			}, ...(instance.disableRegistration ? [{
+				type: 'button',
+				icon: 'fas fa-user',
+				text: i18n.locale.invite,
+				action: invite,
+			}] : [])],
+		}, {
+			title: i18n.locale.administration,
+			items: [{
+				icon: 'fas fa-tachometer-alt',
+				text: i18n.locale.dashboard,
+				to: '/admin/overview',
+				active: page.value === 'overview',
+			}, {
+				icon: 'fas fa-users',
+				text: i18n.locale.users,
+				to: '/admin/users',
+				active: page.value === 'users',
+			}, {
+				icon: 'fas fa-laugh',
+				text: i18n.locale.customEmojis,
+				to: '/admin/emojis',
+				active: page.value === 'emojis',
+			}, {
+				icon: 'fas fa-globe',
+				text: i18n.locale.federation,
+				to: '/admin/federation',
+				active: page.value === 'federation',
+			}, {
+				icon: 'fas fa-clipboard-list',
+				text: i18n.locale.jobQueue,
+				to: '/admin/queue',
+				active: page.value === 'queue',
+			}, {
+				icon: 'fas fa-cloud',
+				text: i18n.locale.files,
+				to: '/admin/files',
+				active: page.value === 'files',
+			}, {
+				icon: 'fas fa-broadcast-tower',
+				text: i18n.locale.announcements,
+				to: '/admin/announcements',
+				active: page.value === 'announcements',
+			}, {
+				icon: 'fas fa-audio-description',
+				text: i18n.locale.ads,
+				to: '/admin/ads',
+				active: page.value === 'ads',
+			}, {
+				icon: 'fas fa-exclamation-circle',
+				text: i18n.locale.abuseReports,
+				to: '/admin/abuses',
+				active: page.value === 'abuses',
+			}],
+		}, {
+			title: i18n.locale.settings,
+			items: [{
+				icon: 'fas fa-cog',
+				text: i18n.locale.general,
+				to: '/admin/settings',
+				active: page.value === 'settings',
+			}, {
+				icon: 'fas fa-cloud',
+				text: i18n.locale.files,
+				to: '/admin/files-settings',
+				active: page.value === 'files-settings',
+			}, {
+				icon: 'fas fa-envelope',
+				text: i18n.locale.emailServer,
+				to: '/admin/email-settings',
+				active: page.value === 'email-settings',
+			}, {
+				icon: 'fas fa-cloud',
+				text: i18n.locale.objectStorage,
+				to: '/admin/object-storage',
+				active: page.value === 'object-storage',
+			}, {
+				icon: 'fas fa-lock',
+				text: i18n.locale.security,
+				to: '/admin/security',
+				active: page.value === 'security',
+			}, {
+				icon: 'fas fa-bolt',
+				text: 'ServiceWorker',
+				to: '/admin/service-worker',
+				active: page.value === 'service-worker',
+			}, {
+				icon: 'fas fa-globe',
+				text: i18n.locale.relays,
+				to: '/admin/relays',
+				active: page.value === 'relays',
+			}, {
+				icon: 'fas fa-share-alt',
+				text: i18n.locale.integration,
+				to: '/admin/integrations',
+				active: page.value === 'integrations',
+			}, {
+				icon: 'fas fa-ban',
+				text: i18n.locale.instanceBlocking,
+				to: '/admin/instance-block',
+				active: page.value === 'instance-block',
+			}, {
+				icon: 'fas fa-ghost',
+				text: i18n.locale.proxyAccount,
+				to: '/admin/proxy-account',
+				active: page.value === 'proxy-account',
+			}, {
+				icon: 'fas fa-cogs',
+				text: i18n.locale.other,
+				to: '/admin/other-settings',
+				active: page.value === 'other-settings',
+			}],
+		}, {
+			title: i18n.locale.info,
+			items: [{
+				icon: 'fas fa-database',
+				text: i18n.locale.database,
+				to: '/admin/database',
+				active: page.value === 'database',
+			}],
+		}]);
+		const component = computed(() => {
+			if (page.value == null) return null;
+			switch (page.value) {
+				case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
+				case 'users': return defineAsyncComponent(() => import('./users.vue'));
+				case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
+				case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
+				case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
+				case 'files': return defineAsyncComponent(() => import('./files.vue'));
+				case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
+				case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
+				case 'database': return defineAsyncComponent(() => import('./database.vue'));
+				case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
+				case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
+				case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
+				case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
+				case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
+				case 'security': return defineAsyncComponent(() => import('./security.vue'));
+				case 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.vue'));
+				case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue'));
+				case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
+				case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
+				case 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue'));
+				case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue'));
+				case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue'));
+				case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
+				case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
+				case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
+			}
+		});
+
+		watch(component, () => {
+			pageProps.value = {};
+
+			nextTick(() => {
+				scroll(el.value, { top: 0 });
+			});
+		}, { immediate: true });
+
+		watch(() => props.initialPage, () => {
+			if (props.initialPage == null && !narrow.value) {
+				page.value = 'overview';
+			} else {
+				page.value = props.initialPage;
+				if (props.initialPage == null) {
+					INFO.value = indexInfo;
+				}
+			}
+		});
+
+		onMounted(() => {
+			narrow.value = el.value.offsetWidth < 800;
+			if (!narrow.value) {
+				page.value = 'overview';
+			}
+		});
+
+		const invite = () => {
+			os.api('admin/invite').then(x => {
+				os.dialog({
+					type: 'info',
+					text: x.code
+				});
+			}).catch(e => {
+				os.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		};
+
+		const lookup = (ev) => {
+			os.popupMenu([{
+				text: i18n.locale.user,
+				icon: 'fas fa-user',
+				action: () => {
+					lookupUser();
+				}
+			}, {
+				text: i18n.locale.note,
+				icon: 'fas fa-pencil-alt',
+				action: () => {
+					alert('TODO');
+				}
+			}, {
+				text: i18n.locale.file,
+				icon: 'fas fa-cloud',
+				action: () => {
+					alert('TODO');
+				}
+			}, {
+				text: i18n.locale.instance,
+				icon: 'fas fa-globe',
+				action: () => {
+					alert('TODO');
+				}
+			}], ev.currentTarget || ev.target);
+		};
+
+		return {
+			[symbols.PAGE_INFO]: INFO,
+			menuDef,
+			header: {
+				title: i18n.locale.controlPanel,
+			},
+			noMaintainerInformation,
+			noBotProtection,
+			page,
+			narrow,
+			view,
+			el,
+			onInfo,
+			childInfo,
+			pageProps,
+			component,
+			invite,
+			lookup,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.hiyeyicy {
+	&.wide {
+		display: flex;
+		margin: 0 auto;
+		height: 100%;
+
+		> .nav {
+			width: 32%;
+			max-width: 280px;
+			box-sizing: border-box;
+			border-right: solid 0.5px var(--divider);
+			overflow: auto;
+			height: 100%;
+		}
+
+		> .main {
+			flex: 1;
+			min-width: 0;
+		}
+	}
+
+	> .nav {
+		.lxpfedzu {
+			> .info {
+				margin: 16px 0;
+			}
+
+			> .banner {
+				margin: 16px;
+
+				> .icon {
+					display: block;
+					margin: auto;
+					height: 42px;
+					border-radius: 8px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
new file mode 100644
index 0000000000..f5b249698d
--- /dev/null
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -0,0 +1,72 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormTextarea v-model="blockedHosts">
+			<span>{{ $ts.blockedInstances }}</span>
+			<template #desc>{{ $ts.blockedInstancesDescription }}</template>
+		</FormTextarea>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormTextarea,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.instanceBlocking,
+				icon: 'fas fa-ban',
+				bg: 'var(--bg)',
+			},
+			blockedHosts: '',
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.blockedHosts = meta.blockedHosts.join('\n');
+		},
+
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				blockedHosts: this.blockedHosts.split('\n') || [],
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/instance.vue b/packages/client/src/pages/admin/instance.vue
new file mode 100644
index 0000000000..614eaa3048
--- /dev/null
+++ b/packages/client/src/pages/admin/instance.vue
@@ -0,0 +1,321 @@
+<template>
+<XModalWindow ref="dialog"
+	:width="520"
+	:height="500"
+	@close="$refs.dialog.close()"
+	@closed="$emit('closed')"
+>
+	<template #header>{{ instance.host }}</template>
+	<div class="mk-instance-info">
+		<div class="_table section">
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.software }}</div>
+					<div class="_data">{{ instance.softwareName || '?' }}</div>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.version }}</div>
+					<div class="_data">{{ instance.softwareVersion || '?' }}</div>
+				</div>
+			</div>
+		</div>
+		<div class="_table data section">
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.registeredAt }}</div>
+					<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.following }}</div>
+					<button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.followers }}</div>
+					<button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.users }}</div>
+					<button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.notes }}</div>
+					<div class="_data">{{ number(instance.notesCount) }}</div>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.files }}</div>
+					<div class="_data">{{ number(instance.driveFiles) }}</div>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.storageUsage }}</div>
+					<div class="_data">{{ bytes(instance.driveUsage) }}</div>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.latestRequestSentAt }}</div>
+					<div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
+				</div>
+				<div class="_cell">
+					<div class="_label">{{ $ts.latestStatus }}</div>
+					<div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div>
+				</div>
+			</div>
+			<div class="_row">
+				<div class="_cell">
+					<div class="_label">{{ $ts.latestRequestReceivedAt }}</div>
+					<div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
+				</div>
+			</div>
+		</div>
+		<div class="chart">
+			<div class="header">
+				<span class="label">{{ $ts.charts }}</span>
+				<div class="selects">
+					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
+						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
+						<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
+						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
+						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
+						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
+						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
+						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
+						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
+						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
+						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
+						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
+					</MkSelect>
+					<MkSelect v-model="chartSpan" style="margin: 0;">
+						<option value="hour">{{ $ts.perHour }}</option>
+						<option value="day">{{ $ts.perDay }}</option>
+					</MkSelect>
+				</div>
+			</div>
+			<div class="chart">
+				<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
+			</div>
+		</div>
+		<div class="operations section">
+			<span class="label">{{ $ts.operations }}</span>
+			<MkSwitch v-model="isSuspended" class="switch">{{ $ts.stopActivityDelivery }}</MkSwitch>
+			<MkSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</MkSwitch>
+			<details>
+				<summary>{{ $ts.deleteAllFiles }}</summary>
+				<MkButton @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton>
+			</details>
+			<details>
+				<summary>{{ $ts.removeAllFollowing }}</summary>
+				<MkButton @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-minus-circle"></i> {{ $ts.removeAllFollowing }}</MkButton>
+				<MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo>
+			</details>
+		</div>
+		<details class="metadata section">
+			<summary class="label">{{ $ts.metadata }}</summary>
+			<pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre>
+		</details>
+	</div>
+</XModalWindow>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import XModalWindow from '@/components/ui/modal-window.vue';
+import MkUsersDialog from '@/components/users-dialog.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import MkInfo from '@/components/ui/info.vue';
+import MkChart from '@/components/chart.vue';
+import bytes from '@/filters/bytes';
+import number from '@/filters/number';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XModalWindow,
+		MkSelect,
+		MkButton,
+		MkSwitch,
+		MkInfo,
+		MkChart,
+	},
+
+	props: {
+		instance: {
+			type: Object,
+			required: true
+		}
+	},
+
+	emits: ['closed'],
+
+	data() {
+		return {
+			isSuspended: this.instance.isSuspended,
+			chartSrc: 'requests',
+			chartSpan: 'hour',
+		};
+	},
+
+	computed: {
+		meta() {
+			return this.$instance;
+		},
+
+		isBlocked() {
+			return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host);
+		}
+	},
+
+	watch: {
+		isSuspended() {
+			os.api('admin/federation/update-instance', {
+				host: this.instance.host,
+				isSuspended: this.isSuspended
+			});
+		},
+	},
+
+	methods: {
+		changeBlock(e) {
+			os.api('admin/update-meta', {
+				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
+			});
+		},
+
+		removeAllFollowing() {
+			os.apiWithDialog('admin/federation/remove-all-following', {
+				host: this.instance.host
+			});
+		},
+
+		deleteAllFiles() {
+			os.apiWithDialog('admin/federation/delete-all-files', {
+				host: this.instance.host
+			});
+		},
+
+		showFollowing() {
+			os.modal(MkUsersDialog, {
+				title: this.$ts.instanceFollowing,
+				pagination: {
+					endpoint: 'federation/following',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				},
+				extract: item => item.follower
+			});
+		},
+
+		showFollowers() {
+			os.modal(MkUsersDialog, {
+				title: this.$ts.instanceFollowers,
+				pagination: {
+					endpoint: 'federation/followers',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				},
+				extract: item => item.followee
+			});
+		},
+
+		showUsers() {
+			os.modal(MkUsersDialog, {
+				title: this.$ts.instanceUsers,
+				pagination: {
+					endpoint: 'federation/users',
+					limit: 10,
+					params: {
+						host: this.instance.host
+					}
+				}
+			});
+		},
+
+		bytes,
+
+		number
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-instance-info {
+	overflow: auto;
+
+	> .section {
+		padding: 16px 32px;
+
+		@media (max-width: 500px) {
+			padding: 8px 16px;
+		}
+
+		&:not(:first-child) {
+			border-top: solid 0.5px var(--divider);
+		}
+	}
+
+	> .chart {
+		border-top: solid 0.5px var(--divider);
+		padding: 16px 0 12px 0;
+
+		> .header {
+			padding: 0 32px;
+
+			@media (max-width: 500px) {
+				padding: 0 16px;
+			}
+
+			> .label {
+				font-size: 80%;
+				opacity: 0.7;
+			}
+
+			> .selects {
+				display: flex;
+			}
+		}
+
+		> .chart {
+			padding: 0 16px;
+
+			@media (max-width: 500px) {
+				padding: 0;
+			}
+		}
+	}
+
+	> .operations {
+		> .label {
+			font-size: 80%;
+			opacity: 0.7;
+		}
+
+		> .switch {
+			margin: 16px 0;
+		}
+	}
+
+	> .metadata {
+		> .label {
+			font-size: 80%;
+			opacity: 0.7;
+		}
+
+		> pre > code {
+			display: block;
+			max-height: 200px;
+			overflow: auto;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/integrations-discord.vue b/packages/client/src/pages/admin/integrations-discord.vue
new file mode 100644
index 0000000000..81e47499c6
--- /dev/null
+++ b/packages/client/src/pages/admin/integrations-discord.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableDiscordIntegration">
+			{{ $ts.enable }}
+		</FormSwitch>
+
+		<template v-if="enableDiscordIntegration">
+			<FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
+		
+			<FormInput v-model="discordClientId">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Client ID
+			</FormInput>
+
+			<FormInput v-model="discordClientSecret">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Client Secret
+			</FormInput>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormInfo,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'Discord',
+				icon: 'fab fa-discord'
+			},
+			enableDiscordIntegration: false,
+			discordClientId: null,
+			discordClientSecret: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableDiscordIntegration = meta.enableDiscordIntegration;
+			this.discordClientId = meta.discordClientId;
+			this.discordClientSecret = meta.discordClientSecret;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableDiscordIntegration: this.enableDiscordIntegration,
+				discordClientId: this.discordClientId,
+				discordClientSecret: this.discordClientSecret,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/integrations-github.vue b/packages/client/src/pages/admin/integrations-github.vue
new file mode 100644
index 0000000000..2bbc3ae9a1
--- /dev/null
+++ b/packages/client/src/pages/admin/integrations-github.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableGithubIntegration">
+			{{ $ts.enable }}
+		</FormSwitch>
+
+		<template v-if="enableGithubIntegration">
+			<FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
+		
+			<FormInput v-model="githubClientId">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Client ID
+			</FormInput>
+
+			<FormInput v-model="githubClientSecret">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Client Secret
+			</FormInput>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormInfo,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'GitHub',
+				icon: 'fab fa-github'
+			},
+			enableGithubIntegration: false,
+			githubClientId: null,
+			githubClientSecret: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableGithubIntegration = meta.enableGithubIntegration;
+			this.githubClientId = meta.githubClientId;
+			this.githubClientSecret = meta.githubClientSecret;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableGithubIntegration: this.enableGithubIntegration,
+				githubClientId: this.githubClientId,
+				githubClientSecret: this.githubClientSecret,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/integrations-twitter.vue b/packages/client/src/pages/admin/integrations-twitter.vue
new file mode 100644
index 0000000000..19ed216ab9
--- /dev/null
+++ b/packages/client/src/pages/admin/integrations-twitter.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableTwitterIntegration">
+			{{ $ts.enable }}
+		</FormSwitch>
+
+		<template v-if="enableTwitterIntegration">
+			<FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
+		
+			<FormInput v-model="twitterConsumerKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Consumer Key
+			</FormInput>
+
+			<FormInput v-model="twitterConsumerSecret">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Consumer Secret
+			</FormInput>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormInfo,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'Twitter',
+				icon: 'fab fa-twitter'
+			},
+			enableTwitterIntegration: false,
+			twitterConsumerKey: null,
+			twitterConsumerSecret: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableTwitterIntegration = meta.enableTwitterIntegration;
+			this.twitterConsumerKey = meta.twitterConsumerKey;
+			this.twitterConsumerSecret = meta.twitterConsumerSecret;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableTwitterIntegration: this.enableTwitterIntegration,
+				twitterConsumerKey: this.twitterConsumerKey,
+				twitterConsumerSecret: this.twitterConsumerSecret,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue
new file mode 100644
index 0000000000..c21eebc1c6
--- /dev/null
+++ b/packages/client/src/pages/admin/integrations.vue
@@ -0,0 +1,74 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormLink to="/admin/integrations/twitter">
+			<i class="fab fa-twitter"></i> Twitter
+			<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
+		</FormLink>
+		<FormLink to="/admin/integrations/github">
+			<i class="fab fa-github"></i> GitHub
+			<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
+		</FormLink>
+		<FormLink to="/admin/integrations/discord">
+			<i class="fab fa-discord"></i> Discord
+			<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
+		</FormLink>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormLink,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormTextarea,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.integration,
+				icon: 'fas fa-share-alt',
+				bg: 'var(--bg)',
+			},
+			enableTwitterIntegration: false,
+			enableGithubIntegration: false,
+			enableDiscordIntegration: false,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableTwitterIntegration = meta.enableTwitterIntegration;
+			this.enableGithubIntegration = meta.enableGithubIntegration;
+			this.enableDiscordIntegration = meta.enableDiscordIntegration;
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/metrics.vue b/packages/client/src/pages/admin/metrics.vue
new file mode 100644
index 0000000000..05b64b235c
--- /dev/null
+++ b/packages/client/src/pages/admin/metrics.vue
@@ -0,0 +1,472 @@
+<template>
+<div class="_debobigegoItem">
+	<div class="_debobigegoLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
+	<div class="_debobigegoPanel xhexznfu">
+		<div>
+			<canvas :ref="cpumem"></canvas>
+		</div>
+		<div v-if="serverInfo">
+			<div class="_table">
+				<div class="_row">
+					<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
+					<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
+					<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<div class="_debobigegoItem">
+	<div class="_debobigegoLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
+	<div class="_debobigegoPanel xhexznfu">
+		<div>
+			<canvas :ref="disk"></canvas>
+		</div>
+		<div v-if="serverInfo">
+			<div class="_table">
+				<div class="_row">
+					<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
+					<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
+					<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<div class="_debobigegoItem">
+	<div class="_debobigegoLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
+	<div class="_debobigegoPanel xhexznfu">
+		<div>
+			<canvas :ref="net"></canvas>
+		</div>
+		<div v-if="serverInfo">
+			<div class="_table">
+				<div class="_row">
+					<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import {
+  Chart,
+  ArcElement,
+  LineElement,
+  BarElement,
+  PointElement,
+  BarController,
+  LineController,
+  CategoryScale,
+  LinearScale,
+  Legend,
+  Title,
+  Tooltip,
+  SubTitle
+} from 'chart.js';
+import MkButton from '@/components/ui/button.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkInput from '@/components/form/input.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkwFederation from '../../widgets/federation.vue';
+import { version, url } from '@/config';
+import bytes from '@/filters/bytes';
+import number from '@/filters/number';
+import MkInstanceInfo from './instance.vue';
+
+Chart.register(
+  ArcElement,
+  LineElement,
+  BarElement,
+  PointElement,
+  BarController,
+  LineController,
+  CategoryScale,
+  LinearScale,
+  Legend,
+  Title,
+  Tooltip,
+  SubTitle
+);
+
+const alpha = (hex, a) => {
+	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+	const r = parseInt(result[1], 16);
+	const g = parseInt(result[2], 16);
+	const b = parseInt(result[3], 16);
+	return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkSelect,
+		MkInput,
+		MkContainer,
+		MkFolder,
+		MkwFederation,
+	},
+
+	data() {
+		return {
+			version,
+			url,
+			stats: null,
+			serverInfo: null,
+			connection: null,
+			queueConnection: markRaw(os.stream.useChannel('queueStats')),
+			memUsage: 0,
+			chartCpuMem: null,
+			chartNet: null,
+			jobs: [],
+			logs: [],
+			logLevel: 'all',
+			logDomain: '',
+			modLogs: [],
+			dbInfo: null,
+			overviewHeight: '1fr',
+			queueHeight: '1fr',
+			paused: false,
+		}
+	},
+
+	computed: {
+		gridColor() {
+			// TODO: var(--panel)の色が暗いか明るいかで判定する
+			return this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+		},
+	},
+
+	mounted() {
+		this.fetchJobs();
+
+		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+		os.api('admin/server-info', {}).then(res => {
+			this.serverInfo = res;
+
+			this.connection = markRaw(os.stream.useChannel('serverStats'));
+			this.connection.on('stats', this.onStats);
+			this.connection.on('statsLog', this.onStatsLog);
+			this.connection.send('requestLog', {
+				id: Math.random().toString().substr(2, 8),
+				length: 150
+			});
+
+			this.$nextTick(() => {
+				this.queueConnection.send('requestLog', {
+					id: Math.random().toString().substr(2, 8),
+					length: 200
+				});
+			});
+		});
+	},
+
+	beforeUnmount() {
+		if (this.connection) {
+			this.connection.off('stats', this.onStats);
+			this.connection.off('statsLog', this.onStatsLog);
+			this.connection.dispose();
+		}
+		this.queueConnection.dispose();
+	},
+
+	methods: {
+		cpumem(el) {
+			if (this.chartCpuMem != null) return;
+			this.chartCpuMem = markRaw(new Chart(el, {
+				type: 'line',
+				data: {
+					labels: [],
+					datasets: [{
+						label: 'CPU',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#86b300',
+						backgroundColor: alpha('#86b300', 0.1),
+						data: []
+					}, {
+						label: 'MEM (active)',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#935dbf',
+						backgroundColor: alpha('#935dbf', 0.02),
+						data: []
+					}, {
+						label: 'MEM (used)',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#935dbf',
+						borderDash: [5, 5],
+						fill: false,
+						data: []
+					}]
+				},
+				options: {
+					aspectRatio: 3,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 0
+						}
+					},
+					legend: {
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
+						}
+					},
+					scales: {
+						x: {
+							gridLines: {
+								display: false,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false,
+							}
+						},
+						y: {
+							position: 'right',
+							gridLines: {
+								display: true,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false,
+								max: 100
+							}
+						}
+					},
+					tooltips: {
+						intersect: false,
+						mode: 'index',
+					}
+				}
+			}));
+		},
+
+		net(el) {
+			if (this.chartNet != null) return;
+			this.chartNet = markRaw(new Chart(el, {
+				type: 'line',
+				data: {
+					labels: [],
+					datasets: [{
+						label: 'In',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#94a029',
+						backgroundColor: alpha('#94a029', 0.1),
+						data: []
+					}, {
+						label: 'Out',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#ff9156',
+						backgroundColor: alpha('#ff9156', 0.1),
+						data: []
+					}]
+				},
+				options: {
+					aspectRatio: 3,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 0
+						}
+					},
+					legend: {
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
+						}
+					},
+					scales: {
+						x: {
+							gridLines: {
+								display: false,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false
+							}
+						},
+						y: {
+							position: 'right',
+							gridLines: {
+								display: true,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false,
+							}
+						}
+					},
+					tooltips: {
+						intersect: false,
+						mode: 'index',
+					}
+				}
+			}));
+		},
+
+		disk(el) {
+			if (this.chartDisk != null) return;
+			this.chartDisk = markRaw(new Chart(el, {
+				type: 'line',
+				data: {
+					labels: [],
+					datasets: [{
+						label: 'Read',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#94a029',
+						backgroundColor: alpha('#94a029', 0.1),
+						data: []
+					}, {
+						label: 'Write',
+						pointRadius: 0,
+						tension: 0,
+						borderWidth: 2,
+						borderColor: '#ff9156',
+						backgroundColor: alpha('#ff9156', 0.1),
+						data: []
+					}]
+				},
+				options: {
+					aspectRatio: 3,
+					layout: {
+						padding: {
+							left: 16,
+							right: 16,
+							top: 16,
+							bottom: 0
+						}
+					},
+					legend: {
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
+						}
+					},
+					scales: {
+						x: {
+							gridLines: {
+								display: false,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false
+							}
+						},
+						y: {
+							position: 'right',
+							gridLines: {
+								display: true,
+								color: this.gridColor,
+								zeroLineColor: this.gridColor,
+							},
+							ticks: {
+								display: false,
+							}
+						}
+					},
+					tooltips: {
+						intersect: false,
+						mode: 'index',
+					}
+				}
+			}));
+		},
+
+		fetchJobs() {
+			os.api('admin/queue/deliver-delayed', {}).then(jobs => {
+				this.jobs = jobs;
+			});
+		},
+
+		onStats(stats) {
+			if (this.paused) return;
+
+			const cpu = (stats.cpu * 100).toFixed(0);
+			const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
+			const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
+			this.memUsage = stats.mem.active;
+
+			this.chartCpuMem.data.labels.push('');
+			this.chartCpuMem.data.datasets[0].data.push(cpu);
+			this.chartCpuMem.data.datasets[1].data.push(memActive);
+			this.chartCpuMem.data.datasets[2].data.push(memUsed);
+			this.chartNet.data.labels.push('');
+			this.chartNet.data.datasets[0].data.push(stats.net.rx);
+			this.chartNet.data.datasets[1].data.push(stats.net.tx);
+			this.chartDisk.data.labels.push('');
+			this.chartDisk.data.datasets[0].data.push(stats.fs.r);
+			this.chartDisk.data.datasets[1].data.push(stats.fs.w);
+			if (this.chartCpuMem.data.datasets[0].data.length > 150) {
+				this.chartCpuMem.data.labels.shift();
+				this.chartCpuMem.data.datasets[0].data.shift();
+				this.chartCpuMem.data.datasets[1].data.shift();
+				this.chartCpuMem.data.datasets[2].data.shift();
+				this.chartNet.data.labels.shift();
+				this.chartNet.data.datasets[0].data.shift();
+				this.chartNet.data.datasets[1].data.shift();
+				this.chartDisk.data.labels.shift();
+				this.chartDisk.data.datasets[0].data.shift();
+				this.chartDisk.data.datasets[1].data.shift();
+			}
+			this.chartCpuMem.update();
+			this.chartNet.update();
+			this.chartDisk.update();
+		},
+
+		onStatsLog(statsLog) {
+			for (const stats of [...statsLog].reverse()) {
+				this.onStats(stats);
+			}
+		},
+
+		bytes,
+
+		number,
+
+		pause() {
+			this.paused = true;
+		},
+
+		resume() {
+			this.paused = false;
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xhexznfu {
+	> div:nth-child(2) {
+		padding: 16px;
+		border-top: solid 0.5px var(--divider);
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue
new file mode 100644
index 0000000000..0f1431c258
--- /dev/null
+++ b/packages/client/src/pages/admin/object-storage.vue
@@ -0,0 +1,155 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
+
+		<template v-if="useObjectStorage">
+			<FormInput v-model="objectStorageBaseUrl">
+				<span>{{ $ts.objectStorageBaseUrl }}</span>
+				<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStorageBucket">
+				<span>{{ $ts.objectStorageBucket }}</span>
+				<template #desc>{{ $ts.objectStorageBucketDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStoragePrefix">
+				<span>{{ $ts.objectStoragePrefix }}</span>
+				<template #desc>{{ $ts.objectStoragePrefixDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStorageEndpoint">
+				<span>{{ $ts.objectStorageEndpoint }}</span>
+				<template #desc>{{ $ts.objectStorageEndpointDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStorageRegion">
+				<span>{{ $ts.objectStorageRegion }}</span>
+				<template #desc>{{ $ts.objectStorageRegionDesc }}</template>
+			</FormInput>
+
+			<FormInput v-model="objectStorageAccessKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				<span>Access key</span>
+			</FormInput>
+
+			<FormInput v-model="objectStorageSecretKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				<span>Secret key</span>
+			</FormInput>
+
+			<FormSwitch v-model="objectStorageUseSSL">
+				{{ $ts.objectStorageUseSSL }}
+				<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template>
+			</FormSwitch>
+
+			<FormSwitch v-model="objectStorageUseProxy">
+				{{ $ts.objectStorageUseProxy }}
+				<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template>
+			</FormSwitch>
+
+			<FormSwitch v-model="objectStorageSetPublicRead">
+				{{ $ts.objectStorageSetPublicRead }}
+			</FormSwitch>
+
+			<FormSwitch v-model="objectStorageS3ForcePathStyle">
+				s3ForcePathStyle
+			</FormSwitch>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.objectStorage,
+				icon: 'fas fa-cloud',
+				bg: 'var(--bg)',
+			},
+			useObjectStorage: false,
+			objectStorageBaseUrl: null,
+			objectStorageBucket: null,
+			objectStoragePrefix: null,
+			objectStorageEndpoint: null,
+			objectStorageRegion: null,
+			objectStoragePort: null,
+			objectStorageAccessKey: null,
+			objectStorageSecretKey: null,
+			objectStorageUseSSL: false,
+			objectStorageUseProxy: false,
+			objectStorageSetPublicRead: false,
+			objectStorageS3ForcePathStyle: true,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.useObjectStorage = meta.useObjectStorage;
+			this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
+			this.objectStorageBucket = meta.objectStorageBucket;
+			this.objectStoragePrefix = meta.objectStoragePrefix;
+			this.objectStorageEndpoint = meta.objectStorageEndpoint;
+			this.objectStorageRegion = meta.objectStorageRegion;
+			this.objectStoragePort = meta.objectStoragePort;
+			this.objectStorageAccessKey = meta.objectStorageAccessKey;
+			this.objectStorageSecretKey = meta.objectStorageSecretKey;
+			this.objectStorageUseSSL = meta.objectStorageUseSSL;
+			this.objectStorageUseProxy = meta.objectStorageUseProxy;
+			this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
+			this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				useObjectStorage: this.useObjectStorage,
+				objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
+				objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
+				objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
+				objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
+				objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
+				objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
+				objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
+				objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
+				objectStorageUseSSL: this.objectStorageUseSSL,
+				objectStorageUseProxy: this.objectStorageUseProxy,
+				objectStorageSetPublicRead: this.objectStorageSetPublicRead,
+				objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue
new file mode 100644
index 0000000000..e8f872bf0a
--- /dev/null
+++ b/packages/client/src/pages/admin/other-settings.vue
@@ -0,0 +1,83 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormGroup>
+			<FormInput v-model="summalyProxy">
+				<template #prefix><i class="fas fa-link"></i></template>
+				Summaly Proxy URL
+			</FormInput>
+		</FormGroup>
+		<FormGroup>
+			<FormInput v-model="deeplAuthKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				DeepL Auth Key
+			</FormInput>
+			<FormSwitch v-model="deeplIsPro">
+				Pro account
+			</FormSwitch>
+		</FormGroup>
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.other,
+				icon: 'fas fa-cogs',
+				bg: 'var(--bg)',
+			},
+			summalyProxy: '',
+			deeplAuthKey: '',
+			deeplIsPro: false,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.summalyProxy = meta.summalyProxy;
+			this.deeplAuthKey = meta.deeplAuthKey;
+			this.deeplIsPro = meta.deeplIsPro;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				summalyProxy: this.summalyProxy,
+				deeplAuthKey: this.deeplAuthKey,
+				deeplIsPro: this.deeplIsPro,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
new file mode 100644
index 0000000000..e1352945a1
--- /dev/null
+++ b/packages/client/src/pages/admin/overview.vue
@@ -0,0 +1,236 @@
+<template>
+<div class="edbbcaef" v-size="{ max: [740] }">
+	<div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
+		<div class="number _panel">
+			<div class="label">Users</div>
+			<div class="value _monospace">
+				{{ number(stats.originalUsersCount) }}
+				<MkNumberDiff v-if="usersComparedToThePrevDay != null" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
+			</div>
+		</div>
+		<div class="number _panel">
+			<div class="label">Notes</div>
+			<div class="value _monospace">
+				{{ number(stats.originalNotesCount) }}
+				<MkNumberDiff v-if="notesComparedToThePrevDay != null" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
+			</div>
+		</div>
+	</div>
+
+	<MkContainer :foldable="true" class="charts">
+		<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
+		<div style="padding-top: 12px;">
+			<MkInstanceStats :chart-limit="500" :detailed="true"/>
+		</div>
+	</MkContainer>
+
+	<div class="queue">
+		<MkContainer :foldable="true" :thin="true" class="deliver">
+			<template #header>Queue: deliver</template>
+			<MkQueueChart :connection="queueStatsConnection" domain="deliver"/>
+		</MkContainer>
+		<MkContainer :foldable="true" :thin="true" class="inbox">
+			<template #header>Queue: inbox</template>
+			<MkQueueChart :connection="queueStatsConnection" domain="inbox"/>
+		</MkContainer>
+	</div>
+
+		<!--<XMetrics/>-->
+
+	<MkFolder style="margin: var(--margin)">
+		<template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
+		<div class="cfcdecdf">
+			<div class="number _panel">
+				<div class="label">Misskey</div>
+				<div class="value _monospace">{{ version }}</div>
+			</div>
+			<div class="number _panel" v-if="serverInfo">
+				<div class="label">Node.js</div>
+				<div class="value _monospace">{{ serverInfo.node }}</div>
+			</div>
+			<div class="number _panel" v-if="serverInfo">
+				<div class="label">PostgreSQL</div>
+				<div class="value _monospace">{{ serverInfo.psql }}</div>
+			</div>
+			<div class="number _panel" v-if="serverInfo">
+				<div class="label">Redis</div>
+				<div class="value _monospace">{{ serverInfo.redis }}</div>
+			</div>
+			<div class="number _panel">
+				<div class="label">Vue</div>
+				<div class="value _monospace">{{ vueVersion }}</div>
+			</div>
+		</div>
+	</MkFolder>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw, version as vueVersion } from 'vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import MkInstanceStats from '@/components/instance-stats.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkNumberDiff from '@/components/number-diff.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkQueueChart from '@/components/queue-chart.vue';
+import { version, url } from '@/config';
+import bytes from '@/filters/bytes';
+import number from '@/filters/number';
+import MkInstanceInfo from './instance.vue';
+import XMetrics from './metrics.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkNumberDiff,
+		FormKeyValueView,
+		MkInstanceStats,
+		MkContainer,
+		MkFolder,
+		MkQueueChart,
+		XMetrics,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.dashboard,
+				icon: 'fas fa-tachometer-alt',
+				bg: 'var(--bg)',
+			},
+			version,
+			vueVersion,
+			url,
+			stats: null,
+			meta: null,
+			serverInfo: null,
+			usersComparedToThePrevDay: null,
+			notesComparedToThePrevDay: null,
+			fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
+			fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
+			queueStatsConnection: markRaw(os.stream.useChannel('queueStats')),
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+		
+		os.api('stats', {}).then(stats => {
+			this.stats = stats;
+
+			os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
+				this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1];
+			});
+
+			os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
+				this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1];
+			});
+		});
+
+		os.api('admin/server-info', {}).then(serverInfo => {
+			this.serverInfo = serverInfo;
+		});
+
+		this.$nextTick(() => {
+			this.queueStatsConnection.send('requestLog', {
+				id: Math.random().toString().substr(2, 8),
+				length: 200
+			});
+		});
+	},
+
+	beforeUnmount() {
+		this.queueStatsConnection.dispose();
+	},
+
+	methods: {
+		async showInstanceInfo(q) {
+			let instance = q;
+			if (typeof q === 'string') {
+				instance = await os.api('federation/show-instance', {
+					host: q
+				});
+			}
+			os.popup(MkInstanceInfo, {
+				instance: instance
+			}, {}, 'closed');
+		},
+
+		bytes,
+
+		number,
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.edbbcaef {
+	.cfcdecdf {
+		display: grid;
+		grid-gap: 8px;
+		grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
+
+		> .number {
+			padding: 12px 16px;
+
+			> .label {
+				opacity: 0.7;
+				font-size: 0.8em;
+			}
+
+			> .value {
+				font-weight: bold;
+				font-size: 1.2em;
+
+				> .diff {
+					font-size: 0.8em;
+				}
+			}
+		}
+	}
+
+	> .charts {
+		margin: var(--margin);
+	}
+
+	> .queue {
+		margin: var(--margin);
+		display: flex;
+
+		> .deliver,
+		> .inbox {
+			flex: 1;
+			width: 50%;
+
+			&:not(:first-child) {
+				margin-left: var(--margin);
+			}
+		}
+	}
+
+	&.max-width_740px {
+		> .queue {
+			display: block;
+
+			> .deliver,
+			> .inbox {
+				width: 100%;
+
+				&:not(:first-child) {
+					margin-top: var(--margin);
+					margin-left: 0;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue
new file mode 100644
index 0000000000..5852c6a20d
--- /dev/null
+++ b/packages/client/src/pages/admin/proxy-account.vue
@@ -0,0 +1,87 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>{{ $ts.proxyAccount }}</template>
+				<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
+			</FormKeyValueView>
+			<template #caption>{{ $ts.proxyAccountDescription }}</template>
+		</FormGroup>
+
+		<FormButton @click="chooseProxyAccount" primary>{{ $ts.selectAccount }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormKeyValueView,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormTextarea,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.proxyAccount,
+				icon: 'fas fa-ghost',
+				bg: 'var(--bg)',
+			},
+			proxyAccount: null,
+			proxyAccountId: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.proxyAccountId = meta.proxyAccountId;
+			if (this.proxyAccountId) {
+				this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
+			}
+		},
+
+		chooseProxyAccount() {
+			os.selectUser().then(user => {
+				this.proxyAccount = user;
+				this.proxyAccountId = user.id;
+				this.save();
+			});
+		},
+
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				proxyAccountId: this.proxyAccountId,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/queue.chart.vue b/packages/client/src/pages/admin/queue.chart.vue
new file mode 100644
index 0000000000..136fb63bb6
--- /dev/null
+++ b/packages/client/src/pages/admin/queue.chart.vue
@@ -0,0 +1,102 @@
+<template>
+<div class="_debobigegoItem">
+	<div class="_debobigegoLabel"><slot name="title"></slot></div>
+	<div class="_debobigegoPanel pumxzjhg">
+		<div class="_table status">
+			<div class="_row">
+				<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
+				<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
+				<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
+				<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
+			</div>
+		</div>
+		<div class="">
+			<MkQueueChart :domain="domain" :connection="connection"/>
+		</div>
+		<div class="jobs">
+			<div v-if="jobs.length > 0">
+				<div v-for="job in jobs" :key="job[0]">
+					<span>{{ job[0] }}</span>
+					<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
+				</div>
+			</div>
+			<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
+import number from '@/filters/number';
+import MkQueueChart from '@/components/queue-chart.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkQueueChart
+	},
+
+	props: {
+		domain: {
+			type: String,
+			required: true,
+		},
+		connection: {
+			required: true,
+		},
+	},
+
+	setup(props) {
+		const activeSincePrevTick = ref(0);
+		const active = ref(0);
+		const waiting = ref(0);
+		const delayed = ref(0);
+		const jobs = ref([]);
+
+		onMounted(() => {
+			os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
+				jobs.value = result;
+			});
+
+			const onStats = (stats) => {
+				activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
+				active.value = stats[props.domain].active;
+				waiting.value = stats[props.domain].waiting;
+				delayed.value = stats[props.domain].delayed;
+			};
+
+			props.connection.on('stats', onStats);
+
+			onUnmounted(() => {
+				props.connection.off('stats', onStats);
+			});
+		});
+
+		return {
+			jobs,
+			activeSincePrevTick,
+			active,
+			waiting,
+			delayed,
+			number,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.pumxzjhg {
+	> .status {
+		padding: 16px;
+		border-bottom: solid 0.5px var(--divider);
+	}
+
+	> .jobs {
+		padding: 16px;
+		border-top: solid 0.5px var(--divider);
+		max-height: 180px;
+		overflow: auto;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue
new file mode 100644
index 0000000000..896298840c
--- /dev/null
+++ b/packages/client/src/pages/admin/queue.vue
@@ -0,0 +1,73 @@
+<template>
+<FormBase>
+	<XQueue :connection="connection" domain="inbox">
+		<template #title>In</template>
+	</XQueue>
+	<XQueue :connection="connection" domain="deliver">
+		<template #title>Out</template>
+	</XQueue>
+	<FormButton @click="clear()" danger><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import XQueue from './queue.chart.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		MkButton,
+		XQueue,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.jobQueue,
+				icon: 'fas fa-clipboard-list',
+				bg: 'var(--bg)',
+			},
+			connection: markRaw(os.stream.useChannel('queueStats')),
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+
+		this.$nextTick(() => {
+			this.connection.send('requestLog', {
+				id: Math.random().toString().substr(2, 8),
+				length: 200
+			});
+		});
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		clear() {
+			os.dialog({
+				type: 'warning',
+				title: this.$ts.clearQueueConfirmTitle,
+				text: this.$ts.clearQueueConfirmText,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.apiWithDialog('admin/queue/clear', {});
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue
new file mode 100644
index 0000000000..fd0ce97d57
--- /dev/null
+++ b/packages/client/src/pages/admin/relays.vue
@@ -0,0 +1,99 @@
+<template>
+<FormBase class="relaycxt">
+	<FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton>
+
+	<div class="_debobigegoItem" v-for="relay in relays" :key="relay.inbox">
+		<div class="_debobigegoPanel" style="padding: 16px;">
+			<div>{{ relay.inbox }}</div>
+			<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
+			<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+		</div>
+	</div>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		MkButton,
+		MkInput,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.relays,
+				icon: 'fas fa-globe',
+				bg: 'var(--bg)',
+			},
+			relays: [],
+			inbox: '',
+		}
+	},
+
+	created() {
+		this.refresh();
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async addRelay() {
+			const { canceled, result: inbox } = await os.dialog({
+				title: this.$ts.addRelay,
+				input: {
+					placeholder: this.$ts.inboxUrl
+				}
+			});
+			if (canceled) return;
+			os.api('admin/relays/add', {
+				inbox
+			}).then((relay: any) => {
+				this.refresh();
+			}).catch((e: any) => {
+				os.dialog({
+					type: 'error',
+					text: e.message || e
+				});
+			});
+		},
+
+		remove(inbox: string) {
+			os.api('admin/relays/remove', {
+				inbox
+			}).then(() => {
+				this.refresh();
+			}).catch((e: any) => {
+				os.dialog({
+					type: 'error',
+					text: e.message || e
+				});
+			});
+		},
+
+		refresh() {
+			os.api('admin/relays/list').then((relays: any) => {
+				this.relays = relays;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
new file mode 100644
index 0000000000..ad53ec4fcf
--- /dev/null
+++ b/packages/client/src/pages/admin/security.vue
@@ -0,0 +1,83 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormLink to="/admin/bot-protection">
+			<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
+			<template #suffix v-if="enableHcaptcha">hCaptcha</template>
+			<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
+			<template #suffix v-else>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
+		</FormLink>
+
+		<FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
+
+		<FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormLink,
+		FormSwitch,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.security,
+				icon: 'fas fa-lock',
+				bg: 'var(--bg)',
+			},
+			enableHcaptcha: false,
+			enableRecaptcha: false,
+			enableRegistration: false,
+			emailRequiredForSignup: false,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableHcaptcha = meta.enableHcaptcha;
+			this.enableRecaptcha = meta.enableRecaptcha;
+			this.enableRegistration = !meta.disableRegistration;
+			this.emailRequiredForSignup = meta.emailRequiredForSignup;
+		},
+	
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				disableRegistration: !this.enableRegistration,
+				emailRequiredForSignup: this.emailRequiredForSignup,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/service-worker.vue b/packages/client/src/pages/admin/service-worker.vue
new file mode 100644
index 0000000000..9e91d6d64f
--- /dev/null
+++ b/packages/client/src/pages/admin/service-worker.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormSwitch v-model="enableServiceWorker">
+			{{ $ts.enableServiceworker }}
+			<template #desc>{{ $ts.serviceworkerInfo }}</template>
+		</FormSwitch>
+
+		<template v-if="enableServiceWorker">
+			<FormInput v-model="swPublicKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Public key
+			</FormInput>
+
+			<FormInput v-model="swPrivateKey">
+				<template #prefix><i class="fas fa-key"></i></template>
+				Private key
+			</FormInput>
+		</template>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'ServiceWorker',
+				icon: 'fas fa-bolt',
+				bg: 'var(--bg)',
+			},
+			enableServiceWorker: false,
+			swPublicKey: null,
+			swPrivateKey: null,
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.enableServiceWorker = meta.enableServiceWorker;
+			this.swPublicKey = meta.swPublickey;
+			this.swPrivateKey = meta.swPrivateKey;
+		},
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				enableServiceWorker: this.enableServiceWorker,
+				swPublicKey: this.swPublicKey,
+				swPrivateKey: this.swPrivateKey,
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
new file mode 100644
index 0000000000..66aa3e21db
--- /dev/null
+++ b/packages/client/src/pages/admin/settings.vue
@@ -0,0 +1,151 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormInput v-model="name">
+			<span>{{ $ts.instanceName }}</span>
+		</FormInput>
+
+		<FormTextarea v-model="description">
+			<span>{{ $ts.instanceDescription }}</span>
+		</FormTextarea>
+
+		<FormInput v-model="iconUrl">
+			<template #prefix><i class="fas fa-link"></i></template>
+			<span>{{ $ts.iconUrl }}</span>
+		</FormInput>
+
+		<FormInput v-model="bannerUrl">
+			<template #prefix><i class="fas fa-link"></i></template>
+			<span>{{ $ts.bannerUrl }}</span>
+		</FormInput>
+
+		<FormInput v-model="backgroundImageUrl">
+			<template #prefix><i class="fas fa-link"></i></template>
+			<span>{{ $ts.backgroundImageUrl }}</span>
+		</FormInput>
+
+		<FormInput v-model="tosUrl">
+			<template #prefix><i class="fas fa-link"></i></template>
+			<span>{{ $ts.tosUrl }}</span>
+		</FormInput>
+
+		<FormInput v-model="maintainerName">
+			<span>{{ $ts.maintainerName }}</span>
+		</FormInput>
+
+		<FormInput v-model="maintainerEmail" type="email">
+			<template #prefix><i class="fas fa-envelope"></i></template>
+			<span>{{ $ts.maintainerEmail }}</span>
+		</FormInput>
+
+		<FormTextarea v-model="pinnedUsers">
+			<span>{{ $ts.pinnedUsers }}</span>
+			<template #desc>{{ $ts.pinnedUsersDescription }}</template>
+		</FormTextarea>
+
+		<FormInput v-model="maxNoteTextLength" type="number">
+			<template #prefix><i class="fas fa-pencil-alt"></i></template>
+			<span>{{ $ts.maxNoteTextLength }}</span>
+		</FormInput>
+
+		<FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
+		<FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
+		<FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo>
+
+		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { fetchInstance } from '@/instance';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormInput,
+		FormBase,
+		FormGroup,
+		FormButton,
+		FormTextarea,
+		FormInfo,
+		FormSuspense,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.general,
+				icon: 'fas fa-cog',
+				bg: 'var(--bg)',
+			},
+			name: null,
+			description: null,
+			tosUrl: null as string | null,
+			maintainerName: null,
+			maintainerEmail: null,
+			iconUrl: null,
+			bannerUrl: null,
+			backgroundImageUrl: null,
+			maxNoteTextLength: 0,
+			enableLocalTimeline: false,
+			enableGlobalTimeline: false,
+			pinnedUsers: '',
+		}
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async init() {
+			const meta = await os.api('meta', { detail: true });
+			this.name = meta.name;
+			this.description = meta.description;
+			this.tosUrl = meta.tosUrl;
+			this.iconUrl = meta.iconUrl;
+			this.bannerUrl = meta.bannerUrl;
+			this.backgroundImageUrl = meta.backgroundImageUrl;
+			this.maintainerName = meta.maintainerName;
+			this.maintainerEmail = meta.maintainerEmail;
+			this.maxNoteTextLength = meta.maxNoteTextLength;
+			this.enableLocalTimeline = !meta.disableLocalTimeline;
+			this.enableGlobalTimeline = !meta.disableGlobalTimeline;
+			this.pinnedUsers = meta.pinnedUsers.join('\n');
+		},
+
+		save() {
+			os.apiWithDialog('admin/update-meta', {
+				name: this.name,
+				description: this.description,
+				tosUrl: this.tosUrl,
+				iconUrl: this.iconUrl,
+				bannerUrl: this.bannerUrl,
+				backgroundImageUrl: this.backgroundImageUrl,
+				maintainerName: this.maintainerName,
+				maintainerEmail: this.maintainerEmail,
+				maxNoteTextLength: this.maxNoteTextLength,
+				disableLocalTimeline: !this.enableLocalTimeline,
+				disableGlobalTimeline: !this.enableGlobalTimeline,
+				pinnedUsers: this.pinnedUsers.split('\n'),
+			}).then(() => {
+				fetchInstance();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
new file mode 100644
index 0000000000..f4a2ffa6d2
--- /dev/null
+++ b/packages/client/src/pages/admin/users.vue
@@ -0,0 +1,254 @@
+<template>
+<div class="lknzcolw">
+	<div class="users">
+		<div class="inputs">
+			<MkSelect v-model="sort" style="flex: 1;">
+				<template #label>{{ $ts.sort }}</template>
+				<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
+				<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
+			</MkSelect>
+			<MkSelect v-model="state" style="flex: 1;">
+				<template #label>{{ $ts.state }}</template>
+				<option value="all">{{ $ts.all }}</option>
+				<option value="available">{{ $ts.normal }}</option>
+				<option value="admin">{{ $ts.administrator }}</option>
+				<option value="moderator">{{ $ts.moderator }}</option>
+				<option value="silenced">{{ $ts.silence }}</option>
+				<option value="suspended">{{ $ts.suspend }}</option>
+			</MkSelect>
+			<MkSelect v-model="origin" style="flex: 1;">
+				<template #label>{{ $ts.instance }}</template>
+				<option value="combined">{{ $ts.all }}</option>
+				<option value="local">{{ $ts.local }}</option>
+				<option value="remote">{{ $ts.remote }}</option>
+			</MkSelect>
+		</div>
+		<div class="inputs">
+			<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
+				<template #prefix>@</template>
+				<template #label>{{ $ts.username }}</template>
+			</MkInput>
+			<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
+				<template #prefix>@</template>
+				<template #label>{{ $ts.host }}</template>
+			</MkInput>
+		</div>
+
+		<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users">
+			<button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)">
+				<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
+				<div class="body">
+					<header>
+						<MkUserName class="name" :user="user"/>
+						<span class="acct">@{{ acct(user) }}</span>
+						<span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span>
+						<span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span>
+						<span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span>
+						<span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span>
+					</header>
+					<div>
+						<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
+					</div>
+					<div>
+						<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
+					</div>
+				</div>
+			</button>
+		</MkPagination>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import { acct } from '@/filters/user';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { lookupUser } from '@/scripts/lookup-user';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkPagination,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.users,
+				icon: 'fas fa-users',
+				bg: 'var(--bg)',
+				actions: [{
+					icon: 'fas fa-search',
+					text: this.$ts.search,
+					handler: this.searchUser
+				}, {
+					asFullButton: true,
+					icon: 'fas fa-plus',
+					text: this.$ts.addUser,
+					handler: this.addUser
+				}, {
+					asFullButton: true,
+					icon: 'fas fa-search',
+					text: this.$ts.lookup,
+					handler: this.lookupUser
+				}],
+			},
+			sort: '+createdAt',
+			state: 'all',
+			origin: 'local',
+			searchUsername: '',
+			searchHost: '',
+			pagination: {
+				endpoint: 'admin/show-users',
+				limit: 10,
+				params: () => ({
+					sort: this.sort,
+					state: this.state,
+					origin: this.origin,
+					username: this.searchUsername,
+					hostname: this.searchHost,
+				}),
+				offsetMode: true
+			},
+		}
+	},
+
+	watch: {
+		sort() {
+			this.$refs.users.reload();
+		},
+		state() {
+			this.$refs.users.reload();
+		},
+		origin() {
+			this.$refs.users.reload();
+		},
+	},
+
+	async mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		lookupUser,
+
+		searchUser() {
+			os.selectUser().then(user => {
+				this.show(user);
+			});
+		},
+
+		async addUser() {
+			const { canceled: canceled1, result: username } = await os.dialog({
+				title: this.$ts.username,
+				input: true
+			});
+			if (canceled1) return;
+
+			const { canceled: canceled2, result: password } = await os.dialog({
+				title: this.$ts.password,
+				input: { type: 'password' }
+			});
+			if (canceled2) return;
+
+			os.apiWithDialog('admin/accounts/create', {
+				username: username,
+				password: password,
+			}).then(res => {
+				this.$refs.users.reload();
+			});
+		},
+
+		show(user) {
+			os.pageWindow(`/user-info/${user.id}`);
+		},
+
+		acct
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.lknzcolw {
+	> .users {
+		margin: var(--margin);
+
+		> .inputs {
+			display: flex;
+			margin-bottom: 16px;
+
+			> * {
+				margin-right: 16px;
+
+				&:last-child {
+					margin-right: 0;
+				}
+			}
+		}
+	
+		> .users {
+			margin-top: var(--margin);
+
+			> .user {
+				display: flex;
+				width: 100%;
+				box-sizing: border-box;
+				text-align: left;
+				align-items: center;
+				padding: 16px;
+
+				&:hover {
+					color: var(--accent);
+				}
+
+				> .avatar {
+					width: 60px;
+					height: 60px;
+				}
+
+				> .body {
+					margin-left: 0.3em;
+					padding: 0 8px;
+					flex: 1;
+
+					@media (max-width: 500px) {
+						font-size: 14px;
+					}
+
+					> header {
+						> .name {
+							font-weight: bold;
+						}
+
+						> .acct {
+							margin-left: 8px;
+							opacity: 0.7;
+						}
+
+						> .staff {
+							margin-left: 0.5em;
+							color: var(--badge);
+						}
+
+						> .punished {
+							margin-left: 0.5em;
+							color: #4dabf7;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/advanced-theme-editor.vue b/packages/client/src/pages/advanced-theme-editor.vue
new file mode 100644
index 0000000000..eebfc21b3f
--- /dev/null
+++ b/packages/client/src/pages/advanced-theme-editor.vue
@@ -0,0 +1,352 @@
+<template>
+<div class="t9makv94">
+	<section class="_section">
+		<div class="_content">
+			<details>
+				<summary>{{ $ts.import }}</summary>
+				<MkTextarea v-model="themeToImport">
+					{{ $ts._theme.importInfo }}
+				</MkTextarea>
+				<MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
+			</details>
+		</div>
+	</section>
+	<section class="_section">
+		<div class="_content _card _gap">
+			<div class="_content">
+				<MkInput v-model="name" required><span>{{ $ts.name }}</span></MkInput>
+				<MkInput v-model="author" required><span>{{ $ts.author }}</span></MkInput>
+				<MkTextarea v-model="description"><span>{{ $ts.description }}</span></MkTextarea>
+				<div class="_inputs">
+					<div v-text="$ts._theme.base" />
+					<MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio>
+					<MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio>
+				</div>
+			</div>
+		</div>
+		<div class="_content _card _gap">
+			<div class="list-view _content">
+				<div class="item" v-for="([ k, v ], i) in theme" :key="k">
+					<div class="_inputs">
+						<div>
+							{{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }}
+							<button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" />
+						</div>
+						<div>
+							<div class="type" @click="chooseType($event, i)">
+								{{ getTypeOf(v) }} <i class="fas fa-chevron-down"></i>
+							</div>
+							<!-- default -->
+							<div v-if="v === null" v-text="baseProps[k]" class="default-value" />
+							<!-- color -->
+							<div v-else-if="typeof v === 'string'" class="color">
+								<input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
+								<MkInput class="select" :value="v" @update:modelValue="colorChanged($event, i)"/>
+							</div>
+							<!-- ref const -->
+							<MkInput v-else-if="v.type === 'refConst'" v-model="v.key">
+								<template #prefix>$</template>
+								<span>{{ $ts.name }}</span>
+							</MkInput>
+							<!-- ref props -->
+							<MkSelect class="select" v-else-if="v.type === 'refProp'" v-model="v.key">
+								<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
+							</MkSelect>
+							<!-- func -->
+							<template v-else-if="v.type === 'func'">
+								<MkSelect class="select" v-model="v.name">
+									<template #label>{{ $ts._theme.funcKind }}</template>
+									<option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option>
+								</MkSelect>
+								<MkInput type="number" v-model="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput>
+								<MkSelect class="select" v-model="v.value">
+									<template #label>{{ $ts._theme.basedProp }}</template>
+									<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
+								</MkSelect>
+							</template>
+							<!-- CSS -->
+							<MkInput v-else-if="v.type === 'css'" v-model="v.value">
+								<span>CSS</span>
+							</MkInput>
+						</div>
+					</div>
+				</div>
+				<MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton>
+			</div>
+		</div>
+	</section>
+	<section class="_section">
+		<details class="_content">
+			<summary>{{ $ts.sample }}</summary>
+			<MkSample/>
+		</details>
+	</section>
+	<section class="_section">
+		<div class="_content">
+			<MkButton inline @click="preview">{{ $ts.preview }}</MkButton>
+			<MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as JSON5 from 'json5';
+import { toUnicode } from 'punycode/';
+
+import MkRadio from '@/components/form/radio.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkSample from '@/components/sample.vue';
+
+import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor';
+import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
+import { host } from '@/config';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import { addTheme } from '@/theme-store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkRadio,
+		MkButton,
+		MkInput,
+		MkTextarea,
+		MkSelect,
+		MkSample,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.themeEditor,
+				icon: 'fas fa-palette',
+			},
+			theme: [] as ThemeViewModel,
+			name: '',
+			description: '',
+			baseTheme: 'light' as 'dark' | 'light',
+			author: `@${this.$i.username}@${toUnicode(host)}`,
+			themeToImport: '',
+			changed: false,
+			lightTheme, darkTheme, themeProps,
+		}
+	},
+
+	computed: {
+		baseProps() {
+			return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props;
+		},
+	},
+
+	beforeUnmount() {
+		window.removeEventListener('beforeunload', this.beforeunload);
+	},
+
+	async beforeRouteLeave(to, from, next) {
+		if (this.changed && !(await this.confirm())) {
+			next(false);
+		} else {
+			next();
+		}
+	},
+
+	mounted() {
+		this.init();
+		window.addEventListener('beforeunload', this.beforeunload);
+		const changed = () => this.changed = true;
+		this.$watch('name', changed);
+		this.$watch('description', changed);
+		this.$watch('baseTheme', changed);
+		this.$watch('author', changed);
+		this.$watch('theme', changed);
+	},
+
+	methods: {
+		beforeunload(e: BeforeUnloadEvent) {
+			if (this.changed) {
+				e.preventDefault();
+				e.returnValue = '';
+			}
+		},
+
+		async confirm(): Promise<boolean> {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$ts.leaveConfirm,
+				showCancelButton: true
+			});
+			return !canceled;
+		},
+
+		init() {
+			const t: ThemeViewModel = [];
+			for (const key of themeProps) {
+				t.push([ key, null ]);
+			}
+			this.theme = t;
+		},
+	
+		async del(i: number) {
+			const { canceled } = await os.dialog({ 
+				type: 'warning',
+				showCancelButton: true,
+				text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
+			});
+			if (canceled) return;
+			Vue.delete(this.theme, i);
+		},
+	
+		async addConst() {
+			const { canceled, result } = await os.dialog({
+				title: this.$ts._theme.inputConstantName,
+				input: true
+			});
+			if (canceled) return;
+			this.theme.push([ '$' + result, '#000000']);
+		},
+	
+		save() {
+			const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
+			addTheme(theme);
+			os.dialog({
+				type: 'success',
+				text: this.$t('_theme.installed', { name: theme.name })
+			});
+			this.changed = false;
+		},
+	
+		preview() {
+			const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
+			try {
+				applyTheme(theme, false);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: e.message
+				});
+			}
+		},
+	
+		async importTheme() {
+			if (this.changed && (!await this.confirm())) return;
+
+			try {
+				const theme = JSON5.parse(this.themeToImport) as Theme;
+				if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid);
+
+				this.name = theme.name;
+				this.description = theme.desc || '';
+				this.author = theme.author;
+				this.baseTheme = theme.base || 'light';
+				this.theme = convertToViewModel(theme);
+				this.themeToImport = '';
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: e.message
+				});
+			}
+		},
+	
+		colorChanged(color: string, i: number) {
+			this.theme[i] = [this.theme[i][0], color];
+		},
+
+		getTypeOf(v: ThemeValue) {
+			return v === null
+				? this.$ts._theme.defaultValue
+				: typeof v === 'string'
+					? this.$ts._theme.color
+					: this.$t('_theme.' + v.type);
+		},
+	
+		async chooseType(e: MouseEvent, i: number) {
+			const newValue = await this.showTypeMenu(e);
+			this.theme[i] = [ this.theme[i][0], newValue ];
+		},
+	
+		showTypeMenu(e: MouseEvent) {
+			return new Promise<ThemeValue>((resolve) => {
+				os.popupMenu([{
+					text: this.$ts._theme.defaultValue,
+					action: () => resolve(null),
+				}, {
+					text: this.$ts._theme.color,
+					action: () => resolve('#000000'),
+				}, {
+					text: this.$ts._theme.func,
+					action: () => resolve({
+						type: 'func', name: 'alpha', arg: 1, value: 'accent'
+					}),
+				}, {
+					text: this.$ts._theme.refProp,
+					action: () => resolve({
+						type: 'refProp', key: 'accent',
+					}),
+				}, {
+					text: this.$ts._theme.refConst,
+					action: () => resolve({
+						type: 'refConst', key: '',
+					}),
+				}, {
+					text: 'CSS',
+					action: () => resolve({
+						type: 'css', value: '',
+					}),
+				}], e.currentTarget || e.target);
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.t9makv94 {
+	> ._section {
+		> ._content {
+			> .list-view {
+				> .item {
+					min-height: 48px;
+					word-break: break-all;
+
+					&:not(:last-child) {
+						margin-bottom: 8px;
+					}
+
+					.select {
+						margin: 24px 0;
+					}
+
+					.type {
+						cursor: pointer;
+					}
+
+					.default-value {
+						opacity: 0.6;
+						pointer-events: none;
+						user-select: none;
+					}
+
+					.color {
+						> input {
+							display: inline-block;
+							width: 1.5em;
+							height: 1.5em;
+						}
+
+						> div {
+							margin-left: 8px;
+							display: inline-block;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue
new file mode 100644
index 0000000000..946b368733
--- /dev/null
+++ b/packages/client/src/pages/announcements.vue
@@ -0,0 +1,74 @@
+<template>
+<MkSpacer :content-max="800">
+	<MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content">
+		<section class="_card announcement" v-for="(announcement, i) in items" :key="announcement.id">
+			<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
+			<div class="_content">
+				<Mfm :text="announcement.text"/>
+				<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
+			</div>
+			<div class="_footer" v-if="$i && !announcement.isRead">
+				<MkButton @click="read(items, announcement, i)" primary><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
+			</div>
+		</section>
+	</MkPagination>
+</MkSpacer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkButton
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.announcements,
+				icon: 'fas fa-broadcast-tower',
+				bg: 'var(--bg)',
+			},
+			pagination: {
+				endpoint: 'announcements',
+				limit: 10,
+			},
+		};
+	},
+
+	methods: {
+		// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
+		read(items, announcement, i) {
+			items[i] = {
+				...announcement,
+				isRead: true,
+			};
+			os.api('i/read-announcement', { announcementId: announcement.id });
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ruryvtyk {
+	> .announcement {
+		&:not(:last-child) {
+			margin-bottom: var(--margin);
+		}
+
+		> ._content {
+			> img {
+				display: block;
+				max-height: 300px;
+				max-width: 100%;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue
new file mode 100644
index 0000000000..f7f6990fa8
--- /dev/null
+++ b/packages/client/src/pages/antenna-timeline.vue
@@ -0,0 +1,147 @@
+<template>
+<div class="tqmomfks" v-hotkey.global="keymap" v-size="{ min: [800] }">
+	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
+	<div class="tl _block">
+		<XTimeline ref="tl" class="tl"
+			:key="antennaId"
+			src="antenna"
+			:antenna="antennaId"
+			:sound="true"
+			@before="before()"
+			@after="after()"
+			@queue="queueUpdated"
+		/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
+import Progress from '@/scripts/loading';
+import XTimeline from '@/components/timeline.vue';
+import { scroll } from '@/scripts/scroll';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XTimeline,
+	},
+
+	props: {
+		antennaId: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			antenna: null,
+			queue: 0,
+			[symbols.PAGE_INFO]: computed(() => this.antenna ? {
+				title: this.antenna.name,
+				icon: 'fas fa-satellite',
+				bg: 'var(--bg)',
+				actions: [{
+					icon: 'fas fa-calendar-alt',
+					text: this.$ts.jumpToSpecifiedDate,
+					handler: this.timetravel
+				}, {
+					icon: 'fas fa-cog',
+					text: this.$ts.settings,
+					handler: this.settings
+				}],
+			} : null),
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				't': this.focus
+			};
+		},
+	},
+
+	watch: {
+		antennaId: {
+			async handler() {
+				this.antenna = await os.api('antennas/show', {
+					antennaId: this.antennaId
+				});
+			},
+			immediate: true
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		},
+
+		queueUpdated(q) {
+			this.queue = q;
+		},
+
+		top() {
+			scroll(this.$el, { top: 0 });
+		},
+
+		async timetravel() {
+			const { canceled, result: date } = await os.dialog({
+				title: this.$ts.date,
+				input: {
+					type: 'date'
+				}
+			});
+			if (canceled) return;
+
+			this.$refs.tl.timetravel(new Date(date));
+		},
+
+		settings() {
+			this.$router.push(`/my/antennas/${this.antennaId}`);
+		},
+
+		focus() {
+			(this.$refs.tl as any).focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.tqmomfks {
+	padding: var(--margin);
+
+	> .new {
+		position: sticky;
+		top: calc(var(--stickyTop, 0px) + 16px);
+		z-index: 1000;
+		width: 100%;
+
+		> button {
+			display: block;
+			margin: var(--margin) auto 0 auto;
+			padding: 8px 16px;
+			border-radius: 32px;
+		}
+	}
+
+	> .tl {
+		background: var(--bg);
+		border-radius: var(--radius);
+		overflow: clip;
+	}
+
+	&.min-width_800px {
+		max-width: 800px;
+		margin: 0 auto;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/api-console.vue b/packages/client/src/pages/api-console.vue
new file mode 100644
index 0000000000..48495df3c2
--- /dev/null
+++ b/packages/client/src/pages/api-console.vue
@@ -0,0 +1,93 @@
+<template>
+<div class="_root">
+	<div class="_block" style="padding: 24px;">
+		<MkInput v-model="endpoint" :datalist="endpoints" @update:modelValue="onEndpointChange()" class="">
+			<template #label>Endpoint</template>
+		</MkInput>
+		<MkTextarea v-model="body" code>
+			<template #label>Params (JSON or JSON5)</template>
+		</MkTextarea>
+		<MkSwitch v-model="withCredential">
+			With credential
+		</MkSwitch>
+		<MkButton primary full @click="send" :disabled="sending">
+			<template v-if="sending"><MkEllipsis/></template>
+			<template v-else><i class="fas fa-paper-plane"></i> Send</template>
+		</MkButton>
+	</div>
+	<div v-if="res" class="_block" style="padding: 24px;">
+		<MkTextarea v-model="res" code readonly tall>
+			<template #label>Response</template>
+		</MkTextarea>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as JSON5 from 'json5';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton, MkInput, MkTextarea, MkSwitch,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'API console',
+				icon: 'fas fa-terminal'
+			},
+
+			endpoint: '',
+			body: '{}',
+			res: null,
+			sending: false,
+			endpoints: [],
+			withCredential: true,
+
+		};
+	},
+
+	created() {
+		os.api('endpoints').then(endpoints => {
+			this.endpoints = endpoints;
+		});
+	},
+
+	methods: {
+		send() {
+			this.sending = true;
+			os.api(this.endpoint, JSON5.parse(this.body)).then(res => {
+				this.sending = false;
+				this.res = JSON5.stringify(res, null, 2);
+			}, err => {
+				this.sending = false;
+				this.res = JSON5.stringify(err, null, 2);
+			});
+		},
+
+		onEndpointChange() {
+			os.api('endpoint', { endpoint: this.endpoint }, this.withCredential ? undefined : null).then(endpoint => {
+				const body = {};
+				for (const p of endpoint.params) {
+					body[p.name] =
+						p.type === 'String' ? '' :
+						p.type === 'Number' ? 0 :
+						p.type === 'Boolean' ? false :
+						p.type === 'Array' ? [] :
+						p.type === 'Object' ? {} :
+						null;
+				}
+				this.body = JSON5.stringify(body, null, 2);
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/auth.form.vue b/packages/client/src/pages/auth.form.vue
new file mode 100644
index 0000000000..8b2adc3e07
--- /dev/null
+++ b/packages/client/src/pages/auth.form.vue
@@ -0,0 +1,60 @@
+<template>
+<section class="_section">
+	<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
+	<div class="_content">
+		<h2>{{ app.name }}</h2>
+		<p class="id">{{ app.id }}</p>
+		<p class="description">{{ app.description }}</p>
+	</div>
+	<div class="_content">
+		<h2>{{ $ts._auth.permissionAsk }}</h2>
+		<ul>
+			<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
+		</ul>
+	</div>
+	<div class="_footer">
+		<MkButton @click="cancel" inline>{{ $ts.cancel }}</MkButton>
+		<MkButton @click="accept" inline primary>{{ $ts.accept }}</MkButton>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+	props: ['session'],
+	computed: {
+		name(): string {
+			const el = document.createElement('div');
+			el.textContent = this.app.name
+			return el.innerHTML;
+		},
+		app(): any {
+			return this.session.app;
+		}
+	},
+	methods: {
+		cancel() {
+			os.api('auth/deny', {
+				token: this.session.token
+			}).then(() => {
+				this.$emit('denied');
+			});
+		},
+
+		accept() {
+			os.api('auth/accept', {
+				token: this.session.token
+			}).then(() => {
+				this.$emit('accepted');
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/auth.vue b/packages/client/src/pages/auth.vue
new file mode 100644
index 0000000000..522bd4cdf8
--- /dev/null
+++ b/packages/client/src/pages/auth.vue
@@ -0,0 +1,95 @@
+<template>
+<div class="" v-if="$i && fetching">
+	<MkLoading/>
+</div>
+<div v-else-if="$i">
+	<XForm
+		class="form"
+		ref="form"
+		v-if="state == 'waiting'"
+		:session="session"
+		@denied="state = 'denied'"
+		@accepted="accepted"
+	/>
+	<div class="denied" v-if="state == 'denied'">
+		<h1>{{ $ts._auth.denied }}</h1>
+	</div>
+	<div class="accepted" v-if="state == 'accepted'">
+		<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$ts.allowed }}</h1>
+		<p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p>
+		<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p>
+	</div>
+	<div class="error" v-if="state == 'fetch-session-error'">
+		<p>{{ $ts.somethingHappened }}</p>
+	</div>
+</div>
+<div class="signin" v-else>
+	<MkSignin @login="onLogin"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XForm from './auth.form.vue';
+import MkSignin from '@/components/signin.vue';
+import * as os from '@/os';
+import { login } from '@/account';
+
+export default defineComponent({
+	components: {
+		XForm,
+		MkSignin,
+	},
+	data() {
+		return {
+			state: null,
+			session: null,
+			fetching: true
+		};
+	},
+	computed: {
+		token(): string {
+			return this.$route.params.token;
+		}
+	},
+	mounted() {
+		if (!this.$i) return;
+
+		// Fetch session
+		os.api('auth/session/show', {
+			token: this.token
+		}).then(session => {
+			this.session = session;
+			this.fetching = false;
+
+			// 既に連携していた場合
+			if (this.session.app.isAuthorized) {
+				os.api('auth/accept', {
+					token: this.session.token
+				}).then(() => {
+					this.accepted();
+				});
+			} else {
+				this.state = 'waiting';
+			}
+		}).catch(error => {
+			this.state = 'fetch-session-error';
+			this.fetching = false;
+		});
+	},
+	methods: {
+		accepted() {
+			this.state = 'accepted';
+			if (this.session.app.callbackUrl) {
+				location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
+			}
+		}, onLogin(res) {
+			login(res.i);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue
new file mode 100644
index 0000000000..e2cf8b9f00
--- /dev/null
+++ b/packages/client/src/pages/channel-editor.vue
@@ -0,0 +1,129 @@
+<template>
+<div>
+	<div class="_section">
+		<div class="_content">
+			<MkInput v-model="name">
+				<template #label>{{ $ts.name }}</template>
+			</MkInput>
+
+			<MkTextarea v-model="description">
+				<template #label>{{ $ts.description }}</template>
+			</MkTextarea>
+
+			<div class="banner">
+				<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton>
+				<div v-else-if="bannerUrl">
+					<img :src="bannerUrl" style="width: 100%;"/>
+					<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton>
+				</div>
+			</div>
+		</div>
+		<div class="_footer">
+			<MkButton @click="save()" primary><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import { selectFile } from '@/scripts/select-file';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkTextarea, MkButton, MkInput,
+	},
+
+	props: {
+		channelId: {
+			type: String,
+			required: false
+		},
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.channelId ? {
+				title: this.$ts._channel.edit,
+				icon: 'fas fa-satellite-dish',
+			} : {
+				title: this.$ts._channel.create,
+				icon: 'fas fa-satellite-dish',
+			}),
+			channel: null,
+			name: null,
+			description: null,
+			bannerUrl: null,
+			bannerId: null,
+		};
+	},
+
+	watch: {
+		async bannerId() {
+			if (this.bannerId == null) {
+				this.bannerUrl = null;
+			} else {
+				this.bannerUrl = (await os.api('drive/files/show', {
+					fileId: this.bannerId,
+				})).url;
+			}
+		},
+	},
+
+	async created() {
+		if (this.channelId) {
+			this.channel = await os.api('channels/show', {
+				channelId: this.channelId,
+			});
+
+			this.name = this.channel.name;
+			this.description = this.channel.description;
+			this.bannerId = this.channel.bannerId;
+			this.bannerUrl = this.channel.bannerUrl;
+		}
+	},
+
+	methods: {
+		save() {
+			const params = {
+				name: this.name,
+				description: this.description,
+				bannerId: this.bannerId,
+			};
+
+			if (this.channelId) {
+				params.channelId = this.channelId;
+				os.api('channels/update', params)
+				.then(channel => {
+					os.success();
+				});
+			} else {
+				os.api('channels/create', params)
+				.then(channel => {
+					os.success();
+					this.$router.push(`/channels/${channel.id}`);
+				});
+			}
+		},
+
+		setBannerImage(e) {
+			selectFile(e.currentTarget || e.target, null, false).then(file => {
+				this.bannerId = file.id;
+			});
+		},
+
+		removeBannerImage() {
+			this.bannerId = null;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue
new file mode 100644
index 0000000000..f9a9ca29e9
--- /dev/null
+++ b/packages/client/src/pages/channel.vue
@@ -0,0 +1,186 @@
+<template>
+<div v-if="channel" class="_section">
+	<div class="wpgynlbz _content _panel _gap" :class="{ hide: !showBanner }">
+		<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
+		<button class="_button toggle" @click="() => showBanner = !showBanner">
+			<template v-if="showBanner"><i class="fas fa-angle-up"></i></template>
+			<template v-else><i class="fas fa-angle-down"></i></template>
+		</button>
+		<div class="hideOverlay" v-if="!showBanner">
+		</div>
+		<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
+			<div class="status">
+				<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
+				<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
+			</div>
+			<div class="fade"></div>
+		</div>
+		<div class="description" v-if="channel.description">
+			<Mfm :text="channel.description" :is-note="false" :i="$i"/>
+		</div>
+	</div>
+
+	<XPostForm :channel="channel" class="post-form _content _panel _gap" fixed v-if="$i"/>
+
+	<XTimeline class="_content _gap" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import XPostForm from '@/components/post-form.vue';
+import XTimeline from '@/components/timeline.vue';
+import XChannelFollowButton from '@/components/channel-follow-button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkContainer,
+		XPostForm,
+		XTimeline,
+		XChannelFollowButton
+	},
+
+	props: {
+		channelId: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.channel ? {
+				title: this.channel.name,
+				icon: 'fas fa-satellite-dish',
+			} : null),
+			channel: null,
+			showBanner: true,
+			pagination: {
+				endpoint: 'channels/timeline',
+				limit: 10,
+				params: () => ({
+					channelId: this.channelId,
+				})
+			},
+		};
+	},
+
+	watch: {
+		channelId: {
+			async handler() {
+				this.channel = await os.api('channels/show', {
+					channelId: this.channelId,
+				});
+			},
+			immediate: true
+		}
+	},
+
+	created() {
+
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.wpgynlbz {
+	position: relative;
+
+	> .subscribe {
+		position: absolute;
+		z-index: 1;
+		top: 16px;
+		left: 16px;
+	}
+
+	> .toggle {
+		position: absolute;
+		z-index: 2;
+    top: 8px;
+		right: 8px;
+		font-size: 1.2em;
+		width: 48px;
+		height: 48px;
+		color: #fff;
+		background: rgba(0, 0, 0, 0.5);
+		border-radius: 100%;
+		
+		> i {
+			vertical-align: middle;
+		}
+	}
+	
+	> .banner {
+		position: relative;
+		height: 200px;
+		background-position: center;
+		background-size: cover;
+
+		> .fade {
+			position: absolute;
+			bottom: 0;
+			left: 0;
+			width: 100%;
+			height: 64px;
+			background: linear-gradient(0deg, var(--panel), var(--X15));
+		}
+
+		> .status {
+			position: absolute;
+			z-index: 1;
+			bottom: 16px;
+			right: 16px;
+			padding: 8px 12px;
+			font-size: 80%;
+			background: rgba(0, 0, 0, 0.7);
+			border-radius: 6px;
+			color: #fff;
+		}
+	}
+
+	> .description {
+		padding: 16px;
+	}
+
+	> .hideOverlay {
+		position: absolute;
+		z-index: 1;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		-webkit-backdrop-filter: var(--blur, blur(16px));
+		backdrop-filter: var(--blur, blur(16px));
+		background: rgba(0, 0, 0, 0.3);
+	}
+
+	&.hide {
+		> .subscribe {
+			display: none;
+		}
+
+		> .toggle {
+			top: 0;
+			right: 0;
+			height: 100%;
+			background: transparent;
+		}
+
+		> .banner {
+			height: 42px;
+			filter: blur(8px);
+
+			> * {
+				display: none;
+			}
+		}
+
+		> .description {
+			display: none;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue
new file mode 100644
index 0000000000..09e136ac00
--- /dev/null
+++ b/packages/client/src/pages/channels.vue
@@ -0,0 +1,77 @@
+<template>
+<div>
+	<div class="_section" style="padding: 0;" v-if="$i">
+		<MkTab class="_content" v-model="tab">
+			<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._channel.featured }}</option>
+			<option value="following"><i class="fas fa-heart"></i> {{ $ts._channel.following }}</option>
+			<option value="owned"><i class="fas fa-edit"></i> {{ $ts._channel.owned }}</option>
+		</MkTab>
+	</div>
+
+	<div class="_section">
+		<div class="_content grwlizim featured" v-if="tab === 'featured'">
+			<MkPagination :pagination="featuredPagination" #default="{items}">
+				<MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/>
+			</MkPagination>
+		</div>
+
+		<div class="_content grwlizim following" v-if="tab === 'following'">
+			<MkPagination :pagination="followingPagination" #default="{items}">
+				<MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/>
+			</MkPagination>
+		</div>
+
+		<div class="_content grwlizim owned" v-if="tab === 'owned'">
+			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
+			<MkPagination :pagination="ownedPagination" #default="{items}">
+				<MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/>
+			</MkPagination>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkChannelPreview from '@/components/channel-preview.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkTab from '@/components/tab.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkChannelPreview, MkPagination, MkButton, MkTab
+	},
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.channel,
+				icon: 'fas fa-satellite-dish',
+				action: {
+					icon: 'fas fa-plus',
+					handler: this.create
+				}
+			},
+			tab: 'featured',
+			featuredPagination: {
+				endpoint: 'channels/featured',
+				noPaging: true,
+			},
+			followingPagination: {
+				endpoint: 'channels/followed',
+				limit: 5,
+			},
+			ownedPagination: {
+				endpoint: 'channels/owned',
+				limit: 5,
+			},
+		};
+	},
+	methods: {
+		create() {
+			this.$router.push(`/channels/new`);
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue
new file mode 100644
index 0000000000..510a73ce68
--- /dev/null
+++ b/packages/client/src/pages/clip.vue
@@ -0,0 +1,154 @@
+<template>
+<div v-if="clip" class="_section">
+	<div class="okzinsic _content _panel _gap">
+		<div class="description" v-if="clip.description">
+			<Mfm :text="clip.description" :is-note="false" :i="$i"/>
+		</div>
+		<div class="user">
+			<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
+		</div>
+	</div>
+
+	<XNotes class="_content _gap" :pagination="pagination" :detail="true"/>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import XPostForm from '@/components/post-form.vue';
+import XNotes from '@/components/notes.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkContainer,
+		XPostForm,
+		XNotes,
+	},
+
+	props: {
+		clipId: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.clip ? {
+				title: this.clip.name,
+				icon: 'fas fa-paperclip',
+				action: {
+					icon: 'fas fa-ellipsis-h',
+					handler: this.menu
+				}
+			} : null),
+			clip: null,
+			pagination: {
+				endpoint: 'clips/notes',
+				limit: 10,
+				params: () => ({
+					clipId: this.clipId,
+				})
+			},
+		};
+	},
+
+	computed: {
+		isOwned(): boolean {
+			return this.$i && this.clip && (this.$i.id === this.clip.userId);
+		}
+	},
+
+	watch: {
+		clipId: {
+			async handler() {
+				this.clip = await os.api('clips/show', {
+					clipId: this.clipId,
+				});
+			},
+			immediate: true
+		}
+	},
+
+	created() {
+
+	},
+
+	methods: {
+		menu(ev) {
+			os.popupMenu([this.isOwned ? {
+				icon: 'fas fa-pencil-alt',
+				text: this.$ts.edit,
+				action: async () => {
+					const { canceled, result } = await os.form(this.clip.name, {
+						name: {
+							type: 'string',
+							label: this.$ts.name,
+							default: this.clip.name
+						},
+						description: {
+							type: 'string',
+							required: false,
+							multiline: true,
+							label: this.$ts.description,
+							default: this.clip.description
+						},
+						isPublic: {
+							type: 'boolean',
+							label: this.$ts.public,
+							default: this.clip.isPublic
+						}
+					});
+					if (canceled) return;
+
+					os.apiWithDialog('clips/update', {
+						clipId: this.clip.id,
+						...result
+					});
+				}
+			} : undefined, this.isOwned ? {
+				icon: 'fas fa-trash-alt',
+				text: this.$ts.delete,
+				danger: true,
+				action: async () => {
+					const { canceled } = await os.dialog({
+						type: 'warning',
+						text: this.$t('deleteAreYouSure', { x: this.clip.name }),
+						showCancelButton: true
+					});
+					if (canceled) return;
+
+					await os.apiWithDialog('clips/delete', {
+						clipId: this.clip.id,
+					});
+				}
+			} : undefined], ev.currentTarget || ev.target);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.okzinsic {
+	position: relative;
+
+	> .description {
+		padding: 16px;
+	}
+
+	> .user {
+		$height: 32px;
+		padding: 16px;
+		border-top: solid 0.5px var(--divider);
+		line-height: $height;
+
+		> .avatar {
+			width: $height;
+			height: $height;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue
new file mode 100644
index 0000000000..5d7d3b2f5a
--- /dev/null
+++ b/packages/client/src/pages/drive.vue
@@ -0,0 +1,28 @@
+<template>
+<div>
+	<XDrive ref="drive" @cd="x => folder = x"/>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import XDrive from '@/components/drive.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XDrive
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: computed(() => this.folder ? this.folder.name : this.$ts.drive),
+				icon: 'fas fa-cloud',
+			},
+			folder: null,
+		};
+	},
+});
+</script>
diff --git a/packages/client/src/pages/emojis.category.vue b/packages/client/src/pages/emojis.category.vue
new file mode 100644
index 0000000000..327cbce7e8
--- /dev/null
+++ b/packages/client/src/pages/emojis.category.vue
@@ -0,0 +1,135 @@
+<template>
+<div class="driuhtrh">
+	<div class="query">
+		<MkInput v-model="q" class="" :placeholder="$ts.search">
+			<template #prefix><i class="fas fa-search"></i></template>
+		</MkInput>
+
+		<!-- たくさんあると邪魔
+		<div class="tags">
+			<span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
+		</div>
+		-->
+	</div>
+
+	<MkFolder class="emojis" v-if="searchEmojis">
+		<template #header>{{ $ts.searchResult }}</template>
+		<div class="zuvgdzyt">
+			<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/>
+		</div>
+	</MkFolder>
+	
+	<MkFolder class="emojis" v-for="category in customEmojiCategories" :key="category">
+		<template #header>{{ category || $ts.other }}</template>
+		<div class="zuvgdzyt">
+			<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
+		</div>
+	</MkFolder>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkTab from '@/components/tab.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { emojiCategories, emojiTags } from '@/instance';
+import XEmoji from './emojis.emoji.vue';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkFolder,
+		MkTab,
+		XEmoji,
+	},
+
+	data() {
+		return {
+			q: '',
+			customEmojiCategories: emojiCategories,
+			customEmojis: this.$instance.emojis,
+			tags: emojiTags,
+			selectedTags: new Set(),
+			searchEmojis: null,
+		}
+	},
+
+	watch: {
+		q() { this.search(); },
+		selectedTags: {
+			handler() {
+				this.search();
+			},
+			deep: true
+		},
+	},
+
+	methods: {
+		search() {
+			if ((this.q === '' || this.q == null) && this.selectedTags.size === 0) {
+				this.searchEmojis = null;
+				return;
+			}
+
+			if (this.selectedTags.size === 0) {
+				this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q));
+			} else {
+				this.searchEmojis = this.customEmojis.filter(e => (e.name.includes(this.q) || e.aliases.includes(this.q)) && [...this.selectedTags].every(t => e.aliases.includes(t)));
+			}
+		},
+
+		toggleTag(tag) {
+			if (this.selectedTags.has(tag)) {
+				this.selectedTags.delete(tag);
+			} else {
+				this.selectedTags.add(tag);
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.driuhtrh {
+	background: var(--bg);
+
+	> .query {
+		background: var(--bg);
+		padding: 16px;
+
+		> .tags {
+			> .tag {
+				display: inline-block;
+				margin: 8px 8px 0 0;
+				padding: 4px 8px;
+				font-size: 0.9em;
+				background: var(--accentedBg);
+				border-radius: 5px;
+
+				&.active {
+					background: var(--accent);
+					color: var(--fgOnAccent);
+				}
+			}
+		}
+	}
+
+	> .emojis {
+		--x-padding: 0 16px;
+
+		.zuvgdzyt {
+			display: grid;
+			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+			grid-gap: 12px;
+			margin: 0 var(--margin) var(--margin) var(--margin);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue
new file mode 100644
index 0000000000..4ca7c15742
--- /dev/null
+++ b/packages/client/src/pages/emojis.emoji.vue
@@ -0,0 +1,94 @@
+<template>
+<button class="zuvgdzyu _button" @click="menu">
+	<img :src="emoji.url" class="img" :alt="emoji.name"/>
+	<div class="body">
+		<div class="name _monospace">{{ emoji.name }}</div>
+		<div class="info">{{ emoji.aliases.join(' ') }}</div>
+	</div>
+</button>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import VanillaTilt from 'vanilla-tilt';
+
+export default defineComponent({
+	props: {
+		emoji: {
+			type: Object,
+			required: true,
+		}
+	},
+
+	mounted() {
+		if (this.$store.animation) {
+			VanillaTilt.init(this.$el, {
+				reverse: true,
+				gyroscope: false,
+				scale: 1.1,
+				speed: 500,
+			});
+		}
+	},
+
+	methods: {
+		menu(ev) {
+			os.popupMenu([{
+				type: 'label',
+				text: ':' + this.emoji.name + ':',
+			}, {
+				text: this.$ts.copy,
+				icon: 'fas fa-copy',
+				action: () => {
+					copyToClipboard(`:${this.emoji.name}:`);
+					os.success();
+				}
+			}], ev.currentTarget || ev.target);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.zuvgdzyu {
+	display: flex;
+	align-items: center;
+	padding: 12px;
+	text-align: left;
+	background: var(--panel);
+	border-radius: 8px;
+	transform-style: preserve-3d;
+	transform: perspective(1000px);
+
+	&:hover {
+		border-color: var(--accent);
+	}
+
+	> .img {
+		width: 42px;
+		height: 42px;
+		transform: translateZ(20px);
+	}
+
+	> .body {
+		padding: 0 0 0 8px;
+		white-space: nowrap;
+		overflow: hidden;
+		transform: translateZ(10px);
+
+		> .name {
+			text-overflow: ellipsis;
+			overflow: hidden;
+		}
+
+		> .info {
+			opacity: 0.5;
+			font-size: 0.9em;
+			text-overflow: ellipsis;
+			overflow: hidden;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue
new file mode 100644
index 0000000000..ae06fa7938
--- /dev/null
+++ b/packages/client/src/pages/emojis.vue
@@ -0,0 +1,36 @@
+<template>
+<div :class="$style.root">
+	<XCategory v-if="tab === 'category'"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed } from 'vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import XCategory from './emojis.category.vue';
+
+export default defineComponent({
+	components: {
+		XCategory,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.$ts.customEmojis,
+				icon: 'fas fa-laugh',
+				bg: 'var(--bg)',
+			})),
+			tab: 'category',
+		}
+	},
+});
+</script>
+
+<style lang="scss" module>
+.root {
+	max-width: 1000px;
+	margin: 0 auto;
+}
+</style>
diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue
new file mode 100644
index 0000000000..7b1fcd0910
--- /dev/null
+++ b/packages/client/src/pages/explore.vue
@@ -0,0 +1,261 @@
+<template>
+<div>
+	<MkSpacer :content-max="1200">
+		<div class="lznhrdub">
+			<div v-if="tab === 'local'">
+				<div class="localfedi7 _block _isolated" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
+					<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header>
+					<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div>
+				</div>
+
+				<template v-if="tag == null">
+					<MkFolder class="_gap" persist-key="explore-pinned-users">
+						<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
+						<XUserList :pagination="pinnedUsers"/>
+					</MkFolder>
+					<MkFolder class="_gap" persist-key="explore-popular-users">
+						<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
+						<XUserList :pagination="popularUsers"/>
+					</MkFolder>
+					<MkFolder class="_gap" persist-key="explore-recently-updated-users">
+						<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
+						<XUserList :pagination="recentlyUpdatedUsers"/>
+					</MkFolder>
+					<MkFolder class="_gap" persist-key="explore-recently-registered-users">
+						<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
+						<XUserList :pagination="recentlyRegisteredUsers"/>
+					</MkFolder>
+				</template>
+			</div>
+			<div v-else-if="tab === 'remote'">
+				<div class="localfedi7 _block _isolated" v-if="tag == null" :style="{ backgroundImage: `url(/client-assets/fedi.jpg)` }">
+					<header><span>{{ $ts.exploreFediverse }}</span></header>
+				</div>
+
+				<MkFolder :foldable="true" :expanded="false" ref="tags" class="_gap">
+					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
+
+					<div class="vxjfqztj">
+						<MkA v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</MkA>
+						<MkA v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</MkA>
+					</div>
+				</MkFolder>
+
+				<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
+					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
+					<XUserList :pagination="tagUsers"/>
+				</MkFolder>
+
+				<template v-if="tag == null">
+					<MkFolder class="_gap">
+						<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
+						<XUserList :pagination="popularUsersF"/>
+					</MkFolder>
+					<MkFolder class="_gap">
+						<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
+						<XUserList :pagination="recentlyUpdatedUsersF"/>
+					</MkFolder>
+					<MkFolder class="_gap">
+						<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
+						<XUserList :pagination="recentlyRegisteredUsersF"/>
+					</MkFolder>
+				</template>
+			</div>
+			<div v-else-if="tab === 'search'">
+				<div class="_isolated">
+					<MkInput v-model="searchQuery" :debounce="true" type="search">
+						<template #prefix><i class="fas fa-search"></i></template>
+						<template #label>{{ $ts.searchUser }}</template>
+					</MkInput>
+					<MkRadios v-model="searchOrigin">
+						<option value="local">{{ $ts.local }}</option>
+						<option value="remote">{{ $ts.remote }}</option>
+						<option value="both">{{ $ts.all }}</option>
+					</MkRadios>
+				</div>
+
+				<XUserList v-if="searchQuery" class="_gap" :pagination="searchPagination" ref="search"/>
+			</div>
+		</div>
+	</MkSpacer>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import XUserList from '@/components/user-list.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkInput from '@/components/form/input.vue';
+import MkRadios from '@/components/form/radios.vue';
+import number from '@/filters/number';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XUserList,
+		MkFolder,
+		MkInput,
+		MkRadios,
+	},
+
+	props: {
+		tag: {
+			type: String,
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.$ts.explore,
+				icon: 'fas fa-hashtag',
+				bg: 'var(--bg)',
+				tabs: [{
+					active: this.tab === 'local',
+					title: this.$ts.local,
+					onClick: () => { this.tab = 'local'; },
+				}, {
+					active: this.tab === 'remote',
+					title: this.$ts.remote,
+					onClick: () => { this.tab = 'remote'; },
+				}, {
+					active: this.tab === 'search',
+					title: this.$ts.search,
+					onClick: () => { this.tab = 'search'; },
+				},]
+			})),
+			tab: 'local',
+			pinnedUsers: { endpoint: 'pinned-users' },
+			popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				state: 'alive',
+				origin: 'local',
+				sort: '+follower',
+			} },
+			recentlyUpdatedUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				origin: 'local',
+				sort: '+updatedAt',
+			} },
+			recentlyRegisteredUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				origin: 'local',
+				state: 'alive',
+				sort: '+createdAt',
+			} },
+			popularUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				state: 'alive',
+				origin: 'remote',
+				sort: '+follower',
+			} },
+			recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				origin: 'combined',
+				sort: '+updatedAt',
+			} },
+			recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
+				origin: 'combined',
+				sort: '+createdAt',
+			} },
+			searchPagination: {
+				endpoint: 'users/search',
+				limit: 10,
+				params: computed(() => (this.searchQuery && this.searchQuery !== '') ? {
+					query: this.searchQuery,
+					origin: this.searchOrigin,
+				} : null)
+			},
+			tagsLocal: [],
+			tagsRemote: [],
+			stats: null,
+			searchQuery: null,
+			searchOrigin: 'combined',
+			num: number,
+		};
+	},
+
+	computed: {
+		meta() {
+			return this.$instance;
+		},
+		tagUsers(): any {
+			return {
+				endpoint: 'hashtags/users',
+				limit: 30,
+				params: {
+					tag: this.tag,
+					origin: 'combined',
+					sort: '+follower',
+				}
+			};
+		},
+	},
+
+	watch: {
+		tag() {
+			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
+		},
+	},
+
+	created() {
+		os.api('hashtags/list', {
+			sort: '+attachedLocalUsers',
+			attachedToLocalUserOnly: true,
+			limit: 30
+		}).then(tags => {
+			this.tagsLocal = tags;
+		});
+		os.api('hashtags/list', {
+			sort: '+attachedRemoteUsers',
+			attachedToRemoteUserOnly: true,
+			limit: 30
+		}).then(tags => {
+			this.tagsRemote = tags;
+		});
+		os.api('stats').then(stats => {
+			this.stats = stats;
+		});
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.localfedi7 {
+	color: #fff;
+	padding: 16px;
+	height: 80px;
+	background-position: 50%;
+	background-size: cover;
+	margin-bottom: var(--margin);
+
+	> * {
+		&:not(:last-child) {
+			margin-bottom: 8px;
+		}
+
+		> span {
+			display: inline-block;
+			padding: 6px 8px;
+			background: rgba(0, 0, 0, 0.7);
+		}
+	}
+
+	> header {
+		font-size: 20px;
+		font-weight: bold;
+	}
+
+	> div {
+		font-size: 14px;
+		opacity: 0.8;
+	}
+}
+
+.vxjfqztj {
+	> * {
+		margin-right: 16px;
+
+		&.local {
+			font-weight: bold;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue
new file mode 100644
index 0000000000..980d59835f
--- /dev/null
+++ b/packages/client/src/pages/favorites.vue
@@ -0,0 +1,60 @@
+<template>
+<div class="jmelgwjh">
+	<div class="body">
+		<XNotes class="notes" :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XNotes from '@/components/notes.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.favorites,
+				icon: 'fas fa-star',
+				bg: 'var(--bg)',
+			},
+			pagination: {
+				endpoint: 'i/favorites',
+				limit: 10,
+				params: () => ({
+				})
+			},
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.jmelgwjh {
+	background: var(--bg);
+
+	> .body {
+		box-sizing: border-box;
+		max-width: 800px;
+		margin: 0 auto;
+		padding: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue
new file mode 100644
index 0000000000..f5edf25594
--- /dev/null
+++ b/packages/client/src/pages/featured.vue
@@ -0,0 +1,43 @@
+<template>
+<MkSpacer :content-max="800">
+	<XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/>
+</MkSpacer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XNotes from '@/components/notes.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.featured,
+				icon: 'fas fa-fire-alt',
+				bg: 'var(--bg)',
+			},
+			pagination: {
+				endpoint: 'notes/featured',
+				limit: 10,
+				offsetMode: true,
+			},
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue
new file mode 100644
index 0000000000..1bd5da58e3
--- /dev/null
+++ b/packages/client/src/pages/federation.vue
@@ -0,0 +1,265 @@
+<template>
+<div class="taeiyria">
+	<div class="query">
+		<MkInput v-model="host" :debounce="true" class="">
+			<template #prefix><i class="fas fa-search"></i></template>
+			<template #label>{{ $ts.host }}</template>
+		</MkInput>
+		<div class="_inputSplit">
+			<MkSelect v-model="state">
+				<template #label>{{ $ts.state }}</template>
+				<option value="all">{{ $ts.all }}</option>
+				<option value="federating">{{ $ts.federating }}</option>
+				<option value="subscribing">{{ $ts.subscribing }}</option>
+				<option value="publishing">{{ $ts.publishing }}</option>
+				<option value="suspended">{{ $ts.suspended }}</option>
+				<option value="blocked">{{ $ts.blocked }}</option>
+				<option value="notResponding">{{ $ts.notResponding }}</option>
+			</MkSelect>
+			<MkSelect v-model="sort">
+				<template #label>{{ $ts.sort }}</template>
+				<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
+				<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
+				<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
+				<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
+				<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
+				<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
+				<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
+				<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option>
+				<option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option>
+				<option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option>
+				<option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option>
+			</MkSelect>
+		</div>
+	</div>
+
+	<MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
+		<div class="dqokceoi">
+			<MkA class="instance" v-for="instance in items" :key="instance.id" :to="`/instance-info/${instance.host}`">
+				<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
+				<div class="table">
+					<div class="cell">
+						<div class="key">{{ $ts.registeredAt }}</div>
+						<div class="value"><MkTime :time="instance.caughtAt"/></div>
+					</div>
+					<div class="cell">
+						<div class="key">{{ $ts.software }}</div>
+						<div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div>
+					</div>
+					<div class="cell">
+						<div class="key">{{ $ts.version }}</div>
+						<div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div>
+					</div>
+					<div class="cell">
+						<div class="key">{{ $ts.users }}</div>
+						<div class="value">{{ instance.usersCount }}</div>
+					</div>
+					<div class="cell">
+						<div class="key">{{ $ts.notes }}</div>
+						<div class="value">{{ instance.notesCount }}</div>
+					</div>
+					<div class="cell">
+						<div class="key">{{ $ts.sent }}</div>
+						<div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
+					</div>
+					<div class="cell">
+						<div class="key">{{ $ts.received }}</div>
+						<div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
+					</div>
+				</div>
+				<div class="footer">
+					<span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span>
+					<span class="pubSub">
+						<span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span>
+						<span class="sub" v-else><i class="fas fa-caret-down icon"></i>-</span>
+						<span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span>
+						<span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span>
+					</span>
+					<span class="right">
+						<span class="latestStatus">{{ instance.latestStatus || '-' }}</span>
+						<span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span>
+					</span>
+				</div>
+			</MkA>
+		</div>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSelect,
+		MkPagination,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.federation,
+				icon: 'fas fa-globe',
+				bg: 'var(--bg)',
+			},
+			host: '',
+			state: 'federating',
+			sort: '+pubSub',
+			pagination: {
+				endpoint: 'federation/instances',
+				limit: 10,
+				offsetMode: true,
+				params: () => ({
+					sort: this.sort,
+					host: this.host != '' ? this.host : null,
+					...(
+						this.state === 'federating' ? { federating: true } :
+						this.state === 'subscribing' ? { subscribing: true } :
+						this.state === 'publishing' ? { publishing: true } :
+						this.state === 'suspended' ? { suspended: true } :
+						this.state === 'blocked' ? { blocked: true } :
+						this.state === 'notResponding' ? { notResponding: true } :
+						{})
+				})
+			},
+		}
+	},
+
+	watch: {
+		host() {
+			this.$refs.instances.reload();
+		},
+		state() {
+			this.$refs.instances.reload();
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		getStatus(instance) {
+			if (instance.isSuspended) return 'suspended';
+			if (instance.isNotResponding) return 'error';
+			return 'alive';
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.taeiyria {
+	> .query {
+		background: var(--bg);
+		padding: 16px;
+	}
+}
+
+.dqokceoi {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
+	grid-gap: 12px;
+	padding: 16px;
+
+	> .instance {
+		padding: 16px;
+		border: solid 1px var(--divider);
+		border-radius: 6px;
+
+		&:hover {
+			border: solid 1px var(--accent);
+			text-decoration: none;
+		}
+
+		> .host {
+			font-weight: bold;
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+
+			> img {
+				width: 18px;
+				height: 18px;
+				margin-right: 6px;
+				vertical-align: middle;
+			}
+		}
+
+		> .table {
+			display: grid;
+			grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
+			grid-gap: 6px;
+			margin: 6px 0;
+			font-size: 70%;
+
+			> .cell {
+				> .key, > .value {
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+				}
+
+				> .key {
+					opacity: 0.7;
+				}
+
+				> .value {
+				}
+			}
+		}
+
+		> .footer {
+			display: flex;
+			align-items: center;
+			font-size: 0.9em;
+
+			> .status {
+				&.suspended {
+					opacity: 0.5;
+				}
+
+				&.error {
+					color: var(--error);
+				}
+
+				&.alive {
+					color: var(--success);
+				}
+			}
+
+			> .pubSub {
+				margin-left: 8px;
+			}
+
+			> .right {
+				margin-left: auto;
+
+				> .latestStatus {
+					border: solid 1px var(--divider);
+					border-radius: 4px;
+					margin: 0 8px;
+					padding: 0 4px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
new file mode 100644
index 0000000000..d8967dc9d9
--- /dev/null
+++ b/packages/client/src/pages/follow-requests.vue
@@ -0,0 +1,153 @@
+<template>
+<div>
+	<MkPagination :pagination="pagination" class="mk-follow-requests" ref="list">
+		<template #empty>
+			<div class="_fullinfo">
+				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+				<div>{{ $ts.noFollowRequests }}</div>
+			</div>
+		</template>
+		<template #default="{items}">
+			<div class="user _panel" v-for="req in items" :key="req.id">
+				<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
+				<div class="body">
+					<div class="name">
+						<MkA class="name" :to="userPage(req.follower)" v-user-preview="req.follower.id"><MkUserName :user="req.follower"/></MkA>
+						<p class="acct">@{{ acct(req.follower) }}</p>
+					</div>
+					<div class="description" v-if="req.follower.description" :title="req.follower.description">
+						<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
+					</div>
+					<div class="actions">
+						<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
+						<button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
+					</div>
+				</div>
+			</div>
+		</template>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import { userPage, acct } from '@/filters/user';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkPagination
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.followRequests,
+				icon: 'fas fa-user-clock',
+			},
+			pagination: {
+				endpoint: 'following/requests/list',
+				limit: 10,
+			},
+		};
+	},
+
+	methods: {
+		accept(user) {
+			os.api('following/requests/accept', { userId: user.id }).then(() => {
+				this.$refs.list.reload();
+			});
+		},
+		reject(user) {
+			os.api('following/requests/reject', { userId: user.id }).then(() => {
+				this.$refs.list.reload();
+			});
+		},
+		userPage,
+		acct
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-follow-requests {
+	> .user {
+		display: flex;
+		padding: 16px;
+
+		> .avatar {
+			display: block;
+			flex-shrink: 0;
+			margin: 0 12px 0 0;
+			width: 42px;
+			height: 42px;
+			border-radius: 8px;
+		}
+
+		> .body {
+			display: flex;
+			width: calc(100% - 54px);
+			position: relative;
+
+			> .name {
+				width: 45%;
+
+				@media (max-width: 500px) {
+					width: 100%;
+				}
+
+				> .name,
+				> .acct {
+					display: block;
+					white-space: nowrap;
+					text-overflow: ellipsis;
+					overflow: hidden;
+					margin: 0;
+				}
+
+				> .name {
+					font-size: 16px;
+					line-height: 24px;
+				}
+
+				> .acct {
+					font-size: 15px;
+					line-height: 16px;
+					opacity: 0.7;
+				}
+			}
+
+			> .description {
+				width: 55%;
+				line-height: 42px;
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				opacity: 0.7;
+				font-size: 14px;
+				padding-right: 40px;
+				padding-left: 8px;
+				box-sizing: border-box;
+
+				@media (max-width: 500px) {
+					display: none;
+				}
+			}
+
+			> .actions {
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				right: 0;
+				margin: auto 0;
+
+				> button {
+					padding: 12px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/follow.vue b/packages/client/src/pages/follow.vue
new file mode 100644
index 0000000000..e8eaad73bf
--- /dev/null
+++ b/packages/client/src/pages/follow.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="mk-follow-page">
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import * as Acct from 'misskey-js/built/acct';
+
+export default defineComponent({
+	created() {
+		const acct = new URL(location.href).searchParams.get('acct');
+		if (acct == null) return;
+
+		let promise;
+
+		if (acct.startsWith('https://')) {
+			promise = os.api('ap/show', {
+				uri: acct
+			});
+			promise.then(res => {
+				if (res.type == 'User') {
+					this.follow(res.object);
+				} else if (res.type === 'Note') {
+					this.$router.push(`/notes/${res.object.id}`);
+				} else {
+					os.dialog({
+						type: 'error',
+						text: 'Not a user'
+					}).then(() => {
+						window.close();
+					});
+				}
+			});
+		} else {
+			promise = os.api('users/show', Acct.parse(acct));
+			promise.then(user => {
+				this.follow(user);
+			});
+		}
+
+		os.promiseDialog(promise, null, null, this.$ts.fetchingAsApObject);
+	},
+
+	methods: {
+		async follow(user) {
+			const { canceled } = await os.dialog({
+				type: 'question',
+				text: this.$t('followConfirm', { name: user.name || user.username }),
+				showCancelButton: true
+			});
+
+			if (canceled) {
+				window.close();
+				return;
+			}
+			
+			os.apiWithDialog('following/create', {
+				userId: user.id
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue
new file mode 100644
index 0000000000..1ee3a9390b
--- /dev/null
+++ b/packages/client/src/pages/gallery/edit.vue
@@ -0,0 +1,168 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormInput v-model="title">
+			<span>{{ $ts.title }}</span>
+		</FormInput>
+
+		<FormTextarea v-model="description" :max="500">
+			<span>{{ $ts.description }}</span>
+		</FormTextarea>
+
+		<FormGroup>
+			<div v-for="file in files" :key="file.id" class="_debobigegoItem _debobigegoPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
+				<div class="name">{{ file.name }}</div>
+				<button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button>
+			</div>
+			<FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
+		</FormGroup>
+
+		<FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
+
+		<FormButton v-if="postId" @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+		<FormButton v-else @click="save" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
+
+		<FormButton v-if="postId" @click="del" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormTuple from '@/components/debobigego/tuple.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import { selectFile } from '@/scripts/select-file';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormButton,
+		FormInput,
+		FormTextarea,
+		FormSwitch,
+		FormBase,
+		FormGroup,
+		FormSuspense,
+	},
+
+	props: {
+		postId: {
+			type: String,
+			required: false,
+			default: null,
+		}
+	},
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.postId ? {
+				title: this.$ts.edit,
+				icon: 'fas fa-pencil-alt'
+			} : {
+				title: this.$ts.postToGallery,
+				icon: 'fas fa-pencil-alt'
+			}),
+			init: null,
+			files: [],
+			description: null,
+			title: null,
+			isSensitive: false,
+		}
+	},
+
+	watch: {
+		postId: {
+			handler() {
+				this.init = () => this.postId ? os.api('gallery/posts/show', {
+					postId: this.postId
+				}).then(post => {
+					this.files = post.files;
+					this.title = post.title;
+					this.description = post.description;
+					this.isSensitive = post.isSensitive;
+				}) : Promise.resolve(null);
+			},
+			immediate: true,
+		}
+	},
+
+	methods: {
+		selectFile(e) {
+			selectFile(e.currentTarget || e.target, null, true).then(files => {
+				this.files = this.files.concat(files);
+			});
+		},
+
+		remove(file) {
+			this.files = this.files.filter(f => f.id !== file.id);
+		},
+
+		async save() {
+			if (this.postId) {
+				await os.apiWithDialog('gallery/posts/update', {
+					postId: this.postId,
+					title: this.title,
+					description: this.description,
+					fileIds: this.files.map(file => file.id),
+					isSensitive: this.isSensitive,
+				});
+				this.$router.push(`/gallery/${this.postId}`);
+			} else {
+				const post = await os.apiWithDialog('gallery/posts/create', {
+					title: this.title,
+					description: this.description,
+					fileIds: this.files.map(file => file.id),
+					isSensitive: this.isSensitive,
+				});
+				this.$router.push(`/gallery/${post.id}`);
+			}
+		},
+
+		async del() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$ts.deleteConfirm,
+				showCancelButton: true
+			});
+			if (canceled) return;
+			await os.apiWithDialog('gallery/posts/delete', {
+				postId: this.postId,
+			});
+			this.$router.push(`/gallery`);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wqugxsfx {
+	height: 200px;
+	background-size: contain;
+	background-position: center;
+	background-repeat: no-repeat;
+	position: relative;
+
+	> .name {
+		position: absolute;
+		top: 8px;
+		left: 9px;
+		padding: 8px;
+		background: var(--panel);
+	}
+
+	> .remove {
+		position: absolute;
+		top: 8px;
+		right: 9px;
+		padding: 8px;
+		background: var(--panel);
+	}
+}
+</style>
diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue
new file mode 100644
index 0000000000..dfcd59349e
--- /dev/null
+++ b/packages/client/src/pages/gallery/index.vue
@@ -0,0 +1,152 @@
+<template>
+<div class="xprsixdl _root">
+	<MkTab v-model="tab" v-if="$i">
+		<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
+		<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
+		<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
+	</MkTab>
+
+	<div v-if="tab === 'explore'">
+		<MkFolder class="_gap">
+			<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
+			<MkPagination :pagination="recentPostsPagination" #default="{items}" :disable-auto-load="true">
+				<div class="vfpdbgtk">
+					<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
+				</div>
+			</MkPagination>
+		</MkFolder>
+		<MkFolder class="_gap">
+			<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
+			<MkPagination :pagination="popularPostsPagination" #default="{items}" :disable-auto-load="true">
+				<div class="vfpdbgtk">
+					<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
+				</div>
+			</MkPagination>
+		</MkFolder>
+	</div>
+	<div v-else-if="tab === 'liked'">
+		<MkPagination :pagination="likedPostsPagination" #default="{items}">
+			<div class="vfpdbgtk">
+				<MkGalleryPostPreview v-for="like in items" :post="like.post" :key="like.id" class="post"/>
+			</div>
+		</MkPagination>
+	</div>
+	<div v-else-if="tab === 'my'">
+		<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
+		<MkPagination :pagination="myPostsPagination" #default="{items}">
+			<div class="vfpdbgtk">
+				<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
+			</div>
+		</MkPagination>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import XUserList from '@/components/user-list.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkInput from '@/components/form/input.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkTab from '@/components/tab.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
+import number from '@/filters/number';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XUserList,
+		MkFolder,
+		MkInput,
+		MkButton,
+		MkTab,
+		MkPagination,
+		MkGalleryPostPreview,
+	},
+
+	props: {
+		tag: {
+			type: String,
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.gallery,
+				icon: 'fas fa-icons'
+			},
+			tab: 'explore',
+			recentPostsPagination: {
+				endpoint: 'gallery/posts',
+				limit: 6,
+			},
+			popularPostsPagination: {
+				endpoint: 'gallery/featured',
+				limit: 5,
+			},
+			myPostsPagination: {
+				endpoint: 'i/gallery/posts',
+				limit: 5,
+			},
+			likedPostsPagination: {
+				endpoint: 'i/gallery/likes',
+				limit: 5,
+			},
+			tags: [],
+		};
+	},
+
+	computed: {
+		meta() {
+			return this.$instance;
+		},
+		tagUsers(): any {
+			return {
+				endpoint: 'hashtags/users',
+				limit: 30,
+				params: {
+					tag: this.tag,
+					origin: 'combined',
+					sort: '+follower',
+				}
+			};
+		},
+	},
+
+	watch: {
+		tag() {
+			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
+		},
+	},
+
+	created() {
+
+	},
+
+	methods: {
+
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.xprsixdl {
+	max-width: 1400px;
+	margin: 0 auto;
+}
+
+.vfpdbgtk {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+	grid-gap: 12px;
+	margin: 0 var(--margin);
+
+	> .post {
+
+	}
+}
+</style>
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
new file mode 100644
index 0000000000..255954def0
--- /dev/null
+++ b/packages/client/src/pages/gallery/post.vue
@@ -0,0 +1,282 @@
+<template>
+<div class="_root">
+	<transition name="fade" mode="out-in">
+		<div v-if="post" class="rkxwuolj">
+			<div class="files">
+				<div class="file" v-for="file in post.files" :key="file.id">
+					<img :src="file.url"/>
+				</div>
+			</div>
+			<div class="body _block">
+				<div class="title">{{ post.title }}</div>
+				<div class="description"><Mfm :text="post.description"/></div>
+				<div class="info">
+					<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
+				</div>
+				<div class="actions">
+					<div class="like">
+						<MkButton class="button" @click="unlike()" v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton>
+						<MkButton class="button" @click="like()" v-else v-tooltip="$ts._gallery.like"><i class="far fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton>
+					</div>
+					<div class="other">
+						<button v-if="$i && $i.id === post.user.id" class="_button" @click="edit" v-tooltip="$ts.edit" v-click-anime><i class="fas fa-pencil-alt fa-fw"></i></button>
+						<button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
+						<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
+					</div>
+				</div>
+				<div class="user">
+					<MkAvatar :user="post.user" class="avatar"/>
+					<div class="name">
+						<MkUserName :user="post.user" style="display: block;"/>
+						<MkAcct :user="post.user"/>
+					</div>
+					<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
+				</div>
+			</div>
+			<MkAd :prefer="['horizontal', 'horizontal-big']"/>
+			<MkContainer :max-height="300" :foldable="true" class="other">
+				<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
+				<MkPagination :pagination="otherPostsPagination" #default="{items}">
+					<div class="sdrarzaf">
+						<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
+					</div>
+				</MkPagination>
+			</MkContainer>
+		</div>
+		<MkError v-else-if="error" @retry="fetch()"/>
+		<MkLoading v-else/>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import MkContainer from '@/components/ui/container.vue';
+import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
+import MkFollowButton from '@/components/follow-button.vue';
+import { url } from '@/config';
+
+export default defineComponent({
+	components: {
+		MkContainer,
+		ImgWithBlurhash,
+		MkPagination,
+		MkGalleryPostPreview,
+		MkButton,
+		MkFollowButton,
+	},
+	props: {
+		postId: {
+			type: String,
+			required: true
+		}
+	},
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.post ? {
+				title: this.post.title,
+				avatar: this.post.user,
+				path: `/gallery/${this.post.id}`,
+				share: {
+					title: this.post.title,
+					text: this.post.description,
+				},
+				actions: [{
+					icon: 'fas fa-pencil-alt',
+					text: this.$ts.edit,
+					handler: this.edit
+				}]
+			} : null),
+			otherPostsPagination: {
+				endpoint: 'users/gallery/posts',
+				limit: 6,
+				params: () => ({
+					userId: this.post.user.id
+				})
+			},
+			post: null,
+			error: null,
+		};
+	},
+
+	watch: {
+		postId: 'fetch'
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			this.post = null;
+			os.api('gallery/posts/show', {
+				postId: this.postId
+			}).then(post => {
+				this.post = post;
+			}).catch(e => {
+				this.error = e;
+			});
+		},
+
+		share() {
+			navigator.share({
+				title: this.post.title,
+				text: this.post.description,
+				url: `${url}/gallery/${this.post.id}`
+			});
+		},
+
+		shareWithNote() {
+			os.post({
+				initialText: `${this.post.title} ${url}/gallery/${this.post.id}`
+			});
+		},
+
+		like() {
+			os.apiWithDialog('gallery/posts/like', {
+				postId: this.postId,
+			}).then(() => {
+				this.post.isLiked = true;
+				this.post.likedCount++;
+			});
+		},
+
+		async unlike() {
+			const confirm = await os.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				text: this.$ts.unlikeConfirm,
+			});
+			if (confirm.canceled) return;
+			os.apiWithDialog('gallery/posts/unlike', {
+				postId: this.postId,
+			}).then(() => {
+				this.post.isLiked = false;
+				this.post.likedCount--;
+			});
+		},
+
+		edit() {
+			this.$router.push(`/gallery/${this.post.id}/edit`);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.rkxwuolj {
+	> .files {
+		> .file {
+			> img {
+				display: block;
+				max-width: 100%;
+				max-height: 500px;
+				margin: 0 auto;
+			}
+
+			& + .file {
+				margin-top: 16px;
+			}
+		}
+	}
+
+	> .body {
+		padding: 32px;
+
+		> .title {
+			font-weight: bold;
+			font-size: 1.2em;
+			margin-bottom: 16px;
+		}
+
+		> .info {
+			margin-top: 16px;
+			font-size: 90%;
+			opacity: 0.7;
+		}
+
+		> .actions {
+			display: flex;
+			align-items: center;
+			margin-top: 16px;
+			padding: 16px 0 0 0;
+			border-top: solid 0.5px var(--divider);
+
+			> .like {
+				> .button {
+					--accent: rgb(241 97 132);
+					--X8: rgb(241 92 128);
+					--buttonBg: rgb(216 71 106 / 5%);
+					--buttonHoverBg: rgb(216 71 106 / 10%);
+					color: #ff002f;
+
+					::v-deep(.count) {
+						margin-left: 0.5em;
+					}
+				}
+			}
+
+			> .other {
+				margin-left: auto;
+
+				> button {
+					padding: 8px;
+					margin: 0 8px;
+
+					&:hover {
+						color: var(--fgHighlighted);
+					}
+				}
+			}
+		}
+
+		> .user {
+			margin-top: 16px;
+			padding: 16px 0 0 0;
+			border-top: solid 0.5px var(--divider);
+			display: flex;
+			align-items: center;
+
+			> .avatar {
+				width: 52px;
+				height: 52px;
+			}
+
+			> .name {
+				margin: 0 0 0 12px;
+				font-size: 90%;
+			}
+
+			> .koudoku {
+				margin-left: auto;
+			}
+		}
+	}
+}
+
+.sdrarzaf {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+	grid-gap: 12px;
+	margin: var(--margin);
+
+	> .post {
+
+	}
+}
+</style>
diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
new file mode 100644
index 0000000000..586d9d7e52
--- /dev/null
+++ b/packages/client/src/pages/instance-info.vue
@@ -0,0 +1,238 @@
+<template>
+<FormBase>
+	<FormGroup v-if="instance">
+		<template #label>{{ instance.host }}</template>
+		<FormGroup>
+			<div class="_debobigegoItem">
+				<div class="_debobigegoPanel fnfelxur">
+					<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
+				</div>
+			</div>
+			<FormKeyValueView>
+				<template #key>Name</template>
+				<template #value><span class="_monospace">{{ instance.name || `(${$ts.unknown})` }}</span></template>
+			</FormKeyValueView>
+		</FormGroup>
+
+		<FormButton v-if="$i.isAdmin || $i.isModerator" @click="info" primary>{{ $ts.settings }}</FormButton>
+
+		<FormTextarea readonly :value="instance.description">
+			<span>{{ $ts.description }}</span>
+		</FormTextarea>
+
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>{{ $ts.software }}</template>
+				<template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }}</span></template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>{{ $ts.version }}</template>
+				<template #value><span class="_monospace">{{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template>
+			</FormKeyValueView>
+		</FormGroup>
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>{{ $ts.administrator }}</template>
+				<template #value><span class="_monospace">{{ instance.maintainerName || `(${$ts.unknown})` }}</span></template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>{{ $ts.contact }}</template>
+				<template #value><span class="_monospace">{{ instance.maintainerEmail || `(${$ts.unknown})` }}</span></template>
+			</FormKeyValueView>
+		</FormGroup>
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>{{ $ts.latestRequestSentAt }}</template>
+				<template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>{{ $ts.latestStatus }}</template>
+				<template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>{{ $ts.latestRequestReceivedAt }}</template>
+				<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
+			</FormKeyValueView>
+		</FormGroup>
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>Open Registrations</template>
+				<template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template>
+			</FormKeyValueView>
+		</FormGroup>
+		<div class="_debobigegoItem">
+			<div class="_debobigegoLabel">{{ $ts.statistics }}</div>
+			<div class="_debobigegoPanel cmhjzshl">
+				<div class="selects">
+					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
+						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
+						<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
+						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
+						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
+						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
+						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
+						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
+						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
+						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
+						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
+						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
+					</MkSelect>
+					<MkSelect v-model="chartSpan" style="margin: 0;">
+						<option value="hour">{{ $ts.perHour }}</option>
+						<option value="day">{{ $ts.perDay }}</option>
+					</MkSelect>
+				</div>
+				<div class="chart">
+					<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
+				</div>
+			</div>
+		</div>
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>{{ $ts.registeredAt }}</template>
+				<template #value><MkTime mode="detail" :time="instance.caughtAt"/></template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>{{ $ts.updatedAt }}</template>
+				<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
+			</FormKeyValueView>
+		</FormGroup>
+		<FormObjectView tall :value="instance">
+			<span>Raw</span>
+		</FormObjectView>
+		<FormGroup>
+			<template #label>Well-known resources</template>
+			<FormLink :to="`https://${host}/.well-known/host-meta`" external>host-meta</FormLink>
+			<FormLink :to="`https://${host}/.well-known/host-meta.json`" external>host-meta.json</FormLink>
+			<FormLink :to="`https://${host}/.well-known/nodeinfo`" external>nodeinfo</FormLink>
+			<FormLink :to="`https://${host}/robots.txt`" external>robots.txt</FormLink>
+			<FormLink :to="`https://${host}/manifest.json`" external>manifest.json</FormLink>
+		</FormGroup>
+		<FormSuspense :p="dnsPromiseFactory" v-slot="{ result: dns }">
+			<FormGroup>
+				<template #label>DNS</template>
+				<FormKeyValueView v-for="record in dns.a" :key="record">
+					<template #key>A</template>
+					<template #value><span class="_monospace">{{ record }}</span></template>
+				</FormKeyValueView>
+				<FormKeyValueView v-for="record in dns.aaaa" :key="record">
+					<template #key>AAAA</template>
+					<template #value><span class="_monospace">{{ record }}</span></template>
+				</FormKeyValueView>
+				<FormKeyValueView v-for="record in dns.cname" :key="record">
+					<template #key>CNAME</template>
+					<template #value><span class="_monospace">{{ record }}</span></template>
+				</FormKeyValueView>
+				<FormKeyValueView v-for="record in dns.txt">
+					<template #key>TXT</template>
+					<template #value><span class="_monospace">{{ record[0] }}</span></template>
+				</FormKeyValueView>
+			</FormGroup>
+		</FormSuspense>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import MkChart from '@/components/chart.vue';
+import FormObjectView from '@/components/debobigego/object-view.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import MkSelect from '@/components/form/select.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import bytes from '@/filters/bytes';
+import * as symbols from '@/symbols';
+import MkInstanceInfo from '@/pages/admin/instance.vue';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormTextarea,
+		FormObjectView,
+		FormButton,
+		FormLink,
+		FormGroup,
+		FormKeyValueView,
+		FormSuspense,
+		MkSelect,
+		MkChart,
+	},
+
+	props: {
+		host: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.instanceInfo,
+				icon: 'fas fa-info-circle',
+				actions: [{
+					text: `https://${this.host}`,
+					icon: 'fas fa-external-link-alt',
+					handler: () => {
+						window.open(`https://${this.host}`, '_blank');
+					}
+				}],
+			},
+			instance: null,
+			dnsPromiseFactory: () => os.api('federation/dns', {
+				host: this.host
+			}),
+			chartSrc: 'instance-requests',
+			chartSpan: 'hour',
+		}
+	},
+
+	mounted() {
+		this.fetch();
+	},
+
+	methods: {
+		number,
+		bytes,
+
+		async fetch() {
+			this.instance = await os.api('federation/show-instance', {
+				host: this.host
+			});
+		},
+
+		info() {
+			os.popup(MkInstanceInfo, {
+				instance: this.instance
+			}, {}, 'closed');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fnfelxur {
+	padding: 16px;
+
+	> .icon {
+		display: block;
+		margin: auto;
+		height: 64px;
+		border-radius: 8px;
+	}
+}
+
+.cmhjzshl {
+	> .selects {
+		display: flex;
+		padding: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue
new file mode 100644
index 0000000000..cd9c6a8fdf
--- /dev/null
+++ b/packages/client/src/pages/mentions.vue
@@ -0,0 +1,42 @@
+<template>
+<MkSpacer :content-max="800">
+	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
+</MkSpacer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XNotes from '@/components/notes.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.mentions,
+				icon: 'fas fa-at',
+				bg: 'var(--bg)',
+			},
+			pagination: {
+				endpoint: 'notes/mentions',
+				limit: 10,
+			},
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue
new file mode 100644
index 0000000000..9fde0bc7d5
--- /dev/null
+++ b/packages/client/src/pages/messages.vue
@@ -0,0 +1,45 @@
+<template>
+<MkSpacer :content-max="800">
+	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
+</MkSpacer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XNotes from '@/components/notes.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.directNotes,
+				icon: 'fas fa-envelope',
+				bg: 'var(--bg)',
+			},
+			pagination: {
+				endpoint: 'notes/mentions',
+				limit: 10,
+				params: () => ({
+					visibility: 'specified'
+				})
+			},
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue
new file mode 100644
index 0000000000..896c3927ce
--- /dev/null
+++ b/packages/client/src/pages/messaging/index.vue
@@ -0,0 +1,307 @@
+<template>
+<MkSpacer :content-max="800">
+	<div class="yweeujhr" v-size="{ max: [400] }">
+		<MkButton @click="start" primary class="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton>
+
+		<div class="history" v-if="messages.length > 0">
+			<MkA v-for="(message, i) in messages"
+				class="message _block"
+				:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
+				:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
+				:data-index="i"
+				:key="message.id"
+				v-anim="i"
+			>
+				<div>
+					<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/>
+					<header v-if="message.groupId">
+						<span class="name">{{ message.group.name }}</span>
+						<MkTime :time="message.createdAt" class="time"/>
+					</header>
+					<header v-else>
+						<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
+						<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
+						<MkTime :time="message.createdAt" class="time"/>
+					</header>
+					<div class="body">
+						<p class="text"><span class="me" v-if="isMe(message)">{{ $ts.you }}:</span>{{ message.text }}</p>
+					</div>
+				</div>
+			</MkA>
+		</div>
+		<div class="_fullinfo" v-if="!fetching && messages.length == 0">
+			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+			<div>{{ $ts.noHistory }}</div>
+		</div>
+		<MkLoading v-if="fetching"/>
+	</div>
+</MkSpacer>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
+import MkButton from '@/components/ui/button.vue';
+import { acct } from '@/filters/user';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.messaging,
+				icon: 'fas fa-comments',
+				bg: 'var(--bg)',
+			},
+			fetching: true,
+			moreFetching: false,
+			messages: [],
+			connection: null,
+		};
+	},
+
+	mounted() {
+		this.connection = markRaw(os.stream.useChannel('messagingIndex'));
+
+		this.connection.on('message', this.onMessage);
+		this.connection.on('read', this.onRead);
+
+		os.api('messaging/history', { group: false }).then(userMessages => {
+			os.api('messaging/history', { group: true }).then(groupMessages => {
+				const messages = userMessages.concat(groupMessages);
+				messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
+				this.messages = messages;
+				this.fetching = false;
+			});
+		});
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		getAcct: Acct.toString,
+
+		isMe(message) {
+			return message.userId == this.$i.id;
+		},
+
+		onMessage(message) {
+			if (message.recipientId) {
+				this.messages = this.messages.filter(m => !(
+					(m.recipientId == message.recipientId && m.userId == message.userId) ||
+					(m.recipientId == message.userId && m.userId == message.recipientId)));
+
+				this.messages.unshift(message);
+			} else if (message.groupId) {
+				this.messages = this.messages.filter(m => m.groupId !== message.groupId);
+				this.messages.unshift(message);
+			}
+		},
+
+		onRead(ids) {
+			for (const id of ids) {
+				const found = this.messages.find(m => m.id == id);
+				if (found) {
+					if (found.recipientId) {
+						found.isRead = true;
+					} else if (found.groupId) {
+						found.reads.push(this.$i.id);
+					}
+				}
+			}
+		},
+
+		start(ev) {
+			os.popupMenu([{
+				text: this.$ts.messagingWithUser,
+				icon: 'fas fa-user',
+				action: () => { this.startUser() }
+			}, {
+				text: this.$ts.messagingWithGroup,
+				icon: 'fas fa-users',
+				action: () => { this.startGroup() }
+			}], ev.currentTarget || ev.target);
+		},
+
+		async startUser() {
+			os.selectUser().then(user => {
+				this.$router.push(`/my/messaging/${Acct.toString(user)}`);
+			});
+		},
+
+		async startGroup() {
+			const groups1 = await os.api('users/groups/owned');
+			const groups2 = await os.api('users/groups/joined');
+			if (groups1.length === 0 && groups2.length === 0) {
+				os.dialog({
+					type: 'warning',
+					title: this.$ts.youHaveNoGroups,
+					text: this.$ts.joinOrCreateGroup,
+				});
+				return;
+			}
+			const { canceled, result: group } = await os.dialog({
+				type: null,
+				title: this.$ts.group,
+				select: {
+					items: groups1.concat(groups2).map(group => ({
+						value: group, text: group.name
+					}))
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			this.$router.push(`/my/messaging/group/${group.id}`);
+		},
+
+		acct
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.yweeujhr {
+
+	> .start {
+		margin: 0 auto var(--margin) auto;
+	}
+
+	> .history {
+		> .message {
+			display: block;
+			text-decoration: none;
+			margin-bottom: var(--margin);
+
+			* {
+				pointer-events: none;
+				user-select: none;
+			}
+
+			&:hover {
+				.avatar {
+					filter: saturate(200%);
+				}
+			}
+
+			&:active {
+			}
+
+			&.isRead,
+			&.isMe {
+				opacity: 0.8;
+			}
+
+			&:not(.isMe):not(.isRead) {
+				> div {
+					background-image: url("/client-assets/unread.svg");
+					background-repeat: no-repeat;
+					background-position: 0 center;
+				}
+			}
+
+			&:after {
+				content: "";
+				display: block;
+				clear: both;
+			}
+
+			> div {
+				padding: 20px 30px;
+
+				&:after {
+					content: "";
+					display: block;
+					clear: both;
+				}
+
+				> header {
+					display: flex;
+					align-items: center;
+					margin-bottom: 2px;
+					white-space: nowrap;
+					overflow: hidden;
+
+					> .name {
+						margin: 0;
+						padding: 0;
+						overflow: hidden;
+						text-overflow: ellipsis;
+						font-size: 1em;
+						font-weight: bold;
+						transition: all 0.1s ease;
+					}
+
+					> .username {
+						margin: 0 8px;
+					}
+
+					> .time {
+						margin: 0 0 0 auto;
+					}
+				}
+
+				> .avatar {
+					float: left;
+					width: 54px;
+					height: 54px;
+					margin: 0 16px 0 0;
+					border-radius: 8px;
+					transition: all 0.1s ease;
+				}
+
+				> .body {
+
+					> .text {
+						display: block;
+						margin: 0 0 0 0;
+						padding: 0;
+						overflow: hidden;
+						overflow-wrap: break-word;
+						font-size: 1.1em;
+						color: var(--faceText);
+
+						.me {
+							opacity: 0.7;
+						}
+					}
+
+					> .image {
+						display: block;
+						max-width: 100%;
+						max-height: 512px;
+					}
+				}
+			}
+		}
+	}
+
+	&.max-width_400px {
+		> .history {
+			> .message {
+				&:not(.isMe):not(.isRead) {
+					> div {
+						background-image: none;
+						border-left: solid 4px #3aa2dc;
+					}
+				}
+
+				> div {
+					padding: 16px;
+					font-size: 0.9em;
+
+					> .avatar {
+						margin: 0 12px 0 0;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue
new file mode 100644
index 0000000000..aafed2632d
--- /dev/null
+++ b/packages/client/src/pages/messaging/messaging-room.form.vue
@@ -0,0 +1,348 @@
+<template>
+<div class="pemppnzi _block"
+	@dragover.stop="onDragover"
+	@drop.stop="onDrop"
+>
+	<textarea
+		v-model="text"
+		ref="text"
+		@keypress="onKeypress"
+		@compositionupdate="onCompositionUpdate"
+		@paste="onPaste"
+		:placeholder="$ts.inputMessageHere"
+	></textarea>
+	<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
+	<button class="send _button" @click="send" :disabled="!canSend || sending" :title="$ts.send">
+		<template v-if="!sending"><i class="fas fa-paper-plane"></i></template><template v-if="sending"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>
+	</button>
+	<button class="_button" @click="chooseFile"><i class="fas fa-photo-video"></i></button>
+	<button class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
+	<input ref="file" type="file" @change="onChangeFile"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import insertTextAtCursor from 'insert-text-at-cursor';
+import * as autosize from 'autosize';
+import { formatTimeString } from '@/scripts/format-time-string';
+import { selectFile } from '@/scripts/select-file';
+import * as os from '@/os';
+import { Autocomplete } from '@/scripts/autocomplete';
+import { throttle } from 'throttle-debounce';
+
+export default defineComponent({
+	props: {
+		user: {
+			type: Object,
+			requird: false,
+		},
+		group: {
+			type: Object,
+			requird: false,
+		},
+	},
+	data() {
+		return {
+			text: null,
+			file: null,
+			sending: false,
+			typing: throttle(3000, () => {
+				os.stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id });
+			}),
+		};
+	},
+	computed: {
+		draftKey(): string {
+			return this.user ? 'user:' + this.user.id : 'group:' + this.group.id;
+		},
+		canSend(): boolean {
+			return (this.text != null && this.text != '') || this.file != null;
+		},
+		room(): any {
+			return this.$parent;
+		}
+	},
+	watch: {
+		text() {
+			this.saveDraft();
+		},
+		file() {
+			this.saveDraft();
+		}
+	},
+	mounted() {
+		autosize(this.$refs.text);
+
+		// TODO: detach when unmount
+		new Autocomplete(this.$refs.text, this, { model: 'text' });
+
+		// 書きかけの投稿を復元
+		const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
+		if (draft) {
+			this.text = draft.data.text;
+			this.file = draft.data.file;
+		}
+	},
+	methods: {
+		async onPaste(e: ClipboardEvent) {
+			const data = e.clipboardData;
+			const items = data.items;
+
+			if (items.length == 1) {
+				if (items[0].kind == 'file') {
+					const file = items[0].getAsFile();
+					const lio = file.name.lastIndexOf('.');
+					const ext = lio >= 0 ? file.name.slice(lio) : '';
+					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
+					const name = this.$store.state.pasteDialog
+						? await os.dialog({
+							title: this.$ts.enterFileName,
+							input: {
+								default: formatted
+							},
+							allowEmpty: false
+						}).then(({ canceled, result }) => canceled ? false : result)
+						: formatted;
+					if (name) this.upload(file, name);
+				}
+			} else {
+				if (items[0].kind == 'file') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.onlyOneFileCanBeAttached
+					});
+				}
+			}
+		},
+
+		onDragover(e) {
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+			if (isFile || isDriveFile) {
+				e.preventDefault();
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			}
+		},
+
+		onDrop(e): void {
+			// ファイルだったら
+			if (e.dataTransfer.files.length == 1) {
+				e.preventDefault();
+				this.upload(e.dataTransfer.files[0]);
+				return;
+			} else if (e.dataTransfer.files.length > 1) {
+				e.preventDefault();
+				os.dialog({
+					type: 'error',
+					text: this.$ts.onlyOneFileCanBeAttached
+				});
+				return;
+			}
+
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+			if (driveFile != null && driveFile != '') {
+				this.file = JSON.parse(driveFile);
+				e.preventDefault();
+			}
+			//#endregion
+		},
+
+		onKeypress(e) {
+			this.typing();
+			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
+				this.send();
+			}
+		},
+
+		onCompositionUpdate() {
+			this.typing();
+		},
+
+		chooseFile(e) {
+			selectFile(e.currentTarget || e.target, this.$ts.selectFile, false).then(file => {
+				this.file = file;
+			});
+		},
+
+		onChangeFile() {
+			this.upload((this.$refs.file as any).files[0]);
+		},
+
+		upload(file: File, name?: string) {
+			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
+				this.file = res;
+			});
+		},
+
+		send() {
+			this.sending = true;
+			os.api('messaging/messages/create', {
+				userId: this.user ? this.user.id : undefined,
+				groupId: this.group ? this.group.id : undefined,
+				text: this.text ? this.text : undefined,
+				fileId: this.file ? this.file.id : undefined
+			}).then(message => {
+				this.clear();
+			}).catch(err => {
+				console.error(err);
+			}).then(() => {
+				this.sending = false;
+			});
+		},
+
+		clear() {
+			this.text = '';
+			this.file = null;
+			this.deleteDraft();
+		},
+
+		saveDraft() {
+			const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
+
+			data[this.draftKey] = {
+				updatedAt: new Date(),
+				data: {
+					text: this.text,
+					file: this.file
+				}
+			}
+
+			localStorage.setItem('message_drafts', JSON.stringify(data));
+		},
+
+		deleteDraft() {
+			const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
+
+			delete data[this.draftKey];
+
+			localStorage.setItem('message_drafts', JSON.stringify(data));
+		},
+
+		async insertEmoji(ev) {
+			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.pemppnzi {
+	position: relative;
+
+	> textarea {
+		cursor: auto;
+		display: block;
+		width: 100%;
+		min-width: 100%;
+		max-width: 100%;
+		height: 80px;
+		margin: 0;
+		padding: 16px 16px 0 16px;
+		resize: none;
+		font-size: 1em;
+		font-family: inherit;
+		outline: none;
+		border: none;
+		border-radius: 0;
+		box-shadow: none;
+		background: transparent;
+		box-sizing: border-box;
+		color: var(--fg);
+	}
+
+	> .file {
+		padding: 8px;
+		color: #444;
+		background: #eee;
+		cursor: pointer;
+	}
+
+	> .send {
+		position: absolute;
+		bottom: 0;
+		right: 0;
+		margin: 0;
+		padding: 16px;
+		font-size: 1em;
+		transition: color 0.1s ease;
+		color: var(--accent);
+
+		&:active {
+			color: var(--accentDarken);
+			transition: color 0s ease;
+		}
+	}
+
+	.files {
+		display: block;
+		margin: 0;
+		padding: 0 8px;
+		list-style: none;
+
+		&:after {
+			content: '';
+			display: block;
+			clear: both;
+		}
+
+		> li {
+			display: block;
+			float: left;
+			margin: 4px;
+			padding: 0;
+			width: 64px;
+			height: 64px;
+			background-color: #eee;
+			background-repeat: no-repeat;
+			background-position: center center;
+			background-size: cover;
+			cursor: move;
+
+			&:hover {
+				> .remove {
+					display: block;
+				}
+			}
+
+			> .remove {
+				display: none;
+				position: absolute;
+				right: -6px;
+				top: -6px;
+				margin: 0;
+				padding: 0;
+				background: transparent;
+				outline: none;
+				border: none;
+				border-radius: 0;
+				box-shadow: none;
+				cursor: pointer;
+			}
+		}
+	}
+
+	._button {
+		margin: 0;
+		padding: 16px;
+		font-size: 1em;
+		font-weight: normal;
+		text-decoration: none;
+		transition: color 0.1s ease;
+
+		&:hover {
+			color: var(--accent);
+		}
+
+		&:active {
+			color: var(--accentDarken);
+			transition: color 0s ease;
+		}
+	}
+
+	input[type=file] {
+		display: none;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/messaging/messaging-room.message.vue b/packages/client/src/pages/messaging/messaging-room.message.vue
new file mode 100644
index 0000000000..432d11add8
--- /dev/null
+++ b/packages/client/src/pages/messaging/messaging-room.message.vue
@@ -0,0 +1,350 @@
+<template>
+<div class="thvuemwp" :class="{ isMe }" v-size="{ max: [400, 500] }">
+	<MkAvatar class="avatar" :user="message.user" :show-indicator="true"/>
+	<div class="content">
+		<div class="balloon" :class="{ noText: message.text == null }" >
+			<button class="delete-button" v-if="isMe" :title="$ts.delete" @click="del">
+				<img src="/client-assets/remove.png" alt="Delete"/>
+			</button>
+			<div class="content" v-if="!message.isDeleted">
+				<Mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$i"/>
+				<div class="file" v-if="message.file">
+					<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
+						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
+						<p v-else>{{ message.file.name }}</p>
+					</a>
+				</div>
+			</div>
+			<div class="content" v-else>
+				<p class="is-deleted">{{ $ts.deleted }}</p>
+			</div>
+		</div>
+		<div></div>
+		<MkUrlPreview v-for="url in urls" :url="url" :key="url" style="margin: 8px 0;"/>
+		<footer>
+			<template v-if="isGroup">
+				<span class="read" v-if="message.reads.length > 0">{{ $ts.messageRead }} {{ message.reads.length }}</span>
+			</template>
+			<template v-else>
+				<span class="read" v-if="isMe && message.isRead">{{ $ts.messageRead }}</span>
+			</template>
+			<MkTime :time="message.createdAt"/>
+			<template v-if="message.is_edited"><i class="fas fa-pencil-alt"></i></template>
+		</footer>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as mfm from 'mfm-js';
+import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
+import MkUrlPreview from '@/components/url-preview.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkUrlPreview
+	},
+	props: {
+		message: {
+			required: true
+		},
+		isGroup: {
+			required: false
+		}
+	},
+	computed: {
+		isMe(): boolean {
+			return this.message.userId === this.$i.id;
+		},
+		urls(): string[] {
+			if (this.message.text) {
+				return extractUrlFromMfm(mfm.parse(this.message.text));
+			} else {
+				return [];
+			}
+		}
+	},
+	methods: {
+		del() {
+			os.api('messaging/messages/delete', {
+				messageId: this.message.id
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.thvuemwp {
+	$me-balloon-color: var(--accent);
+
+	position: relative;
+	background-color: transparent;
+	display: flex;
+
+	> .avatar {
+		position: sticky;
+		top: calc(var(--stickyTop, 0px) + 16px);
+		display: block;
+		width: 54px;
+		height: 54px;
+		transition: all 0.1s ease;
+	}
+
+	> .content {
+		min-width: 0;
+
+		> .balloon {
+			position: relative;
+			display: inline-flex;
+			align-items: center;
+			padding: 0;
+			min-height: 38px;
+			border-radius: 16px;
+			max-width: 100%;
+
+			&:before {
+				content: "";
+				pointer-events: none;
+				display: block;
+				position: absolute;
+				top: 12px;
+			}
+
+			& + * {
+				clear: both;
+			}
+
+			&:hover {
+				> .delete-button {
+					display: block;
+				}
+			}
+
+			> .delete-button {
+				display: none;
+				position: absolute;
+				z-index: 1;
+				top: -4px;
+				right: -4px;
+				margin: 0;
+				padding: 0;
+				cursor: pointer;
+				outline: none;
+				border: none;
+				border-radius: 0;
+				box-shadow: none;
+				background: transparent;
+
+				> img {
+					vertical-align: bottom;
+					width: 16px;
+					height: 16px;
+					cursor: pointer;
+				}
+			}
+
+			> .content {
+				max-width: 100%;
+
+				> .is-deleted {
+					display: block;
+					margin: 0;
+					padding: 0;
+					overflow: hidden;
+					overflow-wrap: break-word;
+					font-size: 1em;
+					color: rgba(#000, 0.5);
+				}
+
+				> .text {
+					display: block;
+					margin: 0;
+					padding: 12px 18px;
+					overflow: hidden;
+					overflow-wrap: break-word;
+					word-break: break-word;
+					font-size: 1em;
+					color: rgba(#000, 0.8);
+
+					& + .file {
+						> a {
+							border-radius: 0 0 16px 16px;
+						}
+					}
+				}
+
+				> .file {
+					> a {
+						display: block;
+						max-width: 100%;
+						border-radius: 16px;
+						overflow: hidden;
+						text-decoration: none;
+
+						&:hover {
+							text-decoration: none;
+
+							> p {
+								background: #ccc;
+							}
+						}
+
+						> * {
+							display: block;
+							margin: 0;
+							width: 100%;
+							max-height: 512px;
+							object-fit: contain;
+							box-sizing: border-box;
+						}
+
+						> p {
+							padding: 30px;
+							text-align: center;
+							color: #555;
+							background: #ddd;
+						}
+					}
+				}
+			}
+		}
+
+		> footer {
+			display: block;
+			margin: 2px 0 0 0;
+			font-size: 0.65em;
+
+			> .read {
+				margin: 0 8px;
+			}
+
+			> i {
+				margin-left: 4px;
+			}
+		}
+	}
+
+	&:not(.isMe) {
+		padding-left: var(--margin);
+
+		> .content {
+			padding-left: 16px;
+			padding-right: 32px;
+
+			> .balloon {
+				$color: var(--messageBg);
+				background: $color;
+
+				&.noText {
+					background: transparent;
+				}
+
+				&:not(.noText):before {
+					left: -14px;
+					border-top: solid 8px transparent;
+					border-right: solid 8px $color;
+					border-bottom: solid 8px transparent;
+					border-left: solid 8px transparent;
+				}
+
+				> .content {
+					> .text {
+						color: var(--fg);
+					}
+				}
+			}
+
+			> footer {
+				text-align: left;
+			}
+		}
+	}
+
+	&.isMe {
+		flex-direction: row-reverse;
+		padding-right: var(--margin);
+
+		> .content {
+			padding-right: 16px;
+			padding-left: 32px;
+			text-align: right;
+
+			> .balloon {
+				background: $me-balloon-color;
+				text-align: left;
+
+				::selection {
+					color: var(--accent);
+					background-color: #fff;
+				} 
+
+				&.noText {
+					background: transparent;
+				}
+
+				&:not(.noText):before {
+					right: -14px;
+					left: auto;
+					border-top: solid 8px transparent;
+					border-right: solid 8px transparent;
+					border-bottom: solid 8px transparent;
+					border-left: solid 8px $me-balloon-color;
+				}
+
+				> .content {
+
+					> p.is-deleted {
+						color: rgba(#fff, 0.5);
+					}
+
+					> .text {
+						&, ::v-deep(*) {
+							color: var(--fgOnAccent) !important;
+						}
+					}
+				}
+			}
+
+			> footer {
+				text-align: right;
+
+				> .read {
+					user-select: none;
+				}
+			}
+		}
+	}
+
+	&.max-width_400px {
+		> .avatar {
+			width: 48px;
+			height: 48px;
+		}
+
+		> .content {
+			> .balloon {
+				> .content {
+					> .text {
+						font-size: 0.9em;
+					}
+				}
+			}
+		}
+	}
+
+	&.max-width_500px {
+		> .content {
+			> .balloon {
+				> .content {
+					> .text {
+						padding: 8px 16px;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
new file mode 100644
index 0000000000..3a19b12762
--- /dev/null
+++ b/packages/client/src/pages/messaging/messaging-room.vue
@@ -0,0 +1,470 @@
+<template>
+<div class="_section"
+	@dragover.prevent.stop="onDragover"
+	@drop.prevent.stop="onDrop"
+>
+	<div class="_content mk-messaging-room">
+		<div class="body">
+			<MkLoading v-if="fetching"/>
+			<p class="empty" v-if="!fetching && messages.length == 0"><i class="fas fa-info-circle"></i>{{ $ts.noMessagesYet }}</p>
+			<p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><i class="fas fa-flag"></i>{{ $ts.noMoreHistory }}</p>
+			<button class="more _button" ref="loadMore" :class="{ fetching: fetchingMoreMessages }" v-show="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
+				<template v-if="fetchingMoreMessages"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ fetchingMoreMessages ? $ts.loading : $ts.loadMore }}
+			</button>
+			<XList class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed>
+				<XMessage :message="message" :is-group="group != null" :key="message.id"/>
+			</XList>
+		</div>
+		<footer>
+			<div class="typers" v-if="typers.length > 0">
+				<I18n :src="$ts.typingUsers" text-tag="span" class="users">
+					<template #users>
+						<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
+					</template>
+				</I18n>
+				<MkEllipsis/>
+			</div>
+			<transition name="fade">
+				<div class="new-message" v-show="showIndicator">
+					<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ $ts.newMessageExists }}</button>
+				</div>
+			</transition>
+			<XForm v-if="!fetching" :user="user" :group="group" ref="form" class="form"/>
+		</footer>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw } from 'vue';
+import XList from '@/components/date-separated-list.vue';
+import XMessage from './messaging-room.message.vue';
+import XForm from './messaging-room.form.vue';
+import * as Acct from 'misskey-js/built/acct';
+import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
+import * as os from '@/os';
+import { popout } from '@/scripts/popout';
+import * as sound from '@/scripts/sound';
+import * as symbols from '@/symbols';
+
+const Component = defineComponent({
+	components: {
+		XMessage,
+		XForm,
+		XList,
+	},
+
+	inject: ['inWindow'],
+
+	props: {
+		userAcct: {
+			type: String,
+			required: false,
+		},
+		groupId: {
+			type: String,
+			required: false,
+		},
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => !this.fetching ? this.user ? {
+				userName: this.user,
+				avatar: this.user,
+				action: {
+					icon: 'fas fa-ellipsis-h',
+					handler: this.menu,
+				},
+			} : {
+				title: this.group.name,
+				icon: 'fas fa-users',
+				action: {
+					icon: 'fas fa-ellipsis-h',
+					handler: this.menu,
+				},
+			} : null),
+			fetching: true,
+			user: null,
+			group: null,
+			fetchingMoreMessages: false,
+			messages: [],
+			existMoreMessages: false,
+			connection: null,
+			showIndicator: false,
+			timer: null,
+			typers: [],
+			ilObserver: new IntersectionObserver(
+				(entries) => entries.some((entry) => entry.isIntersecting)
+					&& !this.fetching
+					&& !this.fetchingMoreMessages
+					&& this.existMoreMessages
+					&& this.fetchMoreMessages()
+			),
+		};
+	},
+
+	computed: {
+		form(): any {
+			return this.$refs.form;
+		}
+	},
+
+	watch: {
+		userAcct: 'fetch',
+		groupId: 'fetch',
+	},
+
+	mounted() {
+		this.fetch();
+		if (this.$store.state.enableInfiniteScroll) {
+			this.$nextTick(() => this.ilObserver.observe(this.$refs.loadMore as Element));
+		}
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+
+		document.removeEventListener('visibilitychange', this.onVisibilitychange);
+
+		this.ilObserver.disconnect();
+	},
+
+	methods: {
+		async fetch() {
+			this.fetching = true;
+			if (this.userAcct) {
+				const user = await os.api('users/show', Acct.parse(this.userAcct));
+				this.user = user;
+			} else {
+				const group = await os.api('users/groups/show', { groupId: this.groupId });
+				this.group = group;
+			}
+
+			this.connection = markRaw(os.stream.useChannel('messaging', {
+				otherparty: this.user ? this.user.id : undefined,
+				group: this.group ? this.group.id : undefined,
+			}));
+
+			this.connection.on('message', this.onMessage);
+			this.connection.on('read', this.onRead);
+			this.connection.on('deleted', this.onDeleted);
+			this.connection.on('typers', typers => {
+				this.typers = typers.filter(u => u.id !== this.$i.id);
+			});
+
+			document.addEventListener('visibilitychange', this.onVisibilitychange);
+
+			this.fetchMessages().then(() => {
+				this.scrollToBottom();
+
+				// もっと見るの交差検知を発火させないためにfetchは
+				// スクロールが終わるまでfalseにしておく
+				// scrollendのようなイベントはないのでsetTimeoutで
+				setTimeout(() => this.fetching = false, 300);
+			});
+		},
+
+		onDragover(e) {
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+
+			if (isFile || isDriveFile) {
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			} else {
+				e.dataTransfer.dropEffect = 'none';
+			}
+		},
+
+		onDrop(e): void {
+			// ファイルだったら
+			if (e.dataTransfer.files.length == 1) {
+				this.form.upload(e.dataTransfer.files[0]);
+				return;
+			} else if (e.dataTransfer.files.length > 1) {
+				os.dialog({
+					type: 'error',
+					text: this.$ts.onlyOneFileCanBeAttached
+				});
+				return;
+			}
+
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.form.file = file;
+			}
+			//#endregion
+		},
+
+		fetchMessages() {
+			return new Promise((resolve, reject) => {
+				const max = this.existMoreMessages ? 20 : 10;
+
+				os.api('messaging/messages', {
+					userId: this.user ? this.user.id : undefined,
+					groupId: this.group ? this.group.id : undefined,
+					limit: max + 1,
+					untilId: this.existMoreMessages ? this.messages[0].id : undefined
+				}).then(messages => {
+					if (messages.length == max + 1) {
+						this.existMoreMessages = true;
+						messages.pop();
+					} else {
+						this.existMoreMessages = false;
+					}
+
+					this.messages.unshift.apply(this.messages, messages.reverse());
+					resolve();
+				});
+			});
+		},
+
+		fetchMoreMessages() {
+			this.fetchingMoreMessages = true;
+			this.fetchMessages().then(() => {
+				this.fetchingMoreMessages = false;
+			});
+		},
+
+		onMessage(message) {
+			sound.play('chat');
+
+			const _isBottom = isBottom(this.$el, 64);
+
+			this.messages.push(message);
+			if (message.userId != this.$i.id && !document.hidden) {
+				this.connection.send('read', {
+					id: message.id
+				});
+			}
+
+			if (_isBottom) {
+				// Scroll to bottom
+				this.$nextTick(() => {
+					this.scrollToBottom();
+				});
+			} else if (message.userId != this.$i.id) {
+				// Notify
+				this.notifyNewMessage();
+			}
+		},
+
+		onRead(x) {
+			if (this.user) {
+				if (!Array.isArray(x)) x = [x];
+				for (const id of x) {
+					if (this.messages.some(x => x.id == id)) {
+						const exist = this.messages.map(x => x.id).indexOf(id);
+						this.messages[exist] = {
+							...this.messages[exist],
+							isRead: true,
+						};
+					}
+				}
+			} else if (this.group) {
+				for (const id of x.ids) {
+					if (this.messages.some(x => x.id == id)) {
+						const exist = this.messages.map(x => x.id).indexOf(id);
+						this.messages[exist] = {
+							...this.messages[exist],
+							reads: [...this.messages[exist].reads, x.userId]
+						};
+					}
+				}
+			}
+		},
+
+		onDeleted(id) {
+			const msg = this.messages.find(m => m.id === id);
+			if (msg) {
+				this.messages = this.messages.filter(m => m.id !== msg.id);
+			}
+		},
+
+		scrollToBottom() {
+			scroll(this.$el, { top: this.$el.offsetHeight });
+		},
+
+		onIndicatorClick() {
+			this.showIndicator = false;
+			this.scrollToBottom();
+		},
+
+		notifyNewMessage() {
+			this.showIndicator = true;
+
+			onScrollBottom(this.$el, () => {
+				this.showIndicator = false;
+			});
+
+			if (this.timer) clearTimeout(this.timer);
+
+			this.timer = setTimeout(() => {
+				this.showIndicator = false;
+			}, 4000);
+		},
+
+		onVisibilitychange() {
+			if (document.hidden) return;
+			for (const message of this.messages) {
+				if (message.userId !== this.$i.id && !message.isRead) {
+					this.connection.send('read', {
+						id: message.id
+					});
+				}
+			}
+		},
+
+		menu(ev) {
+			const path = this.groupId ? `/my/messaging/group/${this.groupId}` : `/my/messaging/${this.userAcct}`;
+
+			os.popupMenu([this.inWindow ? undefined : {
+				text: this.$ts.openInWindow,
+				icon: 'fas fa-window-maximize',
+				action: () => {
+					os.pageWindow(path);
+					this.$router.back();
+				},
+			}, this.inWindow ? undefined : {
+				text: this.$ts.popout,
+				icon: 'fas fa-external-link-alt',
+				action: () => {
+					popout(path);
+					this.$router.back();
+				},
+			}], ev.currentTarget || ev.target);
+		}
+	}
+});
+
+export default Component;
+</script>
+
+<style lang="scss" scoped>
+.mk-messaging-room {
+	> .body {
+		> .empty {
+			width: 100%;
+			margin: 0;
+			padding: 16px 8px 8px 8px;
+			text-align: center;
+			font-size: 0.8em;
+			opacity: 0.5;
+
+			i {
+				margin-right: 4px;
+			}
+		}
+
+		> .no-history {
+			display: block;
+			margin: 0;
+			padding: 16px;
+			text-align: center;
+			font-size: 0.8em;
+			color: var(--messagingRoomInfo);
+			opacity: 0.5;
+
+			i {
+				margin-right: 4px;
+			}
+		}
+
+		> .more {
+			display: block;
+			margin: 16px auto;
+			padding: 0 12px;
+			line-height: 24px;
+			color: #fff;
+			background: rgba(#000, 0.3);
+			border-radius: 12px;
+
+			&:hover {
+				background: rgba(#000, 0.4);
+			}
+
+			&:active {
+				background: rgba(#000, 0.5);
+			}
+
+			&.fetching {
+				cursor: wait;
+			}
+
+			> i {
+				margin-right: 4px;
+			}
+		}
+
+		> .messages {
+			> ::v-deep(*) {
+				margin-bottom: 16px;
+			}
+		}
+	}
+
+	> footer {
+		width: 100%;
+		position: relative;
+
+		> .new-message {
+			position: absolute;
+			top: -48px;
+			width: 100%;
+			padding: 8px 0;
+			text-align: center;
+
+			> button {
+				display: inline-block;
+				margin: 0;
+				padding: 0 12px 0 30px;
+				line-height: 32px;
+				font-size: 12px;
+				border-radius: 16px;
+
+				> i {
+					position: absolute;
+					top: 0;
+					left: 10px;
+					line-height: 32px;
+					font-size: 16px;
+				}
+			}
+		}
+
+		> .typers {
+			position: absolute;
+			bottom: 100%;
+			padding: 0 8px 0 8px;
+			font-size: 0.9em;
+			color: var(--fgTransparentWeak);
+
+			> .users {
+				> .user + .user:before {
+					content: ", ";
+					font-weight: normal;
+				}
+
+				> .user:last-of-type:after {
+					content: " ";
+				}
+			}
+		}
+
+		> .form {
+			border-top: solid 0.5px var(--divider);
+		}
+	}
+}
+
+.fade-enter-active, .fade-leave-active {
+	transition: opacity 0.1s;
+}
+
+.fade-enter-from, .fade-leave-to {
+	transition: opacity 0.5s;
+	opacity: 0;
+}
+</style>
diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue
new file mode 100644
index 0000000000..e9a3b6debc
--- /dev/null
+++ b/packages/client/src/pages/mfm-cheat-sheet.vue
@@ -0,0 +1,365 @@
+<template>
+<div class="mwysmxbg">
+	<div class="_isolated">{{ $ts._mfm.intro }}</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.mention }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.mentionDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_mention"/>
+				<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.hashtag }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.hashtagDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_hashtag"/>
+				<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.url }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.urlDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_url"/>
+				<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.link }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.linkDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_link"/>
+				<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.emoji }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.emojiDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_emoji"/>
+				<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.bold }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.boldDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_bold"/>
+				<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.small }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.smallDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_small"/>
+				<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.quote }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.quoteDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_quote"/>
+				<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.center }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.centerDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_center"/>
+				<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.inlineCode }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.inlineCodeDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_inlineCode"/>
+				<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.blockCode }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.blockCodeDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_blockCode"/>
+				<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.inlineMath }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.inlineMathDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_inlineMath"/>
+				<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.search }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.searchDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_search"/>
+				<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.flip }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.flipDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_flip"/>
+				<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.font }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.fontDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_font"/>
+				<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.x2 }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.x2Description }}</p>
+			<div class="preview">
+				<Mfm :text="preview_x2"/>
+				<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.x3 }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.x3Description }}</p>
+			<div class="preview">
+				<Mfm :text="preview_x3"/>
+				<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.x4 }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.x4Description }}</p>
+			<div class="preview">
+				<Mfm :text="preview_x4"/>
+				<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.blur }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.blurDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_blur"/>
+				<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.jelly }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.jellyDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_jelly"/>
+				<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.tada }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.tadaDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_tada"/>
+				<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.jump }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.jumpDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_jump"/>
+				<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.bounce }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.bounceDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_bounce"/>
+				<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.spin }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.spinDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_spin"/>
+				<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.shake }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.shakeDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_shake"/>
+				<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.twitch }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.twitchDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_twitch"/>
+				<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.rainbow }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.rainbowDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_rainbow"/>
+				<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
+			</div>
+		</div>
+	</div>
+	<div class="section _block">
+		<div class="title">{{ $ts._mfm.sparkle }}</div>
+		<div class="content">
+			<p>{{ $ts._mfm.sparkleDescription }}</p>
+			<div class="preview">
+				<Mfm :text="preview_sparkle"/>
+				<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkTextarea
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts._mfm.cheatSheet,
+				icon: 'fas fa-question-circle',
+			},
+			preview_mention: '@example',
+			preview_hashtag: '#test',
+			preview_url: `https://example.com`,
+			preview_link: `[${this.$ts._mfm.dummy}](https://example.com)`,
+			preview_emoji: this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:`,
+			preview_bold: `**${this.$ts._mfm.dummy}**`,
+			preview_small: `<small>${this.$ts._mfm.dummy}</small>`,
+			preview_center: `<center>${this.$ts._mfm.dummy}</center>`,
+			preview_inlineCode: '`<: "Hello, world!"`',
+			preview_blockCode: '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
+			preview_inlineMath: '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)',
+			preview_quote: `> ${this.$ts._mfm.dummy}`,
+			preview_search: `${this.$ts._mfm.dummy} 検索`,
+			preview_jelly: `$[jelly 🍮]`,
+			preview_tada: `$[tada 🍮]`,
+			preview_jump: `$[jump 🍮]`,
+			preview_bounce: `$[bounce 🍮]`,
+			preview_shake: `$[shake 🍮]`,
+			preview_twitch: `$[twitch 🍮]`,
+			preview_spin: `$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]`,
+			preview_flip: `$[flip ${this.$ts._mfm.dummy}]\n$[flip.v ${this.$ts._mfm.dummy}]\n$[flip.h,v ${this.$ts._mfm.dummy}]`,
+			preview_font: `$[font.serif ${this.$ts._mfm.dummy}]\n$[font.monospace ${this.$ts._mfm.dummy}]\n$[font.cursive ${this.$ts._mfm.dummy}]\n$[font.fantasy ${this.$ts._mfm.dummy}]`,
+			preview_x2: `$[x2 🍮]`,
+			preview_x3: `$[x3 🍮]`,
+			preview_x4: `$[x4 🍮]`,
+			preview_blur: `$[blur ${this.$ts._mfm.dummy}]`,
+			preview_rainbow: `$[rainbow 🍮]`,
+			preview_sparkle: `$[sparkle 🍮]`,
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mwysmxbg {
+	background: var(--bg);
+
+	> .section {
+		> .title {
+			position: sticky;
+			z-index: 1;
+			top: var(--stickyTop, 0px);
+			padding: 16px;
+			font-weight: bold;
+			-webkit-backdrop-filter: var(--blur, blur(10px));
+			backdrop-filter: var(--blur, blur(10px));
+			background-color: var(--X16);
+		}
+
+		> .content {
+			> p {
+				margin: 0;
+				padding: 16px;
+			}
+
+			> .preview {
+				border-top: solid 0.5px var(--divider);
+				padding: 16px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/miauth.vue b/packages/client/src/pages/miauth.vue
new file mode 100644
index 0000000000..6430588c46
--- /dev/null
+++ b/packages/client/src/pages/miauth.vue
@@ -0,0 +1,100 @@
+<template>
+<div v-if="$i">
+	<div class="waiting _section" v-if="state == 'waiting'">
+		<div class="_content">
+			<MkLoading/>
+		</div>
+	</div>
+	<div class="denied _section" v-if="state == 'denied'">
+		<div class="_content">
+			<p>{{ $ts._auth.denied }}</p>
+		</div>
+	</div>
+	<div class="accepted _section" v-else-if="state == 'accepted'">
+		<div class="_content">
+			<p v-if="callback">{{ $ts._auth.callback }}<MkEllipsis/></p>
+			<p v-else>{{ $ts._auth.pleaseGoBack }}</p>
+		</div>
+	</div>
+	<div class="_section" v-else>
+		<div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div>
+		<div class="_title" v-else>{{ $ts._auth.shareAccessAsk }}</div>
+		<div class="_content">
+			<p>{{ $ts._auth.permissionAsk }}</p>
+			<ul>
+				<li v-for="p in permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
+			</ul>
+		</div>
+		<div class="_footer">
+			<MkButton @click="deny" inline>{{ $ts.cancel }}</MkButton>
+			<MkButton @click="accept" inline primary>{{ $ts.accept }}</MkButton>
+		</div>
+	</div>
+</div>
+<div class="signin" v-else>
+	<MkSignin @login="onLogin"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkSignin from '@/components/signin.vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import { login } from '@/account';
+
+export default defineComponent({
+	components: {
+		MkSignin,
+		MkButton,
+	},
+	data() {
+		return {
+			state: null
+		};
+	},
+	computed: {
+		session(): string {
+			return this.$route.params.session;
+		},
+		callback(): string {
+			return this.$route.query.callback;
+		},
+		name(): string {
+			return this.$route.query.name;
+		},
+		icon(): string {
+			return this.$route.query.icon;
+		},
+		permission(): string[] {
+			return this.$route.query.permission ? this.$route.query.permission.split(',') : [];
+		},
+	},
+	methods: {
+		async accept() {
+			this.state = 'waiting';
+			await os.api('miauth/gen-token', {
+				session: this.session,
+				name: this.name,
+				iconUrl: this.icon,
+				permission: this.permission,
+			});
+
+			this.state = 'accepted';
+			if (this.callback) {
+				location.href = `${this.callback}?session=${this.session}`;
+			}
+		},
+		deny() {
+			this.state = 'denied';
+		},
+		onLogin(res) {
+			login(res.i);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue
new file mode 100644
index 0000000000..173807475a
--- /dev/null
+++ b/packages/client/src/pages/my-antennas/create.vue
@@ -0,0 +1,51 @@
+<template>
+<div class="geegznzt">
+	<XAntenna :antenna="draft" @created="onAntennaCreated"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import XAntenna from './editor.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		XAntenna,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.manageAntennas,
+				icon: 'fas fa-satellite',
+			},
+			draft: {
+				name: '',
+				src: 'all',
+				userListId: null,
+				userGroupId: null,
+				users: [],
+				keywords: [],
+				excludeKeywords: [],
+				withReplies: false,
+				caseSensitive: false,
+				withFile: false,
+				notify: false
+			},
+		};
+	},
+
+	methods: {
+		onAntennaCreated() {
+			this.$router.push('/my/antennas');
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/my-antennas/edit.vue b/packages/client/src/pages/my-antennas/edit.vue
new file mode 100644
index 0000000000..04928c81a3
--- /dev/null
+++ b/packages/client/src/pages/my-antennas/edit.vue
@@ -0,0 +1,56 @@
+<template>
+<div class="">
+	<XAntenna v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import XAntenna from './editor.vue';
+import * as symbols from '@/symbols';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		XAntenna,
+	},
+
+	props: {
+		antennaId: {
+			type: String,
+			required: true,
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.manageAntennas,
+				icon: 'fas fa-satellite',
+			},
+			antenna: null,
+		};
+	},
+
+	watch: {
+		antennaId: {
+			async handler() {
+				this.antenna = await os.api('antennas/show', { antennaId: this.antennaId });
+			},
+			immediate: true,
+		}
+	},
+
+	methods: {
+		onAntennaUpdated() {
+			this.$router.push('/my/antennas');
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/my-antennas/editor.vue b/packages/client/src/pages/my-antennas/editor.vue
new file mode 100644
index 0000000000..5ad3d50486
--- /dev/null
+++ b/packages/client/src/pages/my-antennas/editor.vue
@@ -0,0 +1,190 @@
+<template>
+<div class="shaynizk">
+	<div class="form">
+		<MkInput v-model="name" class="_formBlock">
+			<template #label>{{ $ts.name }}</template>
+		</MkInput>
+		<MkSelect v-model="src" class="_formBlock">
+			<template #label>{{ $ts.antennaSource }}</template>
+			<option value="all">{{ $ts._antennaSources.all }}</option>
+			<option value="home">{{ $ts._antennaSources.homeTimeline }}</option>
+			<option value="users">{{ $ts._antennaSources.users }}</option>
+			<option value="list">{{ $ts._antennaSources.userList }}</option>
+			<option value="group">{{ $ts._antennaSources.userGroup }}</option>
+		</MkSelect>
+		<MkSelect v-model="userListId" v-if="src === 'list'" class="_formBlock">
+			<template #label>{{ $ts.userList }}</template>
+			<option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option>
+		</MkSelect>
+		<MkSelect v-model="userGroupId" v-else-if="src === 'group'" class="_formBlock">
+			<template #label>{{ $ts.userGroup }}</template>
+			<option v-for="group in userGroups" :value="group.id" :key="group.id">{{ group.name }}</option>
+		</MkSelect>
+		<MkTextarea v-model="users" v-else-if="src === 'users'" class="_formBlock">
+			<template #label>{{ $ts.users }}</template>
+			<template #caption>{{ $ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ $ts.addUser }}</button></template>
+		</MkTextarea>
+		<MkSwitch v-model="withReplies" class="_formBlock">{{ $ts.withReplies }}</MkSwitch>
+		<MkTextarea v-model="keywords" class="_formBlock">
+			<template #label>{{ $ts.antennaKeywords }}</template>
+			<template #caption>{{ $ts.antennaKeywordsDescription }}</template>
+		</MkTextarea>
+		<MkTextarea v-model="excludeKeywords" class="_formBlock">
+			<template #label>{{ $ts.antennaExcludeKeywords }}</template>
+			<template #caption>{{ $ts.antennaKeywordsDescription }}</template>
+		</MkTextarea>
+		<MkSwitch v-model="caseSensitive" class="_formBlock">{{ $ts.caseSensitive }}</MkSwitch>
+		<MkSwitch v-model="withFile" class="_formBlock">{{ $ts.withFileAntenna }}</MkSwitch>
+		<MkSwitch v-model="notify" class="_formBlock">{{ $ts.notifyAntenna }}</MkSwitch>
+	</div>
+	<div class="actions">
+		<MkButton inline @click="saveAntenna()" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+		<MkButton inline @click="deleteAntenna()" v-if="antenna.id != null" danger><i class="fas fa-trash"></i> {{ $ts.delete }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import * as Acct from 'misskey-js/built/acct';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		MkButton, MkInput, MkTextarea, MkSelect, MkSwitch
+	},
+
+	props: {
+		antenna: {
+			type: Object,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			name: '',
+			src: '',
+			userListId: null,
+			userGroupId: null,
+			users: '',
+			keywords: '',
+			excludeKeywords: '',
+			caseSensitive: false,
+			withReplies: false,
+			withFile: false,
+			notify: false,
+			userLists: null,
+			userGroups: null,
+		};
+	},
+
+	watch: {
+		async src() {
+			if (this.src === 'list' && this.userLists === null) {
+				this.userLists = await os.api('users/lists/list');
+			}
+
+			if (this.src === 'group' && this.userGroups === null) {
+				const groups1 = await os.api('users/groups/owned');
+				const groups2 = await os.api('users/groups/joined');
+
+				this.userGroups = [...groups1, ...groups2];
+			}
+		}
+	},
+
+	created() {
+		this.name = this.antenna.name;
+		this.src = this.antenna.src;
+		this.userListId = this.antenna.userListId;
+		this.userGroupId = this.antenna.userGroupId;
+		this.users = this.antenna.users.join('\n');
+		this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n');
+		this.excludeKeywords = this.antenna.excludeKeywords.map(x => x.join(' ')).join('\n');
+		this.caseSensitive = this.antenna.caseSensitive;
+		this.withReplies = this.antenna.withReplies;
+		this.withFile = this.antenna.withFile;
+		this.notify = this.antenna.notify;
+	},
+
+	methods: {
+		async saveAntenna() {
+			if (this.antenna.id == null) {
+				await os.apiWithDialog('antennas/create', {
+					name: this.name,
+					src: this.src,
+					userListId: this.userListId,
+					userGroupId: this.userGroupId,
+					withReplies: this.withReplies,
+					withFile: this.withFile,
+					notify: this.notify,
+					caseSensitive: this.caseSensitive,
+					users: this.users.trim().split('\n').map(x => x.trim()),
+					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
+					excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
+				});
+				this.$emit('created');
+			} else {
+				await os.apiWithDialog('antennas/update', {
+					antennaId: this.antenna.id,
+					name: this.name,
+					src: this.src,
+					userListId: this.userListId,
+					userGroupId: this.userGroupId,
+					withReplies: this.withReplies,
+					withFile: this.withFile,
+					notify: this.notify,
+					caseSensitive: this.caseSensitive,
+					users: this.users.trim().split('\n').map(x => x.trim()),
+					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
+					excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
+				});
+				this.$emit('updated');
+			}
+		},
+
+		async deleteAntenna() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.antenna.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			await os.api('antennas/delete', {
+				antennaId: this.antenna.id,
+			});
+
+			os.success();
+			this.$emit('deleted');
+		},
+
+		addUser() {
+			os.selectUser().then(user => {
+				this.users = this.users.trim();
+				this.users += '\n@' + Acct.toString(user);
+				this.users = this.users.trim();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.shaynizk {
+	> .form {
+		padding: 32px;
+	}
+
+	> .actions {
+		padding: 24px 32px;
+		border-top: solid 0.5px var(--divider);
+	}
+}
+</style>
diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue
new file mode 100644
index 0000000000..029f1949d7
--- /dev/null
+++ b/packages/client/src/pages/my-antennas/index.vue
@@ -0,0 +1,71 @@
+<template>
+<div class="ieepwinx _section">
+	<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
+
+	<div class="_content">
+		<MkPagination :pagination="pagination" #default="{items}" ref="list">
+			<MkA class="ljoevbzj" v-for="antenna in items" :key="antenna.id" :to="`/my/antennas/${antenna.id}`">
+				<div class="name">{{ antenna.name }}</div>
+			</MkA>
+		</MkPagination>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkButton from '@/components/ui/button.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkButton,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.manageAntennas,
+				icon: 'fas fa-satellite',
+				action: {
+					icon: 'fas fa-plus',
+					handler: this.create
+				}
+			},
+			pagination: {
+				endpoint: 'antennas/list',
+				limit: 10,
+			},
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.ieepwinx {
+	padding: 16px;
+
+	> .add {
+		margin: 0 auto 16px auto;
+	}
+
+	.ljoevbzj {
+		display: block;
+		padding: 16px;
+		margin-bottom: 8px;
+		border: solid 1px var(--divider);
+		border-radius: 6px;
+
+		&:hover {
+			border: solid 1px var(--accent);
+			text-decoration: none;
+		}
+
+		> .name {
+			font-weight: bold;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
new file mode 100644
index 0000000000..cbcdb85fa5
--- /dev/null
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -0,0 +1,104 @@
+<template>
+<div class="_section qtcaoidl">
+	<MkButton @click="create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
+
+	<div class="_content">
+		<MkPagination :pagination="pagination" #default="{items}" ref="list" class="list">
+			<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
+				<b>{{ item.name }}</b>
+				<div v-if="item.description" class="description">{{ item.description }}</div>
+			</MkA>
+		</MkPagination>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkButton,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.clip,
+				icon: 'fas fa-paperclip',
+				action: {
+					icon: 'fas fa-plus',
+					handler: this.create
+				}
+			},
+			pagination: {
+				endpoint: 'clips/list',
+				limit: 10,
+			},
+			draft: null,
+		};
+	},
+
+	methods: {
+		async create() {
+			const { canceled, result } = await os.form(this.$ts.createNewClip, {
+				name: {
+					type: 'string',
+					label: this.$ts.name
+				},
+				description: {
+					type: 'string',
+					required: false,
+					multiline: true,
+					label: this.$ts.description
+				},
+				isPublic: {
+					type: 'boolean',
+					label: this.$ts.public,
+					default: false
+				}
+			});
+			if (canceled) return;
+
+			os.apiWithDialog('clips/create', result);
+		},
+
+		onClipCreated() {
+			this.$refs.list.reload();
+			this.draft = null;
+		},
+
+		onClipDeleted() {
+			this.$refs.list.reload();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qtcaoidl {
+	> .add {
+		margin: 0 auto 16px auto;
+	}
+
+	> ._content {
+		> .list {
+			> .item {
+				display: block;
+				padding: 16px;
+
+				> .description {
+					margin-top: 8px;
+					padding-top: 8px;
+					border-top: solid 0.5px var(--divider);
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/my-groups/group.vue b/packages/client/src/pages/my-groups/group.vue
new file mode 100644
index 0000000000..9548c374d2
--- /dev/null
+++ b/packages/client/src/pages/my-groups/group.vue
@@ -0,0 +1,184 @@
+<template>
+<div class="mk-group-page">
+	<transition name="zoom" mode="out-in">
+		<div v-if="group" class="_section">
+			<div class="_content">
+				<MkButton inline @click="invite()">{{ $ts.invite }}</MkButton>
+				<MkButton inline @click="renameGroup()">{{ $ts.rename }}</MkButton>
+				<MkButton inline @click="transfer()">{{ $ts.transfer }}</MkButton>
+				<MkButton inline @click="deleteGroup()">{{ $ts.delete }}</MkButton>
+			</div>
+		</div>
+	</transition>
+
+	<transition name="zoom" mode="out-in">
+		<div v-if="group" class="_section members _gap">
+			<div class="_title">{{ $ts.members }}</div>
+			<div class="_content">
+				<div class="users">
+					<div class="user _panel" v-for="user in users" :key="user.id">
+						<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
+						<div class="body">
+							<MkUserName :user="user" class="name"/>
+							<MkAcct :user="user" class="acct"/>
+						</div>
+						<div class="action">
+							<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+
+	props: {
+		groupId: {
+			type: String,
+			required: true,
+		},
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.group ? {
+				title: this.group.name,
+				icon: 'fas fa-users',
+			} : null),
+			group: null,
+			users: [],
+		};
+	},
+
+	watch: {
+		groupId: 'fetch',
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			Progress.start();
+			os.api('users/groups/show', {
+				groupId: this.groupId
+			}).then(group => {
+				this.group = group;
+				os.api('users/show', {
+					userIds: this.group.userIds
+				}).then(users => {
+					this.users = users;
+					Progress.done();
+				});
+			});
+		},
+
+		invite() {
+			os.selectUser().then(user => {
+				os.apiWithDialog('users/groups/invite', {
+					groupId: this.group.id,
+					userId: user.id
+				});
+			});
+		},
+
+		removeUser(user) {
+			os.api('users/groups/pull', {
+				groupId: this.group.id,
+				userId: user.id
+			}).then(() => {
+				this.users = this.users.filter(x => x.id !== user.id);
+			});
+		},
+
+		async renameGroup() {
+			const { canceled, result: name } = await os.dialog({
+				title: this.$ts.groupName,
+				input: {
+					default: this.group.name
+				}
+			});
+			if (canceled) return;
+
+			await os.api('users/groups/update', {
+				groupId: this.group.id,
+				name: name
+			});
+
+			this.group.name = name;
+		},
+
+		transfer() {
+			os.selectUser().then(user => {
+				os.apiWithDialog('users/groups/transfer', {
+					groupId: this.group.id,
+					userId: user.id
+				});
+			});
+		},
+
+		async deleteGroup() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.group.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			await os.apiWithDialog('users/groups/delete', {
+				groupId: this.group.id
+			});
+			this.$router.push('/my/groups');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-group-page {
+	> .members {
+		> ._content {
+			> .users {
+				> .user {
+					display: flex;
+					align-items: center;
+					padding: 16px;
+
+					> .avatar {
+						width: 50px;
+						height: 50px;
+					}
+
+					> .body {
+						flex: 1;
+						padding: 8px;
+
+						> .name {
+							display: block;
+							font-weight: bold;
+						}
+
+						> .acct {
+							opacity: 0.5;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/my-groups/index.vue b/packages/client/src/pages/my-groups/index.vue
new file mode 100644
index 0000000000..77e7d6088e
--- /dev/null
+++ b/packages/client/src/pages/my-groups/index.vue
@@ -0,0 +1,121 @@
+<template>
+<div class="">
+	<div class="_section" style="padding: 0;">
+		<MkTab v-model="tab">
+			<option value="owned">{{ $ts.ownedGroups }}</option>
+			<option value="joined">{{ $ts.joinedGroups }}</option>
+			<option value="invites"><i class="fas fa-envelope-open-text"></i> {{ $ts.invites }}</option>
+		</MkTab>
+	</div>
+
+	<div class="_section">
+		<div class="_content" v-if="tab === 'owned'">
+			<MkButton @click="create" primary style="margin: 0 auto var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.createGroup }}</MkButton>
+
+			<MkPagination :pagination="ownedPagination" #default="{items}" ref="owned">
+				<div class="_card" v-for="group in items" :key="group.id">
+					<div class="_title"><MkA :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</MkA></div>
+					<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
+				</div>
+			</MkPagination>
+		</div>
+
+		<div class="_content" v-else-if="tab === 'joined'">
+			<MkPagination :pagination="joinedPagination" #default="{items}" ref="joined">
+				<div class="_card" v-for="group in items" :key="group.id">
+					<div class="_title">{{ group.name }}</div>
+					<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
+				</div>
+			</MkPagination>
+		</div>
+	
+		<div class="_content" v-else-if="tab === 'invites'">
+			<MkPagination :pagination="invitationPagination" #default="{items}" ref="invitations">
+				<div class="_card" v-for="invitation in items" :key="invitation.id">
+					<div class="_title">{{ invitation.group.name }}</div>
+					<div class="_content"><MkAvatars :user-ids="invitation.group.userIds"/></div>
+					<div class="_footer">
+						<MkButton @click="acceptInvite(invitation)" primary inline><i class="fas fa-check"></i> {{ $ts.accept }}</MkButton>
+						<MkButton @click="rejectInvite(invitation)" primary inline><i class="fas fa-ban"></i> {{ $ts.reject }}</MkButton>
+					</div>
+				</div>
+			</MkPagination>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkAvatars from '@/components/avatars.vue';
+import MkTab from '@/components/tab.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkButton,
+		MkContainer,
+		MkTab,
+		MkAvatars,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.groups,
+				icon: 'fas fa-users'
+			},
+			tab: 'owned',
+			ownedPagination: {
+				endpoint: 'users/groups/owned',
+				limit: 10,
+			},
+			joinedPagination: {
+				endpoint: 'users/groups/joined',
+				limit: 10,
+			},
+			invitationPagination: {
+				endpoint: 'i/user-group-invites',
+				limit: 10,
+			},
+		};
+	},
+
+	methods: {
+		async create() {
+			const { canceled, result: name } = await os.dialog({
+				title: this.$ts.groupName,
+				input: true
+			});
+			if (canceled) return;
+			await os.api('users/groups/create', { name: name });
+			this.$refs.owned.reload();
+			os.success();
+		},
+		acceptInvite(invitation) {
+			os.api('users/groups/invitations/accept', {
+				invitationId: invitation.id
+			}).then(() => {
+				os.success();
+				this.$refs.invitations.reload();
+				this.$refs.joined.reload();
+			});
+		},
+		rejectInvite(invitation) {
+			os.api('users/groups/invitations/reject', {
+				invitationId: invitation.id
+			}).then(() => {
+				this.$refs.invitations.reload();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue
new file mode 100644
index 0000000000..adb59db665
--- /dev/null
+++ b/packages/client/src/pages/my-lists/index.vue
@@ -0,0 +1,88 @@
+<template>
+<div class="qkcjvfiv">
+	<MkButton @click="create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
+
+	<MkPagination :pagination="pagination" #default="{items}" class="lists _content" ref="list">
+		<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
+			<div class="name">{{ list.name }}</div>
+			<MkAvatars :user-ids="list.userIds"/>
+		</MkA>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkAvatars from '@/components/avatars.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkButton,
+		MkAvatars,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.manageLists,
+				icon: 'fas fa-list-ul',
+				bg: 'var(--bg)',
+				action: {
+					icon: 'fas fa-plus',
+					handler: this.create
+				},
+			},
+			pagination: {
+				endpoint: 'users/lists/list',
+				limit: 10,
+			},
+		};
+	},
+
+	methods: {
+		async create() {
+			const { canceled, result: name } = await os.dialog({
+				title: this.$ts.enterListName,
+				input: true
+			});
+			if (canceled) return;
+			await os.api('users/lists/create', { name: name });
+			this.$refs.list.reload();
+			os.success();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qkcjvfiv {
+	padding: 16px;
+
+	> .add {
+		margin: 0 auto var(--margin) auto;
+	}
+
+	> .lists {
+		> .list {
+			display: block;
+			padding: 16px;
+			border: solid 1px var(--divider);
+			border-radius: 6px;
+
+			&:hover {
+				border: solid 1px var(--accent);
+				text-decoration: none;
+			}
+
+			> .name {
+				margin-bottom: 4px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue
new file mode 100644
index 0000000000..f2a02cadc9
--- /dev/null
+++ b/packages/client/src/pages/my-lists/list.vue
@@ -0,0 +1,170 @@
+<template>
+<div class="mk-list-page">
+	<transition name="zoom" mode="out-in">
+		<div v-if="list" class="_section">
+			<div class="_content">
+				<MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
+				<MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton>
+				<MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton>
+			</div>
+		</div>
+	</transition>
+
+	<transition name="zoom" mode="out-in">
+		<div v-if="list" class="_section members _gap">
+			<div class="_title">{{ $ts.members }}</div>
+			<div class="_content">
+				<div class="users">
+					<div class="user _panel" v-for="user in users" :key="user.id">
+						<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
+						<div class="body">
+							<MkUserName :user="user" class="name"/>
+							<MkAcct :user="user" class="acct"/>
+						</div>
+						<div class="action">
+							<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.list ? {
+				title: this.list.name,
+				icon: 'fas fa-list-ul',
+			} : null),
+			list: null,
+			users: [],
+		};
+	},
+
+	watch: {
+		$route: 'fetch'
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			Progress.start();
+			os.api('users/lists/show', {
+				listId: this.$route.params.list
+			}).then(list => {
+				this.list = list;
+				os.api('users/show', {
+					userIds: this.list.userIds
+				}).then(users => {
+					this.users = users;
+					Progress.done();
+				});
+			});
+		},
+
+		addUser() {
+			os.selectUser().then(user => {
+				os.apiWithDialog('users/lists/push', {
+					listId: this.list.id,
+					userId: user.id
+				}).then(() => {
+					this.users.push(user);
+				});
+			});
+		},
+
+		removeUser(user) {
+			os.api('users/lists/pull', {
+				listId: this.list.id,
+				userId: user.id
+			}).then(() => {
+				this.users = this.users.filter(x => x.id !== user.id);
+			});
+		},
+
+		async renameList() {
+			const { canceled, result: name } = await os.dialog({
+				title: this.$ts.enterListName,
+				input: {
+					default: this.list.name
+				}
+			});
+			if (canceled) return;
+
+			await os.api('users/lists/update', {
+				listId: this.list.id,
+				name: name
+			});
+
+			this.list.name = name;
+		},
+
+		async deleteList() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.list.name }),
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			await os.api('users/lists/delete', {
+				listId: this.list.id
+			});
+			os.success();
+			this.$router.push('/my/lists');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-list-page {
+	> .members {
+		> ._content {
+			> .users {
+				> .user {
+					display: flex;
+					align-items: center;
+					padding: 16px;
+
+					> .avatar {
+						width: 50px;
+						height: 50px;
+					}
+
+					> .body {
+						flex: 1;
+						padding: 8px;
+
+						> .name {
+							display: block;
+							font-weight: bold;
+						}
+
+						> .acct {
+							opacity: 0.5;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/not-found.vue b/packages/client/src/pages/not-found.vue
new file mode 100644
index 0000000000..92d3f399f7
--- /dev/null
+++ b/packages/client/src/pages/not-found.vue
@@ -0,0 +1,25 @@
+<template>
+<div class="ipledcug">
+	<div class="_fullinfo">
+		<img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/>
+		<div>{{ $ts.notFoundDescription }}</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.notFound,
+				icon: 'fas fa-exclamation-triangle'
+			},
+		}
+	},
+});
+</script>
diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue
new file mode 100644
index 0000000000..ecd391dfbf
--- /dev/null
+++ b/packages/client/src/pages/note.vue
@@ -0,0 +1,209 @@
+<template>
+<MkSpacer :content-max="800">
+	<div class="fcuexfpr">
+		<transition name="fade" mode="out-in">
+			<div v-if="note" class="note">
+				<div class="_gap" v-if="showNext">
+					<XNotes class="_content" :pagination="next" :no-gap="true"/>
+				</div>
+
+				<div class="main _gap">
+					<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton>
+					<div class="note _gap">
+						<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_isolated"/>
+						<XNoteDetailed v-model:note="note" :key="note.id" class="_isolated note"/>
+					</div>
+					<div class="_content clips _gap" v-if="clips && clips.length > 0">
+						<div class="title">{{ $ts.clip }}</div>
+						<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
+							<b>{{ item.name }}</b>
+							<div v-if="item.description" class="description">{{ item.description }}</div>
+							<div class="user">
+								<MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/>
+							</div>
+						</MkA>
+					</div>
+					<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton>
+				</div>
+
+				<div class="_gap" v-if="showPrev">
+					<XNotes class="_content" :pagination="prev" :no-gap="true"/>
+				</div>
+			</div>
+			<MkError v-else-if="error" @retry="fetch()"/>
+			<MkLoading v-else/>
+		</transition>
+	</div>
+</MkSpacer>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import XNote from '@/components/note.vue';
+import XNoteDetailed from '@/components/note-detailed.vue';
+import XNotes from '@/components/notes.vue';
+import MkRemoteCaution from '@/components/remote-caution.vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNote,
+		XNoteDetailed,
+		XNotes,
+		MkRemoteCaution,
+		MkButton,
+	},
+	props: {
+		noteId: {
+			type: String,
+			required: true
+		}
+	},
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.note ? {
+				title: this.$ts.note,
+				subtitle: new Date(this.note.createdAt).toLocaleString(),
+				avatar: this.note.user,
+				path: `/notes/${this.note.id}`,
+				share: {
+					title: this.$t('noteOf', { user: this.note.user.name }),
+					text: this.note.text,
+				},
+				bg: 'var(--bg)',
+			} : null),
+			note: null,
+			clips: null,
+			hasPrev: false,
+			hasNext: false,
+			showPrev: false,
+			showNext: false,
+			error: null,
+			prev: {
+				endpoint: 'users/notes',
+				limit: 10,
+				params: init => ({
+					userId: this.note.userId,
+					untilId: this.note.id,
+				})
+			},
+			next: {
+				reversed: true,
+				endpoint: 'users/notes',
+				limit: 10,
+				params: init => ({
+					userId: this.note.userId,
+					sinceId: this.note.id,
+				})
+			},
+		};
+	},
+	watch: {
+		noteId: 'fetch'
+	},
+	created() {
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			this.note = null;
+			os.api('notes/show', {
+				noteId: this.noteId
+			}).then(note => {
+				this.note = note;
+				Promise.all([
+					os.api('notes/clips', {
+						noteId: note.id,
+					}),
+					os.api('users/notes', {
+						userId: note.userId,
+						untilId: note.id,
+						limit: 1,
+					}),
+					os.api('users/notes', {
+						userId: note.userId,
+						sinceId: note.id,
+						limit: 1,
+					}),
+				]).then(([clips, prev, next]) => {
+					this.clips = clips;
+					this.hasPrev = prev.length !== 0;
+					this.hasNext = next.length !== 0;
+				});
+			}).catch(e => {
+				this.error = e;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.fcuexfpr {
+	background: var(--bg);
+
+	> .note {
+		> .main {
+			> .load {
+				min-width: 0;
+				margin: 0 auto;
+				border-radius: 999px;
+
+				&.next {
+					margin-bottom: var(--margin);
+				}
+
+				&.prev {
+					margin-top: var(--margin);
+				}
+			}
+
+			> .note {
+				> .note {
+					border-radius: var(--radius);
+					background: var(--panel);
+				}
+			}
+
+			> .clips {
+				> .title {
+					font-weight: bold;
+					padding: 12px;
+				}
+
+				> .item {
+					display: block;
+					padding: 16px;
+
+					> .description {
+						padding: 8px 0;
+					}
+
+					> .user {
+						$height: 32px;
+						padding-top: 16px;
+						border-top: solid 0.5px var(--divider);
+						line-height: $height;
+
+						> .avatar {
+							width: $height;
+							height: $height;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue
new file mode 100644
index 0000000000..f8e610a719
--- /dev/null
+++ b/packages/client/src/pages/notifications.vue
@@ -0,0 +1,88 @@
+<template>
+<MkSpacer :content-max="800">
+	<div class="clupoqwt">
+		<XNotifications class="notifications" @before="before" @after="after" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
+	</div>
+</MkSpacer>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XNotifications from '@/components/notifications.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { notificationTypes } from 'misskey-js';
+
+export default defineComponent({
+	components: {
+		XNotifications
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.$ts.notifications,
+				icon: 'fas fa-bell',
+				bg: 'var(--bg)',
+				actions: [{
+					text: this.$ts.filter,
+					icon: 'fas fa-filter',
+					highlighted: this.includeTypes != null,
+					handler: this.setFilter,
+				}, {
+					text: this.$ts.markAllAsRead,
+					icon: 'fas fa-check',
+					handler: () => {
+						os.apiWithDialog('notifications/mark-all-as-read');
+					},
+				}],
+				tabs: [{
+					active: this.tab === 'all',
+					title: this.$ts.all,
+					onClick: () => { this.tab = 'all'; },
+				}, {
+					active: this.tab === 'unread',
+					title: this.$ts.unread,
+					onClick: () => { this.tab = 'unread'; },
+				},]
+			})),
+			tab: 'all',
+			includeTypes: null,
+		};
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		},
+
+		setFilter(ev) {
+			const typeItems = notificationTypes.map(t => ({
+				text: this.$t(`_notification._types.${t}`),
+				active: this.includeTypes && this.includeTypes.includes(t),
+				action: () => {
+					this.includeTypes = [t];
+				}
+			}));
+			const items = this.includeTypes != null ? [{
+				icon: 'fas fa-times',
+				text: this.$ts.clear,
+				action: () => {
+					this.includeTypes = null;
+				}
+			}, null, ...typeItems] : typeItems;
+			os.popupMenu(items, ev.currentTarget || ev.target);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.clupoqwt {
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.button.vue b/packages/client/src/pages/page-editor/els/page-editor.el.button.vue
new file mode 100644
index 0000000000..a25a892eaa
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.button.vue
@@ -0,0 +1,84 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.button }}</template>
+
+	<section class="xfhsjczc">
+		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._button.text }}</template></MkInput>
+		<MkSwitch v-model="value.primary"><span>{{ $ts._pages.blocks._button.colored }}</span></MkSwitch>
+		<MkSelect v-model="value.action">
+			<template #label>{{ $ts._pages.blocks._button.action }}</template>
+			<option value="dialog">{{ $ts._pages.blocks._button._action.dialog }}</option>
+			<option value="resetRandom">{{ $ts._pages.blocks._button._action.resetRandom }}</option>
+			<option value="pushEvent">{{ $ts._pages.blocks._button._action.pushEvent }}</option>
+			<option value="callAiScript">{{ $ts._pages.blocks._button._action.callAiScript }}</option>
+		</MkSelect>
+		<template v-if="value.action === 'dialog'">
+			<MkInput v-model="value.content"><template #label>{{ $ts._pages.blocks._button._action._dialog.content }}</template></MkInput>
+		</template>
+		<template v-else-if="value.action === 'pushEvent'">
+			<MkInput v-model="value.event"><template #label>{{ $ts._pages.blocks._button._action._pushEvent.event }}</template></MkInput>
+			<MkInput v-model="value.message"><template #label>{{ $ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput>
+			<MkSelect v-model="value.var">
+				<template #label>{{ $ts._pages.blocks._button._action._pushEvent.variable }}</template>
+				<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
+				<option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option>
+				<optgroup :label="$ts._pages.script.pageVariables">
+					<option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option>
+				</optgroup>
+				<optgroup :label="$ts._pages.script.enviromentVariables">
+					<option v-for="v in hpml.getEnvVarsByType()" :value="v">{{ v }}</option>
+				</optgroup>
+			</MkSelect>
+		</template>
+		<template v-else-if="value.action === 'callAiScript'">
+			<MkInput v-model="value.fn"><template #label>{{ $ts._pages.blocks._button._action._callAiScript.functionName }}</template></MkInput>
+		</template>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkSelect, MkInput, MkSwitch
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+		hpml: {
+			required: true,
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.text == null) this.value.text = '';
+		if (this.value.action == null) this.value.action = 'dialog';
+		if (this.value.content == null) this.value.content = null;
+		if (this.value.event == null) this.value.event = null;
+		if (this.value.message == null) this.value.message = null;
+		if (this.value.primary == null) this.value.primary = false;
+		if (this.value.var == null) this.value.var = null;
+		if (this.value.fn == null) this.value.fn = null;
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.xfhsjczc {
+	padding: 0 16px 0 16px;
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.canvas.vue b/packages/client/src/pages/page-editor/els/page-editor.el.canvas.vue
new file mode 100644
index 0000000000..5d009561e2
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.canvas.vue
@@ -0,0 +1,50 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-paint-brush"></i> {{ $ts._pages.blocks.canvas }}</template>
+
+	<section style="padding: 0 16px 0 16px;">
+		<MkInput v-model="value.name">
+			<template #prefix><i class="fas fa-magic"></i></template>
+			<template #label>{{ $ts._pages.blocks._canvas.id }}</template>
+		</MkInput>
+		<MkInput v-model="value.width" type="number">
+			<template #label>{{ $ts._pages.blocks._canvas.width }}</template>
+			<template #suffix>px</template>
+		</MkInput>
+		<MkInput v-model="value.height" type="number">
+			<template #label>{{ $ts._pages.blocks._canvas.height }}</template>
+			<template #suffix>px</template>
+		</MkInput>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.name == null) this.value.name = '';
+		if (this.value.width == null) this.value.width = 300;
+		if (this.value.height == null) this.value.height = 200;
+	},
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.counter.vue b/packages/client/src/pages/page-editor/els/page-editor.el.counter.vue
new file mode 100644
index 0000000000..3704c64250
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.counter.vue
@@ -0,0 +1,46 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.counter }}</template>
+
+	<section style="padding: 0 16px 0 16px;">
+		<MkInput v-model="value.name">
+			<template #prefix><i class="fas fa-magic"></i></template>
+			<template #label>{{ $ts._pages.blocks._counter.name }}</template>
+		</MkInput>
+		<MkInput v-model="value.text">
+			<template #label>{{ $ts._pages.blocks._counter.text }}</template>
+		</MkInput>
+		<MkInput v-model="value.inc" type="number">
+			<template #label>{{ $ts._pages.blocks._counter.inc }}</template>
+		</MkInput>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.name == null) this.value.name = '';
+	},
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.if.vue b/packages/client/src/pages/page-editor/els/page-editor.el.if.vue
new file mode 100644
index 0000000000..f76d59abe3
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.if.vue
@@ -0,0 +1,84 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-question"></i> {{ $ts._pages.blocks.if }}</template>
+	<template #func>
+		<button @click="add()" class="_button">
+			<i class="fas fa-plus"></i>
+		</button>
+	</template>
+
+	<section class="romcojzs">
+		<MkSelect v-model="value.var">
+			<template #label>{{ $ts._pages.blocks._if.variable }}</template>
+			<option v-for="v in hpml.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option>
+			<optgroup :label="$ts._pages.script.pageVariables">
+				<option v-for="v in hpml.getPageVarsByType('boolean')" :value="v">{{ v }}</option>
+			</optgroup>
+			<optgroup :label="$ts._pages.script.enviromentVariables">
+				<option v-for="v in hpml.getEnvVarsByType('boolean')" :value="v">{{ v }}</option>
+			</optgroup>
+		</MkSelect>
+
+		<XBlocks class="children" v-model="value.children" :hpml="hpml"/>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import XContainer from '../page-editor.container.vue';
+import MkSelect from '@/components/form/select.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkSelect,
+		XBlocks: defineAsyncComponent(() => import('../page-editor.blocks.vue')),
+	},
+
+	inject: ['getPageBlockList'],
+
+	props: {
+		value: {
+			required: true
+		},
+		hpml: {
+			required: true,
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.children == null) this.value.children = [];
+		if (this.value.var === undefined) this.value.var = null;
+	},
+
+	methods: {
+		async add() {
+			const { canceled, result: type } = await os.dialog({
+				type: null,
+				title: this.$ts._pages.chooseBlock,
+				select: {
+					groupedItems: this.getPageBlockList()
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			const id = uuid();
+			this.value.children.push({ id, type });
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.romcojzs {
+	padding: 0 16px 16px 16px;
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
new file mode 100644
index 0000000000..396c83f512
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
@@ -0,0 +1,72 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-image"></i> {{ $ts._pages.blocks.image }}</template>
+	<template #func>
+		<button @click="choose()">
+			<i class="fas fa-folder-open"></i>
+		</button>
+	</template>
+
+	<section class="oyyftmcf">
+		<MkDriveFileThumbnail class="preview" v-if="file" :file="file" fit="contain" @click="choose()"/>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkDriveFileThumbnail
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			file: null,
+		};
+	},
+
+	created() {
+		if (this.value.fileId === undefined) this.value.fileId = null;
+	},
+
+	mounted() {
+		if (this.value.fileId == null) {
+			this.choose();
+		} else {
+			os.api('drive/files/show', {
+				fileId: this.value.fileId
+			}).then(file => {
+				this.file = file;
+			});
+		}
+	},
+
+	methods: {
+		async choose() {
+			os.selectDriveFile(false).then(file => {
+				this.file = file;
+				this.value.fileId = file.id;
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.oyyftmcf {
+	> .preview {
+		height: 150px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.note.vue b/packages/client/src/pages/page-editor/els/page-editor.el.note.vue
new file mode 100644
index 0000000000..263b60d3e0
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.note.vue
@@ -0,0 +1,65 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-sticky-note"></i> {{ $ts._pages.blocks.note }}</template>
+
+	<section style="padding: 0 16px 0 16px;">
+		<MkInput v-model="id">
+			<template #label>{{ $ts._pages.blocks._note.id }}</template>
+			<template #caption>{{ $ts._pages.blocks._note.idDescription }}</template>
+		</MkInput>
+		<MkSwitch v-model="value.detailed"><span>{{ $ts._pages.blocks._note.detailed }}</span></MkSwitch>
+
+		<XNote v-if="note && !value.detailed" v-model:note="note" :key="note.id + ':normal'" style="margin-bottom: 16px;"/>
+		<XNoteDetailed v-if="note && value.detailed" v-model:note="note" :key="note.id + ':detail'" style="margin-bottom: 16px;"/>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import XNote from '@/components/note.vue';
+import XNoteDetailed from '@/components/note-detailed.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkInput, MkSwitch, XNote, XNoteDetailed,
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			id: this.value.note,
+			note: null,
+		};
+	},
+
+	watch: {
+		id: {
+			async handler() {
+				if (this.id && (this.id.startsWith('http://') || this.id.startsWith('https://'))) {
+					this.value.note = this.id.endsWith('/') ? this.id.substr(0, this.id.length - 1).split('/').pop() : this.id.split('/').pop();
+				} else {
+					this.value.note = this.id;
+				}
+
+				this.note = await os.api('notes/show', { noteId: this.value.note });
+			},
+			immediate: true
+		},
+	},
+
+	created() {
+		if (this.value.note == null) this.value.note = null;
+		if (this.value.detailed == null) this.value.detailed = false;
+	},
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.number-input.vue b/packages/client/src/pages/page-editor/els/page-editor.el.number-input.vue
new file mode 100644
index 0000000000..3a2f4a762b
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.number-input.vue
@@ -0,0 +1,46 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.numberInput }}</template>
+
+	<section style="padding: 0 16px 0 16px;">
+		<MkInput v-model="value.name">
+			<template #prefix><i class="fas fa-magic"></i></template>
+			<template #label>{{ $ts._pages.blocks._numberInput.name }}</template>
+		</MkInput>
+		<MkInput v-model="value.text">
+			<template #label>{{ $ts._pages.blocks._numberInput.text }}</template>
+		</MkInput>
+		<MkInput v-model="value.default" type="number">
+			<template #label>{{ $ts._pages.blocks._numberInput.default }}</template>
+		</MkInput>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.name == null) this.value.name = '';
+	},
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.post.vue b/packages/client/src/pages/page-editor/els/page-editor.el.post.vue
new file mode 100644
index 0000000000..780786144e
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.post.vue
@@ -0,0 +1,43 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-paper-plane"></i> {{ $ts._pages.blocks.post }}</template>
+
+	<section style="padding: 16px;">
+		<MkTextarea v-model="value.text"><template #label>{{ $ts._pages.blocks._post.text }}</template></MkTextarea>
+		<MkSwitch v-model="value.attachCanvasImage"><span>{{ $ts._pages.blocks._post.attachCanvasImage }}</span></MkSwitch>
+		<MkInput v-if="value.attachCanvasImage" v-model="value.canvasId"><template #label>{{ $ts._pages.blocks._post.canvasId }}</template></MkInput>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkTextarea, MkInput, MkSwitch
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.text == null) this.value.text = '';
+		if (this.value.attachCanvasImage == null) this.value.attachCanvasImage = false;
+		if (this.value.canvasId == null) this.value.canvasId = '';
+	},
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue b/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue
new file mode 100644
index 0000000000..f01a47c54a
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue
@@ -0,0 +1,50 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.radioButton }}</template>
+
+	<section style="padding: 0 16px 16px 16px;">
+		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._radioButton.name }}</template></MkInput>
+		<MkInput v-model="value.title"><template #label>{{ $ts._pages.blocks._radioButton.title }}</template></MkInput>
+		<MkTextarea v-model="values"><template #label>{{ $ts._pages.blocks._radioButton.values }}</template></MkTextarea>
+		<MkInput v-model="value.default"><template #label>{{ $ts._pages.blocks._radioButton.default }}</template></MkInput>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkTextarea, MkInput
+	},
+	props: {
+		value: {
+			required: true
+		},
+	},
+	data() {
+		return {
+			values: '',
+		};
+	},
+	watch: {
+		values: {
+			handler() {
+				this.value.values = this.values.split('\n');
+			},
+			deep: true
+		}
+	},
+	created() {
+		if (this.value.name == null) this.value.name = '';
+		if (this.value.title == null) this.value.title = '';
+		if (this.value.values == null) this.value.values = [];
+		this.values = this.value.values.join('\n');
+	},
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.section.vue b/packages/client/src/pages/page-editor/els/page-editor.el.section.vue
new file mode 100644
index 0000000000..16e32d8400
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.section.vue
@@ -0,0 +1,96 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-sticky-note"></i> {{ value.title }}</template>
+	<template #func>
+		<button @click="rename()" class="_button">
+			<i class="fas fa-pencil-alt"></i>
+		</button>
+		<button @click="add()" class="_button">
+			<i class="fas fa-plus"></i>
+		</button>
+	</template>
+
+	<section class="ilrvjyvi">
+		<XBlocks class="children" v-model="value.children" :hpml="hpml"/>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import XContainer from '../page-editor.container.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer,
+		XBlocks: defineAsyncComponent(() => import('../page-editor.blocks.vue')),
+	},
+
+	inject: ['getPageBlockList'],
+
+	props: {
+		value: {
+			required: true
+		},
+		hpml: {
+			required: true,
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.title == null) this.value.title = null;
+		if (this.value.children == null) this.value.children = [];
+	},
+
+	mounted() {
+		if (this.value.title == null) {
+			this.rename();
+		}
+	},
+
+	methods: {
+		async rename() {
+			const { canceled, result: title } = await os.dialog({
+				title: 'Enter title',
+				input: {
+					type: 'text',
+					default: this.value.title
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			this.value.title = title;
+		},
+
+		async add() {
+			const { canceled, result: type } = await os.dialog({
+				type: null,
+				title: this.$ts._pages.chooseBlock,
+				select: {
+					groupedItems: this.getPageBlockList()
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			const id = uuid();
+			this.value.children.push({ id, type });
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ilrvjyvi {
+	> .children {
+		padding: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.switch.vue b/packages/client/src/pages/page-editor/els/page-editor.el.switch.vue
new file mode 100644
index 0000000000..e72f7b44d0
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.switch.vue
@@ -0,0 +1,46 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.switch }}</template>
+
+	<section class="kjuadyyj">
+		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._switch.name }}</template></MkInput>
+		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._switch.text }}</template></MkInput>
+		<MkSwitch v-model="value.default"><span>{{ $ts._pages.blocks._switch.default }}</span></MkSwitch>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkSwitch, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.name == null) this.value.name = '';
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.kjuadyyj {
+	padding: 0 16px 16px 16px;
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.text-input.vue b/packages/client/src/pages/page-editor/els/page-editor.el.text-input.vue
new file mode 100644
index 0000000000..908862cf07
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.text-input.vue
@@ -0,0 +1,39 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.textInput }}</template>
+
+	<section style="padding: 0 16px 0 16px;">
+		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._textInput.name }}</template></MkInput>
+		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._textInput.text }}</template></MkInput>
+		<MkInput v-model="value.default" type="text"><template #label>{{ $ts._pages.blocks._textInput.default }}</template></MkInput>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.name == null) this.value.name = '';
+	},
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.text.vue b/packages/client/src/pages/page-editor/els/page-editor.el.text.vue
new file mode 100644
index 0000000000..05b1a9c67d
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.text.vue
@@ -0,0 +1,57 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-align-left"></i> {{ $ts._pages.blocks.text }}</template>
+
+	<section class="vckmsadr">
+		<textarea v-model="value.text"></textarea>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.text == null) this.value.text = '';
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.vckmsadr {
+	> textarea {
+		display: block;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		width: 100%;
+		min-width: 100%;
+		min-height: 150px;
+		border: none;
+		box-shadow: none;
+		padding: 16px;
+		background: transparent;
+		color: var(--fg);
+		font-size: 14px;
+		box-sizing: border-box;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.textarea-input.vue b/packages/client/src/pages/page-editor/els/page-editor.el.textarea-input.vue
new file mode 100644
index 0000000000..bb37158ecb
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.textarea-input.vue
@@ -0,0 +1,40 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.textareaInput }}</template>
+
+	<section style="padding: 0 16px 16px 16px;">
+		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._textareaInput.name }}</template></MkInput>
+		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._textareaInput.text }}</template></MkInput>
+		<MkTextarea v-model="value.default"><template #label>{{ $ts._pages.blocks._textareaInput.default }}</template></MkTextarea>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkInput from '@/components/form/input.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer, MkTextarea, MkInput
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.name == null) this.value.name = '';
+	},
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.textarea.vue b/packages/client/src/pages/page-editor/els/page-editor.el.textarea.vue
new file mode 100644
index 0000000000..4ca83da17c
--- /dev/null
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.textarea.vue
@@ -0,0 +1,57 @@
+<template>
+<XContainer @remove="() => $emit('remove')" :draggable="true">
+	<template #header><i class="fas fa-align-left"></i> {{ $ts._pages.blocks.textarea }}</template>
+
+	<section class="ihymsbbe">
+		<textarea v-model="value.text"></textarea>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XContainer from '../page-editor.container.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XContainer
+	},
+
+	props: {
+		value: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	created() {
+		if (this.value.text == null) this.value.text = '';
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.ihymsbbe {
+	> textarea {
+		display: block;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		width: 100%;
+		min-width: 100%;
+		min-height: 150px;
+		border: none;
+		box-shadow: none;
+		padding: 16px;
+		background: transparent;
+		color: var(--fg);
+		font-size: 14px;
+		box-sizing: border-box;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/page-editor.blocks.vue b/packages/client/src/pages/page-editor/page-editor.blocks.vue
new file mode 100644
index 0000000000..b91d9abae8
--- /dev/null
+++ b/packages/client/src/pages/page-editor/page-editor.blocks.vue
@@ -0,0 +1,78 @@
+<template>
+<XDraggable tag="div" v-model="blocks" item-key="id" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5">
+	<template #item="{element}">
+		<component :is="'x-' + element.type" :value="element" @update:value="updateItem" @remove="() => removeItem(element)" :hpml="hpml"/>
+	</template>
+</XDraggable>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import XSection from './els/page-editor.el.section.vue';
+import XText from './els/page-editor.el.text.vue';
+import XTextarea from './els/page-editor.el.textarea.vue';
+import XImage from './els/page-editor.el.image.vue';
+import XButton from './els/page-editor.el.button.vue';
+import XTextInput from './els/page-editor.el.text-input.vue';
+import XTextareaInput from './els/page-editor.el.textarea-input.vue';
+import XNumberInput from './els/page-editor.el.number-input.vue';
+import XSwitch from './els/page-editor.el.switch.vue';
+import XIf from './els/page-editor.el.if.vue';
+import XPost from './els/page-editor.el.post.vue';
+import XCounter from './els/page-editor.el.counter.vue';
+import XRadioButton from './els/page-editor.el.radio-button.vue';
+import XCanvas from './els/page-editor.el.canvas.vue';
+import XNote from './els/page-editor.el.note.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
+		XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas, XNote
+	},
+
+	props: {
+		modelValue: {
+			type: Array,
+			required: true
+		},
+		hpml: {
+			required: true,
+		},
+	},
+
+	emits: ['update:modelValue'],
+
+	computed: {
+		blocks: {
+			get() {
+				return this.modelValue;
+			},
+			set(value) {
+				this.$emit('update:modelValue', value);
+			}
+		}
+	},
+
+	methods: {
+		updateItem(v) {
+			const i = this.blocks.findIndex(x => x.id === v.id);
+			const newValue = [
+				...this.blocks.slice(0, i),
+				v,
+				...this.blocks.slice(i + 1)
+			];
+			this.$emit('update:modelValue', newValue);
+		},
+
+		removeItem(el) {
+			const i = this.blocks.findIndex(x => x.id === el.id);
+			const newValue = [
+				...this.blocks.slice(0, i),
+				...this.blocks.slice(i + 1)
+			];
+			this.$emit('update:modelValue', newValue);
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/page-editor/page-editor.container.vue b/packages/client/src/pages/page-editor/page-editor.container.vue
new file mode 100644
index 0000000000..afd261fac7
--- /dev/null
+++ b/packages/client/src/pages/page-editor/page-editor.container.vue
@@ -0,0 +1,159 @@
+<template>
+<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
+	<header>
+		<div class="title"><slot name="header"></slot></div>
+		<div class="buttons">
+			<slot name="func"></slot>
+			<button v-if="removable" @click="remove()" class="_button">
+				<i class="fas fa-trash-alt"></i>
+			</button>
+			<button v-if="draggable" class="drag-handle _button">
+				<i class="fas fa-bars"></i>
+			</button>
+			<button @click="toggleContent(!showBody)" class="_button">
+				<template v-if="showBody"><i class="fas fa-angle-up"></i></template>
+				<template v-else><i class="fas fa-angle-down"></i></template>
+			</button>
+		</div>
+	</header>
+	<p v-show="showBody" class="error" v-if="error != null">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
+	<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
+	<div v-show="showBody" class="body">
+		<slot></slot>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		expanded: {
+			type: Boolean,
+			default: true
+		},
+		removable: {
+			type: Boolean,
+			default: true
+		},
+		draggable: {
+			type: Boolean,
+			default: false
+		},
+		error: {
+			required: false,
+			default: null
+		},
+		warn: {
+			required: false,
+			default: null
+		}
+	},
+	emits: ['toggle', 'remove'],
+	data() {
+		return {
+			showBody: this.expanded,
+		};
+	},
+	methods: {
+		toggleContent(show: boolean) {
+			this.showBody = show;
+			this.$emit('toggle', show);
+		},
+		remove() {
+			this.$emit('remove');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.cpjygsrt {
+	position: relative;
+	overflow: hidden;
+	background: var(--panel);
+	border: solid 2px var(--X12);
+	border-radius: 6px;
+
+	&:hover {
+		border: solid 2px var(--X13);
+	}
+
+	&.warn {
+		border: solid 2px #dec44c;
+	}
+
+	&.error {
+		border: solid 2px #f00;
+	}
+
+	& + .cpjygsrt {
+		margin-top: 16px;
+	}
+
+	> header {
+		> .title {
+			z-index: 1;
+			margin: 0;
+			padding: 0 16px;
+			line-height: 42px;
+			font-size: 0.9em;
+			font-weight: bold;
+			box-shadow: 0 1px rgba(#000, 0.07);
+
+			> i {
+				margin-right: 6px;
+			}
+
+			&:empty {
+				display: none;
+			}
+		}
+
+		> .buttons {
+			position: absolute;
+			z-index: 2;
+			top: 0;
+			right: 0;
+
+			> button {
+				padding: 0;
+				width: 42px;
+				font-size: 0.9em;
+				line-height: 42px;
+			}
+
+			.drag-handle {
+				cursor: move;
+			}
+		}
+	}
+
+	> .warn {
+		color: #b19e49;
+		margin: 0;
+		padding: 16px 16px 0 16px;
+		font-size: 14px;
+	}
+
+	> .error {
+		color: #f00;
+		margin: 0;
+		padding: 16px 16px 0 16px;
+		font-size: 14px;
+	}
+
+	> .body {
+		::v-deep(.juejbjww), ::v-deep(.eiipwacr) {
+			&:not(.inline):first-child {
+				margin-top: 28px;
+			}
+
+			&:not(.inline):last-child {
+				margin-bottom: 20px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/page-editor.script-block.vue b/packages/client/src/pages/page-editor/page-editor.script-block.vue
new file mode 100644
index 0000000000..07958c902b
--- /dev/null
+++ b/packages/client/src/pages/page-editor/page-editor.script-block.vue
@@ -0,0 +1,281 @@
+<template>
+<XContainer :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable">
+	<template #header><i v-if="icon" :class="icon"></i> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template>
+	<template #func>
+		<button @click="changeType()" class="_button">
+			<i class="fas fa-pencil-alt"></i>
+		</button>
+	</template>
+
+	<section v-if="modelValue.type === null" class="pbglfege" @click="changeType()">
+		{{ $ts._pages.script.emptySlot }}
+	</section>
+	<section v-else-if="modelValue.type === 'text'" class="tbwccoaw">
+		<input v-model="modelValue.value"/>
+	</section>
+	<section v-else-if="modelValue.type === 'multiLineText'" class="tbwccoaw">
+		<textarea v-model="modelValue.value"></textarea>
+	</section>
+	<section v-else-if="modelValue.type === 'textList'" class="tbwccoaw">
+		<textarea v-model="modelValue.value" :placeholder="$ts._pages.script.blocks._textList.info"></textarea>
+	</section>
+	<section v-else-if="modelValue.type === 'number'" class="tbwccoaw">
+		<input v-model="modelValue.value" type="number"/>
+	</section>
+	<section v-else-if="modelValue.type === 'ref'" class="hpdwcrvs">
+		<select v-model="modelValue.value">
+			<option v-for="v in hpml.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
+			<optgroup :label="$ts._pages.script.argVariables">
+				<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
+			</optgroup>
+			<optgroup :label="$ts._pages.script.pageVariables">
+				<option v-for="v in hpml.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
+			</optgroup>
+			<optgroup :label="$ts._pages.script.enviromentVariables">
+				<option v-for="v in hpml.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
+			</optgroup>
+		</select>
+	</section>
+	<section v-else-if="modelValue.type === 'aiScriptVar'" class="tbwccoaw">
+		<input v-model="modelValue.value"/>
+	</section>
+	<section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
+		<MkTextarea v-model="slots">
+			<template #label>{{ $ts._pages.script.blocks._fn.slots }}</template>
+			<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
+		</MkTextarea>
+		<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/>
+	</section>
+	<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
+		<XV v-for="(x, i) in modelValue.args" v-model="value.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name" :key="i"/>
+	</section>
+	<section v-else class="" style="padding:16px;">
+		<XV v-for="(x, i) in modelValue.args" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots" :key="i"/>
+	</section>
+</XContainer>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import XContainer from './page-editor.container.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import { blockDefs } from '@/scripts/hpml/index';
+import * as os from '@/os';
+import { isLiteralValue } from '@/scripts/hpml/expr';
+import { funcDefs } from '@/scripts/hpml/lib';
+
+export default defineComponent({
+	components: {
+		XContainer, MkTextarea,
+		XV: defineAsyncComponent(() => import('./page-editor.script-block.vue')),
+	},
+
+	inject: ['getScriptBlockList'],
+
+	props: {
+		getExpectedType: {
+			required: false,
+			default: null
+		},
+		modelValue: {
+			required: true
+		},
+		title: {
+			required: false
+		},
+		removable: {
+			required: false,
+			default: false
+		},
+		hpml: {
+			required: true,
+		},
+		name: {
+			required: true,
+		},
+		fnSlots: {
+			required: false,
+		},
+		draggable: {
+			required: false,
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			error: null,
+			warn: null,
+			slots: '',
+		};
+	},
+
+	computed: {
+		icon(): any {
+			if (this.modelValue.type === null) return null;
+			if (this.modelValue.type.startsWith('fn:')) return 'fas fa-plug';
+			return blockDefs.find(x => x.type === this.modelValue.type).icon;
+		},
+		typeText(): any {
+			if (this.modelValue.type === null) return null;
+			if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1];
+			return this.$t(`_pages.script.blocks.${this.modelValue.type}`);
+		},
+	},
+
+	watch: {
+		slots: {
+			handler() {
+				this.modelValue.value.slots = this.slots.split('\n').map(x => ({
+					name: x,
+					type: null
+				}));
+			},
+			deep: true
+		}
+	},
+
+	created() {
+		if (this.modelValue.value == null) this.modelValue.value = null;
+
+		if (this.modelValue.value && this.modelValue.value.slots) this.slots = this.modelValue.value.slots.map(x => x.name).join('\n');
+
+		this.$watch(() => this.modelValue.type, (t) => {
+			this.warn = null;
+
+			if (this.modelValue.type === 'fn') {
+				const id = uuid();
+				this.modelValue.value = {
+					slots: [],
+					expression: { id, type: null }
+				};
+				return;
+			}
+
+			if (this.modelValue.type && this.modelValue.type.startsWith('fn:')) {
+				const fnName = this.modelValue.type.split(':')[1];
+				const fn = this.hpml.getVarByName(fnName);
+
+				const empties = [];
+				for (let i = 0; i < fn.value.slots.length; i++) {
+					const id = uuid();
+					empties.push({ id, type: null });
+				}
+				this.modelValue.args = empties;
+				return;
+			}
+
+			if (isLiteralValue(this.modelValue)) return;
+
+			const empties = [];
+			for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
+				const id = uuid();
+				empties.push({ id, type: null });
+			}
+			this.modelValue.args = empties;
+
+			for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
+				const inType = funcDefs[this.modelValue.type].in[i];
+				if (typeof inType !== 'number') {
+					if (inType === 'number') this.modelValue.args[i].type = 'number';
+					if (inType === 'string') this.modelValue.args[i].type = 'text';
+				}
+			}
+		});
+
+		this.$watch(() => this.modelValue.args, (args) => {
+			if (args == null) {
+				this.warn = null;
+				return;
+			}
+			const emptySlotIndex = args.findIndex(x => x.type === null);
+			if (emptySlotIndex !== -1 && emptySlotIndex < args.length) {
+				this.warn = {
+					slot: emptySlotIndex
+				};
+			} else {
+				this.warn = null;
+			}
+		}, {
+			deep: true
+		});
+
+		this.$watch(() => this.hpml.variables, () => {
+			if (this.type != null && this.modelValue) {
+				this.error = this.hpml.typeCheck(this.modelValue);
+			}
+		}, {
+			deep: true
+		});
+	},
+
+	methods: {
+		async changeType() {
+			const { canceled, result: type } = await os.dialog({
+				type: null,
+				title: this.$ts._pages.selectType,
+				select: {
+					groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null)
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			this.modelValue.type = type;
+		},
+
+		_getExpectedType(slot: number) {
+			return this.hpml.getExpectedType(this.modelValue, slot);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.turmquns {
+	opacity: 0.7;
+}
+
+.pbglfege {
+	opacity: 0.5;
+	padding: 16px;
+	text-align: center;
+	cursor: pointer;
+	color: var(--fg);
+}
+
+.tbwccoaw {
+	> input,
+	> textarea {
+		display: block;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		appearance: none;
+		width: 100%;
+		max-width: 100%;
+		min-width: 100%;
+		border: none;
+		box-shadow: none;
+		padding: 16px;
+		font-size: 16px;
+		background: transparent;
+		color: var(--fg);
+		box-sizing: border-box;
+	}
+
+	> textarea {
+		min-height: 100px;
+	}
+}
+
+.hpdwcrvs {
+	padding: 16px;
+
+	> select {
+		display: block;
+		padding: 4px;
+		font-size: 16px;
+		width: 100%;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue
new file mode 100644
index 0000000000..684b1f8c75
--- /dev/null
+++ b/packages/client/src/pages/page-editor/page-editor.vue
@@ -0,0 +1,561 @@
+<template>
+<div>
+	<div class="jqqmcavi" style="margin: 16px;">
+		<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton>
+		<MkButton inline @click="save" primary class="button" v-if="!readonly"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+		<MkButton inline @click="duplicate" class="button" v-if="pageId"><i class="fas fa-copy"></i> {{ $ts.duplicate }}</MkButton>
+		<MkButton inline @click="del" class="button" v-if="pageId && !readonly" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
+	</div>
+
+	<div v-if="tab === 'settings'">
+		<div style="padding: 16px;" class="_formRoot">
+			<MkInput v-model="title" class="_formBlock">
+				<template #label>{{ $ts._pages.title }}</template>
+			</MkInput>
+
+			<MkInput v-model="summary" class="_formBlock">
+				<template #label>{{ $ts._pages.summary }}</template>
+			</MkInput>
+
+			<MkInput v-model="name" class="_formBlock">
+				<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
+				<template #label>{{ $ts._pages.url }}</template>
+			</MkInput>
+
+			<MkSwitch v-model="alignCenter" class="_formBlock">{{ $ts._pages.alignCenter }}</MkSwitch>
+
+			<MkSelect v-model="font" class="_formBlock">
+				<template #label>{{ $ts._pages.font }}</template>
+				<option value="serif">{{ $ts._pages.fontSerif }}</option>
+				<option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option>
+			</MkSelect>
+
+			<MkSwitch v-model="hideTitleWhenPinned" class="_formBlock">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch>
+
+			<div class="eyeCatch">
+				<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="fas fa-plus"></i> {{ $ts._pages.eyeCatchingImageSet }}</MkButton>
+				<div v-else-if="eyeCatchingImage">
+					<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/>
+					<MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><i class="fas fa-trash-alt"></i> {{ $ts._pages.eyeCatchingImageRemove }}</MkButton>
+				</div>
+			</div>
+		</div>
+	</div>
+
+	<div v-else-if="tab === 'contents'">
+		<div style="padding: 16px;">
+			<XBlocks class="content" v-model="content" :hpml="hpml"/>
+
+			<MkButton @click="add()" v-if="!readonly"><i class="fas fa-plus"></i></MkButton>
+		</div>
+	</div>
+
+	<div v-else-if="tab === 'variables'">
+		<div class="qmuvgica">
+			<XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
+				<template #item="{element}">
+					<XVariable
+						:modelValue="element"
+						:removable="true"
+						@remove="() => removeVariable(element)"
+						:hpml="hpml"
+						:name="element.name"
+						:title="element.name"
+						:draggable="true"
+					/>
+				</template>
+			</XDraggable>
+
+			<MkButton @click="addVariable()" class="add" v-if="!readonly"><i class="fas fa-plus"></i></MkButton>
+		</div>
+	</div>
+
+	<div v-else-if="tab === 'script'">
+		<div>
+			<MkTextarea class="_code" v-model="script"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
+import 'prismjs';
+import { highlight, languages } from 'prismjs/components/prism-core';
+import 'prismjs/components/prism-clike';
+import 'prismjs/components/prism-javascript';
+import 'prismjs/themes/prism-okaidia.css';
+import 'vue-prism-editor/dist/prismeditor.min.css';
+import { v4 as uuid } from 'uuid';
+import XVariable from './page-editor.script-block.vue';
+import XBlocks from './page-editor.blocks.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkSelect from '@/components/form/select.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import MkInput from '@/components/form/input.vue';
+import { blockDefs } from '@/scripts/hpml/index';
+import { HpmlTypeChecker } from '@/scripts/hpml/type-checker';
+import { url } from '@/config';
+import { collectPageVars } from '@/scripts/collect-page-vars';
+import * as os from '@/os';
+import { selectFile } from '@/scripts/select-file';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
+		XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput,
+	},
+
+	props: {
+		initPageId: {
+			type: String,
+			required: false
+		},
+		initPageName: {
+			type: String,
+			required: false
+		},
+		initUser: {
+			type: String,
+			required: false
+		},
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => {
+				let title = this.$ts._pages.newPage;
+				if (this.initPageId) {
+					title = this.$ts._pages.editPage;
+				}
+				else if (this.initPageName && this.initUser) {
+					title = this.$ts._pages.readPage;
+				}
+				return {
+					title: title,
+					icon: 'fas fa-pencil-alt',
+					bg: 'var(--bg)',
+					tabs: [{
+						active: this.tab === 'settings',
+						title: this.$ts._pages.pageSetting,
+						icon: 'fas fa-cog',
+						onClick: () => { this.tab = 'settings'; },
+					}, {
+						active: this.tab === 'contents',
+						title: this.$ts._pages.contents,
+						icon: 'fas fa-sticky-note',
+						onClick: () => { this.tab = 'contents'; },
+					}, {
+						active: this.tab === 'variables',
+						title: this.$ts._pages.variables,
+						icon: 'fas fa-magic',
+						onClick: () => { this.tab = 'variables'; },
+					}, {
+						active: this.tab === 'script',
+						title: this.$ts.script,
+						icon: 'fas fa-code',
+						onClick: () => { this.tab = 'script'; },
+					}],
+				};
+			}),
+			tab: 'settings',
+			author: this.$i,
+			readonly: false,
+			page: null,
+			pageId: null,
+			currentName: null,
+			title: '',
+			summary: null,
+			name: Date.now().toString(),
+			eyeCatchingImage: null,
+			eyeCatchingImageId: null,
+			font: 'sans-serif',
+			content: [],
+			alignCenter: false,
+			hideTitleWhenPinned: false,
+			variables: [],
+			hpml: null,
+			script: '',
+			url,
+		};
+	},
+
+	watch: {
+		async eyeCatchingImageId() {
+			if (this.eyeCatchingImageId == null) {
+				this.eyeCatchingImage = null;
+			} else {
+				this.eyeCatchingImage = await os.api('drive/files/show', {
+					fileId: this.eyeCatchingImageId,
+				});
+			}
+		},
+	},
+
+	async created() {
+		this.hpml = new HpmlTypeChecker();
+
+		this.$watch('variables', () => {
+			this.hpml.variables = this.variables;
+		}, { deep: true });
+
+		this.$watch('content', () => {
+			this.hpml.pageVars = collectPageVars(this.content);
+		}, { deep: true });
+
+		if (this.initPageId) {
+			this.page = await os.api('pages/show', {
+				pageId: this.initPageId,
+			});
+		} else if (this.initPageName && this.initUser) {
+			this.page = await os.api('pages/show', {
+				name: this.initPageName,
+				username: this.initUser,
+			});
+			this.readonly = true;
+		}
+
+		if (this.page) {
+			this.author = this.page.user;
+			this.pageId = this.page.id;
+			this.title = this.page.title;
+			this.name = this.page.name;
+			this.currentName = this.page.name;
+			this.summary = this.page.summary;
+			this.font = this.page.font;
+			this.script = this.page.script;
+			this.hideTitleWhenPinned = this.page.hideTitleWhenPinned;
+			this.alignCenter = this.page.alignCenter;
+			this.content = this.page.content;
+			this.variables = this.page.variables;
+			this.eyeCatchingImageId = this.page.eyeCatchingImageId;
+		} else {
+			const id = uuid();
+			this.content = [{
+				id,
+				type: 'text',
+				text: 'Hello World!'
+			}];
+		}
+	},
+
+	provide() {
+		return {
+			readonly: this.readonly,
+			getScriptBlockList: this.getScriptBlockList,
+			getPageBlockList: this.getPageBlockList
+		}
+	},
+
+	methods: {
+		getSaveOptions() {
+			return {
+				title: this.title.trim(),
+				name: this.name.trim(),
+				summary: this.summary,
+				font: this.font,
+				script: this.script,
+				hideTitleWhenPinned: this.hideTitleWhenPinned,
+				alignCenter: this.alignCenter,
+				content: this.content,
+				variables: this.variables,
+				eyeCatchingImageId: this.eyeCatchingImageId,
+			};
+		},
+
+		save() {
+			const options = this.getSaveOptions();
+
+			const onError = err => {
+				if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') {
+					if (err.info.param == 'name') {
+						os.dialog({
+							type: 'error',
+							title: this.$ts._pages.invalidNameTitle,
+							text: this.$ts._pages.invalidNameText
+						});
+					}
+				} else if (err.code == 'NAME_ALREADY_EXISTS') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts._pages.nameAlreadyExists
+					});
+				}
+			};
+
+			if (this.pageId) {
+				options.pageId = this.pageId;
+				os.api('pages/update', options)
+				.then(page => {
+					this.currentName = this.name.trim();
+					os.dialog({
+						type: 'success',
+						text: this.$ts._pages.updated
+					});
+				}).catch(onError);
+			} else {
+				os.api('pages/create', options)
+				.then(page => {
+					this.pageId = page.id;
+					this.currentName = this.name.trim();
+					os.dialog({
+						type: 'success',
+						text: this.$ts._pages.created
+					});
+					this.$router.push(`/pages/edit/${this.pageId}`);
+				}).catch(onError);
+			}
+		},
+
+		del() {
+			os.dialog({
+				type: 'warning',
+				text: this.$t('removeAreYouSure', { x: this.title.trim() }),
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				os.api('pages/delete', {
+					pageId: this.pageId,
+				}).then(() => {
+					os.dialog({
+						type: 'success',
+						text: this.$ts._pages.deleted
+					});
+					this.$router.push(`/pages`);
+				});
+			});
+		},
+
+		duplicate() {
+			this.title = this.title + ' - copy';
+			this.name = this.name + '-copy';
+			os.api('pages/create', this.getSaveOptions()).then(page => {
+				this.pageId = page.id;
+				this.currentName = this.name.trim();
+				os.dialog({
+					type: 'success',
+					text: this.$ts._pages.created
+				});
+				this.$router.push(`/pages/edit/${this.pageId}`);
+			});
+		},
+
+		async add() {
+			const { canceled, result: type } = await os.dialog({
+				type: null,
+				title: this.$ts._pages.chooseBlock,
+				select: {
+					groupedItems: this.getPageBlockList()
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			const id = uuid();
+			this.content.push({ id, type });
+		},
+
+		async addVariable() {
+			let { canceled, result: name } = await os.dialog({
+				title: this.$ts._pages.enterVariableName,
+				input: {
+					type: 'text',
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			name = name.trim();
+
+			if (this.hpml.isUsedName(name)) {
+				os.dialog({
+					type: 'error',
+					text: this.$ts._pages.variableNameIsAlreadyUsed
+				});
+				return;
+			}
+
+			const id = uuid();
+			this.variables.push({ id, name, type: null });
+		},
+
+		removeVariable(v) {
+			this.variables = this.variables.filter(x => x.name !== v.name);
+		},
+
+		getPageBlockList() {
+			return [{
+				label: this.$ts._pages.contentBlocks,
+				items: [
+					{ value: 'section', text: this.$ts._pages.blocks.section },
+					{ value: 'text', text: this.$ts._pages.blocks.text },
+					{ value: 'image', text: this.$ts._pages.blocks.image },
+					{ value: 'textarea', text: this.$ts._pages.blocks.textarea },
+					{ value: 'note', text: this.$ts._pages.blocks.note },
+					{ value: 'canvas', text: this.$ts._pages.blocks.canvas },
+				]
+			}, {
+				label: this.$ts._pages.inputBlocks,
+				items: [
+					{ value: 'button', text: this.$ts._pages.blocks.button },
+					{ value: 'radioButton', text: this.$ts._pages.blocks.radioButton },
+					{ value: 'textInput', text: this.$ts._pages.blocks.textInput },
+					{ value: 'textareaInput', text: this.$ts._pages.blocks.textareaInput },
+					{ value: 'numberInput', text: this.$ts._pages.blocks.numberInput },
+					{ value: 'switch', text: this.$ts._pages.blocks.switch },
+					{ value: 'counter', text: this.$ts._pages.blocks.counter }
+				]
+			}, {
+				label: this.$ts._pages.specialBlocks,
+				items: [
+					{ value: 'if', text: this.$ts._pages.blocks.if },
+					{ value: 'post', text: this.$ts._pages.blocks.post }
+				]
+			}];
+		},
+
+		getScriptBlockList(type: string = null) {
+			const list = [];
+
+			const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number');
+
+			for (const block of blocks) {
+				const category = list.find(x => x.category === block.category);
+				if (category) {
+					category.items.push({
+						value: block.type,
+						text: this.$t(`_pages.script.blocks.${block.type}`)
+					});
+				} else {
+					list.push({
+						category: block.category,
+						label: this.$t(`_pages.script.categories.${block.category}`),
+						items: [{
+							value: block.type,
+							text: this.$t(`_pages.script.blocks.${block.type}`)
+						}]
+					});
+				}
+			}
+
+			const userFns = this.variables.filter(x => x.type === 'fn');
+			if (userFns.length > 0) {
+				list.unshift({
+					label: this.$t(`_pages.script.categories.fn`),
+					items: userFns.map(v => ({
+						value: 'fn:' + v.name,
+						text: v.name
+					}))
+				});
+			}
+
+			return list;
+		},
+
+		setEyeCatchingImage(e) {
+			selectFile(e.currentTarget || e.target, null, false).then(file => {
+				this.eyeCatchingImageId = file.id;
+			});
+		},
+
+		removeEyeCatchingImage() {
+			this.eyeCatchingImageId = null;
+		},
+
+		highlighter(code) {
+			return highlight(code, languages.js, 'javascript');
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.jqqmcavi {
+	> .button {
+		& + .button {
+			margin-left: 8px;
+		}
+	}
+}
+
+.gwbmwxkm {
+	position: relative;
+
+	> header {
+		> .title {
+			z-index: 1;
+			margin: 0;
+			padding: 0 16px;
+			line-height: 42px;
+			font-size: 0.9em;
+			font-weight: bold;
+			box-shadow: 0 1px rgba(#000, 0.07);
+
+			> i {
+				margin-right: 6px;
+			}
+
+			&:empty {
+				display: none;
+			}
+		}
+
+		> .buttons {
+			position: absolute;
+			z-index: 2;
+			top: 0;
+			right: 0;
+
+			> button {
+				padding: 0;
+				width: 42px;
+				font-size: 0.9em;
+				line-height: 42px;
+			}
+		}
+	}
+
+	> section {
+		padding: 0 32px 32px 32px;
+
+		@media (max-width: 500px) {
+			padding: 0 16px 16px 16px;
+		}
+
+		> .view {
+			display: inline-block;
+			margin: 16px 0 0 0;
+			font-size: 14px;
+		}
+
+		> .content {
+			margin-bottom: 16px;
+		}
+
+		> .eyeCatch {
+			margin-bottom: 16px;
+
+			> div {
+				> img {
+					max-width: 100%;
+				}
+			}
+		}
+	}
+}
+
+.qmuvgica {
+	padding: 16px;
+
+	> .variables {
+		margin-bottom: 16px;
+	}
+
+	> .add {
+		margin-bottom: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue
new file mode 100644
index 0000000000..1eff1a98cb
--- /dev/null
+++ b/packages/client/src/pages/page.vue
@@ -0,0 +1,311 @@
+<template>
+<div>
+	<transition name="fade" mode="out-in">
+		<div v-if="page" class="xcukqgmh" :key="page.id" v-size="{ max: [450] }">
+			<div class="_block main">
+				<!--
+				<div class="header">
+					<h1>{{ page.title }}</h1>
+				</div>
+				-->
+				<div class="banner">
+					<img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/>
+				</div>
+				<div class="content">
+					<XPage :page="page"/>
+				</div>
+				<div class="actions">
+					<div class="like">
+						<MkButton class="button" @click="unlike()" v-if="page.isLiked" v-tooltip="$ts._pages.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
+						<MkButton class="button" @click="like()" v-else v-tooltip="$ts._pages.like"><i class="far fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
+					</div>
+					<div class="other">
+						<button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
+						<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
+					</div>
+				</div>
+				<div class="user">
+					<MkAvatar :user="page.user" class="avatar"/>
+					<div class="name">
+						<MkUserName :user="page.user" style="display: block;"/>
+						<MkAcct :user="page.user"/>
+					</div>
+					<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
+				</div>
+				<div class="links">
+					<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
+					<template v-if="$i && $i.id === page.userId">
+						<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
+						<button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button>
+						<button v-else @click="pin(true)" class="link _textButton">{{ $ts.pin }}</button>
+					</template>
+				</div>
+			</div>
+			<div class="footer">
+				<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
+				<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
+			</div>
+			<MkAd :prefer="['horizontal', 'horizontal-big']"/>
+			<MkContainer :max-height="300" :foldable="true" class="other">
+				<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
+				<MkPagination :pagination="otherPostsPagination" #default="{items}">
+					<MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/>
+				</MkPagination>
+			</MkContainer>
+		</div>
+		<MkError v-else-if="error" @retry="fetch()"/>
+		<MkLoading v-else/>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import XPage from '@/components/page/page.vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { url } from '@/config';
+import MkFollowButton from '@/components/follow-button.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkPagePreview from '@/components/page-preview.vue';
+
+export default defineComponent({
+	components: {
+		XPage,
+		MkButton,
+		MkFollowButton,
+		MkContainer,
+		MkPagination,
+		MkPagePreview,
+	},
+
+	props: {
+		pageName: {
+			type: String,
+			required: true
+		},
+		username: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.page ? {
+				title: computed(() => this.page.title || this.page.name),
+				avatar: this.page.user,
+				path: `/@${this.page.user.username}/pages/${this.page.name}`,
+				share: {
+					title: this.page.title || this.page.name,
+					text: this.page.summary,
+				},
+			} : null),
+			page: null,
+			error: null,
+			otherPostsPagination: {
+				endpoint: 'users/pages',
+				limit: 6,
+				params: () => ({
+					userId: this.page.user.id
+				})
+			},
+		};
+	},
+
+	computed: {
+		path(): string {
+			return this.username + '/' + this.pageName;
+		}
+	},
+
+	watch: {
+		path() {
+			this.fetch();
+		}
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			this.page = null;
+			os.api('pages/show', {
+				name: this.pageName,
+				username: this.username,
+			}).then(page => {
+				this.page = page;
+			}).catch(e => {
+				this.error = e;
+			});
+		},
+
+		share() {
+			navigator.share({
+				title: this.page.title || this.page.name,
+				text: this.page.summary,
+				url: `${url}/@${this.page.user.username}/pages/${this.page.name}`
+			});
+		},
+
+		shareWithNote() {
+			os.post({
+				initialText: `${this.page.title || this.page.name} ${url}/@${this.page.user.username}/pages/${this.page.name}`
+			});
+		},
+
+		like() {
+			os.apiWithDialog('pages/like', {
+				pageId: this.page.id,
+			}).then(() => {
+				this.page.isLiked = true;
+				this.page.likedCount++;
+			});
+		},
+
+		async unlike() {
+			const confirm = await os.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				text: this.$ts.unlikeConfirm,
+			});
+			if (confirm.canceled) return;
+			os.apiWithDialog('pages/unlike', {
+				pageId: this.page.id,
+			}).then(() => {
+				this.page.isLiked = false;
+				this.page.likedCount--;
+			});
+		},
+
+		pin(pin) {
+			os.apiWithDialog('i/update', {
+				pinnedPageId: pin ? this.page.id : null,
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.xcukqgmh {
+	--padding: 32px;
+
+	&.max-width_450px {
+		--padding: 16px;
+	}
+
+	> .main {
+		padding: var(--padding);
+
+		> .header {
+			padding: 16px;
+
+			> h1 {
+				margin: 0;
+			}
+		}
+
+		> .banner {
+			> img {
+				// TODO: 良い感じのアスペクト比で表示
+				display: block;
+				width: 100%;
+				height: 150px;
+				object-fit: cover;
+			}
+		}
+
+		> .content {
+			margin-top: 16px;
+			padding: 16px 0 0 0;
+		}
+
+		> .actions {
+			display: flex;
+			align-items: center;
+			margin-top: 16px;
+			padding: 16px 0 0 0;
+			border-top: solid 0.5px var(--divider);
+
+			> .like {
+				> .button {
+					--accent: rgb(241 97 132);
+					--X8: rgb(241 92 128);
+					--buttonBg: rgb(216 71 106 / 5%);
+					--buttonHoverBg: rgb(216 71 106 / 10%);
+					color: #ff002f;
+
+					::v-deep(.count) {
+						margin-left: 0.5em;
+					}
+				}
+			}
+
+			> .other {
+				margin-left: auto;
+
+				> button {
+					padding: 8px;
+					margin: 0 8px;
+
+					&:hover {
+						color: var(--fgHighlighted);
+					}
+				}
+			}
+		}
+
+		> .user {
+			margin-top: 16px;
+			padding: 16px 0 0 0;
+			border-top: solid 0.5px var(--divider);
+			display: flex;
+			align-items: center;
+
+			> .avatar {
+				width: 52px;
+				height: 52px;
+			}
+
+			> .name {
+				margin: 0 0 0 12px;
+				font-size: 90%;
+			}
+
+			> .koudoku {
+				margin-left: auto;
+			}
+		}
+
+		> .links {
+			margin-top: 16px;
+			padding: 24px 0 0 0;
+			border-top: solid 0.5px var(--divider);
+
+			> .link {
+				margin-right: 0.75em;
+			}
+		}
+	}
+
+	> .footer {
+		margin: var(--padding);
+		font-size: 85%;
+		opacity: 0.75;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue
new file mode 100644
index 0000000000..d66fc2ad5b
--- /dev/null
+++ b/packages/client/src/pages/pages.vue
@@ -0,0 +1,96 @@
+<template>
+<MkSpacer>
+	<!-- TODO: MkHeaderに統合 -->
+	<MkTab v-model="tab" v-if="$i">
+		<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._pages.featured }}</option>
+		<option value="my"><i class="fas fa-edit"></i> {{ $ts._pages.my }}</option>
+		<option value="liked"><i class="fas fa-heart"></i> {{ $ts._pages.liked }}</option>
+	</MkTab>
+
+	<div class="_section">
+		<div class="rknalgpo _content" v-if="tab === 'featured'">
+			<MkPagination :pagination="featuredPagesPagination" #default="{items}">
+				<MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
+			</MkPagination>
+		</div>
+
+		<div class="rknalgpo _content my" v-if="tab === 'my'">
+			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
+			<MkPagination :pagination="myPagesPagination" #default="{items}">
+				<MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
+			</MkPagination>
+		</div>
+
+		<div class="rknalgpo _content" v-if="tab === 'liked'">
+			<MkPagination :pagination="likedPagesPagination" #default="{items}">
+				<MkPagePreview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
+			</MkPagination>
+		</div>
+	</div>
+</MkSpacer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagePreview from '@/components/page-preview.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkTab from '@/components/tab.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkPagePreview, MkPagination, MkButton, MkTab
+	},
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.pages,
+				icon: 'fas fa-sticky-note',
+				bg: 'var(--bg)',
+				actions: [{
+					icon: 'fas fa-plus',
+					text: this.$ts.create,
+					handler: this.create,
+				}],
+			},
+			tab: 'featured',
+			featuredPagesPagination: {
+				endpoint: 'pages/featured',
+				noPaging: true,
+			},
+			myPagesPagination: {
+				endpoint: 'i/pages',
+				limit: 5,
+			},
+			likedPagesPagination: {
+				endpoint: 'i/page-likes',
+				limit: 5,
+			},
+		};
+	},
+	methods: {
+		create() {
+			this.$router.push(`/pages/new`);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rknalgpo {
+	&.my .ckltabjg:first-child {
+		margin-top: 16px;
+	}
+
+	.ckltabjg:not(:last-child) {
+		margin-bottom: 8px;
+	}
+
+	@media (min-width: 500px) {
+		.ckltabjg:not(:last-child) {
+			margin-bottom: 16px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue
new file mode 100644
index 0000000000..9d1ebb74ed
--- /dev/null
+++ b/packages/client/src/pages/preview.vue
@@ -0,0 +1,32 @@
+<template>
+<div class="graojtoi">
+	<MkSample/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkSample from '@/components/sample.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkSample,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.preview,
+				icon: 'fas fa-eye',
+			},
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.graojtoi {
+	padding: var(--margin);
+}
+</style>
diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue
new file mode 100644
index 0000000000..f9a2500840
--- /dev/null
+++ b/packages/client/src/pages/reset-password.vue
@@ -0,0 +1,69 @@
+<template>
+<FormBase v-if="token">
+	<FormInput v-model="password" type="password">
+		<template #prefix><i class="fas fa-lock"></i></template>
+		<span>{{ $ts.newPassword }}</span>
+	</FormInput>
+	
+	<FormButton primary @click="save">{{ $ts.save }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormGroup,
+		FormLink,
+		FormInput,
+		FormButton,
+	},
+
+	props: {
+		token: {
+			type: String,
+			required: false
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.resetPassword,
+				icon: 'fas fa-lock'
+			},
+			password: '',
+		}
+	},
+
+	mounted() {
+		if (this.token == null) {
+			os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed');
+			this.$router.push('/');
+		}
+	},
+
+	methods: {
+		async save() {
+			await os.apiWithDialog('reset-password', {
+				token: this.token,
+				password: this.password,
+			});
+			this.$router.push('/');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/reversi/game.board.vue b/packages/client/src/pages/reversi/game.board.vue
new file mode 100644
index 0000000000..529e00d969
--- /dev/null
+++ b/packages/client/src/pages/reversi/game.board.vue
@@ -0,0 +1,528 @@
+<template>
+<div class="xqnhankfuuilcwvhgsopeqncafzsquya">
+	<header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ $ts._reversi.black }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ $ts._reversi.white }})</header>
+
+	<div style="overflow: hidden; line-height: 28px;">
+		<p class="turn" v-if="!iAmPlayer && !game.isEnded">
+			<Mfm :key="'turn:' + turnUser().name" :text="$t('_reversi.turnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
+			<MkEllipsis/>
+		</p>
+		<p class="turn" v-if="logPos != logs.length">
+			<Mfm :key="'past-turn-of:' + turnUser().name" :text="$t('_reversi.pastTurnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
+		</p>
+		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn()">{{ $ts._reversi.opponentTurn }}<MkEllipsis/></p>
+		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn()" style="animation: tada 1s linear infinite both;">{{ $ts._reversi.myTurn }}</p>
+		<p class="result" v-if="game.isEnded && logPos == logs.length">
+			<template v-if="game.winner">
+				<Mfm :key="'won'" :text="$t('_reversi.won', { name: game.winner.name })" :plain="true" :custom-emojis="game.winner.emojis"/>
+				<span v-if="game.surrendered != null"> ({{ $ts._reversi.surrendered }})</span>
+			</template>
+			<template v-else>{{ $ts._reversi.drawn }}</template>
+		</p>
+	</div>
+
+	<div class="board">
+		<div class="labels-x" v-if="$store.state.gamesReversiShowBoardLabels">
+			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
+		</div>
+		<div class="flex">
+			<div class="labels-y" v-if="$store.state.gamesReversiShowBoardLabels">
+				<div v-for="i in game.map.length">{{ i }}</div>
+			</div>
+			<div class="cells" :style="cellsStyle">
+				<div v-for="(stone, i) in o.board"
+					:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn(), can: turnUser() ? o.canPut(turnUser().id == blackUser.id, i) : null, prev: o.prevPos == i }"
+					@click="set(i)"
+					:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"
+				>
+					<template v-if="$store.state.gamesReversiUseAvatarStones || true">
+						<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black">
+						<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white">
+					</template>
+					<template v-else>
+						<i v-if="stone === true" class="fas fa-circle"></i>
+						<i v-if="stone === false" class="far fa-circle"></i>
+					</template>
+				</div>
+			</div>
+			<div class="labels-y" v-if="$store.state.gamesReversiShowBoardLabels">
+				<div v-for="i in game.map.length">{{ i }}</div>
+			</div>
+		</div>
+		<div class="labels-x" v-if="$store.state.gamesReversiShowBoardLabels">
+			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
+		</div>
+	</div>
+
+	<p class="status"><b>{{ $t('_reversi.turnCount', { count: logPos }) }}</b> {{ $ts._reversi.black }}:{{ o.blackCount }} {{ $ts._reversi.white }}:{{ o.whiteCount }} {{ $ts._reversi.total }}:{{ o.blackCount + o.whiteCount }}</p>
+
+	<div class="actions" v-if="!game.isEnded && iAmPlayer">
+		<MkButton @click="surrender" inline>{{ $ts._reversi.surrender }}</MkButton>
+	</div>
+
+	<div class="player" v-if="game.isEnded">
+		<span>{{ logPos }} / {{ logs.length }}</span>
+		<div class="buttons" v-if="!autoplaying">
+			<MkButton inline @click="logPos = 0" :disabled="logPos == 0"><i class="fas fa-angle-double-left"></i></MkButton>
+			<MkButton inline @click="logPos--" :disabled="logPos == 0"><i class="fas fa-angle-left"></i></MkButton>
+			<MkButton inline @click="logPos++" :disabled="logPos == logs.length"><i class="fas fa-angle-right"></i></MkButton>
+			<MkButton inline @click="logPos = logs.length" :disabled="logPos == logs.length"><i class="fas fa-angle-double-right"></i></MkButton>
+		</div>
+		<MkButton @click="autoplay()" :disabled="autoplaying" style="margin: var(--margin) auto 0 auto;"><i class="fas fa-play"></i></MkButton>
+	</div>
+
+	<div class="info">
+		<p v-if="game.isLlotheo">{{ $ts._reversi.isLlotheo }}</p>
+		<p v-if="game.loopedBoard">{{ $ts._reversi.loopedMap }}</p>
+		<p v-if="game.canPutEverywhere">{{ $ts._reversi.canPutEverywhere }}</p>
+	</div>
+
+	<div class="watchers">
+		<MkAvatar v-for="user in watchers" :key="user.id" :user="user" class="avatar"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as CRC32 from 'crc-32';
+import Reversi, { Color } from '@/scripts/games/reversi/core';
+import { url } from '@/config';
+import MkButton from '@/components/ui/button.vue';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+import * as sound from '@/scripts/sound';
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+
+	props: {
+		initGame: {
+			type: Object,
+			require: true
+		},
+		connection: {
+			type: Object,
+			require: true
+		},
+	},
+
+	data() {
+		return {
+			game: JSON.parse(JSON.stringify(this.initGame)),
+			o: null as Reversi,
+			logs: [],
+			logPos: 0,
+			watchers: [],
+			pollingClock: null,
+		};
+	},
+
+	computed: {
+		iAmPlayer(): boolean {
+			if (!this.$i) return false;
+			return this.game.user1Id == this.$i.id || this.game.user2Id == this.$i.id;
+		},
+
+		myColor(): Color {
+			if (!this.iAmPlayer) return null;
+			if (this.game.user1Id == this.$i.id && this.game.black == 1) return true;
+			if (this.game.user2Id == this.$i.id && this.game.black == 2) return true;
+			return false;
+		},
+
+		opColor(): Color {
+			if (!this.iAmPlayer) return null;
+			return this.myColor === true ? false : true;
+		},
+
+		blackUser(): any {
+			return this.game.black == 1 ? this.game.user1 : this.game.user2;
+		},
+
+		whiteUser(): any {
+			return this.game.black == 1 ? this.game.user2 : this.game.user1;
+		},
+
+		cellsStyle(): any {
+			return {
+				'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`,
+				'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)`
+			};
+		}
+	},
+
+	watch: {
+		logPos(v) {
+			if (!this.game.isEnded) return;
+			const o = new Reversi(this.game.map, {
+				isLlotheo: this.game.isLlotheo,
+				canPutEverywhere: this.game.canPutEverywhere,
+				loopedBoard: this.game.loopedBoard
+			});
+			for (const log of this.logs.slice(0, v)) {
+				o.put(log.color, log.pos);
+			}
+			this.o = o;
+			//this.$forceUpdate();
+		}
+	},
+
+	created() {
+		this.o = new Reversi(this.game.map, {
+			isLlotheo: this.game.isLlotheo,
+			canPutEverywhere: this.game.canPutEverywhere,
+			loopedBoard: this.game.loopedBoard
+		});
+
+		for (const log of this.game.logs) {
+			this.o.put(log.color, log.pos);
+		}
+
+		this.logs = this.game.logs;
+		this.logPos = this.logs.length;
+
+		// 通信を取りこぼしてもいいように定期的にポーリングさせる
+		if (this.game.isStarted && !this.game.isEnded) {
+			this.pollingClock = setInterval(() => {
+				if (this.game.isEnded) return;
+				const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
+				this.connection.send('check', {
+					crc32: crc32
+				});
+			}, 3000);
+		}
+	},
+
+	mounted() {
+		this.connection.on('set', this.onSet);
+		this.connection.on('rescue', this.onRescue);
+		this.connection.on('ended', this.onEnded);
+		this.connection.on('watchers', this.onWatchers);
+	},
+
+	beforeUnmount() {
+		this.connection.off('set', this.onSet);
+		this.connection.off('rescue', this.onRescue);
+		this.connection.off('ended', this.onEnded);
+		this.connection.off('watchers', this.onWatchers);
+
+		clearInterval(this.pollingClock);
+	},
+
+	methods: {
+		userPage,
+
+		// this.o がリアクティブになった折にはcomputedにできる
+		turnUser(): any {
+			if (this.o.turn === true) {
+				return this.game.black == 1 ? this.game.user1 : this.game.user2;
+			} else if (this.o.turn === false) {
+				return this.game.black == 1 ? this.game.user2 : this.game.user1;
+			} else {
+				return null;
+			}
+		},
+
+		// this.o がリアクティブになった折にはcomputedにできる
+		isMyTurn(): boolean {
+			if (!this.iAmPlayer) return false;
+			if (this.turnUser() == null) return false;
+			return this.turnUser().id == this.$i.id;
+		},
+
+		set(pos) {
+			if (this.game.isEnded) return;
+			if (!this.iAmPlayer) return;
+			if (!this.isMyTurn()) return;
+			if (!this.o.canPut(this.myColor, pos)) return;
+
+			this.o.put(this.myColor, pos);
+
+			// サウンドを再生する
+			sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite');
+
+			this.connection.send('set', {
+				pos: pos
+			});
+
+			this.checkEnd();
+
+			this.$forceUpdate();
+		},
+
+		onSet(x) {
+			this.logs.push(x);
+			this.logPos++;
+			this.o.put(x.color, x.pos);
+			this.checkEnd();
+			this.$forceUpdate();
+
+			// サウンドを再生する
+			if (x.color !== this.myColor) {
+				sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
+			}
+		},
+
+		onEnded(x) {
+			this.game = JSON.parse(JSON.stringify(x.game));
+		},
+
+		checkEnd() {
+			this.game.isEnded = this.o.isEnded;
+			if (this.game.isEnded) {
+				if (this.o.winner === true) {
+					this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id;
+					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
+				} else if (this.o.winner === false) {
+					this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id;
+					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
+				} else {
+					this.game.winnerId = null;
+					this.game.winner = null;
+				}
+			}
+		},
+
+		// 正しいゲーム情報が送られてきたとき
+		onRescue(game) {
+			this.game = JSON.parse(JSON.stringify(game));
+
+			this.o = new Reversi(this.game.map, {
+				isLlotheo: this.game.isLlotheo,
+				canPutEverywhere: this.game.canPutEverywhere,
+				loopedBoard: this.game.loopedBoard
+			});
+
+			for (const log of this.game.logs) {
+				this.o.put(log.color, log.pos, true);
+			}
+
+			this.logs = this.game.logs;
+			this.logPos = this.logs.length;
+
+			this.checkEnd();
+			this.$forceUpdate();
+		},
+
+		onWatchers(users) {
+			this.watchers = users;
+		},
+
+		surrender() {
+			os.api('games/reversi/games/surrender', {
+				gameId: this.game.id
+			});
+		},
+
+		autoplay() {
+			this.autoplaying = true;
+			this.logPos = 0;
+
+			setTimeout(() => {
+				this.logPos = 1;
+
+				let i = 1;
+				let previousLog = this.game.logs[0];
+				const tick = () => {
+					const log = this.game.logs[i];
+					const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime()
+					setTimeout(() => {
+						i++;
+						this.logPos++;
+						previousLog = log;
+
+						if (i < this.game.logs.length) {
+							tick();
+						} else {
+							this.autoplaying = false;
+						}
+					}, time);
+				};
+
+				tick();
+			}, 1000);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+@use "sass:math";
+
+.xqnhankfuuilcwvhgsopeqncafzsquya {
+	text-align: center;
+
+	> .go-index {
+		position: absolute;
+		top: 0;
+		left: 0;
+		z-index: 1;
+		width: 42px;
+		height :42px;
+	}
+
+	> header {
+		padding: 8px;
+		border-bottom: dashed 1px var(--divider);
+	}
+
+	> .board {
+		width: calc(100% - 16px);
+		max-width: 500px;
+		margin: 0 auto;
+
+		$label-size: 16px;
+		$gap: 4px;
+
+		> .labels-x {
+			height: $label-size;
+			padding: 0 $label-size;
+			display: flex;
+
+			> * {
+				flex: 1;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				font-size: 0.8em;
+
+				&:first-child {
+					margin-left: -(math.div($gap, 2));
+				}
+
+				&:last-child {
+					margin-right: -(math.div($gap, 2));
+				}
+			}
+		}
+
+		> .flex {
+			display: flex;
+
+			> .labels-y {
+				width: $label-size;
+				display: flex;
+				flex-direction: column;
+
+				> * {
+					flex: 1;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					font-size: 12px;
+
+					&:first-child {
+						margin-top: -(math.div($gap, 2));
+					}
+
+					&:last-child {
+						margin-bottom: -(math.div($gap, 2));
+					}
+				}
+			}
+
+			> .cells {
+				flex: 1;
+				display: grid;
+				grid-gap: $gap;
+
+				> div {
+					background: transparent;
+					border-radius: 6px;
+					overflow: hidden;
+
+					* {
+						pointer-events: none;
+						user-select: none;
+					}
+
+					&.empty {
+						border: solid 2px var(--divider);
+					}
+
+					&.empty.can {
+						border-color: var(--accent);
+					}
+
+					&.empty.myTurn {
+						border-color: var(--divider);
+
+						&.can {
+							border-color: var(--accent);
+							cursor: pointer;
+
+							&:hover {
+								background: var(--accent);
+							}
+						}
+					}
+
+					&.prev {
+						box-shadow: 0 0 0 4px var(--accent);
+					}
+
+					&.isEnded {
+						border-color: var(--divider);
+					}
+
+					&.none {
+						border-color: transparent !important;
+					}
+
+					> svg, > img {
+						display: block;
+						width: 100%;
+						height: 100%;
+					}
+				}
+			}
+		}
+	}
+
+	> .status {
+		margin: 0;
+		padding: 16px 0;
+	}
+
+	> .actions {
+		padding-bottom: 16px;
+	}
+
+	> .player {
+		padding: 0 16px 32px 16px;
+		margin: 0 auto;
+		max-width: 500px;
+
+		> span {
+			display: inline-block;
+			margin: 0 8px;
+			min-width: 70px;
+		}
+
+		> .buttons {
+			display: flex;
+
+			> * {
+				flex: 1;
+			}
+		}
+	}
+
+	> .watchers {
+		padding: 0 0 16px 0;
+
+		&:empty {
+			display: none;
+		}
+
+		> .avatar {
+			width: 32px;
+			height: 32px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/reversi/game.setting.vue b/packages/client/src/pages/reversi/game.setting.vue
new file mode 100644
index 0000000000..e6a6661f16
--- /dev/null
+++ b/packages/client/src/pages/reversi/game.setting.vue
@@ -0,0 +1,390 @@
+<template>
+<div class="urbixznjwwuukfsckrwzwsqzsxornqij">
+	<header><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></header>
+
+	<div>
+		<p>{{ $ts._reversi.gameSettings }}</p>
+
+		<div class="card map _panel">
+			<header>
+				<select v-model="mapName" :placeholder="$ts._reversi.chooseBoard" @change="onMapChange">
+					<option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/>
+					<option :label="$ts.random" :value="null"/>
+					<optgroup v-for="c in mapCategories" :key="c" :label="c">
+						<option v-for="m in Object.values(maps).filter(m => m.category == c)" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
+					</optgroup>
+				</select>
+			</header>
+
+			<div>
+				<div class="random" v-if="game.map == null"><i class="fas fa-dice"></i></div>
+				<div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
+					<div v-for="(x, i) in game.map.join('')" :class="{ none: x == ' ' }" @click="onPixelClick(i, x)">
+						<i v-if="x === 'b'" class="fas fa-circle"></i>
+						<i v-if="x === 'w'" class="far fa-circle"></i>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<div class="card _panel">
+			<header>
+				<span>{{ $ts._reversi.blackOrWhite }}</span>
+			</header>
+
+			<div>
+				<MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $ts.random }}</MkRadio>
+				<MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')">
+					<I18n :src="$ts._reversi.blackIs" tag="span">
+						<template #name>
+							<b><MkUserName :user="game.user1"/></b>
+						</template>
+					</I18n>
+				</MkRadio>
+				<MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')">
+					<I18n :src="$ts._reversi.blackIs" tag="span">
+						<template #name>
+							<b><MkUserName :user="game.user2"/></b>
+						</template>
+					</I18n>
+				</MkRadio>
+			</div>
+		</div>
+
+		<div class="card _panel">
+			<header>
+				<span>{{ $ts._reversi.rules }}</span>
+			</header>
+
+			<div>
+				<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ $ts._reversi.isLlotheo }}</MkSwitch>
+				<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ $ts._reversi.loopedMap }}</MkSwitch>
+				<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ $ts._reversi.canPutEverywhere }}</MkSwitch>
+			</div>
+		</div>
+
+		<div class="card form _panel" v-if="form">
+			<header>
+				<span>{{ $ts._reversi.botSettings }}</span>
+			</header>
+
+			<div>
+				<template v-for="item in form">
+					<MkSwitch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</MkSwitch>
+
+					<div class="card" v-if="item.type == 'radio'" :key="item.id">
+						<header>
+							<span>{{ item.label }}</span>
+						</header>
+
+						<div>
+							<MkRadio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @update:modelValue="onChangeForm(item)">{{ r.label }}</MkRadio>
+						</div>
+					</div>
+
+					<div class="card" v-if="item.type == 'slider'" :key="item.id">
+						<header>
+							<span>{{ item.label }}</span>
+						</header>
+
+						<div>
+							<input type="range" :min="item.min" :max="item.max" :step="item.step || 1" v-model="item.value" @change="onChangeForm(item)"/>
+						</div>
+					</div>
+
+					<div class="card" v-if="item.type == 'textbox'" :key="item.id">
+						<header>
+							<span>{{ item.label }}</span>
+						</header>
+
+						<div>
+							<input v-model="item.value" @change="onChangeForm(item)"/>
+						</div>
+					</div>
+				</template>
+			</div>
+		</div>
+	</div>
+
+	<footer class="_acrylic">
+		<p class="status">
+			<template v-if="isAccepted && isOpAccepted">{{ $ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
+			<template v-if="isAccepted && !isOpAccepted">{{ $ts._reversi.waitingForOther }}<MkEllipsis/></template>
+			<template v-if="!isAccepted && isOpAccepted">{{ $ts._reversi.waitingForMe }}</template>
+			<template v-if="!isAccepted && !isOpAccepted">{{ $ts._reversi.waitingBoth }}<MkEllipsis/></template>
+		</p>
+
+		<div class="actions">
+			<MkButton inline @click="exit">{{ $ts.cancel }}</MkButton>
+			<MkButton inline primary @click="accept" v-if="!isAccepted">{{ $ts._reversi.ready }}</MkButton>
+			<MkButton inline primary @click="cancel" v-if="isAccepted">{{ $ts._reversi.cancelReady }}</MkButton>
+		</div>
+	</footer>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as maps from '@/scripts/games/reversi/maps';
+import MkButton from '@/components/ui/button.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import MkRadio from '@/components/form/radio.vue';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkSwitch,
+		MkRadio,
+	},
+
+	props: {
+		initGame: {
+			type: Object,
+			require: true
+		},
+		connection: {
+			type: Object,
+			require: true
+		},
+	},
+
+	data() {
+		return {
+			game: this.initGame,
+			o: null,
+			isLlotheo: false,
+			mapName: maps.eighteight.name,
+			maps: maps,
+			form: null,
+			messages: [],
+		};
+	},
+
+	computed: {
+		mapCategories(): string[] {
+			const categories = Object.values(maps).map(x => x.category);
+			return categories.filter((item, pos) => categories.indexOf(item) == pos);
+		},
+		isAccepted(): boolean {
+			if (this.game.user1Id == this.$i.id && this.game.user1Accepted) return true;
+			if (this.game.user2Id == this.$i.id && this.game.user2Accepted) return true;
+			return false;
+		},
+		isOpAccepted(): boolean {
+			if (this.game.user1Id != this.$i.id && this.game.user1Accepted) return true;
+			if (this.game.user2Id != this.$i.id && this.game.user2Accepted) return true;
+			return false;
+		}
+	},
+
+	created() {
+		this.connection.on('changeAccepts', this.onChangeAccepts);
+		this.connection.on('updateSettings', this.onUpdateSettings);
+		this.connection.on('initForm', this.onInitForm);
+		this.connection.on('message', this.onMessage);
+
+		if (this.game.user1Id != this.$i.id && this.game.form1) this.form = this.game.form1;
+		if (this.game.user2Id != this.$i.id && this.game.form2) this.form = this.game.form2;
+	},
+
+	beforeUnmount() {
+		this.connection.off('changeAccepts', this.onChangeAccepts);
+		this.connection.off('updateSettings', this.onUpdateSettings);
+		this.connection.off('initForm', this.onInitForm);
+		this.connection.off('message', this.onMessage);
+	},
+
+	methods: {
+		exit() {
+
+		},
+
+		accept() {
+			this.connection.send('accept', {});
+		},
+
+		cancel() {
+			this.connection.send('cancelAccept', {});
+		},
+
+		onChangeAccepts(accepts) {
+			this.game.user1Accepted = accepts.user1;
+			this.game.user2Accepted = accepts.user2;
+		},
+
+		updateSettings(key: string) {
+			this.connection.send('updateSettings', {
+				key: key,
+				value: this.game[key]
+			});
+		},
+
+		onUpdateSettings({ key, value }) {
+			this.game[key] = value;
+			if (this.game.map == null) {
+				this.mapName = null;
+			} else {
+				const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join(''));
+				this.mapName = found ? found.name : '-Custom-';
+			}
+		},
+
+		onInitForm(x) {
+			if (x.userId == this.$i.id) return;
+			this.form = x.form;
+		},
+
+		onMessage(x) {
+			if (x.userId == this.$i.id) return;
+			this.messages.unshift(x.message);
+		},
+
+		onChangeForm(item) {
+			this.connection.send('updateForm', {
+				id: item.id,
+				value: item.value
+			});
+		},
+
+		onMapChange() {
+			if (this.mapName == null) {
+				this.game.map = null;
+			} else {
+				this.game.map = Object.values(maps).find(x => x.name == this.mapName).data;
+			}
+			this.updateSettings('map');
+		},
+
+		onPixelClick(pos, pixel) {
+			const x = pos % this.game.map[0].length;
+			const y = Math.floor(pos / this.game.map[0].length);
+			const newPixel =
+				pixel == ' ' ? '-' :
+				pixel == '-' ? 'b' :
+				pixel == 'b' ? 'w' :
+				' ';
+			const line = this.game.map[y].split('');
+			line[x] = newPixel;
+			this.game.map[y] = line.join('');
+			this.updateSettings('map');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.urbixznjwwuukfsckrwzwsqzsxornqij {
+	text-align: center;
+	background: var(--bg);
+
+	> header {
+		padding: 8px;
+		border-bottom: dashed 1px #c4cdd4;
+	}
+
+	> div {
+		padding: 0 16px;
+
+		> .card {
+			margin: 0 auto 16px auto;
+
+			&.map {
+				> header {
+					> select {
+						width: 100%;
+						padding: 12px 14px;
+						background: var(--face);
+						border: 1px solid var(--inputBorder);
+						border-radius: 4px;
+						color: var(--fg);
+						cursor: pointer;
+						transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+						-webkit-appearance: none;
+						-moz-appearance: none;
+						appearance: none;
+
+						&:focus-visible,
+						&:active {
+							border-color: var(--accent);
+						}
+					}
+				}
+
+				> div {
+					> .random {
+						padding: 32px 0;
+						font-size: 64px;
+						color: var(--fg);
+						opacity: 0.7;
+					}
+
+					> .board {
+						display: grid;
+						grid-gap: 4px;
+						width: 300px;
+						height: 300px;
+						margin: 0 auto;
+						color: var(--fg);
+
+						> div {
+							background: transparent;
+							border: solid 2px var(--divider);
+							border-radius: 6px;
+							overflow: hidden;
+							cursor: pointer;
+
+							* {
+								pointer-events: none;
+								user-select: none;
+								width: 100%;
+								height: 100%;
+							}
+
+							&.none {
+								border-color: transparent;
+							}
+						}
+					}
+				}
+			}
+
+			&.form {
+				> div {
+					> .card + .card {
+						margin-top: 16px;
+					}
+
+					input[type='range'] {
+						width: 100%;
+					}
+				}
+			}
+		}
+
+		.card {
+			max-width: 400px;
+
+			> header {
+				padding: 18px 20px;
+				border-bottom: 1px solid var(--divider);
+			}
+
+			> div {
+				padding: 20px;
+				color: var(--fg);
+			}
+		}
+	}
+
+	> footer {
+		position: sticky;
+		bottom: 0;
+		padding: 16px;
+		border-top: solid 1px var(--divider);
+
+		> .status {
+			margin: 0 0 16px 0;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/reversi/game.vue b/packages/client/src/pages/reversi/game.vue
new file mode 100644
index 0000000000..b1ed632904
--- /dev/null
+++ b/packages/client/src/pages/reversi/game.vue
@@ -0,0 +1,76 @@
+<template>
+<div v-if="game == null"><MkLoading/></div>
+<GameSetting v-else-if="!game.isStarted" :init-game="game" :connection="connection"/>
+<GameBoard v-else :init-game="game" :connection="connection"/>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import GameSetting from './game.setting.vue';
+import GameBoard from './game.board.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		GameSetting,
+		GameBoard,
+	},
+
+	props: {
+		gameId: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts._reversi.reversi,
+				icon: 'fas fa-gamepad'
+			},
+			game: null,
+			connection: null,
+		};
+	},
+
+	watch: {
+		gameId() {
+			this.fetch();
+		}
+	},
+
+	mounted() {
+		this.fetch();
+	},
+
+	beforeUnmount() {
+		if (this.connection) {
+			this.connection.dispose();
+		}
+	},
+
+	methods: {
+		fetch() {
+			os.api('games/reversi/games/show', {
+				gameId: this.gameId
+			}).then(game => {
+				this.game = game;
+
+				if (this.connection) {
+					this.connection.dispose();
+				}
+				this.connection = markRaw(os.stream.useChannel('gamesReversiGame', {
+					gameId: this.game.id
+				}));
+				this.connection.on('started', this.onStarted);
+			});
+		},
+
+		onStarted(game) {
+			Object.assign(this.game, game);
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/reversi/index.vue b/packages/client/src/pages/reversi/index.vue
new file mode 100644
index 0000000000..1b8f1ffb71
--- /dev/null
+++ b/packages/client/src/pages/reversi/index.vue
@@ -0,0 +1,279 @@
+<template>
+<div class="bgvwxkhb" v-if="!matching">
+	<h1>Misskey {{ $ts._reversi.reversi }}</h1>
+
+	<div class="play">
+		<MkButton primary round @click="match" style="margin: var(--margin) auto 0 auto;">{{ $ts.invite }}</MkButton>
+	</div>
+
+	<div class="_section">
+		<MkFolder v-if="invitations.length > 0">
+			<template #header>{{ $ts.invitations }}</template>
+			<div class="nfcacttm">
+				<button class="invitation _panel _button" v-for="invitation in invitations" tabindex="-1" @click="accept(invitation)">
+					<MkAvatar class="avatar" :user="invitation.parent" :show-indicator="true"/>
+					<span class="name"><b><MkUserName :user="invitation.parent"/></b></span>
+					<span class="username">@{{ invitation.parent.username }}</span>
+					<MkTime :time="invitation.createdAt" class="time"/>
+				</button>
+			</div>
+		</MkFolder>
+
+		<MkFolder v-if="myGames.length > 0">
+			<template #header>{{ $ts._reversi.myGames }}</template>
+			<div class="knextgwz">
+				<MkA class="game _panel" v-for="g in myGames" tabindex="-1" :to="`/games/reversi/${g.id}`" :key="g.id">
+					<div class="players">
+						<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
+					</div>
+					<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
+				</MkA>
+			</div>
+		</MkFolder>
+
+		<MkFolder v-if="games.length > 0">
+			<template #header>{{ $ts._reversi.allGames }}</template>
+			<div class="knextgwz">
+				<MkA class="game _panel" v-for="g in games" tabindex="-1" :to="`/games/reversi/${g.id}`" :key="g.id">
+					<div class="players">
+						<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
+					</div>
+					<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
+				</MkA>
+			</div>
+		</MkFolder>
+	</div>
+</div>
+<div class="sazhgisb" v-else>
+	<h1>
+		<I18n :src="$ts.waitingFor" tag="span">
+			<template #x>
+				<b><MkUserName :user="matching"/></b>
+			</template>
+		</I18n>
+		<MkEllipsis/>
+	</h1>
+	<div class="cancel">
+		<MkButton inline round @click="cancel">{{ $ts.cancel }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import * as os from '@/os';
+import MkButton from '@/components/ui/button.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton, MkFolder,
+	},
+
+	inject: ['navHook'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts._reversi.reversi,
+				icon: 'fas fa-gamepad'
+			},
+			games: [],
+			gamesFetching: true,
+			gamesMoreFetching: false,
+			myGames: [],
+			matching: null,
+			invitations: [],
+			connection: null,
+			pingClock: null,
+		};
+	},
+
+	mounted() {
+		if (this.$i) {
+			this.connection = markRaw(os.stream.useChannel('gamesReversi'));
+
+			this.connection.on('invited', this.onInvited);
+
+			this.connection.on('matched', this.onMatched);
+
+			this.pingClock = setInterval(() => {
+				if (this.matching) {
+					this.connection.send('ping', {
+						id: this.matching.id
+					});
+				}
+			}, 3000);
+
+			os.api('games/reversi/games', {
+				my: true
+			}).then(games => {
+				this.myGames = games;
+			});
+
+			os.api('games/reversi/invitations').then(invitations => {
+				this.invitations = this.invitations.concat(invitations);
+			});
+		}
+
+		os.api('games/reversi/games').then(games => {
+			this.games = games;
+			this.gamesFetching = false;
+		});
+	},
+
+	beforeUnmount() {
+		if (this.connection) {
+			this.connection.dispose();
+			clearInterval(this.pingClock);
+		}
+	},
+
+	methods: {
+		go(game) {
+			const url = '/games/reversi/' + game.id;
+			if (this.navHook) {
+				this.navHook(url);
+			} else {
+				this.$router.push(url);
+			}
+		},
+
+		async match() {
+			const user = await os.selectUser({ local: true });
+			if (user == null) return;
+			os.api('games/reversi/match', {
+				userId: user.id
+			}).then(res => {
+				if (res == null) {
+					this.matching = user;
+				} else {
+					this.go(res);
+				}
+			});
+		},
+
+		cancel() {
+			this.matching = null;
+			os.api('games/reversi/match/cancel');
+		},
+
+		accept(invitation) {
+			os.api('games/reversi/match', {
+				userId: invitation.parent.id
+			}).then(game => {
+				if (game) {
+					this.go(game);
+				}
+			});
+		},
+
+		onMatched(game) {
+			this.go(game);
+		},
+
+		onInvited(invite) {
+			this.invitations.unshift(invite);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.bgvwxkhb {
+	> h1 {
+		margin: 0;
+		padding: 24px;
+		text-align: center;
+		font-size: 1.5em;
+		background: linear-gradient(0deg, #43c583, #438881);
+		color: #fff;
+	}
+
+	> .play {
+		text-align: center;
+	}
+}
+
+.sazhgisb {
+	text-align: center;
+}
+
+.nfcacttm {
+	> .invitation {
+		display: flex;
+		box-sizing: border-box;
+		width: 100%;
+		padding: 16px;
+		line-height: 32px;
+		text-align: left;
+
+		> .avatar {
+			width: 32px;
+			height: 32px;
+			margin-right: 8px;
+		}
+
+		> .name {
+			margin-right: 8px;
+		}
+
+		> .username {
+			margin-right: 8px;
+			opacity: 0.7;
+		}
+
+		> .time {
+			margin-left: auto;
+			opacity: 0.7;
+		}
+	}
+}
+
+.knextgwz {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+	grid-gap: var(--margin);
+
+	> .game {
+		> .players {
+			text-align: center;
+			padding: 16px;
+			line-height: 32px;
+
+			> .avatar {
+				width: 32px;
+				height: 32px;
+
+				&:first-child {
+					margin-right: 8px;
+				}
+
+				&:last-child {
+					margin-left: 8px;
+				}
+			}
+		}
+
+		> footer {
+			display: flex;
+			align-items: baseline;
+			border-top: solid 0.5px var(--divider);
+			padding: 6px 8px;
+			font-size: 0.9em;
+
+			> .state {
+				&.playing {
+					color: var(--accent);
+				}
+			}
+
+			> .time {
+				margin-left: auto;
+				opacity: 0.7;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/room/preview.vue b/packages/client/src/pages/room/preview.vue
new file mode 100644
index 0000000000..b0e600d4fb
--- /dev/null
+++ b/packages/client/src/pages/room/preview.vue
@@ -0,0 +1,107 @@
+<template>
+<canvas width="224" height="128"></canvas>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as THREE from 'three';
+import * as os from '@/os';
+
+export default defineComponent({
+	data() {
+		return {
+			selected: null,
+			objectHeight: 0,
+			orbitRadius: 5
+		};
+	},
+
+	mounted() {
+		const canvas = this.$el;
+
+		const width = canvas.width;
+		const height = canvas.height;
+
+		const scene = new THREE.Scene();
+
+		const renderer = new THREE.WebGLRenderer({
+			canvas: canvas,
+			antialias: true,
+			alpha: false
+		});
+		renderer.setPixelRatio(window.devicePixelRatio);
+		renderer.setSize(width, height);
+		renderer.setClearColor(0x000000);
+		renderer.autoClear = false;
+		renderer.shadowMap.enabled = true;
+		renderer.shadowMap.cullFace = THREE.CullFaceBack;
+
+		const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
+		camera.zoom = 10;
+		camera.position.x = 0;
+		camera.position.y = 2;
+		camera.position.z = 0;
+		camera.updateProjectionMatrix();
+		scene.add(camera);
+
+		const ambientLight = new THREE.AmbientLight(0xffffff, 1);
+		ambientLight.castShadow = false;
+		scene.add(ambientLight);
+
+		const light = new THREE.PointLight(0xffffff, 1, 100);
+		light.position.set(3, 3, 3);
+		scene.add(light);
+
+		const grid = new THREE.GridHelper(5, 16, 0x444444, 0x222222);
+		scene.add(grid);
+
+		const render = () => {
+			const timer = Date.now() * 0.0004;
+			requestAnimationFrame(render);
+			
+			camera.position.y = Math.sin(Math.PI / 6) * this.orbitRadius;	// Math.PI / 6 => 30deg
+			camera.position.z = Math.cos(timer) * this.orbitRadius;
+			camera.position.x = Math.sin(timer) * this.orbitRadius;
+			camera.lookAt(new THREE.Vector3(0, this.objectHeight / 2, 0));
+			renderer.render(scene, camera);
+		};
+
+		this.selected = selected => {
+			const obj = selected.clone();
+
+			// Remove current object
+			const current = scene.getObjectByName('obj');
+			if (current != null) {
+				scene.remove(current);
+			}
+
+			// Add new object
+			obj.name = 'obj';
+			obj.position.x = 0;
+			obj.position.y = 0;
+			obj.position.z = 0;
+			obj.rotation.x = 0;
+			obj.rotation.y = 0;
+			obj.rotation.z = 0;
+			obj.traverse(child => {
+				if (child instanceof THREE.Mesh) {
+					child.material = child.material.clone();
+					return child.material.emissive.setHex(0x000000);
+				}
+			});
+			const objectBoundingBox = new THREE.Box3().setFromObject(obj);
+			this.objectHeight = objectBoundingBox.max.y - objectBoundingBox.min.y;
+
+			const objectWidth = objectBoundingBox.max.x - objectBoundingBox.min.x;
+			const objectDepth = objectBoundingBox.max.z - objectBoundingBox.min.z;
+
+			const horizontal = Math.hypot(objectWidth, objectDepth) / camera.aspect;
+			this.orbitRadius = Math.max(horizontal, this.objectHeight) * camera.zoom * 0.625 / Math.tan(camera.fov * 0.5 * (Math.PI / 180));
+		
+			scene.add(obj);
+		};
+
+		render();
+	},
+});
+</script>
diff --git a/packages/client/src/pages/room/room.vue b/packages/client/src/pages/room/room.vue
new file mode 100644
index 0000000000..1671bcd587
--- /dev/null
+++ b/packages/client/src/pages/room/room.vue
@@ -0,0 +1,285 @@
+<template>
+<div class="hveuntkp">
+	<div class="controller _section" v-if="objectSelected">
+		<div class="_content">
+			<p class="name">{{ selectedFurnitureName }}</p>
+			<XPreview ref="preview"/>
+			<template v-if="selectedFurnitureInfo.props">
+				<div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k">
+					<p>{{ k }}</p>
+					<template v-if="selectedFurnitureInfo.props[k] === 'image'">
+						<MkButton @click="chooseImage(k, $event)">{{ $ts._rooms.chooseImage }}</MkButton>
+					</template>
+					<template v-else-if="selectedFurnitureInfo.props[k] === 'color'">
+						<input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/>
+					</template>
+				</div>
+			</template>
+		</div>
+		<div class="_content">
+			<MkButton inline @click="translate()" :primary="isTranslateMode"><i class="fas fa-arrows-alt"></i> {{ $ts._rooms.translate }}</MkButton>
+			<MkButton inline @click="rotate()" :primary="isRotateMode"><i class="fas fa-undo"></i> {{ $ts._rooms.rotate }}</MkButton>
+			<MkButton inline v-if="isTranslateMode || isRotateMode" @click="exit()"><i class="fas fa-ban"></i> {{ $ts._rooms.exit }}</MkButton>
+		</div>
+		<div class="_content">
+			<MkButton @click="remove()"><i class="fas fa-trash-alt"></i> {{ $ts._rooms.remove }}</MkButton>
+		</div>
+	</div>
+
+	<div class="menu _section" v-if="isMyRoom">
+		<div class="_content">
+			<MkButton @click="add()"><i class="fas fa-box-open"></i> {{ $ts._rooms.addFurniture }}</MkButton>
+		</div>
+		<div class="_content">
+			<MkSelect :model-value="roomType" @update:modelValue="updateRoomType($event)">
+				<template #label>{{ $ts._rooms.roomType }}</template>
+				<option value="default">{{ $ts._rooms._roomType.default }}</option>
+				<option value="washitsu">{{ $ts._rooms._roomType.washitsu }}</option>
+			</MkSelect>
+			<label v-if="roomType === 'default'">
+				<span>{{ $ts._rooms.carpetColor }}</span>
+				<input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/>
+			</label>
+		</div>
+		<div class="_content">
+			<MkButton inline :disabled="!changed" primary @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+			<MkButton inline @click="clear()"><i class="fas fa-broom"></i> {{ $ts._rooms.clear }}</MkButton>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { Room } from '@/scripts/room/room';
+import * as Acct from 'misskey-js/built/acct';
+import XPreview from './preview.vue';
+const storeItems = require('@/scripts/room/furnitures.json5');
+import { query as urlQuery } from '@/scripts/url';
+import MkButton from '@/components/ui/button.vue';
+import MkSelect from '@/components/form/select.vue';
+import { selectFile } from '@/scripts/select-file';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import * as symbols from '@/symbols';
+
+let room: Room;
+
+export default defineComponent({
+	components: {
+		XPreview,
+		MkButton,
+		MkSelect,
+	},
+
+	props: {
+		acct: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.user ? {
+				title: this.$ts.room,
+				avatar: this.user,
+			} : null),
+			user: null,
+			objectSelected: false,
+			selectedFurnitureName: null,
+			selectedFurnitureInfo: null,
+			selectedFurnitureProps: null,
+			roomType: null,
+			carpetColor: null,
+			isTranslateMode: false,
+			isRotateMode: false,
+			isMyRoom: false,
+			changed: false,
+		};
+	},
+
+	async mounted() {
+		window.addEventListener('beforeunload', this.beforeunload);
+
+		this.user = await os.api('users/show', {
+			...Acct.parse(this.acct)
+		});
+
+		this.isMyRoom = this.$i && (this.$i.id === this.user.id);
+
+		const roomInfo = await os.api('room/show', {
+			userId: this.user.id
+		});
+
+		this.roomType = roomInfo.roomType;
+		this.carpetColor = roomInfo.carpetColor;
+
+		room = new Room(this.user, this.isMyRoom, roomInfo, this.$el, {
+			graphicsQuality: ColdDeviceStorage.get('roomGraphicsQuality'),
+			onChangeSelect: obj => {
+				this.objectSelected = obj != null;
+				if (obj) {
+					const f = room.findFurnitureById(obj.name);
+					this.selectedFurnitureName = this.$t('_rooms._furnitures.' + f.type);
+					this.selectedFurnitureInfo = storeItems.find(x => x.id === f.type);
+					this.selectedFurnitureProps = f.props
+						? JSON.parse(JSON.stringify(f.props)) // Disable reactivity
+						: null;
+					this.$nextTick(() => {
+						this.$refs.preview.selected(obj);
+					});
+				}
+			},
+			useOrthographicCamera: ColdDeviceStorage.get('roomUseOrthographicCamera'),
+		});
+	},
+
+	beforeRouteLeave(to, from, next) {
+		if (this.changed) {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.leaveConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) {
+					next(false);
+				} else {
+					next();
+				}
+			});
+		} else {
+			next();
+		}
+	},
+
+	beforeUnmount() {
+		room.destroy();
+		window.removeEventListener('beforeunload', this.beforeunload);
+	},
+
+	methods: {
+		beforeunload(e: BeforeUnloadEvent) {
+			if (this.changed) {
+				e.preventDefault();
+				e.returnValue = '';
+			}
+		},
+
+		async add() {
+			const { canceled, result: id } = await os.dialog({
+				type: null,
+				title: this.$ts._rooms.addFurniture,
+				select: {
+					items: storeItems.map(item => ({
+						value: item.id, text: this.$t('_rooms._furnitures.' + item.id)
+					}))
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			room.addFurniture(id);
+			this.changed = true;
+		},
+
+		remove() {
+			this.isTranslateMode = false;
+			this.isRotateMode = false;
+			room.removeFurniture();
+			this.changed = true;
+		},
+
+		save() {
+			os.api('room/update', {
+				room: room.getRoomInfo()
+			}).then(() => {
+				this.changed = false;
+				os.success();
+			}).catch((e: any) => {
+				os.dialog({
+					type: 'error',
+					text: e.message
+				});
+			});
+		},
+
+		clear() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts._rooms.clearConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				room.removeAllFurnitures();
+				this.changed = true;
+			});
+		},
+
+		chooseImage(key, e) {
+			selectFile(e.currentTarget || e.target, null, false).then(file => {
+				room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`);
+				this.$refs.preview.selected(room.getSelectedObject());
+				this.changed = true;
+			});
+		},
+
+		updateColor(key, ev) {
+			room.updateProp(key, ev.target.value);
+			this.$refs.preview.selected(room.getSelectedObject());
+			this.changed = true;
+		},
+
+		updateCarpetColor(ev) {
+			room.updateCarpetColor(ev.target.value);
+			this.carpetColor = ev.target.value;
+			this.changed = true;
+		},
+
+		updateRoomType(type) {
+			room.changeRoomType(type);
+			this.roomType = type;
+			this.changed = true;
+		},
+
+		translate() {
+			if (this.isTranslateMode) {
+				this.exit();
+			} else {
+				this.isRotateMode = false;
+				this.isTranslateMode = true;
+				room.enterTransformMode('translate');
+			}
+			this.changed = true;
+		},
+
+		rotate() {
+			if (this.isRotateMode) {
+				this.exit();
+			} else {
+				this.isTranslateMode = false;
+				this.isRotateMode = true;
+				room.enterTransformMode('rotate');
+			}
+			this.changed = true;
+		},
+
+		exit() {
+			this.isTranslateMode = false;
+			this.isRotateMode = false;
+			room.exitTransformMode();
+			this.changed = true;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hveuntkp {
+	position: relative;
+	min-height: 500px;
+
+	> ::v-deep(canvas) {
+		display: block;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/scratchpad.vue b/packages/client/src/pages/scratchpad.vue
new file mode 100644
index 0000000000..c26658cbc4
--- /dev/null
+++ b/packages/client/src/pages/scratchpad.vue
@@ -0,0 +1,149 @@
+<template>
+<div class="iltifgqe">
+	<div class="editor _panel _gap">
+		<PrismEditor class="_code code" v-model="code" :highlight="highlighter" :line-numbers="false"/>
+		<MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><i class="fas fa-play"></i></MkButton>
+	</div>
+
+	<MkContainer :foldable="true" class="_gap">
+		<template #header>{{ $ts.output }}</template>
+		<div class="bepmlvbi">
+			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div>
+		</div>
+	</MkContainer>
+
+	<div class="_gap">
+		{{ $ts.scratchpadDescription }}
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import 'prismjs';
+import { highlight, languages } from 'prismjs/components/prism-core';
+import 'prismjs/components/prism-clike';
+import 'prismjs/components/prism-javascript';
+import 'prismjs/themes/prism-okaidia.css';
+import { PrismEditor } from 'vue-prism-editor';
+import 'vue-prism-editor/dist/prismeditor.min.css';
+import { AiScript, parse, utils, values } from '@syuilo/aiscript';
+import MkContainer from '@/components/ui/container.vue';
+import MkButton from '@/components/ui/button.vue';
+import { createAiScriptEnv } from '@/scripts/aiscript/api';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkContainer,
+		MkButton,
+		PrismEditor,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.scratchpad,
+				icon: 'fas fa-terminal',
+			},
+			code: '',
+			logs: [],
+		}
+	},
+
+	watch: {
+		code() {
+			localStorage.setItem('scratchpad', this.code);
+		}
+	},
+
+	created() {
+		const saved = localStorage.getItem('scratchpad');
+		if (saved) {
+			this.code = saved;
+		}
+	},
+
+	methods: {
+		async run() {
+			this.logs = [];
+			const aiscript = new AiScript(createAiScriptEnv({
+				storageKey: 'scratchpad',
+				token: this.$i?.token,
+			}), {
+				in: (q) => {
+					return new Promise(ok => {
+						os.dialog({
+							title: q,
+							input: {}
+						}).then(({ canceled, result: a }) => {
+							ok(a);
+						});
+					});
+				},
+				out: (value) => {
+					this.logs.push({
+						id: Math.random(),
+						text: value.type === 'str' ? value.value : utils.valToString(value),
+						print: true
+					});
+				},
+				log: (type, params) => {
+					switch (type) {
+						case 'end': this.logs.push({
+							id: Math.random(),
+							text: utils.valToString(params.val, true),
+							print: false
+						}); break;
+						default: break;
+					}
+				}
+			});
+
+			let ast;
+			try {
+				ast = parse(this.code);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: 'Syntax error :('
+				});
+				return;
+			}
+			try {
+				await aiscript.exec(ast);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: e
+				});
+			}
+		},
+
+		highlighter(code) {
+			return highlight(code, languages.js, 'javascript');
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.iltifgqe {
+	padding: 16px;
+
+	> .editor {
+		position: relative;
+	}
+}
+
+.bepmlvbi {
+	padding: 16px;
+
+	> .log {
+		&:not(.print) {
+			opacity: 0.7;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue
new file mode 100644
index 0000000000..c7da3fe1c1
--- /dev/null
+++ b/packages/client/src/pages/search.vue
@@ -0,0 +1,53 @@
+<template>
+<div class="_section">
+	<div class="_content">
+		<XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XNotes from '@/components/notes.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: computed(() => this.$t('searchWith', { q: this.$route.query.q })),
+				icon: 'fas fa-search',
+			},
+			pagination: {
+				endpoint: 'notes/search',
+				limit: 10,
+				params: () => ({
+					query: this.$route.query.q,
+					channelId: this.$route.query.channel,
+				})
+			},
+		};
+	},
+
+	watch: {
+		$route() {
+			(this.$refs.notes as any).reload();
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue
new file mode 100644
index 0000000000..dce217559a
--- /dev/null
+++ b/packages/client/src/pages/settings/2fa.vue
@@ -0,0 +1,247 @@
+<template>
+<section class="_card">
+	<div class="_title"><i class="fas fa-lock"></i> {{ $ts.twoStepAuthentication }}</div>
+	<div class="_content">
+		<MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $ts._2fa.registerDevice }}</MkButton>
+		<template v-if="$i.twoFactorEnabled">
+			<p>{{ $ts._2fa.alreadyRegistered }}</p>
+			<MkButton @click="unregister">{{ $ts.unregister }}</MkButton>
+
+			<template v-if="supportsCredentials">
+				<hr class="totp-method-sep">
+
+				<h2 class="heading">{{ $ts.securityKey }}</h2>
+				<p>{{ $ts._2fa.securityKeyInfo }}</p>
+				<div class="key-list">
+					<div class="key" v-for="key in $i.securityKeysList">
+						<h3>{{ key.name }}</h3>
+						<div class="last-used">{{ $ts.lastUsed }}<MkTime :time="key.lastUsed"/></div>
+						<MkButton @click="unregisterKey(key)">{{ $ts.unregister }}</MkButton>
+					</div>
+				</div>
+
+				<MkSwitch v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin" v-if="$i.securityKeysList.length > 0">{{ $ts.passwordLessLogin }}</MkSwitch>
+
+				<MkInfo warn v-if="registration && registration.error">{{ $ts.error }} {{ registration.error }}</MkInfo>
+				<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $ts._2fa.registerKey }}</MkButton>
+
+				<ol v-if="registration && !registration.error">
+					<li v-if="registration.stage >= 0">
+						{{ $ts.tapSecurityKey }}
+						<i v-if="registration.saving && registration.stage == 0" class="fas fa-spinner fa-pulse fa-fw"></i>
+					</li>
+					<li v-if="registration.stage >= 1">
+						<MkForm :disabled="registration.stage != 1 || registration.saving">
+							<MkInput v-model="keyName" :max="30">
+								<template #label>{{ $ts.securityKeyName }}</template>
+							</MkInput>
+							<MkButton @click="registerKey" :disabled="keyName.length == 0">{{ $ts.registerSecurityKey }}</MkButton>
+							<i v-if="registration.saving && registration.stage == 1" class="fas fa-spinner fa-pulse fa-fw"></i>
+						</MkForm>
+					</li>
+				</ol>
+			</template>
+		</template>
+		<div v-if="data && !$i.twoFactorEnabled">
+			<ol style="margin: 0; padding: 0 0 0 1em;">
+				<li>
+					<I18n :src="$ts._2fa.step1" tag="span">
+						<template #a>
+							<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
+						</template>
+						<template #b>
+							<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
+						</template>
+					</I18n>
+				</li>
+				<li>{{ $ts._2fa.step2 }}<br><img :src="data.qr"></li>
+				<li>{{ $ts._2fa.step3 }}<br>
+					<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ $ts.token }}</template></MkInput>
+					<MkButton primary @click="submit">{{ $ts.done }}</MkButton>
+				</li>
+			</ol>
+			<MkInfo>{{ $ts._2fa.step4 }}</MkInfo>
+		</div>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { hostname } from '@/config';
+import { byteify, hexify, stringify } from '@/scripts/2fa';
+import MkButton from '@/components/ui/button.vue';
+import MkInfo from '@/components/ui/info.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		MkButton, MkInfo, MkInput, MkSwitch
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.twoStepAuthentication,
+				icon: 'fas fa-lock'
+			},
+			data: null,
+			supportsCredentials: !!navigator.credentials,
+			usePasswordLessLogin: this.$i.usePasswordLessLogin,
+			registration: null,
+			keyName: '',
+			token: null,
+		};
+	},
+
+	methods: {
+		register() {
+			os.dialog({
+				title: this.$ts.password,
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				os.api('i/2fa/register', {
+					password: password
+				}).then(data => {
+					this.data = data;
+				});
+			});
+		},
+
+		unregister() {
+			os.dialog({
+				title: this.$ts.password,
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				os.api('i/2fa/unregister', {
+					password: password
+				}).then(() => {
+					this.usePasswordLessLogin = false;
+					this.updatePasswordLessLogin();
+				}).then(() => {
+					os.success();
+					this.$i.twoFactorEnabled = false;
+				});
+			});
+		},
+
+		submit() {
+			os.api('i/2fa/done', {
+				token: this.token
+			}).then(() => {
+				os.success();
+				this.$i.twoFactorEnabled = true;
+			}).catch(e => {
+				os.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
+		registerKey() {
+			this.registration.saving = true;
+			os.api('i/2fa/key-done', {
+				password: this.registration.password,
+				name: this.keyName,
+				challengeId: this.registration.challengeId,
+				// we convert each 16 bits to a string to serialise
+				clientDataJSON: stringify(this.registration.credential.response.clientDataJSON),
+				attestationObject: hexify(this.registration.credential.response.attestationObject)
+			}).then(key => {
+				this.registration = null;
+				key.lastUsed = new Date();
+				os.success();
+			})
+		},
+
+		unregisterKey(key) {
+			os.dialog({
+				title: this.$ts.password,
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				return os.api('i/2fa/remove-key', {
+					password,
+					credentialId: key.id
+				}).then(() => {
+					this.usePasswordLessLogin = false;
+					this.updatePasswordLessLogin();
+				}).then(() => {
+					os.success();
+				});
+			});
+		},
+
+		addSecurityKey() {
+			os.dialog({
+				title: this.$ts.password,
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				os.api('i/2fa/register-key', {
+					password
+				}).then(registration => {
+					this.registration = {
+						password,
+						challengeId: registration.challengeId,
+						stage: 0,
+						publicKeyOptions: {
+							challenge: byteify(registration.challenge, 'base64'),
+							rp: {
+								id: hostname,
+								name: 'Misskey'
+							},
+							user: {
+								id: byteify(this.$i.id, 'ascii'),
+								name: this.$i.username,
+								displayName: this.$i.name,
+							},
+							pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
+							timeout: 60000,
+							attestation: 'direct'
+						},
+						saving: true
+					};
+					return navigator.credentials.create({
+						publicKey: this.registration.publicKeyOptions
+					});
+				}).then(credential => {
+					this.registration.credential = credential;
+					this.registration.saving = false;
+					this.registration.stage = 1;
+				}).catch(err => {
+					console.warn('Error while registering?', err);
+					this.registration.error = err.message;
+					this.registration.stage = -1;
+				});
+			});
+		},
+
+		updatePasswordLessLogin() {
+			os.api('i/2fa/password-less', {
+				value: !!this.usePasswordLessLogin
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/account-info.vue b/packages/client/src/pages/settings/account-info.vue
new file mode 100644
index 0000000000..f3d5e2f2c3
--- /dev/null
+++ b/packages/client/src/pages/settings/account-info.vue
@@ -0,0 +1,185 @@
+<template>
+<FormBase>
+	<FormKeyValueView>
+		<template #key>ID</template>
+		<template #value><span class="_monospace">{{ $i.id }}</span></template>
+	</FormKeyValueView>
+
+	<FormGroup>
+		<FormKeyValueView>
+			<template #key>{{ $ts.registeredDate }}</template>
+			<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
+		</FormKeyValueView>
+	</FormGroup>
+
+	<FormGroup v-if="stats">
+		<template #label>{{ $ts.statistics }}</template>
+		<FormKeyValueView>
+			<template #key>{{ $ts.notesCount }}</template>
+			<template #value>{{ number(stats.notesCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.repliesCount }}</template>
+			<template #value>{{ number(stats.repliesCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.renotesCount }}</template>
+			<template #value>{{ number(stats.renotesCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.repliedCount }}</template>
+			<template #value>{{ number(stats.repliedCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.renotedCount }}</template>
+			<template #value>{{ number(stats.renotedCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.pollVotesCount }}</template>
+			<template #value>{{ number(stats.pollVotesCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.pollVotedCount }}</template>
+			<template #value>{{ number(stats.pollVotedCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.sentReactionsCount }}</template>
+			<template #value>{{ number(stats.sentReactionsCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.receivedReactionsCount }}</template>
+			<template #value>{{ number(stats.receivedReactionsCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.noteFavoritesCount }}</template>
+			<template #value>{{ number(stats.noteFavoritesCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.followingCount }}</template>
+			<template #value>{{ number(stats.followingCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template>
+			<template #value>{{ number(stats.localFollowingCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template>
+			<template #value>{{ number(stats.remoteFollowingCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.followersCount }}</template>
+			<template #value>{{ number(stats.followersCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template>
+			<template #value>{{ number(stats.localFollowersCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template>
+			<template #value>{{ number(stats.remoteFollowersCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.pageLikesCount }}</template>
+			<template #value>{{ number(stats.pageLikesCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.pageLikedCount }}</template>
+			<template #value>{{ number(stats.pageLikedCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.driveFilesCount }}</template>
+			<template #value>{{ number(stats.driveFilesCount) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.driveUsage }}</template>
+			<template #value>{{ bytes(stats.driveUsage) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.reversiCount }}</template>
+			<template #value>{{ number(stats.reversiCount) }}</template>
+		</FormKeyValueView>
+	</FormGroup>
+
+	<FormGroup>
+		<template #label>{{ $ts.other }}</template>
+		<FormKeyValueView>
+			<template #key>emailVerified</template>
+			<template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>twoFactorEnabled</template>
+			<template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>securityKeys</template>
+			<template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>usePasswordLessLogin</template>
+			<template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>isModerator</template>
+			<template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>isAdmin</template>
+			<template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template>
+		</FormKeyValueView>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import bytes from '@/filters/bytes';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSelect,
+		FormSwitch,
+		FormButton,
+		FormLink,
+		FormGroup,
+		FormKeyValueView,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.accountInfo,
+				icon: 'fas fa-info-circle'
+			},
+			stats: null
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+
+		os.api('users/stats', {
+			userId: this.$i.id
+		}).then(stats => {
+			this.stats = stats;
+		});
+	},
+
+	methods: {
+		number,
+		bytes,
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
new file mode 100644
index 0000000000..94a3c9483d
--- /dev/null
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -0,0 +1,149 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormButton @click="addAccount" primary><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton>
+
+		<div class="_debobigegoItem _button" v-for="account in accounts" :key="account.id" @click="menu(account, $event)">
+			<div class="_debobigegoPanel lcjjdxlm">
+				<div class="avatar">
+					<MkAvatar :user="account" class="avatar"/>
+				</div>
+				<div class="body">
+					<div class="name">
+						<MkUserName :user="account"/>
+					</div>
+					<div class="acct">
+						<MkAcct :user="account"/>
+					</div>
+				</div>
+			</div>
+		</div>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { getAccounts, addAccount, login } from '@/account';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSuspense,
+		FormButton,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.accounts,
+				icon: 'fas fa-users',
+				bg: 'var(--bg)',
+			},
+			storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)),
+			accounts: null,
+			init: async () => os.api('users/show', {
+				userIds: (await this.storedAccounts).map(x => x.id)
+			}).then(accounts => {
+				this.accounts = accounts;
+			}),
+		};
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		menu(account, ev) {
+			os.popupMenu([{
+				text: this.$ts.switch,
+				icon: 'fas fa-exchange-alt',
+				action: () => this.switchAccount(account),
+			}, {
+				text: this.$ts.remove,
+				icon: 'fas fa-trash-alt',
+				danger: true,
+				action: () => this.removeAccount(account),
+			}], ev.currentTarget || ev.target);
+		},
+
+		addAccount(ev) {
+			os.popupMenu([{
+				text: this.$ts.existingAccount,
+				action: () => { this.addExistingAccount(); },
+			}, {
+				text: this.$ts.createAccount,
+				action: () => { this.createAccount(); },
+			}], ev.currentTarget || ev.target);
+		},
+
+		addExistingAccount() {
+			os.popup(import('@/components/signin-dialog.vue'), {}, {
+				done: res => {
+					addAccount(res.id, res.i);
+					os.success();
+				},
+			}, 'closed');
+		},
+
+		createAccount() {
+			os.popup(import('@/components/signup-dialog.vue'), {}, {
+				done: res => {
+					addAccount(res.id, res.i);
+					this.switchAccountWithToken(res.i);
+				},
+			}, 'closed');
+		},
+
+		async switchAccount(account: any) {
+			const storedAccounts = await getAccounts();
+			const token = storedAccounts.find(x => x.id === account.id).token;
+			this.switchAccountWithToken(token);
+		},
+
+		switchAccountWithToken(token: string) {
+			login(token);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.lcjjdxlm {
+	display: flex;
+	padding: 16px;
+
+	> .avatar {
+		display: block;
+		flex-shrink: 0;
+		margin: 0 12px 0 0;
+
+		> .avatar {
+			width: 50px;
+			height: 50px;
+		}
+	}
+
+	> .body {
+		display: flex;
+		flex-direction: column;
+		justify-content: center;
+		width: calc(100% - 62px);
+		position: relative;
+
+		> .name {
+			font-weight: bold;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue
new file mode 100644
index 0000000000..1def0189ec
--- /dev/null
+++ b/packages/client/src/pages/settings/api.vue
@@ -0,0 +1,65 @@
+<template>
+<FormBase>
+	<FormButton @click="generateToken" primary>{{ $ts.generateAccessToken }}</FormButton>
+	<FormLink to="/settings/apps">{{ $ts.manageAccessTokens }}</FormLink>
+	<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		FormLink,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'API',
+				icon: 'fas fa-key',
+				bg: 'var(--bg)',
+			},
+			isDesktop: window.innerWidth >= 1100,
+		};
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		generateToken() {
+			os.popup(import('@/components/token-generate-window.vue'), {}, {
+				done: async result => {
+					const { name, permissions } = result;
+					const { token } = await os.api('miauth/gen-token', {
+						session: null,
+						name: name,
+						permission: permissions,
+					});
+
+					os.dialog({
+						type: 'success',
+						title: this.$ts.token,
+						text: token
+					});
+				},
+			}, 'closed');
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue
new file mode 100644
index 0000000000..6eec80d805
--- /dev/null
+++ b/packages/client/src/pages/settings/apps.vue
@@ -0,0 +1,113 @@
+<template>
+<FormBase>
+	<FormPagination :pagination="pagination" ref="list">
+		<template #empty>
+			<div class="_fullinfo">
+				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+				<div>{{ $ts.nothing }}</div>
+			</div>
+		</template>
+		<template #default="{items}">
+			<div class="_debobigegoPanel bfomjevm" v-for="token in items" :key="token.id">
+				<img class="icon" :src="token.iconUrl" alt="" v-if="token.iconUrl"/>
+				<div class="body">
+					<div class="name">{{ token.name }}</div>
+					<div class="description">{{ token.description }}</div>
+					<div class="_keyValue">
+						<div>{{ $ts.installedDate }}:</div>
+						<div><MkTime :time="token.createdAt"/></div>
+					</div>
+					<div class="_keyValue">
+						<div>{{ $ts.lastUsedDate }}:</div>
+						<div><MkTime :time="token.lastUsedAt"/></div>
+					</div>
+					<div class="actions">
+						<button class="_button" @click="revoke(token)"><i class="fas fa-trash-alt"></i></button>
+					</div>
+					<details>
+						<summary>{{ $ts.details }}</summary>
+						<ul>
+							<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
+						</ul>
+					</details>
+				</div>
+			</div>
+		</template>
+	</FormPagination>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormPagination from '@/components/debobigego/pagination.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormPagination,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.installedApps,
+				icon: 'fas fa-plug',
+				bg: 'var(--bg)',
+			},
+			pagination: {
+				endpoint: 'i/apps',
+				limit: 100,
+				params: {
+					sort: '+lastUsedAt'
+				}
+			},
+		};
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		revoke(token) {
+			os.api('i/revoke-token', { tokenId: token.id }).then(() => {
+				this.$refs.list.reload();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.bfomjevm {
+	display: flex;
+	padding: 16px;
+
+	> .icon {
+		display: block;
+		flex-shrink: 0;
+		margin: 0 12px 0 0;
+		width: 50px;
+		height: 50px;
+		border-radius: 8px;
+	}
+
+	> .body {
+		width: calc(100% - 62px);
+		position: relative;
+
+		> .name {
+			font-weight: bold;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue
new file mode 100644
index 0000000000..8c878fb084
--- /dev/null
+++ b/packages/client/src/pages/settings/custom-css.vue
@@ -0,0 +1,73 @@
+<template>
+<FormBase>
+	<FormInfo warn>{{ $ts.customCssWarn }}</FormInfo>
+
+	<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;">
+		<span>{{ $ts.local }}</span>
+	</FormTextarea>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import { unisonReload } from '@/scripts/unison-reload';
+import * as symbols from '@/symbols';
+import { defaultStore } from '@/store';
+
+export default defineComponent({
+	components: {
+		FormTextarea,
+		FormSelect,
+		FormRadios,
+		FormBase,
+		FormGroup,
+		FormLink,
+		FormButton,
+		FormInfo,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.customCss,
+				icon: 'fas fa-code',
+				bg: 'var(--bg)',
+			},
+			localCustomCss: localStorage.getItem('customCss')
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+
+		this.$watch('localCustomCss', this.apply);
+	},
+
+	methods: {
+		async apply() {
+			localStorage.setItem('customCss', this.localCustomCss);
+
+			const { canceled } = await os.dialog({
+				type: 'info',
+				text: this.$ts.reloadToApplySetting,
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			unisonReload();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue
new file mode 100644
index 0000000000..a96c6cd685
--- /dev/null
+++ b/packages/client/src/pages/settings/deck.vue
@@ -0,0 +1,107 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
+		<FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch>
+	</FormGroup>
+
+	<FormSwitch v-model="alwaysShowMainColumn">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
+
+	<FormRadios v-model="columnAlign">
+		<template #desc>{{ $ts._deck.columnAlign }}</template>
+		<option value="left">{{ $ts.left }}</option>
+		<option value="center">{{ $ts.center }}</option>
+	</FormRadios>
+
+	<FormRadios v-model="columnHeaderHeight">
+		<template #desc>{{ $ts._deck.columnHeaderHeight }}</template>
+		<option :value="42">{{ $ts.narrow }}</option>
+		<option :value="45">{{ $ts.medium }}</option>
+		<option :value="48">{{ $ts.wide }}</option>
+	</FormRadios>
+
+	<FormInput v-model="columnMargin" type="number">
+		<span>{{ $ts._deck.columnMargin }}</span>
+		<template #suffix>px</template>
+	</FormInput>
+
+	<FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormRadios from '@/components/debobigego/radios.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import { deckStore } from '@/ui/deck/deck-store';
+import * as os from '@/os';
+import { unisonReload } from '@/scripts/unison-reload';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormLink,
+		FormInput,
+		FormRadios,
+		FormBase,
+		FormGroup,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.deck,
+				icon: 'fas fa-columns',
+				bg: 'var(--bg)',
+			},
+		}
+	},
+
+	computed: {
+		navWindow: deckStore.makeGetterSetter('navWindow'),
+		alwaysShowMainColumn: deckStore.makeGetterSetter('alwaysShowMainColumn'),
+		columnAlign: deckStore.makeGetterSetter('columnAlign'),
+		columnMargin: deckStore.makeGetterSetter('columnMargin'),
+		columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'),
+		profile: deckStore.makeGetterSetter('profile'),
+	},
+
+	watch: {
+		async navWindow() {
+			const { canceled } = await os.dialog({
+				type: 'info',
+				text: this.$ts.reloadToApplySetting,
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			unisonReload();
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async setProfile() {
+			const { canceled, result: name } = await os.dialog({
+				title: this.$ts._deck.profile,
+				input: {
+					allowEmpty: false
+				}
+			});
+			if (canceled) return;
+			this.profile = name;
+			unisonReload();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue
new file mode 100644
index 0000000000..018f7c795e
--- /dev/null
+++ b/packages/client/src/pages/settings/delete-account.vue
@@ -0,0 +1,68 @@
+<template>
+<FormBase>
+	<FormInfo warn>{{ $ts._accountDelete.mayTakeTime }}</FormInfo>
+	<FormInfo>{{ $ts._accountDelete.sendEmail }}</FormInfo>
+	<FormButton @click="deleteAccount" danger v-if="!$i.isDeleted">{{ $ts._accountDelete.requestAccountDelete }}</FormButton>
+	<FormButton disabled v-else>{{ $ts._accountDelete.inProgress }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import { debug } from '@/config';
+import { signout } from '@/account';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		FormGroup,
+		FormInfo,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts._accountDelete.accountDelete,
+				icon: 'fas fa-exclamation-triangle',
+				bg: 'var(--bg)',
+			},
+			debug,
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async deleteAccount() {
+			const { canceled, result: password } = await os.dialog({
+				title: this.$ts.password,
+				input: {
+					type: 'password'
+				}
+			});
+			if (canceled) return;
+
+			await os.apiWithDialog('i/delete-account', {
+				password: password
+			});
+
+			await os.dialog({
+				title: this.$ts._accountDelete.started,
+			});
+
+			signout();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue
new file mode 100644
index 0000000000..ed5282e23d
--- /dev/null
+++ b/packages/client/src/pages/settings/drive.vue
@@ -0,0 +1,147 @@
+<template>
+<FormBase class="">
+	<FormGroup v-if="!fetching">
+		<template #label>{{ $ts.usageAmount }}</template>
+		<div class="_debobigegoItem uawsfosz">
+			<div class="_debobigegoPanel">
+				<div class="meter"><div :style="meterStyle"></div></div>
+			</div>
+		</div>
+		<FormKeyValueView>
+			<template #key>{{ $ts.capacity }}</template>
+			<template #value>{{ bytes(capacity, 1) }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.inUse }}</template>
+			<template #value>{{ bytes(usage, 1) }}</template>
+		</FormKeyValueView>
+	</FormGroup>
+
+	<div class="_debobigegoItem">
+		<div class="_debobigegoLabel">{{ $ts.statistics }}</div>
+		<div class="_debobigegoPanel">
+			<div ref="chart"></div>
+		</div>
+	</div>
+
+	<FormButton :center="false" @click="chooseUploadFolder()" primary>
+		{{ $ts.uploadFolder }}
+		<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
+		<template #suffixIcon><i class="fas fa-folder-open"></i></template>
+	</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as tinycolor from 'tinycolor2';
+import FormButton from '@/components/debobigego/button.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import * as os from '@/os';
+import bytes from '@/filters/bytes';
+import * as symbols from '@/symbols';
+
+// TODO: render chart
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		FormGroup,
+		FormKeyValueView,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.drive,
+				icon: 'fas fa-cloud',
+				bg: 'var(--bg)',
+			},
+			fetching: true,
+			usage: null,
+			capacity: null,
+			uploadFolder: null,
+		}
+	},
+
+	computed: {
+		meterStyle(): any {
+			return {
+				width: `${this.usage / this.capacity * 100}%`,
+				background: tinycolor({
+					h: 180 - (this.usage / this.capacity * 180),
+					s: 0.7,
+					l: 0.5
+				})
+			};
+		}
+	},
+
+	async created() {
+		os.api('drive').then(info => {
+			this.capacity = info.capacity;
+			this.usage = info.usage;
+			this.fetching = false;
+			this.$nextTick(() => {
+				this.renderChart();
+			});
+		});
+
+		if (this.$store.state.uploadFolder) {
+			this.uploadFolder = await os.api('drive/folders/show', {
+				folderId: this.$store.state.uploadFolder
+			});
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		chooseUploadFolder() {
+			os.selectDriveFolder(false).then(async folder => {
+				this.$store.set('uploadFolder', folder ? folder.id : null);
+				os.success();
+				if (this.$store.state.uploadFolder) {
+					this.uploadFolder = await os.api('drive/folders/show', {
+						folderId: this.$store.state.uploadFolder
+					});
+				} else {
+					this.uploadFolder = null;
+				}
+			});
+		},
+
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+@use "sass:math";
+
+.uawsfosz {
+	> div {
+		padding: 24px;
+
+		> .meter {
+			$size: 12px;
+			background: rgba(0, 0, 0, 0.1);
+			border-radius: math.div($size, 2);
+			overflow: hidden;
+
+			> div {
+				height: $size;
+				border-radius: math.div($size, 2);
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/settings/email-address.vue b/packages/client/src/pages/settings/email-address.vue
new file mode 100644
index 0000000000..476d0c0e17
--- /dev/null
+++ b/packages/client/src/pages/settings/email-address.vue
@@ -0,0 +1,70 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<FormInput v-model="emailAddress" type="email">
+			{{ $ts.emailAddress }}
+			<template #desc v-if="$i.email && !$i.emailVerified">{{ $ts.verificationEmailSent }}</template>
+			<template #desc v-else-if="emailAddress === $i.email && $i.emailVerified">{{ $ts.emailVerified }}</template>
+		</FormInput>
+	</FormGroup>
+	<FormButton @click="save" primary>{{ $ts.save }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormInput from '@/components/form/input.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormInput,
+		FormButton,
+		FormGroup,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.emailAddress,
+				icon: 'fas fa-envelope',
+				bg: 'var(--bg)',
+			},
+			emailAddress: null,
+			code: null,
+		}
+	},
+
+	created() {
+		this.emailAddress = this.$i.email;
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		save() {
+			os.dialog({
+				title: this.$ts.password,
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				os.apiWithDialog('i/update-email', {
+					password: password,
+					email: this.emailAddress,
+				});
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/email-notification.vue b/packages/client/src/pages/settings/email-notification.vue
new file mode 100644
index 0000000000..c1735a0728
--- /dev/null
+++ b/packages/client/src/pages/settings/email-notification.vue
@@ -0,0 +1,91 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<FormSwitch v-model="mention">
+			{{ $ts._notification._types.mention }}
+		</FormSwitch>
+		<FormSwitch v-model="reply">
+			{{ $ts._notification._types.reply }}
+		</FormSwitch>
+		<FormSwitch v-model="quote">
+			{{ $ts._notification._types.quote }}
+		</FormSwitch>
+		<FormSwitch v-model="follow">
+			{{ $ts._notification._types.follow }}
+		</FormSwitch>
+		<FormSwitch v-model="receiveFollowRequest">
+			{{ $ts._notification._types.receiveFollowRequest }}
+		</FormSwitch>
+		<FormSwitch v-model="groupInvited">
+			{{ $ts._notification._types.groupInvited }}
+		</FormSwitch>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSwitch,
+		FormButton,
+		FormGroup,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.emailNotification,
+				icon: 'fas fa-envelope',
+				bg: 'var(--bg)',
+			},
+
+			mention: this.$i.emailNotificationTypes.includes('mention'),
+			reply: this.$i.emailNotificationTypes.includes('reply'),
+			quote: this.$i.emailNotificationTypes.includes('quote'),
+			follow: this.$i.emailNotificationTypes.includes('follow'),
+			receiveFollowRequest: this.$i.emailNotificationTypes.includes('receiveFollowRequest'),
+			groupInvited: this.$i.emailNotificationTypes.includes('groupInvited'),
+		}
+	},
+
+	created() {
+		this.$watch('mention', this.save);
+		this.$watch('reply', this.save);
+		this.$watch('quote', this.save);
+		this.$watch('follow', this.save);
+		this.$watch('receiveFollowRequest', this.save);
+		this.$watch('groupInvited', this.save);
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		save() {
+			os.api('i/update', {
+				emailNotificationTypes: [
+					...[this.mention ? 'mention' : null],
+					...[this.reply ? 'reply' : null],
+					...[this.quote ? 'quote' : null],
+					...[this.follow ? 'follow' : null],
+					...[this.receiveFollowRequest ? 'receiveFollowRequest' : null],
+					...[this.groupInvited ? 'groupInvited' : null],
+				].filter(x => x != null)
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue
new file mode 100644
index 0000000000..d1dda20f00
--- /dev/null
+++ b/packages/client/src/pages/settings/email.vue
@@ -0,0 +1,66 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<template #label>{{ $ts.emailAddress }}</template>
+		<FormLink to="/settings/email/address">
+			<template v-if="$i.email && !$i.emailVerified" #icon><i class="fas fa-exclamation-triangle" style="color: var(--warn);"></i></template>
+			<template v-else-if="$i.email && $i.emailVerified" #icon><i class="fas fa-check" style="color: var(--success);"></i></template>
+			{{ $i.email || $ts.notSet }}
+		</FormLink>
+	</FormGroup>
+
+	<FormLink to="/settings/email/notification">
+		<template #icon><i class="fas fa-bell"></i></template>
+		{{ $ts.emailNotification }}
+	</FormLink>
+
+	<FormSwitch :value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
+		{{ $ts.receiveAnnouncementFromInstance }}
+	</FormSwitch>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormLink,
+		FormButton,
+		FormSwitch,
+		FormGroup,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.email,
+				icon: 'fas fa-envelope',
+				bg: 'var(--bg)',
+			},
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		onChangeReceiveAnnouncementEmail(v) {
+			os.api('i/update', {
+				receiveAnnouncementEmail: v
+			});
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/experimental-features.vue b/packages/client/src/pages/settings/experimental-features.vue
new file mode 100644
index 0000000000..5a7bcb3b41
--- /dev/null
+++ b/packages/client/src/pages/settings/experimental-features.vue
@@ -0,0 +1,52 @@
+<template>
+<FormBase>
+	<FormButton @click="error()">error test</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSelect,
+		FormSwitch,
+		FormButton,
+		FormLink,
+		FormGroup,
+		FormKeyValueView,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.experimentalFeatures,
+				icon: 'fas fa-flask'
+			},
+			stats: null
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		error() {
+			throw new Error('Test error');
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue
new file mode 100644
index 0000000000..8e3dcc3e41
--- /dev/null
+++ b/packages/client/src/pages/settings/general.vue
@@ -0,0 +1,223 @@
+<template>
+<FormBase>
+	<FormSwitch v-model="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch>
+
+	<FormSelect v-model="lang">
+		<template #label>{{ $ts.uiLanguage }}</template>
+		<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+		<template #caption>
+			<I18n :src="$ts.i18nInfo" tag="span">
+				<template #link>
+					<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
+				</template>
+			</I18n>
+		</template>
+	</FormSelect>
+
+	<FormGroup>
+		<template #label>{{ $ts.behavior }}</template>
+		<FormSwitch v-model="imageNewTab">{{ $ts.openImageInNewTab }}</FormSwitch>
+		<FormSwitch v-model="enableInfiniteScroll">{{ $ts.enableInfiniteScroll }}</FormSwitch>
+		<FormSwitch v-model="useReactionPickerForContextMenu">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch>
+		<FormSwitch v-model="disablePagesScript">{{ $ts.disablePagesScript }}</FormSwitch>
+	</FormGroup>
+
+	<FormSelect v-model="serverDisconnectedBehavior">
+		<template #label>{{ $ts.whenServerDisconnected }}</template>
+		<option value="reload">{{ $ts._serverDisconnectedBehavior.reload }}</option>
+		<option value="dialog">{{ $ts._serverDisconnectedBehavior.dialog }}</option>
+		<option value="quiet">{{ $ts._serverDisconnectedBehavior.quiet }}</option>
+	</FormSelect>
+
+	<FormGroup>
+		<template #label>{{ $ts.appearance }}</template>
+		<FormSwitch v-model="disableAnimatedMfm">{{ $ts.disableAnimatedMfm }}</FormSwitch>
+		<FormSwitch v-model="reduceAnimation">{{ $ts.reduceUiAnimation }}</FormSwitch>
+		<FormSwitch v-model="useBlurEffect">{{ $ts.useBlurEffect }}</FormSwitch>
+		<FormSwitch v-model="useBlurEffectForModal">{{ $ts.useBlurEffectForModal }}</FormSwitch>
+		<FormSwitch v-model="showGapBetweenNotesInTimeline">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch>
+		<FormSwitch v-model="loadRawImages">{{ $ts.loadRawImages }}</FormSwitch>
+		<FormSwitch v-model="disableShowingAnimatedImages">{{ $ts.disableShowingAnimatedImages }}</FormSwitch>
+		<FormSwitch v-model="squareAvatars">{{ $ts.squareAvatars }}</FormSwitch>
+		<FormSwitch v-model="useSystemFont">{{ $ts.useSystemFont }}</FormSwitch>
+		<FormSwitch v-model="useOsNativeEmojis">{{ $ts.useOsNativeEmojis }}
+			<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪" :key="useOsNativeEmojis"/></div>
+		</FormSwitch>
+	</FormGroup>
+
+	<FormGroup>
+		<FormSwitch v-model="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch>
+	</FormGroup>
+
+	<FormRadios v-model="fontSize">
+		<template #desc>{{ $ts.fontSize }}</template>
+		<option value="small"><span style="font-size: 14px;">Aa</span></option>
+		<option :value="null"><span style="font-size: 16px;">Aa</span></option>
+		<option value="large"><span style="font-size: 18px;">Aa</span></option>
+		<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
+	</FormRadios>
+
+	<FormSelect v-model="instanceTicker">
+		<template #label>{{ $ts.instanceTicker }}</template>
+		<option value="none">{{ $ts._instanceTicker.none }}</option>
+		<option value="remote">{{ $ts._instanceTicker.remote }}</option>
+		<option value="always">{{ $ts._instanceTicker.always }}</option>
+	</FormSelect>
+
+	<FormSelect v-model="nsfw">
+		<template #label>{{ $ts.nsfw }}</template>
+		<option value="respect">{{ $ts._nsfw.respect }}</option>
+		<option value="ignore">{{ $ts._nsfw.ignore }}</option>
+		<option value="force">{{ $ts._nsfw.force }}</option>
+	</FormSelect>
+
+	<FormGroup>
+		<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
+		<FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch>
+	</FormGroup>
+
+	<FormSelect v-model="chatOpenBehavior">
+		<template #label>{{ $ts.chatOpenBehavior }}</template>
+		<option value="page">{{ $ts.showInPage }}</option>
+		<option value="window">{{ $ts.openInWindow }}</option>
+		<option value="popout">{{ $ts.popout }}</option>
+	</FormSelect>
+
+	<FormLink to="/settings/deck">{{ $ts.deck }}</FormLink>
+
+	<FormLink to="/settings/custom-css"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormSelect from '@/components/debobigego/select.vue';
+import FormRadios from '@/components/debobigego/radios.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import MkLink from '@/components/link.vue';
+import { langs } from '@/config';
+import { defaultStore } from '@/store';
+import { ColdDeviceStorage } from '@/store';
+import * as os from '@/os';
+import { unisonReload } from '@/scripts/unison-reload';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkLink,
+		FormSwitch,
+		FormSelect,
+		FormRadios,
+		FormBase,
+		FormGroup,
+		FormLink,
+		FormButton,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.general,
+				icon: 'fas fa-cogs',
+				bg: 'var(--bg)'
+			},
+			langs,
+			lang: localStorage.getItem('lang'),
+			fontSize: localStorage.getItem('fontSize'),
+			useSystemFont: localStorage.getItem('useSystemFont') != null,
+		}
+	},
+
+	computed: {
+		serverDisconnectedBehavior: defaultStore.makeGetterSetter('serverDisconnectedBehavior'),
+		reduceAnimation: defaultStore.makeGetterSetter('animation', v => !v, v => !v),
+		useBlurEffectForModal: defaultStore.makeGetterSetter('useBlurEffectForModal'),
+		useBlurEffect: defaultStore.makeGetterSetter('useBlurEffect'),
+		showGapBetweenNotesInTimeline: defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'),
+		disableAnimatedMfm: defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v),
+		useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'),
+		disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'),
+		loadRawImages: defaultStore.makeGetterSetter('loadRawImages'),
+		imageNewTab: defaultStore.makeGetterSetter('imageNewTab'),
+		nsfw: defaultStore.makeGetterSetter('nsfw'),
+		disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'),
+		showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'),
+		defaultSideView: defaultStore.makeGetterSetter('defaultSideView'),
+		chatOpenBehavior: ColdDeviceStorage.makeGetterSetter('chatOpenBehavior'),
+		instanceTicker: defaultStore.makeGetterSetter('instanceTicker'),
+		enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
+		useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'),
+		squareAvatars: defaultStore.makeGetterSetter('squareAvatars'),
+		aiChanMode: defaultStore.makeGetterSetter('aiChanMode'),
+	},
+
+	watch: {
+		lang() {
+			localStorage.setItem('lang', this.lang);
+			localStorage.removeItem('locale');
+			this.reloadAsk();
+		},
+
+		fontSize() {
+			if (this.fontSize == null) {
+				localStorage.removeItem('fontSize');
+			} else {
+				localStorage.setItem('fontSize', this.fontSize);
+			}
+			this.reloadAsk();
+		},
+
+		useSystemFont() {
+			if (this.useSystemFont) {
+				localStorage.setItem('useSystemFont', 't');
+			} else {
+				localStorage.removeItem('useSystemFont');
+			}
+			this.reloadAsk();
+		},
+
+		enableInfiniteScroll() {
+			this.reloadAsk();
+		},
+
+		squareAvatars() {
+			this.reloadAsk();
+		},
+
+		aiChanMode() {
+			this.reloadAsk();
+		},
+
+		showGapBetweenNotesInTimeline() {
+			this.reloadAsk();
+		},
+
+		instanceTicker() {
+			this.reloadAsk();
+		},
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async reloadAsk() {
+			const { canceled } = await os.dialog({
+				type: 'info',
+				text: this.$ts.reloadToApplySetting,
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			unisonReload();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue
new file mode 100644
index 0000000000..8923483b98
--- /dev/null
+++ b/packages/client/src/pages/settings/import-export.vue
@@ -0,0 +1,112 @@
+<template>
+<div style="margin: 16px;">
+	<FormSection>
+		<template #label>{{ $ts._exportOrImport.allNotes }}</template>
+		<MkButton :class="$style.button" inline @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+	</FormSection>
+	<FormSection>
+		<template #label>{{ $ts._exportOrImport.followingList }}</template>
+		<MkButton :class="$style.button" inline @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+		<MkButton :class="$style.button" inline @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+	</FormSection>
+	<FormSection>
+		<template #label>{{ $ts._exportOrImport.userLists }}</template>
+		<MkButton :class="$style.button" inline @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+		<MkButton :class="$style.button" inline @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+	</FormSection>
+	<FormSection>
+		<template #label>{{ $ts._exportOrImport.muteList }}</template>
+		<MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+		<MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+	</FormSection>
+	<FormSection>
+		<template #label>{{ $ts._exportOrImport.blockingList }}</template>
+		<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+		<MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+	</FormSection>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import FormSection from '@/components/form/section.vue';
+import * as os from '@/os';
+import { selectFile } from '@/scripts/select-file';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormSection,
+		MkButton,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.importAndExport,
+				icon: 'fas fa-boxes',
+				bg: 'var(--bg)',
+			},
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		doExport(target) {
+			os.api(
+				target === 'notes' ? 'i/export-notes' :
+				target === 'following' ? 'i/export-following' :
+				target === 'blocking' ? 'i/export-blocking' :
+				target === 'user-lists' ? 'i/export-user-lists' :
+				target === 'muting' ? 'i/export-mute' :
+				null, {})
+			.then(() => {
+				os.dialog({
+					type: 'info',
+					text: this.$ts.exportRequested
+				});
+			}).catch((e: any) => {
+				os.dialog({
+					type: 'error',
+					text: e.message
+				});
+			});
+		},
+
+		async doImport(target, e) {
+			const file = await selectFile(e.currentTarget || e.target);
+			
+			os.api(
+				target === 'following' ? 'i/import-following' :
+				target === 'user-lists' ? 'i/import-user-lists' :
+				target === 'muting' ? 'i/import-muting' :
+				target === 'blocking' ? 'i/import-blocking' :
+				null, {
+					fileId: file.id
+			}).then(() => {
+				os.dialog({
+					type: 'info',
+					text: this.$ts.importRequested
+				});
+			}).catch((e: any) => {
+				os.dialog({
+					type: 'error',
+					text: e.message
+				});
+			});
+		},
+	}
+});
+</script>
+
+<style module>
+.button {
+	margin-right: 16px;
+}
+</style>
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
new file mode 100644
index 0000000000..b9d3903269
--- /dev/null
+++ b/packages/client/src/pages/settings/index.vue
@@ -0,0 +1,326 @@
+<template>
+<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
+	<div class="nav" v-if="!narrow || page == null">
+		<MkSpacer :content-max="700">
+			<div class="baaadecd">
+				<div class="title">{{ $ts.settings }}</div>
+				<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+				<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
+			</div>
+		</MkSpacer>
+	</div>
+	<div class="main">
+		<component :is="component" :key="page" v-bind="pageProps"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
+import { i18n } from '@/i18n';
+import MkInfo from '@/components/ui/info.vue';
+import MkSuperMenu from '@/components/ui/super-menu.vue';
+import { scroll } from '@/scripts/scroll';
+import { signout } from '@/account';
+import { unisonReload } from '@/scripts/unison-reload';
+import * as symbols from '@/symbols';
+import { instance } from '@/instance';
+import { $i } from '@/account';
+
+export default defineComponent({
+	components: {
+		MkInfo,
+		MkSuperMenu,
+	},
+
+	props: {
+		initialPage: {
+			type: String,
+			required: false
+		}
+	},
+
+	setup(props, context) {
+		const indexInfo = {
+			title: i18n.locale.settings,
+			icon: 'fas fa-cog',
+			bg: 'var(--bg)',
+			hideHeader: true,
+		};
+		const INFO = ref(indexInfo);
+		const page = ref(props.initialPage);
+		const narrow = ref(false);
+		const view = ref(null);
+		const el = ref(null);
+		const menuDef = computed(() => [{
+			title: i18n.locale.basicSettings,
+			items: [{
+				icon: 'fas fa-user',
+				text: i18n.locale.profile,
+				to: '/settings/profile',
+				active: page.value === 'profile',
+			}, {
+				icon: 'fas fa-lock-open',
+				text: i18n.locale.privacy,
+				to: '/settings/privacy',
+				active: page.value === 'privacy',
+			}, {
+				icon: 'fas fa-laugh',
+				text: i18n.locale.reaction,
+				to: '/settings/reaction',
+				active: page.value === 'reaction',
+			}, {
+				icon: 'fas fa-cloud',
+				text: i18n.locale.drive,
+				to: '/settings/drive',
+				active: page.value === 'drive',
+			}, {
+				icon: 'fas fa-bell',
+				text: i18n.locale.notifications,
+				to: '/settings/notifications',
+				active: page.value === 'notifications',
+			}, {
+				icon: 'fas fa-envelope',
+				text: i18n.locale.email,
+				to: '/settings/email',
+				active: page.value === 'email',
+			}, {
+				icon: 'fas fa-share-alt',
+				text: i18n.locale.integration,
+				to: '/settings/integration',
+				active: page.value === 'integration',
+			}, {
+				icon: 'fas fa-lock',
+				text: i18n.locale.security,
+				to: '/settings/security',
+				active: page.value === 'security',
+			}],
+		}, {
+			title: i18n.locale.clientSettings,
+			items: [{
+				icon: 'fas fa-cogs',
+				text: i18n.locale.general,
+				to: '/settings/general',
+				active: page.value === 'general',
+			}, {
+				icon: 'fas fa-palette',
+				text: i18n.locale.theme,
+				to: '/settings/theme',
+				active: page.value === 'theme',
+			}, {
+				icon: 'fas fa-list-ul',
+				text: i18n.locale.menu,
+				to: '/settings/menu',
+				active: page.value === 'menu',
+			}, {
+				icon: 'fas fa-music',
+				text: i18n.locale.sounds,
+				to: '/settings/sounds',
+				active: page.value === 'sounds',
+			}, {
+				icon: 'fas fa-plug',
+				text: i18n.locale.plugins,
+				to: '/settings/plugin',
+				active: page.value === 'plugin',
+			}],
+		}, {
+			title: i18n.locale.otherSettings,
+			items: [{
+				icon: 'fas fa-boxes',
+				text: i18n.locale.importAndExport,
+				to: '/settings/import-export',
+				active: page.value === 'import-export',
+			}, {
+				icon: 'fas fa-ban',
+				text: i18n.locale.muteAndBlock,
+				to: '/settings/mute-block',
+				active: page.value === 'mute-block',
+			}, {
+				icon: 'fas fa-comment-slash',
+				text: i18n.locale.wordMute,
+				to: '/settings/word-mute',
+				active: page.value === 'word-mute',
+			}, {
+				icon: 'fas fa-key',
+				text: 'API',
+				to: '/settings/api',
+				active: page.value === 'api',
+			}, {
+				icon: 'fas fa-ellipsis-h',
+				text: i18n.locale.other,
+				to: '/settings/other',
+				active: page.value === 'other',
+			}],
+		}, {
+			items: [{
+				type: 'button',
+				icon: 'fas fa-trash',
+				text: i18n.locale.clearCache,
+				action: () => {
+					localStorage.removeItem('locale');
+					localStorage.removeItem('theme');
+					unisonReload();
+				},
+			}, {
+				type: 'button',
+				icon: 'fas fa-sign-in-alt fa-flip-horizontal',
+				text: i18n.locale.logout,
+				action: () => {
+					signout();
+				},
+				danger: true,
+			},],
+		}]);
+
+		const pageProps = ref({});
+		const component = computed(() => {
+			if (page.value == null) return null;
+			switch (page.value) {
+				case 'accounts': return defineAsyncComponent(() => import('./accounts.vue'));
+				case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
+				case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
+				case 'reaction': return defineAsyncComponent(() => import('./reaction.vue'));
+				case 'drive': return defineAsyncComponent(() => import('./drive.vue'));
+				case 'notifications': return defineAsyncComponent(() => import('./notifications.vue'));
+				case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue'));
+				case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
+				case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
+				case 'security': return defineAsyncComponent(() => import('./security.vue'));
+				case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
+				case 'api': return defineAsyncComponent(() => import('./api.vue'));
+				case 'apps': return defineAsyncComponent(() => import('./apps.vue'));
+				case 'other': return defineAsyncComponent(() => import('./other.vue'));
+				case 'general': return defineAsyncComponent(() => import('./general.vue'));
+				case 'email': return defineAsyncComponent(() => import('./email.vue'));
+				case 'email/address': return defineAsyncComponent(() => import('./email-address.vue'));
+				case 'email/notification': return defineAsyncComponent(() => import('./email-notification.vue'));
+				case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
+				case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
+				case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
+				case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
+				case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
+				case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
+				case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
+				case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
+				case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
+				case 'plugin/manage': return defineAsyncComponent(() => import('./plugin.manage.vue'));
+				case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
+				case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
+				case 'update': return defineAsyncComponent(() => import('./update.vue'));
+				case 'registry': return defineAsyncComponent(() => import('./registry.vue'));
+				case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue'));
+				case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
+			}
+			if (page.value.startsWith('registry/keys/system/')) {
+				return defineAsyncComponent(() => import('./registry.keys.vue'));
+			}
+			if (page.value.startsWith('registry/value/system/')) {
+				return defineAsyncComponent(() => import('./registry.value.vue'));
+			}
+		});
+
+		watch(component, () => {
+			pageProps.value = {};
+
+			if (page.value) {
+				if (page.value.startsWith('registry/keys/system/')) {
+					pageProps.value.scope = page.value.replace('registry/keys/system/', '').split('/');
+				}
+				if (page.value.startsWith('registry/value/system/')) {
+					const path = page.value.replace('registry/value/system/', '').split('/');
+					pageProps.value.xKey = path.pop();
+					pageProps.value.scope = path;
+				}
+			}
+
+			nextTick(() => {
+				scroll(el.value, { top: 0 });
+			});
+		}, { immediate: true });
+
+		watch(() => props.initialPage, () => {
+			if (props.initialPage == null && !narrow.value) {
+				page.value = 'profile';
+			} else {
+				page.value = props.initialPage;
+				if (props.initialPage == null) {
+					INFO.value = indexInfo;
+				}
+			}
+		});
+
+		onMounted(() => {
+			narrow.value = el.value.offsetWidth < 800;
+			if (!narrow.value) {
+				page.value = 'profile';
+			}
+		});
+
+		const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
+
+		return {
+			[symbols.PAGE_INFO]: INFO,
+			page,
+			menuDef,
+			narrow,
+			view,
+			el,
+			pageProps,
+			component,
+			emailNotConfigured,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.vvcocwet {
+	> .nav {
+		.baaadecd {
+			> .title {
+				margin: 16px;
+				font-size: 1.5em;
+				font-weight: bold;
+			}
+
+			> .info {
+				margin: 0 16px;
+			}
+
+			> .accounts {
+				> .avatar {
+					display: block;
+					width: 50px;
+					height: 50px;
+					margin: 8px auto 16px auto;
+				}
+			}
+		}
+	}
+
+	&.wide {
+		display: flex;
+		max-width: 1000px;
+		margin: 0 auto;
+		height: 100%;
+
+		> .nav {
+			width: 32%;
+			box-sizing: border-box;
+			overflow: auto;
+
+			.baaadecd {
+				> .title {
+					margin: 24px 0;
+				}
+			}
+		}
+
+		> .main {
+			flex: 1;
+			min-width: 0;
+			overflow: auto;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue
new file mode 100644
index 0000000000..405f93b779
--- /dev/null
+++ b/packages/client/src/pages/settings/integration.vue
@@ -0,0 +1,141 @@
+<template>
+<FormBase>
+	<div class="_debobigegoItem" v-if="enableTwitterIntegration">
+		<div class="_debobigegoLabel"><i class="fab fa-twitter"></i> Twitter</div>
+		<div class="_debobigegoPanel" style="padding: 16px;">
+			<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
+			<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectService }}</MkButton>
+			<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectService }}</MkButton>
+		</div>
+	</div>
+
+	<div class="_debobigegoItem" v-if="enableDiscordIntegration">
+		<div class="_debobigegoLabel"><i class="fab fa-discord"></i> Discord</div>
+		<div class="_debobigegoPanel" style="padding: 16px;">
+			<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
+			<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectService }}</MkButton>
+			<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectService }}</MkButton>
+		</div>
+	</div>
+
+	<div class="_debobigegoItem" v-if="enableGithubIntegration">
+		<div class="_debobigegoLabel"><i class="fab fa-github"></i> GitHub</div>
+		<div class="_debobigegoPanel" style="padding: 16px;">
+			<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
+			<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectService }}</MkButton>
+			<MkButton v-else @click="connectGithub" primary>{{ $ts.connectService }}</MkButton>
+		</div>
+	</div>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { apiUrl } from '@/config';
+import FormBase from '@/components/debobigego/base.vue';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		MkButton
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.integration,
+				icon: 'fas fa-share-alt',
+				bg: 'var(--bg)',
+			},
+			apiUrl,
+			twitterForm: null,
+			discordForm: null,
+			githubForm: null,
+			enableTwitterIntegration: false,
+			enableDiscordIntegration: false,
+			enableGithubIntegration: false,
+		};
+	},
+
+	computed: {
+		integrations() {
+			return this.$i.integrations;
+		},
+		
+		meta() {
+			return this.$instance;
+		},
+	},
+
+	created() {
+		this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
+		this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
+		this.enableGithubIntegration = this.meta.enableGithubIntegration;
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+
+		document.cookie = `igi=${this.$i.token}; path=/;` +
+			` max-age=31536000;` +
+			(document.location.protocol.startsWith('https') ? ' secure' : '');
+
+		this.$watch('integrations', () => {
+			if (this.integrations.twitter) {
+				if (this.twitterForm) this.twitterForm.close();
+			}
+			if (this.integrations.discord) {
+				if (this.discordForm) this.discordForm.close();
+			}
+			if (this.integrations.github) {
+				if (this.githubForm) this.githubForm.close();
+			}
+		}, {
+			deep: true
+		});
+	},
+
+	methods: {
+		connectTwitter() {
+			this.twitterForm = window.open(apiUrl + '/connect/twitter',
+				'twitter_connect_window',
+				'height=570, width=520');
+		},
+
+		disconnectTwitter() {
+			window.open(apiUrl + '/disconnect/twitter',
+				'twitter_disconnect_window',
+				'height=570, width=520');
+		},
+
+		connectDiscord() {
+			this.discordForm = window.open(apiUrl + '/connect/discord',
+				'discord_connect_window',
+				'height=570, width=520');
+		},
+
+		disconnectDiscord() {
+			window.open(apiUrl + '/disconnect/discord',
+				'discord_disconnect_window',
+				'height=570, width=520');
+		},
+
+		connectGithub() {
+			this.githubForm = window.open(apiUrl + '/connect/github',
+				'github_connect_window',
+				'height=570, width=520');
+		},
+
+		disconnectGithub() {
+			window.open(apiUrl + '/disconnect/github',
+				'github_disconnect_window',
+				'height=570, width=520');
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/menu.vue
new file mode 100644
index 0000000000..e40740a3a4
--- /dev/null
+++ b/packages/client/src/pages/settings/menu.vue
@@ -0,0 +1,117 @@
+<template>
+<FormBase>
+	<FormTextarea v-model="items" tall manual-save>
+		<span>{{ $ts.menu }}</span>
+		<template #desc><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template>
+	</FormTextarea>
+
+	<FormRadios v-model="menuDisplay">
+		<template #desc>{{ $ts.display }}</template>
+		<option value="sideFull">{{ $ts._menuDisplay.sideFull }}</option>
+		<option value="sideIcon">{{ $ts._menuDisplay.sideIcon }}</option>
+		<option value="top">{{ $ts._menuDisplay.top }}</option>
+		<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ $ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
+	</FormRadios>
+
+	<FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormRadios from '@/components/debobigego/radios.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import { defaultStore } from '@/store';
+import * as symbols from '@/symbols';
+import { unisonReload } from '@/scripts/unison-reload';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		FormTextarea,
+		FormRadios,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.menu,
+				icon: 'fas fa-list-ul',
+				bg: 'var(--bg)',
+			},
+			menuDef: menuDef,
+			items: defaultStore.state.menu.join('\n'),
+		}
+	},
+
+	computed: {
+		splited(): string[] {
+			return this.items.trim().split('\n').filter(x => x.trim() !== '');
+		},
+
+		menuDisplay: defaultStore.makeGetterSetter('menuDisplay')
+	},
+
+	watch: {
+		menuDisplay() {
+			this.reloadAsk();
+		},
+
+		items() {
+			this.save();
+		},
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async addItem() {
+			const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k));
+			const { canceled, result: item } = await os.dialog({
+				type: null,
+				title: this.$ts.addItem,
+				select: {
+					items: [...menu.map(k => ({
+						value: k, text: this.$ts[this.menuDef[k].title]
+					})), ...[{
+						value: '-', text: this.$ts.divider
+					}]]
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			this.items = [...this.splited, item].join('\n');
+		},
+
+		save() {
+			this.$store.set('menu', this.splited);
+			this.reloadAsk();
+		},
+
+		reset() {
+			this.$store.reset('menu');
+			this.items = this.$store.state.menu.join('\n');
+		},
+
+		async reloadAsk() {
+			const { canceled } = await os.dialog({
+				type: 'info',
+				text: this.$ts.reloadToApplySetting,
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			unisonReload();
+		}
+	},
+});
+</script>
diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue
new file mode 100644
index 0000000000..4a9633a20d
--- /dev/null
+++ b/packages/client/src/pages/settings/mute-block.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+	<MkTab v-model="tab" style="margin-bottom: var(--margin);">
+		<option value="mute">{{ $ts.mutedUsers }}</option>
+		<option value="block">{{ $ts.blockedUsers }}</option>
+	</MkTab>
+	<div v-if="tab === 'mute'">
+		<MkPagination :pagination="mutingPagination" class="muting">
+			<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
+			<template #default="{items}">
+				<FormGroup>
+					<FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
+						<MkAcct :user="mute.mutee"/>
+					</FormLink>
+				</FormGroup>
+			</template>
+		</MkPagination>
+	</div>
+	<div v-if="tab === 'block'">
+		<MkPagination :pagination="blockingPagination" class="blocking">
+			<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
+			<template #default="{items}">
+				<FormGroup>
+					<FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
+						<MkAcct :user="block.blockee"/>
+					</FormLink>
+				</FormGroup>
+			</template>
+		</MkPagination>
+	</div>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkTab from '@/components/tab.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkTab,
+		FormInfo,
+		FormBase,
+		FormGroup,
+		FormLink,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.muteAndBlock,
+				icon: 'fas fa-ban',
+				bg: 'var(--bg)',
+			},
+			tab: 'mute',
+			mutingPagination: {
+				endpoint: 'mute/list',
+				limit: 10,
+			},
+			blockingPagination: {
+				endpoint: 'blocking/list',
+				limit: 10,
+			},
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		userPage
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
new file mode 100644
index 0000000000..7de10a182c
--- /dev/null
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -0,0 +1,77 @@
+<template>
+<FormBase>
+	<FormLink @click="configure">{{ $ts.notificationSetting }}</FormLink>
+	<FormGroup>
+		<FormButton @click="readAllNotifications">{{ $ts.markAsReadAllNotifications }}</FormButton>
+		<FormButton @click="readAllUnreadNotes">{{ $ts.markAsReadAllUnreadNotes }}</FormButton>
+		<FormButton @click="readAllMessagingMessages">{{ $ts.markAsReadAllTalkMessages }}</FormButton>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import { notificationTypes } from 'misskey-js';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormLink,
+		FormButton,
+		FormGroup,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.notifications,
+				icon: 'fas fa-bell',
+				bg: 'var(--bg)',
+			},
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		readAllUnreadNotes() {
+			os.api('i/read-all-unread-notes');
+		},
+
+		readAllMessagingMessages() {
+			os.api('i/read-all-messaging-messages');
+		},
+
+		readAllNotifications() {
+			os.api('notifications/mark-all-as-read');
+		},
+
+		configure() {
+			const includingTypes = notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
+			os.popup(import('@/components/notification-setting-window.vue'), {
+				includingTypes,
+				showGlobalToggle: false,
+			}, {
+				done: async (res) => {
+					const { includingTypes: value } = res;
+					await os.apiWithDialog('i/update', {
+						mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
+					}).then(i => {
+						this.$i.mutingNotificationTypes = i.mutingNotificationTypes;
+					});
+				}
+			}, 'closed');
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue
new file mode 100644
index 0000000000..fbc895a07d
--- /dev/null
+++ b/packages/client/src/pages/settings/other.vue
@@ -0,0 +1,97 @@
+<template>
+<FormBase>
+	<FormLink to="/settings/update">Misskey Update</FormLink>
+
+	<FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote">
+		{{ $ts.showFeaturedNotesInTimeline }}
+	</FormSwitch>
+
+	<FormSwitch v-model="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
+
+	<FormLink to="/settings/account-info">{{ $ts.accountInfo }}</FormLink>
+	<FormLink to="/settings/experimental-features">{{ $ts.experimentalFeatures }}</FormLink>
+
+	<FormGroup>
+		<template #label>{{ $ts.developer }}</template>
+		<FormSwitch v-model="debug" @update:modelValue="changeDebug">
+			DEBUG MODE
+		</FormSwitch>
+		<template v-if="debug">
+			<FormButton @click="taskmanager">Task Manager</FormButton>
+		</template>
+	</FormGroup>
+
+	<FormLink to="/settings/registry"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink>
+
+	<FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
+	<FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
+
+	<FormLink to="/settings/delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import { debug } from '@/config';
+import { defaultStore } from '@/store';
+import { unisonReload } from '@/scripts/unison-reload';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSelect,
+		FormSwitch,
+		FormButton,
+		FormLink,
+		FormGroup,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.other,
+				icon: 'fas fa-ellipsis-h',
+				bg: 'var(--bg)',
+			},
+			debug,
+		}
+	},
+
+	computed: {
+		reportError: defaultStore.makeGetterSetter('reportError'),
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		changeDebug(v) {
+			console.log(v);
+			localStorage.setItem('debug', v.toString());
+			unisonReload();
+		},
+
+		onChangeInjectFeaturedNote(v) {
+			os.api('i/update', {
+				injectFeaturedNote: v
+			});
+		},
+
+		taskmanager() {
+			os.popup(import('@/components/taskmanager.vue'), {
+			}, {}, 'closed');
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue
new file mode 100644
index 0000000000..9958f98f58
--- /dev/null
+++ b/packages/client/src/pages/settings/plugin.install.vue
@@ -0,0 +1,147 @@
+<template>
+<FormBase>
+	<FormInfo warn>{{ $ts._plugin.installWarn }}</FormInfo>
+
+	<FormGroup>
+		<FormTextarea v-model="code" tall>
+			<span>{{ $ts.code }}</span>
+		</FormTextarea>
+	</FormGroup>
+
+	<FormButton @click="install" :disabled="code == null" primary inline><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { AiScript, parse } from '@syuilo/aiscript';
+import { serialize } from '@syuilo/aiscript/built/serializer';
+import { v4 as uuid } from 'uuid';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import { unisonReload } from '@/scripts/unison-reload';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormTextarea,
+		FormSelect,
+		FormRadios,
+		FormBase,
+		FormGroup,
+		FormLink,
+		FormButton,
+		FormInfo,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts._plugin.install,
+				icon: 'fas fa-download',
+				bg: 'var(--bg)',
+			},
+			code: null,
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		installPlugin({ id, meta, ast, token }) {
+			ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
+				...meta,
+				id,
+				active: true,
+				configData: {},
+				token: token,
+				ast: ast
+			}));
+		},
+
+		async install() {
+			let ast;
+			try {
+				ast = parse(this.code);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: 'Syntax error :('
+				});
+				return;
+			}
+			const meta = AiScript.collectMetadata(ast);
+			if (meta == null) {
+				os.dialog({
+					type: 'error',
+					text: 'No metadata found :('
+				});
+				return;
+			}
+			const data = meta.get(null);
+			if (data == null) {
+				os.dialog({
+					type: 'error',
+					text: 'No metadata found :('
+				});
+				return;
+			}
+			const { name, version, author, description, permissions, config } = data;
+			if (name == null || version == null || author == null) {
+				os.dialog({
+					type: 'error',
+					text: 'Required property not found :('
+				});
+				return;
+			}
+
+			const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
+				os.popup(import('@/components/token-generate-window.vue'), {
+					title: this.$ts.tokenRequested,
+					information: this.$ts.pluginTokenRequestedDescription,
+					initialName: name,
+					initialPermissions: permissions
+				}, {
+					done: async result => {
+						const { name, permissions } = result;
+						const { token } = await os.api('miauth/gen-token', {
+							session: null,
+							name: name,
+							permission: permissions,
+						});
+
+						res(token);
+					}
+				}, 'closed');
+			});
+
+			this.installPlugin({
+				id: uuid(),
+				meta: {
+					name, version, author, description, permissions, config
+				},
+				token,
+				ast: serialize(ast)
+			});
+
+			os.success();
+
+			this.$nextTick(() => {
+				unisonReload();
+			});
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/plugin.manage.vue b/packages/client/src/pages/settings/plugin.manage.vue
new file mode 100644
index 0000000000..3a0168d13d
--- /dev/null
+++ b/packages/client/src/pages/settings/plugin.manage.vue
@@ -0,0 +1,115 @@
+<template>
+<FormBase>
+	<FormGroup v-for="plugin in plugins" :key="plugin.id">
+		<template #label><span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span></template>
+
+		<FormSwitch :value="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
+		<div class="_debobigegoItem">
+			<div class="_debobigegoPanel" style="padding: 16px;">
+				<div class="_keyValue">
+					<div>{{ $ts.author }}:</div>
+					<div>{{ plugin.author }}</div>
+				</div>
+				<div class="_keyValue">
+					<div>{{ $ts.description }}:</div>
+					<div>{{ plugin.description }}</div>
+				</div>
+				<div class="_keyValue">
+					<div>{{ $ts.permission }}:</div>
+					<div>{{ plugin.permissions }}</div>
+				</div>
+			</div>
+		</div>
+		<div class="_debobigegoItem">
+			<div class="_debobigegoPanel" style="padding: 16px;">
+				<MkButton @click="config(plugin)" inline v-if="plugin.config"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton>
+				<MkButton @click="uninstall(plugin)" inline danger><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton>
+			</div>
+		</div>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkSelect from '@/components/form/select.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkTextarea,
+		MkSelect,
+		FormSwitch,
+		FormBase,
+		FormGroup,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts._plugin.manage,
+				icon: 'fas fa-plug',
+				bg: 'var(--bg)',
+			},
+			plugins: ColdDeviceStorage.get('plugins'),
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		uninstall(plugin) {
+			ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
+			os.success();
+			this.$nextTick(() => {
+				unisonReload();
+			});
+		},
+
+		// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
+		async config(plugin) {
+			const config = plugin.config;
+			for (const key in plugin.configData) {
+				config[key].default = plugin.configData[key];
+			}
+
+			const { canceled, result } = await os.form(plugin.name, config);
+			if (canceled) return;
+
+			const plugins = ColdDeviceStorage.get('plugins');
+			plugins.find(p => p.id === plugin.id).configData = result;
+			ColdDeviceStorage.set('plugins', plugins);
+
+			this.$nextTick(() => {
+				location.reload();
+			});
+		},
+
+		changeActive(plugin, active) {
+			const plugins = ColdDeviceStorage.get('plugins');
+			plugins.find(p => p.id === plugin.id).active = active;
+			ColdDeviceStorage.set('plugins', plugins);
+
+			this.$nextTick(() => {
+				location.reload();
+			});
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue
new file mode 100644
index 0000000000..50e53f459f
--- /dev/null
+++ b/packages/client/src/pages/settings/plugin.vue
@@ -0,0 +1,44 @@
+<template>
+<FormBase>
+	<FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._plugin.install }}</FormLink>
+	<FormLink to="/settings/plugin/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._plugin.manage }}<template #suffix>{{ plugins }}</template></FormLink>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormLink,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.plugins,
+				icon: 'fas fa-plug',
+				bg: 'var(--bg)',
+			},
+			plugins: ColdDeviceStorage.get('plugins').length,
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue
new file mode 100644
index 0000000000..94afba9aa4
--- /dev/null
+++ b/packages/client/src/pages/settings/privacy.vue
@@ -0,0 +1,120 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<FormSwitch v-model="isLocked" @update:modelValue="save()">{{ $ts.makeFollowManuallyApprove }}</FormSwitch>
+		<FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
+		<template #caption>{{ $ts.lockedAccountInfo }}</template>
+	</FormGroup>
+	<FormSwitch v-model="publicReactions" @update:modelValue="save()">
+		{{ $ts.makeReactionsPublic }}
+		<template #desc>{{ $ts.makeReactionsPublicDescription }}</template>
+	</FormSwitch>
+	<FormGroup>
+		<template #label>{{ $ts.ffVisibility }}</template>
+		<FormSelect v-model="ffVisibility">
+			<option value="public">{{ $ts._ffVisibility.public }}</option>
+			<option value="followers">{{ $ts._ffVisibility.followers }}</option>
+			<option value="private">{{ $ts._ffVisibility.private }}</option>
+		</FormSelect>
+		<template #caption>{{ $ts.ffVisibilityDescription }}</template>
+	</FormGroup>
+	<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
+		{{ $ts.hideOnlineStatus }}
+		<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
+	</FormSwitch>
+	<FormSwitch v-model="noCrawle" @update:modelValue="save()">
+		{{ $ts.noCrawle }}
+		<template #desc>{{ $ts.noCrawleDescription }}</template>
+	</FormSwitch>
+	<FormSwitch v-model="isExplorable" @update:modelValue="save()">
+		{{ $ts.makeExplorable }}
+		<template #desc>{{ $ts.makeExplorableDescription }}</template>
+	</FormSwitch>
+	<FormSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ $ts.rememberNoteVisibility }}</FormSwitch>
+	<FormGroup v-if="!rememberNoteVisibility">
+		<template #label>{{ $ts.defaultNoteVisibility }}</template>
+		<FormSelect v-model="defaultNoteVisibility">
+			<option value="public">{{ $ts._visibility.public }}</option>
+			<option value="home">{{ $ts._visibility.home }}</option>
+			<option value="followers">{{ $ts._visibility.followers }}</option>
+			<option value="specified">{{ $ts._visibility.specified }}</option>
+		</FormSelect>
+		<FormSwitch v-model="defaultNoteLocalOnly">{{ $ts._visibility.localOnly }}</FormSwitch>
+	</FormGroup>
+	<FormSwitch v-model="keepCw" @update:modelValue="save()">{{ $ts.keepCw }}</FormSwitch>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormSelect from '@/components/debobigego/select.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSelect,
+		FormGroup,
+		FormSwitch,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.privacy,
+				icon: 'fas fa-lock-open',
+				bg: 'var(--bg)',
+			},
+			isLocked: false,
+			autoAcceptFollowed: false,
+			noCrawle: false,
+			isExplorable: false,
+			hideOnlineStatus: false,
+			publicReactions: false,
+			ffVisibility: 'public',
+		}
+	},
+
+	computed: {
+		defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'),
+		defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'),
+		rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'),
+		keepCw: defaultStore.makeGetterSetter('keepCw'),
+	},
+
+	created() {
+		this.isLocked = this.$i.isLocked;
+		this.autoAcceptFollowed = this.$i.autoAcceptFollowed;
+		this.noCrawle = this.$i.noCrawle;
+		this.isExplorable = this.$i.isExplorable;
+		this.hideOnlineStatus = this.$i.hideOnlineStatus;
+		this.publicReactions = this.$i.publicReactions;
+		this.ffVisibility = this.$i.ffVisibility;
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		save() {
+			os.api('i/update', {
+				isLocked: !!this.isLocked,
+				autoAcceptFollowed: !!this.autoAcceptFollowed,
+				noCrawle: !!this.noCrawle,
+				isExplorable: !!this.isExplorable,
+				hideOnlineStatus: !!this.hideOnlineStatus,
+				publicReactions: !!this.publicReactions,
+				ffVisibility: this.ffVisibility,
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
new file mode 100644
index 0000000000..a7ddc6d178
--- /dev/null
+++ b/packages/client/src/pages/settings/profile.vue
@@ -0,0 +1,281 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<div class="_debobigegoItem _debobigegoPanel llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
+			<MkAvatar class="avatar" :user="$i"/>
+		</div>
+		<FormButton @click="changeAvatar" primary>{{ $ts._profile.changeAvatar }}</FormButton>
+		<FormButton @click="changeBanner" primary>{{ $ts._profile.changeBanner }}</FormButton>
+	</FormGroup>
+
+	<FormInput v-model="name" :max="30" manual-save>
+		<span>{{ $ts._profile.name }}</span>
+	</FormInput>
+
+	<FormTextarea v-model="description" :max="500" tall manual-save>
+		<span>{{ $ts._profile.description }}</span>
+		<template #desc>{{ $ts._profile.youCanIncludeHashtags }}</template>
+	</FormTextarea>
+
+	<FormInput v-model="location" manual-save>
+		<span>{{ $ts.location }}</span>
+		<template #prefix><i class="fas fa-map-marker-alt"></i></template>
+	</FormInput>
+
+	<FormInput v-model="birthday" type="date" manual-save>
+		<span>{{ $ts.birthday }}</span>
+		<template #prefix><i class="fas fa-birthday-cake"></i></template>
+	</FormInput>
+
+	<FormSelect v-model="lang">
+		<template #label>{{ $ts.language }}</template>
+		<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+	</FormSelect>
+
+	<FormGroup>
+		<FormButton @click="editMetadata" primary>{{ $ts._profile.metadataEdit }}</FormButton>
+		<template #caption>{{ $ts._profile.metadataDescription }}</template>
+	</FormGroup>
+
+	<FormSwitch v-model="isCat">{{ $ts.flagAsCat }}<template #desc>{{ $ts.flagAsCatDescription }}</template></FormSwitch>
+
+	<FormSwitch v-model="isBot">{{ $ts.flagAsBot }}<template #desc>{{ $ts.flagAsBotDescription }}</template></FormSwitch>
+
+	<FormSwitch v-model="alwaysMarkNsfw">{{ $ts.alwaysMarkSensitive }}</FormSwitch>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormSelect from '@/components/debobigego/select.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import { host, langs } from '@/config';
+import { selectFile } from '@/scripts/select-file';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormButton,
+		FormInput,
+		FormTextarea,
+		FormSwitch,
+		FormSelect,
+		FormBase,
+		FormGroup,
+	},
+	
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.profile,
+				icon: 'fas fa-user',
+				bg: 'var(--bg)',
+			},
+			host,
+			langs,
+			name: null,
+			description: null,
+			birthday: null,
+			lang: null,
+			location: null,
+			fieldName0: null,
+			fieldValue0: null,
+			fieldName1: null,
+			fieldValue1: null,
+			fieldName2: null,
+			fieldValue2: null,
+			fieldName3: null,
+			fieldValue3: null,
+			avatarId: null,
+			bannerId: null,
+			isBot: false,
+			isCat: false,
+			alwaysMarkNsfw: false,
+			saving: false,
+		}
+	},
+
+	created() {
+		this.name = this.$i.name;
+		this.description = this.$i.description;
+		this.location = this.$i.location;
+		this.birthday = this.$i.birthday;
+		this.lang = this.$i.lang;
+		this.avatarId = this.$i.avatarId;
+		this.bannerId = this.$i.bannerId;
+		this.isBot = this.$i.isBot;
+		this.isCat = this.$i.isCat;
+		this.alwaysMarkNsfw = this.$i.alwaysMarkNsfw;
+
+		this.fieldName0 = this.$i.fields[0] ? this.$i.fields[0].name : null;
+		this.fieldValue0 = this.$i.fields[0] ? this.$i.fields[0].value : null;
+		this.fieldName1 = this.$i.fields[1] ? this.$i.fields[1].name : null;
+		this.fieldValue1 = this.$i.fields[1] ? this.$i.fields[1].value : null;
+		this.fieldName2 = this.$i.fields[2] ? this.$i.fields[2].name : null;
+		this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null;
+		this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null;
+		this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null;
+
+		this.$watch('name', this.save);
+		this.$watch('description', this.save);
+		this.$watch('location', this.save);
+		this.$watch('birthday', this.save);
+		this.$watch('lang', this.save);
+		this.$watch('isBot', this.save);
+		this.$watch('isCat', this.save);
+		this.$watch('alwaysMarkNsfw', this.save);
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		changeAvatar(e) {
+			selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => {
+				os.api('i/update', {
+					avatarId: file.id,
+				});
+			});
+		},
+
+		changeBanner(e) {
+			selectFile(e.currentTarget || e.target, this.$ts.banner).then(file => {
+				os.api('i/update', {
+					bannerId: file.id,
+				});
+			});
+		},
+
+		async editMetadata() {
+			const { canceled, result } = await os.form(this.$ts._profile.metadata, {
+				fieldName0: {
+					type: 'string',
+					label: this.$ts._profile.metadataLabel + ' 1',
+					default: this.fieldName0,
+				},
+				fieldValue0: {
+					type: 'string',
+					label: this.$ts._profile.metadataContent + ' 1',
+					default: this.fieldValue0,
+				},
+				fieldName1: {
+					type: 'string',
+					label: this.$ts._profile.metadataLabel + ' 2',
+					default: this.fieldName1,
+				},
+				fieldValue1: {
+					type: 'string',
+					label: this.$ts._profile.metadataContent + ' 2',
+					default: this.fieldValue1,
+				},
+				fieldName2: {
+					type: 'string',
+					label: this.$ts._profile.metadataLabel + ' 3',
+					default: this.fieldName2,
+				},
+				fieldValue2: {
+					type: 'string',
+					label: this.$ts._profile.metadataContent + ' 3',
+					default: this.fieldValue2,
+				},
+				fieldName3: {
+					type: 'string',
+					label: this.$ts._profile.metadataLabel + ' 4',
+					default: this.fieldName3,
+				},
+				fieldValue3: {
+					type: 'string',
+					label: this.$ts._profile.metadataContent + ' 4',
+					default: this.fieldValue3,
+				},
+			});
+			if (canceled) return;
+
+			this.fieldName0 = result.fieldName0;
+			this.fieldValue0 = result.fieldValue0;
+			this.fieldName1 = result.fieldName1;
+			this.fieldValue1 = result.fieldValue1;
+			this.fieldName2 = result.fieldName2;
+			this.fieldValue2 = result.fieldValue2;
+			this.fieldName3 = result.fieldName3;
+			this.fieldValue3 = result.fieldValue3;
+
+			const fields = [
+				{ name: this.fieldName0, value: this.fieldValue0 },
+				{ name: this.fieldName1, value: this.fieldValue1 },
+				{ name: this.fieldName2, value: this.fieldValue2 },
+				{ name: this.fieldName3, value: this.fieldValue3 },
+			];
+
+			os.api('i/update', {
+				fields,
+			}).then(i => {
+				os.success();
+			}).catch(err => {
+				os.dialog({
+					type: 'error',
+					text: err.id
+				});
+			});
+		},
+
+		save() {
+			this.saving = true;
+
+			os.apiWithDialog('i/update', {
+				name: this.name || null,
+				description: this.description || null,
+				location: this.location || null,
+				birthday: this.birthday || null,
+				lang: this.lang || null,
+				isBot: !!this.isBot,
+				isCat: !!this.isCat,
+				alwaysMarkNsfw: !!this.alwaysMarkNsfw,
+			}).then(i => {
+				this.saving = false;
+				this.$i.avatarId = i.avatarId;
+				this.$i.avatarUrl = i.avatarUrl;
+				this.$i.bannerId = i.bannerId;
+				this.$i.bannerUrl = i.bannerUrl;
+			}).catch(err => {
+				this.saving = false;
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.llvierxe {
+	position: relative;
+	height: 150px;
+	background-size: cover;
+	background-position: center;
+
+	> * {
+		pointer-events: none;
+	}
+
+	> .avatar {
+		position: absolute;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		display: block;
+		width: 72px;
+		height: 72px;
+		margin: auto;
+		box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
+	}
+}
+</style>
diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue
new file mode 100644
index 0000000000..905a3e4957
--- /dev/null
+++ b/packages/client/src/pages/settings/reaction.vue
@@ -0,0 +1,152 @@
+<template>
+<FormBase>
+	<div class="_debobigegoItem">
+		<div class="_debobigegoLabel">{{ $ts.reactionSettingDescription }}</div>
+		<div class="_debobigegoPanel">
+			<XDraggable class="zoaiodol" v-model="reactions" :item-key="item => item" animation="150" delay="100" delay-on-touch-only="true">
+				<template #item="{element}">
+					<button class="_button item" @click="remove(element, $event)">
+						<MkEmoji :emoji="element" :normal="true"/>
+					</button>
+				</template>
+				<template #footer>
+					<button class="_button add" @click="chooseEmoji"><i class="fas fa-plus"></i></button>
+				</template>
+			</XDraggable>
+		</div>
+		<div class="_debobigegoCaption">{{ $ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ $ts.preview }}</button></div>
+	</div>
+
+	<FormRadios v-model="reactionPickerWidth">
+		<template #desc>{{ $ts.width }}</template>
+		<option :value="1">{{ $ts.small }}</option>
+		<option :value="2">{{ $ts.medium }}</option>
+		<option :value="3">{{ $ts.large }}</option>
+	</FormRadios>
+	<FormRadios v-model="reactionPickerHeight">
+		<template #desc>{{ $ts.height }}</template>
+		<option :value="1">{{ $ts.small }}</option>
+		<option :value="2">{{ $ts.medium }}</option>
+		<option :value="3">{{ $ts.large }}</option>
+	</FormRadios>
+	<FormButton @click="preview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
+	<FormButton danger @click="setDefault"><i class="fas fa-undo"></i> {{ $ts.default }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XDraggable from 'vuedraggable';
+import FormInput from '@/components/debobigego/input.vue';
+import FormRadios from '@/components/debobigego/radios.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormInput,
+		FormButton,
+		FormBase,
+		FormRadios,
+		XDraggable,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.reaction,
+				icon: 'fas fa-laugh',
+				action: {
+					icon: 'fas fa-eye',
+					handler: this.preview
+				},
+				bg: 'var(--bg)',
+			},
+			reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)),
+		}
+	},
+
+	computed: {
+		reactionPickerWidth: defaultStore.makeGetterSetter('reactionPickerWidth'),
+		reactionPickerHeight: defaultStore.makeGetterSetter('reactionPickerHeight'),
+	},
+
+	watch: {
+		reactions: {
+			handler() {
+				this.save();
+			},
+			deep: true
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		save() {
+			this.$store.set('reactions', this.reactions);
+		},
+
+		remove(reaction, ev) {
+			os.popupMenu([{
+				text: this.$ts.remove,
+				action: () => {
+					this.reactions = this.reactions.filter(x => x !== reaction)
+				}
+			}], ev.currentTarget || ev.target);
+		},
+
+		preview(ev) {
+			os.popup(import('@/components/emoji-picker-dialog.vue'), {
+				asReactionPicker: true,
+				src: ev.currentTarget || ev.target,
+			}, {}, 'closed');
+		},
+
+		async setDefault() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$ts.resetAreYouSure,
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			this.reactions = JSON.parse(JSON.stringify(this.$store.def.reactions.default));
+		},
+
+		chooseEmoji(ev) {
+			os.pickEmoji(ev.currentTarget || ev.target, {
+				showPinned: false
+			}).then(emoji => {
+				if (!this.reactions.includes(emoji)) {
+					this.reactions.push(emoji);
+				}
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.zoaiodol {
+	padding: 16px;
+
+	> .item {
+		display: inline-block;
+		padding: 8px;
+		cursor: move;
+	}
+
+	> .add {
+		display: inline-block;
+		padding: 8px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/settings/registry.keys.vue b/packages/client/src/pages/settings/registry.keys.vue
new file mode 100644
index 0000000000..ca4d01cc94
--- /dev/null
+++ b/packages/client/src/pages/settings/registry.keys.vue
@@ -0,0 +1,114 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<FormKeyValueView>
+			<template #key>{{ $ts._registry.domain }}</template>
+			<template #value>{{ $ts.system }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts._registry.scope }}</template>
+			<template #value>{{ scope.join('/') }}</template>
+		</FormKeyValueView>
+	</FormGroup>
+	
+	<FormGroup v-if="keys">
+		<template #label>{{ $ts._registry.keys }}</template>
+		<FormLink v-for="key in keys" :to="`/settings/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
+	</FormGroup>
+
+	<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import * as JSON5 from 'json5';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSelect,
+		FormSwitch,
+		FormButton,
+		FormLink,
+		FormGroup,
+		FormKeyValueView,
+	},
+
+	props: {
+		scope: {
+			required: true
+		}
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.registry,
+				icon: 'fas fa-cogs',
+				bg: 'var(--bg)',
+			},
+			keys: null,
+		}
+	},
+
+	watch: {
+		scope() {
+			this.fetch();
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			os.api('i/registry/keys-with-type', {
+				scope: this.scope
+			}).then(keys => {
+				this.keys = Object.entries(keys).sort((a, b) => a[0].localeCompare(b[0]));
+			});
+		},
+
+		async createKey() {
+			const { canceled, result } = await os.form(this.$ts._registry.createKey, {
+				key: {
+					type: 'string',
+					label: this.$ts._registry.key,
+				},
+				value: {
+					type: 'string',
+					multiline: true,
+					label: this.$ts.value,
+				},
+				scope: {
+					type: 'string',
+					label: this.$ts._registry.scope,
+					default: this.scope.join('/')
+				}
+			});
+			if (canceled) return;
+			os.apiWithDialog('i/registry/set', {
+				scope: result.scope.split('/'),
+				key: result.key,
+				value: JSON5.parse(result.value),
+			}).then(() => {
+				this.fetch();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/registry.value.vue b/packages/client/src/pages/settings/registry.value.vue
new file mode 100644
index 0000000000..36f989dbc5
--- /dev/null
+++ b/packages/client/src/pages/settings/registry.value.vue
@@ -0,0 +1,149 @@
+<template>
+<FormBase>
+	<FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo>
+
+	<template v-if="value">
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>{{ $ts._registry.domain }}</template>
+				<template #value>{{ $ts.system }}</template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>{{ $ts._registry.scope }}</template>
+				<template #value>{{ scope.join('/') }}</template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>{{ $ts._registry.key }}</template>
+				<template #value>{{ xKey }}</template>
+			</FormKeyValueView>
+		</FormGroup>
+
+		<FormGroup>
+			<FormTextarea tall v-model="valueForEditor" class="_monospace" style="tab-size: 2;">
+				<span>{{ $ts.value }} (JSON)</span>
+			</FormTextarea>
+			<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+		</FormGroup>
+
+		<FormKeyValueView>
+			<template #key>{{ $ts.updatedAt }}</template>
+			<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
+		</FormKeyValueView>
+
+		<FormButton danger @click="del"><i class="fas fa-trash"></i> {{ $ts.delete }}</FormButton>
+	</template>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import * as JSON5 from 'json5';
+import FormInfo from '@/components/debobigego/info.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormInfo,
+		FormBase,
+		FormSelect,
+		FormSwitch,
+		FormButton,
+		FormTextarea,
+		FormGroup,
+		FormKeyValueView,
+	},
+
+	props: {
+		scope: {
+			required: true
+		},
+		xKey: {
+			required: true
+		},
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.registry,
+				icon: 'fas fa-cogs',
+				bg: 'var(--bg)',
+			},
+			value: null,
+			valueForEditor: null,
+		}
+	},
+
+	watch: {
+		key() {
+			this.fetch();
+		},
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			os.api('i/registry/get-detail', {
+				scope: this.scope,
+				key: this.xKey
+			}).then(value => {
+				this.value = value;
+				this.valueForEditor = JSON5.stringify(this.value.value, null, '\t');
+			});
+		},
+
+		save() {
+			try {
+				JSON5.parse(this.valueForEditor);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: this.$ts.invalidValue
+				});
+				return;
+			}
+
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.saveConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				os.apiWithDialog('i/registry/set', {
+					scope: this.scope,
+					key: this.xKey,
+					value: JSON5.parse(this.valueForEditor)
+				});
+			});
+		},
+
+		del() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.deleteConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+				os.apiWithDialog('i/registry/remove', {
+					scope: this.scope,
+					key: this.xKey
+				});
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/registry.vue b/packages/client/src/pages/settings/registry.vue
new file mode 100644
index 0000000000..0bfed0ddb7
--- /dev/null
+++ b/packages/client/src/pages/settings/registry.vue
@@ -0,0 +1,90 @@
+<template>
+<FormBase>
+	<FormGroup v-if="scopes">
+		<template #label>{{ $ts.system }}</template>
+		<FormLink v-for="scope in scopes" :to="`/settings/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
+	</FormGroup>
+	<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import * as JSON5 from 'json5';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSelect,
+		FormSwitch,
+		FormButton,
+		FormLink,
+		FormGroup,
+		FormKeyValueView,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.registry,
+				icon: 'fas fa-cogs',
+				bg: 'var(--bg)',
+			},
+			scopes: null,
+		}
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		fetch() {
+			os.api('i/registry/scopes').then(scopes => {
+				this.scopes = scopes.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
+			});
+		},
+
+		async createKey() {
+			const { canceled, result } = await os.form(this.$ts._registry.createKey, {
+				key: {
+					type: 'string',
+					label: this.$ts._registry.key,
+				},
+				value: {
+					type: 'string',
+					multiline: true,
+					label: this.$ts.value,
+				},
+				scope: {
+					type: 'string',
+					label: this.$ts._registry.scope,
+				}
+			});
+			if (canceled) return;
+			os.apiWithDialog('i/registry/set', {
+				scope: result.scope.split('/'),
+				key: result.key,
+				value: JSON5.parse(result.value),
+			}).then(() => {
+				this.fetch();
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue
new file mode 100644
index 0000000000..4d81bf1b9e
--- /dev/null
+++ b/packages/client/src/pages/settings/security.vue
@@ -0,0 +1,158 @@
+<template>
+<FormBase>
+	<X2fa/>
+	<FormLink to="/settings/2fa"><template #icon><i class="fas fa-mobile-alt"></i></template>{{ $ts.twoStepAuthentication }}</FormLink>
+	<FormButton primary @click="change()">{{ $ts.changePassword }}</FormButton>
+	<FormPagination :pagination="pagination">
+		<template #label>{{ $ts.signinHistory }}</template>
+		<template #default="{items}">
+			<div class="_debobigegoPanel timnmucd" v-for="item in items" :key="item.id">
+				<header>
+					<i v-if="item.success" class="fas fa-check icon succ"></i>
+					<i v-else class="fas fa-times-circle icon fail"></i>
+					<code class="ip _monospace">{{ item.ip }}</code>
+					<MkTime :time="item.createdAt" class="time"/>
+				</header>
+			</div>
+		</template>
+	</FormPagination>
+	<FormGroup>
+		<FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ $ts.regenerateLoginToken }}</FormButton>
+		<template #caption>{{ $ts.regenerateLoginTokenDescription }}</template>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormPagination from '@/components/debobigego/pagination.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormLink,
+		FormButton,
+		FormPagination,
+		FormGroup,
+	},
+	
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.security,
+				icon: 'fas fa-lock',
+				bg: 'var(--bg)',
+			},
+			pagination: {
+				endpoint: 'i/signin-history',
+				limit: 5,
+			},
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async change() {
+			const { canceled: canceled1, result: currentPassword } = await os.dialog({
+				title: this.$ts.currentPassword,
+				input: {
+					type: 'password'
+				}
+			});
+			if (canceled1) return;
+
+			const { canceled: canceled2, result: newPassword } = await os.dialog({
+				title: this.$ts.newPassword,
+				input: {
+					type: 'password'
+				}
+			});
+			if (canceled2) return;
+
+			const { canceled: canceled3, result: newPassword2 } = await os.dialog({
+				title: this.$ts.newPasswordRetype,
+				input: {
+					type: 'password'
+				}
+			});
+			if (canceled3) return;
+
+			if (newPassword !== newPassword2) {
+				os.dialog({
+					type: 'error',
+					text: this.$ts.retypedNotMatch
+				});
+				return;
+			}
+			
+			os.apiWithDialog('i/change-password', {
+				currentPassword,
+				newPassword
+			});
+		},
+
+		regenerateToken() {
+			os.dialog({
+				title: this.$ts.password,
+				input: {
+					type: 'password'
+				}
+			}).then(({ canceled, result: password }) => {
+				if (canceled) return;
+				os.api('i/regenerate_token', {
+					password: password
+				});
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.timnmucd {
+	padding: 16px;
+
+	> header {
+		display: flex;
+		align-items: center;
+
+		> .icon {
+			width: 1em;
+			margin-right: 0.75em;
+
+			&.succ {
+				color: var(--success);
+			}
+
+			&.fail {
+				color: var(--error);
+			}
+		}
+
+		> .ip {
+			flex: 1;
+			min-width: 0;
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			margin-right: 12px;
+		}
+
+		> .time {
+			margin-left: auto;
+			opacity: 0.7;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue
new file mode 100644
index 0000000000..ea3daced9d
--- /dev/null
+++ b/packages/client/src/pages/settings/sounds.vue
@@ -0,0 +1,155 @@
+<template>
+<FormBase>
+	<FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05">
+		<template #label><i class="fas fa-volume-icon"></i> {{ $ts.masterVolume }}</template>
+	</FormRange>
+
+	<FormGroup>
+		<template #label>{{ $ts.sounds }}</template>
+		<FormButton v-for="type in Object.keys(sounds)" :key="type" :center="false" @click="edit(type)">
+			{{ $t('_sfx.' + type) }}
+			<template #suffix>{{ sounds[type].type || $ts.none }}</template>
+			<template #suffixIcon><i class="fas fa-chevron-down"></i></template>
+		</FormButton>
+	</FormGroup>
+
+	<FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormRange from '@/components/debobigego/range.vue';
+import FormSelect from '@/components/debobigego/select.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import { playFile } from '@/scripts/sound';
+import * as symbols from '@/symbols';
+
+const soundsTypes = [
+	null,
+	'syuilo/up',
+	'syuilo/down',
+	'syuilo/pope1',
+	'syuilo/pope2',
+	'syuilo/waon',
+	'syuilo/popo',
+	'syuilo/triple',
+	'syuilo/poi1',
+	'syuilo/poi2',
+	'syuilo/pirori',
+	'syuilo/pirori-wet',
+	'syuilo/pirori-square-wet',
+	'syuilo/square-pico',
+	'syuilo/reverved',
+	'syuilo/ryukyu',
+	'syuilo/kick',
+	'syuilo/snare',
+	'syuilo/queue-jammed',
+	'aisha/1',
+	'aisha/2',
+	'aisha/3',
+	'noizenecio/kick_gaba',
+	'noizenecio/kick_gaba2',
+];
+
+export default defineComponent({
+	components: {
+		FormSelect,
+		FormButton,
+		FormBase,
+		FormRange,
+		FormGroup,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.sounds,
+				icon: 'fas fa-music',
+				bg: 'var(--bg)',
+			},
+			sounds: {},
+		}
+	},
+
+	computed: {
+		masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す
+			get() { return ColdDeviceStorage.get('sound_masterVolume'); },
+			set(value) { ColdDeviceStorage.set('sound_masterVolume', value); }
+		},
+		volumeIcon() {
+			return this.masterVolume === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up';
+		}
+	},
+
+	created() {
+		this.sounds.note = ColdDeviceStorage.get('sound_note');
+		this.sounds.noteMy = ColdDeviceStorage.get('sound_noteMy');
+		this.sounds.notification = ColdDeviceStorage.get('sound_notification');
+		this.sounds.chat = ColdDeviceStorage.get('sound_chat');
+		this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg');
+		this.sounds.antenna = ColdDeviceStorage.get('sound_antenna');
+		this.sounds.channel = ColdDeviceStorage.get('sound_channel');
+		this.sounds.reversiPutBlack = ColdDeviceStorage.get('sound_reversiPutBlack');
+		this.sounds.reversiPutWhite = ColdDeviceStorage.get('sound_reversiPutWhite');
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async edit(type) {
+			const { canceled, result } = await os.form(this.$t('_sfx.' + type), {
+				type: {
+					type: 'enum',
+					enum: soundsTypes.map(x => ({
+						value: x,
+						label: x == null ? this.$ts.none : x,
+					})),
+					label: this.$ts.sound,
+					default: this.sounds[type].type,
+				},
+				volume: {
+					type: 'range',
+					mim: 0,
+					max: 1,
+					step: 0.05,
+					label: this.$ts.volume,
+					default: this.sounds[type].volume
+				},
+				listen: {
+					type: 'button',
+					content: this.$ts.listen,
+					action: (_, values) => {
+						playFile(values.type, values.volume);
+					}
+				}
+			});
+			if (canceled) return;
+
+			const v = {
+				type: result.type,
+				volume: result.volume,
+			};
+
+			ColdDeviceStorage.set('sound_' + type, v);
+			this.sounds[type] = v;
+		},
+
+		reset() {
+			for (const sound of Object.keys(this.sounds)) {
+				const v = ColdDeviceStorage.default['sound_' + sound];
+				ColdDeviceStorage.set('sound_' + sound, v);
+				this.sounds[sound] = v;
+			}
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue
new file mode 100644
index 0000000000..59ad3ad9b7
--- /dev/null
+++ b/packages/client/src/pages/settings/theme.install.vue
@@ -0,0 +1,105 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<FormTextarea v-model="installThemeCode">
+			<span>{{ $ts._theme.code }}</span>
+		</FormTextarea>
+		<FormButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
+	</FormGroup>
+
+	<FormButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as JSON5 from 'json5';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import { applyTheme, validateTheme } from '@/scripts/theme';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import { addTheme, getThemes } from '@/theme-store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormTextarea,
+		FormSelect,
+		FormRadios,
+		FormBase,
+		FormGroup,
+		FormLink,
+		FormButton,
+	},
+
+	emits: ['info'],
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts._theme.install,
+				icon: 'fas fa-download',
+				bg: 'var(--bg)',
+			},
+			installThemeCode: null,
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		parseThemeCode(code) {
+			let theme;
+
+			try {
+				theme = JSON5.parse(code);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: this.$ts._theme.invalid
+				});
+				return false;
+			}
+			if (!validateTheme(theme)) {
+				os.dialog({
+					type: 'error',
+					text: this.$ts._theme.invalid
+				});
+				return false;
+			}
+			if (getThemes().some(t => t.id === theme.id)) {
+				os.dialog({
+					type: 'info',
+					text: this.$ts._theme.alreadyInstalled
+				});
+				return false;
+			}
+
+			return theme;
+		},
+
+		preview(code) {
+			const theme = this.parseThemeCode(code);
+			if (theme) applyTheme(theme, false);
+		},
+
+		async install(code) {
+			const theme = this.parseThemeCode(code);
+			if (!theme) return;
+			await addTheme(theme);
+			os.dialog({
+				type: 'success',
+				text: this.$t('_theme.installed', { name: theme.name })
+			});
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue
new file mode 100644
index 0000000000..8a24481ae2
--- /dev/null
+++ b/packages/client/src/pages/settings/theme.manage.vue
@@ -0,0 +1,105 @@
+<template>
+<FormBase>
+	<FormSelect v-model="selectedThemeId">
+		<template #label>{{ $ts.theme }}</template>
+		<optgroup :label="$ts._theme.installedThemes">
+			<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+		</optgroup>
+		<optgroup :label="$ts._theme.builtinThemes">
+			<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+		</optgroup>
+	</FormSelect>
+	<template v-if="selectedTheme">
+		<FormInput readonly :modelValue="selectedTheme.author">
+			<span>{{ $ts.author }}</span>
+		</FormInput>
+		<FormTextarea readonly :modelValue="selectedTheme.desc" v-if="selectedTheme.desc">
+			<span>{{ $ts._theme.description }}</span>
+		</FormTextarea>
+		<FormTextarea readonly tall :modelValue="selectedThemeCode">
+			<span>{{ $ts._theme.code }}</span>
+			<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template>
+		</FormTextarea>
+		<FormButton @click="uninstall()" danger v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
+	</template>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as JSON5 from 'json5';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormSelect from '@/components/debobigego/select.vue';
+import FormRadios from '@/components/debobigego/radios.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormInput from '@/components/debobigego/input.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import { Theme, builtinThemes } from '@/scripts/theme';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import { getThemes, removeTheme } from '@/theme-store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormTextarea,
+		FormSelect,
+		FormRadios,
+		FormBase,
+		FormGroup,
+		FormInput,
+		FormButton,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts._theme.manage,
+				icon: 'fas fa-folder-open',
+				bg: 'var(--bg)',
+			},
+			installedThemes: getThemes(),
+			builtinThemes,
+			selectedThemeId: null,
+		}
+	},
+
+	computed: {
+		themes(): Theme[] {
+			return this.builtinThemes.concat(this.installedThemes);
+		},
+	
+		selectedTheme() {
+			if (this.selectedThemeId == null) return null;
+			return this.themes.find(x => x.id === this.selectedThemeId);
+		},
+
+		selectedThemeCode() {
+			if (this.selectedTheme == null) return null;
+			return JSON5.stringify(this.selectedTheme, null, '\t');
+		},
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		copyThemeCode() {
+			copyToClipboard(this.selectedThemeCode);
+			os.success();
+		},
+
+		uninstall() {
+			removeTheme(this.selectedTheme);
+			this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId);
+			this.selectedThemeId = null;
+			os.success();
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
new file mode 100644
index 0000000000..a9cca40f3c
--- /dev/null
+++ b/packages/client/src/pages/settings/theme.vue
@@ -0,0 +1,424 @@
+<template>
+<FormBase>
+	<FormGroup>
+		<div class="rfqxtzch _debobigegoItem _debobigegoPanel">
+			<div class="darkMode">
+				<div class="toggleWrapper">
+					<input type="checkbox" class="dn" id="dn" v-model="darkMode"/>
+					<label for="dn" class="toggle">
+						<span class="before">{{ $ts.light }}</span>
+						<span class="after">{{ $ts.dark }}</span>
+						<span class="toggle__handler">
+							<span class="crater crater--1"></span>
+							<span class="crater crater--2"></span>
+							<span class="crater crater--3"></span>
+						</span>
+						<span class="star star--1"></span>
+						<span class="star star--2"></span>
+						<span class="star star--3"></span>
+						<span class="star star--4"></span>
+						<span class="star star--5"></span>
+						<span class="star star--6"></span>
+					</label>
+				</div>
+			</div>
+		</div>
+		<FormSwitch v-model="syncDeviceDarkMode">{{ $ts.syncDeviceDarkMode }}</FormSwitch>
+	</FormGroup>
+
+	<template v-if="darkMode">
+		<FormSelect v-model="darkThemeId">
+			<template #label>{{ $ts.themeForDarkMode }}</template>
+			<optgroup :label="$ts.darkThemes">
+				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+			<optgroup :label="$ts.lightThemes">
+				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+		</FormSelect>
+		<FormSelect v-model="lightThemeId">
+			<template #label>{{ $ts.themeForLightMode }}</template>
+			<optgroup :label="$ts.lightThemes">
+				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+			<optgroup :label="$ts.darkThemes">
+				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+		</FormSelect>
+	</template>
+	<template v-else>
+		<FormSelect v-model="lightThemeId">
+			<template #label>{{ $ts.themeForLightMode }}</template>
+			<optgroup :label="$ts.lightThemes">
+				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+			<optgroup :label="$ts.darkThemes">
+				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+		</FormSelect>
+		<FormSelect v-model="darkThemeId">
+			<template #label>{{ $ts.themeForDarkMode }}</template>
+			<optgroup :label="$ts.darkThemes">
+				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+			<optgroup :label="$ts.lightThemes">
+				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</optgroup>
+		</FormSelect>
+	</template>
+
+	<FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $ts.setWallpaper }}</FormButton>
+	<FormButton primary v-else @click="wallpaper = null">{{ $ts.removeWallpaper }}</FormButton>
+
+	<FormGroup>
+		<FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="fas fa-globe"></i></template>{{ $ts._theme.explore }}</FormLink>
+		<FormLink to="/settings/theme/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._theme.install }}</FormLink>
+	</FormGroup>
+
+	<FormGroup>
+		<FormLink to="/theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }}</FormLink>
+		<!--<FormLink to="/advanced-theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>-->
+	</FormGroup>
+
+	<FormLink to="/settings/theme/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormSelect from '@/components/debobigego/select.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import { builtinThemes } from '@/scripts/theme';
+import { selectFile } from '@/scripts/select-file';
+import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
+import { ColdDeviceStorage } from '@/store';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
+import { fetchThemes, getThemes } from '@/theme-store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormSwitch,
+		FormSelect,
+		FormBase,
+		FormGroup,
+		FormLink,
+		FormButton,
+	},
+
+	emits: ['info'],
+
+	setup(props, { emit }) {
+		const INFO = {
+			title: i18n.locale.theme,
+			icon: 'fas fa-palette',
+				bg: 'var(--bg)',
+		};
+
+		const installedThemes = ref(getThemes());
+		const themes = computed(() => builtinThemes.concat(installedThemes.value));
+		const darkThemes = computed(() => themes.value.filter(t => t.base == 'dark' || t.kind == 'dark'));
+		const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light'));
+		const darkTheme = ColdDeviceStorage.ref('darkTheme');
+		const darkThemeId = computed({
+			get() {
+				return darkTheme.value.id;
+			},
+			set(id) {
+				ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id))
+			}
+		});
+		const lightTheme = ColdDeviceStorage.ref('lightTheme');
+		const lightThemeId = computed({
+			get() {
+				return lightTheme.value.id;
+			},
+			set(id) {
+				ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id))
+			}
+		});
+		const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
+		const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
+		const wallpaper = ref(localStorage.getItem('wallpaper'));
+		const themesCount = installedThemes.value.length;
+
+		watch(syncDeviceDarkMode, () => {
+			if (syncDeviceDarkMode) {
+				defaultStore.set('darkMode', isDeviceDarkmode());
+			}
+		});
+
+		watch(wallpaper, () => {
+			if (wallpaper.value == null) {
+				localStorage.removeItem('wallpaper');
+			} else {
+				localStorage.setItem('wallpaper', wallpaper.value);
+			}
+			location.reload();
+		});
+
+		onMounted(() => {
+			emit('info', INFO);
+		});
+
+		onActivated(() => {
+			fetchThemes().then(() => {
+				installedThemes.value = getThemes();
+			});
+		});
+
+		fetchThemes().then(() => {
+			installedThemes.value = getThemes();
+		});
+
+		return {
+			[symbols.PAGE_INFO]: INFO,
+			darkThemes,
+			lightThemes,
+			darkThemeId,
+			lightThemeId,
+			darkMode,
+			syncDeviceDarkMode,
+			themesCount,
+			wallpaper,
+			setWallpaper(e) {
+				selectFile(e.currentTarget || e.target, null, false).then(file => {
+					wallpaper.value = file.url;
+				});
+			},
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rfqxtzch {
+	padding: 16px;
+
+	> .darkMode {
+		position: relative;
+		padding: 32px 0;
+
+		&.disabled {
+			opacity: 0.7;
+
+			&, * {
+				cursor: not-allowed !important;
+			}
+		}
+
+		.toggleWrapper {
+			position: absolute;
+			top: 50%;
+			left: 50%;
+			overflow: hidden;
+			padding: 0 100px;
+			transform: translate3d(-50%, -50%, 0);
+
+			input {
+				position: absolute;
+				left: -99em;
+			}
+		}
+
+		.toggle {
+			cursor: pointer;
+			display: inline-block;
+			position: relative;
+			width: 90px;
+			height: 50px;
+			background-color: #83D8FF;
+			border-radius: 90px - 6;
+			transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+
+			> .before, > .after {
+				position: absolute;
+				top: 15px;
+				font-size: 18px;
+				transition: color 1s ease;
+			}
+
+			> .before {
+				left: -70px;
+				color: var(--accent);
+			}
+
+			> .after {
+				right: -68px;
+				color: var(--fg);
+			}
+		}
+
+		.toggle__handler {
+			display: inline-block;
+			position: relative;
+			z-index: 1;
+			top: 3px;
+			left: 3px;
+			width: 50px - 6;
+			height: 50px - 6;
+			background-color: #FFCF96;
+			border-radius: 50px;
+			box-shadow: 0 2px 6px rgba(0,0,0,.3);
+			transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
+			transform:  rotate(-45deg);
+
+			.crater {
+				position: absolute;
+				background-color: #E8CDA5;
+				opacity: 0;
+				transition: opacity 200ms ease-in-out !important;
+				border-radius: 100%;
+			}
+
+			.crater--1 {
+				top: 18px;
+				left: 10px;
+				width: 4px;
+				height: 4px;
+			}
+
+			.crater--2 {
+				top: 28px;
+				left: 22px;
+				width: 6px;
+				height: 6px;
+			}
+
+			.crater--3 {
+				top: 10px;
+				left: 25px;
+				width: 8px;
+				height: 8px;
+			}
+		}
+
+		.star {
+			position: absolute;
+			background-color: #ffffff;
+			transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+			border-radius: 50%;
+		}
+
+		.star--1 {
+			top: 10px;
+			left: 35px;
+			z-index: 0;
+			width: 30px;
+			height: 3px;
+		}
+
+		.star--2 {
+			top: 18px;
+			left: 28px;
+			z-index: 1;
+			width: 30px;
+			height: 3px;
+		}
+
+		.star--3 {
+			top: 27px;
+			left: 40px;
+			z-index: 0;
+			width: 30px;
+			height: 3px;
+		}
+
+		.star--4,
+		.star--5,
+		.star--6 {
+			opacity: 0;
+			transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+		}
+
+		.star--4 {
+			top: 16px;
+			left: 11px;
+			z-index: 0;
+			width: 2px;
+			height: 2px;
+			transform: translate3d(3px,0,0);
+		}
+
+		.star--5 {
+			top: 32px;
+			left: 17px;
+			z-index: 0;
+			width: 3px;
+			height: 3px;
+			transform: translate3d(3px,0,0);
+		}
+
+		.star--6 {
+			top: 36px;
+			left: 28px;
+			z-index: 0;
+			width: 2px;
+			height: 2px;
+			transform: translate3d(3px,0,0);
+		}
+
+		input:checked {
+			+ .toggle {
+				background-color: #749DD6;
+
+				> .before {
+					color: var(--fg);
+				}
+
+				> .after {
+					color: var(--accent);
+				}
+
+				.toggle__handler {
+					background-color: #FFE5B5;
+					transform: translate3d(40px, 0, 0) rotate(0);
+
+					.crater { opacity: 1; }
+				}
+
+				.star--1 {
+					width: 2px;
+					height: 2px;
+				}
+
+				.star--2 {
+					width: 4px;
+					height: 4px;
+					transform: translate3d(-5px, 0, 0);
+				}
+
+				.star--3 {
+					width: 2px;
+					height: 2px;
+					transform: translate3d(-7px, 0, 0);
+				}
+
+				.star--4,
+				.star--5,
+				.star--6 {
+					opacity: 1;
+					transform: translate3d(0,0,0);
+				}
+
+				.star--4 {
+					transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+				}
+
+				.star--5 {
+					transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+				}
+
+				.star--6 {
+					transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/settings/update.vue b/packages/client/src/pages/settings/update.vue
new file mode 100644
index 0000000000..aa4050fe9f
--- /dev/null
+++ b/packages/client/src/pages/settings/update.vue
@@ -0,0 +1,95 @@
+<template>
+<FormBase>
+	<template v-if="meta">
+		<FormInfo v-if="version === meta.version">{{ $ts.youAreRunningUpToDateClient }}</FormInfo>
+		<FormInfo v-else warn>{{ $ts.newVersionOfClientAvailable }}</FormInfo>
+	</template>
+	<FormGroup>
+		<template #label>{{ instanceName }}</template>
+		<FormKeyValueView>
+			<template #key>{{ $ts.currentVersion }}</template>
+			<template #value>{{ version }}</template>
+		</FormKeyValueView>
+		<FormKeyValueView>
+			<template #key>{{ $ts.latestVersion }}</template>
+			<template #value v-if="meta">{{ meta.version }}</template>
+			<template #value v-else><MkEllipsis/></template>
+		</FormKeyValueView>
+	</FormGroup>
+	<FormGroup>
+		<template #label>Misskey</template>
+		<FormKeyValueView>
+			<template #key>{{ $ts.latestVersion }}</template>
+			<template #value v-if="releases">{{ releases[0].tag_name }}</template>
+			<template #value v-else><MkEllipsis/></template>
+		</FormKeyValueView>
+		<template #caption v-if="releases"><MkTime :time="releases[0].published_at" mode="detail"/></template>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import * as os from '@/os';
+import { version, instanceName } from '@/config';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormSelect,
+		FormSwitch,
+		FormButton,
+		FormLink,
+		FormGroup,
+		FormKeyValueView,
+		FormInfo,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'Misskey Update',
+				icon: 'fas fa-sync-alt',
+				bg: 'var(--bg)',
+			},
+			version,
+			instanceName,
+			releases: null,
+			meta: null
+		}
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+
+		os.api('meta', {
+			detail: false
+		}).then(meta => {
+			this.meta = meta;
+			localStorage.setItem('v', meta.version);
+		});
+
+		fetch('https://api.github.com/repos/misskey-dev/misskey/releases', {
+			method: 'GET',
+		})
+		.then(res => res.json())
+		.then(res => {
+			this.releases = res;
+		});
+	},
+
+	methods: {
+	}
+});
+</script>
diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue
new file mode 100644
index 0000000000..c2162bb1f3
--- /dev/null
+++ b/packages/client/src/pages/settings/word-mute.vue
@@ -0,0 +1,110 @@
+<template>
+<div>
+	<MkTab v-model="tab">
+		<option value="soft">{{ $ts._wordMute.soft }}</option>
+		<option value="hard">{{ $ts._wordMute.hard }}</option>
+	</MkTab>
+	<FormBase>
+		<div class="_debobigegoItem">
+			<div v-show="tab === 'soft'">
+				<FormInfo>{{ $ts._wordMute.softDescription }}</FormInfo>
+				<FormTextarea v-model="softMutedWords">
+					<span>{{ $ts._wordMute.muteWords }}</span>
+					<template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
+				</FormTextarea>
+			</div>
+			<div v-show="tab === 'hard'">
+				<FormInfo>{{ $ts._wordMute.hardDescription }}</FormInfo>
+				<FormTextarea v-model="hardMutedWords">
+					<span>{{ $ts._wordMute.muteWords }}</span>
+					<template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
+				</FormTextarea>
+				<FormKeyValueView v-if="hardWordMutedNotesCount != null">
+					<template #key>{{ $ts._wordMute.mutedNotes }}</template>
+					<template #value>{{ number(hardWordMutedNotesCount) }}</template>
+				</FormKeyValueView>
+			</div>
+		</div>
+		<FormButton @click="save()" primary inline :disabled="!changed"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+	</FormBase>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormInfo from '@/components/debobigego/info.vue';
+import MkTab from '@/components/tab.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		FormTextarea,
+		FormKeyValueView,
+		MkTab,
+		FormInfo,
+	},
+
+	emits: ['info'],
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.wordMute,
+				icon: 'fas fa-comment-slash',
+				bg: 'var(--bg)',
+			},
+			tab: 'soft',
+			softMutedWords: '',
+			hardMutedWords: '',
+			hardWordMutedNotesCount: null,
+			changed: false,
+		}
+	},
+
+	watch: {
+		softMutedWords: {
+			handler() {
+				this.changed = true;
+			},
+			deep: true
+		},
+		hardMutedWords: {
+			handler() {
+				this.changed = true;
+			},
+			deep: true
+		},
+	},
+
+	async created() {
+		this.softMutedWords = this.$store.state.mutedWords.map(x => x.join(' ')).join('\n');
+		this.hardMutedWords = this.$i.mutedWords.map(x => x.join(' ')).join('\n');
+
+		this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
+	},
+
+	mounted() {
+		this.$emit('info', this[symbols.PAGE_INFO]);
+	},
+
+	methods: {
+		async save() {
+			this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')));
+			await os.api('i/update', {
+				mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
+			});
+			this.changed = false;
+		},
+
+		number
+	}
+});
+</script>
diff --git a/packages/client/src/pages/share.vue b/packages/client/src/pages/share.vue
new file mode 100644
index 0000000000..c0af44fdd1
--- /dev/null
+++ b/packages/client/src/pages/share.vue
@@ -0,0 +1,184 @@
+<template>
+<div class="">
+	<section class="_section">
+		<div class="_content">
+			<XPostForm
+				v-if="state === 'writing'"
+				fixed
+				:share="true"
+				:initial-text="initialText"
+				:initial-visibility="visibility"
+				:initial-files="files"
+				:initial-local-only="localOnly"
+				:reply="reply"
+				:renote="renote"
+				:visible-users="visibleUsers"
+				@posted="state = 'posted'"
+				class="_panel"
+			/>
+			<MkButton v-else-if="state === 'posted'" primary @click="close()" class="close">{{ $ts.close }}</MkButton>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+// SPECIFICATION: https://misskey-hub.net/docs/features/share-form.html
+
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import XPostForm from '@/components/post-form.vue';
+import * as os from '@/os';
+import { noteVisibilities } from 'misskey-js';
+import * as Acct from 'misskey-js/built/acct';
+import * as symbols from '@/symbols';
+import * as Misskey from 'misskey-js';
+
+export default defineComponent({
+	components: {
+		XPostForm,
+		MkButton,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.share,
+				icon: 'fas fa-share-alt'
+			},
+			state: 'fetching' as 'fetching' | 'writing' | 'posted',
+
+			title: null as string | null,
+			initialText: null as string | null,
+			reply: null as Misskey.entities.Note | null,
+			renote: null as Misskey.entities.Note | null,
+			visibility: null as string | null,
+			localOnly: null as boolean | null,
+			files: [] as Misskey.entities.DriveFile[],
+			visibleUsers: [] as Misskey.entities.User[],
+		}
+	},
+
+	async created() {
+		const urlParams = new URLSearchParams(window.location.search);
+
+		this.title = urlParams.get('title');
+		const text = urlParams.get('text');
+		const url = urlParams.get('url');
+
+		let noteText = '';
+		if (this.title) noteText += `[ ${this.title} ]\n`;
+		// Googleニュース対策
+		if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, '');
+		else if (text && this.title !== text) noteText += `${text}\n`;
+		if (url) noteText += `${url}`;
+		this.initialText = noteText.trim();
+
+		const visibility = urlParams.get('visibility');
+		if (noteVisibilities.includes(visibility)) {
+			this.visibility = visibility;
+		}
+
+		if (this.visibility === 'specified') {
+			const visibleUserIds = urlParams.get('visibleUserIds');
+			const visibleAccts = urlParams.get('visibleAccts');
+			await Promise.all(
+				[
+					...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []),
+					...(visibleAccts ? visibleAccts.split(',').map(Acct.parse) : [])
+				]
+				// TypeScriptの指示通りに変換する
+				.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
+				.map(q => os.api('users/show', q)
+					.then(user => {
+						this.visibleUsers.push(user);
+					}, () => {
+						console.error(`Invalid user query: ${JSON.stringify(q)}`);
+					})
+				)
+			);
+		}
+
+		const localOnly = urlParams.get('localOnly');
+		if (localOnly === '0') this.localOnly = false;
+		else if (localOnly === '1') this.localOnly = true;
+
+		try {
+			//#region Reply
+			const replyId = urlParams.get('replyId');
+			const replyUri = urlParams.get('replyUri');
+			if (replyId) {
+				this.reply = await os.api('notes/show', {
+					noteId: replyId
+				});
+			} else if (replyUri) {
+				const obj = await os.api('ap/show', {
+					uri: replyUri
+				});
+				if (obj.type === 'Note') {
+					this.reply = obj.object;
+				}
+			}
+			//#endregion
+
+			//#region Renote
+			const renoteId = urlParams.get('renoteId');
+			const renoteUri = urlParams.get('renoteUri');
+			if (renoteId) {
+				this.renote = await os.api('notes/show', {
+					noteId: renoteId
+				});
+			} else if (renoteUri) {
+				const obj = await os.api('ap/show', {
+					uri: renoteUri
+				});
+				if (obj.type === 'Note') {
+					this.renote = obj.object;
+				}
+			}
+			//#endregion
+
+			//#region Drive files
+			const fileIds = urlParams.get('fileIds');
+			if (fileIds) {
+				await Promise.all(
+					fileIds.split(',')
+					.map(fileId => os.api('drive/files/show', { fileId })
+						.then(file => {
+							this.files.push(file);
+						}, () => {
+							console.error(`Failed to fetch a file ${fileId}`);
+						})
+					)
+				);
+			}
+			//#endregion
+		} catch (e) {
+			os.dialog({
+				type: 'error',
+				title: e.message,
+				text: e.name
+			});
+		}
+
+		this.state = 'writing';
+	},
+
+	methods: {
+		close() {
+			window.close();
+
+			// 閉じなければ100ms後タイムラインに
+			setTimeout(() => {
+				this.$router.push('/');
+			}, 100);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.close {
+	margin: 16px auto;
+}
+</style>
diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue
new file mode 100644
index 0000000000..3bbc9938dd
--- /dev/null
+++ b/packages/client/src/pages/signup-complete.vue
@@ -0,0 +1,50 @@
+<template>
+<div>
+	{{ $ts.processing }}
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+import { login } from '@/account';
+
+export default defineComponent({
+	components: {
+
+	},
+
+	props: {
+		code: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.signup,
+				icon: 'fas fa-user'
+			},
+		}
+	},
+
+	mounted() {
+		os.apiWithDialog('signup-pending', {
+			code: this.code,
+		}).then(res => {
+			login(res.i, '/');
+		});
+	},
+
+	methods: {
+
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/tag.vue b/packages/client/src/pages/tag.vue
new file mode 100644
index 0000000000..f4709659e3
--- /dev/null
+++ b/packages/client/src/pages/tag.vue
@@ -0,0 +1,57 @@
+<template>
+<div class="_section">
+	<XNotes ref="notes" class="_content" :pagination="pagination" @before="before" @after="after"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XNotes from '@/components/notes.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes
+	},
+
+	props: {
+		tag: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.tag,
+				icon: 'fas fa-hashtag'
+			},
+			pagination: {
+				endpoint: 'notes/search-by-tag',
+				limit: 10,
+				params: () => ({
+					tag: this.tag,
+				})
+			},
+		};
+	},
+
+	watch: {
+		tag() {
+			(this.$refs.notes as any).reload();
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/test.vue b/packages/client/src/pages/test.vue
new file mode 100644
index 0000000000..9dd9ae5e0c
--- /dev/null
+++ b/packages/client/src/pages/test.vue
@@ -0,0 +1,259 @@
+<template>
+<div class="_section">
+	<div class="_content">
+		<div class="_card _gap">
+			<div class="_title">Dialog</div>
+			<div class="_content">
+				<MkInput v-model="dialogTitle">
+					<template #label>Title</template>
+				</MkInput>
+				<MkInput v-model="dialogBody">
+					<template #label>Body</template>
+				</MkInput>
+				<MkRadio v-model="dialogType" value="info">Info</MkRadio>
+				<MkRadio v-model="dialogType" value="success">Success</MkRadio>
+				<MkRadio v-model="dialogType" value="warning">Warn</MkRadio>
+				<MkRadio v-model="dialogType" value="error">Error</MkRadio>
+				<MkSwitch v-model="dialogCancel">
+					<span>With cancel button</span>
+				</MkSwitch>
+				<MkSwitch v-model="dialogCancelByBgClick">
+					<span>Can cancel by modal bg click</span>
+				</MkSwitch>
+				<MkSwitch v-model="dialogInput">
+					<span>With input field</span>
+				</MkSwitch>
+				<MkButton @click="showDialog()">Show</MkButton>
+			</div>
+			<div class="_content">
+				<code>Result: {{ dialogResult }}</code>
+			</div>
+		</div>
+
+		<div class="_card _gap">
+			<div class="_title">Form</div>
+			<div class="_content">
+				<MkInput v-model="formTitle">
+					<template #label>Title</template>
+				</MkInput>
+				<MkTextarea v-model="formForm">
+					<template #label>Form</template>
+				</MkTextarea>
+				<MkButton @click="form()">Show</MkButton>
+			</div>
+			<div class="_content">
+				<code>Result: {{ formResult }}</code>
+			</div>
+		</div>
+
+		<div class="_card _gap">
+			<div class="_title">MFM</div>
+			<div class="_content">
+				<MkTextarea v-model="mfm">
+					<template #label>MFM</template>
+				</MkTextarea>
+			</div>
+			<div class="_content">
+				<Mfm :text="mfm"/>
+			</div>
+		</div>
+
+		<div class="_card _gap">
+			<div class="_title">selectDriveFile</div>
+			<div class="_content">
+				<MkSwitch v-model="selectDriveFileMultiple">
+					<span>Multiple</span>
+				</MkSwitch>
+				<MkButton @click="selectDriveFile()">selectDriveFile</MkButton>
+			</div>
+			<div class="_content">
+				<code>Result: {{ JSON.stringify(selectDriveFileResult) }}</code>
+			</div>
+		</div>
+
+		<div class="_card _gap">
+			<div class="_title">selectDriveFolder</div>
+			<div class="_content">
+				<MkSwitch v-model="selectDriveFolderMultiple">
+					<span>Multiple</span>
+				</MkSwitch>
+				<MkButton @click="selectDriveFolder()">selectDriveFolder</MkButton>
+			</div>
+			<div class="_content">
+				<code>Result: {{ JSON.stringify(selectDriveFolderResult) }}</code>
+			</div>
+		</div>
+
+		<div class="_card _gap">
+			<div class="_title">selectUser</div>
+			<div class="_content">
+				<MkButton @click="selectUser()">selectUser</MkButton>
+			</div>
+			<div class="_content">
+				<code>Result: {{ user }}</code>
+			</div>
+		</div>
+
+		<div class="_card _gap">
+			<div class="_title">Notification</div>
+			<div class="_content">
+				<MkInput v-model="notificationIconUrl">
+					<template #label>Icon URL</template>
+				</MkInput>
+				<MkInput v-model="notificationHeader">
+					<template #label>Header</template>
+				</MkInput>
+				<MkTextarea v-model="notificationBody">
+					<template #label>Body</template>
+				</MkTextarea>
+				<MkButton @click="createNotification()">createNotification</MkButton>
+			</div>
+		</div>
+
+		<div class="_card _gap">
+			<div class="_title">Waiting dialog</div>
+			<div class="_content">
+				<MkButton inline @click="openWaitingDialog()">icon only</MkButton>
+				<MkButton inline @click="openWaitingDialog('Doing')">with text</MkButton>
+			</div>
+		</div>
+
+		<div class="_card _gap">
+			<div class="_title">Messaging window</div>
+			<div class="_content">
+				<MkButton @click="messagingWindowOpen()">open</MkButton>
+			</div>
+		</div>
+
+		<MkButton @click="resetTutorial()">Reset tutorial</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import MkTextarea from '@/components/form/textarea.vue';
+import MkRadio from '@/components/form/radio.vue';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+		MkSwitch,
+		MkTextarea,
+		MkRadio,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'TEST',
+				icon: 'fas fa-exclamation-triangle'
+			},
+			dialogTitle: 'Hello',
+			dialogBody: 'World!',
+			dialogType: 'info',
+			dialogCancel: false,
+			dialogCancelByBgClick: true,
+			dialogInput: false,
+			dialogResult: null,
+			formTitle: 'Test form',
+			formForm: JSON.stringify({
+				foo: {
+					type: 'boolean',
+					default: true,
+					label: 'This is a boolean property'
+				},
+				bar: {
+					type: 'number',
+					default: 300,
+					label: 'This is a number property'
+				},
+				baz: {
+					type: 'string',
+					default: 'Misskey makes you happy.',
+					label: 'This is a string property'
+				},
+				qux: {
+					type: 'string',
+					multiline: true,
+					default: 'Misskey makes\nyou happy.',
+					label: 'Multiline string'
+				},
+			}, null, '\t'),
+			formResult: null,
+			mfm: '',
+			selectDriveFileMultiple: false,
+			selectDriveFolderMultiple: false,
+			selectDriveFileResult: null,
+			selectDriveFolderResult: null,
+			user: null,
+			notificationIconUrl: null,
+			notificationHeader: '',
+			notificationBody: '',
+		}
+	},
+
+	methods: {
+		async showDialog() {
+			this.dialogResult = null;
+			this.dialogResult = await os.dialog({
+				type: this.dialogType,
+				title: this.dialogTitle,
+				text: this.dialogBody,
+				showCancelButton: this.dialogCancel,
+				cancelableByBgClick: this.dialogCancelByBgClick,
+				input: this.dialogInput ? {} : null
+			});
+		},
+
+		async form() {
+			this.formResult = null;
+			this.formResult = await os.form(this.formTitle, JSON.parse(this.formForm));
+		},
+
+		async selectDriveFile() {
+			this.selectDriveFileResult = null;
+			this.selectDriveFileResult = await os.selectDriveFile(this.selectDriveFileMultiple);
+		},
+
+		async selectDriveFolder() {
+			this.selectDriveFolderResult = null;
+			this.selectDriveFolderResult = await os.selectDriveFolder(this.selectDriveFolderMultiple);
+		},
+
+		async selectUser() {
+			this.user = null;
+			this.user = await os.selectUser();
+		},
+
+		async createNotification() {
+			os.api('notifications/create', {
+				header: this.notificationHeader,
+				body: this.notificationBody,
+				icon: this.notificationIconUrl,
+			});
+		},
+
+		messagingWindowOpen() {
+			os.pageWindow('/my/messaging');
+		},
+
+		openWaitingDialog(text?) {
+			const promise = new Promise((resolve, reject) => {
+				setTimeout(resolve, 2000);
+			});
+			os.promiseDialog(promise, null, null, text);
+		},
+
+		resetTutorial() {
+			this.$store.set('tutorial', 0);
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue
new file mode 100644
index 0000000000..d1a892629b
--- /dev/null
+++ b/packages/client/src/pages/theme-editor.vue
@@ -0,0 +1,306 @@
+<template>
+<FormBase class="cwepdizn">
+	<div class="_debobigegoItem colorPicker">
+		<div class="_debobigegoLabel">{{ $ts.backgroundColor }}</div>
+		<div class="_debobigegoPanel colors">
+			<div class="row">
+				<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
+					<div class="preview" :style="{ background: color.forPreview }"></div>
+				</button>
+			</div>
+			<div class="row">
+				<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
+					<div class="preview" :style="{ background: color.forPreview }"></div>
+				</button>
+			</div>
+		</div>
+	</div>
+	<div class="_debobigegoItem colorPicker">
+		<div class="_debobigegoLabel">{{ $ts.accentColor }}</div>
+		<div class="_debobigegoPanel colors">
+			<div class="row">
+				<button v-for="color in accentColors" :key="color" @click="setAccentColor(color)" class="color rounded _button" :class="{ active: theme.props.accent === color }">
+					<div class="preview" :style="{ background: color }"></div>
+				</button>
+			</div>
+		</div>
+	</div>
+	<div class="_debobigegoItem colorPicker">
+		<div class="_debobigegoLabel">{{ $ts.textColor }}</div>
+		<div class="_debobigegoPanel colors">
+			<div class="row">
+				<button v-for="color in fgColors" :key="color" @click="setFgColor(color)" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }">
+					<div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
+				</button>
+			</div>
+		</div>
+	</div>
+
+	<FormGroup v-if="codeEnabled">
+		<FormTextarea v-model="themeCode" tall>
+			<span>{{ $ts._theme.code }}</span>
+		</FormTextarea>
+		<FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton>
+	</FormGroup>
+	<FormButton v-else @click="codeEnabled = true"><i class="fas fa-code"></i> {{ $ts.editCode }}</FormButton>
+
+	<FormGroup v-if="descriptionEnabled">
+		<FormTextarea v-model="description">
+			<span>{{ $ts._theme.description }}</span>
+		</FormTextarea>
+	</FormGroup>
+	<FormButton v-else @click="descriptionEnabled = true">{{ $ts.addDescription }}</FormButton>
+
+	<FormGroup>
+		<FormButton @click="showPreview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
+		<FormButton @click="saveAs" primary><i class="fas fa-save"></i> {{ $ts.saveAs }}</FormButton>
+	</FormGroup>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { toUnicode } from 'punycode/';
+import * as tinycolor from 'tinycolor2';
+import { v4 as uuid} from 'uuid';
+import * as JSON5 from 'json5';
+
+import FormBase from '@/components/debobigego/base.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+
+import { Theme, applyTheme, validateTheme, darkTheme, lightTheme } from '@/scripts/theme';
+import { host } from '@/config';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+import { addTheme } from '@/theme-store';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormButton,
+		FormTextarea,
+		FormGroup,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.themeEditor,
+				icon: 'fas fa-palette',
+			},
+			theme: {
+				base: 'light',
+				props: lightTheme.props
+			} as Theme,
+			codeEnabled: false,
+			descriptionEnabled: false,
+			description: null,
+			themeCode: null,
+			bgColors: [
+				{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
+				{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
+				{ color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' },
+				{ color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' },
+				{ color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' },
+				{ color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' },
+				{ color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' },
+				{ color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' },
+				{ color: '#2b2b2b', kind: 'dark', forPreview: '#444444' },
+				{ color: '#362e29', kind: 'dark', forPreview: '#735c4d' },
+				{ color: '#303629', kind: 'dark', forPreview: '#506d2f' },
+				{ color: '#293436', kind: 'dark', forPreview: '#258192' },
+				{ color: '#2e2936', kind: 'dark', forPreview: '#504069' },
+				{ color: '#252722', kind: 'dark', forPreview: '#3c462f' },
+				{ color: '#212525', kind: 'dark', forPreview: '#303e3e' },
+				{ color: '#191919', kind: 'dark', forPreview: '#272727' },
+			],
+			accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'],
+			fgColors: [
+				{ color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
+				{ color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
+				{ color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' },
+				{ color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' },
+				{ color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' },
+				{ color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
+				{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
+			],
+			changed: false,
+		}
+	},
+
+	created() {
+		this.$watch('theme', this.apply, { deep: true });
+		window.addEventListener('beforeunload', this.beforeunload);
+	},
+
+	beforeUnmount() {
+		window.removeEventListener('beforeunload', this.beforeunload);
+	},
+
+	async beforeRouteLeave(to, from) {
+		if (this.changed && !(await this.leaveConfirm())) {
+			return false;
+		}
+	},
+
+	methods: {
+		beforeunload(e: BeforeUnloadEvent) {
+			if (this.changed) {
+				e.preventDefault();
+				e.returnValue = '';
+			}
+		},
+
+		async leaveConfirm(): Promise<boolean> {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$ts.leaveConfirm,
+				showCancelButton: true
+			});
+			return !canceled;
+		},
+
+		showPreview() {
+			os.pageWindow('preview');
+		},
+
+		setBgColor(color) {
+			if (this.theme.base != color.kind) {
+				const base = color.kind === 'dark' ? darkTheme : lightTheme;
+				for (const prop of Object.keys(base.props)) {
+					if (prop === 'accent') continue;
+					if (prop === 'fg') continue;
+					this.theme.props[prop] = base.props[prop];
+				}
+			}
+			this.theme.base = color.kind;
+			this.theme.props.bg = color.color;
+
+			if (this.theme.props.fg) {
+				const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(this.theme.props.fg).toRgbString()));
+				if (matchedFgColor) this.setFgColor(matchedFgColor);
+			}
+		},
+
+		setAccentColor(color) {
+			this.theme.props.accent = color;
+		},
+
+		setFgColor(color) {
+			this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark;
+		},
+
+		apply() {
+			this.themeCode = JSON5.stringify(this.theme, null, '\t');
+			applyTheme(this.theme, false);
+			this.changed = true;
+		},
+
+		applyThemeCode() {
+			let parsed;
+
+			try {
+				parsed = JSON5.parse(this.themeCode);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: this.$ts._theme.invalid
+				});
+				return;
+			}
+
+			this.theme = parsed;
+		},
+
+		async saveAs() {
+			const { canceled, result: name } = await os.dialog({
+				title: this.$ts.name,
+				input: {
+					allowEmpty: false
+				}
+			});
+			if (canceled) return;
+
+			this.theme.id = uuid();
+			this.theme.name = name;
+			this.theme.author = `@${this.$i.username}@${toUnicode(host)}`;
+			if (this.description) this.theme.desc = this.description;
+			addTheme(this.theme);
+			applyTheme(this.theme);
+			if (this.$store.state.darkMode) {
+				ColdDeviceStorage.set('darkTheme', this.theme);
+			} else {
+				ColdDeviceStorage.set('lightTheme', this.theme);
+			}
+			this.changed = false;
+			os.dialog({
+				type: 'success',
+				text: this.$t('_theme.installed', { name: this.theme.name })
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.cwepdizn {
+	max-width: 800px;
+	margin: 0 auto;
+
+	> .colorPicker {
+		> .colors {
+			padding: 32px;
+			text-align: center;
+
+			> .row {
+				> .color {
+					display: inline-block;
+					position: relative;
+					width: 64px;
+					height: 64px;
+					border-radius: 8px;
+
+					> .preview {
+						position: absolute;
+						top: 0;
+						left: 0;
+						right: 0;
+						bottom: 0;
+						margin: auto;
+						width: 42px;
+						height: 42px;
+						border-radius: 4px;
+						box-shadow: 0 2px 4px rgb(0 0 0 / 30%);
+						transition: transform 0.15s ease;
+					}
+
+					&:hover {
+						> .preview {
+							transform: scale(1.1);
+						}
+					}
+
+					&.active {
+						box-shadow: 0 0 0 2px var(--divider) inset;
+					}
+
+					&.rounded {
+						border-radius: 999px;
+
+						> .preview {
+							border-radius: 999px;
+						}
+					}
+
+					&.char {
+						line-height: 42px;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue
new file mode 100644
index 0000000000..4d6dd0af41
--- /dev/null
+++ b/packages/client/src/pages/timeline.tutorial.vue
@@ -0,0 +1,131 @@
+<template>
+<div class="_card tbkwesmv">
+	<div class="_title"><i class="fas fa-info-circle"></i> {{ $ts._tutorial.title }}</div>
+	<div class="_content" v-if="tutorial === 0">
+		<div>{{ $ts._tutorial.step1_1 }}</div>
+		<div>{{ $ts._tutorial.step1_2 }}</div>
+		<div>{{ $ts._tutorial.step1_3 }}</div>
+	</div>
+	<div class="_content" v-else-if="tutorial === 1">
+		<div>{{ $ts._tutorial.step2_1 }}</div>
+		<div>{{ $ts._tutorial.step2_2 }}</div>
+		<MkA class="_link" to="/settings/profile">{{ $ts.editProfile }}</MkA>
+	</div>
+	<div class="_content" v-else-if="tutorial === 2">
+		<div>{{ $ts._tutorial.step3_1 }}</div>
+		<div>{{ $ts._tutorial.step3_2 }}</div>
+		<div>{{ $ts._tutorial.step3_3 }}</div>
+		<small>{{ $ts._tutorial.step3_4 }}</small>
+	</div>
+	<div class="_content" v-else-if="tutorial === 3">
+		<div>{{ $ts._tutorial.step4_1 }}</div>
+		<div>{{ $ts._tutorial.step4_2 }}</div>
+	</div>
+	<div class="_content" v-else-if="tutorial === 4">
+		<div>{{ $ts._tutorial.step5_1 }}</div>
+		<I18n :src="$ts._tutorial.step5_2" tag="div">
+			<template #featured>
+				<MkA class="_link" to="/featured">{{ $ts.featured }}</MkA>
+			</template>
+			<template #explore>
+				<MkA class="_link" to="/explore">{{ $ts.explore }}</MkA>
+			</template>
+		</I18n>
+		<div>{{ $ts._tutorial.step5_3 }}</div>
+		<small>{{ $ts._tutorial.step5_4 }}</small>
+	</div>
+	<div class="_content" v-else-if="tutorial === 5">
+		<div>{{ $ts._tutorial.step6_1 }}</div>
+		<div>{{ $ts._tutorial.step6_2 }}</div>
+		<div>{{ $ts._tutorial.step6_3 }}</div>
+	</div>
+	<div class="_content" v-else-if="tutorial === 6">
+		<div>{{ $ts._tutorial.step7_1 }}</div>
+		<I18n :src="$ts._tutorial.step7_2" tag="div">
+			<template #help>
+				<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ $ts.help }}</a>
+			</template>
+		</I18n>
+		<div>{{ $ts._tutorial.step7_3 }}</div>
+	</div>
+
+	<div class="_footer navigation">
+		<div class="step">
+			<button class="arrow _button" @click="tutorial--" :disabled="tutorial === 0">
+				<i class="fas fa-chevron-left"></i>
+			</button>
+			<span>{{ tutorial + 1 }} / 7</span>
+			<button class="arrow _button" @click="tutorial++" :disabled="tutorial === 6">
+				<i class="fas fa-chevron-right"></i>
+			</button>
+		</div>
+		<MkButton class="ok" @click="tutorial = -1" primary v-if="tutorial === 6"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
+		<MkButton class="ok" @click="tutorial++" primary v-else><i class="fas fa-check"></i> {{ $ts.next }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+	components: {
+		MkButton,
+	},
+
+	data() {
+		return {
+		}
+	},
+
+	computed: {
+		tutorial: {
+			get() { return this.$store.reactiveState.tutorial.value || 0; },
+			set(value) { this.$store.set('tutorial', value); }
+		},
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.tbkwesmv {
+	> ._content {
+		> small {
+			opacity: 0.7;
+		}
+	}
+
+	> .navigation {
+		display: flex;
+		flex-direction: row;
+		align-items: baseline;
+
+		> .step {
+			> .arrow {
+				padding: 4px;
+
+				&:disabled {
+					opacity: 0.5;
+				}
+
+				&:first-child {
+					padding-right: 8px;
+				}
+
+				&:last-child {
+					padding-left: 8px;
+				}
+			}
+
+			> span {
+				margin: 0 4px;
+			}
+		}
+
+		> .ok {
+			margin-left: auto;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
new file mode 100644
index 0000000000..911d6f5c6a
--- /dev/null
+++ b/packages/client/src/pages/timeline.vue
@@ -0,0 +1,225 @@
+<template>
+<div class="cmuxhskf" v-size="{ min: [800] }" v-hotkey.global="keymap">
+	<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
+	<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
+
+	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
+	<div class="tl _block">
+		<XTimeline ref="tl" class="tl"
+			:key="src"
+			:src="src"
+			:sound="true"
+			@before="before()"
+			@after="after()"
+			@queue="queueUpdated"
+		/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
+import Progress from '@/scripts/loading';
+import XTimeline from '@/components/timeline.vue';
+import XPostForm from '@/components/post-form.vue';
+import { scroll } from '@/scripts/scroll';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	name: 'timeline',
+
+	components: {
+		XTimeline,
+		XTutorial: defineAsyncComponent(() => import('./timeline.tutorial.vue')),
+		XPostForm,
+	},
+
+	data() {
+		return {
+			src: 'home',
+			queue: 0,
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.$ts.timeline,
+				icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home',
+				bg: 'var(--bg)',
+				actions: [{
+					icon: 'fas fa-list-ul',
+					text: this.$ts.lists,
+					handler: this.chooseList
+				}, {
+					icon: 'fas fa-satellite',
+					text: this.$ts.antennas,
+					handler: this.chooseAntenna
+				}, {
+					icon: 'fas fa-satellite-dish',
+					text: this.$ts.channel,
+					handler: this.chooseChannel
+				}, {
+					icon: 'fas fa-calendar-alt',
+					text: this.$ts.jumpToSpecifiedDate,
+					handler: this.timetravel
+				}],
+				tabs: [{
+					active: this.src === 'home',
+					title: this.$ts._timelines.home,
+					icon: 'fas fa-home',
+					iconOnly: true,
+					onClick: () => { this.src = 'home'; this.saveSrc(); },
+				}, {
+					active: this.src === 'local',
+					title: this.$ts._timelines.local,
+					icon: 'fas fa-comments',
+					iconOnly: true,
+					onClick: () => { this.src = 'local'; this.saveSrc(); },
+				}, {
+					active: this.src === 'social',
+					title: this.$ts._timelines.social,
+					icon: 'fas fa-share-alt',
+					iconOnly: true,
+					onClick: () => { this.src = 'social'; this.saveSrc(); },
+				}, {
+					active: this.src === 'global',
+					title: this.$ts._timelines.global,
+					icon: 'fas fa-globe',
+					iconOnly: true,
+					onClick: () => { this.src = 'global'; this.saveSrc(); },
+				}],
+			})),
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				't': this.focus
+			};
+		},
+
+		isLocalTimelineAvailable(): boolean {
+			return !this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin;
+		},
+
+		isGlobalTimelineAvailable(): boolean {
+			return !this.$instance.disableGlobalTimeline || this.$i.isModerator || this.$i.isAdmin;
+		},
+	},
+
+	watch: {
+		src() {
+			this.showNav = false;
+		},
+	},
+
+	created() {
+		this.src = this.$store.state.tl.src;
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		},
+
+		queueUpdated(q) {
+			this.queue = q;
+		},
+
+		top() {
+			scroll(this.$el, { top: 0 });
+		},
+
+		async chooseList(ev) {
+			const lists = await os.api('users/lists/list');
+			const items = lists.map(list => ({
+				type: 'link',
+				text: list.name,
+				to: `/timeline/list/${list.id}`
+			}));
+			os.popupMenu(items, ev.currentTarget || ev.target);
+		},
+
+		async chooseAntenna(ev) {
+			const antennas = await os.api('antennas/list');
+			const items = antennas.map(antenna => ({
+				type: 'link',
+				text: antenna.name,
+				indicate: antenna.hasUnreadNote,
+				to: `/timeline/antenna/${antenna.id}`
+			}));
+			os.popupMenu(items, ev.currentTarget || ev.target);
+		},
+
+		async chooseChannel(ev) {
+			const channels = await os.api('channels/followed');
+			const items = channels.map(channel => ({
+				type: 'link',
+				text: channel.name,
+				indicate: channel.hasUnreadNote,
+				to: `/channels/${channel.id}`
+			}));
+			os.popupMenu(items, ev.currentTarget || ev.target);
+		},
+
+		saveSrc() {
+			this.$store.set('tl', {
+				src: this.src,
+			});
+		},
+
+		async timetravel() {
+			const { canceled, result: date } = await os.dialog({
+				title: this.$ts.date,
+				input: {
+					type: 'date'
+				}
+			});
+			if (canceled) return;
+
+			this.$refs.tl.timetravel(new Date(date));
+		},
+
+		focus() {
+			(this.$refs.tl as any).focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.cmuxhskf {
+	padding: var(--margin);
+
+	> .new {
+		position: sticky;
+		top: calc(var(--stickyTop, 0px) + 16px);
+		z-index: 1000;
+		width: 100%;
+
+		> button {
+			display: block;
+			margin: var(--margin) auto 0 auto;
+			padding: 8px 16px;
+			border-radius: 32px;
+		}
+	}
+
+	> .post-form {
+		border-radius: var(--radius);
+	}
+
+	> .tl {
+		background: var(--bg);
+		border-radius: var(--radius);
+		overflow: clip;
+	}
+
+	&.min-width_800px {
+		max-width: 800px;
+		margin: 0 auto;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user-ap-info.vue b/packages/client/src/pages/user-ap-info.vue
new file mode 100644
index 0000000000..6253faa242
--- /dev/null
+++ b/packages/client/src/pages/user-ap-info.vue
@@ -0,0 +1,124 @@
+<template>
+<FormBase>
+	<FormSuspense :p="apPromiseFactory" v-slot="{ result: ap }">
+		<FormGroup>
+			<template #label>ActivityPub</template>
+			<FormKeyValueView>
+				<template #key>Type</template>
+				<template #value><span class="_monospace">{{ ap.type }}</span></template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>URI</template>
+				<template #value><span class="_monospace">{{ ap.id }}</span></template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>URL</template>
+				<template #value><span class="_monospace">{{ ap.url }}</span></template>
+			</FormKeyValueView>
+			<FormGroup>
+				<FormKeyValueView>
+					<template #key>Inbox</template>
+					<template #value><span class="_monospace">{{ ap.inbox }}</span></template>
+				</FormKeyValueView>
+				<FormKeyValueView>
+					<template #key>Shared Inbox</template>
+					<template #value><span class="_monospace">{{ ap.sharedInbox || ap.endpoints.sharedInbox }}</span></template>
+				</FormKeyValueView>
+				<FormKeyValueView>
+					<template #key>Outbox</template>
+					<template #value><span class="_monospace">{{ ap.outbox }}</span></template>
+				</FormKeyValueView>
+			</FormGroup>
+			<FormTextarea readonly tall code pre :value="ap.publicKey.publicKeyPem">
+				<span>Public Key</span>
+			</FormTextarea>
+			<FormKeyValueView>
+				<template #key>Discoverable</template>
+				<template #value>{{ ap.discoverable ? $ts.yes : $ts.no }}</template>
+			</FormKeyValueView>
+			<FormKeyValueView>
+				<template #key>ManuallyApprovesFollowers</template>
+				<template #value>{{ ap.manuallyApprovesFollowers ? $ts.yes : $ts.no }}</template>
+			</FormKeyValueView>
+			<FormObjectView tall :value="ap">
+				<span>Raw</span>
+			</FormObjectView>
+			<FormGroup>
+				<FormLink :to="`https://${user.host}/.well-known/webfinger?resource=acct:${user.username}`" external>WebFinger</FormLink>
+			</FormGroup>
+			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
+			<FormKeyValueView v-else>
+				<template #key>{{ $ts.instanceInfo }}</template>
+				<template #value>(Local user)</template>
+			</FormKeyValueView>
+		</FormGroup>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormObjectView from '@/components/debobigego/object-view.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import bytes from '@/filters/bytes';
+import * as symbols from '@/symbols';
+import { url } from '@/config';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormTextarea,
+		FormObjectView,
+		FormButton,
+		FormLink,
+		FormGroup,
+		FormKeyValueView,
+		FormSuspense,
+	},
+
+	props: {
+		userId: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: this.$ts.userInfo,
+				icon: 'fas fa-info-circle'
+			},
+			user: null,
+			apPromiseFactory: null,
+		}
+	},
+
+	mounted() {
+		this.fetch();
+	},
+
+	methods: {
+		number,
+		bytes,
+
+		async fetch() {
+			this.user = await os.api('users/show', {
+				userId: this.userId
+			});
+
+			this.apPromiseFactory = () => os.api('ap/get', {
+				uri: this.user.uri || `${url}/users/${this.user.id}`
+			});
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
new file mode 100644
index 0000000000..b77d879a7e
--- /dev/null
+++ b/packages/client/src/pages/user-info.vue
@@ -0,0 +1,245 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<div class="_debobigegoItem aeakzknw">
+			<MkAvatar class="avatar" :user="user" :show-indicator="true"/>
+		</div>
+
+		<FormLink :to="userPage(user)">Profile</FormLink>
+
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>Acct</template>
+				<template #value><span class="_monospace">{{ acct(user) }}</span></template>
+			</FormKeyValueView>
+
+			<FormKeyValueView>
+				<template #key>ID</template>
+				<template #value><span class="_monospace">{{ user.id }}</span></template>
+			</FormKeyValueView>
+		</FormGroup>
+
+		<FormGroup v-if="iAmModerator">
+			<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:modelValue="toggleModerator" v-model="moderator">{{ $ts.moderator }}</FormSwitch>
+			<FormSwitch @update:modelValue="toggleSilence" v-model="silenced">{{ $ts.silence }}</FormSwitch>
+			<FormSwitch @update:modelValue="toggleSuspend" v-model="suspended">{{ $ts.suspend }}</FormSwitch>
+		</FormGroup>
+
+		<FormGroup>
+			<FormButton v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
+			<FormButton v-if="user.host == null && iAmModerator" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
+		</FormGroup>
+
+		<FormGroup>
+			<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink>
+
+			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
+			<FormKeyValueView v-else>
+				<template #key>{{ $ts.instanceInfo }}</template>
+				<template #value>(Local user)</template>
+			</FormKeyValueView>
+		</FormGroup>
+
+		<FormGroup>
+			<FormKeyValueView>
+				<template #key>{{ $ts.updatedAt }}</template>
+				<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
+			</FormKeyValueView>
+		</FormGroup>
+
+		<FormObjectView tall :value="user">
+			<span>Raw</span>
+		</FormObjectView>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { computed, defineAsyncComponent, defineComponent } from 'vue';
+import FormObjectView from '@/components/debobigego/object-view.vue';
+import FormTextarea from '@/components/debobigego/textarea.vue';
+import FormSwitch from '@/components/debobigego/switch.vue';
+import FormLink from '@/components/debobigego/link.vue';
+import FormBase from '@/components/debobigego/base.vue';
+import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/debobigego/button.vue';
+import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormSuspense from '@/components/debobigego/suspense.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import bytes from '@/filters/bytes';
+import * as symbols from '@/symbols';
+import { url } from '@/config';
+import { userPage, acct } from '@/filters/user';
+
+export default defineComponent({
+	components: {
+		FormBase,
+		FormTextarea,
+		FormSwitch,
+		FormObjectView,
+		FormButton,
+		FormLink,
+		FormGroup,
+		FormKeyValueView,
+		FormSuspense,
+	},
+
+	props: {
+		userId: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.user ? acct(this.user) : this.$ts.userInfo,
+				icon: 'fas fa-info-circle',
+				actions: this.user ? [this.user.url ? {
+					text: this.user.url,
+					icon: 'fas fa-external-link-alt',
+					handler: () => {
+						window.open(this.user.url, '_blank');
+					}
+				} : undefined].filter(x => x !== undefined) : [],
+			})),
+			init: null,
+			user: null,
+			info: null,
+			moderator: false,
+			silenced: false,
+			suspended: false,
+		}
+	},
+
+	computed: {
+		iAmModerator(): boolean {
+			return this.$i && (this.$i.isAdmin || this.$i.isModerator);
+		}
+	},
+
+	watch: {
+		userId: {
+			handler() {
+				this.init = this.createFetcher();
+			},
+			immediate: true
+		}
+	},
+
+	methods: {
+		number,
+		bytes,
+		userPage,
+		acct,
+
+		createFetcher() {
+			if (this.iAmModerator) {
+				return () => Promise.all([os.api('users/show', {
+					userId: this.userId
+				}), os.api('admin/show-user', {
+					userId: this.userId
+				})]).then(([user, info]) => {
+					this.user = user;
+					this.info = info;
+					this.moderator = this.info.isModerator;
+					this.silenced = this.info.isSilenced;
+					this.suspended = this.info.isSuspended;
+				});
+			} else {
+				return () => os.api('users/show', {
+					userId: this.userId
+				}).then((user) => {
+					this.user = user;
+				});
+			}
+		},
+
+		refreshUser() {
+			this.init = this.createFetcher();
+		},
+
+		async updateRemoteUser() {
+			await os.apiWithDialog('federation/update-remote-user', { userId: this.user.id });
+			this.refreshUser();
+		},
+
+		async resetPassword() {
+			const { password } = await os.api('admin/reset-password', {
+				userId: this.user.id,
+			});
+
+			os.dialog({
+				type: 'success',
+				text: this.$t('newPasswordIs', { password })
+			});
+		},
+
+		async toggleSilence(v) {
+			const confirm = await os.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				text: v ? this.$ts.silenceConfirm : this.$ts.unsilenceConfirm,
+			});
+			if (confirm.canceled) {
+				this.silenced = !v;
+			} else {
+				await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
+				await this.refreshUser();
+			}
+		},
+
+		async toggleSuspend(v) {
+			const confirm = await os.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				text: v ? this.$ts.suspendConfirm : this.$ts.unsuspendConfirm,
+			});
+			if (confirm.canceled) {
+				this.suspended = !v;
+			} else {
+				await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
+				await this.refreshUser();
+			}
+		},
+
+		async toggleModerator(v) {
+			await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id });
+			await this.refreshUser();
+		},
+
+		async deleteAllFiles() {
+			const confirm = await os.dialog({
+				type: 'warning',
+				showCancelButton: true,
+				text: this.$ts.deleteAllFilesConfirm,
+			});
+			if (confirm.canceled) return;
+			const process = async () => {
+				await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
+				os.success();
+			};
+			await process().catch(e => {
+				os.dialog({
+					type: 'error',
+					text: e.toString()
+				});
+			});
+			await this.refreshUser();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.aeakzknw {
+	> .avatar {
+		display: block;
+		margin: 0 auto;
+		width: 64px;
+		height: 64px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue
new file mode 100644
index 0000000000..2fc2476fba
--- /dev/null
+++ b/packages/client/src/pages/user-list-timeline.vue
@@ -0,0 +1,147 @@
+<template>
+<div class="eqqrhokj" v-hotkey.global="keymap" v-size="{ min: [800] }">
+	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
+	<div class="tl _block">
+		<XTimeline ref="tl" class="tl"
+			:key="listId"
+			src="list"
+			:list="listId"
+			:sound="true"
+			@before="before()"
+			@after="after()"
+			@queue="queueUpdated"
+		/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
+import Progress from '@/scripts/loading';
+import XTimeline from '@/components/timeline.vue';
+import { scroll } from '@/scripts/scroll';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XTimeline,
+	},
+
+	props: {
+		listId: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			list: null,
+			queue: 0,
+			[symbols.PAGE_INFO]: computed(() => this.list ? {
+				title: this.list.name,
+				icon: 'fas fa-list-ul',
+				bg: 'var(--bg)',
+				actions: [{
+					icon: 'fas fa-calendar-alt',
+					text: this.$ts.jumpToSpecifiedDate,
+					handler: this.timetravel
+				}, {
+					icon: 'fas fa-cog',
+					text: this.$ts.settings,
+					handler: this.settings
+				}],
+			} : null),
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				't': this.focus
+			};
+		},
+	},
+
+	watch: {
+		listId: {
+			async handler() {
+				this.list = await os.api('users/lists/show', {
+					listId: this.listId
+				});
+			},
+			immediate: true
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		},
+
+		queueUpdated(q) {
+			this.queue = q;
+		},
+
+		top() {
+			scroll(this.$el, { top: 0 });
+		},
+
+		settings() {
+			this.$router.push(`/my/lists/${this.listId}`);
+		},
+
+		async timetravel() {
+			const { canceled, result: date } = await os.dialog({
+				title: this.$ts.date,
+				input: {
+					type: 'date'
+				}
+			});
+			if (canceled) return;
+
+			this.$refs.tl.timetravel(new Date(date));
+		},
+
+		focus() {
+			(this.$refs.tl as any).focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.eqqrhokj {
+	padding: var(--margin);
+
+	> .new {
+		position: sticky;
+		top: calc(var(--stickyTop, 0px) + 16px);
+		z-index: 1000;
+		width: 100%;
+
+		> button {
+			display: block;
+			margin: var(--margin) auto 0 auto;
+			padding: 8px 16px;
+			border-radius: 32px;
+		}
+	}
+
+	> .tl {
+		background: var(--bg);
+		border-radius: var(--radius);
+		overflow: clip;
+	}
+
+	&.min-width_800px {
+		max-width: 800px;
+		margin: 0 auto;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user/clips.vue b/packages/client/src/pages/user/clips.vue
new file mode 100644
index 0000000000..2ec96d2286
--- /dev/null
+++ b/packages/client/src/pages/user/clips.vue
@@ -0,0 +1,50 @@
+<template>
+<div>
+	<MkPagination :pagination="pagination" #default="{items}" ref="list">
+		<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
+			<b>{{ item.name }}</b>
+			<div v-if="item.description" class="description">{{ item.description }}</div>
+		</MkA>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'users/clips',
+				limit: 20,
+				params: {
+					userId: this.user.id,
+				}
+			},
+		};
+	},
+
+	watch: {
+		user() {
+			this.$refs.list.reload();
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/user/follow-list.vue b/packages/client/src/pages/user/follow-list.vue
new file mode 100644
index 0000000000..fec4431419
--- /dev/null
+++ b/packages/client/src/pages/user/follow-list.vue
@@ -0,0 +1,65 @@
+<template>
+<div>
+	<MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list">
+		<div class="users _isolated">
+			<MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/>
+		</div>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkUserInfo from '@/components/user-info.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkUserInfo,
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+		type: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers',
+				limit: 20,
+				params: {
+					userId: this.user.id,
+				}
+			},
+		};
+	},
+
+	watch: {
+		type() {
+			this.$refs.list.reload();
+		},
+
+		user() {
+			this.$refs.list.reload();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-following-or-followers {
+	> .users {
+		display: grid;
+		grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+		grid-gap: var(--margin);
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue
new file mode 100644
index 0000000000..fb99cdff19
--- /dev/null
+++ b/packages/client/src/pages/user/gallery.vue
@@ -0,0 +1,56 @@
+<template>
+<div>
+	<MkPagination :pagination="pagination" #default="{items}">
+		<div class="jrnovfpt">
+			<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
+		</div>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkGalleryPostPreview,
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'users/gallery/posts',
+				limit: 6,
+				params: () => ({
+					userId: this.user.id
+				})
+			},
+		};
+	},
+
+	watch: {
+		user() {
+			this.$refs.list.reload();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.jrnovfpt {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+	grid-gap: 12px;
+	margin: var(--margin);
+}
+</style>
diff --git a/packages/client/src/pages/user/index.activity.vue b/packages/client/src/pages/user/index.activity.vue
new file mode 100644
index 0000000000..e51d6c6090
--- /dev/null
+++ b/packages/client/src/pages/user/index.activity.vue
@@ -0,0 +1,34 @@
+<template>
+<MkContainer>
+	<template #header><i class="fas fa-chart-bar" style="margin-right: 0.5em;"></i>{{ $ts.activity }}</template>
+
+	<div style="padding: 8px;">
+		<MkChart src="per-user-notes" :args="{ user, withoutAll: true }" span="day" :limit="limit" :stacked="true" :detailed="false" :aspect-ratio="6"/>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
+import MkChart from '@/components/chart.vue';
+
+export default defineComponent({
+	components: {
+		MkContainer,
+		MkChart,
+	},
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+		limit: {
+			type: Number,
+			required: false,
+			default: 40
+		}
+	},
+});
+</script>
diff --git a/packages/client/src/pages/user/index.photos.vue b/packages/client/src/pages/user/index.photos.vue
new file mode 100644
index 0000000000..4c52dceae6
--- /dev/null
+++ b/packages/client/src/pages/user/index.photos.vue
@@ -0,0 +1,107 @@
+<template>
+<MkContainer :max-height="300" :foldable="true">
+	<template #header><i class="fas fa-image" style="margin-right: 0.5em;"></i>{{ $ts.images }}</template>
+	<div class="ujigsodd">
+		<MkLoading v-if="fetching"/>
+		<div class="stream" v-if="!fetching && images.length > 0">
+			<MkA v-for="image in images"
+				class="img"
+				:to="notePage(image.note)"
+				:key="image.id"
+			>
+				<ImgWithBlurhash :hash="image.blurhash" :src="thumbnail(image.file)" :alt="image.name" :title="image.name"/>
+			</MkA>
+		</div>
+		<p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+import notePage from '@/filters/note';
+import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
+import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
+
+export default defineComponent({
+	components: {
+		MkContainer,
+		ImgWithBlurhash,
+	},
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+	},
+	data() {
+		return {
+			fetching: true,
+			images: [],
+		};
+	},
+	mounted() {
+		const image = [
+			'image/jpeg',
+			'image/png',
+			'image/gif',
+			'image/apng',
+			'image/vnd.mozilla.apng',
+		];
+		os.api('users/notes', {
+			userId: this.user.id,
+			fileType: image,
+			excludeNsfw: this.$store.state.nsfw !== 'ignore',
+			limit: 10,
+		}).then(notes => {
+			for (const note of notes) {
+				for (const file of note.files) {
+					this.images.push({
+						note,
+						file
+					});
+				}
+			}
+			this.fetching = false;
+		});
+	},
+	methods: {
+		thumbnail(image: any): string {
+			return this.$store.state.disableShowingAnimatedImages
+				? getStaticImageUrl(image.thumbnailUrl)
+				: image.thumbnailUrl;
+		},
+		notePage
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.ujigsodd {
+	padding: 8px;
+
+	> .stream {
+		display: grid;
+		grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+		grid-gap: 6px;
+
+		> .img {
+			height: 128px;
+			border-radius: 6px;
+			overflow: clip;
+		}
+	}
+
+	> .empty {
+		margin: 0;
+		padding: 16px;
+		text-align: center;
+
+		> i {
+			margin-right: 4px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user/index.timeline.vue b/packages/client/src/pages/user/index.timeline.vue
new file mode 100644
index 0000000000..eff38ec3c8
--- /dev/null
+++ b/packages/client/src/pages/user/index.timeline.vue
@@ -0,0 +1,68 @@
+<template>
+<div class="yrzkoczt" v-sticky-container>
+	<MkTab v-model="with_" class="tab">
+		<option :value="null">{{ $ts.notes }}</option>
+		<option value="replies">{{ $ts.notesAndReplies }}</option>
+		<option value="files">{{ $ts.withFiles }}</option>
+	</MkTab>
+	<XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XNotes from '@/components/notes.vue';
+import MkTab from '@/components/tab.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XNotes,
+		MkTab,
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true,
+		},
+	},
+
+	watch: {
+		user() {
+			this.$refs.timeline.reload();
+		},
+
+		with_() {
+			this.$refs.timeline.reload();
+		},
+	},
+
+	data() {
+		return {
+			date: null,
+			with_: null,
+			pagination: {
+				endpoint: 'users/notes',
+				limit: 10,
+				params: init => ({
+					userId: this.user.id,
+					includeReplies: this.with_ === 'replies',
+					withFiles: this.with_ === 'files',
+					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
+				})
+			}
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.yrzkoczt {
+	> .tab {
+		margin: calc(var(--margin) / 2) 0;
+		padding: calc(var(--margin) / 2) 0;
+		background: var(--bg);
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
new file mode 100644
index 0000000000..d2531c0d1b
--- /dev/null
+++ b/packages/client/src/pages/user/index.vue
@@ -0,0 +1,829 @@
+<template>
+<div>
+<transition name="fade" mode="out-in">
+	<div class="ftskorzw wide" v-if="user && narrow === false">
+		<MkRemoteCaution v-if="user.host != null" :href="user.url"/>
+
+		<div class="banner-container" :style="style">
+			<div class="banner" ref="banner" :style="style"></div>
+		</div>
+		<div class="contents">
+			<div class="side _forceContainerFull_">
+				<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
+				<div class="name">
+					<MkUserName :user="user" :nowrap="false" class="name"/>
+					<MkAcct :user="user" :detail="true" class="acct"/>
+				</div>
+				<div class="followed" v-if="$i && $i.id != user.id && user.isFollowed"><span>{{ $ts.followsYou }}</span></div>
+				<div class="status">
+					<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
+						<b>{{ number(user.notesCount) }}</b>
+						<span>{{ $ts.notes }}</span>
+					</MkA>
+					<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
+						<b>{{ number(user.followingCount) }}</b>
+						<span>{{ $ts.following }}</span>
+					</MkA>
+					<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
+						<b>{{ number(user.followersCount) }}</b>
+						<span>{{ $ts.followers }}</span>
+					</MkA>
+				</div>
+				<div class="description">
+					<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+					<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
+				</div>
+				<div class="fields system">
+					<dl class="field" v-if="user.location">
+						<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
+						<dd class="value">{{ user.location }}</dd>
+					</dl>
+					<dl class="field" v-if="user.birthday">
+						<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
+						<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+					</dl>
+					<dl class="field">
+						<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
+						<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
+					</dl>
+				</div>
+				<div class="fields" v-if="user.fields.length > 0">
+					<dl class="field" v-for="(field, i) in user.fields" :key="i">
+						<dt class="name">
+							<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+						</dt>
+						<dd class="value">
+							<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
+						</dd>
+					</dl>
+				</div>
+				<XActivity :user="user" :key="user.id" class="_gap"/>
+				<XPhotos :user="user" :key="user.id" class="_gap"/>
+			</div>
+			<div class="main">
+				<div class="actions">
+					<button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
+					<MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
+				</div>
+				<template v-if="page === 'index'">
+					<div v-if="user.pinnedNotes.length > 0" class="_gap">
+						<XNote v-for="note in user.pinnedNotes" class="note _gap" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
+					</div>
+					<div class="_gap">
+						<XUserTimeline :user="user"/>
+					</div>
+				</template>
+				<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_gap"/>
+				<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_gap"/>
+				<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
+				<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
+			</div>
+		</div>
+	</div>
+	<MkSpacer v-else-if="user && narrow === true" :content-max="800">
+		<div class="ftskorzw narrow" v-size="{ max: [500] }">
+			<!-- TODO -->
+			<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
+			<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
+
+			<div class="profile">
+				<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
+
+				<div class="_block main" :key="user.id">
+					<div class="banner-container" :style="style">
+						<div class="banner" ref="banner" :style="style"></div>
+						<div class="fade"></div>
+						<div class="title">
+							<MkUserName class="name" :user="user" :nowrap="true"/>
+							<div class="bottom">
+								<span class="username"><MkAcct :user="user" :detail="true" /></span>
+								<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+								<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+								<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+								<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+							</div>
+						</div>
+						<span class="followed" v-if="$i && $i.id != user.id && user.isFollowed">{{ $ts.followsYou }}</span>
+						<div class="actions" v-if="$i">
+							<button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
+							<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+						</div>
+					</div>
+					<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
+					<div class="title">
+						<MkUserName :user="user" :nowrap="false" class="name"/>
+						<div class="bottom">
+							<span class="username"><MkAcct :user="user" :detail="true" /></span>
+							<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+							<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+							<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+							<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+						</div>
+					</div>
+					<div class="description">
+						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+						<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
+					</div>
+					<div class="fields system">
+						<dl class="field" v-if="user.location">
+							<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
+							<dd class="value">{{ user.location }}</dd>
+						</dl>
+						<dl class="field" v-if="user.birthday">
+							<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
+							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+						</dl>
+						<dl class="field">
+							<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
+							<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
+						</dl>
+					</div>
+					<div class="fields" v-if="user.fields.length > 0">
+						<dl class="field" v-for="(field, i) in user.fields" :key="i">
+							<dt class="name">
+								<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+							</dt>
+							<dd class="value">
+								<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
+							</dd>
+						</dl>
+					</div>
+					<div class="status">
+						<MkA :to="userPage(user)" :class="{ active: page === 'index' }" v-click-anime>
+							<b>{{ number(user.notesCount) }}</b>
+							<span>{{ $ts.notes }}</span>
+						</MkA>
+						<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }" v-click-anime>
+							<b>{{ number(user.followingCount) }}</b>
+							<span>{{ $ts.following }}</span>
+						</MkA>
+						<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }" v-click-anime>
+							<b>{{ number(user.followersCount) }}</b>
+							<span>{{ $ts.followers }}</span>
+						</MkA>
+					</div>
+				</div>
+			</div>
+
+			<div class="contents">
+				<template v-if="page === 'index'">
+					<div>
+						<div v-if="user.pinnedNotes.length > 0" class="_gap">
+							<XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
+						</div>
+						<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
+						<XPhotos :user="user" :key="user.id"/>
+						<XActivity :user="user" :key="user.id" style="margin-top: var(--margin);"/>
+					</div>
+					<div>
+						<XUserTimeline :user="user"/>
+					</div>
+				</template>
+				<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
+				<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
+				<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
+				<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
+				<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
+				<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
+			</div>
+		</div>
+	</MkSpacer>
+	<MkError v-else-if="error" @retry="fetch()"/>
+	<MkLoading v-else/>
+</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, computed } from 'vue';
+import * as age from 's-age';
+import XUserTimeline from './index.timeline.vue';
+import XNote from '@/components/note.vue';
+import MkFollowButton from '@/components/follow-button.vue';
+import MkContainer from '@/components/ui/container.vue';
+import MkFolder from '@/components/ui/folder.vue';
+import MkRemoteCaution from '@/components/remote-caution.vue';
+import MkTab from '@/components/tab.vue';
+import MkInfo from '@/components/ui/info.vue';
+import Progress from '@/scripts/loading';
+import * as Acct from 'misskey-js/built/acct';
+import { getScrollPosition } from '@/scripts/scroll';
+import { getUserMenu } from '@/scripts/get-user-menu';
+import number from '@/filters/number';
+import { userPage, acct as getAcct } from '@/filters/user';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XUserTimeline,
+		XNote,
+		MkFollowButton,
+		MkContainer,
+		MkRemoteCaution,
+		MkFolder,
+		MkTab,
+		MkInfo,
+		XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
+		XReactions: defineAsyncComponent(() => import('./reactions.vue')),
+		XClips: defineAsyncComponent(() => import('./clips.vue')),
+		XPages: defineAsyncComponent(() => import('./pages.vue')),
+		XGallery: defineAsyncComponent(() => import('./gallery.vue')),
+		XPhotos: defineAsyncComponent(() => import('./index.photos.vue')),
+		XActivity: defineAsyncComponent(() => import('./index.activity.vue')),
+	},
+
+	props: {
+		acct: {
+			type: String,
+			required: true
+		},
+		page: {
+			type: String,
+			required: false,
+			default: 'index'
+		}
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.user ? {
+				icon: 'fas fa-user',
+				title: this.user.name ? `${this.user.name} (@${this.user.username})` : `@${this.user.username}`,
+				subtitle: `@${getAcct(this.user)}`,
+				userName: this.user,
+				avatar: this.user,
+				path: `/@${this.user.username}`,
+				share: {
+					title: this.user.name,
+				},
+				bg: 'var(--bg)',
+				tabs: [{
+					active: this.page === 'index',
+					title: this.$ts.overview,
+					icon: 'fas fa-home',
+					onClick: () => { this.$router.push('/@' + getAcct(this.user)); },
+				}, ...(this.$i && (this.$i.id === this.user.id)) || this.user.publicReactions ? [{
+					active: this.page === 'reactions',
+					title: this.$ts.reaction,
+					icon: 'fas fa-laugh',
+					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/reactions'); },
+				}] : [], {
+					active: this.page === 'clips',
+					title: this.$ts.clips,
+					icon: 'fas fa-paperclip',
+					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/clips'); },
+				}, {
+					active: this.page === 'pages',
+					title: this.$ts.pages,
+					icon: 'fas fa-file-alt',
+					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/pages'); },
+				}, {
+					active: this.page === 'gallery',
+					title: this.$ts.gallery,
+					icon: 'fas fa-icons',
+					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/gallery'); },
+				}],
+			} : null),
+			user: null,
+			error: null,
+			parallaxAnimationId: null,
+			narrow: null,
+		};
+	},
+
+	computed: {
+		style(): any {
+			if (this.user.bannerUrl == null) return {};
+			return {
+				backgroundImage: `url(${ this.user.bannerUrl })`
+			};
+		},
+
+		age(): number {
+			return age(this.user.birthday);
+		}
+	},
+
+	watch: {
+		acct: 'fetch'
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	mounted() {
+		window.requestAnimationFrame(this.parallaxLoop);
+		this.narrow = true//this.$el.clientWidth < 1000;
+	},
+
+	beforeUnmount() {
+		window.cancelAnimationFrame(this.parallaxAnimationId);
+	},
+
+	methods: {
+		getAcct,
+
+		fetch() {
+			if (this.acct == null) return;
+			this.user = null;
+			Progress.start();
+			os.api('users/show', Acct.parse(this.acct)).then(user => {
+				this.user = user;
+			}).catch(e => {
+				this.error = e;
+			}).finally(() => {
+				Progress.done();
+			});
+		},
+
+		menu(ev) {
+			os.popupMenu(getUserMenu(this.user), ev.currentTarget || ev.target);
+		},
+
+		parallaxLoop() {
+			this.parallaxAnimationId = window.requestAnimationFrame(this.parallaxLoop);
+			this.parallax();
+		},
+
+		parallax() {
+			const banner = this.$refs.banner as any;
+			if (banner == null) return;
+
+			const top = getScrollPosition(this.$el);
+
+			if (top < 0) return;
+
+			const z = 1.75; // 奥行き(小さいほど奥)
+			const pos = -(top / z);
+			banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
+		},
+
+		pinnedNoteUpdated(oldValue, newValue) {
+			const i = this.user.pinnedNotes.findIndex(n => n === oldValue);
+			this.user.pinnedNotes[i] = newValue;
+		},
+
+		number,
+
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+	transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+	opacity: 0;
+}
+
+.ftskorzw.wide {
+
+	> .banner-container {
+		position: relative;
+		height: 300px;
+		overflow: hidden;
+		background-size: cover;
+		background-position: center;
+
+		> .banner {
+			height: 100%;
+			background-color: #4c5e6d;
+			background-size: cover;
+			background-position: center;
+			box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
+			will-change: background-position;
+		}
+	}
+
+	> .contents {
+		display: flex;
+		padding: 16px;
+
+		> .side {
+			width: 360px;
+
+			> .avatar {
+				display: block;
+				width: 180px;
+				height: 180px;
+				margin: -130px auto 0 auto;
+			}
+
+			> .name {
+				padding: 16px 0px 20px 0;
+				text-align: center;
+
+				> .name {
+					display: block;
+					font-size: 1.75em;
+					font-weight: bold;
+				}
+			}
+
+			> .followed {
+				text-align: center;
+
+				> span {
+					display: inline-block;
+					font-size: 80%;
+					padding: 8px 12px;
+					margin-bottom: 20px;
+					border: solid 0.5px var(--divider);
+					border-radius: 999px;
+				}
+			}
+
+			> .status {
+				display: flex;
+				padding: 20px 16px;
+				border-top: solid 0.5px var(--divider);
+				font-size: 90%;
+
+				> a {
+					flex: 1;
+					text-align: center;
+
+					&.active {
+						color: var(--accent);
+					}
+
+					&:hover {
+						text-decoration: none;
+					}
+
+					> b {
+						display: block;
+						line-height: 16px;
+					}
+
+					> span {
+						font-size: 75%;
+					}
+				}
+			}
+
+			> .description {
+				padding: 20px 16px;
+				border-top: solid 0.5px var(--divider);
+				font-size: 90%;
+			}
+
+			> .fields {
+				padding: 20px 16px;
+				border-top: solid 0.5px var(--divider);
+				font-size: 90%;
+
+				> .field {
+					display: flex;
+					padding: 0;
+					margin: 0;
+					align-items: center;
+
+					&:not(:last-child) {
+						margin-bottom: 8px;
+					}
+
+					> .name {
+						width: 30%;
+						overflow: hidden;
+						white-space: nowrap;
+						text-overflow: ellipsis;
+						font-weight: bold;
+					}
+
+					> .value {
+						width: 70%;
+						overflow: hidden;
+						white-space: nowrap;
+						text-overflow: ellipsis;
+						margin: 0;
+					}
+				}
+			}
+		}
+
+		> .main {
+			flex: 1;
+			margin-left: var(--margin);
+			min-width: 0;
+
+			> .nav {
+				display: flex;
+				align-items: center;
+				margin-top: var(--margin);
+				//font-size: 120%;
+				font-weight: bold;
+
+				> .link {
+					display: inline-block;
+					padding: 15px 24px 12px 24px;
+					text-align: center;
+					border-bottom: solid 3px transparent;
+
+					&:hover {
+						text-decoration: none;
+					}
+
+					&.active {
+						color: var(--accent);
+						border-bottom-color: var(--accent);
+					}
+
+					&:not(.active):hover {
+						color: var(--fgHighlighted);
+					}
+
+					> .icon {
+						margin-right: 6px;
+					}
+				}
+
+				> .actions {
+					display: flex;
+					align-items: center;
+					margin-left: auto;
+
+					> .menu {
+						padding: 12px 16px;
+					}
+				}
+			}
+		}
+	}
+}
+
+.ftskorzw.narrow {
+	box-sizing: border-box;
+	overflow: clip;
+	background: var(--bg);
+
+	> .punished {
+		font-size: 0.8em;
+		padding: 16px;
+	}
+
+	> .profile {
+
+		> .main {
+			position: relative;
+			overflow: hidden;
+
+			> .banner-container {
+				position: relative;
+				height: 250px;
+				overflow: hidden;
+				background-size: cover;
+				background-position: center;
+
+				> .banner {
+					height: 100%;
+					background-color: #4c5e6d;
+					background-size: cover;
+					background-position: center;
+					box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
+					will-change: background-position;
+				}
+
+				> .fade {
+					position: absolute;
+					bottom: 0;
+					left: 0;
+					width: 100%;
+					height: 78px;
+					background: linear-gradient(transparent, rgba(#000, 0.7));
+				}
+
+				> .followed {
+					position: absolute;
+					top: 12px;
+					left: 12px;
+					padding: 4px 8px;
+					color: #fff;
+					background: rgba(0, 0, 0, 0.7);
+					font-size: 0.7em;
+					border-radius: 6px;
+				}
+
+				> .actions {
+					position: absolute;
+					top: 12px;
+					right: 12px;
+					-webkit-backdrop-filter: var(--blur, blur(8px));
+					backdrop-filter: var(--blur, blur(8px));
+					background: rgba(0, 0, 0, 0.2);
+					padding: 8px;
+					border-radius: 24px;
+
+					> .menu {
+						vertical-align: bottom;
+						height: 31px;
+						width: 31px;
+						color: #fff;
+						text-shadow: 0 0 8px #000;
+						font-size: 16px;
+					}
+
+					> .koudoku {
+						margin-left: 4px;
+						vertical-align: bottom;
+					}
+				}
+
+				> .title {
+					position: absolute;
+					bottom: 0;
+					left: 0;
+					width: 100%;
+					padding: 0 0 8px 154px;
+					box-sizing: border-box;
+					color: #fff;
+
+					> .name {
+						display: block;
+						margin: 0;
+						line-height: 32px;
+						font-weight: bold;
+						font-size: 1.8em;
+						text-shadow: 0 0 8px #000;
+					}
+
+					> .bottom {
+						> * {
+							display: inline-block;
+							margin-right: 16px;
+							line-height: 20px;
+							opacity: 0.8;
+
+							&.username {
+								font-weight: bold;
+							}
+						}
+					}
+				}
+			}
+
+			> .title {
+				display: none;
+				text-align: center;
+				padding: 50px 8px 16px 8px;
+				font-weight: bold;
+				border-bottom: solid 0.5px var(--divider);
+
+				> .bottom {
+					> * {
+						display: inline-block;
+						margin-right: 8px;
+						opacity: 0.8;
+					}
+				}
+			}
+
+			> .avatar {
+				display: block;
+				position: absolute;
+				top: 170px;
+				left: 16px;
+				z-index: 2;
+				width: 120px;
+				height: 120px;
+				box-shadow: 1px 1px 3px rgba(#000, 0.2);
+			}
+
+			> .description {
+				padding: 24px 24px 24px 154px;
+				font-size: 0.95em;
+
+				> .empty {
+					margin: 0;
+					opacity: 0.5;
+				}
+			}
+
+			> .fields {
+				padding: 24px;
+				font-size: 0.9em;
+				border-top: solid 0.5px var(--divider);
+
+				> .field {
+					display: flex;
+					padding: 0;
+					margin: 0;
+					align-items: center;
+
+					&:not(:last-child) {
+						margin-bottom: 8px;
+					}
+
+					> .name {
+						width: 30%;
+						overflow: hidden;
+						white-space: nowrap;
+						text-overflow: ellipsis;
+						font-weight: bold;
+						text-align: center;
+					}
+
+					> .value {
+						width: 70%;
+						overflow: hidden;
+						white-space: nowrap;
+						text-overflow: ellipsis;
+						margin: 0;
+					}
+				}
+
+				&.system > .field > .name {
+				}
+			}
+
+			> .status {
+				display: flex;
+				padding: 24px;
+				border-top: solid 0.5px var(--divider);
+
+				> a {
+					flex: 1;
+					text-align: center;
+
+					&.active {
+						color: var(--accent);
+					}
+
+					&:hover {
+						text-decoration: none;
+					}
+
+					> b {
+						display: block;
+						line-height: 16px;
+					}
+
+					> span {
+						font-size: 70%;
+					}
+				}
+			}
+		}
+	}
+
+	> .contents {
+		> .content {
+			margin-bottom: var(--margin);
+		}
+	}
+
+	&.max-width_500px {
+		> .profile > .main {
+			> .banner-container {
+				height: 140px;
+
+				> .fade {
+					display: none;
+				}
+
+				> .title {
+					display: none;
+				}
+			}
+
+			> .title {
+				display: block;
+			}
+
+			> .avatar {
+				top: 90px;
+				left: 0;
+				right: 0;
+				width: 92px;
+				height: 92px;
+				margin: auto;
+			}
+
+			> .description {
+				padding: 16px;
+				text-align: center;
+			}
+
+			> .fields {
+				padding: 16px;
+			}
+
+			> .status {
+				padding: 16px;
+			}
+		}
+
+		> .contents {
+			> .nav {
+				font-size: 80%;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/user/pages.vue b/packages/client/src/pages/user/pages.vue
new file mode 100644
index 0000000000..0bf925d7d5
--- /dev/null
+++ b/packages/client/src/pages/user/pages.vue
@@ -0,0 +1,49 @@
+<template>
+<div>
+	<MkPagination :pagination="pagination" #default="{items}" ref="list">
+		<MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagePreview from '@/components/page-preview.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkPagePreview,
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'users/pages',
+				limit: 20,
+				params: {
+					userId: this.user.id,
+				}
+			},
+		};
+	},
+
+	watch: {
+		user() {
+			this.$refs.list.reload();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/packages/client/src/pages/user/reactions.vue b/packages/client/src/pages/user/reactions.vue
new file mode 100644
index 0000000000..3ca3b2aac8
--- /dev/null
+++ b/packages/client/src/pages/user/reactions.vue
@@ -0,0 +1,81 @@
+<template>
+<div>
+	<MkPagination :pagination="pagination" #default="{items}" ref="list">
+		<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb">
+			<div class="header">
+				<MkAvatar class="avatar" :user="user"/>
+				<MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/>
+				<MkTime :time="item.createdAt" class="createdAt"/>
+			</div>
+			<MkNote :note="item.note" @update:note="updated(note, $event)" :key="item.id"/>
+		</div>
+	</MkPagination>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkNote from '@/components/note.vue';
+import MkReactionIcon from '@/components/reaction-icon.vue';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkNote,
+		MkReactionIcon,
+	},
+
+	props: {
+		user: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'users/reactions',
+				limit: 20,
+				params: {
+					userId: this.user.id,
+				}
+			},
+		};
+	},
+
+	watch: {
+		user() {
+			this.$refs.list.reload();
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.afdcfbfb {
+	> .header {
+		display: flex;
+		align-items: center;
+		padding: 8px 16px;
+		margin-bottom: 8px;
+		border-bottom: solid 2px var(--divider);
+
+		> .avatar {
+			width: 24px;
+			height: 24px;
+			margin-right: 8px;
+		}
+
+		> .reaction {
+			width: 32px;
+			height: 32px;
+		}
+
+		> .createdAt {
+			margin-left: auto;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/v.vue b/packages/client/src/pages/v.vue
new file mode 100644
index 0000000000..3b1bb20861
--- /dev/null
+++ b/packages/client/src/pages/v.vue
@@ -0,0 +1,29 @@
+<template>
+<div>
+	<section class="_section">
+		<div class="_content" style="text-align: center;">
+			<img src="/static-assets/icons/512.png" alt="" style="display: block; width: 100px; margin: 0 auto; border-radius: 16px;"/>
+			<div style="margin-top: 0.75em;">Misskey</div>
+			<div style="opacity: 0.5;">v{{ version }}</div>
+		</div>
+	</section>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { version } from '@/config';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: 'Misskey',
+				icon: null
+			},
+			version,
+		}
+	},
+});
+</script>
diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue
new file mode 100644
index 0000000000..2e0c520bc6
--- /dev/null
+++ b/packages/client/src/pages/welcome.entrance.a.vue
@@ -0,0 +1,320 @@
+<template>
+<div class="rsqzvsbo" v-if="meta">
+	<div class="top">
+		<MkFeaturedPhotos class="bg"/>
+		<XTimeline class="tl"/>
+		<div class="shape1"></div>
+		<div class="shape2"></div>
+		<img src="/client-assets/misskey.svg" class="misskey"/>
+		<div class="emojis">
+			<MkEmoji :normal="true" :no-style="true" emoji="👍"/>
+			<MkEmoji :normal="true" :no-style="true" emoji="❤"/>
+			<MkEmoji :normal="true" :no-style="true" emoji="😆"/>
+			<MkEmoji :normal="true" :no-style="true" emoji="🎉"/>
+			<MkEmoji :normal="true" :no-style="true" emoji="🍮"/>
+		</div>
+		<div class="main _panel">
+			<div class="bg">
+				<div class="fade"></div>
+			</div>
+			<div class="fg">
+				<h1>
+					<!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に -->
+					<!-- <img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> -->
+					<span class="text">{{ instanceName }}</span>
+				</h1>
+				<div class="about">
+					<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
+				</div>
+				<div class="action">
+					<MkButton @click="signup()" inline gradate data-cy-signup style="margin-right: 12px;">{{ $ts.signup }}</MkButton>
+					<MkButton @click="signin()" inline data-cy-signin>{{ $ts.login }}</MkButton>
+				</div>
+				<div class="status" v-if="onlineUsersCount && stats">
+					<div>
+						<I18n :src="$ts.nUsers" text-tag="span" class="users">
+							<template #n><b>{{ number(stats.originalUsersCount) }}</b></template>
+						</I18n>
+						<I18n :src="$ts.nNotes" text-tag="span" class="notes">
+							<template #n><b>{{ number(stats.originalNotesCount) }}</b></template>
+						</I18n>
+					</div>
+					<I18n :src="$ts.onlineUsersCount" text-tag="span" class="online">
+						<template #n><b>{{ onlineUsersCount }}</b></template>
+					</I18n>
+				</div>
+				<button class="_button _acrylic menu" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { toUnicode } from 'punycode/';
+import XSigninDialog from '@/components/signin-dialog.vue';
+import XSignupDialog from '@/components/signup-dialog.vue';
+import MkButton from '@/components/ui/button.vue';
+import XNote from '@/components/note.vue';
+import MkFeaturedPhotos from '@/components/featured-photos.vue';
+import XTimeline from './welcome.timeline.vue';
+import { host, instanceName } from '@/config';
+import * as os from '@/os';
+import number from '@/filters/number';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		XNote,
+		MkFeaturedPhotos,
+		XTimeline,
+	},
+
+	data() {
+		return {
+			host: toUnicode(host),
+			instanceName,
+			meta: null,
+			stats: null,
+			tags: [],
+			onlineUsersCount: null,
+		};
+	},
+
+	created() {
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+
+		os.api('stats').then(stats => {
+			this.stats = stats;
+		});
+
+		os.api('get-online-users-count').then(res => {
+			this.onlineUsersCount = res.count;
+		});
+
+		os.api('hashtags/list', {
+			sort: '+mentionedLocalUsers',
+			limit: 8
+		}).then(tags => {
+			this.tags = tags;
+		});
+	},
+
+	methods: {
+		signin() {
+			os.popup(XSigninDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		signup() {
+			os.popup(XSignupDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		showMenu(ev) {
+			os.popupMenu([{
+				text: this.$t('aboutX', { x: instanceName }),
+				icon: 'fas fa-info-circle',
+				action: () => {
+					os.pageWindow('/about');
+				}
+			}, {
+				text: this.$ts.aboutMisskey,
+				icon: 'fas fa-info-circle',
+				action: () => {
+					os.pageWindow('/about-misskey');
+				}
+			}, null, {
+				text: this.$ts.help,
+				icon: 'fas fa-question-circle',
+				action: () => {
+					window.open(`https://misskey-hub.net/help.md`, '_blank');
+				}
+			}], ev.currentTarget || ev.target);
+		},
+
+		number
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rsqzvsbo {
+	> .top {
+		display: flex;
+		text-align: center;
+		min-height: 100vh;
+		box-sizing: border-box;
+		padding: 16px;
+
+		> .bg {
+			position: absolute;
+			top: 0;
+			right: 0;
+			width: 80%; // 100%からshapeの幅を引いている
+			height: 100%;
+		}
+
+		> .tl {
+			position: absolute;
+			top: 0;
+			bottom: 0;
+			right: 64px;
+			margin: auto;
+			width: 500px;
+			height: calc(100% - 128px);
+			overflow: hidden;
+			-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%);
+			mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%);
+
+			@media (max-width: 1200px) {
+				display: none;
+			}
+		}
+
+		> .shape1 {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			background: var(--accent);
+			clip-path: polygon(0% 0%, 45% 0%, 20% 100%, 0% 100%);
+		}
+		> .shape2 {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			background: var(--accent);
+			clip-path: polygon(0% 0%, 25% 0%, 35% 100%, 0% 100%);
+			opacity: 0.5;
+		}
+
+		> .misskey {
+			position: absolute;
+			top: 42px;
+			left: 42px;
+			width: 160px;
+
+			@media (max-width: 450px) {
+				width: 130px;
+			}
+		}
+
+		> .emojis {
+			position: absolute;
+			bottom: 32px;
+			left: 35px;
+
+			> * {
+				margin-right: 8px;
+			}
+
+			@media (max-width: 1200px) {
+				display: none;
+			}
+		}
+
+		> .main {
+			position: relative;
+			width: min(480px, 100%);
+			margin: auto auto auto 128px;
+			box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
+
+			@media (max-width: 1200px) {
+				margin: auto;
+			}
+
+			> .bg {
+				position: absolute;
+				top: 0;
+				left: 0;
+				width: 100%;
+				height: 128px;
+				background-position: center;
+				background-size: cover;
+				opacity: 0.75;
+
+				> .fade {
+					position: absolute;
+					bottom: 0;
+					left: 0;
+					width: 100%;
+					height: 128px;
+					background: linear-gradient(0deg, var(--panel), var(--X15));
+				}
+			}
+
+			> .fg {
+				position: relative;
+				z-index: 1;
+
+				> h1 {
+					display: block;
+					margin: 0;
+					padding: 32px 32px 24px 32px;
+					font-size: 1.5em;
+
+					> .logo {
+						vertical-align: bottom;
+						max-height: 120px;
+						max-width: min(100%, 300px);
+					}
+				}
+
+				> .about {
+					padding: 0 32px;
+				}
+
+				> .action {
+					padding: 32px;
+
+					> * {
+						line-height: 28px;
+					}
+				}
+
+				> .status {
+					border-top: solid 0.5px var(--divider);
+					padding: 32px;
+					font-size: 90%;
+
+					> div {
+						> span:not(:last-child) {
+							padding-right: 1em;
+							margin-right: 1em;
+							border-right: solid 0.5px var(--divider);
+						}
+					}
+
+					> .online {
+						::v-deep(b) {
+							color: #41b781;
+						}
+
+						::v-deep(span) {
+							opacity: 0.7;
+						}
+					}
+				}
+
+				> .menu {
+					position: absolute;
+					top: 16px;
+					right: 16px;
+					width: 32px;
+					height: 32px;
+					border-radius: 8px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/welcome.entrance.b.vue b/packages/client/src/pages/welcome.entrance.b.vue
new file mode 100644
index 0000000000..efb8b09360
--- /dev/null
+++ b/packages/client/src/pages/welcome.entrance.b.vue
@@ -0,0 +1,236 @@
+<template>
+<div class="rsqzvsbo" v-if="meta">
+	<div class="top">
+		<MkFeaturedPhotos class="bg"/>
+		<XTimeline class="tl"/>
+		<div class="shape"></div>
+		<div class="main">
+			<h1>
+				<img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span>
+			</h1>
+			<div class="about">
+				<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
+			</div>
+			<div class="action">
+				<MkButton class="signup" @click="signup()" inline gradate>{{ $ts.signup }}</MkButton>
+				<MkButton class="signin" @click="signin()" inline>{{ $ts.login }}</MkButton>
+			</div>
+			<div class="status" v-if="onlineUsersCount && stats">
+				<div>
+					<I18n :src="$ts.nUsers" text-tag="span" class="users">
+						<template #n><b>{{ number(stats.originalUsersCount) }}</b></template>
+					</I18n>
+					<I18n :src="$ts.nNotes" text-tag="span" class="notes">
+						<template #n><b>{{ number(stats.originalNotesCount) }}</b></template>
+					</I18n>
+				</div>
+				<I18n :src="$ts.onlineUsersCount" text-tag="span" class="online">
+					<template #n><b>{{ onlineUsersCount }}</b></template>
+				</I18n>
+			</div>
+		</div>
+		<img src="/client-assets/misskey.svg" class="misskey"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { toUnicode } from 'punycode/';
+import XSigninDialog from '@/components/signin-dialog.vue';
+import XSignupDialog from '@/components/signup-dialog.vue';
+import MkButton from '@/components/ui/button.vue';
+import XNote from '@/components/note.vue';
+import MkFeaturedPhotos from '@/components/featured-photos.vue';
+import XTimeline from './welcome.timeline.vue';
+import { host, instanceName } from '@/config';
+import * as os from '@/os';
+import number from '@/filters/number';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		XNote,
+		XTimeline,
+		MkFeaturedPhotos,
+	},
+
+	data() {
+		return {
+			host: toUnicode(host),
+			instanceName,
+			meta: null,
+			stats: null,
+			tags: [],
+			onlineUsersCount: null,
+		};
+	},
+
+	created() {
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+
+		os.api('stats').then(stats => {
+			this.stats = stats;
+		});
+
+		os.api('get-online-users-count').then(res => {
+			this.onlineUsersCount = res.count;
+		});
+
+		os.api('hashtags/list', {
+			sort: '+mentionedLocalUsers',
+			limit: 8
+		}).then(tags => {
+			this.tags = tags;
+		});
+	},
+
+	methods: {
+		signin() {
+			os.popup(XSigninDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		signup() {
+			os.popup(XSignupDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		showMenu(ev) {
+			os.popupMenu([{
+				text: this.$t('aboutX', { x: instanceName }),
+				icon: 'fas fa-info-circle',
+				action: () => {
+					os.pageWindow('/about');
+				}
+			}, {
+				text: this.$ts.aboutMisskey,
+				icon: 'fas fa-info-circle',
+				action: () => {
+					os.pageWindow('/about-misskey');
+				}
+			}, null, {
+				text: this.$ts.help,
+				icon: 'fas fa-question-circle',
+				action: () => {
+					window.open(`https://misskey-hub.net/help.md`, '_blank');
+				}
+			}], ev.currentTarget || ev.target);
+		},
+
+		number
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rsqzvsbo {
+	> .top {
+		min-height: 100vh;
+		box-sizing: border-box;
+
+		> .bg {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+		}
+
+		> .tl {
+			position: absolute;
+			top: 0;
+			bottom: 0;
+			right: 64px;
+			margin: auto;
+			width: 500px;
+			height: calc(100% - 128px);
+			overflow: hidden;
+			-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%);
+			mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%);
+		}
+
+		> .shape {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			background: var(--accent);
+			clip-path: polygon(0% 0%, 40% 0%, 22% 100%, 0% 100%);
+		}
+
+		> .misskey {
+			position: absolute;
+			bottom: 64px;
+			left: 64px;
+			width: 160px;
+		}
+
+		> .main {
+			position: relative;
+			width: min(450px, 100%);
+			padding: 64px;
+			color: #fff;
+			font-size: 1.1em;
+
+			@media (max-width: 1200px) {
+				margin: auto;
+			}
+
+			> h1 {
+				display: block;
+				margin: 0 0 32px 0;
+				padding: 0;
+
+				> .logo {
+					vertical-align: bottom;
+					max-height: 100px;
+				}
+			}
+
+			> .about {
+				padding: 0;
+			}
+
+			> .action {
+				margin: 32px 0;
+
+				> * {
+					line-height: 32px;
+				}
+
+				> .signup {
+					background: var(--panel);
+					color: var(--fg);
+				}
+
+				> .signin {
+					background: var(--accent);
+					color: inherit;
+				}
+			}
+
+			> .status {
+				margin: 32px 0;
+				border-top: solid 1px rgba(255, 255, 255, 0.5);
+				font-size: 90%;
+
+				> div {
+					padding: 16px 0;
+
+					> span:not(:last-child) {
+						padding-right: 1em;
+						margin-right: 1em;
+						border-right: solid 1px rgba(255, 255, 255, 0.5);
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/welcome.entrance.c.vue b/packages/client/src/pages/welcome.entrance.c.vue
new file mode 100644
index 0000000000..2b0ff7a31c
--- /dev/null
+++ b/packages/client/src/pages/welcome.entrance.c.vue
@@ -0,0 +1,305 @@
+<template>
+<div class="rsqzvsbo" v-if="meta">
+	<div class="top">
+		<MkFeaturedPhotos class="bg"/>
+		<div class="fade"></div>
+		<div class="emojis">
+			<MkEmoji :normal="true" :no-style="true" emoji="👍"/>
+			<MkEmoji :normal="true" :no-style="true" emoji="❤"/>
+			<MkEmoji :normal="true" :no-style="true" emoji="😆"/>
+			<MkEmoji :normal="true" :no-style="true" emoji="🎉"/>
+			<MkEmoji :normal="true" :no-style="true" emoji="🍮"/>
+		</div>
+		<div class="main">
+			<img src="/client-assets/misskey.svg" class="misskey"/>
+			<div class="form _panel">
+				<div class="bg">
+					<div class="fade"></div>
+				</div>
+				<div class="fg">
+					<h1>
+						<img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span>
+					</h1>
+					<div class="about">
+						<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
+					</div>
+					<div class="action">
+						<MkButton @click="signup()" inline gradate>{{ $ts.signup }}</MkButton>
+						<MkButton @click="signin()" inline>{{ $ts.login }}</MkButton>
+					</div>
+					<div class="status" v-if="onlineUsersCount && stats">
+						<div>
+							<I18n :src="$ts.nUsers" text-tag="span" class="users">
+								<template #n><b>{{ number(stats.originalUsersCount) }}</b></template>
+							</I18n>
+							<I18n :src="$ts.nNotes" text-tag="span" class="notes">
+								<template #n><b>{{ number(stats.originalNotesCount) }}</b></template>
+							</I18n>
+						</div>
+						<I18n :src="$ts.onlineUsersCount" text-tag="span" class="online">
+							<template #n><b>{{ onlineUsersCount }}</b></template>
+						</I18n>
+					</div>
+					<button class="_button _acrylic menu" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
+				</div>
+			</div>
+			<nav class="nav">
+				<MkA to="/announcements">{{ $ts.announcements }}</MkA>
+				<MkA to="/explore">{{ $ts.explore }}</MkA>
+				<MkA to="/channels">{{ $ts.channel }}</MkA>
+				<MkA to="/featured">{{ $ts.featured }}</MkA>
+			</nav>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { toUnicode } from 'punycode/';
+import XSigninDialog from '@/components/signin-dialog.vue';
+import XSignupDialog from '@/components/signup-dialog.vue';
+import MkButton from '@/components/ui/button.vue';
+import XNote from '@/components/note.vue';
+import MkFeaturedPhotos from '@/components/featured-photos.vue';
+import XTimeline from './welcome.timeline.vue';
+import { host, instanceName } from '@/config';
+import * as os from '@/os';
+import number from '@/filters/number';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		XNote,
+		MkFeaturedPhotos,
+		XTimeline,
+	},
+
+	data() {
+		return {
+			host: toUnicode(host),
+			instanceName,
+			meta: null,
+			stats: null,
+			tags: [],
+			onlineUsersCount: null,
+		};
+	},
+
+	created() {
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+
+		os.api('stats').then(stats => {
+			this.stats = stats;
+		});
+
+		os.api('get-online-users-count').then(res => {
+			this.onlineUsersCount = res.count;
+		});
+
+		os.api('hashtags/list', {
+			sort: '+mentionedLocalUsers',
+			limit: 8
+		}).then(tags => {
+			this.tags = tags;
+		});
+	},
+
+	methods: {
+		signin() {
+			os.popup(XSigninDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		signup() {
+			os.popup(XSignupDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		showMenu(ev) {
+			os.popupMenu([{
+				text: this.$t('aboutX', { x: instanceName }),
+				icon: 'fas fa-info-circle',
+				action: () => {
+					os.pageWindow('/about');
+				}
+			}, {
+				text: this.$ts.aboutMisskey,
+				icon: 'fas fa-info-circle',
+				action: () => {
+					os.pageWindow('/about-misskey');
+				}
+			}, null, {
+				text: this.$ts.help,
+				icon: 'fas fa-question-circle',
+				action: () => {
+					window.open(`https://misskey-hub.net/help.md`, '_blank');
+				}
+			}], ev.currentTarget || ev.target);
+		},
+
+		number
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rsqzvsbo {
+	> .top {
+		display: flex;
+		text-align: center;
+		min-height: 100vh;
+		box-sizing: border-box;
+		padding: 16px;
+
+		> .bg {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+		}
+
+		> .fade {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			background: rgba(0, 0, 0, 0.25);
+		}
+
+		> .emojis {
+			position: absolute;
+			bottom: 32px;
+			left: 35px;
+
+			> * {
+				margin-right: 8px;
+			}
+
+			@media (max-width: 1200px) {
+				display: none;
+			}
+		}
+
+		> .main {
+			position: relative;
+			width: min(460px, 100%);
+			margin: auto;
+
+			> .misskey {
+				width: 150px;
+				margin-bottom: 16px;
+
+				@media (max-width: 450px) {
+					width: 130px;
+				}
+			}
+
+			> .form {
+				position: relative;
+				box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
+
+				> .bg {
+					position: absolute;
+					top: 0;
+					left: 0;
+					width: 100%;
+					height: 128px;
+					background-position: center;
+					background-size: cover;
+					opacity: 0.75;
+
+					> .fade {
+						position: absolute;
+						bottom: 0;
+						left: 0;
+						width: 100%;
+						height: 128px;
+						background: linear-gradient(0deg, var(--panel), var(--X15));
+					}
+				}
+
+				> .fg {
+					position: relative;
+					z-index: 1;
+
+					> h1 {
+						display: block;
+						margin: 0;
+						padding: 32px 32px 24px 32px;
+
+						> .logo {
+							vertical-align: bottom;
+							max-height: 120px;
+						}
+					}
+
+					> .about {
+						padding: 0 32px;
+					}
+
+					> .action {
+						padding: 32px;
+
+						> * {
+							line-height: 28px;
+						}
+					}
+
+					> .status {
+						border-top: solid 0.5px var(--divider);
+						padding: 32px;
+						font-size: 90%;
+
+						> div {
+							> span:not(:last-child) {
+								padding-right: 1em;
+								margin-right: 1em;
+								border-right: solid 0.5px var(--divider);
+							}
+						}
+
+						> .online {
+							::v-deep(b) {
+								color: #41b781;
+							}
+
+							::v-deep(span) {
+								opacity: 0.7;
+							}
+						}
+					}
+
+					> .menu {
+						position: absolute;
+						top: 16px;
+						right: 16px;
+						width: 32px;
+						height: 32px;
+						border-radius: 8px;
+					}
+				}
+			}
+
+			> .nav {
+				position: relative;
+				z-index: 2;
+				margin-top: 20px;
+				color: #fff;
+				text-shadow: 0 0 8px black;
+				font-size: 0.9em;
+
+				> *:not(:last-child) {
+					margin-right: 1.5em;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/welcome.setup.vue b/packages/client/src/pages/welcome.setup.vue
new file mode 100644
index 0000000000..8c88720cf3
--- /dev/null
+++ b/packages/client/src/pages/welcome.setup.vue
@@ -0,0 +1,102 @@
+<template>
+<form class="mk-setup" @submit.prevent="submit()">
+	<h1>Welcome to Misskey!</h1>
+	<div>
+		<p>{{ $ts.intro }}</p>
+		<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-admin-username>
+			<template #label>{{ $ts.username }}</template>
+			<template #prefix>@</template>
+			<template #suffix>@{{ host }}</template>
+		</MkInput>
+		<MkInput v-model="password" type="password" data-cy-admin-password>
+			<template #label>{{ $ts.password }}</template>
+			<template #prefix><i class="fas fa-lock"></i></template>
+		</MkInput>
+		<footer>
+			<MkButton primary type="submit" :disabled="submitting" data-cy-admin-ok>
+				{{ submitting ? $ts.processing : $ts.done }}<MkEllipsis v-if="submitting"/>
+			</MkButton>
+		</footer>
+	</div>
+</form>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/form/input.vue';
+import { host } from '@/config';
+import * as os from '@/os';
+import { login } from '@/account';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		MkInput,
+	},
+
+	data() {
+		return {
+			username: '',
+			password: '',
+			submitting: false,
+			host,
+		}
+	},
+
+	methods: {
+		submit() {
+			if (this.submitting) return;
+			this.submitting = true;
+
+			os.api('admin/accounts/create', {
+				username: this.username,
+				password: this.password,
+			}).then(res => {
+				return login(res.token);
+			}).catch(() => {
+				this.submitting = false;
+
+				os.dialog({
+					type: 'error',
+					text: this.$ts.somethingHappened
+				});
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-setup {
+	border-radius: var(--radius);
+	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+	overflow: hidden;
+	max-width: 500px;
+	margin: 32px auto;
+
+	> h1 {
+		margin: 0;
+		font-size: 1.5em;
+		text-align: center;
+		padding: 32px;
+		background: var(--accent);
+		color: #fff;
+	}
+
+	> div {
+		padding: 32px;
+		background: var(--panel);
+
+		> p {
+			margin-top: 0;
+		}
+
+		> footer {
+			> * {
+				margin: 0 auto;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/welcome.timeline.vue b/packages/client/src/pages/welcome.timeline.vue
new file mode 100644
index 0000000000..46e3dbb5ed
--- /dev/null
+++ b/packages/client/src/pages/welcome.timeline.vue
@@ -0,0 +1,99 @@
+<template>
+<div class="civpbkhh">
+	<div class="scrollbox" ref="scroll" v-bind:class="{ scroll: isScrolling }">
+		<div v-for="note in notes" class="note">
+			<div class="content _panel">
+				<div class="body">
+					<MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
+					<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
+					<MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
+				</div>
+				<div v-if="note.files.length > 0" class="richcontent">
+					<XMediaList :media-list="note.files"/>
+				</div>
+				<div v-if="note.poll">
+					<XPoll :note="note" :readOnly="true" />
+				</div>
+			</div>
+			<XReactionsViewer :note="note" ref="reactionsViewer"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XReactionsViewer from '@/components/reactions-viewer.vue';
+import XMediaList from '@/components/media-list.vue';
+import XPoll from '@/components/poll.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XReactionsViewer,
+		XMediaList,
+		XPoll
+	},
+
+	data() {
+		return {
+			notes: [],
+			isScrolling: false,
+		}
+	},
+
+	created() {
+		os.api('notes/featured').then(notes => {
+			this.notes = notes;
+		});
+	},
+
+	updated() {
+		if (this.$refs.scroll.clientHeight > window.innerHeight) {
+			this.isScrolling = true;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+@keyframes scroll {
+	0% {
+		transform: translate3d(0, 0, 0);
+	}
+	5% {
+		transform: translate3d(0, 0, 0);
+	}
+	75% {
+		transform: translate3d(0, calc(-100% + 90vh), 0);
+	}
+	90% {
+		transform: translate3d(0, calc(-100% + 90vh), 0);
+	}
+}
+
+.civpbkhh {
+	text-align: right;
+
+	> .scrollbox {
+		&.scroll {
+			animation: scroll 45s linear infinite;
+		}
+
+		> .note {
+			margin: 16px 0 16px auto;
+
+			> .content {
+				padding: 16px;
+				margin: 0 0 0 auto;
+				max-width: max-content;
+				border-radius: 16px;
+
+				> .richcontent {
+					min-width: 250px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/pages/welcome.vue b/packages/client/src/pages/welcome.vue
new file mode 100644
index 0000000000..4c038b5113
--- /dev/null
+++ b/packages/client/src/pages/welcome.vue
@@ -0,0 +1,38 @@
+<template>
+<div v-if="meta">
+	<XSetup v-if="meta.requireSetup"/>
+	<XEntrance v-else/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XSetup from './welcome.setup.vue';
+import XEntrance from './welcome.entrance.a.vue';
+import { instanceName } from '@/config';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XSetup,
+		XEntrance,
+	},
+
+	data() {
+		return {
+			[symbols.PAGE_INFO]: {
+				title: instanceName,
+				icon: null
+			},
+			meta: null
+		}
+	},
+
+	created() {
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+	}
+});
+</script>
diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts
new file mode 100644
index 0000000000..396abc2418
--- /dev/null
+++ b/packages/client/src/pizzax.ts
@@ -0,0 +1,153 @@
+import { onUnmounted, Ref, ref, watch } from 'vue';
+import { $i } from './account';
+import { api } from './os';
+
+type StateDef = Record<string, {
+	where: 'account' | 'device' | 'deviceAccount';
+	default: any;
+}>;
+
+type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
+
+export class Storage<T extends StateDef> {
+	public readonly key: string;
+	public readonly keyForLocalStorage: string;
+
+	public readonly def: T;
+
+	// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
+	public readonly state: { [K in keyof T]: T[K]['default'] };
+	public readonly reactiveState: { [K in keyof T]: Ref<T[K]['default']> };
+
+	constructor(key: string, def: T) {
+		this.key = key;
+		this.keyForLocalStorage = 'pizzax::' + key;
+		this.def = def;
+
+		// TODO: indexedDBにする
+		const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
+		const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {};
+		const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {};
+
+		const state = {};
+		const reactiveState = {};
+		for (const [k, v] of Object.entries(def)) {
+			if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
+				state[k] = deviceState[k];
+			} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
+				state[k] = registryCache[k];
+			} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
+				state[k] = deviceAccountState[k];
+			} else {
+				state[k] = v.default;
+				if (_DEV_) console.log('Use default value', k, v.default);
+			}
+		}
+		for (const [k, v] of Object.entries(state)) {
+			reactiveState[k] = ref(v);
+		}
+		this.state = state as any;
+		this.reactiveState = reactiveState as any;
+
+		if ($i) {
+			// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
+			setTimeout(() => {
+				api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => {
+					const cache = {};
+					for (const [k, v] of Object.entries(def)) {
+						if (v.where === 'account') {
+							if (Object.prototype.hasOwnProperty.call(kvs, k)) {
+								state[k] = kvs[k];
+								reactiveState[k].value = kvs[k];
+								cache[k] = kvs[k];
+							} else {
+								state[k] = v.default;
+								reactiveState[k].value = v.default;
+							}
+						}
+					}
+					localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
+				});
+			}, 1);
+
+			// TODO: streamingのuser storage updateイベントを監視して更新
+		}
+	}
+
+	public set<K extends keyof T>(key: K, value: T[K]['default']): void {
+		if (_DEV_) console.log('set', key, value);
+
+		this.state[key] = value;
+		this.reactiveState[key].value = value;
+
+		switch (this.def[key].where) {
+			case 'device': {
+				const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
+				deviceState[key] = value;
+				localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState));
+				break;
+			}
+			case 'deviceAccount': {
+				if ($i == null) break;
+				const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}');
+				deviceAccountState[key] = value;
+				localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState));
+				break;
+			}
+			case 'account': {
+				if ($i == null) break;
+				const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
+				cache[key] = value;
+				localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
+				api('i/registry/set', {
+					scope: ['client', this.key],
+					key: key,
+					value: value
+				});
+				break;
+			}
+		}
+	}
+
+	public push<K extends keyof T>(key: K, value: ArrayElement<T[K]['default']>): void {
+		const currentState = this.state[key];
+		this.set(key, [...currentState, value]);
+	}
+
+	public reset(key: keyof T) {
+		this.set(key, this.def[key].default);
+	}
+
+	/**
+	 * 特定のキーの、簡易的なgetter/setterを作ります
+	 * 主にvue場で設定コントロールのmodelとして使う用
+	 */
+	public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) {
+		const valueRef = ref(this.state[key]);
+
+		const stop = watch(this.reactiveState[key], val => {
+			valueRef.value = val;
+		});
+
+		// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
+		onUnmounted(() => {
+			stop();
+		});
+
+		// TODO: VueのcustomRef使うと良い感じになるかも
+		return {
+			get: () => {
+				if (getter) {
+					return getter(valueRef.value);
+				} else {
+					return valueRef.value;
+				}
+			},
+			set: (value: unknown) => {
+				const val = setter ? setter(value) : value;
+				this.set(key, val);
+				valueRef.value = val;
+			}
+		};
+	}
+}
diff --git a/packages/client/src/plugin.ts b/packages/client/src/plugin.ts
new file mode 100644
index 0000000000..c56ee1eb25
--- /dev/null
+++ b/packages/client/src/plugin.ts
@@ -0,0 +1,124 @@
+import { AiScript, utils, values } from '@syuilo/aiscript';
+import { deserialize } from '@syuilo/aiscript/built/serializer';
+import { jsToVal } from '@syuilo/aiscript/built/interpreter/util';
+import { createAiScriptEnv } from '@/scripts/aiscript/api';
+import { dialog } from '@/os';
+import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store';
+
+const pluginContexts = new Map<string, AiScript>();
+
+export function install(plugin) {
+	console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
+
+	const aiscript = new AiScript(createPluginEnv({
+		plugin: plugin,
+		storageKey: 'plugins:' + plugin.id
+	}), {
+		in: (q) => {
+			return new Promise(ok => {
+				dialog({
+					title: q,
+					input: {}
+				}).then(({ canceled, result: a }) => {
+					ok(a);
+				});
+			});
+		},
+		out: (value) => {
+			console.log(value);
+		},
+		log: (type, params) => {
+		},
+	});
+
+	initPlugin({ plugin, aiscript });
+
+	aiscript.exec(deserialize(plugin.ast));
+}
+
+function createPluginEnv(opts) {
+	const config = new Map();
+	for (const [k, v] of Object.entries(opts.plugin.config || {})) {
+		config.set(k, jsToVal(opts.plugin.configData.hasOwnProperty(k) ? opts.plugin.configData[k] : v.default));
+	}
+
+	return {
+		...createAiScriptEnv({ ...opts, token: opts.plugin.token }),
+		//#region Deprecated
+		'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
+			registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
+		}),
+		'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => {
+			registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
+		}),
+		'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
+			registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
+		}),
+		//#endregion
+		'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
+			registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
+		}),
+		'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
+			registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
+		}),
+		'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
+			registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
+		}),
+		'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
+			registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler });
+		}),
+		'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
+			registerNotePostInterruptor({ pluginId: opts.plugin.id, handler });
+		}),
+		'Plugin:open_url': values.FN_NATIVE(([url]) => {
+			window.open(url.value, '_blank');
+		}),
+		'Plugin:config': values.OBJ(config),
+	};
+}
+
+function initPlugin({ plugin, aiscript }) {
+	pluginContexts.set(plugin.id, aiscript);
+}
+
+function registerPostFormAction({ pluginId, title, handler }) {
+	postFormActions.push({
+		title, handler: (form, update) => {
+			pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
+				update(key.value, value.value);
+			})]);
+		}
+	});
+}
+
+function registerUserAction({ pluginId, title, handler }) {
+	userActions.push({
+		title, handler: (user) => {
+			pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]);
+		}
+	});
+}
+
+function registerNoteAction({ pluginId, title, handler }) {
+	noteActions.push({
+		title, handler: (note) => {
+			pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
+		}
+	});
+}
+
+function registerNoteViewInterruptor({ pluginId, handler }) {
+	noteViewInterruptors.push({
+		handler: async (note) => {
+			return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]));
+		}
+	});
+}
+
+function registerNotePostInterruptor({ pluginId, handler }) {
+	notePostInterruptors.push({
+		handler: async (note) => {
+			return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]));
+		}
+	});
+}
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
new file mode 100644
index 0000000000..9b4dd162f3
--- /dev/null
+++ b/packages/client/src/router.ts
@@ -0,0 +1,149 @@
+import { defineAsyncComponent, markRaw } from 'vue';
+import { createRouter, createWebHistory } from 'vue-router';
+import MkLoading from '@/pages/_loading_.vue';
+import MkError from '@/pages/_error_.vue';
+import MkTimeline from '@/pages/timeline.vue';
+import { $i } from './account';
+import { ui } from '@/config';
+
+const page = (path: string, ui?: string) => defineAsyncComponent({
+	loader: ui ? () => import(`./ui/${ui}/pages/${path}.vue`) : () => import(`./pages/${path}.vue`),
+	loadingComponent: MkLoading,
+	errorComponent: MkError,
+});
+
+let indexScrollPos = 0;
+
+const defaultRoutes = [
+	// NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる
+	{ path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') },
+	{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
+	{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
+	{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
+	{ path: '/@:acct/room', props: true, component: page('room/room') },
+	{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
+	{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
+	{ path: '/signup-complete/:code', component: page('signup-complete'), props: route => ({ code: route.params.code }) },
+	{ path: '/announcements', component: page('announcements') },
+	{ path: '/about', component: page('about') },
+	{ path: '/about-misskey', component: page('about-misskey') },
+	{ path: '/featured', component: page('featured') },
+	{ path: '/theme-editor', component: page('theme-editor') },
+	{ path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
+	{ path: '/explore', component: page('explore') },
+	{ path: '/explore/tags/:tag', props: true, component: page('explore') },
+	{ path: '/federation', component: page('federation') },
+	{ path: '/emojis', component: page('emojis') },
+	{ path: '/search', component: page('search') },
+	{ path: '/pages', name: 'pages', component: page('pages') },
+	{ path: '/pages/new', component: page('page-editor/page-editor') },
+	{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
+	{ path: '/gallery', component: page('gallery/index') },
+	{ path: '/gallery/new', component: page('gallery/edit') },
+	{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) },
+	{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
+	{ path: '/channels', component: page('channels') },
+	{ path: '/channels/new', component: page('channel-editor') },
+	{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
+	{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
+	{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
+	{ path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) },
+	{ path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) },
+	{ path: '/my/notifications', component: page('notifications') },
+	{ path: '/my/favorites', component: page('favorites') },
+	{ path: '/my/messages', component: page('messages') },
+	{ path: '/my/mentions', component: page('mentions') },
+	{ path: '/my/messaging', name: 'messaging', component: page('messaging/index') },
+	{ path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) },
+	{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
+	{ path: '/my/drive', name: 'drive', component: page('drive') },
+	{ path: '/my/drive/folder/:folder', component: page('drive') },
+	{ path: '/my/follow-requests', component: page('follow-requests') },
+	{ path: '/my/lists', component: page('my-lists/index') },
+	{ path: '/my/lists/:list', component: page('my-lists/list') },
+	{ path: '/my/groups', component: page('my-groups/index') },
+	{ path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) },
+	{ path: '/my/antennas', component: page('my-antennas/index') },
+	{ path: '/my/antennas/create', component: page('my-antennas/create') },
+	{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
+	{ path: '/my/clips', component: page('my-clips/index') },
+	{ path: '/scratchpad', component: page('scratchpad') },
+	{ path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) },
+	{ path: '/admin', component: page('admin/index') },
+	{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
+	{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
+	{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
+	{ path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) },
+	{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
+	{ path: '/games/reversi', component: page('reversi/index') },
+	{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
+	{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
+	{ path: '/api-console', component: page('api-console') },
+	{ path: '/preview', component: page('preview') },
+	{ path: '/test', component: page('test') },
+	{ path: '/auth/:token', component: page('auth') },
+	{ path: '/miauth/:session', component: page('miauth') },
+	{ path: '/authorize-follow', component: page('follow') },
+	{ path: '/share', component: page('share') },
+	{ path: '/:catchAll(.*)', component: page('not-found') }
+];
+
+const chatRoutes = [
+	{ path: '/timeline', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
+	{ path: '/timeline/home', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
+	{ path: '/timeline/local', component: page('timeline', 'chat'), props: route => ({ src: 'local' }) },
+	{ path: '/timeline/social', component: page('timeline', 'chat'), props: route => ({ src: 'social' }) },
+	{ path: '/timeline/global', component: page('timeline', 'chat'), props: route => ({ src: 'global' }) },
+	{ path: '/channels/:channelId', component: page('channel', 'chat'), props: route => ({ channelId: route.params.channelId }) },
+];
+
+function margeRoutes(routes: any[]) {
+	const result = defaultRoutes;
+	for (const route of routes) {
+		const found = result.findIndex(x => x.path === route.path);
+		if (found > -1) {
+			result[found] = route;
+		} else {
+			result.unshift(route);
+		}
+	}
+	return result;
+}
+
+export const router = createRouter({
+	history: createWebHistory(),
+	routes: margeRoutes(ui === 'chat' ? chatRoutes : []),
+	// なんかHacky
+	// 通常の使い方をすると scroll メソッドの behavior を設定できないため、自前で window.scroll するようにする
+	scrollBehavior(to) {
+		window._scroll = () => { // さらにHacky
+			if (to.name === 'index') {
+				window.scroll({ top: indexScrollPos, behavior: 'instant' });
+				const i = setInterval(() => {
+					window.scroll({ top: indexScrollPos, behavior: 'instant' });
+				}, 10);
+				setTimeout(() => {
+					clearInterval(i);
+				}, 500);
+			} else {
+				window.scroll({ top: 0, behavior: 'instant' });
+			}
+		};
+	}
+});
+
+router.afterEach((to, from) => {
+	if (from.name === 'index') {
+		indexScrollPos = window.scrollY;
+	}
+});
+
+export function resolve(path: string) {
+	const resolved = router.resolve(path);
+	const route = resolved.matched[0];
+	return {
+		component: markRaw(route.components.default),
+		// TODO: route.propsには関数以外も入る可能性があるのでよしなにハンドリングする
+		props: route.props?.default ? route.props.default(resolved) : resolved.params
+	};
+}
diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts
new file mode 100644
index 0000000000..00363cffa6
--- /dev/null
+++ b/packages/client/src/scripts/2fa.ts
@@ -0,0 +1,33 @@
+export function byteify(data: string, encoding: 'ascii' | 'base64' | 'hex') {
+	switch (encoding) {
+		case 'ascii':
+			return Uint8Array.from(data, c => c.charCodeAt(0));
+		case 'base64':
+			return Uint8Array.from(
+				atob(
+					data
+						.replace(/-/g, '+')
+						.replace(/_/g, '/')
+				),
+				c => c.charCodeAt(0)
+			);
+		case 'hex':
+			return new Uint8Array(
+				data
+					.match(/.{1,2}/g)
+					.map(byte => parseInt(byte, 16))
+			);
+	}
+}
+
+export function hexify(buffer: ArrayBuffer) {
+	return Array.from(new Uint8Array(buffer))
+		.reduce(
+			(str, byte) => str + byte.toString(16).padStart(2, '0'),
+			''
+		);
+}
+
+export function stringify(buffer: ArrayBuffer) {
+	return String.fromCharCode(... new Uint8Array(buffer));
+}
diff --git a/packages/client/src/scripts/aiscript/api.ts b/packages/client/src/scripts/aiscript/api.ts
new file mode 100644
index 0000000000..20c15d809e
--- /dev/null
+++ b/packages/client/src/scripts/aiscript/api.ts
@@ -0,0 +1,44 @@
+import { utils, values } from '@syuilo/aiscript';
+import * as os from '@/os';
+import { $i } from '@/account';
+
+export function createAiScriptEnv(opts) {
+	let apiRequests = 0;
+	return {
+		USER_ID: $i ? values.STR($i.id) : values.NULL,
+		USER_NAME: $i ? values.STR($i.name) : values.NULL,
+		USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
+		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
+			await os.dialog({
+				type: type ? type.value : 'info',
+				title: title.value,
+				text: text.value,
+			});
+		}),
+		'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
+			const confirm = await os.dialog({
+				type: type ? type.value : 'question',
+				showCancelButton: true,
+				title: title.value,
+				text: text.value,
+			});
+			return confirm.canceled ? values.FALSE : values.TRUE;
+		}),
+		'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
+			if (token) utils.assertString(token);
+			apiRequests++;
+			if (apiRequests > 16) return values.NULL;
+			const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
+			return utils.jsToVal(res);
+		}),
+		'Mk:save': values.FN_NATIVE(([key, value]) => {
+			utils.assertString(key);
+			localStorage.setItem('aiscript:' + opts.storageKey + ':' + key.value, JSON.stringify(utils.valToJs(value)));
+			return values.NULL;
+		}),
+		'Mk:load': values.FN_NATIVE(([key]) => {
+			utils.assertString(key);
+			return utils.jsToVal(JSON.parse(localStorage.getItem('aiscript:' + opts.storageKey + ':' + key.value)));
+		}),
+	};
+}
diff --git a/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts
new file mode 100644
index 0000000000..d63f0475d0
--- /dev/null
+++ b/packages/client/src/scripts/array.ts
@@ -0,0 +1,138 @@
+import { EndoRelation, Predicate } from './relation';
+
+/**
+ * Count the number of elements that satisfy the predicate
+ */
+
+export function countIf<T>(f: Predicate<T>, xs: T[]): number {
+	return xs.filter(f).length;
+}
+
+/**
+ * Count the number of elements that is equal to the element
+ */
+export function count<T>(a: T, xs: T[]): number {
+	return countIf(x => x === a, xs);
+}
+
+/**
+ * Concatenate an array of arrays
+ */
+export function concat<T>(xss: T[][]): T[] {
+	return ([] as T[]).concat(...xss);
+}
+
+/**
+ * Intersperse the element between the elements of the array
+ * @param sep The element to be interspersed
+ */
+export function intersperse<T>(sep: T, xs: T[]): T[] {
+	return concat(xs.map(x => [sep, x])).slice(1);
+}
+
+/**
+ * Returns the array of elements that is not equal to the element
+ */
+export function erase<T>(a: T, xs: T[]): T[] {
+	return xs.filter(x => x !== a);
+}
+
+/**
+ * Finds the array of all elements in the first array not contained in the second array.
+ * The order of result values are determined by the first array.
+ */
+export function difference<T>(xs: T[], ys: T[]): T[] {
+	return xs.filter(x => !ys.includes(x));
+}
+
+/**
+ * Remove all but the first element from every group of equivalent elements
+ */
+export function unique<T>(xs: T[]): T[] {
+	return [...new Set(xs)];
+}
+
+export function sum(xs: number[]): number {
+	return xs.reduce((a, b) => a + b, 0);
+}
+
+export function maximum(xs: number[]): number {
+	return Math.max(...xs);
+}
+
+/**
+ * Splits an array based on the equivalence relation.
+ * The concatenation of the result is equal to the argument.
+ */
+export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
+	const groups = [] as T[][];
+	for (const x of xs) {
+		if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
+			groups[groups.length - 1].push(x);
+		} else {
+			groups.push([x]);
+		}
+	}
+	return groups;
+}
+
+/**
+ * Splits an array based on the equivalence relation induced by the function.
+ * The concatenation of the result is equal to the argument.
+ */
+export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
+	return groupBy((a, b) => f(a) === f(b), xs);
+}
+
+export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
+	return collections.reduce((obj: Record<string, T[]>, item: T) => {
+		const key = keySelector(item);
+		if (!obj.hasOwnProperty(key)) {
+			obj[key] = [];
+		}
+
+		obj[key].push(item);
+
+		return obj;
+	}, {});
+}
+
+/**
+ * Compare two arrays by lexicographical order
+ */
+export function lessThan(xs: number[], ys: number[]): boolean {
+	for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
+		if (xs[i] < ys[i]) return true;
+		if (xs[i] > ys[i]) return false;
+	}
+	return xs.length < ys.length;
+}
+
+/**
+ * Returns the longest prefix of elements that satisfy the predicate
+ */
+export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
+	const ys = [];
+	for (const x of xs) {
+		if (f(x)) {
+			ys.push(x);
+		} else {
+			break;
+		}
+	}
+	return ys;
+}
+
+export function cumulativeSum(xs: number[]): number[] {
+	const ys = Array.from(xs); // deep copy
+	for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
+	return ys;
+}
+
+export function toArray<T>(x: T | T[] | undefined): T[] {
+	return Array.isArray(x) ? x : x != null ? [x] : [];
+}
+
+export function toSingle<T>(x: T | T[] | undefined): T | undefined {
+	return Array.isArray(x) ? x[0] : x;
+}
diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts
new file mode 100644
index 0000000000..f2d5806484
--- /dev/null
+++ b/packages/client/src/scripts/autocomplete.ts
@@ -0,0 +1,276 @@
+import { Ref, ref } from 'vue';
+import * as getCaretCoordinates from 'textarea-caret';
+import { toASCII } from 'punycode/';
+import { popup } from '@/os';
+
+export class Autocomplete {
+	private suggestion: {
+		x: Ref<number>;
+		y: Ref<number>;
+		q: Ref<string | null>;
+		close: Function;
+	} | null;
+	private textarea: any;
+	private vm: any;
+	private currentType: string;
+	private opts: {
+		model: string;
+	};
+	private opening: boolean;
+
+	private get text(): string {
+		return this.vm[this.opts.model];
+	}
+
+	private set text(text: string) {
+		this.vm[this.opts.model] = text;
+	}
+
+	/**
+	 * 対象のテキストエリアを与えてインスタンスを初期化します。
+	 */
+	constructor(textarea, vm, opts) {
+		//#region BIND
+		this.onInput = this.onInput.bind(this);
+		this.complete = this.complete.bind(this);
+		this.close = this.close.bind(this);
+		//#endregion
+
+		this.suggestion = null;
+		this.textarea = textarea;
+		this.vm = vm;
+		this.opts = opts;
+		this.opening = false;
+
+		this.attach();
+	}
+
+	/**
+	 * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
+	 */
+	public attach() {
+		this.textarea.addEventListener('input', this.onInput);
+	}
+
+	/**
+	 * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
+	 */
+	public detach() {
+		this.textarea.removeEventListener('input', this.onInput);
+		this.close();
+	}
+
+	/**
+	 * テキスト入力時
+	 */
+	private onInput() {
+		const caretPos = this.textarea.selectionStart;
+		const text = this.text.substr(0, caretPos).split('\n').pop()!;
+
+		const mentionIndex = text.lastIndexOf('@');
+		const hashtagIndex = text.lastIndexOf('#');
+		const emojiIndex = text.lastIndexOf(':');
+		const mfmTagIndex = text.lastIndexOf('$');
+
+		const max = Math.max(
+			mentionIndex,
+			hashtagIndex,
+			emojiIndex,
+			mfmTagIndex);
+
+		if (max == -1) {
+			this.close();
+			return;
+		}
+
+		const isMention = mentionIndex != -1;
+		const isHashtag = hashtagIndex != -1;
+		const isMfmTag = mfmTagIndex != -1;
+		const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
+
+		let opened = false;
+
+		if (isMention) {
+			const username = text.substr(mentionIndex + 1);
+			if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
+				this.open('user', username);
+				opened = true;
+			} else if (username === '') {
+				this.open('user', null);
+				opened = true;
+			}
+		}
+
+		if (isHashtag && !opened) {
+			const hashtag = text.substr(hashtagIndex + 1);
+			if (!hashtag.includes(' ')) {
+				this.open('hashtag', hashtag);
+				opened = true;
+			}
+		}
+
+		if (isEmoji && !opened) {
+			const emoji = text.substr(emojiIndex + 1);
+			if (!emoji.includes(' ')) {
+				this.open('emoji', emoji);
+				opened = true;
+			}
+		}
+
+		if (isMfmTag && !opened) {
+			const mfmTag = text.substr(mfmTagIndex + 1);
+			if (!mfmTag.includes(' ')) {
+				this.open('mfmTag', mfmTag.replace('[', ''));
+				opened = true;
+			}
+		}
+
+		if (!opened) {
+			this.close();
+		}
+	}
+
+	/**
+	 * サジェストを提示します。
+	 */
+	private async open(type: string, q: string | null) {
+		if (type != this.currentType) {
+			this.close();
+		}
+		if (this.opening) return;
+		this.opening = true;
+		this.currentType = type;
+
+		//#region サジェストを表示すべき位置を計算
+		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
+
+		const rect = this.textarea.getBoundingClientRect();
+
+		const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
+		const y = rect.top + caretPosition.top - this.textarea.scrollTop;
+		//#endregion
+
+		if (this.suggestion) {
+			this.suggestion.x.value = x;
+			this.suggestion.y.value = y;
+			this.suggestion.q.value = q;
+
+			this.opening = false;
+		} else {
+			const _x = ref(x);
+			const _y = ref(y);
+			const _q = ref(q);
+
+			const { dispose } = await popup(import('@/components/autocomplete.vue'), {
+				textarea: this.textarea,
+				close: this.close,
+				type: type,
+				q: _q,
+				x: _x,
+				y: _y,
+			}, {
+				done: (res) => {
+					this.complete(res);
+				}
+			});
+
+			this.suggestion = {
+				q: _q,
+				x: _x,
+				y: _y,
+				close: () => dispose(),
+			};
+
+			this.opening = false;
+		}
+	}
+
+	/**
+	 * サジェストを閉じます。
+	 */
+	private close() {
+		if (this.suggestion == null) return;
+
+		this.suggestion.close();
+		this.suggestion = null;
+
+		this.textarea.focus();
+	}
+
+	/**
+	 * オートコンプリートする
+	 */
+	private complete({ type, value }) {
+		this.close();
+
+		const caret = this.textarea.selectionStart;
+
+		if (type == 'user') {
+			const source = this.text;
+
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
+			const after = source.substr(caret);
+
+			const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
+
+			// 挿入
+			this.text = `${trimmedBefore}@${acct} ${after}`;
+
+			// キャレットを戻す
+			this.vm.$nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (acct.length + 2);
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		} else if (type == 'hashtag') {
+			const source = this.text;
+
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
+			const after = source.substr(caret);
+
+			// 挿入
+			this.text = `${trimmedBefore}#${value} ${after}`;
+
+			// キャレットを戻す
+			this.vm.$nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (value.length + 2);
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		} else if (type == 'emoji') {
+			const source = this.text;
+
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
+			const after = source.substr(caret);
+
+			// 挿入
+			this.text = trimmedBefore + value + after;
+
+			// キャレットを戻す
+			this.vm.$nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + value.length;
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		} else if (type == 'mfmTag') {
+			const source = this.text;
+
+			const before = source.substr(0, caret);
+			const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
+			const after = source.substr(caret);
+
+			// 挿入
+			this.text = `${trimmedBefore}$[${value} ]${after}`;
+
+			// キャレットを戻す
+			this.vm.$nextTick(() => {
+				this.textarea.focus();
+				const pos = trimmedBefore.length + (value.length + 3);
+				this.textarea.setSelectionRange(pos, pos);
+			});
+		}
+	}
+}
diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts
new file mode 100644
index 0000000000..3b1fa75b1e
--- /dev/null
+++ b/packages/client/src/scripts/check-word-mute.ts
@@ -0,0 +1,26 @@
+export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> {
+	// 自分自身
+	if (me && (note.userId === me.id)) return false;
+
+	const words = mutedWords
+		// Clean up
+		.map(xs => xs.filter(x => x !== ''))
+		.filter(xs => xs.length > 0);
+
+	if (words.length > 0) {
+		if (note.text == null) return false;
+
+		const matched = words.some(and =>
+			and.every(keyword => {
+				const regexp = keyword.match(/^\/(.+)\/(.*)$/);
+				if (regexp) {
+					return new RegExp(regexp[1], regexp[2]).test(note.text!);
+				}
+				return note.text!.includes(keyword);
+			}));
+
+		if (matched) return true;
+	}
+
+	return false;
+}
diff --git a/packages/client/src/scripts/collect-page-vars.ts b/packages/client/src/scripts/collect-page-vars.ts
new file mode 100644
index 0000000000..a4096fb2c2
--- /dev/null
+++ b/packages/client/src/scripts/collect-page-vars.ts
@@ -0,0 +1,48 @@
+export function collectPageVars(content) {
+	const pageVars = [];
+	const collect = (xs: any[]) => {
+		for (const x of xs) {
+			if (x.type === 'textInput') {
+				pageVars.push({
+					name: x.name,
+					type: 'string',
+					value: x.default || ''
+				});
+			} else if (x.type === 'textareaInput') {
+				pageVars.push({
+					name: x.name,
+					type: 'string',
+					value: x.default || ''
+				});
+			} else if (x.type === 'numberInput') {
+				pageVars.push({
+					name: x.name,
+					type: 'number',
+					value: x.default || 0
+				});
+			} else if (x.type === 'switch') {
+				pageVars.push({
+					name: x.name,
+					type: 'boolean',
+					value: x.default || false
+				});
+			} else if (x.type === 'counter') {
+				pageVars.push({
+					name: x.name,
+					type: 'number',
+					value: 0
+				});
+			} else if (x.type === 'radioButton') {
+				pageVars.push({
+					name: x.name,
+					type: 'string',
+					value: x.default || ''
+				});
+			} else if (x.children) {
+				collect(x.children);
+			}
+		}
+	};
+	collect(content);
+	return pageVars;
+}
diff --git a/packages/client/src/scripts/contains.ts b/packages/client/src/scripts/contains.ts
new file mode 100644
index 0000000000..770bda63bb
--- /dev/null
+++ b/packages/client/src/scripts/contains.ts
@@ -0,0 +1,9 @@
+export default (parent, child, checkSame = true) => {
+	if (checkSame && parent === child) return true;
+	let node = child.parentNode;
+	while (node) {
+		if (node == parent) return true;
+		node = node.parentNode;
+	}
+	return false;
+};
diff --git a/packages/client/src/scripts/copy-to-clipboard.ts b/packages/client/src/scripts/copy-to-clipboard.ts
new file mode 100644
index 0000000000..ab13cab970
--- /dev/null
+++ b/packages/client/src/scripts/copy-to-clipboard.ts
@@ -0,0 +1,33 @@
+/**
+ * Clipboardに値をコピー(TODO: 文字列以外も対応)
+ */
+export default val => {
+	// 空div 生成
+	const tmp = document.createElement('div');
+	// 選択用のタグ生成
+	const pre = document.createElement('pre');
+
+	// 親要素のCSSで user-select: none だとコピーできないので書き換える
+	pre.style.webkitUserSelect = 'auto';
+	pre.style.userSelect = 'auto';
+
+	tmp.appendChild(pre).textContent = val;
+
+	// 要素を画面外へ
+	const s = tmp.style;
+	s.position = 'fixed';
+	s.right = '200%';
+
+	// body に追加
+	document.body.appendChild(tmp);
+	// 要素を選択
+	document.getSelection().selectAllChildren(tmp);
+
+	// クリップボードにコピー
+	const result = document.execCommand('copy');
+
+	// 要素削除
+	document.body.removeChild(tmp);
+
+	return result;
+};
diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts
new file mode 100644
index 0000000000..de7591f5a0
--- /dev/null
+++ b/packages/client/src/scripts/emojilist.ts
@@ -0,0 +1,7 @@
+// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
+export const emojilist = require('../emojilist.json') as {
+	name: string;
+	keywords: string[];
+	char: string;
+	category: 'people' | 'animals_and_nature' | 'food_and_drink' | 'activity' | 'travel_and_places' | 'objects' | 'symbols' | 'flags';
+}[];
diff --git a/packages/client/src/scripts/extract-avg-color-from-blurhash.ts b/packages/client/src/scripts/extract-avg-color-from-blurhash.ts
new file mode 100644
index 0000000000..123ab7a06d
--- /dev/null
+++ b/packages/client/src/scripts/extract-avg-color-from-blurhash.ts
@@ -0,0 +1,9 @@
+export function extractAvgColorFromBlurhash(hash: string) {
+	return typeof hash == 'string'
+		? '#' + [...hash.slice(2, 6)]
+				.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
+				.reduce((a, c) => a * 83 + c, 0)
+				.toString(16)
+				.padStart(6, '0')
+		: undefined;
+}
diff --git a/packages/client/src/scripts/extract-mentions.ts b/packages/client/src/scripts/extract-mentions.ts
new file mode 100644
index 0000000000..cc19b161a8
--- /dev/null
+++ b/packages/client/src/scripts/extract-mentions.ts
@@ -0,0 +1,11 @@
+// test is located in test/extract-mentions
+
+import * as mfm from 'mfm-js';
+
+export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] {
+	// TODO: 重複を削除
+	const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention');
+	const mentions = mentionNodes.map(x => x.props);
+
+	return mentions;
+}
diff --git a/packages/client/src/scripts/extract-url-from-mfm.ts b/packages/client/src/scripts/extract-url-from-mfm.ts
new file mode 100644
index 0000000000..34e3eb6c19
--- /dev/null
+++ b/packages/client/src/scripts/extract-url-from-mfm.ts
@@ -0,0 +1,19 @@
+import * as mfm from 'mfm-js';
+import { unique } from '@/scripts/array';
+
+// unique without hash
+// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
+const removeHash = (x: string) => x.replace(/#[^#]*$/, '');
+
+export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] {
+	const urlNodes = mfm.extract(nodes, (node) => {
+		return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent));
+	});
+	const urls: string[] = unique(urlNodes.map(x => x.props.url));
+
+	return urls.reduce((array, url) => {
+		const urlWithoutHash = removeHash(url);
+		if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url);
+		return array;
+	}, [] as string[]);
+}
diff --git a/packages/client/src/scripts/focus.ts b/packages/client/src/scripts/focus.ts
new file mode 100644
index 0000000000..0894877820
--- /dev/null
+++ b/packages/client/src/scripts/focus.ts
@@ -0,0 +1,27 @@
+export function focusPrev(el: Element | null, self = false, scroll = true) {
+	if (el == null) return;
+	if (!self) el = el.previousElementSibling;
+	if (el) {
+		if (el.hasAttribute('tabindex')) {
+			(el as HTMLElement).focus({
+				preventScroll: !scroll
+			});
+		} else {
+			focusPrev(el.previousElementSibling, true);
+		}
+	}
+}
+
+export function focusNext(el: Element | null, self = false, scroll = true) {
+	if (el == null) return;
+	if (!self) el = el.nextElementSibling;
+	if (el) {
+		if (el.hasAttribute('tabindex')) {
+			(el as HTMLElement).focus({
+				preventScroll: !scroll
+			});
+		} else {
+			focusPrev(el.nextElementSibling, true);
+		}
+	}
+}
diff --git a/packages/client/src/scripts/form.ts b/packages/client/src/scripts/form.ts
new file mode 100644
index 0000000000..7bf6cec452
--- /dev/null
+++ b/packages/client/src/scripts/form.ts
@@ -0,0 +1,31 @@
+export type FormItem = {
+	label?: string;
+	type: 'string';
+	default: string | null;
+	hidden?: boolean;
+	multiline?: boolean;
+} | {
+	label?: string;
+	type: 'number';
+	default: number | null;
+	hidden?: boolean;
+	step?: number;
+} | {
+	label?: string;
+	type: 'boolean';
+	default: boolean | null;
+	hidden?: boolean;
+} | {
+	label?: string;
+	type: 'enum';
+	default: string | null;
+	hidden?: boolean;
+	enum: string[];
+} | {
+	label?: string;
+	type: 'array';
+	default: unknown[] | null;
+	hidden?: boolean;
+};
+
+export type Form = Record<string, FormItem>;
diff --git a/packages/client/src/scripts/format-time-string.ts b/packages/client/src/scripts/format-time-string.ts
new file mode 100644
index 0000000000..bfb2c397ae
--- /dev/null
+++ b/packages/client/src/scripts/format-time-string.ts
@@ -0,0 +1,50 @@
+const defaultLocaleStringFormats: {[index: string]: string} = {
+	'weekday': 'narrow',
+	'era': 'narrow',
+	'year': 'numeric',
+	'month': 'numeric',
+	'day': 'numeric',
+	'hour': 'numeric',
+	'minute': 'numeric',
+	'second': 'numeric',
+	'timeZoneName': 'short'
+};
+
+function formatLocaleString(date: Date, format: string): string {
+	return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => {
+		if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) {
+			return date.toLocaleString(window.navigator.language, {[kind]: option ? option : defaultLocaleStringFormats[kind]});
+		} else {
+			return match;
+		}
+	});
+}
+
+export function formatDateTimeString(date: Date, format: string): string {
+	return format
+		.replace(/yyyy/g, date.getFullYear().toString())
+		.replace(/yy/g, date.getFullYear().toString().slice(-2))
+		.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long'}))
+		.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short'}))
+		.replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2))
+		.replace(/M/g, (date.getMonth() + 1).toString())
+		.replace(/dd/g, (`0${date.getDate()}`).slice(-2))
+		.replace(/d/g, date.getDate().toString())
+		.replace(/HH/g, (`0${date.getHours()}`).slice(-2))
+		.replace(/H/g, date.getHours().toString())
+		.replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2))
+		.replace(/h/g, ((date.getHours() % 12) || 12).toString())
+		.replace(/mm/g, (`0${date.getMinutes()}`).slice(-2))
+		.replace(/m/g, date.getMinutes().toString())
+		.replace(/ss/g, (`0${date.getSeconds()}`).slice(-2))
+		.replace(/s/g, date.getSeconds().toString())
+		.replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM');
+}
+
+export function formatTimeString(date: Date, format: string): string {
+	return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
+		if (localeformat) return formatLocaleString(date, localeformat);
+		if (datetimeformat) return formatDateTimeString(date, datetimeformat);
+		return match;
+	});
+}
diff --git a/packages/client/src/scripts/games/reversi/core.ts b/packages/client/src/scripts/games/reversi/core.ts
new file mode 100644
index 0000000000..0cb8922e19
--- /dev/null
+++ b/packages/client/src/scripts/games/reversi/core.ts
@@ -0,0 +1,263 @@
+import { count, concat } from '@/scripts/array';
+
+// MISSKEY REVERSI ENGINE
+
+/**
+ * true ... 黒
+ * false ... 白
+ */
+export type Color = boolean;
+const BLACK = true;
+const WHITE = false;
+
+export type MapPixel = 'null' | 'empty';
+
+export type Options = {
+	isLlotheo: boolean;
+	canPutEverywhere: boolean;
+	loopedBoard: boolean;
+};
+
+export type Undo = {
+	/**
+	 * 色
+	 */
+	color: Color;
+
+	/**
+	 * どこに打ったか
+	 */
+	pos: number;
+
+	/**
+	 * 反転した石の位置の配列
+	 */
+	effects: number[];
+
+	/**
+	 * ターン
+	 */
+	turn: Color | null;
+};
+
+/**
+ * リバーシエンジン
+ */
+export default class Reversi {
+	public map: MapPixel[];
+	public mapWidth: number;
+	public mapHeight: number;
+	public board: (Color | null | undefined)[];
+	public turn: Color | null = BLACK;
+	public opts: Options;
+
+	public prevPos = -1;
+	public prevColor: Color | null = null;
+
+	private logs: Undo[] = [];
+
+	/**
+	 * ゲームを初期化します
+	 */
+	constructor(map: string[], opts: Options) {
+		//#region binds
+		this.put = this.put.bind(this);
+		//#endregion
+
+		//#region Options
+		this.opts = opts;
+		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
+		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
+		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
+		//#endregion
+
+		//#region Parse map data
+		this.mapWidth = map[0].length;
+		this.mapHeight = map.length;
+		const mapData = map.join('');
+
+		this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
+
+		this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
+		//#endregion
+
+		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
+		if (!this.canPutSomewhere(BLACK))
+			this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
+	}
+
+	/**
+	 * 黒石の数
+	 */
+	public get blackCount() {
+		return count(BLACK, this.board);
+	}
+
+	/**
+	 * 白石の数
+	 */
+	public get whiteCount() {
+		return count(WHITE, this.board);
+	}
+
+	public transformPosToXy(pos: number): number[] {
+		const x = pos % this.mapWidth;
+		const y = Math.floor(pos / this.mapWidth);
+		return [x, y];
+	}
+
+	public transformXyToPos(x: number, y: number): number {
+		return x + (y * this.mapWidth);
+	}
+
+	/**
+	 * 指定のマスに石を打ちます
+	 * @param color 石の色
+	 * @param pos 位置
+	 */
+	public put(color: Color, pos: number) {
+		this.prevPos = pos;
+		this.prevColor = color;
+
+		this.board[pos] = color;
+
+		// 反転させられる石を取得
+		const effects = this.effects(color, pos);
+
+		// 反転させる
+		for (const pos of effects) {
+			this.board[pos] = color;
+		}
+
+		const turn = this.turn;
+
+		this.logs.push({
+			color,
+			pos,
+			effects,
+			turn
+		});
+
+		this.calcTurn();
+	}
+
+	private calcTurn() {
+		// ターン計算
+		this.turn =
+			this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
+			this.canPutSomewhere(this.prevColor!) ? this.prevColor :
+			null;
+	}
+
+	public undo() {
+		const undo = this.logs.pop()!;
+		this.prevColor = undo.color;
+		this.prevPos = undo.pos;
+		this.board[undo.pos] = null;
+		for (const pos of undo.effects) {
+			const color = this.board[pos];
+			this.board[pos] = !color;
+		}
+		this.turn = undo.turn;
+	}
+
+	/**
+	 * 指定した位置のマップデータのマスを取得します
+	 * @param pos 位置
+	 */
+	public mapDataGet(pos: number): MapPixel {
+		const [x, y] = this.transformPosToXy(pos);
+		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
+	}
+
+	/**
+	 * 打つことができる場所を取得します
+	 */
+	public puttablePlaces(color: Color): number[] {
+		return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
+	}
+
+	/**
+	 * 打つことができる場所があるかどうかを取得します
+	 */
+	public canPutSomewhere(color: Color): boolean {
+		return this.puttablePlaces(color).length > 0;
+	}
+
+	/**
+	 * 指定のマスに石を打つことができるかどうかを取得します
+	 * @param color 自分の色
+	 * @param pos 位置
+	 */
+	public canPut(color: Color, pos: number): boolean {
+		return (
+			this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
+			this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
+			this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
+	}
+
+	/**
+	 * 指定のマスに石を置いた時の、反転させられる石を取得します
+	 * @param color 自分の色
+	 * @param initPos 位置
+	 */
+	public effects(color: Color, initPos: number): number[] {
+		const enemyColor = !color;
+
+		const diffVectors: [number, number][] = [
+			[  0,  -1], // 上
+			[ +1,  -1], // 右上
+			[ +1,   0], // 右
+			[ +1,  +1], // 右下
+			[  0,  +1], // 下
+			[ -1,  +1], // 左下
+			[ -1,   0], // 左
+			[ -1,  -1]  // 左上
+		];
+
+		const effectsInLine = ([dx, dy]: [number, number]): number[] => {
+			const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
+
+			const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
+			let [x, y] = this.transformPosToXy(initPos);
+			while (true) {
+				[x, y] = nextPos(x, y);
+
+				// 座標が指し示す位置がボード外に出たとき
+				if (this.opts.loopedBoard && this.transformXyToPos(
+					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
+					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos)
+						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
+					return found;
+				else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight)
+					return []; // 挟めないことが確定 (盤面外に到達)
+
+				const pos = this.transformXyToPos(x, y);
+				if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
+				const stone = this.board[pos];
+				if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
+				if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
+				if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
+			}
+		};
+
+		return concat(diffVectors.map(effectsInLine));
+	}
+
+	/**
+	 * ゲームが終了したか否か
+	 */
+	public get isEnded(): boolean {
+		return this.turn === null;
+	}
+
+	/**
+	 * ゲームの勝者 (null = 引き分け)
+	 */
+	public get winner(): Color | null {
+		return this.isEnded ?
+			this.blackCount == this.whiteCount ? null :
+			this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
+			undefined as never;
+	}
+}
diff --git a/packages/client/src/scripts/games/reversi/maps.ts b/packages/client/src/scripts/games/reversi/maps.ts
new file mode 100644
index 0000000000..dc0d1bf9d0
--- /dev/null
+++ b/packages/client/src/scripts/games/reversi/maps.ts
@@ -0,0 +1,896 @@
+/**
+ * 組み込みマップ定義
+ *
+ * データ値:
+ * (スペース) ... マス無し
+ * - ... マス
+ * b ... 初期配置される黒石
+ * w ... 初期配置される白石
+ */
+
+export type Map = {
+	name?: string;
+	category?: string;
+	author?: string;
+	data: string[];
+};
+
+export const fourfour: Map = {
+	name: '4x4',
+	category: '4x4',
+	data: [
+		'----',
+		'-wb-',
+		'-bw-',
+		'----'
+	]
+};
+
+export const sixsix: Map = {
+	name: '6x6',
+	category: '6x6',
+	data: [
+		'------',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
+		'------'
+	]
+};
+
+export const roundedSixsix: Map = {
+	name: '6x6 rounded',
+	category: '6x6',
+	author: 'syuilo',
+	data: [
+		' ---- ',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
+		' ---- '
+	]
+};
+
+export const roundedSixsix2: Map = {
+	name: '6x6 rounded 2',
+	category: '6x6',
+	author: 'syuilo',
+	data: [
+		'  --  ',
+		' ---- ',
+		'--wb--',
+		'--bw--',
+		' ---- ',
+		'  --  '
+	]
+};
+
+export const eighteight: Map = {
+	name: '8x8',
+	category: '8x8',
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const eighteightH1: Map = {
+	name: '8x8 handicap 1',
+	category: '8x8',
+	data: [
+		'b-------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const eighteightH2: Map = {
+	name: '8x8 handicap 2',
+	category: '8x8',
+	data: [
+		'b-------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'-------b'
+	]
+};
+
+export const eighteightH3: Map = {
+	name: '8x8 handicap 3',
+	category: '8x8',
+	data: [
+		'b------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'-------b'
+	]
+};
+
+export const eighteightH4: Map = {
+	name: '8x8 handicap 4',
+	category: '8x8',
+	data: [
+		'b------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'b------b'
+	]
+};
+
+export const eighteightH28: Map = {
+	name: '8x8 handicap 28',
+	category: '8x8',
+	data: [
+		'bbbbbbbb',
+		'b------b',
+		'b------b',
+		'b--wb--b',
+		'b--bw--b',
+		'b------b',
+		'b------b',
+		'bbbbbbbb'
+	]
+};
+
+export const roundedEighteight: Map = {
+	name: '8x8 rounded',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		' ------ ',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		' ------ '
+	]
+};
+
+export const roundedEighteight2: Map = {
+	name: '8x8 rounded 2',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'  ----  ',
+		' ------ ',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		' ------ ',
+		'  ----  '
+	]
+};
+
+export const roundedEighteight3: Map = {
+	name: '8x8 rounded 3',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'   --   ',
+		'  ----  ',
+		' ------ ',
+		'---wb---',
+		'---bw---',
+		' ------ ',
+		'  ----  ',
+		'   --   '
+	]
+};
+
+export const eighteightWithNotch: Map = {
+	name: '8x8 with notch',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'---  ---',
+		'--------',
+		'--------',
+		' --wb-- ',
+		' --bw-- ',
+		'--------',
+		'--------',
+		'---  ---'
+	]
+};
+
+export const eighteightWithSomeHoles: Map = {
+	name: '8x8 with some holes',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'--- ----',
+		'----- --',
+		'-- -----',
+		'---wb---',
+		'---bw- -',
+		' -------',
+		'--- ----',
+		'--------'
+	]
+};
+
+export const circle: Map = {
+	name: 'Circle',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'   --   ',
+		' ------ ',
+		' ------ ',
+		'---wb---',
+		'---bw---',
+		' ------ ',
+		' ------ ',
+		'   --   '
+	]
+};
+
+export const smile: Map = {
+	name: 'Smile',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		' ------ ',
+		'--------',
+		'-- -- --',
+		'---wb---',
+		'-- bw --',
+		'---  ---',
+		'--------',
+		' ------ '
+	]
+};
+
+export const window: Map = {
+	name: 'Window',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'--------',
+		'-  --  -',
+		'-  --  -',
+		'---wb---',
+		'---bw---',
+		'-  --  -',
+		'-  --  -',
+		'--------'
+	]
+};
+
+export const reserved: Map = {
+	name: 'Reserved',
+	category: '8x8',
+	author: 'Aya',
+	data: [
+		'w------b',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'b------w'
+	]
+};
+
+export const x: Map = {
+	name: 'X',
+	category: '8x8',
+	author: 'Aya',
+	data: [
+		'w------b',
+		'-w----b-',
+		'--w--b--',
+		'---wb---',
+		'---bw---',
+		'--b--w--',
+		'-b----w-',
+		'b------w'
+	]
+};
+
+export const parallel: Map = {
+	name: 'Parallel',
+	category: '8x8',
+	author: 'Aya',
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---bb---',
+		'---ww---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const lackOfBlack: Map = {
+	name: 'Lack of Black',
+	category: '8x8',
+	data: [
+		'--------',
+		'--------',
+		'--------',
+		'---w----',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------'
+	]
+};
+
+export const squareParty: Map = {
+	name: 'Square Party',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'--------',
+		'-wwwbbb-',
+		'-w-wb-b-',
+		'-wwwbbb-',
+		'-bbbwww-',
+		'-b-bw-w-',
+		'-bbbwww-',
+		'--------'
+	]
+};
+
+export const minesweeper: Map = {
+	name: 'Minesweeper',
+	category: '8x8',
+	author: 'syuilo',
+	data: [
+		'b-b--w-w',
+		'-w-wb-b-',
+		'w-b--w-b',
+		'-b-wb-w-',
+		'-w-bw-b-',
+		'b-w--b-w',
+		'-b-bw-w-',
+		'w-w--b-b'
+	]
+};
+
+export const tenthtenth: Map = {
+	name: '10x10',
+	category: '10x10',
+	data: [
+		'----------',
+		'----------',
+		'----------',
+		'----------',
+		'----wb----',
+		'----bw----',
+		'----------',
+		'----------',
+		'----------',
+		'----------'
+	]
+};
+
+export const hole: Map = {
+	name: 'The Hole',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'----------',
+		'----------',
+		'--wb--wb--',
+		'--bw--bw--',
+		'----  ----',
+		'----  ----',
+		'--wb--wb--',
+		'--bw--bw--',
+		'----------',
+		'----------'
+	]
+};
+
+export const grid: Map = {
+	name: 'Grid',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'----------',
+		'- - -- - -',
+		'----------',
+		'- - -- - -',
+		'----wb----',
+		'----bw----',
+		'- - -- - -',
+		'----------',
+		'- - -- - -',
+		'----------'
+	]
+};
+
+export const cross: Map = {
+	name: 'Cross',
+	category: '10x10',
+	author: 'Aya',
+	data: [
+		'   ----   ',
+		'   ----   ',
+		'   ----   ',
+		'----------',
+		'----wb----',
+		'----bw----',
+		'----------',
+		'   ----   ',
+		'   ----   ',
+		'   ----   '
+	]
+};
+
+export const charX: Map = {
+	name: 'Char X',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'---    ---',
+		'----  ----',
+		'----------',
+		' -------- ',
+		'  --wb--  ',
+		'  --bw--  ',
+		' -------- ',
+		'----------',
+		'----  ----',
+		'---    ---'
+	]
+};
+
+export const charY: Map = {
+	name: 'Char Y',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'---    ---',
+		'----  ----',
+		'----------',
+		' -------- ',
+		'  --wb--  ',
+		'  --bw--  ',
+		'  ------  ',
+		'  ------  ',
+		'  ------  ',
+		'  ------  '
+	]
+};
+
+export const walls: Map = {
+	name: 'Walls',
+	category: '10x10',
+	author: 'Aya',
+	data: [
+		' bbbbbbbb ',
+		'w--------w',
+		'w--------w',
+		'w--------w',
+		'w---wb---w',
+		'w---bw---w',
+		'w--------w',
+		'w--------w',
+		'w--------w',
+		' bbbbbbbb '
+	]
+};
+
+export const cpu: Map = {
+	name: 'CPU',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		' b b  b b ',
+		'w--------w',
+		' -------- ',
+		'w--------w',
+		' ---wb--- ',
+		' ---bw--- ',
+		'w--------w',
+		' -------- ',
+		'w--------w',
+		' b b  b b '
+	]
+};
+
+export const checker: Map = {
+	name: 'Checker',
+	category: '10x10',
+	author: 'Aya',
+	data: [
+		'----------',
+		'----------',
+		'----------',
+		'---wbwb---',
+		'---bwbw---',
+		'---wbwb---',
+		'---bwbw---',
+		'----------',
+		'----------',
+		'----------'
+	]
+};
+
+export const japaneseCurry: Map = {
+	name: 'Japanese curry',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'w-b-b-b-b-',
+		'-w-b-b-b-b',
+		'w-w-b-b-b-',
+		'-w-w-b-b-b',
+		'w-w-wwb-b-',
+		'-w-wbb-b-b',
+		'w-w-w-b-b-',
+		'-w-w-w-b-b',
+		'w-w-w-w-b-',
+		'-w-w-w-w-b'
+	]
+};
+
+export const mosaic: Map = {
+	name: 'Mosaic',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'- - - - - ',
+		' - - - - -',
+		'- - - - - ',
+		' - w w - -',
+		'- - b b - ',
+		' - w w - -',
+		'- - b b - ',
+		' - - - - -',
+		'- - - - - ',
+		' - - - - -',
+	]
+};
+
+export const arena: Map = {
+	name: 'Arena',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'- - -- - -',
+		' - -  - - ',
+		'- ------ -',
+		' -------- ',
+		'- --wb-- -',
+		'- --bw-- -',
+		' -------- ',
+		'- ------ -',
+		' - -  - - ',
+		'- - -- - -'
+	]
+};
+
+export const reactor: Map = {
+	name: 'Reactor',
+	category: '10x10',
+	author: 'syuilo',
+	data: [
+		'-w------b-',
+		'b- -  - -w',
+		'- --wb-- -',
+		'---b  w---',
+		'- b wb w -',
+		'- w bw b -',
+		'---w  b---',
+		'- --bw-- -',
+		'w- -  - -b',
+		'-b------w-'
+	]
+};
+
+export const sixeight: Map = {
+	name: '6x8',
+	category: 'Special',
+	data: [
+		'------',
+		'------',
+		'------',
+		'--wb--',
+		'--bw--',
+		'------',
+		'------',
+		'------'
+	]
+};
+
+export const spark: Map = {
+	name: 'Spark',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		' -      - ',
+		'----------',
+		' -------- ',
+		' -------- ',
+		' ---wb--- ',
+		' ---bw--- ',
+		' -------- ',
+		' -------- ',
+		'----------',
+		' -      - '
+	]
+};
+
+export const islands: Map = {
+	name: 'Islands',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'--------  ',
+		'---wb---  ',
+		'---bw---  ',
+		'--------  ',
+		'  -    -  ',
+		'  -    -  ',
+		'  --------',
+		'  --------',
+		'  --------',
+		'  --------'
+	]
+};
+
+export const galaxy: Map = {
+	name: 'Galaxy',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'   ------   ',
+		'  --www---  ',
+		' ------w--- ',
+		'---bbb--w---',
+		'--b---b-w-b-',
+		'-b--wwb-w-b-',
+		'-b-w-bww--b-',
+		'-b-w-b---b--',
+		'---w--bbb---',
+		' ---w------ ',
+		'  ---www--  ',
+		'   ------   '
+	]
+};
+
+export const triangle: Map = {
+	name: 'Triangle',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'    --    ',
+		'    --    ',
+		'   ----   ',
+		'   ----   ',
+		'  --wb--  ',
+		'  --bw--  ',
+		' -------- ',
+		' -------- ',
+		'----------',
+		'----------'
+	]
+};
+
+export const iphonex: Map = {
+	name: 'iPhone X',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		' --  -- ',
+		'--------',
+		'--------',
+		'--------',
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------',
+		'--------',
+		'--------',
+		'--------',
+		' ------ '
+	]
+};
+
+export const dealWithIt: Map = {
+	name: 'Deal with it!',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		'------------',
+		'--w-b-------',
+		' --b-w------',
+		'  --w-b---- ',
+		'   -------  '
+	]
+};
+
+export const experiment: Map = {
+	name: 'Let\'s experiment',
+	category: 'Special',
+	author: 'syuilo',
+	data: [
+		' ------------ ',
+		'------wb------',
+		'------bw------',
+		'--------------',
+		'    -    -    ',
+		'------  ------',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'bbbbbb  wwwwww',
+		'wwwwww  bbbbbb'
+	]
+};
+
+export const bigBoard: Map = {
+	name: 'Big board',
+	category: 'Special',
+	data: [
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'-------wb-------',
+		'-------bw-------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------',
+		'----------------'
+	]
+};
+
+export const twoBoard: Map = {
+	name: 'Two board',
+	category: 'Special',
+	author: 'Aya',
+	data: [
+		'-------- --------',
+		'-------- --------',
+		'-------- --------',
+		'---wb--- ---wb---',
+		'---bw--- ---bw---',
+		'-------- --------',
+		'-------- --------',
+		'-------- --------'
+	]
+};
+
+export const test1: Map = {
+	name: 'Test1',
+	category: 'Test',
+	data: [
+		'--------',
+		'---wb---',
+		'---bw---',
+		'--------'
+	]
+};
+
+export const test2: Map = {
+	name: 'Test2',
+	category: 'Test',
+	data: [
+		'------',
+		'------',
+		'-b--w-',
+		'-w--b-',
+		'-w--b-'
+	]
+};
+
+export const test3: Map = {
+	name: 'Test3',
+	category: 'Test',
+	data: [
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'--w',
+		'w--',
+		'-w-',
+		'---',
+		'b--',
+	]
+};
+
+export const test4: Map = {
+	name: 'Test4',
+	category: 'Test',
+	data: [
+		'-w--b-',
+		'-w--b-',
+		'------',
+		'-w--b-',
+		'-w--b-'
+	]
+};
+
+// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)A1に打ってしまう
+export const test6: Map = {
+	name: 'Test6',
+	category: 'Test',
+	data: [
+		'--wwwww-',
+		'wwwwwwww',
+		'wbbbwbwb',
+		'wbbbbwbb',
+		'wbwbbwbb',
+		'wwbwbbbb',
+		'--wbbbbb',
+		'-wwwww--',
+	]
+};
+
+// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)G7に打ってしまう
+export const test7: Map = {
+	name: 'Test7',
+	category: 'Test',
+	data: [
+		'b--w----',
+		'b-wwww--',
+		'bwbwwwbb',
+		'wbwwwwb-',
+		'wwwwwww-',
+		'-wwbbwwb',
+		'--wwww--',
+		'--wwww--',
+	]
+};
+
+// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
+export const test8: Map = {
+	name: 'Test8',
+	category: 'Test',
+	data: [
+		'--------',
+		'-----w--',
+		'w--www--',
+		'wwwwww--',
+		'bbbbwww-',
+		'wwwwww--',
+		'--www---',
+		'--ww----',
+	]
+};
diff --git a/packages/client/src/scripts/games/reversi/package.json b/packages/client/src/scripts/games/reversi/package.json
new file mode 100644
index 0000000000..a4415ad141
--- /dev/null
+++ b/packages/client/src/scripts/games/reversi/package.json
@@ -0,0 +1,18 @@
+{
+  "name": "misskey-reversi",
+  "version": "0.0.5",
+  "description": "Misskey reversi engine",
+  "keywords": [
+    "misskey"
+  ],
+  "author": "syuilo <i@syuilo.com>",
+  "license": "MIT",
+  "repository": "https://github.com/misskey-dev/misskey.git",
+  "bugs": "https://github.com/misskey-dev/misskey/issues",
+  "main": "./built/core.js",
+  "types": "./built/core.d.ts",
+  "scripts": {
+    "build": "tsc"
+  },
+  "dependencies": {}
+}
diff --git a/packages/client/src/scripts/games/reversi/tsconfig.json b/packages/client/src/scripts/games/reversi/tsconfig.json
new file mode 100644
index 0000000000..851fb6b7e4
--- /dev/null
+++ b/packages/client/src/scripts/games/reversi/tsconfig.json
@@ -0,0 +1,21 @@
+{
+	"compilerOptions": {
+		"noEmitOnError": false,
+		"noImplicitAny": false,
+		"noImplicitReturns": true,
+		"noFallthroughCasesInSwitch": true,
+		"experimentalDecorators": true,
+		"declaration": true,
+		"sourceMap": false,
+		"target": "es2017",
+		"module": "commonjs",
+		"removeComments": false,
+		"noLib": false,
+		"outDir": "./built",
+		"rootDir": "./"
+	},
+	"compileOnSave": false,
+	"include": [
+		"./core.ts"
+	]
+}
diff --git a/packages/client/src/scripts/gen-search-query.ts b/packages/client/src/scripts/gen-search-query.ts
new file mode 100644
index 0000000000..57a06c280c
--- /dev/null
+++ b/packages/client/src/scripts/gen-search-query.ts
@@ -0,0 +1,31 @@
+import * as Acct from 'misskey-js/built/acct';
+import { host as localHost } from '@/config';
+
+export async function genSearchQuery(v: any, q: string) {
+	let host: string;
+	let userId: string;
+	if (q.split(' ').some(x => x.startsWith('@'))) {
+		for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) {
+			if (at.includes('.')) {
+				if (at === localHost || at === '.') {
+					host = null;
+				} else {
+					host = at;
+				}
+			} else {
+				const user = await v.os.api('users/show', Acct.parse(at)).catch(x => null);
+				if (user) {
+					userId = user.id;
+				} else {
+					// todo: show error
+				}
+			}
+		}
+
+	}
+	return {
+		query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
+		host: host,
+		userId: userId
+	};
+}
diff --git a/packages/client/src/scripts/get-account-from-id.ts b/packages/client/src/scripts/get-account-from-id.ts
new file mode 100644
index 0000000000..ba3adceecc
--- /dev/null
+++ b/packages/client/src/scripts/get-account-from-id.ts
@@ -0,0 +1,7 @@
+import { get } from '@/scripts/idb-proxy';
+
+export async function getAccountFromId(id: string) {
+	const accounts = await get('accounts') as { token: string; id: string; }[];
+	if (!accounts) console.log('Accounts are not recorded');
+	return accounts.find(e => e.id === id);
+}
diff --git a/packages/client/src/scripts/get-md5.ts b/packages/client/src/scripts/get-md5.ts
new file mode 100644
index 0000000000..b002d762b1
--- /dev/null
+++ b/packages/client/src/scripts/get-md5.ts
@@ -0,0 +1,10 @@
+// スクリプトサイズがデカい
+//import * as crypto from 'crypto';
+
+export default (data: ArrayBuffer) => {
+	//const buf = new Buffer(data);
+	//const hash = crypto.createHash('md5');
+	//hash.update(buf);
+	//return hash.digest('hex');
+	return '';
+};
diff --git a/packages/client/src/scripts/get-note-summary.ts b/packages/client/src/scripts/get-note-summary.ts
new file mode 100644
index 0000000000..bd394279cb
--- /dev/null
+++ b/packages/client/src/scripts/get-note-summary.ts
@@ -0,0 +1,55 @@
+import * as misskey from 'misskey-js';
+import { i18n } from '@/i18n';
+
+/**
+ * 投稿を表す文字列を取得します。
+ * @param {*} note (packされた)投稿
+ */
+export const getNoteSummary = (note: misskey.entities.Note): string => {
+	if (note.deletedAt) {
+		return `(${i18n.locale.deletedNote})`;
+	}
+
+	if (note.isHidden) {
+		return `(${i18n.locale.invisibleNote})`;
+	}
+
+	let summary = '';
+
+	// 本文
+	if (note.cw != null) {
+		summary += note.cw;
+	} else {
+		summary += note.text ? note.text : '';
+	}
+
+	// ファイルが添付されているとき
+	if ((note.files || []).length != 0) {
+		summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`;
+	}
+
+	// 投票が添付されているとき
+	if (note.poll) {
+		summary += ` (${i18n.locale.poll})`;
+	}
+
+	// 返信のとき
+	if (note.replyId) {
+		if (note.reply) {
+			summary += `\n\nRE: ${getNoteSummary(note.reply)}`;
+		} else {
+			summary += '\n\nRE: ...';
+		}
+	}
+
+	// Renoteのとき
+	if (note.renoteId) {
+		if (note.renote) {
+			summary += `\n\nRN: ${getNoteSummary(note.renote)}`;
+		} else {
+			summary += '\n\nRN: ...';
+		}
+	}
+
+	return summary.trim();
+};
diff --git a/packages/client/src/scripts/get-static-image-url.ts b/packages/client/src/scripts/get-static-image-url.ts
new file mode 100644
index 0000000000..e9a3e87cc8
--- /dev/null
+++ b/packages/client/src/scripts/get-static-image-url.ts
@@ -0,0 +1,16 @@
+import { url as instanceUrl } from '@/config';
+import * as url from '@/scripts/url';
+
+export function getStaticImageUrl(baseUrl: string): string {
+	const u = new URL(baseUrl);
+	if (u.href.startsWith(`${instanceUrl}/proxy/`)) {
+		// もう既にproxyっぽそうだったらsearchParams付けるだけ
+		u.searchParams.set('static', '1');
+		return u.href;
+	}
+	const dummy = `${u.host}${u.pathname}`;	// 拡張子がないとキャッシュしてくれないCDNがあるので
+	return `${instanceUrl}/proxy/${dummy}?${url.query({
+		url: u.href,
+		static: '1'
+	})}`;
+}
diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts
new file mode 100644
index 0000000000..8d767afa25
--- /dev/null
+++ b/packages/client/src/scripts/get-user-menu.ts
@@ -0,0 +1,205 @@
+import { i18n } from '@/i18n';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { host } from '@/config';
+import * as Acct from 'misskey-js/built/acct';
+import * as os from '@/os';
+import { userActions } from '@/store';
+import { router } from '@/router';
+import { $i } from '@/account';
+
+export function getUserMenu(user) {
+	const meId = $i ? $i.id : null;
+
+	async function pushList() {
+		const t = i18n.locale.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく
+		const lists = await os.api('users/lists/list');
+		if (lists.length === 0) {
+			os.dialog({
+				type: 'error',
+				text: i18n.locale.youHaveNoLists
+			});
+			return;
+		}
+		const { canceled, result: listId } = await os.dialog({
+			type: null,
+			title: t,
+			select: {
+				items: lists.map(list => ({
+					value: list.id, text: list.name
+				}))
+			},
+			showCancelButton: true
+		});
+		if (canceled) return;
+		os.apiWithDialog('users/lists/push', {
+			listId: listId,
+			userId: user.id
+		});
+	}
+
+	async function inviteGroup() {
+		const groups = await os.api('users/groups/owned');
+		if (groups.length === 0) {
+			os.dialog({
+				type: 'error',
+				text: i18n.locale.youHaveNoGroups
+			});
+			return;
+		}
+		const { canceled, result: groupId } = await os.dialog({
+			type: null,
+			title: i18n.locale.group,
+			select: {
+				items: groups.map(group => ({
+					value: group.id, text: group.name
+				}))
+			},
+			showCancelButton: true
+		});
+		if (canceled) return;
+		os.apiWithDialog('users/groups/invite', {
+			groupId: groupId,
+			userId: user.id
+		});
+	}
+
+	async function toggleMute() {
+		os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', {
+			userId: user.id
+		}).then(() => {
+			user.isMuted = !user.isMuted;
+		});
+	}
+
+	async function toggleBlock() {
+		if (!await getConfirmed(user.isBlocking ? i18n.locale.unblockConfirm : i18n.locale.blockConfirm)) return;
+
+		os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', {
+			userId: user.id
+		}).then(() => {
+			user.isBlocking = !user.isBlocking;
+		});
+	}
+
+	async function toggleSilence() {
+		if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return;
+
+		os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
+			userId: user.id
+		}).then(() => {
+			user.isSilenced = !user.isSilenced;
+		});
+	}
+
+	async function toggleSuspend() {
+		if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return;
+
+		os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
+			userId: user.id
+		}).then(() => {
+			user.isSuspended = !user.isSuspended;
+		});
+	}
+
+	function reportAbuse() {
+		os.popup(import('@/components/abuse-report-window.vue'), {
+			user: user,
+		}, {}, 'closed');
+	}
+
+	async function getConfirmed(text: string): Promise<boolean> {
+		const confirm = await os.dialog({
+			type: 'warning',
+			showCancelButton: true,
+			title: 'confirm',
+			text,
+		});
+
+		return !confirm.canceled;
+	}
+
+	let menu = [{
+		icon: 'fas fa-at',
+		text: i18n.locale.copyUsername,
+		action: () => {
+			copyToClipboard(`@${user.username}@${user.host || host}`);
+		}
+	}, {
+		icon: 'fas fa-info-circle',
+		text: i18n.locale.info,
+		action: () => {
+			os.pageWindow(`/user-info/${user.id}`);
+		}
+	}, {
+		icon: 'fas fa-envelope',
+		text: i18n.locale.sendMessage,
+		action: () => {
+			os.post({ specified: user });
+		}
+	}, meId != user.id ? {
+		type: 'link',
+		icon: 'fas fa-comments',
+		text: i18n.locale.startMessaging,
+		to: '/my/messaging/' + Acct.toString(user),
+	} : undefined, null, {
+		icon: 'fas fa-list-ul',
+		text: i18n.locale.addToList,
+		action: pushList
+	}, meId != user.id ? {
+		icon: 'fas fa-users',
+		text: i18n.locale.inviteToGroup,
+		action: inviteGroup
+	} : undefined] as any;
+
+	if ($i && meId != user.id) {
+		menu = menu.concat([null, {
+			icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash',
+			text: user.isMuted ? i18n.locale.unmute : i18n.locale.mute,
+			action: toggleMute
+		}, {
+			icon: 'fas fa-ban',
+			text: user.isBlocking ? i18n.locale.unblock : i18n.locale.block,
+			action: toggleBlock
+		}]);
+
+		menu = menu.concat([null, {
+			icon: 'fas fa-exclamation-circle',
+			text: i18n.locale.reportAbuse,
+			action: reportAbuse
+		}]);
+
+		if ($i && ($i.isAdmin || $i.isModerator)) {
+			menu = menu.concat([null, {
+				icon: 'fas fa-microphone-slash',
+				text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence,
+				action: toggleSilence
+			}, {
+				icon: 'fas fa-snowflake',
+				text: user.isSuspended ? i18n.locale.unsuspend : i18n.locale.suspend,
+				action: toggleSuspend
+			}]);
+		}
+	}
+
+	if ($i && meId === user.id) {
+		menu = menu.concat([null, {
+			icon: 'fas fa-pencil-alt',
+			text: i18n.locale.editProfile,
+			action: () => {
+				router.push('/settings/profile');
+			}
+		}]);
+	}
+
+	if (userActions.length > 0) {
+		menu = menu.concat([null, ...userActions.map(action => ({
+			icon: 'fas fa-plug',
+			text: action.title,
+			action: () => {
+				action.handler(user);
+			}
+		}))]);
+	}
+
+	return menu;
+}
diff --git a/packages/client/src/scripts/hotkey.ts b/packages/client/src/scripts/hotkey.ts
new file mode 100644
index 0000000000..2b3f491fd8
--- /dev/null
+++ b/packages/client/src/scripts/hotkey.ts
@@ -0,0 +1,88 @@
+import keyCode from './keycode';
+
+type Keymap = Record<string, Function>;
+
+type Pattern = {
+	which: string[];
+	ctrl?: boolean;
+	shift?: boolean;
+	alt?: boolean;
+};
+
+type Action = {
+	patterns: Pattern[];
+	callback: Function;
+	allowRepeat: boolean;
+};
+
+const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => {
+	const result = {
+		patterns: [],
+		callback: callback,
+		allowRepeat: true
+	} as Action;
+
+	if (patterns.match(/^\(.*\)$/) !== null) {
+		result.allowRepeat = false;
+		patterns = patterns.slice(1, -1);
+	}
+
+	result.patterns = patterns.split('|').map(part => {
+		const pattern = {
+			which: [],
+			ctrl: false,
+			alt: false,
+			shift: false
+		} as Pattern;
+
+		const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
+		for (const key of keys) {
+			switch (key) {
+				case 'ctrl': pattern.ctrl = true; break;
+				case 'alt': pattern.alt = true; break;
+				case 'shift': pattern.shift = true; break;
+				default: pattern.which = keyCode(key).map(k => k.toLowerCase());
+			}
+		}
+
+		return pattern;
+	});
+
+	return result;
+});
+
+const ignoreElemens = ['input', 'textarea'];
+
+function match(e: KeyboardEvent, patterns: Action['patterns']): boolean {
+	const key = e.code.toLowerCase();
+	return patterns.some(pattern => pattern.which.includes(key) &&
+		pattern.ctrl === e.ctrlKey &&
+		pattern.shift === e.shiftKey &&
+		pattern.alt === e.altKey &&
+		!e.metaKey
+	);
+}
+
+export const makeHotkey = (keymap: Keymap) => {
+	const actions = parseKeymap(keymap);
+
+	return (e: KeyboardEvent) => {
+		if (document.activeElement) {
+			if (ignoreElemens.some(el => document.activeElement!.matches(el))) return;
+			if (document.activeElement.attributes['contenteditable']) return;
+		}
+
+		for (const action of actions) {
+			const matched = match(e, action.patterns);
+
+			if (matched) {
+				if (!action.allowRepeat && e.repeat) return;
+
+				e.preventDefault();
+				e.stopPropagation();
+				action.callback(e);
+				break;
+			}
+		}
+	};
+};
diff --git a/packages/client/src/scripts/hpml/block.ts b/packages/client/src/scripts/hpml/block.ts
new file mode 100644
index 0000000000..804c5c1124
--- /dev/null
+++ b/packages/client/src/scripts/hpml/block.ts
@@ -0,0 +1,109 @@
+// blocks
+
+export type BlockBase = {
+	id: string;
+	type: string;
+};
+
+export type TextBlock = BlockBase & {
+	type: 'text';
+	text: string;
+};
+
+export type SectionBlock = BlockBase & {
+	type: 'section';
+	title: string;
+	children: (Block | VarBlock)[];
+};
+
+export type ImageBlock = BlockBase & {
+	type: 'image';
+	fileId: string | null;
+};
+
+export type ButtonBlock = BlockBase & {
+	type: 'button';
+	text: any;
+	primary: boolean;
+	action: string;
+	content: string;
+	event: string;
+	message: string;
+	var: string;
+	fn: string;
+};
+
+export type IfBlock = BlockBase & {
+	type: 'if';
+	var: string;
+	children: Block[];
+};
+
+export type TextareaBlock = BlockBase & {
+	type: 'textarea';
+	text: string;
+};
+
+export type PostBlock = BlockBase & {
+	type: 'post';
+	text: string;
+	attachCanvasImage: boolean;
+	canvasId: string;
+};
+
+export type CanvasBlock = BlockBase & {
+	type: 'canvas';
+	name: string; // canvas id
+	width: number;
+	height: number;
+};
+
+export type NoteBlock = BlockBase & {
+	type: 'note';
+	detailed: boolean;
+	note: string | null;
+};
+
+export type Block =
+	TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock;
+
+// variable blocks
+
+export type VarBlockBase = BlockBase & {
+	name: string;
+};
+
+export type NumberInputVarBlock = VarBlockBase & {
+	type: 'numberInput';
+	text: string;
+};
+
+export type TextInputVarBlock = VarBlockBase & {
+	type: 'textInput';
+	text: string;
+};
+
+export type SwitchVarBlock = VarBlockBase & {
+	type: 'switch';
+	text: string;
+};
+
+export type RadioButtonVarBlock = VarBlockBase & {
+	type: 'radioButton';
+	title: string;
+	values: string[];
+};
+
+export type CounterVarBlock = VarBlockBase & {
+	type: 'counter';
+	text: string;
+	inc: number;
+};
+
+export type VarBlock =
+	NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock;
+
+const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter'];
+export function isVarBlock(block: Block): block is VarBlock {
+	return varBlock.includes(block.type);
+}
diff --git a/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts
new file mode 100644
index 0000000000..20261d333d
--- /dev/null
+++ b/packages/client/src/scripts/hpml/evaluator.ts
@@ -0,0 +1,234 @@
+import autobind from 'autobind-decorator';
+import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.';
+import { version } from '@/config';
+import { AiScript, utils, values } from '@syuilo/aiscript';
+import { createAiScriptEnv } from '../aiscript/api';
+import { collectPageVars } from '../collect-page-vars';
+import { initHpmlLib, initAiLib } from './lib';
+import * as os from '@/os';
+import { markRaw, ref, Ref, unref } from 'vue';
+import { Expr, isLiteralValue, Variable } from './expr';
+
+/**
+ * Hpml evaluator
+ */
+export class Hpml {
+	private variables: Variable[];
+	private pageVars: PageVar[];
+	private envVars: Record<keyof typeof envVarsDef, any>;
+	public aiscript?: AiScript;
+	public pageVarUpdatedCallback?: values.VFn;
+	public canvases: Record<string, HTMLCanvasElement> = {};
+	public vars: Ref<Record<string, any>> = ref({});
+	public page: Record<string, any>;
+
+	private opts: {
+		randomSeed: string; visitor?: any; url?: string;
+		enableAiScript: boolean;
+	};
+
+	constructor(page: Hpml['page'], opts: Hpml['opts']) {
+		this.page = page;
+		this.variables = this.page.variables;
+		this.pageVars = collectPageVars(this.page.content);
+		this.opts = opts;
+
+		if (this.opts.enableAiScript) {
+			this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({
+				storageKey: 'pages:' + this.page.id
+			}), ...initAiLib(this)}, {
+				in: (q) => {
+					return new Promise(ok => {
+						os.dialog({
+							title: q,
+							input: {}
+						}).then(({ canceled, result: a }) => {
+							ok(a);
+						});
+					});
+				},
+				out: (value) => {
+					console.log(value);
+				},
+				log: (type, params) => {
+				},
+			}));
+
+			this.aiscript.scope.opts.onUpdated = (name, value) => {
+				this.eval();
+			};
+		}
+
+		const date = new Date();
+
+		this.envVars = {
+			AI: 'kawaii',
+			VERSION: version,
+			URL: this.page ? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}` : '',
+			LOGIN: opts.visitor != null,
+			NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '',
+			USERNAME: opts.visitor ? opts.visitor.username : '',
+			USERID: opts.visitor ? opts.visitor.id : '',
+			NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0,
+			FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0,
+			FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0,
+			IS_CAT: opts.visitor ? opts.visitor.isCat : false,
+			SEED: opts.randomSeed ? opts.randomSeed : '',
+			YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`,
+			AISCRIPT_DISABLED: !this.opts.enableAiScript,
+			NULL: null
+		};
+
+		this.eval();
+	}
+
+	@autobind
+	public eval() {
+		try {
+			this.vars.value = this.evaluateVars();
+		} catch (e) {
+			//this.onError(e);
+		}
+	}
+
+	@autobind
+	public interpolate(str: string) {
+		if (str == null) return null;
+		return str.replace(/{(.+?)}/g, match => {
+			const v = unref(this.vars)[match.slice(1, -1).trim()];
+			return v == null ? 'NULL' : v.toString();
+		});
+	}
+
+	@autobind
+	public callAiScript(fn: string) {
+		try {
+			if (this.aiscript) this.aiscript.execFn(this.aiscript.scope.get(fn), []);
+		} catch (e) {}
+	}
+
+	@autobind
+	public registerCanvas(id: string, canvas: any) {
+		this.canvases[id] = canvas;
+	}
+
+	@autobind
+	public updatePageVar(name: string, value: any) {
+		const pageVar = this.pageVars.find(v => v.name === name);
+		if (pageVar !== undefined) {
+			pageVar.value = value;
+			if (this.pageVarUpdatedCallback) {
+				if (this.aiscript) this.aiscript.execFn(this.pageVarUpdatedCallback, [values.STR(name), utils.jsToVal(value)]);
+			}
+		} else {
+			throw new HpmlError(`No such page var '${name}'`);
+		}
+	}
+
+	@autobind
+	public updateRandomSeed(seed: string) {
+		this.opts.randomSeed = seed;
+		this.envVars.SEED = seed;
+	}
+
+	@autobind
+	private _interpolateScope(str: string, scope: HpmlScope) {
+		return str.replace(/{(.+?)}/g, match => {
+			const v = scope.getState(match.slice(1, -1).trim());
+			return v == null ? 'NULL' : v.toString();
+		});
+	}
+
+	@autobind
+	public evaluateVars(): Record<string, any> {
+		const values: Record<string, any> = {};
+
+		for (const [k, v] of Object.entries(this.envVars)) {
+			values[k] = v;
+		}
+
+		for (const v of this.pageVars) {
+			values[v.name] = v.value;
+		}
+
+		for (const v of this.variables) {
+			values[v.name] = this.evaluate(v, new HpmlScope([values]));
+		}
+
+		return values;
+	}
+
+	@autobind
+	private evaluate(expr: Expr, scope: HpmlScope): any {
+
+		if (isLiteralValue(expr)) {
+			if (expr.type === null) {
+				return null;
+			}
+
+			if (expr.type === 'number') {
+				return parseInt((expr.value as any), 10);
+			}
+
+			if (expr.type === 'text' || expr.type === 'multiLineText') {
+				return this._interpolateScope(expr.value || '', scope);
+			}
+
+			if (expr.type === 'textList') {
+				return this._interpolateScope(expr.value || '', scope).trim().split('\n');
+			}
+
+			if (expr.type === 'ref') {
+				return scope.getState(expr.value);
+			}
+
+			if (expr.type === 'aiScriptVar') {
+				if (this.aiscript) {
+					try {
+						return utils.valToJs(this.aiscript.scope.get(expr.value));
+					} catch (e) {
+						return null;
+					}
+				} else {
+					return null;
+				}
+			}
+
+			// Define user function
+			if (expr.type == 'fn') {
+				return {
+					slots: expr.value.slots.map(x => x.name),
+					exec: (slotArg: Record<string, any>) => {
+						return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id));
+					}
+				} as Fn;
+			}
+			return;
+		}
+
+		// Call user function
+		if (expr.type.startsWith('fn:')) {
+			const fnName = expr.type.split(':')[1];
+			const fn = scope.getState(fnName);
+			const args = {} as Record<string, any>;
+			for (let i = 0; i < fn.slots.length; i++) {
+				const name = fn.slots[i];
+				args[name] = this.evaluate(expr.args[i], scope);
+			}
+			return fn.exec(args);
+		}
+
+		if (expr.args === undefined) return null;
+
+		const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor);
+
+		// Call function
+		const fnName = expr.type;
+		const fn = (funcs as any)[fnName];
+		if (fn == null) {
+			throw new HpmlError(`No such function '${fnName}'`);
+		} else {
+			return fn(...expr.args.map(x => this.evaluate(x, scope)));
+		}
+	}
+}
diff --git a/packages/client/src/scripts/hpml/expr.ts b/packages/client/src/scripts/hpml/expr.ts
new file mode 100644
index 0000000000..00e3ed118b
--- /dev/null
+++ b/packages/client/src/scripts/hpml/expr.ts
@@ -0,0 +1,79 @@
+import { literalDefs, Type } from '.';
+
+export type ExprBase = {
+	id: string;
+};
+
+// value
+
+export type EmptyValue = ExprBase & {
+	type: null;
+	value: null;
+};
+
+export type TextValue = ExprBase & {
+	type: 'text';
+	value: string;
+};
+
+export type MultiLineTextValue = ExprBase  & {
+	type: 'multiLineText';
+	value: string;
+};
+
+export type TextListValue = ExprBase & {
+	type: 'textList';
+	value: string;
+};
+
+export type NumberValue = ExprBase & {
+	type: 'number';
+	value: number;
+};
+
+export type RefValue = ExprBase & {
+	type: 'ref';
+	value: string; // value is variable name
+};
+
+export type AiScriptRefValue = ExprBase & {
+	type: 'aiScriptVar';
+	value: string; // value is variable name
+};
+
+export type UserFnValue = ExprBase & {
+	type: 'fn';
+	value: UserFnInnerValue;
+};
+type UserFnInnerValue = {
+	slots: {
+		name: string;
+		type: Type;
+	}[];
+	expression: Expr;
+};
+
+export type Value =
+	EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue;
+
+export function isLiteralValue(expr: Expr): expr is Value {
+	if (expr.type == null) return true;
+	if (literalDefs[expr.type]) return true;
+	return false;
+}
+
+// call function
+
+export type CallFn = ExprBase & { // "fn:hoge" or string
+	type: string;
+	args: Expr[];
+	value: null;
+};
+
+// variable
+export type Variable = (Value | CallFn) & {
+	name: string;
+};
+
+// expression
+export type Expr = Variable | Value | CallFn;
diff --git a/packages/client/src/scripts/hpml/index.ts b/packages/client/src/scripts/hpml/index.ts
new file mode 100644
index 0000000000..ac81eac2d9
--- /dev/null
+++ b/packages/client/src/scripts/hpml/index.ts
@@ -0,0 +1,103 @@
+/**
+ * Hpml
+ */
+
+import autobind from 'autobind-decorator';
+import { Hpml } from './evaluator';
+import { funcDefs } from './lib';
+
+export type Fn = {
+	slots: string[];
+	exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>;
+};
+
+export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
+
+export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = {
+	text:          { out: 'string',      category: 'value', icon: 'fas fa-quote-right', },
+	multiLineText: { out: 'string',      category: 'value', icon: 'fas fa-align-left', },
+	textList:      { out: 'stringArray', category: 'value', icon: 'fas fa-list', },
+	number:        { out: 'number',      category: 'value', icon: 'fas fa-sort-numeric-up', },
+	ref:           { out: null,          category: 'value', icon: 'fas fa-magic', },
+	aiScriptVar:   { out: null,          category: 'value', icon: 'fas fa-magic', },
+	fn:            { out: 'function',    category: 'value', icon: 'fas fa-square-root-alt', },
+};
+
+export const blockDefs = [
+	...Object.entries(literalDefs).map(([k, v]) => ({
+		type: k, out: v.out, category: v.category, icon: v.icon
+	})),
+	...Object.entries(funcDefs).map(([k, v]) => ({
+		type: k, out: v.out, category: v.category, icon: v.icon
+	}))
+];
+
+export type PageVar = { name: string; value: any; type: Type; };
+
+export const envVarsDef: Record<string, Type> = {
+	AI: 'string',
+	URL: 'string',
+	VERSION: 'string',
+	LOGIN: 'boolean',
+	NAME: 'string',
+	USERNAME: 'string',
+	USERID: 'string',
+	NOTES_COUNT: 'number',
+	FOLLOWERS_COUNT: 'number',
+	FOLLOWING_COUNT: 'number',
+	IS_CAT: 'boolean',
+	SEED: null,
+	YMD: 'string',
+	AISCRIPT_DISABLED: 'boolean',
+	NULL: null,
+};
+
+export class HpmlScope {
+	private layerdStates: Record<string, any>[];
+	public name: string;
+
+	constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) {
+		this.layerdStates = layerdStates;
+		this.name = name || 'anonymous';
+	}
+
+	@autobind
+	public createChildScope(states: Record<string, any>, name?: HpmlScope['name']): HpmlScope {
+		const layer = [states, ...this.layerdStates];
+		return new HpmlScope(layer, name);
+	}
+
+	/**
+	 * 指定した名前の変数の値を取得します
+	 * @param name 変数名
+	 */
+	@autobind
+	public getState(name: string): any {
+		for (const later of this.layerdStates) {
+			const state = later[name];
+			if (state !== undefined) {
+				return state;
+			}
+		}
+
+		throw new HpmlError(
+			`No such variable '${name}' in scope '${this.name}'`, {
+				scope: this.layerdStates
+			});
+	}
+}
+
+export class HpmlError extends Error {
+	public info?: any;
+
+	constructor(message: string, info?: any) {
+		super(message);
+
+		this.info = info;
+
+		// Maintains proper stack trace for where our error was thrown (only available on V8)
+		if (Error.captureStackTrace) {
+			Error.captureStackTrace(this, HpmlError);
+		}
+	}
+}
diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts
new file mode 100644
index 0000000000..2a1ac73a40
--- /dev/null
+++ b/packages/client/src/scripts/hpml/lib.ts
@@ -0,0 +1,246 @@
+import * as tinycolor from 'tinycolor2';
+import { Hpml } from './evaluator';
+import { values, utils } from '@syuilo/aiscript';
+import { Fn, HpmlScope } from '.';
+import { Expr } from './expr';
+import * as seedrandom from 'seedrandom';
+
+/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color
+// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
+Chart.pluginService.register({
+	beforeDraw: (chart, easing) => {
+		if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) {
+			const ctx = chart.chart.ctx;
+			ctx.save();
+			ctx.fillStyle = chart.config.options.chartArea.backgroundColor;
+			ctx.fillRect(0, 0, chart.chart.width, chart.chart.height);
+			ctx.restore();
+		}
+	}
+});
+*/
+
+export function initAiLib(hpml: Hpml) {
+	return {
+		'MkPages:updated': values.FN_NATIVE(([callback]) => {
+			hpml.pageVarUpdatedCallback = (callback as values.VFn);
+		}),
+		'MkPages:get_canvas': values.FN_NATIVE(([id]) => {
+			utils.assertString(id);
+			const canvas = hpml.canvases[id.value];
+			const ctx = canvas.getContext('2d');
+			return values.OBJ(new Map([
+				['clear_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.clearRect(x.value, y.value, width.value, height.value); })],
+				['fill_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.fillRect(x.value, y.value, width.value, height.value); })],
+				['stroke_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.strokeRect(x.value, y.value, width.value, height.value); })],
+				['fill_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.fillText(text.value, x.value, y.value, width ? width.value : undefined); })],
+				['stroke_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.strokeText(text.value, x.value, y.value, width ? width.value : undefined); })],
+				['set_line_width', values.FN_NATIVE(([width]) => { ctx.lineWidth = width.value; })],
+				['set_font', values.FN_NATIVE(([font]) => { ctx.font = font.value; })],
+				['set_fill_style', values.FN_NATIVE(([style]) => { ctx.fillStyle = style.value; })],
+				['set_stroke_style', values.FN_NATIVE(([style]) => { ctx.strokeStyle = style.value; })],
+				['begin_path', values.FN_NATIVE(() => { ctx.beginPath(); })],
+				['close_path', values.FN_NATIVE(() => { ctx.closePath(); })],
+				['move_to', values.FN_NATIVE(([x, y]) => { ctx.moveTo(x.value, y.value); })],
+				['line_to', values.FN_NATIVE(([x, y]) => { ctx.lineTo(x.value, y.value); })],
+				['arc', values.FN_NATIVE(([x, y, radius, startAngle, endAngle]) => { ctx.arc(x.value, y.value, radius.value, startAngle.value, endAngle.value); })],
+				['rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.rect(x.value, y.value, width.value, height.value); })],
+				['fill', values.FN_NATIVE(() => { ctx.fill(); })],
+				['stroke', values.FN_NATIVE(() => { ctx.stroke(); })],
+			]));
+		}),
+		'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
+			/* TODO
+			utils.assertString(id);
+			utils.assertObject(opts);
+			const canvas = hpml.canvases[id.value];
+			const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
+			Chart.defaults.color = '#555';
+			const chart = new Chart(canvas, {
+				type: opts.value.get('type').value,
+				data: {
+					labels: opts.value.get('labels').value.map(x => x.value),
+					datasets: opts.value.get('datasets').value.map(x => ({
+						label: x.value.has('label') ? x.value.get('label').value : '',
+						data: x.value.get('data').value.map(x => x.value),
+						pointRadius: 0,
+						lineTension: 0,
+						borderWidth: 2,
+						borderColor: x.value.has('color') ? x.value.get('color') : color,
+						backgroundColor: tinycolor(x.value.has('color') ? x.value.get('color') : color).setAlpha(0.1).toRgbString(),
+					}))
+				},
+				options: {
+					responsive: false,
+					devicePixelRatio: 1.5,
+					title: {
+						display: opts.value.has('title'),
+						text: opts.value.has('title') ? opts.value.get('title').value : '',
+						fontSize: 14,
+					},
+					layout: {
+						padding: {
+							left: 32,
+							right: 32,
+							top: opts.value.has('title') ? 16 : 32,
+							bottom: 16
+						}
+					},
+					legend: {
+						display: opts.value.get('datasets').value.filter(x => x.value.has('label') && x.value.get('label').value).length === 0 ? false : true,
+						position: 'bottom',
+						labels: {
+							boxWidth: 16,
+						}
+					},
+					tooltips: {
+						enabled: false,
+					},
+					chartArea: {
+						backgroundColor: '#fff'
+					},
+					...(opts.value.get('type').value === 'radar' ? {
+						scale: {
+							ticks: {
+								display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : false,
+								min: opts.value.has('min') ? opts.value.get('min').value : undefined,
+								max: opts.value.has('max') ? opts.value.get('max').value : undefined,
+								maxTicksLimit: 8,
+							},
+							pointLabels: {
+								fontSize: 12
+							}
+						}
+					} : {
+						scales: {
+							yAxes: [{
+								ticks: {
+									display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : true,
+									min: opts.value.has('min') ? opts.value.get('min').value : undefined,
+									max: opts.value.has('max') ? opts.value.get('max').value : undefined,
+								}
+							}]
+						}
+					})
+				}
+			});
+			*/
+		})
+	};
+}
+
+export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
+	if:              { in: ['boolean', 0, 0],              out: 0,             category: 'flow',       icon: 'fas fa-share-alt', },
+	for:             { in: ['number', 'function'],         out: null,          category: 'flow',       icon: 'fas fa-recycle', },
+	not:             { in: ['boolean'],                    out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
+	or:              { in: ['boolean', 'boolean'],         out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
+	and:             { in: ['boolean', 'boolean'],         out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
+	add:             { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-plus', },
+	subtract:        { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-minus', },
+	multiply:        { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-times', },
+	divide:          { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-divide', },
+	mod:             { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-divide', },
+	round:           { in: ['number'],                     out: 'number',      category: 'operation',  icon: 'fas fa-calculator', },
+	eq:              { in: [0, 0],                         out: 'boolean',     category: 'comparison', icon: 'fas fa-equals', },
+	notEq:           { in: [0, 0],                         out: 'boolean',     category: 'comparison', icon: 'fas fa-not-equal', },
+	gt:              { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-greater-than', },
+	lt:              { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-less-than', },
+	gtEq:            { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-greater-than-equal', },
+	ltEq:            { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-less-than-equal', },
+	strLen:          { in: ['string'],                     out: 'number',      category: 'text',       icon: 'fas fa-quote-right', },
+	strPick:         { in: ['string', 'number'],           out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
+	strReplace:      { in: ['string', 'string', 'string'], out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
+	strReverse:      { in: ['string'],                     out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
+	join:            { in: ['stringArray', 'string'],      out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
+	stringToNumber:  { in: ['string'],                     out: 'number',      category: 'convert',    icon: 'fas fa-exchange-alt', },
+	numberToString:  { in: ['number'],                     out: 'string',      category: 'convert',    icon: 'fas fa-exchange-alt', },
+	splitStrByLine:  { in: ['string'],                     out: 'stringArray', category: 'convert',    icon: 'fas fa-exchange-alt', },
+	pick:            { in: [null, 'number'],               out: null,          category: 'list',       icon: 'fas fa-indent', },
+	listLen:         { in: [null],                         out: 'number',      category: 'list',       icon: 'fas fa-indent', },
+	rannum:          { in: ['number', 'number'],           out: 'number',      category: 'random',     icon: 'fas fa-dice', },
+	dailyRannum:     { in: ['number', 'number'],           out: 'number',      category: 'random',     icon: 'fas fa-dice', },
+	seedRannum:      { in: [null, 'number', 'number'],     out: 'number',      category: 'random',     icon: 'fas fa-dice', },
+	random:          { in: ['number'],                     out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
+	dailyRandom:     { in: ['number'],                     out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
+	seedRandom:      { in: [null, 'number'],               out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
+	randomPick:      { in: [0],                            out: 0,             category: 'random',     icon: 'fas fa-dice', },
+	dailyRandomPick: { in: [0],                            out: 0,             category: 'random',     icon: 'fas fa-dice', },
+	seedRandomPick:  { in: [null, 0],                      out: 0,             category: 'random',     icon: 'fas fa-dice', },
+	DRPWPM:      { in: ['stringArray'],                out: 'string',      category: 'random',     icon: 'fas fa-dice', }, // dailyRandomPickWithProbabilityMapping
+};
+
+export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) {
+
+	const date = new Date();
+	const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
+
+	const funcs: Record<string, Function> = {
+		not: (a: boolean) => !a,
+		or: (a: boolean, b: boolean) => a || b,
+		and: (a: boolean, b: boolean) => a && b,
+		eq: (a: any, b: any) => a === b,
+		notEq: (a: any, b: any) => a !== b,
+		gt: (a: number, b: number) => a > b,
+		lt: (a: number, b: number) => a < b,
+		gtEq: (a: number, b: number) => a >= b,
+		ltEq: (a: number, b: number) => a <= b,
+		if: (bool: boolean, a: any, b: any) => bool ? a : b,
+		for: (times: number, fn: Fn) => {
+			const result: any[] = [];
+			for (let i = 0; i < times; i++) {
+				result.push(fn.exec({
+					[fn.slots[0]]: i + 1
+				}));
+			}
+			return result;
+		},
+		add: (a: number, b: number) => a + b,
+		subtract: (a: number, b: number) => a - b,
+		multiply: (a: number, b: number) => a * b,
+		divide: (a: number, b: number) => a / b,
+		mod: (a: number, b: number) => a % b,
+		round: (a: number) => Math.round(a),
+		strLen: (a: string) => a.length,
+		strPick: (a: string, b: number) => a[b - 1],
+		strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
+		strReverse: (a: string) => a.split('').reverse().join(''),
+		join: (texts: string[], separator: string) => texts.join(separator || ''),
+		stringToNumber: (a: string) => parseInt(a),
+		numberToString: (a: number) => a.toString(),
+		splitStrByLine: (a: string) => a.split('\n'),
+		pick: (list: any[], i: number) => list[i - 1],
+		listLen: (list: any[]) => list.length,
+		random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability,
+		rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)),
+		randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)],
+		dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability,
+		dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)),
+		dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)],
+		seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability,
+		seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)),
+		seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)],
+		DRPWPM: (list: string[]) => {
+			const xs: any[] = [];
+			let totalFactor = 0;
+			for (const x of list) {
+				const parts = x.split(' ');
+				const factor = parseInt(parts.pop()!, 10);
+				const text = parts.join(' ');
+				totalFactor += factor;
+				xs.push({ factor, text });
+			}
+			const r = seedrandom(`${day}:${expr.id}`)() * totalFactor;
+			let stackedFactor = 0;
+			for (const x of xs) {
+				if (r >= stackedFactor && r <= stackedFactor + x.factor) {
+					return x.text;
+				} else {
+					stackedFactor += x.factor;
+				}
+			}
+			return xs[0].text;
+		},
+	};
+
+	return funcs;
+}
diff --git a/packages/client/src/scripts/hpml/type-checker.ts b/packages/client/src/scripts/hpml/type-checker.ts
new file mode 100644
index 0000000000..9633b3cd01
--- /dev/null
+++ b/packages/client/src/scripts/hpml/type-checker.ts
@@ -0,0 +1,189 @@
+import autobind from 'autobind-decorator';
+import { Type, envVarsDef, PageVar } from '.';
+import { Expr, isLiteralValue, Variable } from './expr';
+import { funcDefs } from './lib';
+
+type TypeError = {
+	arg: number;
+	expect: Type;
+	actual: Type;
+};
+
+/**
+ * Hpml type checker
+ */
+export class HpmlTypeChecker {
+	public variables: Variable[];
+	public pageVars: PageVar[];
+
+	constructor(variables: HpmlTypeChecker['variables'] = [], pageVars: HpmlTypeChecker['pageVars'] = []) {
+		this.variables = variables;
+		this.pageVars = pageVars;
+	}
+
+	@autobind
+	public typeCheck(v: Expr): TypeError | null {
+		if (isLiteralValue(v)) return null;
+
+		const def = funcDefs[v.type || ''];
+		if (def == null) {
+			throw new Error('Unknown type: ' + v.type);
+		}
+
+		const generic: Type[] = [];
+
+		for (let i = 0; i < def.in.length; i++) {
+			const arg = def.in[i];
+			const type = this.infer(v.args[i]);
+			if (type === null) continue;
+
+			if (typeof arg === 'number') {
+				if (generic[arg] === undefined) {
+					generic[arg] = type;
+				} else if (type !== generic[arg]) {
+					return {
+						arg: i,
+						expect: generic[arg],
+						actual: type
+					};
+				}
+			} else if (type !== arg) {
+				return {
+					arg: i,
+					expect: arg,
+					actual: type
+				};
+			}
+		}
+
+		return null;
+	}
+
+	@autobind
+	public getExpectedType(v: Expr, slot: number): Type {
+		const def = funcDefs[v.type || ''];
+		if (def == null) {
+			throw new Error('Unknown type: ' + v.type);
+		}
+
+		const generic: Type[] = [];
+
+		for (let i = 0; i < def.in.length; i++) {
+			const arg = def.in[i];
+			const type = this.infer(v.args[i]);
+			if (type === null) continue;
+
+			if (typeof arg === 'number') {
+				if (generic[arg] === undefined) {
+					generic[arg] = type;
+				}
+			}
+		}
+
+		if (typeof def.in[slot] === 'number') {
+			return generic[def.in[slot]] || null;
+		} else {
+			return def.in[slot];
+		}
+	}
+
+	@autobind
+	public infer(v: Expr): Type {
+		if (v.type === null) return null;
+		if (v.type === 'text') return 'string';
+		if (v.type === 'multiLineText') return 'string';
+		if (v.type === 'textList') return 'stringArray';
+		if (v.type === 'number') return 'number';
+		if (v.type === 'ref') {
+			const variable = this.variables.find(va => va.name === v.value);
+			if (variable) {
+				return this.infer(variable);
+			}
+
+			const pageVar = this.pageVars.find(va => va.name === v.value);
+			if (pageVar) {
+				return pageVar.type;
+			}
+
+			const envVar = envVarsDef[v.value || ''];
+			if (envVar !== undefined) {
+				return envVar;
+			}
+
+			return null;
+		}
+		if (v.type === 'aiScriptVar') return null;
+		if (v.type === 'fn') return null; // todo
+		if (v.type.startsWith('fn:')) return null; // todo
+
+		const generic: Type[] = [];
+
+		const def = funcDefs[v.type];
+
+		for (let i = 0; i < def.in.length; i++) {
+			const arg = def.in[i];
+			if (typeof arg === 'number') {
+				const type = this.infer(v.args[i]);
+
+				if (generic[arg] === undefined) {
+					generic[arg] = type;
+				} else {
+					if (type !== generic[arg]) {
+						generic[arg] = null;
+					}
+				}
+			}
+		}
+
+		if (typeof def.out === 'number') {
+			return generic[def.out];
+		} else {
+			return def.out;
+		}
+	}
+
+	@autobind
+	public getVarByName(name: string): Variable {
+		const v = this.variables.find(x => x.name === name);
+		if (v !== undefined) {
+			return v;
+		} else {
+			throw new Error(`No such variable '${name}'`);
+		}
+	}
+
+	@autobind
+	public getVarsByType(type: Type): Variable[] {
+		if (type == null) return this.variables;
+		return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type));
+	}
+
+	@autobind
+	public getEnvVarsByType(type: Type): string[] {
+		if (type == null) return Object.keys(envVarsDef);
+		return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k);
+	}
+
+	@autobind
+	public getPageVarsByType(type: Type): string[] {
+		if (type == null) return this.pageVars.map(v => v.name);
+		return this.pageVars.filter(v => type === v.type).map(v => v.name);
+	}
+
+	@autobind
+	public isUsedName(name: string) {
+		if (this.variables.some(v => v.name === name)) {
+			return true;
+		}
+
+		if (this.pageVars.some(v => v.name === name)) {
+			return true;
+		}
+
+		if (envVarsDef[name]) {
+			return true;
+		}
+
+		return false;
+	}
+}
diff --git a/packages/client/src/scripts/i18n.ts b/packages/client/src/scripts/i18n.ts
new file mode 100644
index 0000000000..4fa398763a
--- /dev/null
+++ b/packages/client/src/scripts/i18n.ts
@@ -0,0 +1,29 @@
+export class I18n<T extends Record<string, any>> {
+	public locale: T;
+
+	constructor(locale: T) {
+		this.locale = locale;
+
+		//#region BIND
+		this.t = this.t.bind(this);
+		//#endregion
+	}
+
+	// string にしているのは、ドット区切りでのパス指定を許可するため
+	// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
+	public t(key: string, args?: Record<string, any>): string {
+		try {
+			let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
+
+			if (args) {
+				for (const [k, v] of Object.entries(args)) {
+					str = str.replace(`{${k}}`, v);
+				}
+			}
+			return str;
+		} catch (e) {
+			console.warn(`missing localization '${key}'`);
+			return key;
+		}
+	}
+}
diff --git a/packages/client/src/scripts/idb-proxy.ts b/packages/client/src/scripts/idb-proxy.ts
new file mode 100644
index 0000000000..5f76ae30bb
--- /dev/null
+++ b/packages/client/src/scripts/idb-proxy.ts
@@ -0,0 +1,37 @@
+// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、
+// indexedDBが使えない環境ではlocalStorageを使う
+import {
+	get as iget,
+	set as iset,
+	del as idel,
+} from 'idb-keyval';
+
+const fallbackName = (key: string) => `idbfallback::${key}`;
+
+let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true;
+
+if (idbAvailable) {
+	try {
+		await iset('idb-test', 'test');
+	} catch (e) {
+		console.error('idb error', e);
+		idbAvailable = false;
+	}
+}
+
+if (!idbAvailable) console.error('indexedDB is unavailable. It will use localStorage.');
+
+export async function get(key: string) {
+	if (idbAvailable) return iget(key);
+	return JSON.parse(localStorage.getItem(fallbackName(key)));
+}
+
+export async function set(key: string, val: any) {
+	if (idbAvailable) return iset(key, val);
+	return localStorage.setItem(fallbackName(key), JSON.stringify(val));
+}
+
+export async function del(key: string) {
+	if (idbAvailable) return idel(key);
+	return localStorage.removeItem(fallbackName(key));
+}
diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts
new file mode 100644
index 0000000000..d6dbd5dbd4
--- /dev/null
+++ b/packages/client/src/scripts/initialize-sw.ts
@@ -0,0 +1,68 @@
+import { instance } from '@/instance';
+import { $i } from '@/account';
+import { api } from '@/os';
+import { lang } from '@/config';
+
+export async function initializeSw() {
+	if (instance.swPublickey &&
+		('serviceWorker' in navigator) &&
+		('PushManager' in window) &&
+		$i && $i.token) {
+		navigator.serviceWorker.register(`/sw.js`);
+
+		navigator.serviceWorker.ready.then(registration => {
+			registration.active?.postMessage({
+				msg: 'initialize',
+				lang,
+			});
+			// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
+			registration.pushManager.subscribe({
+				userVisibleOnly: true,
+				applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
+			}).then(subscription => {
+				function encode(buffer: ArrayBuffer | null) {
+					return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
+				}
+
+				// Register
+				api('sw/register', {
+					endpoint: subscription.endpoint,
+					auth: encode(subscription.getKey('auth')),
+					publickey: encode(subscription.getKey('p256dh'))
+				});
+			})
+			// When subscribe failed
+			.catch(async (err: Error) => {
+				// 通知が許可されていなかったとき
+				if (err.name === 'NotAllowedError') {
+					return;
+				}
+
+				// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
+				// 既に存在していることが原因でエラーになった可能性があるので、
+				// そのサブスクリプションを解除しておく
+				const subscription = await registration.pushManager.getSubscription();
+				if (subscription) subscription.unsubscribe();
+			});
+		});
+	}
+}
+
+/**
+ * Convert the URL safe base64 string to a Uint8Array
+ * @param base64String base64 string
+ */
+function urlBase64ToUint8Array(base64String: string): Uint8Array {
+	const padding = '='.repeat((4 - base64String.length % 4) % 4);
+	const base64 = (base64String + padding)
+		.replace(/-/g, '+')
+		.replace(/_/g, '/');
+
+	const rawData = window.atob(base64);
+	const outputArray = new Uint8Array(rawData.length);
+
+	for (let i = 0; i < rawData.length; ++i) {
+		outputArray[i] = rawData.charCodeAt(i);
+	}
+	return outputArray;
+}
diff --git a/packages/client/src/scripts/is-device-darkmode.ts b/packages/client/src/scripts/is-device-darkmode.ts
new file mode 100644
index 0000000000..854f38e517
--- /dev/null
+++ b/packages/client/src/scripts/is-device-darkmode.ts
@@ -0,0 +1,3 @@
+export function isDeviceDarkmode() {
+	return window.matchMedia('(prefers-color-scheme: dark)').matches;
+}
diff --git a/packages/client/src/scripts/is-device-touch.ts b/packages/client/src/scripts/is-device-touch.ts
new file mode 100644
index 0000000000..3f0bfefed2
--- /dev/null
+++ b/packages/client/src/scripts/is-device-touch.ts
@@ -0,0 +1 @@
+export const isDeviceTouch = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
diff --git a/packages/client/src/scripts/is-mobile.ts b/packages/client/src/scripts/is-mobile.ts
new file mode 100644
index 0000000000..60cb59f91e
--- /dev/null
+++ b/packages/client/src/scripts/is-mobile.ts
@@ -0,0 +1,2 @@
+const ua = navigator.userAgent.toLowerCase();
+export const isMobile = /mobile|iphone|ipad|android/.test(ua);
diff --git a/packages/client/src/scripts/keycode.ts b/packages/client/src/scripts/keycode.ts
new file mode 100644
index 0000000000..c127d54bb2
--- /dev/null
+++ b/packages/client/src/scripts/keycode.ts
@@ -0,0 +1,33 @@
+export default (input: string): string[] => {
+	if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) {
+		const codes = aliases[input];
+		return Array.isArray(codes) ? codes : [codes];
+	} else {
+		return [input];
+	}
+};
+
+export const aliases = {
+	'esc': 'Escape',
+	'enter': ['Enter', 'NumpadEnter'],
+	'up': 'ArrowUp',
+	'down': 'ArrowDown',
+	'left': 'ArrowLeft',
+	'right': 'ArrowRight',
+	'plus': ['NumpadAdd', 'Semicolon'],
+};
+
+/*!
+* Programatically add the following
+*/
+
+// lower case chars
+for (let i = 97; i < 123; i++) {
+	const char = String.fromCharCode(i);
+	aliases[char] = `Key${char.toUpperCase()}`;
+}
+
+// numbers
+for (let i = 0; i < 10; i++) {
+	aliases[i] = [`Numpad${i}`, `Digit${i}`];
+}
diff --git a/packages/client/src/scripts/loading.ts b/packages/client/src/scripts/loading.ts
new file mode 100644
index 0000000000..4b0a560e34
--- /dev/null
+++ b/packages/client/src/scripts/loading.ts
@@ -0,0 +1,11 @@
+export default {
+	start: () => {
+		// TODO
+	},
+	done: () => {
+		// TODO
+	},
+	set: val => {
+		// TODO
+	}
+};
diff --git a/packages/client/src/scripts/login-id.ts b/packages/client/src/scripts/login-id.ts
new file mode 100644
index 0000000000..0f9c6be4a9
--- /dev/null
+++ b/packages/client/src/scripts/login-id.ts
@@ -0,0 +1,11 @@
+export function getUrlWithLoginId(url: string, loginId: string) {
+	const u = new URL(url, origin);
+	u.searchParams.append('loginId', loginId);
+	return u.toString();
+}
+
+export function getUrlWithoutLoginId(url: string) {
+	const u = new URL(url);
+	u.searchParams.delete('loginId');
+	return u.toString();
+}
diff --git a/packages/client/src/scripts/lookup-user.ts b/packages/client/src/scripts/lookup-user.ts
new file mode 100644
index 0000000000..174fa9f879
--- /dev/null
+++ b/packages/client/src/scripts/lookup-user.ts
@@ -0,0 +1,37 @@
+import * as Acct from 'misskey-js/built/acct';
+import { i18n } from '@/i18n';
+import * as os from '@/os';
+
+export async function lookupUser() {
+	const { canceled, result } = await os.dialog({
+		title: i18n.locale.usernameOrUserId,
+		input: true
+	});
+	if (canceled) return;
+
+	const show = (user) => {
+		os.pageWindow(`/user-info/${user.id}`);
+	};
+
+	const usernamePromise = os.api('users/show', Acct.parse(result));
+	const idPromise = os.api('users/show', { userId: result });
+	let _notFound = false;
+	const notFound = () => {
+		if (_notFound) {
+			os.dialog({
+				type: 'error',
+				text: i18n.locale.noSuchUser
+			});
+		} else {
+			_notFound = true;
+		}
+	};
+	usernamePromise.then(show).catch(e => {
+		if (e.code === 'NO_SUCH_USER') {
+			notFound();
+		}
+	});
+	idPromise.then(show).catch(e => {
+		notFound();
+	});
+}
diff --git a/packages/client/src/scripts/mfm-tags.ts b/packages/client/src/scripts/mfm-tags.ts
new file mode 100644
index 0000000000..1b18210aa9
--- /dev/null
+++ b/packages/client/src/scripts/mfm-tags.ts
@@ -0,0 +1 @@
+export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle'];
diff --git a/packages/client/src/scripts/paging.ts b/packages/client/src/scripts/paging.ts
new file mode 100644
index 0000000000..ef63ecc450
--- /dev/null
+++ b/packages/client/src/scripts/paging.ts
@@ -0,0 +1,246 @@
+import { markRaw } from 'vue';
+import * as os from '@/os';
+import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll';
+
+const SECOND_FETCH_LIMIT = 30;
+
+// reversed: items 配列の中身を逆順にする(新しい方が最後)
+
+export default (opts) => ({
+	emits: ['queue'],
+
+	data() {
+		return {
+			items: [],
+			queue: [],
+			offset: 0,
+			fetching: true,
+			moreFetching: false,
+			inited: false,
+			more: false,
+			backed: false, // 遡り中か否か
+			isBackTop: false,
+		};
+	},
+
+	computed: {
+		empty(): boolean {
+			return this.items.length === 0 && !this.fetching && this.inited;
+		},
+
+		error(): boolean {
+			return !this.fetching && !this.inited;
+		},
+	},
+
+	watch: {
+		pagination: {
+			handler() {
+				this.init();
+			},
+			deep: true
+		},
+
+		queue: {
+			handler(a, b) {
+				if (a.length === 0 && b.length === 0) return;
+				this.$emit('queue', this.queue.length);
+			},
+			deep: true
+		}
+	},
+
+	created() {
+		opts.displayLimit = opts.displayLimit || 30;
+		this.init();
+	},
+
+	activated() {
+		this.isBackTop = false;
+	},
+
+	deactivated() {
+		this.isBackTop = window.scrollY === 0;
+	},
+
+	methods: {
+		reload() {
+			this.items = [];
+			this.init();
+		},
+
+		replaceItem(finder, data) {
+			const i = this.items.findIndex(finder);
+			this.items[i] = data;
+		},
+
+		removeItem(finder) {
+			const i = this.items.findIndex(finder);
+			this.items.splice(i, 1);
+		},
+
+		async init() {
+			this.queue = [];
+			this.fetching = true;
+			if (opts.before) opts.before(this);
+			let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params;
+			if (params && params.then) params = await params;
+			if (params === null) return;
+			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
+			await os.api(endpoint, {
+				...params,
+				limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
+			}).then(items => {
+				for (let i = 0; i < items.length; i++) {
+					const item = items[i];
+					markRaw(item);
+					if (this.pagination.reversed) {
+						if (i === items.length - 2) item._shouldInsertAd_ = true;
+					} else {
+						if (i === 3) item._shouldInsertAd_ = true;
+					}
+				}
+				if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
+					items.pop();
+					this.items = this.pagination.reversed ? [...items].reverse() : items;
+					this.more = true;
+				} else {
+					this.items = this.pagination.reversed ? [...items].reverse() : items;
+					this.more = false;
+				}
+				this.offset = items.length;
+				this.inited = true;
+				this.fetching = false;
+				if (opts.after) opts.after(this, null);
+			}, e => {
+				this.fetching = false;
+				if (opts.after) opts.after(this, e);
+			});
+		},
+
+		async fetchMore() {
+			if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
+			this.moreFetching = true;
+			this.backed = true;
+			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
+			if (params && params.then) params = await params;
+			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
+			await os.api(endpoint, {
+				...params,
+				limit: SECOND_FETCH_LIMIT + 1,
+				...(this.pagination.offsetMode ? {
+					offset: this.offset,
+				} : {
+					untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
+				}),
+			}).then(items => {
+				for (let i = 0; i < items.length; i++) {
+					const item = items[i];
+					markRaw(item);
+					if (this.pagination.reversed) {
+						if (i === items.length - 9) item._shouldInsertAd_ = true;
+					} else {
+						if (i === 10) item._shouldInsertAd_ = true;
+					}
+				}
+				if (items.length > SECOND_FETCH_LIMIT) {
+					items.pop();
+					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
+					this.more = true;
+				} else {
+					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
+					this.more = false;
+				}
+				this.offset += items.length;
+				this.moreFetching = false;
+			}, e => {
+				this.moreFetching = false;
+			});
+		},
+
+		async fetchMoreFeature() {
+			if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
+			this.moreFetching = true;
+			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
+			if (params && params.then) params = await params;
+			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
+			await os.api(endpoint, {
+				...params,
+				limit: SECOND_FETCH_LIMIT + 1,
+				...(this.pagination.offsetMode ? {
+					offset: this.offset,
+				} : {
+					sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
+				}),
+			}).then(items => {
+				for (const item of items) {
+					markRaw(item);
+				}
+				if (items.length > SECOND_FETCH_LIMIT) {
+					items.pop();
+					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
+					this.more = true;
+				} else {
+					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
+					this.more = false;
+				}
+				this.offset += items.length;
+				this.moreFetching = false;
+			}, e => {
+				this.moreFetching = false;
+			});
+		},
+
+		prepend(item) {
+			if (this.pagination.reversed) {
+				const container = getScrollContainer(this.$el);
+				const pos = getScrollPosition(this.$el);
+				const viewHeight = container.clientHeight;
+				const height = container.scrollHeight;
+				const isBottom = (pos + viewHeight > height - 32);
+				if (isBottom) {
+					// オーバーフローしたら古いアイテムは捨てる
+					if (this.items.length >= opts.displayLimit) {
+						// このやり方だとVue 3.2以降アニメーションが動かなくなる
+						//this.items = this.items.slice(-opts.displayLimit);
+						while (this.items.length >= opts.displayLimit) {
+							this.items.shift();
+						}
+						this.more = true;
+					}
+				}
+				this.items.push(item);
+				// TODO
+			} else {
+				const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el));
+
+				if (isTop) {
+					// Prepend the item
+					this.items.unshift(item);
+
+					// オーバーフローしたら古いアイテムは捨てる
+					if (this.items.length >= opts.displayLimit) {
+						// このやり方だとVue 3.2以降アニメーションが動かなくなる
+						//this.items = this.items.slice(0, opts.displayLimit);
+						while (this.items.length >= opts.displayLimit) {
+							this.items.pop();
+						}
+						this.more = true;
+					}
+				} else {
+					this.queue.push(item);
+					onScrollTop(this.$el, () => {
+						for (const item of this.queue) {
+							this.prepend(item);
+						}
+						this.queue = [];
+					});
+				}
+			}
+		},
+
+		append(item) {
+			this.items.push(item);
+		},
+	}
+});
diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts
new file mode 100644
index 0000000000..445b6296eb
--- /dev/null
+++ b/packages/client/src/scripts/physics.ts
@@ -0,0 +1,152 @@
+import * as Matter from 'matter-js';
+
+export function physics(container: HTMLElement) {
+	const containerWidth = container.offsetWidth;
+	const containerHeight = container.offsetHeight;
+	const containerCenterX = containerWidth / 2;
+
+	// サイズ固定化(要らないかも？)
+	container.style.position = 'relative';
+	container.style.boxSizing = 'border-box';
+	container.style.width = `${containerWidth}px`;
+	container.style.height = `${containerHeight}px`;
+
+	// create engine
+	const engine = Matter.Engine.create({
+		constraintIterations: 4,
+		positionIterations: 8,
+		velocityIterations: 8,
+	});
+
+	const world = engine.world;
+
+	// create renderer
+	const render = Matter.Render.create({
+		engine: engine,
+		//element: document.getElementById('debug'),
+		options: {
+			width: containerWidth,
+			height: containerHeight,
+			background: 'transparent', // transparent to hide
+			wireframeBackground: 'transparent', // transparent to hide
+		}
+	});
+
+	// Disable to hide debug
+	Matter.Render.run(render);
+
+	// create runner
+	const runner = Matter.Runner.create();
+	Matter.Runner.run(runner, engine);
+
+	const groundThickness = 1024;
+	const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, {
+		isStatic:     true,
+		restitution:  0.1,
+		friction:     2
+	});
+
+	//const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts);
+	//const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts);
+
+	Matter.World.add(world, [
+		ground,
+		//wallRight,
+		//wallLeft,
+	]);
+
+	const objEls = Array.from(container.children);
+	const objs = [];
+	for (const objEl of objEls) {
+		const left = objEl.dataset.physicsX ? parseInt(objEl.dataset.physicsX) : objEl.offsetLeft;
+		const top = objEl.dataset.physicsY ? parseInt(objEl.dataset.physicsY) : objEl.offsetTop;
+
+		let obj;
+		if (objEl.classList.contains('_physics_circle_')) {
+			obj = Matter.Bodies.circle(
+				left + (objEl.offsetWidth / 2),
+				top + (objEl.offsetHeight / 2),
+				Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2,
+				{
+					restitution: 0.5
+				}
+			);
+		} else {
+			const style = window.getComputedStyle(objEl);
+			obj = Matter.Bodies.rectangle(
+				left + (objEl.offsetWidth / 2),
+				top + (objEl.offsetHeight / 2),
+				objEl.offsetWidth,
+				objEl.offsetHeight,
+				{
+					chamfer: { radius: parseInt(style.borderRadius || '0', 10) },
+					restitution: 0.5
+				}
+			);
+		}
+		objEl.id = obj.id;
+		objs.push(obj);
+	}
+
+	Matter.World.add(engine.world, objs);
+
+	// Add mouse control
+
+	const mouse = Matter.Mouse.create(container);
+	const mouseConstraint = Matter.MouseConstraint.create(engine, {
+		mouse: mouse,
+		constraint: {
+			stiffness: 0.1,
+			render: {
+				visible: false
+			}
+		}
+	});
+
+	Matter.World.add(engine.world, mouseConstraint);
+
+	// keep the mouse in sync with rendering
+	render.mouse = mouse;
+
+	for (const objEl of objEls) {
+		objEl.style.position = `absolute`;
+		objEl.style.top = 0;
+		objEl.style.left = 0;
+		objEl.style.margin = 0;
+	}
+
+	window.requestAnimationFrame(update);
+
+	let stop = false;
+
+	function update() {
+		for (const objEl of objEls) {
+			const obj = objs.find(obj => obj.id.toString() === objEl.id.toString());
+			if (obj == null) continue;
+
+			const x = (obj.position.x - objEl.offsetWidth / 2);
+			const y = (obj.position.y - objEl.offsetHeight / 2);
+			const angle = obj.angle;
+			objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`;
+		}
+
+		if (!stop) {
+			window.requestAnimationFrame(update);
+		}
+	}
+
+	// 奈落に落ちたオブジェクトは消す
+	const intervalId = setInterval(() => {
+		for (const obj of objs) {
+			if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj);
+		}
+	}, 1000 * 10);
+
+	return {
+		stop: () => {
+			stop = true;
+			Matter.Runner.stop(runner);
+			clearInterval(intervalId);
+		}
+	};
+}
diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts
new file mode 100644
index 0000000000..928f6ec0f4
--- /dev/null
+++ b/packages/client/src/scripts/please-login.ts
@@ -0,0 +1,14 @@
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+import { dialog } from '@/os';
+
+export function pleaseLogin() {
+	if ($i) return;
+
+	dialog({
+		title: i18n.locale.signinRequired,
+		text: null
+	});
+
+	throw new Error('signin required');
+}
diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts
new file mode 100644
index 0000000000..51b8d72868
--- /dev/null
+++ b/packages/client/src/scripts/popout.ts
@@ -0,0 +1,22 @@
+import * as config from '@/config';
+
+export function popout(path: string, w?: HTMLElement) {
+	let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
+	url += '?zen'; // TODO: ちゃんとURLパースしてクエリ付ける
+	if (w) {
+		const position = w.getBoundingClientRect();
+		const width = parseInt(getComputedStyle(w, '').width, 10);
+		const height = parseInt(getComputedStyle(w, '').height, 10);
+		const x = window.screenX + position.left;
+		const y = window.screenY + position.top;
+		window.open(url, url,
+			`width=${width}, height=${height}, top=${y}, left=${x}`);
+	} else {
+		const width = 400;
+		const height = 500;
+		const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2);
+		const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2);
+		window.open(url, url,
+			`width=${width}, height=${height}, top=${x}, left=${y}`);
+	}
+}
diff --git a/packages/client/src/scripts/reaction-picker.ts b/packages/client/src/scripts/reaction-picker.ts
new file mode 100644
index 0000000000..e923326ece
--- /dev/null
+++ b/packages/client/src/scripts/reaction-picker.ts
@@ -0,0 +1,41 @@
+import { Ref, ref } from 'vue';
+import { popup } from '@/os';
+
+class ReactionPicker {
+	private src: Ref<HTMLElement | null> = ref(null);
+	private manualShowing = ref(false);
+	private onChosen?: Function;
+	private onClosed?: Function;
+
+	constructor() {
+		// nop
+	}
+
+	public async init() {
+		await popup(import('@/components/emoji-picker-dialog.vue'), {
+			src: this.src,
+			asReactionPicker: true,
+			manualShowing: this.manualShowing
+		}, {
+			done: reaction => {
+				this.onChosen!(reaction);
+			},
+			close: () => {
+				this.manualShowing.value = false;
+			},
+			closed: () => {
+				this.src.value = null;
+				this.onClosed!();
+			}
+		});
+	}
+
+	public show(src: HTMLElement, onChosen: Function, onClosed: Function) {
+		this.src.value = src;
+		this.manualShowing.value = true;
+		this.onChosen = onChosen;
+		this.onClosed = onClosed;
+	}
+}
+
+export const reactionPicker = new ReactionPicker();
diff --git a/packages/client/src/scripts/room/furniture.ts b/packages/client/src/scripts/room/furniture.ts
new file mode 100644
index 0000000000..7734e32668
--- /dev/null
+++ b/packages/client/src/scripts/room/furniture.ts
@@ -0,0 +1,21 @@
+export type RoomInfo = {
+	roomType: string;
+	carpetColor: string;
+	furnitures: Furniture[];
+};
+
+export type Furniture = {
+	id: string; // 同じ家具が複数ある場合にそれぞれを識別するためのIDであり、家具IDではない
+	type: string; // こっちが家具ID(chairとか)
+	position: {
+		x: number;
+		y: number;
+		z: number;
+	};
+	rotation: {
+		x: number;
+		y: number;
+		z: number;
+	};
+	props?: Record<string, any>;
+};
diff --git a/packages/client/src/scripts/room/furnitures.json5 b/packages/client/src/scripts/room/furnitures.json5
new file mode 100644
index 0000000000..4a40994107
--- /dev/null
+++ b/packages/client/src/scripts/room/furnitures.json5
@@ -0,0 +1,407 @@
+// 家具メタデータ
+
+// 家具IDはglbファイル及びそのディレクトリ名と一致する必要があります
+
+// 家具にはユーザーが設定できるプロパティを設定可能です:
+//
+// props: {
+//   <propname>: <proptype>
+// }
+//
+// proptype一覧:
+// * image ... 画像選択ダイアログを出し、その画像のURLが格納されます
+// * color ... 色選択コントロールを出し、選択された色が格納されます
+
+// 家具にカスタムテクスチャを適用できるようにするには、textureプロパティに以下の追加の情報を含めます:
+// 便宜上そのUVのどの部分にカスタムテクスチャを貼り合わせるかのエリアをテクスチャエリアと呼びます。
+// UVは1024*1024だと仮定します。
+//
+// <key>: {
+//   prop: <プロパティ名>,
+//   uv: {
+//     x: <テクスチャエリアX座標>,
+//     y: <テクスチャエリアY座標>,
+//     width: <テクスチャエリアの幅>,
+//     height: <テクスチャエリアの高さ>,
+//   },
+// }
+//
+// <key>には、カスタムテクスチャを適用したいメッシュ名を指定します
+// <プロパティ名>には、カスタムテクスチャとして使用する画像を格納するプロパティ(前述)名を指定します
+
+// 家具にカスタムカラーを適用できるようにするには、colorプロパティに以下の追加の情報を含めます:
+//
+// <key>: <プロパティ名>
+//
+// <key>には、カスタムカラーを適用したいマテリアル名を指定します
+// <プロパティ名>には、カスタムカラーとして使用する色を格納するプロパティ(前述)名を指定します
+
+[
+	{
+		id: "milk",
+		place: "floor"
+	},
+	{
+		id: "bed",
+		place: "floor"
+	},
+	{
+		id: "low-table",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Table: 'color'
+		}
+	},
+	{
+		id: "desk",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Board: 'color'
+		}
+	},
+	{
+		id: "chair",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Chair: 'color'
+		}
+	},
+	{
+		id: "chair2",
+		place: "floor",
+		props: {
+			color1: 'color',
+			color2: 'color'
+		},
+		color: {
+			Cushion: 'color1',
+			Leg: 'color2'
+		}
+	},
+	{
+		id: "fan",
+		place: "wall"
+	},
+	{
+		id: "pc",
+		place: "floor"
+	},
+	{
+		id: "plant",
+		place: "floor"
+	},
+	{
+		id: "plant2",
+		place: "floor"
+	},
+	{
+		id: "eraser",
+		place: "floor"
+	},
+	{
+		id: "pencil",
+		place: "floor"
+	},
+	{
+		id: "pudding",
+		place: "floor"
+	},
+	{
+		id: "cardboard-box",
+		place: "floor"
+	},
+	{
+		id: "cardboard-box2",
+		place: "floor"
+	},
+	{
+		id: "cardboard-box3",
+		place: "floor"
+	},
+	{
+		id: "book",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Cover: 'color'
+		}
+	},
+	{
+		id: "book2",
+		place: "floor"
+	},
+	{
+		id: "piano",
+		place: "floor"
+	},
+	{
+		id: "facial-tissue",
+		place: "floor"
+	},
+	{
+		id: "server",
+		place: "floor"
+	},
+	{
+		id: "moon",
+		place: "floor"
+	},
+	{
+		id: "corkboard",
+		place: "wall"
+	},
+	{
+		id: "mousepad",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Pad: 'color'
+		}
+	},
+	{
+		id: "monitor",
+		place: "floor",
+		props: {
+			screen: 'image'
+		},
+		texture: {
+			Screen: {
+				prop: 'screen',
+				uv: {
+					x: 0,
+					y: 434,
+					width: 1024,
+					height: 588,
+				},
+			},
+		},
+	},
+	{
+		id: "tv",
+		place: "floor",
+		props: {
+			screen: 'image'
+		},
+		texture: {
+			Screen: {
+				prop: 'screen',
+				uv: {
+					x: 0,
+					y: 434,
+					width: 1024,
+					height: 588,
+				},
+			},
+		},
+	},
+	{
+		id: "keyboard",
+		place: "floor"
+	},
+	{
+		id: "carpet-stripe",
+		place: "floor",
+		props: {
+			color1: 'color',
+			color2: 'color'
+		},
+		color: {
+			CarpetAreaA: 'color1',
+			CarpetAreaB: 'color2'
+		},
+	},
+	{
+		id: "mat",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Mat: 'color'
+		}
+	},
+	{
+		id: "color-box",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			main: 'color'
+		}
+	},
+	{
+		id: "wall-clock",
+		place: "wall"
+	},
+	{
+		id: "cube",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Cube: 'color'
+		}
+	},
+	{
+		id: "photoframe",
+		place: "wall",
+		props: {
+			photo: 'image',
+			color: 'color'
+		},
+		texture: {
+			Photo: {
+				prop: 'photo',
+				uv: {
+					x: 0,
+					y: 342,
+					width: 1024,
+					height: 683,
+				},
+			},
+		},
+		color: {
+			Frame: 'color'
+		}
+	},
+	{
+		id: "pinguin",
+		place: "floor",
+		props: {
+			body: 'color',
+			belly: 'color'
+		},
+		color: {
+			Body: 'body',
+			Belly: 'belly',
+		}
+	},
+	{
+		id: "rubik-cube",
+		place: "floor",
+	},
+	{
+		id: "poster-h",
+		place: "wall",
+		props: {
+			picture: 'image'
+		},
+		texture: {
+			Poster: {
+				prop: 'picture',
+				uv: {
+					x: 0,
+					y: 277,
+					width: 1024,
+					height: 745,
+				},
+			},
+		},
+	},
+	{
+		id: "poster-v",
+		place: "wall",
+		props: {
+			picture: 'image'
+		},
+		texture: {
+			Poster: {
+				prop: 'picture',
+				uv: {
+					x: 0,
+					y: 0,
+					width: 745,
+					height: 1024,
+				},
+			},
+		},
+	},
+	{
+		id: "sofa",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Sofa: 'color'
+		}
+	},
+	{
+		id: "spiral",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Step: 'color'
+		}
+	},
+	{
+		id: "bin",
+		place: "floor",
+		props: {
+			color: 'color'
+		},
+		color: {
+			Bin: 'color'
+		}
+	},
+	{
+		id: "cup-noodle",
+		place: "floor"
+	},
+	{
+		id: "holo-display",
+		place: "floor",
+		props: {
+			image: 'image'
+		},
+		texture: {
+			Image_Front: {
+				prop: 'image',
+				uv: {
+					x: 0,
+					y: 0,
+					width: 1024,
+					height: 1024,
+				},
+			},
+			Image_Back: {
+				prop: 'image',
+				uv: {
+					x: 0,
+					y: 0,
+					width: 1024,
+					height: 1024,
+				},
+			},
+		},
+	},
+	{
+		id: 'energy-drink',
+		place: "floor",
+	},
+	{
+		id: 'doll-ai',
+		place: "floor",
+	},
+	{
+		id: 'banknote',
+		place: "floor",
+	},
+]
diff --git a/packages/client/src/scripts/room/room.ts b/packages/client/src/scripts/room/room.ts
new file mode 100644
index 0000000000..7e04bec646
--- /dev/null
+++ b/packages/client/src/scripts/room/room.ts
@@ -0,0 +1,775 @@
+import autobind from 'autobind-decorator';
+import { v4 as uuid } from 'uuid';
+import * as THREE from 'three';
+import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
+import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
+import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js';
+import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js';
+import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
+import { Furniture, RoomInfo } from './furniture';
+import { query as urlQuery } from '@/scripts/url';
+const furnitureDefs = require('./furnitures.json5');
+
+THREE.ImageUtils.crossOrigin = '';
+
+type Options = {
+	graphicsQuality: Room['graphicsQuality'];
+	onChangeSelect: Room['onChangeSelect'];
+	useOrthographicCamera: boolean;
+};
+
+/**
+ * MisskeyRoom Core Engine
+ */
+export class Room {
+	private clock: THREE.Clock;
+	private scene: THREE.Scene;
+	private renderer: THREE.WebGLRenderer;
+	private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
+	private controls: OrbitControls;
+	private composer: EffectComposer;
+	private mixers: THREE.AnimationMixer[] = [];
+	private furnitureControl: TransformControls;
+	private roomInfo: RoomInfo;
+	private graphicsQuality: 'cheep' | 'low' | 'medium' | 'high' | 'ultra';
+	private roomObj: THREE.Object3D;
+	private objects: THREE.Object3D[] = [];
+	private selectedObject: THREE.Object3D = null;
+	private onChangeSelect: Function;
+	private isTransformMode = false;
+	private renderFrameRequestId: number;
+
+	private get canvas(): HTMLCanvasElement {
+		return this.renderer.domElement;
+	}
+
+	private get furnitures(): Furniture[] {
+		return this.roomInfo.furnitures;
+	}
+
+	private set furnitures(furnitures: Furniture[]) {
+		this.roomInfo.furnitures = furnitures;
+	}
+
+	private get enableShadow() {
+		return this.graphicsQuality != 'cheep';
+	}
+
+	private get usePostFXs() {
+		return this.graphicsQuality !== 'cheep' && this.graphicsQuality !== 'low';
+	}
+
+	private get shadowQuality() {
+		return (
+			this.graphicsQuality === 'ultra' ? 16384 :
+			this.graphicsQuality === 'high' ? 8192 :
+			this.graphicsQuality === 'medium' ? 4096 :
+			this.graphicsQuality === 'low' ? 1024 :
+			0); // cheep
+	}
+
+	constructor(user, isMyRoom, roomInfo: RoomInfo, container: Element, options: Options) {
+		this.roomInfo = roomInfo;
+		this.graphicsQuality = options.graphicsQuality;
+		this.onChangeSelect = options.onChangeSelect;
+
+		this.clock = new THREE.Clock(true);
+
+		//#region Init a scene
+		this.scene = new THREE.Scene();
+
+		const width = container.clientWidth;
+		const height = container.clientHeight;
+
+		//#region Init a renderer
+		this.renderer = new THREE.WebGLRenderer({
+			antialias: false,
+			stencil: false,
+			alpha: false,
+			powerPreference:
+				this.graphicsQuality === 'ultra' ? 'high-performance' :
+				this.graphicsQuality === 'high' ? 'high-performance' :
+				this.graphicsQuality === 'medium' ? 'default' :
+				this.graphicsQuality === 'low' ? 'low-power' :
+				'low-power' // cheep
+		});
+
+		this.renderer.setPixelRatio(window.devicePixelRatio);
+		this.renderer.setSize(width, height);
+		this.renderer.autoClear = false;
+		this.renderer.setClearColor(new THREE.Color(0x051f2d));
+		this.renderer.shadowMap.enabled = this.enableShadow;
+		this.renderer.shadowMap.type =
+			this.graphicsQuality === 'ultra' ? THREE.PCFSoftShadowMap :
+			this.graphicsQuality === 'high' ? THREE.PCFSoftShadowMap :
+			this.graphicsQuality === 'medium' ? THREE.PCFShadowMap :
+			this.graphicsQuality === 'low' ? THREE.BasicShadowMap :
+			THREE.BasicShadowMap; // cheep
+
+		container.insertBefore(this.canvas, container.firstChild);
+		//#endregion
+
+		//#region Init a camera
+		this.camera = options.useOrthographicCamera
+			? new THREE.OrthographicCamera(
+				width / - 2, width / 2, height / 2, height / - 2, -10, 10)
+			: new THREE.PerspectiveCamera(45, width / height);
+
+		if (options.useOrthographicCamera) {
+			this.camera.position.x = 2;
+			this.camera.position.y = 2;
+			this.camera.position.z = 2;
+			this.camera.zoom = 100;
+			this.camera.updateProjectionMatrix();
+		} else {
+			this.camera.position.x = 5;
+			this.camera.position.y = 2;
+			this.camera.position.z = 5;
+		}
+
+		this.scene.add(this.camera);
+		//#endregion
+
+		//#region AmbientLight
+		const ambientLight = new THREE.AmbientLight(0xffffff, 1);
+		this.scene.add(ambientLight);
+		//#endregion
+
+		if (this.graphicsQuality !== 'cheep') {
+			//#region Room light
+			const roomLight = new THREE.SpotLight(0xffffff, 0.1);
+
+			roomLight.position.set(0, 8, 0);
+			roomLight.castShadow = this.enableShadow;
+			roomLight.shadow.bias = -0.0001;
+			roomLight.shadow.mapSize.width = this.shadowQuality;
+			roomLight.shadow.mapSize.height = this.shadowQuality;
+			roomLight.shadow.camera.near = 0.1;
+			roomLight.shadow.camera.far = 9;
+			roomLight.shadow.camera.fov = 45;
+
+			this.scene.add(roomLight);
+			//#endregion
+		}
+
+		//#region Out light
+		const outLight1 = new THREE.SpotLight(0xffffff, 0.4);
+		outLight1.position.set(9, 3, -2);
+		outLight1.castShadow = this.enableShadow;
+		outLight1.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
+		outLight1.shadow.mapSize.width = this.shadowQuality;
+		outLight1.shadow.mapSize.height = this.shadowQuality;
+		outLight1.shadow.camera.near = 6;
+		outLight1.shadow.camera.far = 15;
+		outLight1.shadow.camera.fov = 45;
+		this.scene.add(outLight1);
+
+		const outLight2 = new THREE.SpotLight(0xffffff, 0.2);
+		outLight2.position.set(-2, 3, 9);
+		outLight2.castShadow = false;
+		outLight2.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
+		outLight2.shadow.camera.near = 6;
+		outLight2.shadow.camera.far = 15;
+		outLight2.shadow.camera.fov = 45;
+		this.scene.add(outLight2);
+		//#endregion
+
+		//#region Init a controller
+		this.controls = new OrbitControls(this.camera, this.canvas);
+
+		this.controls.target.set(0, 1, 0);
+		this.controls.enableZoom = true;
+		this.controls.enablePan = isMyRoom;
+		this.controls.minPolarAngle = 0;
+		this.controls.maxPolarAngle = Math.PI / 2;
+		this.controls.minAzimuthAngle = 0;
+		this.controls.maxAzimuthAngle = Math.PI / 2;
+		this.controls.enableDamping = true;
+		this.controls.dampingFactor = 0.2;
+		//#endregion
+
+		//#region POST FXs
+		if (!this.usePostFXs) {
+			this.composer = null;
+		} else {
+			const renderTarget = new THREE.WebGLRenderTarget(width, height, {
+				minFilter: THREE.LinearFilter,
+				magFilter: THREE.LinearFilter,
+				format: THREE.RGBFormat,
+				stencilBuffer: false,
+			});
+
+			const fxaa = new ShaderPass(FXAAShader);
+			fxaa.uniforms['resolution'].value = new THREE.Vector2(1 / width, 1 / height);
+			fxaa.renderToScreen = true;
+
+			this.composer = new EffectComposer(this.renderer, renderTarget);
+			this.composer.addPass(new RenderPass(this.scene, this.camera));
+			if (this.graphicsQuality === 'ultra') {
+				this.composer.addPass(new BloomPass(0.25, 30, 128.0, 512));
+			}
+			this.composer.addPass(fxaa);
+		}
+		//#endregion
+		//#endregion
+
+		//#region Label
+		//#region Avatar
+		const avatarUrl = `/proxy/?${urlQuery({ url: user.avatarUrl })}`;
+
+		const textureLoader = new THREE.TextureLoader();
+		textureLoader.crossOrigin = 'anonymous';
+
+		const iconTexture = textureLoader.load(avatarUrl);
+		iconTexture.wrapS = THREE.RepeatWrapping;
+		iconTexture.wrapT = THREE.RepeatWrapping;
+		iconTexture.anisotropy = 16;
+
+		const avatarMaterial = new THREE.MeshBasicMaterial({
+			map: iconTexture,
+			side: THREE.DoubleSide,
+			alphaTest: 0.5
+		});
+
+		const iconGeometry = new THREE.PlaneGeometry(1, 1);
+
+		const avatarObject = new THREE.Mesh(iconGeometry, avatarMaterial);
+		avatarObject.position.set(-3, 2.5, 2);
+		avatarObject.rotation.y = Math.PI / 2;
+		avatarObject.castShadow = false;
+
+		this.scene.add(avatarObject);
+		//#endregion
+
+		//#region Username
+		const name = user.username;
+
+		new THREE.FontLoader().load('/assets/fonts/helvetiker_regular.typeface.json', font => {
+			const nameGeometry = new THREE.TextGeometry(name, {
+				size: 0.5,
+				height: 0,
+				curveSegments: 8,
+				font: font,
+				bevelThickness: 0,
+				bevelSize: 0,
+				bevelEnabled: false
+			});
+
+			const nameMaterial = new THREE.MeshLambertMaterial({
+				color: 0xffffff
+			});
+
+			const nameObject = new THREE.Mesh(nameGeometry, nameMaterial);
+			nameObject.position.set(-3, 2.25, 1.25);
+			nameObject.rotation.y = Math.PI / 2;
+			nameObject.castShadow = false;
+
+			this.scene.add(nameObject);
+		});
+		//#endregion
+		//#endregion
+
+		//#region Interaction
+		if (isMyRoom) {
+			this.furnitureControl = new TransformControls(this.camera, this.canvas);
+			this.scene.add(this.furnitureControl);
+
+			// Hover highlight
+			this.canvas.onmousemove = this.onmousemove;
+
+			// Click
+			this.canvas.onmousedown = this.onmousedown;
+		}
+		//#endregion
+
+		//#region Init room
+		this.loadRoom();
+		//#endregion
+
+		//#region Load furnitures
+		for (const furniture of this.furnitures) {
+			this.loadFurniture(furniture).then(obj => {
+				this.scene.add(obj.scene);
+				this.objects.push(obj.scene);
+			});
+		}
+		//#endregion
+
+		// Start render
+		if (this.usePostFXs) {
+			this.renderWithPostFXs();
+		} else {
+			this.renderWithoutPostFXs();
+		}
+	}
+
+	@autobind
+	private renderWithoutPostFXs() {
+		this.renderFrameRequestId =
+			window.requestAnimationFrame(this.renderWithoutPostFXs);
+
+		// Update animations
+		const clock = this.clock.getDelta();
+		for (const mixer of this.mixers) {
+			mixer.update(clock);
+		}
+
+		this.controls.update();
+		this.renderer.render(this.scene, this.camera);
+	}
+
+	@autobind
+	private renderWithPostFXs() {
+		this.renderFrameRequestId =
+			window.requestAnimationFrame(this.renderWithPostFXs);
+
+		// Update animations
+		const clock = this.clock.getDelta();
+		for (const mixer of this.mixers) {
+			mixer.update(clock);
+		}
+
+		this.controls.update();
+		this.renderer.clear();
+		this.composer.render();
+	}
+
+	@autobind
+	private loadRoom() {
+		const type = this.roomInfo.roomType;
+		new GLTFLoader().load(`/client-assets/room/rooms/${type}/${type}.glb`, gltf => {
+			gltf.scene.traverse(child => {
+				if (!(child instanceof THREE.Mesh)) return;
+
+				child.receiveShadow = this.enableShadow;
+
+				child.material = new THREE.MeshLambertMaterial({
+					color: (child.material as THREE.MeshStandardMaterial).color,
+					map: (child.material as THREE.MeshStandardMaterial).map,
+					name: (child.material as THREE.MeshStandardMaterial).name,
+				});
+
+				// 異方性フィルタリング
+				if ((child.material as THREE.MeshLambertMaterial).map && this.graphicsQuality !== 'cheep') {
+					(child.material as THREE.MeshLambertMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
+					(child.material as THREE.MeshLambertMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
+					(child.material as THREE.MeshLambertMaterial).map.anisotropy = 8;
+				}
+			});
+
+			gltf.scene.position.set(0, 0, 0);
+
+			this.scene.add(gltf.scene);
+			this.roomObj = gltf.scene;
+			if (this.roomInfo.roomType === 'default') {
+				this.applyCarpetColor();
+			}
+		});
+	}
+
+	@autobind
+	private loadFurniture(furniture: Furniture) {
+		const def = furnitureDefs.find(d => d.id === furniture.type);
+		return new Promise<GLTF>((res, rej) => {
+			const loader = new GLTFLoader();
+			loader.load(`/client-assets/room/furnitures/${furniture.type}/${furniture.type}.glb`, gltf => {
+				const model = gltf.scene;
+
+				// Load animation
+				if (gltf.animations.length > 0) {
+					const mixer = new THREE.AnimationMixer(model);
+					this.mixers.push(mixer);
+					for (const clip of gltf.animations) {
+						mixer.clipAction(clip).play();
+					}
+				}
+
+				model.name = furniture.id;
+				model.position.x = furniture.position.x;
+				model.position.y = furniture.position.y;
+				model.position.z = furniture.position.z;
+				model.rotation.x = furniture.rotation.x;
+				model.rotation.y = furniture.rotation.y;
+				model.rotation.z = furniture.rotation.z;
+
+				model.traverse(child => {
+					if (!(child instanceof THREE.Mesh)) return;
+					child.castShadow = this.enableShadow;
+					child.receiveShadow = this.enableShadow;
+					(child.material as THREE.MeshStandardMaterial).metalness = 0;
+
+					// 異方性フィルタリング
+					if ((child.material as THREE.MeshStandardMaterial).map && this.graphicsQuality !== 'cheep') {
+						(child.material as THREE.MeshStandardMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
+						(child.material as THREE.MeshStandardMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
+						(child.material as THREE.MeshStandardMaterial).map.anisotropy = 8;
+					}
+				});
+
+				if (def.color) { // カスタムカラー
+					this.applyCustomColor(model);
+				}
+
+				if (def.texture) { // カスタムテクスチャ
+					this.applyCustomTexture(model);
+				}
+
+				res(gltf);
+			}, null, rej);
+		});
+	}
+
+	@autobind
+	private applyCarpetColor() {
+		this.roomObj.traverse(child => {
+			if (!(child instanceof THREE.Mesh)) return;
+			if (child.material &&
+				(child.material as THREE.MeshStandardMaterial).name &&
+				(child.material as THREE.MeshStandardMaterial).name === 'Carpet'
+			) {
+				const colorHex = parseInt(this.roomInfo.carpetColor.substr(1), 16);
+				(child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
+			}
+		});
+	}
+
+	@autobind
+	private applyCustomColor(model: THREE.Object3D) {
+		const furniture = this.furnitures.find(furniture => furniture.id === model.name);
+		const def = furnitureDefs.find(d => d.id === furniture.type);
+		if (def.color == null) return;
+		model.traverse(child => {
+			if (!(child instanceof THREE.Mesh)) return;
+			for (const t of Object.keys(def.color)) {
+				if (!child.material ||
+					!(child.material as THREE.MeshStandardMaterial).name ||
+					(child.material as THREE.MeshStandardMaterial).name !== t
+				) continue;
+
+				const prop = def.color[t];
+				const val = furniture.props ? furniture.props[prop] : undefined;
+
+				if (val == null) continue;
+
+				const colorHex = parseInt(val.substr(1), 16);
+				(child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
+			}
+		});
+	}
+
+	@autobind
+	private applyCustomTexture(model: THREE.Object3D) {
+		const furniture = this.furnitures.find(furniture => furniture.id === model.name);
+		const def = furnitureDefs.find(d => d.id === furniture.type);
+		if (def.texture == null) return;
+
+		model.traverse(child => {
+			if (!(child instanceof THREE.Mesh)) return;
+			for (const t of Object.keys(def.texture)) {
+				if (child.name !== t) continue;
+
+				const prop = def.texture[t].prop;
+				const val = furniture.props ? furniture.props[prop] : undefined;
+
+				if (val == null) continue;
+
+				const canvas = document.createElement('canvas');
+				canvas.height = 1024;
+				canvas.width = 1024;
+
+				child.material = new THREE.MeshLambertMaterial({
+					emissive: 0x111111,
+					side: THREE.DoubleSide,
+					alphaTest: 0.5,
+				});
+
+				const img = new Image();
+				img.crossOrigin = 'anonymous';
+				img.onload = () => {
+					const uvInfo = def.texture[t].uv;
+
+					const ctx = canvas.getContext('2d');
+					ctx.drawImage(img,
+						0, 0, img.width, img.height,
+						uvInfo.x, uvInfo.y, uvInfo.width, uvInfo.height);
+
+					const texture = new THREE.Texture(canvas);
+					texture.wrapS = THREE.RepeatWrapping;
+					texture.wrapT = THREE.RepeatWrapping;
+					texture.anisotropy = 16;
+					texture.flipY = false;
+
+					(child.material as THREE.MeshLambertMaterial).map = texture;
+					(child.material as THREE.MeshLambertMaterial).needsUpdate = true;
+					(child.material as THREE.MeshLambertMaterial).map.needsUpdate = true;
+				};
+				img.src = val;
+			}
+		});
+	}
+
+	@autobind
+	private onmousemove(ev: MouseEvent) {
+		if (this.isTransformMode) return;
+
+		const rect = (ev.target as HTMLElement).getBoundingClientRect();
+		const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
+		const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
+		const pos = new THREE.Vector2(x, y);
+
+		this.camera.updateMatrixWorld();
+
+		const raycaster = new THREE.Raycaster();
+		raycaster.setFromCamera(pos, this.camera);
+
+		const intersects = raycaster.intersectObjects(this.objects, true);
+
+		for (const object of this.objects) {
+			if (this.isSelectedObject(object)) continue;
+			object.traverse(child => {
+				if (child instanceof THREE.Mesh) {
+					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
+				}
+			});
+		}
+
+		if (intersects.length > 0) {
+			const intersected = this.getRoot(intersects[0].object);
+			if (this.isSelectedObject(intersected)) return;
+			intersected.traverse(child => {
+				if (child instanceof THREE.Mesh) {
+					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x191919);
+				}
+			});
+		}
+	}
+
+	@autobind
+	private onmousedown(ev: MouseEvent) {
+		if (this.isTransformMode) return;
+		if (ev.target !== this.canvas || ev.button !== 0) return;
+
+		const rect = (ev.target as HTMLElement).getBoundingClientRect();
+		const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
+		const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
+		const pos = new THREE.Vector2(x, y);
+
+		this.camera.updateMatrixWorld();
+
+		const raycaster = new THREE.Raycaster();
+		raycaster.setFromCamera(pos, this.camera);
+
+		const intersects = raycaster.intersectObjects(this.objects, true);
+
+		for (const object of this.objects) {
+			object.traverse(child => {
+				if (child instanceof THREE.Mesh) {
+					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
+				}
+			});
+		}
+
+		if (intersects.length > 0) {
+			const selectedObj = this.getRoot(intersects[0].object);
+			this.selectFurniture(selectedObj);
+		} else {
+			this.selectedObject = null;
+			this.onChangeSelect(null);
+		}
+	}
+
+	@autobind
+	private getRoot(obj: THREE.Object3D): THREE.Object3D {
+		let found = false;
+		let x = obj.parent;
+		while (!found) {
+			if (x.parent.parent == null) {
+				found = true;
+			} else {
+				x = x.parent;
+			}
+		}
+		return x;
+	}
+
+	@autobind
+	private isSelectedObject(obj: THREE.Object3D): boolean {
+		if (this.selectedObject == null) {
+			return false;
+		} else {
+			return obj.name === this.selectedObject.name;
+		}
+	}
+
+	@autobind
+	private selectFurniture(obj: THREE.Object3D) {
+		this.selectedObject = obj;
+		this.onChangeSelect(obj);
+		obj.traverse(child => {
+			if (child instanceof THREE.Mesh) {
+				(child.material as THREE.MeshStandardMaterial).emissive.setHex(0xff0000);
+			}
+		});
+	}
+
+	/**
+	 * 家具の移動/回転モードにします
+	 * @param type 移動か回転か
+	 */
+	@autobind
+	public enterTransformMode(type: 'translate' | 'rotate') {
+		this.isTransformMode = true;
+		this.furnitureControl.setMode(type);
+		this.furnitureControl.attach(this.selectedObject);
+		this.controls.enableRotate = false;
+	}
+
+	/**
+	 * 家具の移動/回転モードを終了します
+	 */
+	@autobind
+	public exitTransformMode() {
+		this.isTransformMode = false;
+		this.furnitureControl.detach();
+		this.controls.enableRotate = true;
+	}
+
+	/**
+	 * 家具プロパティを更新します
+	 * @param key プロパティ名
+	 * @param value 値
+	 */
+	@autobind
+	public updateProp(key: string, value: any) {
+		const furniture = this.furnitures.find(furniture => furniture.id === this.selectedObject.name);
+		if (furniture.props == null) furniture.props = {};
+		furniture.props[key] = value;
+		this.applyCustomColor(this.selectedObject);
+		this.applyCustomTexture(this.selectedObject);
+	}
+
+	/**
+	 * 部屋に家具を追加します
+	 * @param type 家具の種類
+	 */
+	@autobind
+	public addFurniture(type: string) {
+		const furniture = {
+			id: uuid(),
+			type: type,
+			position: {
+				x: 0,
+				y: 0,
+				z: 0,
+			},
+			rotation: {
+				x: 0,
+				y: 0,
+				z: 0,
+			},
+		};
+
+		this.furnitures.push(furniture);
+
+		this.loadFurniture(furniture).then(obj => {
+			this.scene.add(obj.scene);
+			this.objects.push(obj.scene);
+		});
+	}
+
+	/**
+	 * 現在選択されている家具を部屋から削除します
+	 */
+	@autobind
+	public removeFurniture() {
+		this.exitTransformMode();
+		const obj = this.selectedObject;
+		this.scene.remove(obj);
+		this.objects = this.objects.filter(object => object.name !== obj.name);
+		this.furnitures = this.furnitures.filter(furniture => furniture.id !== obj.name);
+		this.selectedObject = null;
+		this.onChangeSelect(null);
+	}
+
+	/**
+	 * 全ての家具を部屋から削除します
+	 */
+	@autobind
+	public removeAllFurnitures() {
+		this.exitTransformMode();
+		for (const obj of this.objects) {
+			this.scene.remove(obj);
+		}
+		this.objects = [];
+		this.furnitures = [];
+		this.selectedObject = null;
+		this.onChangeSelect(null);
+	}
+
+	/**
+	 * 部屋の床の色を変更します
+	 * @param color 色
+	 */
+	@autobind
+	public updateCarpetColor(color: string) {
+		this.roomInfo.carpetColor = color;
+		this.applyCarpetColor();
+	}
+
+	/**
+	 * 部屋の種類を変更します
+	 * @param type 種類
+	 */
+	@autobind
+	public changeRoomType(type: string) {
+		this.roomInfo.roomType = type;
+		this.scene.remove(this.roomObj);
+		this.loadRoom();
+	}
+
+	/**
+	 * 部屋データを取得します
+	 */
+	@autobind
+	public getRoomInfo() {
+		for (const obj of this.objects) {
+			const furniture = this.furnitures.find(f => f.id === obj.name);
+			furniture.position.x = obj.position.x;
+			furniture.position.y = obj.position.y;
+			furniture.position.z = obj.position.z;
+			furniture.rotation.x = obj.rotation.x;
+			furniture.rotation.y = obj.rotation.y;
+			furniture.rotation.z = obj.rotation.z;
+		}
+
+		return this.roomInfo;
+	}
+
+	/**
+	 * 選択されている家具を取得します
+	 */
+	@autobind
+	public getSelectedObject() {
+		return this.selectedObject;
+	}
+
+	@autobind
+	public findFurnitureById(id: string) {
+		return this.furnitures.find(furniture => furniture.id === id);
+	}
+
+	/**
+	 * レンダリングを終了します
+	 */
+	@autobind
+	public destroy() {
+		// Stop render loop
+		window.cancelAnimationFrame(this.renderFrameRequestId);
+
+		this.controls.dispose();
+		this.scene.dispose();
+	}
+}
diff --git a/packages/client/src/scripts/scroll.ts b/packages/client/src/scripts/scroll.ts
new file mode 100644
index 0000000000..621fe88105
--- /dev/null
+++ b/packages/client/src/scripts/scroll.ts
@@ -0,0 +1,80 @@
+type ScrollBehavior = 'auto' | 'smooth' | 'instant';
+
+export function getScrollContainer(el: Element | null): Element | null {
+	if (el == null || el.tagName === 'BODY') return null;
+	const overflow = window.getComputedStyle(el).getPropertyValue('overflow');
+	if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる
+		return el;
+	} else {
+		return getScrollContainer(el.parentElement);
+	}
+}
+
+export function getScrollPosition(el: Element | null): number {
+	const container = getScrollContainer(el);
+	return container == null ? window.scrollY : container.scrollTop;
+}
+
+export function isTopVisible(el: Element | null): boolean {
+	const scrollTop = getScrollPosition(el);
+	const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
+
+	return scrollTop <= topPosition;
+}
+
+export function onScrollTop(el: Element, cb) {
+	const container = getScrollContainer(el) || window;
+	const onScroll = ev => {
+		if (!document.body.contains(el)) return;
+		if (isTopVisible(el)) {
+			cb();
+			container.removeEventListener('scroll', onScroll);
+		}
+	};
+	container.addEventListener('scroll', onScroll, { passive: true });
+}
+
+export function onScrollBottom(el: Element, cb) {
+	const container = getScrollContainer(el) || window;
+	const onScroll = ev => {
+		if (!document.body.contains(el)) return;
+		const pos = getScrollPosition(el);
+		if (pos + el.clientHeight > el.scrollHeight - 1) {
+			cb();
+			container.removeEventListener('scroll', onScroll);
+		}
+	};
+	container.addEventListener('scroll', onScroll, { passive: true });
+}
+
+export function scroll(el: Element, options: {
+	top?: number;
+	left?: number;
+	behavior?: ScrollBehavior;
+}) {
+	const container = getScrollContainer(el);
+	if (container == null) {
+		window.scroll(options);
+	} else {
+		container.scroll(options);
+	}
+}
+
+export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
+	scroll(el, { top: 0, ...options });
+}
+
+export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
+	scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する
+}
+
+export function isBottom(el: Element, asobi = 0) {
+	const container = getScrollContainer(el);
+	const current = container
+		? el.scrollTop + el.offsetHeight
+		: window.scrollY + window.innerHeight;
+	const max = container
+		? el.scrollHeight
+		: document.body.offsetHeight;
+	return current >= (max - asobi);
+}
diff --git a/packages/client/src/scripts/search.ts b/packages/client/src/scripts/search.ts
new file mode 100644
index 0000000000..b28cccfab7
--- /dev/null
+++ b/packages/client/src/scripts/search.ts
@@ -0,0 +1,64 @@
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { router } from '@/router';
+
+export async function search() {
+	const { canceled, result: query } = await os.dialog({
+		title: i18n.locale.search,
+		input: true
+	});
+	if (canceled || query == null || query === '') return;
+
+	const q = query.trim();
+
+	if (q.startsWith('@') && !q.includes(' ')) {
+		router.push(`/${q}`);
+		return;
+	}
+
+	if (q.startsWith('#')) {
+		router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
+		return;
+	}
+
+	// like 2018/03/12
+	if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) {
+		const date = new Date(q.replace(/-/g, '/'));
+
+		// 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは
+		// 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので
+		// 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の
+		// 結果になってしまい、2018/03/12 のコンテンツは含まれない)
+		if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) {
+			date.setHours(23, 59, 59, 999);
+		}
+
+		// TODO
+		//v.$root.$emit('warp', date);
+		os.dialog({
+			icon: 'fas fa-history',
+			iconOnly: true, autoClose: true
+		});
+		return;
+	}
+
+	if (q.startsWith('https://')) {
+		const promise = os.api('ap/show', {
+			uri: q
+		});
+
+		os.promiseDialog(promise, null, null, i18n.locale.fetchingAsApObject);
+
+		const res = await promise;
+
+		if (res.type === 'User') {
+			router.push(`/@${res.object.username}@${res.object.host}`);
+		} else if (res.type === 'Note') {
+			router.push(`/notes/${res.object.id}`);
+		}
+
+		return;
+	}
+
+	router.push(`/search?q=${encodeURIComponent(q)}`);
+}
diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts
new file mode 100644
index 0000000000..5fbc545b26
--- /dev/null
+++ b/packages/client/src/scripts/select-file.ts
@@ -0,0 +1,89 @@
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
+
+export function selectFile(src: any, label: string | null, multiple = false) {
+	return new Promise((res, rej) => {
+		const chooseFileFromPc = () => {
+			const input = document.createElement('input');
+			input.type = 'file';
+			input.multiple = multiple;
+			input.onchange = () => {
+				const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder));
+
+				Promise.all(promises).then(driveFiles => {
+					res(multiple ? driveFiles : driveFiles[0]);
+				}).catch(e => {
+					os.dialog({
+						type: 'error',
+						text: e
+					});
+				});
+
+				// 一応廃棄
+				(window as any).__misskey_input_ref__ = null;
+			};
+
+			// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
+			// iOS Safari で正常に動かす為のおまじない
+			(window as any).__misskey_input_ref__ = input;
+
+			input.click();
+		};
+
+		const chooseFileFromDrive = () => {
+			os.selectDriveFile(multiple).then(files => {
+				res(files);
+			});
+		};
+
+		const chooseFileFromUrl = () => {
+			os.dialog({
+				title: i18n.locale.uploadFromUrl,
+				input: {
+					placeholder: i18n.locale.uploadFromUrlDescription
+				}
+			}).then(({ canceled, result: url }) => {
+				if (canceled) return;
+
+				const marker = Math.random().toString(); // TODO: UUIDとか使う
+
+				const connection = os.stream.useChannel('main');
+				connection.on('urlUploadFinished', data => {
+					if (data.marker === marker) {
+						res(multiple ? [data.file] : data.file);
+						connection.dispose();
+					}
+				});
+
+				os.api('drive/files/upload-from-url', {
+					url: url,
+					folderId: defaultStore.state.uploadFolder,
+					marker
+				});
+
+				os.dialog({
+					title: i18n.locale.uploadFromUrlRequested,
+					text: i18n.locale.uploadFromUrlMayTakeTime
+				});
+			});
+		};
+
+		os.popupMenu([label ? {
+			text: label,
+			type: 'label'
+		} : undefined, {
+			text: i18n.locale.upload,
+			icon: 'fas fa-upload',
+			action: chooseFileFromPc
+		}, {
+			text: i18n.locale.fromDrive,
+			icon: 'fas fa-cloud',
+			action: chooseFileFromDrive
+		}, {
+			text: i18n.locale.fromUrl,
+			icon: 'fas fa-link',
+			action: chooseFileFromUrl
+		}], src);
+	});
+}
diff --git a/packages/client/src/scripts/show-suspended-dialog.ts b/packages/client/src/scripts/show-suspended-dialog.ts
new file mode 100644
index 0000000000..3bc4800030
--- /dev/null
+++ b/packages/client/src/scripts/show-suspended-dialog.ts
@@ -0,0 +1,10 @@
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+
+export function showSuspendedDialog() {
+	return os.dialog({
+		type: 'error',
+		title: i18n.locale.yourAccountSuspendedTitle,
+		text: i18n.locale.yourAccountSuspendedDescription
+	});
+}
diff --git a/packages/client/src/scripts/sound.ts b/packages/client/src/scripts/sound.ts
new file mode 100644
index 0000000000..2b8279b3df
--- /dev/null
+++ b/packages/client/src/scripts/sound.ts
@@ -0,0 +1,34 @@
+import { ColdDeviceStorage } from '@/store';
+
+const cache = new Map<string, HTMLAudioElement>();
+
+export function getAudio(file: string, useCache = true): HTMLAudioElement {
+	let audio: HTMLAudioElement;
+	if (useCache && cache.has(file)) {
+		audio = cache.get(file);
+	} else {
+		audio = new Audio(`/client-assets/sounds/${file}.mp3`);
+		if (useCache) cache.set(file, audio);
+	}
+	return audio;
+}
+
+export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
+	const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
+	audio.volume = masterVolume - ((1 - volume) * masterVolume);
+	return audio;
+}
+
+export function play(type: string) {
+	const sound = ColdDeviceStorage.get('sound_' + type as any);
+	if (sound.type == null) return;
+	playFile(sound.type, sound.volume);
+}
+
+export function playFile(file: string, volume: number) {
+	const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
+	if (masterVolume === 0) return;
+
+	const audio = setVolume(getAudio(file), volume);
+	audio.play();
+}
diff --git a/packages/client/src/scripts/sticky-sidebar.ts b/packages/client/src/scripts/sticky-sidebar.ts
new file mode 100644
index 0000000000..c67b8f37ac
--- /dev/null
+++ b/packages/client/src/scripts/sticky-sidebar.ts
@@ -0,0 +1,50 @@
+export class StickySidebar {
+	private lastScrollTop = 0;
+	private container: HTMLElement;
+	private el: HTMLElement;
+	private spacer: HTMLElement;
+	private marginTop: number;
+	private isTop = false;
+	private isBottom = false;
+	private offsetTop: number;
+	private globalHeaderHeight: number = 59;
+
+	constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) {
+		this.container = container;
+		this.el = this.container.children[0] as HTMLElement;
+		this.el.style.position = 'sticky';
+		this.spacer = document.createElement('div');
+		this.container.prepend(this.spacer);
+		this.marginTop = marginTop;
+		this.offsetTop = this.container.getBoundingClientRect().top;
+		this.globalHeaderHeight = globalHeaderHeight;
+	}
+
+	public calc(scrollTop: number) {
+		if (scrollTop > this.lastScrollTop) { // downscroll
+			const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight);
+			this.el.style.bottom = null;
+			this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`;
+
+			this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight);
+
+			if (this.isTop) {
+				this.isTop = false;
+				this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`;
+			}
+		} else { // upscroll
+			const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight;
+			this.el.style.top = null;
+			this.el.style.bottom = `${-overflow}px`;
+
+			this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop;
+
+			if (this.isBottom) {
+				this.isBottom = false;
+				this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`;
+			}
+		}
+
+		this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
+	}
+}
diff --git a/packages/client/src/scripts/theme-editor.ts b/packages/client/src/scripts/theme-editor.ts
new file mode 100644
index 0000000000..3d69d2836a
--- /dev/null
+++ b/packages/client/src/scripts/theme-editor.ts
@@ -0,0 +1,81 @@
+import { v4 as uuid} from 'uuid';
+
+import { themeProps, Theme } from './theme';
+
+export type Default = null;
+export type Color = string;
+export type FuncName = 'alpha' | 'darken' | 'lighten';
+export type Func = { type: 'func'; name: FuncName; arg: number; value: string; };
+export type RefProp = { type: 'refProp'; key: string; };
+export type RefConst = { type: 'refConst'; key: string; };
+export type Css = { type: 'css'; value: string; };
+
+export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default;
+
+export type ThemeViewModel = [ string, ThemeValue ][];
+
+export const fromThemeString = (str?: string) : ThemeValue => {
+	if (!str) return null;
+	if (str.startsWith(':')) {
+		const parts = str.slice(1).split('<');
+		const name = parts[0] as FuncName;
+		const arg = parseFloat(parts[1]);
+		const value = parts[2].startsWith('@') ? parts[2].slice(1) : '';
+		return { type: 'func', name, arg, value };
+	} else if (str.startsWith('@')) {
+		return {
+			type: 'refProp',
+			key: str.slice(1),
+		};
+	} else if (str.startsWith('$')) {
+		return {
+			type: 'refConst',
+			key: str.slice(1),
+		};
+	} else if (str.startsWith('"')) {
+		return {
+			type: 'css',
+			value: str.substr(1).trim(),
+		};
+	} else {
+		return str;
+	}
+};
+
+export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => {
+	if (typeof value === 'string') return value;
+	switch (value.type) {
+		case 'func': return `:${value.name}<${value.arg}<@${value.value}`;
+		case 'refProp': return `@${value.key}`;
+		case 'refConst': return `$${value.key}`;
+		case 'css': return `" ${value.value}`;
+	}
+};
+
+export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => {
+	const props = { } as { [key: string]: string };
+	for (const [ key, value ] of vm) {
+		if (value === null) continue;
+		props[key] = toThemeString(value);
+	}
+
+	return {
+		id: uuid(),
+		name, desc, author, props, base
+	};
+};
+
+export const convertToViewModel = (theme: Theme): ThemeViewModel => {
+	const vm: ThemeViewModel = [];
+	// プロパティの登録
+	vm.push(...themeProps.map(key => [ key, fromThemeString(theme.props[key])] as [ string, ThemeValue ]));
+
+	// 定数の登録
+	const consts = Object
+		.keys(theme.props)
+		.filter(k => k.startsWith('$'))
+		.map(k => [ k, fromThemeString(theme.props[k]) ] as [ string, ThemeValue ]);
+
+		vm.push(...consts);
+	return vm;
+};
diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts
new file mode 100644
index 0000000000..3b7f003d0f
--- /dev/null
+++ b/packages/client/src/scripts/theme.ts
@@ -0,0 +1,127 @@
+import { globalEvents } from '@/events';
+import * as tinycolor from 'tinycolor2';
+
+export type Theme = {
+	id: string;
+	name: string;
+	author: string;
+	desc?: string;
+	base?: 'dark' | 'light';
+	props: Record<string, string>;
+};
+
+export const lightTheme: Theme = require('@/themes/_light.json5');
+export const darkTheme: Theme = require('@/themes/_dark.json5');
+
+export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
+
+export const builtinThemes = [
+	require('@/themes/l-light.json5'),
+	require('@/themes/l-apricot.json5'),
+	require('@/themes/l-rainy.json5'),
+	require('@/themes/l-vivid.json5'),
+	require('@/themes/l-sushi.json5'),
+
+	require('@/themes/d-dark.json5'),
+	require('@/themes/d-persimmon.json5'),
+	require('@/themes/d-astro.json5'),
+	require('@/themes/d-future.json5'),
+	require('@/themes/d-botanical.json5'),
+	require('@/themes/d-pumpkin.json5'),
+	require('@/themes/d-black.json5'),
+] as Theme[];
+
+let timeout = null;
+
+export function applyTheme(theme: Theme, persist = true) {
+	if (timeout) clearTimeout(timeout);
+
+	document.documentElement.classList.add('_themeChanging_');
+
+	timeout = setTimeout(() => {
+		document.documentElement.classList.remove('_themeChanging_');
+	}, 1000);
+
+	// Deep copy
+	const _theme = JSON.parse(JSON.stringify(theme));
+
+	if (_theme.base) {
+		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
+		_theme.props = Object.assign({}, base.props, _theme.props);
+	}
+
+	const props = compile(_theme);
+
+	for (const tag of document.head.children) {
+		if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+			tag.setAttribute('content', props['html']);
+			break;
+		}
+	}
+
+	for (const [k, v] of Object.entries(props)) {
+		document.documentElement.style.setProperty(`--${k}`, v.toString());
+	}
+
+	if (persist) {
+		localStorage.setItem('theme', JSON.stringify(props));
+	}
+
+	// 色計算など再度行えるようにクライアント全体に通知
+	globalEvents.emit('themeChanged');
+}
+
+function compile(theme: Theme): Record<string, string> {
+	function getColor(val: string): tinycolor.Instance {
+		// ref (prop)
+		if (val[0] === '@') {
+			return getColor(theme.props[val.substr(1)]);
+		}
+
+		// ref (const)
+		else if (val[0] === '$') {
+			return getColor(theme.props[val]);
+		}
+
+		// func
+		else if (val[0] === ':') {
+			const parts = val.split('<');
+			const func = parts.shift().substr(1);
+			const arg = parseFloat(parts.shift());
+			const color = getColor(parts.join('<'));
+
+			switch (func) {
+				case 'darken': return color.darken(arg);
+				case 'lighten': return color.lighten(arg);
+				case 'alpha': return color.setAlpha(arg);
+				case 'hue': return color.spin(arg);
+				case 'saturate': return color.saturate(arg);
+			}
+		}
+
+		// other case
+		return tinycolor(val);
+	}
+
+	const props = {};
+
+	for (const [k, v] of Object.entries(theme.props)) {
+		if (k.startsWith('$')) continue; // ignore const
+
+		props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
+	}
+
+	return props;
+}
+
+function genValue(c: tinycolor.Instance): string {
+	return c.toRgbString();
+}
+
+export function validateTheme(theme: Record<string, any>): boolean {
+	if (theme.id == null || typeof theme.id !== 'string') return false;
+	if (theme.name == null || typeof theme.name !== 'string') return false;
+	if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
+	if (theme.props == null || typeof theme.props !== 'object') return false;
+	return true;
+}
diff --git a/packages/client/src/scripts/time.ts b/packages/client/src/scripts/time.ts
new file mode 100644
index 0000000000..34e8b6b17c
--- /dev/null
+++ b/packages/client/src/scripts/time.ts
@@ -0,0 +1,39 @@
+const dateTimeIntervals = {
+	'day': 86400000,
+	'hour': 3600000,
+	'ms': 1,
+};
+
+export function dateUTC(time: number[]): Date {
+	const d = time.length === 2 ? Date.UTC(time[0], time[1])
+					: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
+					: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
+					: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
+					: time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
+					: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
+					: null;
+
+	if (!d) throw 'wrong number of arguments';
+
+	return new Date(d);
+}
+
+export function isTimeSame(a: Date, b: Date): boolean {
+	return a.getTime() === b.getTime();
+}
+
+export function isTimeBefore(a: Date, b: Date): boolean {
+	return (a.getTime() - b.getTime()) < 0;
+}
+
+export function isTimeAfter(a: Date, b: Date): boolean {
+	return (a.getTime() - b.getTime()) > 0;
+}
+
+export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
+	return new Date(x.getTime() + (value * dateTimeIntervals[span]));
+}
+
+export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
+	return new Date(x.getTime() - (value * dateTimeIntervals[span]));
+}
diff --git a/packages/client/src/scripts/twemoji-base.ts b/packages/client/src/scripts/twemoji-base.ts
new file mode 100644
index 0000000000..cd50311b15
--- /dev/null
+++ b/packages/client/src/scripts/twemoji-base.ts
@@ -0,0 +1 @@
+export const twemojiSvgBase = '/twemoji';
diff --git a/packages/client/src/scripts/unison-reload.ts b/packages/client/src/scripts/unison-reload.ts
new file mode 100644
index 0000000000..59af584c1b
--- /dev/null
+++ b/packages/client/src/scripts/unison-reload.ts
@@ -0,0 +1,15 @@
+// SafariがBroadcastChannel未実装なのでライブラリを使う
+import { BroadcastChannel } from 'broadcast-channel';
+
+export const reloadChannel = new BroadcastChannel<string | null>('reload');
+
+// BroadcastChannelを用いて、クライアントが一斉にreloadするようにします。
+export function unisonReload(path?: string) {
+	if (path !== undefined) {
+		reloadChannel.postMessage(path);
+		location.href = path;
+	} else {
+		reloadChannel.postMessage(null);
+		location.reload();
+	}
+}
diff --git a/packages/client/src/scripts/url.ts b/packages/client/src/scripts/url.ts
new file mode 100644
index 0000000000..c7f2b7c1e7
--- /dev/null
+++ b/packages/client/src/scripts/url.ts
@@ -0,0 +1,13 @@
+export function query(obj: {}): string {
+	const params = Object.entries(obj)
+		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
+		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
+
+	return Object.entries(params)
+		.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
+		.join('&');
+}
+
+export function appendQuery(url: string, query: string): string {
+	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
+}
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
new file mode 100644
index 0000000000..955d94a074
--- /dev/null
+++ b/packages/client/src/store.ts
@@ -0,0 +1,318 @@
+import { markRaw, ref } from 'vue';
+import { Storage } from './pizzax';
+import { Theme } from './scripts/theme';
+
+export const postFormActions = [];
+export const userActions = [];
+export const noteActions = [];
+export const noteViewInterruptors = [];
+export const notePostInterruptors = [];
+
+// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
+//       あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
+export const defaultStore = markRaw(new Storage('base', {
+	tutorial: {
+		where: 'account',
+		default: 0
+	},
+	keepCw: {
+		where: 'account',
+		default: false
+	},
+	showFullAcct: {
+		where: 'account',
+		default: false
+	},
+	rememberNoteVisibility: {
+		where: 'account',
+		default: false
+	},
+	defaultNoteVisibility: {
+		where: 'account',
+		default: 'public'
+	},
+	defaultNoteLocalOnly: {
+		where: 'account',
+		default: false
+	},
+	uploadFolder: {
+		where: 'account',
+		default: null as string | null
+	},
+	pastedFileName: {
+		where: 'account',
+		default: 'yyyy-MM-dd HH-mm-ss [{{number}}]'
+	},
+	memo: {
+		where: 'account',
+		default: null
+	},
+	reactions: {
+		where: 'account',
+		default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮']
+	},
+	mutedWords: {
+		where: 'account',
+		default: []
+	},
+	mutedAds: {
+		where: 'account',
+		default: [] as string[]
+	},
+
+	menu: {
+		where: 'deviceAccount',
+		default: [
+			'notifications',
+			'messaging',
+			'drive',
+			'followRequests',
+			'-',
+			'gallery',
+			'featured',
+			'explore',
+			'announcements',
+			'search',
+			'-',
+			'ui',
+		]
+	},
+	visibility: {
+		where: 'deviceAccount',
+		default: 'public' as 'public' | 'home' | 'followers' | 'specified'
+	},
+	localOnly: {
+		where: 'deviceAccount',
+		default: false
+	},
+	widgets: {
+		where: 'deviceAccount',
+		default: [] as {
+			name: string;
+			id: string;
+			place: string | null;
+			data: Record<string, any>;
+		}[]
+	},
+	tl: {
+		where: 'deviceAccount',
+		default: {
+			src: 'home',
+			arg: null
+		}
+	},
+
+	serverDisconnectedBehavior: {
+		where: 'device',
+		default: 'quiet' as 'quiet' | 'reload' | 'dialog'
+	},
+	nsfw: {
+		where: 'device',
+		default: 'respect' as 'respect' | 'force' | 'ignore'
+	},
+	animation: {
+		where: 'device',
+		default: true
+	},
+	animatedMfm: {
+		where: 'device',
+		default: true
+	},
+	loadRawImages: {
+		where: 'device',
+		default: false
+	},
+	imageNewTab: {
+		where: 'device',
+		default: false
+	},
+	disableShowingAnimatedImages: {
+		where: 'device',
+		default: false
+	},
+	disablePagesScript: {
+		where: 'device',
+		default: false
+	},
+	useOsNativeEmojis: {
+		where: 'device',
+		default: false
+	},
+	useBlurEffectForModal: {
+		where: 'device',
+		default: true
+	},
+	useBlurEffect: {
+		where: 'device',
+		default: true
+	},
+	showFixedPostForm: {
+		where: 'device',
+		default: false
+	},
+	enableInfiniteScroll: {
+		where: 'device',
+		default: true
+	},
+	useReactionPickerForContextMenu: {
+		where: 'device',
+		default: true
+	},
+	showGapBetweenNotesInTimeline: {
+		where: 'device',
+		default: false
+	},
+	darkMode: {
+		where: 'device',
+		default: false
+	},
+	instanceTicker: {
+		where: 'device',
+		default: 'remote' as 'none' | 'remote' | 'always'
+	},
+	reactionPickerWidth: {
+		where: 'device',
+		default: 1
+	},
+	reactionPickerHeight: {
+		where: 'device',
+		default: 1
+	},
+	recentlyUsedEmojis: {
+		where: 'device',
+		default: [] as string[]
+	},
+	recentlyUsedUsers: {
+		where: 'device',
+		default: [] as string[]
+	},
+	defaultSideView: {
+		where: 'device',
+		default: false
+	},
+	menuDisplay: {
+		where: 'device',
+		default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top'
+	},
+	reportError: {
+		where: 'device',
+		default: false
+	},
+	squareAvatars: {
+		where: 'device',
+		default: false
+	},
+	postFormWithHashtags: {
+		where: 'device',
+		default: false
+	},
+	postFormHashtags: {
+		where: 'device',
+		default: ''
+	},
+	aiChanMode: {
+		where: 'device',
+		default: false
+	},
+}));
+
+// TODO: 他のタブと永続化されたstateを同期
+
+const PREFIX = 'miux:';
+
+type Plugin = {
+	id: string;
+	name: string;
+	active: boolean;
+	configData: Record<string, any>;
+	token: string;
+	ast: any[];
+};
+
+/**
+ * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
+ */
+export class ColdDeviceStorage {
+	public static default = {
+		lightTheme: require('@/themes/l-light.json5') as Theme,
+		darkTheme: require('@/themes/d-dark.json5') as Theme,
+		syncDeviceDarkMode: true,
+		chatOpenBehavior: 'page' as 'page' | 'window' | 'popout',
+		plugins: [] as Plugin[],
+		mediaVolume: 0.5,
+		sound_masterVolume: 0.3,
+		sound_note: { type: 'syuilo/down', volume: 1 },
+		sound_noteMy: { type: 'syuilo/up', volume: 1 },
+		sound_notification: { type: 'syuilo/pope2', volume: 1 },
+		sound_chat: { type: 'syuilo/pope1', volume: 1 },
+		sound_chatBg: { type: 'syuilo/waon', volume: 1 },
+		sound_antenna: { type: 'syuilo/triple', volume: 1 },
+		sound_channel: { type: 'syuilo/square-pico', volume: 1 },
+		sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
+		sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
+		roomGraphicsQuality: 'medium' as 'cheep' | 'low' | 'medium' | 'high' | 'ultra',
+		roomUseOrthographicCamera: true,
+	};
+
+	public static watchers = [];
+
+	public static get<T extends keyof typeof ColdDeviceStorage.default>(key: T): typeof ColdDeviceStorage.default[T] {
+		// TODO: indexedDBにする
+		//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
+		//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
+		const value = localStorage.getItem(PREFIX + key);
+		if (value == null) {
+			return ColdDeviceStorage.default[key];
+		} else {
+			return JSON.parse(value);
+		}
+	}
+
+	public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
+		localStorage.setItem(PREFIX + key, JSON.stringify(value));
+
+		for (const watcher of this.watchers) {
+			if (watcher.key === key) watcher.callback(value);
+		}
+	}
+
+	public static watch(key, callback) {
+		this.watchers.push({ key, callback });
+	}
+
+	// TODO: VueのcustomRef使うと良い感じになるかも
+	public static ref<T extends keyof typeof ColdDeviceStorage.default>(key: T) {
+		const v = ColdDeviceStorage.get(key);
+		const r = ref(v);
+		// TODO: このままではwatcherがリークするので開放する方法を考える
+		this.watch(key, v => {
+			r.value = v;
+		});
+		return r;
+	}
+
+	/**
+	 * 特定のキーの、簡易的なgetter/setterを作ります
+	 * 主にvue場で設定コントロールのmodelとして使う用
+	 */
+	public static makeGetterSetter<K extends keyof typeof ColdDeviceStorage.default>(key: K) {
+		// TODO: VueのcustomRef使うと良い感じになるかも
+		const valueRef = ColdDeviceStorage.ref(key);
+		return {
+			get: () => {
+				return valueRef.value;
+			},
+			set: (value: unknown) => {
+				const val = value;
+				ColdDeviceStorage.set(key, val);
+			}
+		};
+	}
+}
+
+// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
+declare module '@vue/runtime-core' {
+	interface ComponentCustomProperties {
+		$store: typeof defaultStore;
+	}
+}
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
new file mode 100644
index 0000000000..951d5a14f3
--- /dev/null
+++ b/packages/client/src/style.scss
@@ -0,0 +1,562 @@
+@charset "utf-8";
+
+:root {
+	--radius: 12px;
+	--marginFull: 16px;
+	--marginHalf: 10px;
+
+	--margin: var(--marginFull);
+
+	@media (max-width: 500px) {
+		--margin: var(--marginHalf);
+	}
+
+	//--ad: rgb(255 169 0 / 10%);
+}
+
+::selection {
+	color: #fff;
+	background-color: var(--accent);
+} 
+
+html {
+	touch-action: manipulation;
+	background-color: var(--bg);
+	background-attachment: fixed;
+	background-size: cover;
+	background-position: center;
+	color: var(--fg);
+	overflow: auto;
+	overflow-wrap: break-word;
+	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
+	line-height: 1.35;
+	text-size-adjust: 100%;
+	tab-size: 2;
+
+	&, * {
+		scrollbar-color: var(--scrollbarHandle) inherit;
+		scrollbar-width: thin;
+
+		&:hover {
+			scrollbar-color: var(--scrollbarHandleHover) inherit;
+		}
+
+		&:active {
+			scrollbar-color: var(--accent) inherit;
+		}
+
+		&::-webkit-scrollbar {
+			width: 6px;
+			height: 6px;
+		}
+
+		&::-webkit-scrollbar-track {
+			background: inherit;
+		}
+
+		&::-webkit-scrollbar-thumb {
+			background: var(--scrollbarHandle);
+
+			&:hover {
+				background: var(--scrollbarHandleHover);
+			}
+
+			&:active {
+				background: var(--accent);
+			}
+		}
+	}
+
+	&.f-small {
+		font-size: 0.9em;
+	}
+
+	&.f-large {
+		font-size: 1.1em;
+	}
+
+	&.f-veryLarge {
+		font-size: 1.2em;
+	}
+
+	&.useSystemFont {
+		font-family: sans-serif;
+	}
+}
+
+html._themeChanging_ {
+	&, * {
+		transition: background 1s ease, border 1s ease !important;
+	}
+}
+
+html, body {
+	margin: 0;
+	padding: 0;
+	scroll-behavior: smooth;
+}
+
+a {
+	text-decoration: none;
+	cursor: pointer;
+	color: inherit;
+	tap-highlight-color: transparent;
+	-webkit-tap-highlight-color: transparent;
+
+	&:hover {
+		text-decoration: underline;
+	}
+}
+
+textarea, input {
+	tap-highlight-color: transparent;
+	-webkit-tap-highlight-color: transparent;
+}
+
+optgroup, option {
+	background: var(--panel);
+	color: var(--fg);
+}
+
+hr {
+	margin: var(--margin) 0 var(--margin) 0;
+	border: none;
+	height: 1px;
+	background: var(--divider);
+}
+
+._noSelect {
+	user-select: none;
+	-webkit-user-select: none;
+	-webkit-touch-callout: none;
+}
+
+._ghost {
+	&, * {
+		@extend ._noSelect;
+		pointer-events: none;
+	}
+}
+
+._modalBg {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	background: var(--modalBg);
+	-webkit-backdrop-filter: var(--modalBgFilter);
+	backdrop-filter: var(--modalBgFilter);
+}
+
+._shadow {
+	box-shadow: 0px 4px 32px var(--shadow) !important;
+}
+
+._button {
+	appearance: none;
+	display: inline-block;
+	padding: 0;
+	margin: 0; // for Safari
+	background: none;
+	border: none;
+	cursor: pointer;
+	color: inherit;
+	touch-action: manipulation;
+	tap-highlight-color: transparent;
+	-webkit-tap-highlight-color: transparent;
+	font-size: 1em;
+	font-family: inherit;
+	line-height: inherit;
+
+	&, * {
+		@extend ._noSelect;
+	}
+
+	* {
+		pointer-events: none;
+	}
+
+	&:focus-visible {
+		outline: none;
+	}
+
+	&:disabled {
+		opacity: 0.5;
+		cursor: default;
+	}
+}
+
+._buttonPrimary {
+	@extend ._button;
+	color: var(--fgOnAccent);
+	background: var(--accent);
+
+	&:not(:disabled):hover {
+		background: var(--X8);
+	}
+
+	&:not(:disabled):active {
+		background: var(--X9);
+	}
+}
+
+._buttonGradate {
+	@extend ._buttonPrimary;
+	color: var(--fgOnAccent);
+	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+
+	&:not(:disabled):hover {
+		background: linear-gradient(90deg, var(--X8), var(--X8));
+	}
+
+	&:not(:disabled):active {
+		background: linear-gradient(90deg, var(--X8), var(--X8));
+	}
+}
+
+._help {
+	color: var(--accent);
+	cursor: help
+}
+
+._textButton {
+	@extend ._button;
+	color: var(--accent);
+
+	&:not(:disabled):hover {
+		text-decoration: underline;
+	}
+}
+
+._inputs {
+	display: flex;
+	margin: 32px 0;
+
+	&:first-child {
+		margin-top: 8px;
+	}
+
+	&:last-child {
+		margin-bottom: 8px;
+	}
+
+	> * {
+		flex: 1;
+		margin: 0 !important;
+
+		&:not(:first-child) {
+			margin-left: 8px !important;
+		}
+
+		&:not(:last-child) {
+			margin-right: 8px !important;
+		}
+	}
+}
+
+._panel {
+	background: var(--panel);
+	border-radius: var(--radius);
+	overflow: clip;
+}
+
+._block {
+	@extend ._panel;
+
+	& + ._block {
+		margin-top: var(--margin);
+	}
+}
+
+._gap {
+	margin: var(--margin) 0;
+}
+
+// TODO: 廃止
+._card {
+	@extend ._panel;
+
+	// TODO: _cardTitle に
+	> ._title {
+		margin: 0;
+		padding: 22px 32px;
+		font-size: 1em;
+		border-bottom: solid 1px var(--panelHeaderDivider);
+		font-weight: bold;
+		background: var(--panelHeaderBg);
+		color: var(--panelHeaderFg);
+
+		@media (max-width: 500px) {
+			padding: 16px;
+			font-size: 1em;
+		}
+	}
+
+	// TODO: _cardContent に
+	> ._content {
+		padding: 32px;
+
+		@media (max-width: 500px) {
+			padding: 16px;
+		}
+
+		&._noPad {
+			padding: 0 !important;
+		}
+
+		& + ._content {
+			border-top: solid 0.5px var(--divider);
+		}
+	}
+
+	// TODO: _cardFooter に
+	> ._footer {
+		border-top: solid 0.5px var(--divider);
+		padding: 24px 32px;
+
+		@media (max-width: 500px) {
+			padding: 16px;
+		}
+	}
+}
+
+._borderButton {
+	@extend ._button;
+	display: block;
+	width: 100%;
+	padding: 10px;
+	box-sizing: border-box;
+	text-align: center;
+	border: solid 0.5px var(--divider);
+	border-radius: var(--radius);
+
+	&:active {
+		border-color: var(--accent);
+	}
+}
+
+._window {
+	background: var(--panel);
+	border-radius: var(--radius);
+	contain: content;
+}
+
+._popup {
+	background: var(--popup);
+	border-radius: var(--radius);
+	contain: layout; // ふき出しがボックスから飛び出て表示されるようなデザインをする場合もあるので paint は contain することができない
+}
+
+// TODO: 廃止
+._monolithic_ {
+	._section:not(:empty) {
+		box-sizing: border-box;
+		padding: var(--root-margin, 32px);
+	
+		@media (max-width: 500px) {
+			--root-margin: 10px;
+		}
+	
+		& + ._section:not(:empty) {
+			border-top: solid 0.5px var(--divider);
+		}
+	}
+}
+
+._narrow_ ._card {
+	> ._title {
+		padding: 16px;
+		font-size: 1em;
+	}
+
+	> ._content {
+		padding: 16px;
+	}
+
+	> ._footer {
+		padding: 16px;
+	}
+}
+
+._acrylic {
+	background: var(--acrylicPanel);
+	-webkit-backdrop-filter: var(--blur, blur(15px));
+	backdrop-filter: var(--blur, blur(15px));
+}
+
+._inputSplit {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
+	grid-gap: 8px;
+	margin: 1em 0;
+
+	> * {
+		margin: 0 !important;
+	}
+}
+
+._formBlock {
+	margin: 20px 0;
+}
+
+._formRoot {
+	> ._formBlock:first-child {
+		margin-top: 0;
+	}
+
+	> ._formBlock:last-child {
+		margin-bottom: 0;
+	}
+}
+
+._table {
+	> ._row {
+		display: flex;
+
+		&:not(:last-child) {
+			margin-bottom: 16px;
+
+			@media (max-width: 500px) {
+				margin-bottom: 8px;
+			}
+		}
+
+		> ._cell {
+			flex: 1;
+
+			> ._label {
+				font-size: 80%;
+				opacity: 0.7;
+
+				> ._icon {
+					margin-right: 4px;
+					display: none;
+				}
+			}
+		}
+	}
+}
+
+._fullinfo {
+	padding: 64px 32px;
+	text-align: center;
+
+	> img {
+		vertical-align: bottom;
+		height: 128px;
+		margin-bottom: 16px;
+		border-radius: 16px;
+	}
+}
+
+._keyValue {
+	display: flex;
+
+	> * {
+		flex: 1;
+	}
+}
+
+._link {
+	color: var(--link);
+}
+
+._caption {
+	font-size: 0.8em;
+	opacity: 0.7;
+}
+
+._monospace {
+	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important;
+}
+
+._code {
+	@extend ._monospace;
+	background: #2d2d2d;
+	color: #ccc;
+	font-size: 14px;
+	line-height: 1.5;
+	padding: 5px;
+}
+
+.prism-editor__textarea:focus {
+	outline: none;
+}
+
+._zoom {
+	transition-duration: 0.5s, 0.5s;
+	transition-property: opacity, transform;
+	transition-timing-function: cubic-bezier(0,.5,.5,1);
+}
+
+.zoom-enter-active, .zoom-leave-active {
+	transition: opacity 0.5s, transform 0.5s !important;
+}
+.zoom-enter-from, .zoom-leave-to {
+	opacity: 0;
+	transform: scale(0.9);
+}
+
+@keyframes blink {
+	0% { opacity: 1; transform: scale(1); }
+	30% { opacity: 1; transform: scale(1); }
+	90% { opacity: 0; transform: scale(0.5); }
+}
+
+@keyframes tada {
+	from {
+		transform: scale3d(1, 1, 1);
+	}
+
+	10%,
+	20% {
+		transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
+	}
+
+	30%,
+	50%,
+	70%,
+	90% {
+		transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+	}
+
+	40%,
+	60%,
+	80% {
+		transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+	}
+
+	to {
+		transform: scale3d(1, 1, 1);
+	}
+}
+
+._anime_bounce {
+	will-change: transform;
+  animation: bounce ease 0.7s;
+  animation-iteration-count: 1;
+  transform-origin: 50% 50%;
+}
+._anime_bounce_ready {
+	will-change: transform;
+	transform:  scaleX(0.90) scaleY(0.90) ;
+}
+._anime_bounce_standBy {
+	transition: transform 0.1s ease;
+}
+
+@keyframes bounce{
+  0% {
+    transform:  scaleX(0.90) scaleY(0.90) ;
+  }
+  19% {
+    transform:  scaleX(1.10) scaleY(1.10) ;
+  }
+  48% {
+    transform:  scaleX(0.95) scaleY(0.95) ;
+  }
+  100% {
+    transform:  scaleX(1.00) scaleY(1.00) ;
+  }
+}
diff --git a/packages/client/src/sw/compose-notification.ts b/packages/client/src/sw/compose-notification.ts
new file mode 100644
index 0000000000..0aed9610ea
--- /dev/null
+++ b/packages/client/src/sw/compose-notification.ts
@@ -0,0 +1,103 @@
+/**
+ * Notification composer of Service Worker
+ */
+declare var self: ServiceWorkerGlobalScope;
+
+import { getNoteSummary } from '@/scripts/get-note-summary';
+import * as misskey from 'misskey-js';
+
+function getUserName(user: misskey.entities.User): string {
+	return user.name || user.username;
+}
+
+export default async function(type, data, i18n): Promise<[string, NotificationOptions] | null | undefined> {
+	if (!i18n) {
+		console.log('no i18n');
+		return;
+	}
+
+	switch (type) {
+		case 'driveFileCreated': // TODO (Server Side)
+			return [i18n.t('_notification.fileUploaded'), {
+				body: data.name,
+				icon: data.url
+			}];
+		case 'notification':
+			switch (data.type) {
+				case 'mention':
+					return [i18n.t('_notification.youGotMention', { name: getUserName(data.user) }), {
+						body: getNoteSummary(data.note, i18n.locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'reply':
+					return [i18n.t('_notification.youGotReply', { name: getUserName(data.user) }), {
+						body: getNoteSummary(data.note, i18n.locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'renote':
+					return [i18n.t('_notification.youRenoted', { name: getUserName(data.user) }), {
+						body: getNoteSummary(data.note, i18n.locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'quote':
+					return [i18n.t('_notification.youGotQuote', { name: getUserName(data.user) }), {
+						body: getNoteSummary(data.note, i18n.locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'reaction':
+					return [`${data.reaction} ${getUserName(data.user)}`, {
+						body: getNoteSummary(data.note, i18n.locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'pollVote':
+					return [i18n.t('_notification.youGotPoll', { name: getUserName(data.user) }), {
+						body: getNoteSummary(data.note, i18n.locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'follow':
+					return [i18n.t('_notification.youWereFollowed'), {
+						body: getUserName(data.user),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'receiveFollowRequest':
+					return [i18n.t('_notification.youReceivedFollowRequest'), {
+						body: getUserName(data.user),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'followRequestAccepted':
+					return [i18n.t('_notification.yourFollowRequestAccepted'), {
+						body: getUserName(data.user),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'groupInvited':
+					return [i18n.t('_notification.youWereInvitedToGroup'), {
+						body: data.group.name
+					}];
+
+				default:
+					return null;
+			}
+		case 'unreadMessagingMessage':
+			if (data.groupId === null) {
+				return [i18n.t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.user) }), {
+					icon: data.user.avatarUrl,
+					tag: `messaging:user:${data.user.id}`
+				}];
+			}
+			return [i18n.t('_notification.youGotMessagingMessageFromGroup', { name: data.group.name }), {
+				icon: data.user.avatarUrl,
+				tag: `messaging:group:${data.group.id}`
+			}];
+		default:
+			return null;
+	}
+}
diff --git a/packages/client/src/sw/sw.ts b/packages/client/src/sw/sw.ts
new file mode 100644
index 0000000000..68c650c771
--- /dev/null
+++ b/packages/client/src/sw/sw.ts
@@ -0,0 +1,123 @@
+/**
+ * Service Worker
+ */
+declare var self: ServiceWorkerGlobalScope;
+
+import { get, set } from 'idb-keyval';
+import composeNotification from '@/sw/compose-notification';
+import { I18n } from '@/scripts/i18n';
+
+//#region Variables
+const version = _VERSION_;
+const cacheName = `mk-cache-${version}`;
+
+let lang: string;
+let i18n: I18n<any>;
+let pushesPool: any[] = [];
+//#endregion
+
+//#region Startup
+get('lang').then(async prelang => {
+	if (!prelang) return;
+	lang = prelang;
+	return fetchLocale();
+});
+//#endregion
+
+//#region Lifecycle: Install
+self.addEventListener('install', ev => {
+	self.skipWaiting();
+});
+//#endregion
+
+//#region Lifecycle: Activate
+self.addEventListener('activate', ev => {
+	ev.waitUntil(
+		caches.keys()
+			.then(cacheNames => Promise.all(
+				cacheNames
+					.filter((v) => v !== cacheName)
+					.map(name => caches.delete(name))
+			))
+			.then(() => self.clients.claim())
+	);
+});
+//#endregion
+
+//#region When: Fetching
+self.addEventListener('fetch', ev => {
+	// Nothing to do
+});
+//#endregion
+
+//#region When: Caught Notification
+self.addEventListener('push', ev => {
+	// クライアント取得
+	ev.waitUntil(self.clients.matchAll({
+		includeUncontrolled: true
+	}).then(async clients => {
+		// クライアントがあったらストリームに接続しているということなので通知しない
+		if (clients.length != 0) return;
+
+		const { type, body } = ev.data?.json();
+
+		// localeを読み込めておらずi18nがundefinedだった場合はpushesPoolにためておく
+		if (!i18n) return pushesPool.push({ type, body });
+
+		const n = await composeNotification(type, body, i18n);
+		if (n) return self.registration.showNotification(...n);
+	}));
+});
+//#endregion
+
+//#region When: Caught a message from the client
+self.addEventListener('message', ev => {
+	switch(ev.data) {
+		case 'clear':
+			return; // TODO
+		default:
+			break;
+	}
+
+	if (typeof ev.data === 'object') {
+		// E.g. '[object Array]' → 'array'
+		const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase();
+
+		if (otype === 'object') {
+			if (ev.data.msg === 'initialize') {
+				lang = ev.data.lang;
+				set('lang', lang);
+				fetchLocale();
+			}
+		}
+	}
+});
+//#endregion
+
+//#region Function: (Re)Load i18n instance
+async function fetchLocale() {
+	//#region localeファイルの読み込み
+	// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
+	const localeUrl = `/assets/locales/${lang}.${version}.json`;
+	let localeRes = await caches.match(localeUrl);
+
+	if (!localeRes) {
+		localeRes = await fetch(localeUrl);
+		const clone = localeRes?.clone();
+		if (!clone?.clone().ok) return;
+
+		caches.open(cacheName).then(cache => cache.put(localeUrl, clone));
+	}
+
+	i18n = new I18n(await localeRes.json());
+	//#endregion
+
+	//#region i18nをきちんと読み込んだ後にやりたい処理
+	for (const { type, body } of pushesPool) {
+		const n = await composeNotification(type, body, i18n);
+		if (n) self.registration.showNotification(...n);
+	}
+	pushesPool = [];
+	//#endregion
+}
+//#endregion
diff --git a/packages/client/src/symbols.ts b/packages/client/src/symbols.ts
new file mode 100644
index 0000000000..6913f29c28
--- /dev/null
+++ b/packages/client/src/symbols.ts
@@ -0,0 +1 @@
+export const PAGE_INFO = Symbol('Page info');
diff --git a/packages/client/src/theme-store.ts b/packages/client/src/theme-store.ts
new file mode 100644
index 0000000000..e7962e7e8e
--- /dev/null
+++ b/packages/client/src/theme-store.ts
@@ -0,0 +1,34 @@
+import { api } from '@/os';
+import { $i } from '@/account';
+import { Theme } from './scripts/theme';
+
+const lsCacheKey = $i ? `themes:${$i.id}` : '';
+
+export function getThemes(): Theme[] {
+	return JSON.parse(localStorage.getItem(lsCacheKey) || '[]');
+}
+
+export async function fetchThemes(): Promise<void> {
+	if ($i == null) return;
+
+	try {
+		const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
+		localStorage.setItem(lsCacheKey, JSON.stringify(themes));
+	} catch (e) {
+		if (e.code === 'NO_SUCH_KEY') return;
+		throw e;
+	}
+}
+
+export async function addTheme(theme: Theme): Promise<void> {
+	await fetchThemes();
+	const themes = getThemes().concat(theme);
+	await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
+	localStorage.setItem(lsCacheKey, JSON.stringify(themes));
+}
+
+export async function removeTheme(theme: Theme): Promise<void> {
+	const themes = getThemes().filter(t => t.id != theme.id);
+	await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
+	localStorage.setItem(lsCacheKey, JSON.stringify(themes));
+}
diff --git a/packages/client/src/themes/_dark.json5 b/packages/client/src/themes/_dark.json5
new file mode 100644
index 0000000000..d8be16f60a
--- /dev/null
+++ b/packages/client/src/themes/_dark.json5
@@ -0,0 +1,90 @@
+// ダークテーマのベーステーマ
+// このテーマが直接使われることは無い
+{
+	id: 'dark',
+
+	name: 'Dark',
+	author: 'syuilo',
+	desc: 'Default dark theme',
+	kind: 'dark',
+
+	props: {
+		accent: '#86b300',
+		accentDarken: ':darken<10<@accent',
+		accentLighten: ':lighten<10<@accent',
+		accentedBg: ':alpha<0.15<@accent',
+		focus: ':alpha<0.3<@accent',
+		bg: '#000',
+		acrylicBg: ':alpha<0.5<@bg',
+		fg: '#dadada',
+		fgTransparentWeak: ':alpha<0.75<@fg',
+		fgTransparent: ':alpha<0.5<@fg',
+		fgHighlighted: ':lighten<3<@fg',
+		fgOnAccent: '#fff',
+		divider: 'rgba(255, 255, 255, 0.1)',
+		indicator: '@accent',
+		panel: ':lighten<3<@bg',
+		panelHighlight: ':lighten<3<@panel',
+		panelHeaderBg: ':lighten<3<@panel',
+		panelHeaderFg: '@fg',
+		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
+		panelBorder: '" solid 1px var(--divider)',
+		acrylicPanel: ':alpha<0.5<@panel',
+		popup: ':lighten<3<@panel',
+		shadow: 'rgba(0, 0, 0, 0.3)',
+		header: ':alpha<0.7<@panel',
+		navBg: '@panel',
+		navFg: '@fg',
+		navHoverFg: ':lighten<17<@fg',
+		navActive: '@accent',
+		navIndicator: '@indicator',
+		link: '#44a4c1',
+		hashtag: '#ff9156',
+		mention: '@accent',
+		mentionMe: '@mention',
+		renote: '#229e82',
+		modalBg: 'rgba(0, 0, 0, 0.5)',
+		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
+		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
+		dateLabelFg: '@fg',
+		infoBg: '#253142',
+		infoFg: '#fff',
+		infoWarnBg: '#42321c',
+		infoWarnFg: '#ffbd3e',
+		switchBg: 'rgba(255, 255, 255, 0.15)',
+		cwBg: '#687390',
+		cwFg: '#393f4f',
+		cwHoverBg: '#707b97',
+		buttonBg: 'rgba(255, 255, 255, 0.05)',
+		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+		buttonGradateA: '@accent',
+		buttonGradateB: ':hue<20<@accent',
+		inputBorder: 'rgba(255, 255, 255, 0.1)',
+		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
+		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
+		driveFolderBg: ':alpha<0.3<@accent',
+		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
+		badge: '#31b1ce',
+		messageBg: '@bg',
+		success: '#86b300',
+		error: '#ec4137',
+		warn: '#ecb637',
+		htmlThemeColor: '@bg',
+		X2: ':darken<2<@panel',
+		X3: 'rgba(255, 255, 255, 0.05)',
+		X4: 'rgba(255, 255, 255, 0.1)',
+		X5: 'rgba(255, 255, 255, 0.05)',
+		X6: 'rgba(255, 255, 255, 0.15)',
+		X7: 'rgba(255, 255, 255, 0.05)',
+		X8: ':lighten<5<@accent',
+		X9: ':darken<5<@accent',
+		X10: ':alpha<0.4<@accent',
+		X11: 'rgba(0, 0, 0, 0.3)',
+		X12: 'rgba(255, 255, 255, 0.1)',
+		X13: 'rgba(255, 255, 255, 0.15)',
+		X14: ':alpha<0.5<@navBg',
+		X15: ':alpha<0<@panel',
+		X16: ':alpha<0.7<@panel',
+		X17: ':alpha<0.8<@bg',
+	},
+}
diff --git a/packages/client/src/themes/_light.json5 b/packages/client/src/themes/_light.json5
new file mode 100644
index 0000000000..251aa36c7a
--- /dev/null
+++ b/packages/client/src/themes/_light.json5
@@ -0,0 +1,90 @@
+// ライトテーマのベーステーマ
+// このテーマが直接使われることは無い
+{
+	id: 'light',
+
+	name: 'Light',
+	author: 'syuilo',
+	desc: 'Default light theme',
+	kind: 'light',
+
+	props: {
+		accent: '#86b300',
+		accentDarken: ':darken<10<@accent',
+		accentLighten: ':lighten<10<@accent',
+		accentedBg: ':alpha<0.15<@accent',
+		focus: ':alpha<0.3<@accent',
+		bg: '#fff',
+		acrylicBg: ':alpha<0.5<@bg',
+		fg: '#5f5f5f',
+		fgTransparentWeak: ':alpha<0.75<@fg',
+		fgTransparent: ':alpha<0.5<@fg',
+		fgHighlighted: ':darken<3<@fg',
+		fgOnAccent: '#fff',
+		divider: 'rgba(0, 0, 0, 0.1)',
+		indicator: '@accent',
+		panel: ':lighten<3<@bg',
+		panelHighlight: ':darken<3<@panel',
+		panelHeaderBg: ':lighten<3<@panel',
+		panelHeaderFg: '@fg',
+		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
+		panelBorder: '" solid 1px var(--divider)',
+		acrylicPanel: ':alpha<0.5<@panel',
+		popup: ':lighten<3<@panel',
+		shadow: 'rgba(0, 0, 0, 0.1)',
+		header: ':alpha<0.7<@panel',
+		navBg: '@panel',
+		navFg: '@fg',
+		navHoverFg: ':darken<17<@fg',
+		navActive: '@accent',
+		navIndicator: '@indicator',
+		link: '#44a4c1',
+		hashtag: '#ff9156',
+		mention: '@accent',
+		mentionMe: '@mention',
+		renote: '#229e82',
+		modalBg: 'rgba(0, 0, 0, 0.3)',
+		scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
+		scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
+		dateLabelFg: '@fg',
+		infoBg: '#e5f5ff',
+		infoFg: '#72818a',
+		infoWarnBg: '#fff0db',
+		infoWarnFg: '#8f6e31',
+		switchBg: 'rgba(0, 0, 0, 0.15)',
+		cwBg: '#b1b9c1',
+		cwFg: '#fff',
+		cwHoverBg: '#bbc4ce',
+		buttonBg: 'rgba(0, 0, 0, 0.05)',
+		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
+		buttonGradateA: '@accent',
+		buttonGradateB: ':hue<20<@accent',
+		inputBorder: 'rgba(0, 0, 0, 0.1)',
+		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
+		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
+		driveFolderBg: ':alpha<0.3<@accent',
+		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
+		badge: '#31b1ce',
+		messageBg: '@bg',
+		success: '#86b300',
+		error: '#ec4137',
+		warn: '#ecb637',
+		htmlThemeColor: '@bg',
+		X2: ':darken<2<@panel',
+		X3: 'rgba(0, 0, 0, 0.05)',
+		X4: 'rgba(0, 0, 0, 0.1)',
+		X5: 'rgba(0, 0, 0, 0.05)',
+		X6: 'rgba(0, 0, 0, 0.25)',
+		X7: 'rgba(0, 0, 0, 0.05)',
+		X8: ':lighten<5<@accent',
+		X9: ':darken<5<@accent',
+		X10: ':alpha<0.4<@accent',
+		X11: 'rgba(0, 0, 0, 0.1)',
+		X12: 'rgba(0, 0, 0, 0.1)',
+		X13: 'rgba(0, 0, 0, 0.15)',
+		X14: ':alpha<0.5<@navBg',
+		X15: ':alpha<0<@panel',
+		X16: ':alpha<0.7<@panel',
+		X17: ':alpha<0.8<@bg',
+	},
+}
diff --git a/packages/client/src/themes/d-astro.json5 b/packages/client/src/themes/d-astro.json5
new file mode 100644
index 0000000000..c6a927ec3a
--- /dev/null
+++ b/packages/client/src/themes/d-astro.json5
@@ -0,0 +1,78 @@
+{
+	id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
+	base: 'dark',
+	name: 'Mi Astro Dark',
+	author: 'syuilo',
+	props: {
+		bg: '#232125',
+		fg: '#efdab9',
+		cwBg: '#687390',
+		cwFg: '#393f4f',
+		link: '#78b0a0',
+		warn: '#ecb637',
+		badge: '#31b1ce',
+		error: '#ec4137',
+		focus: ':alpha<0.3<@accent',
+		navBg: '@panel',
+		navFg: '@fg',
+		panel: '#2a272b',
+		accent: '#81c08b',
+		header: ':alpha<0.7<@bg',
+		infoBg: '#253142',
+		infoFg: '#fff',
+		renote: '#659CC8',
+		shadow: 'rgba(0, 0, 0, 0.3)',
+		divider: 'rgba(255, 255, 255, 0.1)',
+		hashtag: '#ff9156',
+		mention: '#ffd152',
+		modalBg: 'rgba(0, 0, 0, 0.5)',
+		success: '#86b300',
+		buttonBg: 'rgba(255, 255, 255, 0.05)',
+		acrylicBg: ':alpha<0.5<@bg',
+		cwHoverBg: '#707b97',
+		indicator: '@accent',
+		mentionMe: '#fb5d38',
+		messageBg: '@bg',
+		navActive: '@accent',
+		infoWarnBg: '#42321c',
+		infoWarnFg: '#ffbd3e',
+		navHoverFg: ':lighten<17<@fg',
+		dateLabelFg: '@fg',
+		inputBorder: 'rgba(255, 255, 255, 0.1)',
+		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
+		panelBorder: '" solid 1px var(--divider)',
+		accentDarken: ':darken<10<@accent',
+		acrylicPanel: ':alpha<0.5<@panel',
+		navIndicator: '@accent',
+		accentLighten: ':lighten<10<@accent',
+		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+		buttonGradateA: '@accent',
+		buttonGradateB: ':hue<-20<@accent',
+		driveFolderBg: ':alpha<0.3<@accent',
+		fgHighlighted: ':lighten<3<@fg',
+		panelHeaderBg: ':lighten<3<@panel',
+		panelHeaderFg: '@fg',
+		htmlThemeColor: '@bg',
+		panelHighlight: ':lighten<3<@panel',
+		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
+		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
+		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
+		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
+		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
+		X2: ':darken<2<@panel',
+		X3: 'rgba(255, 255, 255, 0.05)',
+		X4: 'rgba(255, 255, 255, 0.1)',
+		X5: 'rgba(255, 255, 255, 0.05)',
+		X6: 'rgba(255, 255, 255, 0.15)',
+		X7: 'rgba(255, 255, 255, 0.05)',
+		X8: ':lighten<5<@accent',
+		X9: ':darken<5<@accent',
+		X10: ':alpha<0.4<@accent',
+		X11: 'rgba(0, 0, 0, 0.3)',
+		X12: 'rgba(255, 255, 255, 0.1)',
+		X13: 'rgba(255, 255, 255, 0.15)',
+		X14: ':alpha<0.5<@navBg',
+		X15: ':alpha<0<@panel',
+		X16: ':alpha<0.7<@panel',
+	},
+}
diff --git a/packages/client/src/themes/d-black.json5 b/packages/client/src/themes/d-black.json5
new file mode 100644
index 0000000000..3c18ebdaf1
--- /dev/null
+++ b/packages/client/src/themes/d-black.json5
@@ -0,0 +1,17 @@
+{
+	id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
+
+	name: 'Mi Black',
+	author: 'syuilo',
+
+	base: 'dark',
+
+	props: {
+		divider: '#2d2d2d',
+		panel: '#131313',
+		panelHeaderBg: '@panel',
+		panelHeaderDivider: '@divider',
+		shadow: 'rgba(255, 255, 255, 0.05)',
+		modalBg: 'rgba(255, 255, 255, 0.1)',
+	},
+}
diff --git a/packages/client/src/themes/d-botanical.json5 b/packages/client/src/themes/d-botanical.json5
new file mode 100644
index 0000000000..c03b95e2d7
--- /dev/null
+++ b/packages/client/src/themes/d-botanical.json5
@@ -0,0 +1,26 @@
+{
+	id: '504debaf-4912-6a4c-5059-1db08a76b737',
+
+	name: 'Mi Botanical Dark',
+	author: 'syuilo',
+
+	base: 'dark',
+
+	props: {
+		accent: 'rgb(148, 179, 0)',
+		bg: 'rgb(37, 38, 36)',
+		fg: 'rgb(216, 212, 199)',
+		fgHighlighted: '#fff',
+		divider: 'rgba(255, 255, 255, 0.14)',
+		panel: 'rgb(47, 47, 44)',
+		panelHeaderBg: '@panel',
+		panelHeaderDivider: '@divider',
+		header: ':alpha<0.7<@panel',
+		navBg: '#363636',
+		renote: '@accent',
+		mention: 'rgb(212, 153, 76)',
+		mentionMe: 'rgb(212, 210, 76)',
+		hashtag: '#5bcbb0',
+		link: '@accent',
+	},
+}
diff --git a/packages/client/src/themes/d-dark.json5 b/packages/client/src/themes/d-dark.json5
new file mode 100644
index 0000000000..d24ce4df69
--- /dev/null
+++ b/packages/client/src/themes/d-dark.json5
@@ -0,0 +1,26 @@
+{
+	id: '8050783a-7f63-445a-b270-36d0f6ba1677',
+
+	name: 'Mi Dark',
+	author: 'syuilo',
+	desc: 'Default light theme',
+
+	base: 'dark',
+
+	props: {
+		bg: '#232323',
+		fg: 'rgb(199, 209, 216)',
+		fgHighlighted: '#fff',
+		divider: 'rgba(255, 255, 255, 0.14)',
+		panel: '#2d2d2d',
+		panelHeaderBg: '@panel',
+		panelHeaderDivider: '@divider',
+		header: ':alpha<0.7<@panel',
+		navBg: '#363636',
+		renote: '@accent',
+		mention: '#da6d35',
+		mentionMe: '#d44c4c',
+		hashtag: '#4cb8d4',
+		link: '@accent',
+	},
+}
diff --git a/packages/client/src/themes/d-future.json5 b/packages/client/src/themes/d-future.json5
new file mode 100644
index 0000000000..b6fa1ab0c1
--- /dev/null
+++ b/packages/client/src/themes/d-future.json5
@@ -0,0 +1,27 @@
+{
+	id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
+
+	name: 'Mi Future Dark',
+	author: 'syuilo',
+
+	base: 'dark',
+
+	props: {
+		accent: '#63e2b7',
+		bg: '#101014',
+		fg: '#D5D5D6',
+		fgHighlighted: '#fff',
+		fgOnAccent: '#000',
+		divider: 'rgba(255, 255, 255, 0.1)',
+		panel: '#18181c',
+		panelHeaderBg: '@panel',
+		panelHeaderDivider: '@divider',
+		renote: '@accent',
+		mention: '#f2c97d',
+		mentionMe: '@accent',
+		hashtag: '#70c0e8',
+		link: '#e88080',
+		buttonGradateA: '@accent',
+		buttonGradateB: ':saturate<30<:hue<30<@accent',
+	},
+}
diff --git a/packages/client/src/themes/d-persimmon.json5 b/packages/client/src/themes/d-persimmon.json5
new file mode 100644
index 0000000000..e36265ff10
--- /dev/null
+++ b/packages/client/src/themes/d-persimmon.json5
@@ -0,0 +1,25 @@
+{
+	id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
+
+	name: 'Mi Persimmon Dark',
+	author: 'syuilo',
+
+	base: 'dark',
+
+	props: {
+		accent: 'rgb(206, 102, 65)',
+		bg: 'rgb(31, 33, 31)',
+		fg: '#cdd8c7',
+		fgHighlighted: '#fff',
+		divider: 'rgba(255, 255, 255, 0.14)',
+		panel: 'rgb(41, 43, 41)',
+		infoFg: '@fg',
+		infoBg: '#333c3b',
+		navBg: '#141714',
+		renote: '@accent',
+		mention: '@accent',
+		mentionMe: '#de6161',
+		hashtag: '#68bad0',
+		link: '#a1c758',
+	},
+}
diff --git a/packages/client/src/themes/d-pumpkin.json5 b/packages/client/src/themes/d-pumpkin.json5
new file mode 100644
index 0000000000..064ca4577b
--- /dev/null
+++ b/packages/client/src/themes/d-pumpkin.json5
@@ -0,0 +1,88 @@
+{
+	id: '0b64fef3-02c7-20b5-dd87-b3f77e2b4301',
+
+	name: 'Mi Pumpkin Dark',
+	author: 'syuilo',
+
+	base: 'dark',
+
+	props: {
+		X2: ':darken<2<@panel',
+		X3: 'rgba(255, 255, 255, 0.05)',
+		X4: 'rgba(255, 255, 255, 0.1)',
+		X5: 'rgba(255, 255, 255, 0.05)',
+		X6: 'rgba(255, 255, 255, 0.15)',
+		X7: 'rgba(255, 255, 255, 0.05)',
+		X8: ':lighten<5<@accent',
+		X9: ':darken<5<@accent',
+		bg: 'rgb(37, 32, 47)',
+		fg: '#e0d5c0',
+		X10: ':alpha<0.4<@accent',
+		X11: 'rgba(0, 0, 0, 0.3)',
+		X12: 'rgba(255, 255, 255, 0.1)',
+		X13: 'rgba(255, 255, 255, 0.15)',
+		X14: ':alpha<0.5<@navBg',
+		X15: ':alpha<0<@panel',
+		X16: ':alpha<0.7<@panel',
+		X17: ':alpha<0.8<@bg',
+		cwBg: '#687390',
+		cwFg: '#393f4f',
+		link: 'rgb(172, 193, 68)',
+		warn: '#ecb637',
+		badge: '#31b1ce',
+		error: '#ec4137',
+		focus: ':alpha<0.3<@accent',
+		navBg: '@panel',
+		navFg: '@fg',
+		panel: ':lighten<3<@bg',
+		popup: ':lighten<3<@panel',
+		accent: 'rgb(242, 133, 36)',
+		header: ':alpha<0.7<@panel',
+		infoBg: '#253142',
+		infoFg: '#fff',
+		renote: 'rgb(110, 179, 72)',
+		shadow: 'rgba(0, 0, 0, 0.3)',
+		divider: 'rgba(255, 255, 255, 0.1)',
+		hashtag: 'rgb(188, 90, 255)',
+		mention: 'rgb(72, 179, 139)',
+		modalBg: 'rgba(0, 0, 0, 0.5)',
+		success: '#86b300',
+		buttonBg: 'rgba(255, 255, 255, 0.05)',
+		switchBg: 'rgba(255, 255, 255, 0.15)',
+		acrylicBg: ':alpha<0.5<@bg',
+		cwHoverBg: '#707b97',
+		indicator: '@accent',
+		mentionMe: '@accent',
+		messageBg: '@bg',
+		navActive: '@accent',
+		accentedBg: ':alpha<0.15<@accent',
+		fgOnAccent: '#000',
+		infoWarnBg: '#42321c',
+		infoWarnFg: '#ffbd3e',
+		navHoverFg: ':lighten<17<@fg',
+		dateLabelFg: '@fg',
+		inputBorder: 'rgba(255, 255, 255, 0.1)',
+		panelBorder: '" solid 1px var(--divider)',
+		accentDarken: ':darken<10<@accent',
+		acrylicPanel: ':alpha<0.5<@panel',
+		navIndicator: '@indicator',
+		accentLighten: ':lighten<10<@accent',
+		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+		driveFolderBg: ':alpha<0.3<@accent',
+		fgHighlighted: ':lighten<3<@fg',
+		fgTransparent: ':alpha<0.5<@fg',
+		panelHeaderBg: ':lighten<3<@panel',
+		panelHeaderFg: '@fg',
+		buttonGradateA: '@accent',
+		buttonGradateB: ':hue<20<@accent',
+		htmlThemeColor: '@bg',
+		panelHighlight: ':lighten<3<@panel',
+		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
+		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
+		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
+		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
+		fgTransparentWeak: ':alpha<0.75<@fg',
+		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
+		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
+	},
+}
diff --git a/packages/client/src/themes/l-apricot.json5 b/packages/client/src/themes/l-apricot.json5
new file mode 100644
index 0000000000..1ed5525575
--- /dev/null
+++ b/packages/client/src/themes/l-apricot.json5
@@ -0,0 +1,22 @@
+{
+	id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
+
+	name: 'Mi Apricot Light',
+	author: 'syuilo',
+
+	base: 'light',
+
+	props: {
+		accent: 'rgb(234, 154, 82)',
+		bg: '#e6e5e2',
+		fg: 'rgb(149, 143, 139)',
+		panel: '#EEECE8',
+		renote: '@accent',
+		link: '@accent',
+		mention: '@accent',
+		hashtag: '@accent',
+		inputBorder: 'rgba(0, 0, 0, 0.1)',
+		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
+		infoBg: 'rgb(226, 235, 241)',
+	},
+}
diff --git a/packages/client/src/themes/l-light.json5 b/packages/client/src/themes/l-light.json5
new file mode 100644
index 0000000000..248355c945
--- /dev/null
+++ b/packages/client/src/themes/l-light.json5
@@ -0,0 +1,20 @@
+{
+	id: '4eea646f-7afa-4645-83e9-83af0333cd37',
+
+	name: 'Mi Light',
+	author: 'syuilo',
+	desc: 'Default light theme',
+
+	base: 'light',
+
+	props: {
+		bg: '#f9f9f9',
+		fg: '#676767',
+		divider: '#e8e8e8',
+		header: ':alpha<0.7<@panel',
+		navBg: '#fff',
+		panel: '#fff',
+		panelHeaderDivider: '@divider',
+		mentionMe: 'rgb(0, 179, 70)',
+	},
+}
diff --git a/packages/client/src/themes/l-rainy.json5 b/packages/client/src/themes/l-rainy.json5
new file mode 100644
index 0000000000..283dd74c6c
--- /dev/null
+++ b/packages/client/src/themes/l-rainy.json5
@@ -0,0 +1,21 @@
+{
+	id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
+
+	name: 'Mi Rainy Light',
+	author: 'syuilo',
+
+	base: 'light',
+
+	props: {
+		accent: '#5db0da',
+		bg: 'rgb(246 248 249)',
+		fg: '#636b71',
+		panel: '#fff',
+		divider: 'rgb(230 233 234)',
+		panelHeaderDivider: '@divider',
+		renote: '@accent',
+		link: '@accent',
+		mention: '@accent',
+		hashtag: '@accent',
+	},
+}
diff --git a/packages/client/src/themes/l-sushi.json5 b/packages/client/src/themes/l-sushi.json5
new file mode 100644
index 0000000000..5846927d65
--- /dev/null
+++ b/packages/client/src/themes/l-sushi.json5
@@ -0,0 +1,18 @@
+{
+	id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c',
+
+	name: 'Mi Sushi Light',
+	author: 'syuilo',
+
+	base: 'light',
+
+	props: {
+		accent: '#e36749',
+		bg: '#f0eee9',
+		fg: '#5f5f5f',
+		renote: '@accent',
+		link: '@accent',
+		mention: '@accent',
+		hashtag: '#229e82',
+	},
+}
diff --git a/packages/client/src/themes/l-vivid.json5 b/packages/client/src/themes/l-vivid.json5
new file mode 100644
index 0000000000..b3c08f38ae
--- /dev/null
+++ b/packages/client/src/themes/l-vivid.json5
@@ -0,0 +1,82 @@
+{
+	id: '6128c2a9-5c54-43fe-a47d-17942356470b',
+
+	name: 'Mi Vivid Light',
+	author: 'syuilo',
+
+	base: 'light',
+
+	props: {
+		bg: '#fafafa',
+		fg: '#444',
+		cwBg: '#b1b9c1',
+		cwFg: '#fff',
+		link: '#ff9400',
+		warn: '#ecb637',
+		badge: '#31b1ce',
+		error: '#ec4137',
+		focus: ':alpha<0.3<@accent',
+		navBg: '@panel',
+		navFg: '@fg',
+		panel: '#fff',
+		accent: '#008cff',
+		header: ':alpha<0.7<@panel',
+		infoBg: '#e5f5ff',
+		infoFg: '#72818a',
+		renote: '@accent',
+		shadow: 'rgba(0, 0, 0, 0.1)',
+		divider: 'rgba(0, 0, 0, 0.08)',
+		hashtag: '#92d400',
+		mention: '@accent',
+		modalBg: 'rgba(0, 0, 0, 0.3)',
+		success: '#86b300',
+		buttonBg: 'rgba(0, 0, 0, 0.05)',
+		acrylicBg: ':alpha<0.5<@bg',
+		cwHoverBg: '#bbc4ce',
+		indicator: '@accent',
+		mentionMe: '@mention',
+		messageBg: '@bg',
+		navActive: '@accent',
+		infoWarnBg: '#fff0db',
+		infoWarnFg: '#8f6e31',
+		navHoverFg: ':darken<17<@fg',
+		dateLabelFg: '@fg',
+		inputBorder: 'rgba(0, 0, 0, 0.1)',
+		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
+		panelBorder: '" solid 1px var(--divider)',
+		accentDarken: ':darken<10<@accent',
+		acrylicPanel: ':alpha<0.5<@panel',
+		navIndicator: '@accent',
+		accentLighten: ':lighten<10<@accent',
+		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
+		driveFolderBg: ':alpha<0.3<@accent',
+		fgHighlighted: ':darken<3<@fg',
+		fgTransparent: ':alpha<0.5<@fg',
+		panelHeaderBg: ':lighten<3<@panel',
+		panelHeaderFg: '@fg',
+		htmlThemeColor: '@bg',
+		panelHighlight: ':darken<3<@panel',
+		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
+		scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
+		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
+		fgTransparentWeak: ':alpha<0.75<@fg',
+		panelHeaderDivider: '@divider',
+		scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
+		X2: ':darken<2<@panel',
+		X3: 'rgba(0, 0, 0, 0.05)',
+		X4: 'rgba(0, 0, 0, 0.1)',
+		X5: 'rgba(0, 0, 0, 0.05)',
+		X6: 'rgba(0, 0, 0, 0.25)',
+		X7: 'rgba(0, 0, 0, 0.05)',
+		X8: ':lighten<5<@accent',
+		X9: ':darken<5<@accent',
+		X10: ':alpha<0.4<@accent',
+		X11: 'rgba(0, 0, 0, 0.1)',
+		X12: 'rgba(0, 0, 0, 0.1)',
+		X13: 'rgba(0, 0, 0, 0.15)',
+		X14: ':alpha<0.5<@navBg',
+		X15: ':alpha<0<@panel',
+		X16: ':alpha<0.7<@panel',
+		X17: ':alpha<0.8<@bg',
+	},
+}
diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue
new file mode 100644
index 0000000000..59e26c837e
--- /dev/null
+++ b/packages/client/src/ui/_common_/common.vue
@@ -0,0 +1,89 @@
+<template>
+<component v-for="popup in popups"
+	:key="popup.id"
+	:is="popup.component"
+	v-bind="popup.props"
+	v-on="popup.events"
+/>
+
+<XUpload v-if="uploads.length > 0"/>
+
+<XStreamIndicator/>
+
+<div id="wait" v-if="pendingApiRequestsCount > 0"></div>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os';
+import * as sound from '@/scripts/sound';
+import { $i } from '@/account';
+
+export default defineComponent({
+	components: {
+		XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
+		XUpload: defineAsyncComponent(() => import('./upload.vue')),
+	},
+
+	setup() {
+		const onNotification = notification => {
+			if ($i.mutingNotificationTypes.includes(notification.type)) return;
+
+			if (document.visibilityState === 'visible') {
+				stream.send('readNotification', {
+					id: notification.id
+				});
+
+				popup(import('@/components/toast.vue'), {
+					notification
+				}, {}, 'closed');
+			}
+
+			sound.play('notification');
+		};
+
+		if ($i) {
+			const connection = stream.useChannel('main', null, 'UI');
+			connection.on('notification', onNotification);
+		}
+
+		return {
+			uploads,
+			popups,
+			pendingApiRequestsCount,
+		};
+	},
+});
+</script>
+
+<style lang="scss">
+#wait {
+	display: block;
+	position: fixed;
+	z-index: 10000;
+	top: 15px;
+	right: 15px;
+
+	&:before {
+		content: "";
+		display: block;
+		width: 18px;
+		height: 18px;
+		box-sizing: border-box;
+		border: solid 2px transparent;
+		border-top-color: var(--accent);
+		border-left-color: var(--accent);
+		border-radius: 50%;
+		animation: progress-spinner 400ms linear infinite;
+	}
+}
+
+@keyframes progress-spinner {
+	0% {
+		transform: rotate(0deg);
+	}
+	100% {
+		transform: rotate(360deg);
+	}
+}
+</style>
diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue
new file mode 100644
index 0000000000..9bbf1b3e3d
--- /dev/null
+++ b/packages/client/src/ui/_common_/sidebar.vue
@@ -0,0 +1,388 @@
+<template>
+<div class="mvcprjjd">
+	<transition name="nav-back">
+		<div class="nav-back _modalBg"
+			v-if="showing"
+			@click="showing = false"
+			@touchstart.passive="showing = false"
+		></div>
+	</transition>
+
+	<transition name="nav">
+		<nav class="nav" :class="{ iconOnly, hidden }" v-show="showing">
+			<div>
+				<button class="item _button account" @click="openAccountMenu" v-click-anime>
+					<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
+				</button>
+				<MkA class="item index" active-class="active" to="/" exact v-click-anime>
+					<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
+				</MkA>
+				<template v-for="item in menu">
+					<div v-if="item === '-'" class="divider"></div>
+					<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime>
+						<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
+						<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+					</component>
+				</template>
+				<div class="divider"></div>
+				<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" v-click-anime>
+					<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
+				</MkA>
+				<button class="item _button" @click="more" v-click-anime>
+					<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
+					<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
+				</button>
+				<MkA class="item" active-class="active" to="/settings" v-click-anime>
+					<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
+				</MkA>
+				<button class="item _button post" @click="post" data-cy-open-post-form>
+					<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
+				</button>
+			</div>
+		</nav>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { host } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import { openAccountMenu } from '@/account';
+
+export default defineComponent({
+	props: {
+		defaultHidden: {
+			type: Boolean,
+			required: false,
+			default: false,
+		}
+	},
+
+	data() {
+		return {
+			host: host,
+			showing: false,
+			accounts: [],
+			connection: null,
+			menuDef: menuDef,
+			iconOnly: false,
+			hidden: this.defaultHidden,
+		};
+	},
+
+	computed: {
+		menu(): string[] {
+			return this.$store.state.menu;
+		},
+
+		otherNavItemIndicated(): boolean {
+			for (const def in this.menuDef) {
+				if (this.menu.includes(def)) continue;
+				if (this.menuDef[def].indicated) return true;
+			}
+			return false;
+		},
+	},
+
+	watch: {
+		$route(to, from) {
+			this.showing = false;
+		},
+
+		'$store.reactiveState.menuDisplay.value'() {
+			this.calcViewState();
+		},
+
+		iconOnly() {
+			this.$nextTick(() => {
+				this.$emit('change-view-mode');
+			});
+		},
+
+		hidden() {
+			this.$nextTick(() => {
+				this.$emit('change-view-mode');
+			});
+		}
+	},
+
+	created() {
+		window.addEventListener('resize', this.calcViewState);
+		this.calcViewState();
+	},
+
+	methods: {
+		calcViewState() {
+			this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.menuDisplay === 'sideIcon');
+			if (!this.defaultHidden) {
+				this.hidden = (window.innerWidth <= 650);
+			}
+		},
+
+		show() {
+			this.showing = true;
+		},
+
+		post() {
+			os.post();
+		},
+
+		search() {
+			search();
+		},
+
+		more(ev) {
+			os.popup(import('@/components/launch-pad.vue'), {}, {
+			}, 'closed');
+		},
+
+		openAccountMenu,
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.nav-enter-active,
+.nav-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.nav-enter-from,
+.nav-leave-active {
+	opacity: 0;
+	transform: translateX(-240px);
+}
+
+.nav-back-enter-active,
+.nav-back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.nav-back-enter-from,
+.nav-back-leave-active {
+	opacity: 0;
+}
+
+.mvcprjjd {
+	$ui-font-size: 1em; // TODO: どこかに集約したい
+	$nav-width: 250px;
+	$nav-icon-only-width: 86px;
+
+	> .nav-back {
+		z-index: 1001;
+	}
+
+	> .nav {
+		$avatar-size: 32px;
+		$avatar-margin: 8px;
+
+		flex: 0 0 $nav-width;
+		width: $nav-width;
+		box-sizing: border-box;
+
+		&.iconOnly {
+			flex: 0 0 $nav-icon-only-width;
+			width: $nav-icon-only-width;
+
+			&:not(.hidden) {
+				> div {
+					width: $nav-icon-only-width;
+
+					> .divider {
+						margin: 8px auto;
+						width: calc(100% - 32px);
+					}
+
+					> .item {
+						padding-left: 0;
+						padding: 18px 0;
+						width: 100%;
+						text-align: center;
+						font-size: $ui-font-size * 1.1;
+						line-height: initial;
+
+						> i,
+						> .avatar {
+							display: block;
+							margin: 0 auto;
+						}
+
+						> i {
+							opacity: 0.7;
+						}
+
+						> .text {
+							display: none;
+						}
+
+						&:hover, &.active {
+							> i, > .text {
+								opacity: 1;
+							}
+						}
+
+						&:first-child {
+							margin-bottom: 8px;
+						}
+
+						&:last-child {
+							margin-top: 8px;
+						}
+					}
+				}
+			}
+		}
+
+		&.hidden {
+			position: fixed;
+			top: 0;
+			left: 0;
+			z-index: 1001;
+		}
+
+		&:not(.hidden) {
+			display: block !important;
+		}
+
+		> div {
+			position: fixed;
+			top: 0;
+			left: 0;
+			z-index: 1001;
+			width: $nav-width;
+			// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+			height: calc(var(--vh, 1vh) * 100);
+			box-sizing: border-box;
+			overflow: auto;
+			overflow-x: clip;
+			background: var(--navBg);
+
+			> .divider {
+				margin: 16px 16px;
+				border-top: solid 0.5px var(--divider);
+			}
+
+			> .item {
+				position: relative;
+				display: block;
+				padding-left: 24px;
+				font-size: $ui-font-size;
+				line-height: 2.85rem;
+				text-overflow: ellipsis;
+				overflow: hidden;
+				white-space: nowrap;
+				width: 100%;
+				text-align: left;
+				box-sizing: border-box;
+				color: var(--navFg);
+
+				> i {
+					position: relative;
+					width: 32px;
+				}
+
+				> i,
+				> .avatar {
+					margin-right: $avatar-margin;
+				}
+
+				> .avatar {
+					width: $avatar-size;
+					height: $avatar-size;
+					vertical-align: middle;
+				}
+
+				> .indicator {
+					position: absolute;
+					top: 0;
+					left: 20px;
+					color: var(--navIndicator);
+					font-size: 8px;
+					animation: blink 1s infinite;
+				}
+
+				> .text {
+					position: relative;
+					font-size: 0.9em;
+				}
+
+				&:hover {
+					text-decoration: none;
+					color: var(--navHoverFg);
+				}
+
+				&.active {
+					color: var(--navActive);
+				}
+
+				&:hover, &.active {
+					&:before {
+						content: "";
+						display: block;
+						width: calc(100% - 24px);
+						height: 100%;
+						margin: auto;
+						position: absolute;
+						top: 0;
+						left: 0;
+						right: 0;
+						bottom: 0;
+						border-radius: 999px;
+						background: var(--accentedBg);
+					}
+				}
+
+				&:first-child, &:last-child {
+					position: sticky;
+					z-index: 1;
+					padding-top: 8px;
+					padding-bottom: 8px;
+					background: var(--X14);
+					-webkit-backdrop-filter: var(--blur, blur(8px));
+					backdrop-filter: var(--blur, blur(8px));
+				}
+
+				&:first-child {
+					top: 0;
+
+					&:hover, &.active {
+						&:before {
+							content: none;
+						}
+					}
+				}
+
+				&:last-child {
+					bottom: 0;
+					color: var(--fgOnAccent);
+
+					&:before {
+						content: "";
+						display: block;
+						width: calc(100% - 20px);
+						height: calc(100% - 20px);
+						margin: auto;
+						position: absolute;
+						top: 0;
+						left: 0;
+						right: 0;
+						bottom: 0;
+						border-radius: 999px;
+						background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+					}
+					
+					&:hover, &.active {
+						&:before {
+							background: var(--accentLighten);
+						}
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/_common_/stream-indicator.vue b/packages/client/src/ui/_common_/stream-indicator.vue
new file mode 100644
index 0000000000..8b1b4b567c
--- /dev/null
+++ b/packages/client/src/ui/_common_/stream-indicator.vue
@@ -0,0 +1,70 @@
+<template>
+<div class="nsbbhtug" v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" @click="resetDisconnected">
+	<div>{{ $ts.disconnectedFromServer }}</div>
+	<div class="command">
+		<button class="_textButton" @click="reload">{{ $ts.reload }}</button>
+		<button class="_textButton">{{ $ts.doNothing }}</button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	data() {
+		return {
+			hasDisconnected: false,
+		}
+	},
+	computed: {
+		stream() {
+			return os.stream;
+		},
+	},
+	created() {
+		os.stream.on('_disconnected_', this.onDisconnected);
+	},
+	beforeUnmount() {
+		os.stream.off('_disconnected_', this.onDisconnected);
+	},
+	methods: {
+		onDisconnected() {
+			this.hasDisconnected = true;
+		},
+		resetDisconnected() {
+			this.hasDisconnected = false;
+		},
+		reload() {
+			location.reload();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.nsbbhtug {
+	position: fixed;
+	z-index: 16385;
+	bottom: 8px;
+	right: 8px;
+	margin: 0;
+	padding: 6px 12px;
+	font-size: 0.9em;
+	color: #fff;
+	background: #000;
+	opacity: 0.8;
+	border-radius: 4px;
+	max-width: 320px;
+
+	> .command {
+		display: flex;
+		justify-content: space-around;
+
+		> button {
+			padding: 0.7em;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/_common_/upload.vue b/packages/client/src/ui/_common_/upload.vue
new file mode 100644
index 0000000000..0ca353e4e1
--- /dev/null
+++ b/packages/client/src/ui/_common_/upload.vue
@@ -0,0 +1,134 @@
+<template>
+<div class="mk-uploader _acrylic">
+	<ol v-if="uploads.length > 0">
+		<li v-for="ctx in uploads" :key="ctx.id">
+			<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
+			<div class="top">
+				<p class="name"><i class="fas fa-spinner fa-pulse"></i>{{ ctx.name }}</p>
+				<p class="status">
+					<span class="initing" v-if="ctx.progressValue === undefined">{{ $ts.waiting }}<MkEllipsis/></span>
+					<span class="kb" v-if="ctx.progressValue !== undefined">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
+					<span class="percentage" v-if="ctx.progressValue !== undefined">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
+				</p>
+			</div>
+			<progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === undefined, waiting: ctx.progressValue !== undefined && ctx.progressValue === ctx.progressMax }"></progress>
+		</li>
+	</ol>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	data() {
+		return {
+			uploads: os.uploads,
+		};
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-uploader {
+	position: fixed;
+	z-index: 10000;
+	right: 16px;
+	width: 260px;
+	top: 32px;
+	padding: 16px 20px;
+	pointer-events: none;
+	box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+	border-radius: 8px;
+}
+.mk-uploader:empty {
+  display: none;
+}
+.mk-uploader > ol {
+  display: block;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+.mk-uploader > ol > li {
+  display: grid;
+  margin: 8px 0 0 0;
+  padding: 0;
+  height: 36px;
+  width: 100%;
+  border-top: solid 8px transparent;
+  grid-template-columns: 36px calc(100% - 44px);
+  grid-template-rows: 1fr 8px;
+  column-gap: 8px;
+  box-sizing: content-box;
+}
+.mk-uploader > ol > li:first-child {
+  margin: 0;
+  box-shadow: none;
+  border-top: none;
+}
+.mk-uploader > ol > li > .img {
+  display: block;
+  background-size: cover;
+  background-position: center center;
+  grid-column: 1/2;
+  grid-row: 1/3;
+}
+.mk-uploader > ol > li > .top {
+  display: flex;
+  grid-column: 2/3;
+  grid-row: 1/2;
+}
+.mk-uploader > ol > li > .top > .name {
+  display: block;
+  padding: 0 8px 0 0;
+  margin: 0;
+  font-size: 0.8em;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  flex-shrink: 1;
+}
+.mk-uploader > ol > li > .top > .name > i {
+  margin-right: 4px;
+}
+.mk-uploader > ol > li > .top > .status {
+  display: block;
+  margin: 0 0 0 auto;
+  padding: 0;
+  font-size: 0.8em;
+  flex-shrink: 0;
+}
+.mk-uploader > ol > li > .top > .status > .initing {
+}
+.mk-uploader > ol > li > .top > .status > .kb {
+}
+.mk-uploader > ol > li > .top > .status > .percentage {
+  display: inline-block;
+  width: 48px;
+  text-align: right;
+}
+.mk-uploader > ol > li > .top > .status > .percentage:after {
+  content: '%';
+}
+.mk-uploader > ol > li > progress {
+  display: block;
+  background: transparent;
+  border: none;
+  border-radius: 4px;
+  overflow: hidden;
+  grid-column: 2/3;
+  grid-row: 2/3;
+  z-index: 2;
+	width: 100%;
+	height: 8px;
+}
+.mk-uploader > ol > li > progress::-webkit-progress-value {
+  background: var(--accent);
+}
+.mk-uploader > ol > li > progress::-webkit-progress-bar {
+  //background: var(--accentAlpha01);
+	background: transparent;
+}
+</style>
diff --git a/packages/client/src/ui/chat/date-separated-list.vue b/packages/client/src/ui/chat/date-separated-list.vue
new file mode 100644
index 0000000000..b21e425aba
--- /dev/null
+++ b/packages/client/src/ui/chat/date-separated-list.vue
@@ -0,0 +1,163 @@
+<script lang="ts">
+import { defineComponent, h, PropType, TransitionGroup } from 'vue';
+import MkAd from '@/components/global/ad.vue';
+
+export default defineComponent({
+	props: {
+		items: {
+			type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
+			required: true,
+		},
+		reversed: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		ad: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	methods: {
+		focus() {
+			this.$slots.default[0].elm.focus();
+		}
+	},
+
+	render() {
+		const getDateText = (time: string) => {
+			const date = new Date(time).getDate();
+			const month = new Date(time).getMonth() + 1;
+			return this.$t('monthAndDay', {
+				month: month.toString(),
+				day: date.toString()
+			});
+		}
+
+		return h(this.reversed ? 'div' : TransitionGroup, {
+			class: 'hmjzthxl',
+			name: this.reversed ? 'list-reversed' : 'list',
+			tag: 'div',
+		}, this.items.map((item, i) => {
+			const el = this.$slots.default({
+				item: item
+			})[0];
+			if (el.key == null && item.id) el.key = item.id;
+
+			if (
+				i != this.items.length - 1 &&
+				new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
+			) {
+				const separator = h('div', {
+					class: 'separator',
+					key: item.id + ':separator',
+				}, h('p', {
+					class: 'date'
+				}, [
+					h('span', [
+						h('i', {
+							class: 'fas fa-angle-up icon',
+						}),
+						getDateText(item.createdAt)
+					]),
+					h('span', [
+						getDateText(this.items[i + 1].createdAt),
+						h('i', {
+							class: 'fas fa-angle-down icon',
+						})
+					])
+				]));
+
+				return [el, separator];
+			} else {
+				if (this.ad && item._shouldInsertAd_) {
+					return [h(MkAd, {
+						class: 'a', // advertiseの意(ブロッカー対策)
+						key: item.id + ':ad',
+						prefer: ['horizontal', 'horizontal-big'],
+					}), el];
+				} else {
+					return el;
+				}
+			}
+		}));
+	},
+});
+</script>
+
+<style lang="scss">
+.hmjzthxl {
+	> .list-move {
+		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
+	}
+	> .list-enter-active {
+		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
+	}
+	> .list-enter-from {
+		opacity: 0;
+		transform: translateY(-64px);
+	}
+
+	> .list-reversed-enter-active, > .list-reversed-leave-active {
+		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
+	}
+	> .list-reversed-enter-from {
+		opacity: 0;
+		transform: translateY(64px);
+	}
+}
+</style>
+
+<style lang="scss">
+.hmjzthxl {
+	> .separator {
+		text-align: center;
+		position: relative;
+
+		&:before {
+			content: "";
+			display: block;
+			position: absolute;
+			top: 50%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			width: calc(100% - 32px);
+			height: 1px;
+			background: var(--divider);
+		}
+
+		> .date {
+			display: inline-block;
+			position: relative;
+			margin: 0;
+			padding: 0 16px;
+			line-height: 32px;
+			text-align: center;
+			font-size: 12px;
+			color: var(--dateLabelFg);
+			background: var(--panel);
+
+			> span {
+				&:first-child {
+					margin-right: 8px;
+
+					> .icon {
+						margin-right: 8px;
+					}
+				}
+
+				&:last-child {
+					margin-left: 8px;
+
+					> .icon {
+						margin-left: 8px;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/header-clock.vue b/packages/client/src/ui/chat/header-clock.vue
new file mode 100644
index 0000000000..3488289c21
--- /dev/null
+++ b/packages/client/src/ui/chat/header-clock.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="acemodlh _monospace">
+	<div>
+		<span v-text="y"></span>/<span v-text="m"></span>/<span v-text="d"></span>
+	</div>
+	<div>
+		<span v-text="hh"></span>
+		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
+		<span v-text="mm"></span>
+		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
+		<span v-text="ss"></span>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	data() {
+		return {
+			clock: null,
+			y: null,
+			m: null,
+			d: null,
+			hh: null,
+			mm: null,
+			ss: null,
+			showColon: true,
+		};
+	},
+	created() {
+		this.tick();
+		this.clock = setInterval(this.tick, 1000);
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		tick() {
+			const now = new Date();
+			this.y = now.getFullYear().toString();
+			this.m = (now.getMonth() + 1).toString().padStart(2, '0');
+			this.d = now.getDate().toString().padStart(2, '0');
+			this.hh = now.getHours().toString().padStart(2, '0');
+			this.mm = now.getMinutes().toString().padStart(2, '0');
+			this.ss = now.getSeconds().toString().padStart(2, '0');
+			this.showColon = now.getSeconds() % 2 === 0;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.acemodlh {
+	opacity: 0.7;
+	font-size: 0.85em;
+	line-height: 1em;
+	text-align: center;
+}
+</style>
diff --git a/packages/client/src/ui/chat/index.vue b/packages/client/src/ui/chat/index.vue
new file mode 100644
index 0000000000..e8d15b2cfc
--- /dev/null
+++ b/packages/client/src/ui/chat/index.vue
@@ -0,0 +1,467 @@
+<template>
+<div class="mk-app" @contextmenu.self.prevent="onContextmenu">
+	<XSidebar ref="menu" class="menu" :default-hidden="true"/>
+
+	<div class="nav">
+		<header class="header">
+			<div class="left">
+				<button class="_button account" @click="openAccountMenu">
+					<MkAvatar :user="$i" class="avatar"/><!--<MkAcct class="text" :user="$i"/>-->
+				</button>
+			</div>
+			<div class="right">
+				<MkA class="item" to="/my/messaging" v-tooltip="$ts.messaging"><i class="fas fa-comments icon"></i><span v-if="$i.hasUnreadMessagingMessage" class="indicator"><i class="fas fa-circle"></i></span></MkA>
+				<MkA class="item" to="/my/messages" v-tooltip="$ts.directNotes"><i class="fas fa-envelope icon"></i><span v-if="$i.hasUnreadSpecifiedNotes" class="indicator"><i class="fas fa-circle"></i></span></MkA>
+				<MkA class="item" to="/my/mentions" v-tooltip="$ts.mentions"><i class="fas fa-at icon"></i><span v-if="$i.hasUnreadMentions" class="indicator"><i class="fas fa-circle"></i></span></MkA>
+				<MkA class="item" to="/my/notifications" v-tooltip="$ts.notifications"><i class="fas fa-bell icon"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></MkA>
+			</div>
+		</header>
+		<div class="body">
+			<div class="container">
+				<div class="header">{{ $ts.timeline }}</div>
+				<div class="body">
+					<MkA to="/timeline/home" class="item" :class="{ active: tl === 'home' }"><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA>
+					<MkA to="/timeline/local" class="item" :class="{ active: tl === 'local' }"><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA>
+					<MkA to="/timeline/social" class="item" :class="{ active: tl === 'social' }"><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA>
+					<MkA to="/timeline/global" class="item" :class="{ active: tl === 'global' }"><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA>
+				</div>
+			</div>
+			<div class="container" v-if="followedChannels">
+				<div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
+				<div class="body">
+					<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
+				</div>
+			</div>
+			<div class="container" v-if="featuredChannels">
+				<div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
+				<div class="body">
+					<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
+				</div>
+			</div>
+			<div class="container" v-if="lists">
+				<div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><i class="fas fa-plus"></i></button></div>
+				<div class="body">
+					<MkA v-for="list in lists" :key="list.id" :to="`/my/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA>
+				</div>
+			</div>
+			<div class="container" v-if="antennas">
+				<div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><i class="fas fa-plus"></i></button></div>
+				<div class="body">
+					<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/my/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA>
+				</div>
+			</div>
+			<div class="container">
+				<div class="body">
+					<MkA to="/my/favorites" class="item"><i class="fas fa-star icon"></i>{{ $ts.favorites }}</MkA>
+				</div>
+			</div>
+			<MkAd class="a" :prefer="['square']"/>
+		</div>
+		<footer class="footer">
+			<div class="left">
+				<button class="_button menu" @click="showMenu">
+					<i class="fas fa-bars icon"></i>
+				</button>
+			</div>
+			<div class="right">
+				<button class="_button item search" @click="search" v-tooltip="$ts.search">
+					<i class="fas fa-search icon"></i>
+				</button>
+				<MkA class="item" to="/settings" v-tooltip="$ts.settings"><i class="fas fa-cog icon"></i></MkA>
+			</div>
+		</footer>
+	</div>
+
+	<main class="main" @contextmenu.stop="onContextmenu">
+		<header class="header">
+			<MkHeader class="header" :info="pageInfo" :menu="menu" :center="false" @click="onHeaderClick"/>
+		</header>
+		<router-view v-slot="{ Component }">
+			<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+				<keep-alive :include="['timeline']">
+					<component :is="Component" :ref="changePage" class="body"/>
+				</keep-alive>
+			</transition>
+		</router-view>
+	</main>
+
+	<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
+	<div class="side widgets" :class="{ sideViewOpening }">
+		<XWidgets/>
+	</div>
+
+	<XCommon/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { instanceName, url } from '@/config';
+import XSidebar from '@/ui/_common_/sidebar.vue';
+import XWidgets from './widgets.vue';
+import XCommon from '../_common_/common.vue';
+import XSide from './side.vue';
+import XHeaderClock from './header-clock.vue';
+import * as os from '@/os';
+import { router } from '@/router';
+import { menuDef } from '@/menu';
+import { search } from '@/scripts/search';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { store } from './store';
+import * as symbols from '@/symbols';
+import { openAccountMenu } from '@/account';
+
+export default defineComponent({
+	components: {
+		XCommon,
+		XSidebar,
+		XWidgets,
+		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
+		XHeaderClock,
+	},
+
+	provide() {
+		return {
+			sideViewHook: (path) => {
+				this.$refs.side.navigate(path);
+			}
+		};
+	},
+
+	data() {
+		return {
+			pageInfo: null,
+			lists: null,
+			antennas: null,
+			followedChannels: null,
+			featuredChannels: null,
+			currentChannel: null,
+			menuDef: menuDef,
+			sideViewOpening: false,
+			instanceName,
+		};
+	},
+
+	computed: {
+		menu() {
+			return [{
+				icon: 'fas fa-columns',
+				text: this.$ts.openInSideView,
+				action: () => {
+					this.$refs.side.navigate(this.$route.path);
+				}
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(this.$route.path);
+				}
+			}];
+		}
+	},
+
+	created() {
+		if (window.innerWidth < 1024) {
+			localStorage.setItem('ui', 'default');
+			location.reload();
+		}
+
+		os.api('users/lists/list').then(lists => {
+			this.lists = lists;
+		});
+
+		os.api('antennas/list').then(antennas => {
+			this.antennas = antennas;
+		});
+
+		os.api('channels/followed', { limit: 20 }).then(channels => {
+			this.followedChannels = channels;
+		});
+
+		// TODO: pagination
+		os.api('channels/featured', { limit: 20 }).then(channels => {
+			this.featuredChannels = channels;
+		});
+	},
+
+	methods: {
+		changePage(page) {
+			console.log(page);
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+				document.title = `${this.pageInfo.title} | ${instanceName}`;
+			}
+		},
+
+		onTransition() {
+			if (window._scroll) window._scroll();
+		},
+
+		showMenu() {
+			this.$refs.menu.show();
+		},
+
+		post() {
+			os.post();
+		},
+
+		search() {
+			search();
+		},
+
+		back() {
+			history.back();
+		},
+
+		top() {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		},
+
+		onTransition() {
+			if (window._scroll) window._scroll();
+		},
+
+		onHeaderClick() {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		},
+
+		onContextmenu(e) {
+			const isLink = (el: HTMLElement) => {
+				if (el.tagName === 'A') return true;
+				if (el.parentElement) {
+					return isLink(el.parentElement);
+				}
+			};
+			if (isLink(e.target)) return;
+			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+			if (window.getSelection().toString() !== '') return;
+			const path = this.$route.path;
+			os.contextMenu([{
+				type: 'label',
+				text: path,
+			}, {
+				icon: 'fas fa-columns',
+				text: this.$ts.openInSideView,
+				action: () => {
+					this.$refs.side.navigate(path);
+				}
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(path);
+				}
+			}], e);
+		},
+
+		openAccountMenu,
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-app {
+	$header-height: 54px; // TODO: どこかに集約したい
+	$ui-font-size: 1em; // TODO: どこかに集約したい
+
+	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+	height: calc(var(--vh, 1vh) * 100);
+	display: flex;
+
+	> .nav {
+		display: flex;
+		flex-direction: column;
+		width: 250px;
+		height: 100vh;
+		border-right: solid 4px var(--divider);
+
+		> .header, > .footer {
+			$padding: 8px;
+			display: flex;
+			align-items: center;
+			z-index: 1000;
+			height: $header-height;
+			padding: $padding;
+			box-sizing: border-box;
+			user-select: none;
+
+			&.header {
+				border-bottom: solid 0.5px var(--divider);
+			}
+
+			&.footer {
+				border-top: solid 0.5px var(--divider);
+			}
+
+			> .left, > .right {
+				> .item, > .menu {
+					display: inline-flex;
+					vertical-align: middle;
+					height: ($header-height - ($padding * 2));
+					width: ($header-height - ($padding * 2));
+					box-sizing: border-box;
+					//opacity: 0.6;
+					position: relative;
+					border-radius: 5px;
+
+					&:hover {
+						background: rgba(0, 0, 0, 0.05);
+					}
+
+					> .icon {
+						margin: auto;
+					}
+
+					> .indicator {
+						position: absolute;
+						top: 8px;
+						right: 8px;
+						color: var(--indicator);
+						font-size: 8px;
+						line-height: 8px;
+						animation: blink 1s infinite;
+					}
+				}
+			}
+
+			> .left {
+				flex: 1;
+				min-width: 0;
+
+				> .account {
+					display: flex;
+					align-items: center;
+					padding: 0 8px;
+
+					> .avatar {
+						width: 26px;
+						height: 26px;
+						margin-right: 8px;
+					}
+
+					> .text {
+						white-space: nowrap;
+						overflow: hidden;
+						text-overflow: ellipsis;
+						font-size: 0.9em;
+					}
+				}
+			}
+
+			> .right {
+				margin-left: auto;
+			}
+		}
+
+		> .body {
+			flex: 1;
+			min-width: 0;
+			overflow: auto;
+
+			> .container {
+				margin-top: 8px;
+				margin-bottom: 8px;
+
+				& + .container {
+					margin-top: 16px;
+				}
+
+				> .header {
+					display: flex;
+					font-size: 0.9em;
+					padding: 8px 16px;
+					position: sticky;
+					top: 0;
+					background: var(--X17);
+					-webkit-backdrop-filter: var(--blur, blur(8px));
+					backdrop-filter: var(--blur, blur(8px));
+					z-index: 1;
+					color: var(--fgTransparentWeak);
+
+					> .add {
+						margin-left: auto;
+						color: var(--fgTransparentWeak);
+
+						&:hover {
+							color: var(--fg);
+						}
+					}
+				}
+
+				> .body {
+					padding: 0 8px;
+
+					> .item {
+						display: block;
+						padding: 6px 8px;
+						border-radius: 4px;
+						white-space: nowrap;
+						overflow: hidden;
+						text-overflow: ellipsis;
+
+						&:hover {
+							text-decoration: none;
+							background: rgba(0, 0, 0, 0.05);
+						}
+
+						&.active, &.active:hover {
+							background: var(--accent);
+							color: #fff !important;
+						}
+
+						&.read {
+							color: var(--fgTransparent);
+						}
+
+						> .icon {
+							margin-right: 8px;
+							opacity: 0.6;
+						}
+					}
+				}
+			}
+
+			> .a {
+				margin: 12px;
+			}
+		}
+	}
+
+	> .main {
+		display: flex;
+		flex: 1;
+		flex-direction: column;
+		min-width: 0;
+		height: 100vh;
+		position: relative;
+		background: var(--panel);
+
+		> .header {
+			z-index: 1000;
+			height: $header-height;
+			background-color: var(--panel);
+			border-bottom: solid 0.5px var(--divider);
+			user-select: none;
+		}
+
+		> .body {
+			width: 100%;
+			box-sizing: border-box;
+			overflow: auto;
+		}
+	}
+
+	> .side {
+		width: 350px;
+		border-left: solid 4px var(--divider);
+		background: var(--panel);
+
+		&.widgets.sideViewOpening {
+			@media (max-width: 1400px) {
+				display: none;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/note-header.vue b/packages/client/src/ui/chat/note-header.vue
new file mode 100644
index 0000000000..8ab03501b2
--- /dev/null
+++ b/packages/client/src/ui/chat/note-header.vue
@@ -0,0 +1,112 @@
+<template>
+<header class="dehvdgxo">
+	<MkA class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
+		<MkUserName :user="note.user"/>
+	</MkA>
+	<span class="is-bot" v-if="note.user.isBot">bot</span>
+	<span class="username"><MkAcct :user="note.user"/></span>
+	<span class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></span>
+	<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></span>
+	<div class="info">
+		<span class="mobile" v-if="note.viaMobile"><i class="fas fa-mobile-alt"></i></span>
+		<MkA class="created-at" :to="notePage(note)">
+			<MkTime :time="note.createdAt"/>
+		</MkA>
+		<span class="visibility" v-if="note.visibility !== 'public'">
+			<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
+			<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
+			<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
+		</span>
+		<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
+	</div>
+</header>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import notePage from '@/filters/note';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	methods: {
+		notePage,
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.dehvdgxo {
+	display: flex;
+	align-items: baseline;
+	white-space: nowrap;
+	font-size: 0.9em;
+
+	> .name {
+		display: block;
+		margin: 0 .5em 0 0;
+		padding: 0;
+		overflow: hidden;
+		font-size: 1em;
+		font-weight: bold;
+		text-decoration: none;
+		text-overflow: ellipsis;
+
+		&:hover {
+			text-decoration: underline;
+		}
+	}
+
+	> .is-bot {
+		flex-shrink: 0;
+		align-self: center;
+		margin: 0 .5em 0 0;
+		padding: 1px 6px;
+		font-size: 80%;
+		border: solid 0.5px var(--divider);
+		border-radius: 3px;
+	}
+
+	> .admin,
+	> .moderator {
+		margin-right: 0.5em;
+		color: var(--badge);
+	}
+
+	> .username {
+		margin: 0 .5em 0 0;
+		overflow: hidden;
+		text-overflow: ellipsis;
+	}
+
+	> .info {
+		font-size: 0.9em;
+		opacity: 0.7;
+
+		> .mobile {
+			margin-right: 8px;
+		}
+
+		> .visibility {
+			margin-left: 8px;
+		}
+
+		> .localOnly {
+			margin-left: 8px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/note-preview.vue b/packages/client/src/ui/chat/note-preview.vue
new file mode 100644
index 0000000000..2a08a3d7f5
--- /dev/null
+++ b/packages/client/src/ui/chat/note-preview.vue
@@ -0,0 +1,112 @@
+<template>
+<div class="hduudsxk">
+	<MkAvatar class="avatar" :user="note.user"/>
+	<div class="main">
+		<XNoteHeader class="header" :note="note" :mini="true"/>
+		<div class="body">
+			<p v-if="note.cw != null" class="cw">
+				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+				<XCwButton v-model="showContent" :note="note"/>
+			</p>
+			<div class="content" v-show="note.cw == null || showContent">
+				<XSubNote-content class="text" :note="note"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XNoteHeader from './note-header.vue';
+import XSubNoteContent from './sub-note-content.vue';
+import XCwButton from '@/components/cw-button.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XNoteHeader,
+		XSubNoteContent,
+		XCwButton,
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			showContent: false
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hduudsxk {
+	display: flex;
+	margin: 0;
+	padding: 0;
+	overflow: hidden;
+	font-size: 0.95em;
+
+	> .avatar {
+
+		@media (min-width: 350px) {
+			margin: 0 10px 0 0;
+			width: 44px;
+			height: 44px;
+		}
+
+		@media (min-width: 500px) {
+			margin: 0 12px 0 0;
+			width: 48px;
+			height: 48px;
+		}
+	}
+
+	> .avatar {
+		flex-shrink: 0;
+		display: block;
+		margin: 0 10px 0 0;
+		width: 40px;
+		height: 40px;
+		border-radius: 8px;
+	}
+
+	> .main {
+		flex: 1;
+		min-width: 0;
+
+		> .header {
+			margin-bottom: 2px;
+		}
+
+		> .body {
+
+			> .cw {
+				cursor: default;
+				display: block;
+				margin: 0;
+				padding: 0;
+				overflow-wrap: break-word;
+
+				> .text {
+					margin-right: 8px;
+				}
+			}
+
+			> .content {
+				> .text {
+					cursor: default;
+					margin: 0;
+					padding: 0;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/note.sub.vue b/packages/client/src/ui/chat/note.sub.vue
new file mode 100644
index 0000000000..75d9d98088
--- /dev/null
+++ b/packages/client/src/ui/chat/note.sub.vue
@@ -0,0 +1,137 @@
+<template>
+<div class="wrpstxzv" :class="{ children }">
+	<div class="main">
+		<MkAvatar class="avatar" :user="note.user"/>
+		<div class="body">
+			<XNoteHeader class="header" :note="note" :mini="true"/>
+			<div class="body">
+				<p v-if="note.cw != null" class="cw">
+					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
+					<XCwButton v-model="showContent" :note="note"/>
+				</p>
+				<div class="content" v-show="note.cw == null || showContent">
+					<XSubNote-content class="text" :note="note"/>
+				</div>
+			</div>
+		</div>
+	</div>
+	<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XNoteHeader from './note-header.vue';
+import XSubNoteContent from './sub-note-content.vue';
+import XCwButton from '@/components/cw-button.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	name: 'XSub',
+
+	components: {
+		XNoteHeader,
+		XSubNoteContent,
+		XCwButton,
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		detail: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		children: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		// TODO
+		truncate: {
+			type: Boolean,
+			default: true
+		}
+	},
+
+	data() {
+		return {
+			showContent: false,
+			replies: [],
+		};
+	},
+
+	created() {
+		if (this.detail) {
+			os.api('notes/children', {
+				noteId: this.note.id,
+				limit: 5
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.wrpstxzv {
+	padding: 16px 16px;
+	font-size: 0.8em;
+
+	&.children {
+		padding: 10px 0 0 16px;
+		font-size: 1em;
+	}
+
+	> .main {
+		display: flex;
+
+		> .avatar {
+			flex-shrink: 0;
+			display: block;
+			margin: 0 8px 0 0;
+			width: 36px;
+			height: 36px;
+		}
+
+		> .body {
+			flex: 1;
+			min-width: 0;
+
+			> .header {
+				margin-bottom: 2px;
+			}
+
+			> .body {
+				> .cw {
+					cursor: default;
+					display: block;
+					margin: 0;
+					padding: 0;
+					overflow-wrap: break-word;
+
+					> .text {
+						margin-right: 8px;
+					}
+				}
+
+				> .content {
+					> .text {
+						margin: 0;
+						padding: 0;
+					}
+				}
+			}
+		}
+	}
+
+	> .reply {
+		border-left: solid 0.5px var(--divider);
+		margin-top: 10px;
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/note.vue b/packages/client/src/ui/chat/note.vue
new file mode 100644
index 0000000000..c0b5cebd94
--- /dev/null
+++ b/packages/client/src/ui/chat/note.vue
@@ -0,0 +1,1144 @@
+<template>
+<div
+	class="vfzoeqcg"
+	v-if="!muted"
+	v-show="!isDeleted"
+	:tabindex="!isDeleted ? '-1' : null"
+	:class="{ renote: isRenote, highlighted: appearNote._prId_ || appearNote._featuredId_, operating }"
+	v-hotkey="keymap"
+>
+	<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
+	<div class="info" v-if="pinned"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
+	<div class="info" v-if="appearNote._prId_"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
+	<div class="info" v-if="appearNote._featuredId_"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
+	<div class="renote" v-if="isRenote">
+		<MkAvatar class="avatar" :user="note.user"/>
+		<i class="fas fa-retweet"></i>
+		<I18n :src="$ts.renotedBy" tag="span">
+			<template #user>
+				<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
+					<MkUserName :user="note.user"/>
+				</MkA>
+			</template>
+		</I18n>
+		<div class="info">
+			<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
+				<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
+				<MkTime :time="note.createdAt"/>
+			</button>
+			<span class="visibility" v-if="note.visibility !== 'public'">
+				<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
+				<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
+				<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
+			</span>
+			<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
+		</div>
+	</div>
+	<article class="article" @contextmenu.stop="onContextmenu">
+		<MkAvatar class="avatar" :user="appearNote.user"/>
+		<div class="main">
+			<XNoteHeader class="header" :note="appearNote" :mini="true"/>
+			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
+			<div class="body">
+				<p v-if="appearNote.cw != null" class="cw">
+					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
+					<XCwButton v-model="showContent" :note="appearNote"/>
+				</p>
+				<div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent">
+					<div class="text">
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
+						<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
+						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
+						<a class="rp" v-if="appearNote.renote != null">RN:</a>
+					</div>
+					<div class="files" v-if="appearNote.files.length > 0">
+						<XMediaList :media-list="appearNote.files"/>
+					</div>
+					<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
+					<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
+					<div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
+					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
+						<span>{{ $ts.showMore }}</span>
+					</button>
+				</div>
+				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
+			</div>
+			<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
+			<footer class="footer _panel">
+				<button @click="reply()" class="button _button" v-tooltip="$ts.reply">
+					<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
+					<template v-else><i class="fas fa-reply"></i></template>
+					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
+				</button>
+				<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton" v-tooltip="$ts.renote">
+					<i class="fas fa-retweet"></i><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
+				</button>
+				<button v-else class="button _button">
+					<i class="fas fa-ban"></i>
+				</button>
+				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton" v-tooltip="$ts.reaction">
+					<i class="fas fa-plus"></i>
+				</button>
+				<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton" v-tooltip="$ts.reaction">
+					<i class="fas fa-minus"></i>
+				</button>
+				<button class="button _button" @click="menu()" ref="menuButton">
+					<i class="fas fa-ellipsis-h"></i>
+				</button>
+			</footer>
+		</div>
+	</article>
+</div>
+<div v-else class="muted" @click="muted = false">
+	<I18n :src="$ts.userSaysSomething" tag="small">
+		<template #name>
+			<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
+				<MkUserName :user="appearNote.user"/>
+			</MkA>
+		</template>
+	</I18n>
+</div>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
+import * as mfm from 'mfm-js';
+import { sum } from '@/scripts/array';
+import XSub from './note.sub.vue';
+import XNoteHeader from './note-header.vue';
+import XNoteSimple from './note-preview.vue';
+import XReactionsViewer from '@/components/reactions-viewer.vue';
+import XMediaList from '@/components/media-list.vue';
+import XCwButton from '@/components/cw-button.vue';
+import XPoll from '@/components/poll.vue';
+import { pleaseLogin } from '@/scripts/please-login';
+import { focusPrev, focusNext } from '@/scripts/focus';
+import { url } from '@/config';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { checkWordMute } from '@/scripts/check-word-mute';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+import { noteActions, noteViewInterruptors } from '@/store';
+import { reactionPicker } from '@/scripts/reaction-picker';
+import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
+
+export default defineComponent({
+	components: {
+		XSub,
+		XNoteHeader,
+		XNoteSimple,
+		XReactionsViewer,
+		XMediaList,
+		XCwButton,
+		XPoll,
+		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
+		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
+	},
+
+	inject: {
+		inChannel: {
+			default: null
+		},
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		pinned: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	emits: ['update:note'],
+
+	data() {
+		return {
+			connection: null,
+			replies: [],
+			showContent: false,
+			collapsed: false,
+			isDeleted: false,
+			muted: false,
+			operating: false,
+		};
+	},
+
+	computed: {
+		rs() {
+			return this.$store.state.reactions;
+		},
+		keymap(): any {
+			return {
+				'r': () => this.reply(true),
+				'e|a|plus': () => this.react(true),
+				'q': () => this.renote(true),
+				'f|b': this.favorite,
+				'delete|ctrl+d': this.del,
+				'ctrl+q': this.renoteDirectly,
+				'up|k|shift+tab': this.focusBefore,
+				'down|j|tab': this.focusAfter,
+				'esc': this.blur,
+				'm|o': () => this.menu(true),
+				's': this.toggleShowContent,
+				'1': () => this.reactDirectly(this.rs[0]),
+				'2': () => this.reactDirectly(this.rs[1]),
+				'3': () => this.reactDirectly(this.rs[2]),
+				'4': () => this.reactDirectly(this.rs[3]),
+				'5': () => this.reactDirectly(this.rs[4]),
+				'6': () => this.reactDirectly(this.rs[5]),
+				'7': () => this.reactDirectly(this.rs[6]),
+				'8': () => this.reactDirectly(this.rs[7]),
+				'9': () => this.reactDirectly(this.rs[8]),
+				'0': () => this.reactDirectly(this.rs[9]),
+			};
+		},
+
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.fileIds.length == 0 &&
+				this.note.poll == null);
+		},
+
+		appearNote(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+
+		isMyNote(): boolean {
+			return this.$i && (this.$i.id === this.appearNote.userId);
+		},
+
+		isMyRenote(): boolean {
+			return this.$i && (this.$i.id === this.note.userId);
+		},
+
+		canRenote(): boolean {
+			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
+		},
+
+		reactionsCount(): number {
+			return this.appearNote.reactions
+				? sum(Object.values(this.appearNote.reactions))
+				: 0;
+		},
+
+		urls(): string[] {
+			if (this.appearNote.text) {
+				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
+			} else {
+				return null;
+			}
+		},
+
+		showTicker() {
+			if (this.$store.state.instanceTicker === 'always') return true;
+			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
+			return false;
+		}
+	},
+
+	async created() {
+		if (this.$i) {
+			this.connection = os.stream;
+		}
+
+		this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
+			(this.appearNote.text.split('\n').length > 9) ||
+			(this.appearNote.text.length > 500)
+		);
+		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
+
+		// plugin
+		if (noteViewInterruptors.length > 0) {
+			let result = this.note;
+			for (const interruptor of noteViewInterruptors) {
+				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
+			}
+			this.$emit('update:note', Object.freeze(result));
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if (this.$i) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+	},
+
+	beforeUnmount() {
+		this.decapture(true);
+
+		if (this.$i) {
+			this.connection.off('_connected_', this.onStreamConnected);
+		}
+	},
+
+	methods: {
+		updateAppearNote(v) {
+			this.$emit('update:note', Object.freeze(this.isRenote ? {
+				...this.note,
+				renote: {
+					...this.note.renote,
+					...v
+				}
+			} : {
+				...this.note,
+				...v
+			}));
+		},
+
+		readPromo() {
+			os.api('promo/read', {
+				noteId: this.appearNote.id
+			});
+			this.isDeleted = true;
+		},
+
+		capture(withHandler = false) {
+			if (this.$i) {
+				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
+				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		decapture(withHandler = false) {
+			if (this.$i) {
+				this.connection.send('un', {
+					id: this.appearNote.id
+				});
+				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		onStreamConnected() {
+			this.capture();
+		},
+
+		onStreamNoteUpdated(data) {
+			const { type, id, body } = data;
+
+			if (id !== this.appearNote.id) return;
+
+			switch (type) {
+				case 'reacted': {
+					const reaction = body.reaction;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					if (body.emoji) {
+						const emojis = this.appearNote.emojis || [];
+						if (!emojis.includes(body.emoji)) {
+							n.emojis = [...emojis, body.emoji];
+						}
+					}
+
+					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
+					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
+
+					// Increment the count
+					n.reactions = {
+						...this.appearNote.reactions,
+						[reaction]: currentCount + 1
+					};
+
+					if (body.userId === this.$i.id) {
+						n.myReaction = reaction;
+					}
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'unreacted': {
+					const reaction = body.reaction;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
+					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
+
+					// Decrement the count
+					n.reactions = {
+						...this.appearNote.reactions,
+						[reaction]: Math.max(0, currentCount - 1)
+					};
+
+					if (body.userId === this.$i.id) {
+						n.myReaction = null;
+					}
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'pollVoted': {
+					const choice = body.choice;
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					const choices = [...this.appearNote.poll.choices];
+					choices[choice] = {
+						...choices[choice],
+						votes: choices[choice].votes + 1,
+						...(body.userId === this.$i.id ? {
+							isVoted: true
+						} : {})
+					};
+
+					n.poll = {
+						...this.appearNote.poll,
+						choices: choices
+					};
+
+					this.updateAppearNote(n);
+					break;
+				}
+
+				case 'deleted': {
+					this.isDeleted = true;
+					break;
+				}
+			}
+		},
+
+		reply(viaKeyboard = false) {
+			pleaseLogin();
+			this.operating = true;
+			os.post({
+				reply: this.appearNote,
+				animation: !viaKeyboard,
+			}, () => {
+				this.operating = false;
+				this.focus();
+			});
+		},
+
+		renote(viaKeyboard = false) {
+			pleaseLogin();
+			this.operating = true;
+			this.blur();
+			os.popupMenu([{
+				text: this.$ts.renote,
+				icon: 'fas fa-retweet',
+				action: () => {
+					os.api('notes/create', {
+						renoteId: this.appearNote.id
+					});
+				}
+			}, {
+				text: this.$ts.quote,
+				icon: 'fas fa-quote-right',
+				action: () => {
+					os.post({
+						renote: this.appearNote,
+					});
+				}
+			}], this.$refs.renoteButton, {
+				viaKeyboard
+			}).then(() => {
+				this.operating = false;
+			});
+		},
+
+		renoteDirectly() {
+			os.apiWithDialog('notes/create', {
+				renoteId: this.appearNote.id
+			}, undefined, (res: any) => {
+				os.dialog({
+					type: 'success',
+					text: this.$ts.renoted,
+				});
+			}, (e: Error) => {
+				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantRenote,
+					});
+				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantReRenote,
+					});
+				}
+			});
+		},
+
+		async react(viaKeyboard = false) {
+			pleaseLogin();
+			this.operating = true;
+			this.blur();
+			reactionPicker.show(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.operating = false;
+				this.focus();
+			});
+		},
+
+		reactDirectly(reaction) {
+			os.api('notes/reactions/create', {
+				noteId: this.appearNote.id,
+				reaction: reaction
+			});
+		},
+
+		undoReact(note) {
+			const oldReaction = note.myReaction;
+			if (!oldReaction) return;
+			os.api('notes/reactions/delete', {
+				noteId: note.id
+			});
+		},
+
+		favorite() {
+			pleaseLogin();
+			os.apiWithDialog('notes/favorites/create', {
+				noteId: this.appearNote.id
+			}, undefined, (res: any) => {
+				os.dialog({
+					type: 'success',
+					text: this.$ts.favorited,
+				});
+			}, (e: Error) => {
+				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.alreadyFavorited,
+					});
+				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.cantFavorite,
+					});
+				}
+			});
+		},
+
+		del() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.noteDeleteConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.api('notes/delete', {
+					noteId: this.appearNote.id
+				});
+			});
+		},
+
+		delEdit() {
+			os.dialog({
+				type: 'warning',
+				text: this.$ts.deleteAndEditConfirm,
+				showCancelButton: true
+			}).then(({ canceled }) => {
+				if (canceled) return;
+
+				os.api('notes/delete', {
+					noteId: this.appearNote.id
+				});
+
+				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
+			});
+		},
+
+		toggleFavorite(favorite: boolean) {
+			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
+		toggleWatch(watch: boolean) {
+			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
+				noteId: this.appearNote.id
+			});
+		},
+
+		getMenu() {
+			let menu;
+			if (this.$i) {
+				const statePromise = os.api('notes/state', {
+					noteId: this.appearNote.id
+				});
+
+				menu = [{
+					icon: 'fas fa-copy',
+					text: this.$ts.copyContent,
+					action: this.copyContent
+				}, {
+					icon: 'fas fa-link',
+					text: this.$ts.copyLink,
+					action: this.copyLink
+				}, (this.appearNote.url || this.appearNote.uri) ? {
+					icon: 'fas fa-external-link-square-alt',
+					text: this.$ts.showOnRemote,
+					action: () => {
+						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
+					}
+				} : undefined,
+				{
+					icon: 'fas fa-share-alt',
+					text: this.$ts.share,
+					action: this.share
+				},
+				null,
+				statePromise.then(state => state.isFavorited ? {
+					icon: 'fas fa-star',
+					text: this.$ts.unfavorite,
+					action: () => this.toggleFavorite(false)
+				} : {
+					icon: 'fas fa-star',
+					text: this.$ts.favorite,
+					action: () => this.toggleFavorite(true)
+				}),
+				{
+					icon: 'fas fa-paperclip',
+					text: this.$ts.clip,
+					action: () => this.clip()
+				},
+				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
+					icon: 'fas fa-eye-slash',
+					text: this.$ts.unwatch,
+					action: () => this.toggleWatch(false)
+				} : {
+					icon: 'fas fa-eye',
+					text: this.$ts.watch,
+					action: () => this.toggleWatch(true)
+				}) : undefined,
+				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
+					icon: 'fas fa-thumbtack',
+					text: this.$ts.unpin,
+					action: () => this.togglePin(false)
+				} : {
+					icon: 'fas fa-thumbtack',
+					text: this.$ts.pin,
+					action: () => this.togglePin(true)
+				} : undefined,
+				...(this.$i.isModerator || this.$i.isAdmin ? [
+					null,
+					{
+						icon: 'fas fa-bullhorn',
+						text: this.$ts.promote,
+						action: this.promote
+					}]
+					: []
+				),
+				...(this.appearNote.userId != this.$i.id ? [
+					null,
+					{
+						icon: 'fas fa-exclamation-circle',
+						text: this.$ts.reportAbuse,
+						action: () => {
+							const u = `${url}/notes/${this.appearNote.id}`;
+							os.popup(import('@/components/abuse-report-window.vue'), {
+								user: this.appearNote.user,
+								initialComment: `Note: ${u}\n-----\n`
+							}, {}, 'closed');
+						}
+					}]
+					: []
+				),
+				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
+					null,
+					this.appearNote.userId == this.$i.id ? {
+						icon: 'fas fa-edit',
+						text: this.$ts.deleteAndEdit,
+						action: this.delEdit
+					} : undefined,
+					{
+						icon: 'fas fa-trash-alt',
+						text: this.$ts.delete,
+						danger: true,
+						action: this.del
+					}]
+					: []
+				)]
+				.filter(x => x !== undefined);
+			} else {
+				menu = [{
+					icon: 'fas fa-copy',
+					text: this.$ts.copyContent,
+					action: this.copyContent
+				}, {
+					icon: 'fas fa-link',
+					text: this.$ts.copyLink,
+					action: this.copyLink
+				}, (this.appearNote.url || this.appearNote.uri) ? {
+					icon: 'fas fa-external-link-square-alt',
+					text: this.$ts.showOnRemote,
+					action: () => {
+						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
+					}
+				} : undefined]
+				.filter(x => x !== undefined);
+			}
+
+			if (noteActions.length > 0) {
+				menu = menu.concat([null, ...noteActions.map(action => ({
+					icon: 'fas fa-plug',
+					text: action.title,
+					action: () => {
+						action.handler(this.appearNote);
+					}
+				}))]);
+			}
+
+			return menu;
+		},
+
+		onContextmenu(e) {
+			const isLink = (el: HTMLElement) => {
+				if (el.tagName === 'A') return true;
+				if (el.parentElement) {
+					return isLink(el.parentElement);
+				}
+			};
+			if (isLink(e.target)) return;
+			if (window.getSelection().toString() !== '') return;
+			
+			if (this.$store.state.useReactionPickerForContextMenu) {
+				e.preventDefault();
+				this.react();
+			} else {
+				os.contextMenu(this.getMenu(), e).then(this.focus);
+			}
+		},
+
+		menu(viaKeyboard = false) {
+			this.operating = true;
+			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
+				viaKeyboard
+			}).then(() => {
+				this.operating = false;
+				this.focus();
+			});
+		},
+
+		showRenoteMenu(viaKeyboard = false) {
+			if (!this.isMyRenote) return;
+			os.popupMenu([{
+				text: this.$ts.unrenote,
+				icon: 'fas fa-trash-alt',
+				danger: true,
+				action: () => {
+					os.api('notes/delete', {
+						noteId: this.note.id
+					});
+					this.isDeleted = true;
+				}
+			}], this.$refs.renoteTime, {
+				viaKeyboard: viaKeyboard
+			});
+		},
+
+		toggleShowContent() {
+			this.showContent = !this.showContent;
+		},
+
+		copyContent() {
+			copyToClipboard(this.appearNote.text);
+			os.success();
+		},
+
+		copyLink() {
+			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
+			os.success();
+		},
+
+		togglePin(pin: boolean) {
+			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
+				noteId: this.appearNote.id
+			}, undefined, null, e => {
+				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
+					os.dialog({
+						type: 'error',
+						text: this.$ts.pinLimitExceeded
+					});
+				}
+			});
+		},
+
+		async clip() {
+			const clips = await os.api('clips/list');
+			os.popupMenu([{
+				icon: 'fas fa-plus',
+				text: this.$ts.createNew,
+				action: async () => {
+					const { canceled, result } = await os.form(this.$ts.createNewClip, {
+						name: {
+							type: 'string',
+							label: this.$ts.name
+						},
+						description: {
+							type: 'string',
+							required: false,
+							multiline: true,
+							label: this.$ts.description
+						},
+						isPublic: {
+							type: 'boolean',
+							label: this.$ts.public,
+							default: false
+						}
+					});
+					if (canceled) return;
+
+					const clip = await os.apiWithDialog('clips/create', result);
+
+					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
+				}
+			}, null, ...clips.map(clip => ({
+				text: clip.name,
+				action: () => {
+					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
+				}
+			}))], this.$refs.menuButton, {
+			}).then(this.focus);
+		},
+
+		async promote() {
+			const { canceled, result: days } = await os.dialog({
+				title: this.$ts.numberOfDays,
+				input: { type: 'number' }
+			});
+
+			if (canceled) return;
+
+			os.apiWithDialog('admin/promo/create', {
+				noteId: this.appearNote.id,
+				expiresAt: Date.now() + (86400000 * days)
+			});
+		},
+
+		share() {
+			navigator.share({
+				title: this.$t('noteOf', { user: this.appearNote.user.name }),
+				text: this.appearNote.text,
+				url: `${url}/notes/${this.appearNote.id}`
+			});
+		},
+
+		focus() {
+			this.$el.focus();
+		},
+
+		blur() {
+			this.$el.blur();
+		},
+
+		focusBefore() {
+			focusPrev(this.$el);
+		},
+
+		focusAfter() {
+			focusNext(this.$el);
+		},
+
+		userPage
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vfzoeqcg {
+	position: relative;
+	contain: content;
+
+	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
+	// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
+	// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
+	// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
+	// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか？)
+	//content-visibility: auto;
+  //contain-intrinsic-size: 0 128px;
+
+	&:focus-visible {
+		outline: none;
+	}
+
+	&:hover {
+		background: rgba(0, 0, 0, 0.05);
+	}
+
+	&:hover, &.operating {
+		> .article > .main > .footer {
+			display: block;
+		}
+	}
+
+	&.renote {
+		background: rgba(128, 255, 0, 0.05);
+	}
+
+	&.highlighted {
+		background: rgba(255, 128, 0, 0.05);
+	}
+
+	> .info {
+		display: flex;
+		align-items: center;
+		padding: 12px 16px 4px 16px;
+		line-height: 24px;
+		font-size: 85%;
+		white-space: pre;
+		color: #d28a3f;
+
+		> i {
+			margin-right: 4px;
+		}
+
+		> .hide {
+			margin-left: 16px;
+			color: inherit;
+			opacity: 0.7;
+		}
+	}
+
+	> .info + .article {
+		padding-top: 8px;
+	}
+
+	> .reply-to {
+		opacity: 0.7;
+		padding-bottom: 0;
+	}
+
+	> .renote {
+		display: flex;
+		align-items: center;
+		padding: 12px 16px 4px 16px;
+		line-height: 28px;
+		white-space: pre;
+		color: var(--renote);
+		font-size: 0.9em;
+
+		> .avatar {
+			flex-shrink: 0;
+			display: inline-block;
+			width: 28px;
+			height: 28px;
+			margin: 0 8px 0 0;
+			border-radius: 6px;
+		}
+
+		> i {
+			margin-right: 4px;
+		}
+
+		> span {
+			overflow: hidden;
+			flex-shrink: 1;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+
+			> .name {
+				font-weight: bold;
+			}
+		}
+
+		> .info {
+			margin-left: 8px;
+			font-size: 0.9em;
+			opacity: 0.7;
+
+			> .time {
+				flex-shrink: 0;
+				color: inherit;
+
+				> .dropdownIcon {
+					margin-right: 4px;
+				}
+			}
+
+			> .visibility {
+				margin-left: 8px;
+			}
+
+			> .localOnly {
+				margin-left: 8px;
+			}
+		}
+	}
+
+	> .renote + .article {
+		padding-top: 8px;
+	}
+
+	> .article {
+		display: flex;
+		padding: 12px 16px;
+
+		> .avatar {
+			flex-shrink: 0;
+			display: block;
+			position: sticky;
+			top: 0;
+			margin: 0 14px 0 0;
+			width: 46px;
+			height: 46px;
+		}
+
+		> .main {
+			flex: 1;
+			min-width: 0;
+
+			> .body {
+				> .cw {
+					cursor: default;
+					display: block;
+					margin: 0;
+					padding: 0;
+					overflow-wrap: break-word;
+
+					> .text {
+						margin-right: 8px;
+					}
+				}
+
+				> .content {
+					&.collapsed {
+						position: relative;
+						max-height: 9em;
+						overflow: hidden;
+
+						> .fade {
+							display: block;
+							position: absolute;
+							bottom: 0;
+							left: 0;
+							width: 100%;
+							height: 64px;
+							background: linear-gradient(0deg, var(--panel), var(--X15));
+
+							> span {
+								display: inline-block;
+								background: var(--panel);
+								padding: 6px 10px;
+								font-size: 0.8em;
+								border-radius: 999px;
+								box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
+							}
+
+							&:hover {
+								> span {
+									background: var(--panelHighlight);
+								}
+							}
+						}
+					}
+
+					> .text {
+						overflow-wrap: break-word;
+
+						> .reply {
+							color: var(--accent);
+							margin-right: 0.5em;
+						}
+
+						> .rp {
+							margin-left: 4px;
+							font-style: oblique;
+							color: var(--renote);
+						}
+					}
+
+					> .files {
+						max-width: 500px;
+					}
+
+					> .url-preview {
+						margin-top: 8px;
+						max-width: 500px;
+					}
+
+					> .poll {
+						font-size: 80%;
+						max-width: 500px;
+					}
+
+					> .renote {
+						padding: 8px 0;
+
+						> * {
+							padding: 16px;
+							border: dashed 1px var(--renote);
+							border-radius: 8px;
+						}
+					}
+				}
+
+				> .channel {
+					opacity: 0.7;
+					font-size: 80%;
+				}
+			}
+
+			> .footer {
+				display: none;
+				position: absolute;
+				top: 8px;
+				right: 8px;
+				padding: 0 6px;
+				opacity: 0.7;
+
+				&:hover {
+					opacity: 1;
+				}
+
+				> .button {
+					margin: 0;
+					padding: 8px;
+					opacity: 0.7;
+
+					&:hover {
+						color: var(--accent);
+					}
+
+					> .count {
+						display: inline;
+						margin: 0 0 0 8px;
+						opacity: 0.7;
+					}
+
+					&.reacted {
+						color: var(--accent);
+					}
+				}
+			}
+		}
+	}
+
+	> .reply {
+		border-top: solid 0.5px var(--divider);
+	}
+}
+
+.muted {
+	padding: 8px 16px;
+	opacity: 0.7;
+
+	&:hover {
+		background: rgba(0, 0, 0, 0.05);
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/notes.vue b/packages/client/src/ui/chat/notes.vue
new file mode 100644
index 0000000000..9103f717e6
--- /dev/null
+++ b/packages/client/src/ui/chat/notes.vue
@@ -0,0 +1,94 @@
+<template>
+<div class="">
+	<div class="_fullinfo" v-if="empty">
+		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+		<div>{{ $ts.noNotes }}</div>
+	</div>
+
+	<MkLoading v-if="fetching"/>
+
+	<MkError v-if="error" @retry="init()"/>
+
+	<div v-show="more && reversed" style="margin-bottom: var(--margin);">
+		<MkButton style="margin: 0 auto;" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+			<template v-if="moreFetching"><MkLoading inline/></template>
+		</MkButton>
+	</div>
+
+	<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :ad="true">
+		<XNote :note="note" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
+	</XList>
+
+	<div v-show="more && !reversed" style="margin-top: var(--margin);">
+		<MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+			<template v-if="moreFetching"><MkLoading inline/></template>
+		</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import paging from '@/scripts/paging';
+import XNote from './note.vue';
+import XList from './date-separated-list.vue';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+	components: {
+		XNote, XList, MkButton,
+	},
+
+	mixins: [
+		paging({
+			before: (self) => {
+				self.$emit('before');
+			},
+
+			after: (self, e) => {
+				self.$emit('after', e);
+			}
+		}),
+	],
+
+	props: {
+		pagination: {
+			required: true
+		},
+
+		prop: {
+			type: String,
+			required: false
+		}
+	},
+
+	emits: ['before', 'after'],
+
+	computed: {
+		notes(): any[] {
+			return this.prop ? this.items.map(item => item[this.prop]) : this.items;
+		},
+
+		reversed(): boolean {
+			return this.pagination.reversed;
+		}
+	},
+
+	methods: {
+		updated(oldValue, newValue) {
+			const i = this.notes.findIndex(n => n === oldValue);
+			if (this.prop) {
+				this.items[i][this.prop] = newValue;
+			} else {
+				this.items[i] = newValue;
+			}
+		},
+
+		focus() {
+			this.$refs.notes.focus();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/ui/chat/pages/channel.vue b/packages/client/src/ui/chat/pages/channel.vue
new file mode 100644
index 0000000000..5152af20f9
--- /dev/null
+++ b/packages/client/src/ui/chat/pages/channel.vue
@@ -0,0 +1,259 @@
+<template>
+<div v-if="channel" class="hhizbblb">
+	<div class="info" v-if="date">
+		<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
+	</div>
+	<div class="tl" ref="body">
+		<div class="new" v-if="queue > 0" :style="{ width: width + 'px', bottom: bottom + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
+		<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="true"/>
+	</div>
+	<div class="bottom">
+		<div class="typers" v-if="typers.length > 0">
+			<I18n :src="$ts.typingUsers" text-tag="span" class="users">
+				<template #users>
+					<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
+				</template>
+			</I18n>
+			<MkEllipsis/>
+		</div>
+		<XPostForm :channel="channel"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw } from 'vue';
+import * as Misskey from 'misskey-js';
+import XNotes from '../notes.vue';
+import * as os from '@/os';
+import * as sound from '@/scripts/sound';
+import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
+import follow from '@/directives/follow-append';
+import XPostForm from '../post-form.vue';
+import MkInfo from '@/components/ui/info.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes,
+		XPostForm,
+		MkInfo,
+	},
+
+	directives: {
+		follow
+	},
+	
+	provide() {
+		return {
+			inChannel: true
+		};
+	},
+
+	props: {
+		channelId: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			channel: null as Misskey.entities.Channel | null,
+			connection: null,
+			pagination: null,
+			baseQuery: {
+				includeMyRenotes: this.$store.state.showMyRenotes,
+				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
+				includeLocalRenotes: this.$store.state.showLocalRenotes
+			},
+			queue: 0,
+			width: 0,
+			top: 0,
+			bottom: 0,
+			typers: [],
+			date: null,
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.channel ? this.channel.name : '-',
+				subtitle: this.channel ? this.channel.description : '-',
+				icon: 'fas fa-satellite-dish',
+				actions: [{
+					icon: this.channel?.isFollowing ? 'fas fa-star' : 'far fa-star',
+					text: this.channel?.isFollowing ? this.$ts.unfollow : this.$ts.follow,
+					highlighted: this.channel?.isFollowing,
+					handler: this.toggleChannelFollow
+				}, {
+					icon: 'fas fa-search',
+					text: this.$ts.inChannelSearch,
+					handler: this.inChannelSearch
+				}, {
+					icon: 'fas fa-calendar-alt',
+					text: this.$ts.jumpToSpecifiedDate,
+					handler: this.timetravel
+				}]
+			})),
+		};
+	},
+
+	async created() {
+		this.channel = await os.api('channels/show', { channelId: this.channelId });
+
+		const prepend = note => {
+			(this.$refs.tl as any).prepend(note);
+
+			this.$emit('note');
+
+			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
+		};
+
+		this.connection = markRaw(os.stream.useChannel('channel', {
+			channelId: this.channelId
+		}));
+		this.connection.on('note', prepend);
+		this.connection.on('typers', typers => {
+			this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers;
+		});
+
+		this.pagination = {
+			endpoint: 'channels/timeline',
+			reversed: true,
+			limit: 10,
+			params: init => ({
+				channelId: this.channelId,
+				untilDate: this.date?.getTime(),
+				...this.baseQuery
+			})
+		};
+	},
+
+	mounted() {
+
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		focus() {
+			this.$refs.body.focus();
+		},
+
+		goTop() {
+			const container = getScrollContainer(this.$refs.body);
+			container.scrollTop = 0;
+		},
+
+		queueUpdated(q) {
+			if (this.$refs.body.offsetWidth !== 0) {
+				const rect = this.$refs.body.getBoundingClientRect();
+				this.width = this.$refs.body.offsetWidth;
+				this.top = rect.top;
+				this.bottom = this.$refs.body.offsetHeight;
+			}
+			this.queue = q;
+		},
+
+		async inChannelSearch() {
+			const { canceled, result: query } = await os.dialog({
+				title: this.$ts.inChannelSearch,
+				input: true
+			});
+			if (canceled || query == null || query === '') return;
+			router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.channelId}`);
+		},
+
+		async toggleChannelFollow() {
+			if (this.channel.isFollowing) {
+				await os.apiWithDialog('channels/unfollow', {
+					channelId: this.channel.id
+				});
+				this.channel.isFollowing = false;
+			} else {
+				await os.apiWithDialog('channels/follow', {
+					channelId: this.channel.id
+				});
+				this.channel.isFollowing = true;
+			}
+		},
+
+		openChannelMenu(ev) {
+			os.popupMenu([{
+				text: this.$ts.copyUrl,
+				icon: 'fas fa-link',
+				action: () => {
+					copyToClipboard(`${url}/channels/${this.currentChannel.id}`);
+				}
+			}], ev.currentTarget || ev.target);
+		},
+
+		timetravel(date?: Date) {
+			this.date = date;
+			this.$refs.tl.reload();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hhizbblb {
+	display: flex;
+	flex-direction: column;
+	flex: 1;
+	overflow: auto;
+
+	> .info {
+		padding: 16px 16px 0 16px;
+	}
+
+	> .top {
+		padding: 16px 16px 0 16px;
+	}
+
+	> .bottom {
+		padding: 0 16px 16px 16px;
+		position: relative;
+
+		> .typers {
+			position: absolute;
+			bottom: 100%;
+			padding: 0 8px 0 8px;
+			font-size: 0.9em;
+			background: var(--panel);
+			border-radius: 0 8px 0 0;
+			color: var(--fgTransparentWeak);
+
+			> .users {
+				> .user + .user:before {
+					content: ", ";
+					font-weight: normal;
+				}
+
+				> .user:last-of-type:after {
+					content: " ";
+				}
+			}
+		}
+	}
+
+	> .tl {
+		position: relative;
+		padding: 16px 0;
+		flex: 1;
+		min-width: 0;
+		overflow: auto;
+
+		> .new {
+			position: fixed;
+			z-index: 1000;
+
+			> button {
+				display: block;
+				margin: 16px auto;
+				padding: 8px 16px;
+				border-radius: 32px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/pages/timeline.vue b/packages/client/src/ui/chat/pages/timeline.vue
new file mode 100644
index 0000000000..f4dfdf891e
--- /dev/null
+++ b/packages/client/src/ui/chat/pages/timeline.vue
@@ -0,0 +1,221 @@
+<template>
+<div class="dbiokgaf">
+	<div class="info" v-if="date">
+		<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
+	</div>
+	<div class="top">
+		<XPostForm/>
+	</div>
+	<div class="tl" ref="body">
+		<div class="new" v-if="queue > 0" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
+		<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw } from 'vue';
+import XNotes from '../notes.vue';
+import * as os from '@/os';
+import * as sound from '@/scripts/sound';
+import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
+import follow from '@/directives/follow-append';
+import XPostForm from '../post-form.vue';
+import MkInfo from '@/components/ui/info.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes,
+		XPostForm,
+		MkInfo,
+	},
+
+	directives: {
+		follow
+	},
+
+	props: {
+		src: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			connection: null,
+			connection2: null,
+			pagination: null,
+			baseQuery: {
+				includeMyRenotes: this.$store.state.showMyRenotes,
+				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
+				includeLocalRenotes: this.$store.state.showLocalRenotes
+			},
+			query: {},
+			queue: 0,
+			width: 0,
+			top: 0,
+			bottom: 0,
+			typers: [],
+			date: null,
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.$ts.timeline,
+				icon: 'fas fa-home',
+				actions: [{
+					icon: 'fas fa-calendar-alt',
+					text: this.$ts.jumpToSpecifiedDate,
+					handler: this.timetravel
+				}]
+			})),
+		};
+	},
+
+	created() {
+		const prepend = note => {
+			(this.$refs.tl as any).prepend(note);
+
+			this.$emit('note');
+
+			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
+		};
+
+		const onChangeFollowing = () => {
+			if (!this.$refs.tl.backed) {
+				this.$refs.tl.reload();
+			}
+		};
+
+		let endpoint;
+
+		if (this.src == 'home') {
+			endpoint = 'notes/timeline';
+			this.connection = markRaw(os.stream.useChannel('homeTimeline'));
+			this.connection.on('note', prepend);
+
+			this.connection2 = markRaw(os.stream.useChannel('main'));
+			this.connection2.on('follow', onChangeFollowing);
+			this.connection2.on('unfollow', onChangeFollowing);
+		} else if (this.src == 'local') {
+			endpoint = 'notes/local-timeline';
+			this.connection = markRaw(os.stream.useChannel('localTimeline'));
+			this.connection.on('note', prepend);
+		} else if (this.src == 'social') {
+			endpoint = 'notes/hybrid-timeline';
+			this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
+			this.connection.on('note', prepend);
+		} else if (this.src == 'global') {
+			endpoint = 'notes/global-timeline';
+			this.connection = markRaw(os.stream.useChannel('globalTimeline'));
+			this.connection.on('note', prepend);
+		}
+
+		this.pagination = {
+			endpoint: endpoint,
+			limit: 10,
+			params: init => ({
+				untilDate: this.date?.getTime(),
+				...this.baseQuery, ...this.query
+			})
+		};
+	},
+
+	mounted() {
+
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+		if (this.connection2) this.connection2.dispose();
+	},
+
+	methods: {
+		focus() {
+			this.$refs.body.focus();
+		},
+
+		goTop() {
+			const container = getScrollContainer(this.$refs.body);
+			container.scrollTop = 0;
+		},
+
+		queueUpdated(q) {
+			if (this.$refs.body.offsetWidth !== 0) {
+				const rect = this.$refs.body.getBoundingClientRect();
+				this.width = this.$refs.body.offsetWidth;
+				this.top = rect.top;
+				this.bottom = this.$refs.body.offsetHeight;
+			}
+			this.queue = q;
+		},
+
+		timetravel(date?: Date) {
+			this.date = date;
+			this.$refs.tl.reload();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.dbiokgaf {
+	display: flex;
+	flex-direction: column;
+	flex: 1;
+	overflow: auto;
+
+	> .info {
+		padding: 16px 16px 0 16px;
+	}
+
+	> .top {
+		padding: 16px 16px 0 16px;
+	}
+
+	> .bottom {
+		padding: 0 16px 16px 16px;
+		position: relative;
+
+		> .typers {
+			position: absolute;
+			bottom: 100%;
+			padding: 0 8px 0 8px;
+			font-size: 0.9em;
+			background: var(--panel);
+			border-radius: 0 8px 0 0;
+			color: var(--fgTransparentWeak);
+
+			> .users {
+				> .user + .user:before {
+					content: ", ";
+					font-weight: normal;
+				}
+
+				> .user:last-of-type:after {
+					content: " ";
+				}
+			}
+		}
+	}
+
+	> .tl {
+		position: relative;
+		padding: 16px 0;
+		flex: 1;
+		min-width: 0;
+		overflow: auto;
+
+		> .new {
+			position: fixed;
+			z-index: 1000;
+
+			> button {
+				display: block;
+				margin: 16px auto;
+				padding: 8px 16px;
+				border-radius: 32px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/post-form.vue b/packages/client/src/ui/chat/post-form.vue
new file mode 100644
index 0000000000..44461c4a58
--- /dev/null
+++ b/packages/client/src/ui/chat/post-form.vue
@@ -0,0 +1,772 @@
+<template>
+<div class="pxiwixjf"
+	@dragover.stop="onDragover"
+	@dragenter="onDragenter"
+	@dragleave="onDragleave"
+	@drop.stop="onDrop"
+>
+	<div class="form">
+		<div class="with-quote" v-if="quoteId"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
+		<div v-if="visibility === 'specified'" class="to-specified">
+			<span style="margin-right: 8px;">{{ $ts.recipient }}</span>
+			<div class="visibleUsers">
+				<span v-for="u in visibleUsers" :key="u.id">
+					<MkAcct :user="u"/>
+					<button class="_button" @click="removeVisibleUser(u)"><i class="fas fa-times"></i></button>
+				</span>
+				<button @click="addVisibleUser" class="_buttonPrimary"><i class="fas fa-plus fa-fw"></i></button>
+			</div>
+		</div>
+		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
+		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd" />
+		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
+		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
+		<footer>
+			<div class="left">
+				<button class="_button" @click="chooseFileFrom" v-tooltip="$ts.attachFile"><i class="fas fa-photo-video"></i></button>
+				<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$ts.poll"><i class="fas fa-poll-h"></i></button>
+				<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$ts.useCw"><i class="fas fa-eye-slash"></i></button>
+				<button class="_button" @click="insertMention" v-tooltip="$ts.mention"><i class="fas fa-at"></i></button>
+				<button class="_button" @click="insertEmoji" v-tooltip="$ts.emoji"><i class="fas fa-laugh-squint"></i></button>
+				<button class="_button" @click="showActions" v-tooltip="$ts.plugin" v-if="postFormActions.length > 0"><i class="fas fa-plug"></i></button>
+			</div>
+			<div class="right">
+				<span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
+				<span class="local-only" v-if="localOnly"><i class="fas fa-biohazard"></i></span>
+				<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$ts.visibility" :disabled="channel != null">
+					<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
+					<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
+					<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
+					<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
+				</button>
+				<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
+			</div>
+		</footer>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import insertTextAtCursor from 'insert-text-at-cursor';
+import { length } from 'stringz';
+import { toASCII } from 'punycode/';
+import * as mfm from 'mfm-js';
+import { host, url } from '@/config';
+import { erase, unique } from '@/scripts/array';
+import { extractMentions } from '@/scripts/extract-mentions';
+import * as Acct from 'misskey-js/built/acct';
+import { formatTimeString } from '@/scripts/format-time-string';
+import { Autocomplete } from '@/scripts/autocomplete';
+import * as os from '@/os';
+import { selectFile } from '@/scripts/select-file';
+import { notePostInterruptors, postFormActions } from '@/store';
+import { isMobile } from '@/scripts/is-mobile';
+import { throttle } from 'throttle-debounce';
+
+export default defineComponent({
+	components: {
+		XPostFormAttaches: defineAsyncComponent(() => import('@/components/post-form-attaches.vue')),
+		XPollEditor: defineAsyncComponent(() => import('@/components/poll-editor.vue'))
+	},
+
+	props: {
+		reply: {
+			type: Object,
+			required: false
+		},
+		renote: {
+			type: Object,
+			required: false
+		},
+		channel: {
+			type: String,
+			required: false
+		},
+		mention: {
+			type: Object,
+			required: false
+		},
+		specified: {
+			type: Object,
+			required: false
+		},
+		initialText: {
+			type: String,
+			required: false
+		},
+		initialNote: {
+			type: Object,
+			required: false
+		},
+		share: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		autofocus: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	emits: ['posted', 'cancel', 'esc'],
+
+	data() {
+		return {
+			posting: false,
+			text: '',
+			files: [],
+			poll: null,
+			useCw: false,
+			cw: null,
+			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
+			visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
+			visibleUsers: [],
+			autocomplete: null,
+			draghover: false,
+			quoteId: null,
+			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
+			imeText: '',
+			typing: throttle(3000, () => {
+				if (this.channel) {
+					os.stream.send('typingOnChannel', { channel: this.channel });
+				}
+			}),
+			postFormActions,
+		};
+	},
+
+	computed: {
+		draftKey(): string {
+			let key = this.channel ? `channel:${this.channel}` : '';
+
+			if (this.renote) {
+				key += `renote:${this.renote.id}`;
+			} else if (this.reply) {
+				key += `reply:${this.reply.id}`;
+			} else {
+				key += 'note';
+			}
+
+			return key;
+		},
+
+		placeholder(): string {
+			if (this.renote) {
+				return this.$ts._postForm.quotePlaceholder;
+			} else if (this.reply) {
+				return this.$ts._postForm.replyPlaceholder;
+			} else if (this.channel) {
+				return this.$ts._postForm.channelPlaceholder;
+			} else {
+				const xs = [
+					this.$ts._postForm._placeholders.a,
+					this.$ts._postForm._placeholders.b,
+					this.$ts._postForm._placeholders.c,
+					this.$ts._postForm._placeholders.d,
+					this.$ts._postForm._placeholders.e,
+					this.$ts._postForm._placeholders.f
+				];
+				return xs[Math.floor(Math.random() * xs.length)];
+			}
+		},
+
+		submitText(): string {
+			return this.renote
+				? this.$ts.quote
+				: this.reply
+					? this.$ts.reply
+					: this.$ts.note;
+		},
+
+		textLength(): number {
+			return length((this.text + this.imeText).trim());
+		},
+
+		canPost(): boolean {
+			return !this.posting &&
+				(1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
+				(this.textLength <= this.max) &&
+				(!this.poll || this.poll.choices.length >= 2);
+		},
+
+		max(): number {
+			return this.$instance ? this.$instance.maxNoteTextLength : 1000;
+		}
+	},
+
+	mounted() {
+		if (this.initialText) {
+			this.text = this.initialText;
+		}
+
+		if (this.mention) {
+			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
+			this.text += ' ';
+		}
+
+		if (this.reply && this.reply.user.host != null) {
+			this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
+		}
+
+		if (this.reply && this.reply.text != null) {
+			const ast = mfm.parse(this.reply.text);
+
+			for (const x of extractMentions(ast)) {
+				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
+
+				// 自分は除外
+				if (this.$i.username == x.username && x.host == null) continue;
+				if (this.$i.username == x.username && x.host == host) continue;
+
+				// 重複は除外
+				if (this.text.indexOf(`${mention} `) != -1) continue;
+
+				this.text += `${mention} `;
+			}
+		}
+
+		if (this.channel) {
+			this.visibility = 'public';
+			this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+		}
+
+		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
+		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
+			this.visibility = this.reply.visibility;
+			if (this.reply.visibility === 'specified') {
+				os.api('users/show', {
+					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
+				}).then(users => {
+					this.visibleUsers.push(...users);
+				});
+
+				if (this.reply.userId !== this.$i.id) {
+					os.api('users/show', { userId: this.reply.userId }).then(user => {
+						this.visibleUsers.push(user);
+					});
+				}
+			}
+		}
+
+		if (this.specified) {
+			this.visibility = 'specified';
+			this.visibleUsers.push(this.specified);
+		}
+
+		// keep cw when reply
+		if (this.$store.state.keepCw && this.reply && this.reply.cw) {
+			this.useCw = true;
+			this.cw = this.reply.cw;
+		}
+
+		if (this.autofocus) {
+			this.focus();
+
+			this.$nextTick(() => {
+				this.focus();
+			});
+		}
+
+		// TODO: detach when unmount
+		new Autocomplete(this.$refs.text, this, { model: 'text' });
+		new Autocomplete(this.$refs.cw, this, { model: 'cw' });
+
+		this.$nextTick(() => {
+			// 書きかけの投稿を復元
+			if (!this.share && !this.mention && !this.specified) {
+				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
+				if (draft) {
+					this.text = draft.data.text;
+					this.useCw = draft.data.useCw;
+					this.cw = draft.data.cw;
+					this.visibility = draft.data.visibility;
+					this.localOnly = draft.data.localOnly;
+					this.files = (draft.data.files || []).filter(e => e);
+					if (draft.data.poll) {
+						this.poll = draft.data.poll;
+					}
+				}
+			}
+
+			// 削除して編集
+			if (this.initialNote) {
+				const init = this.initialNote;
+				this.text = init.text ? init.text : '';
+				this.files = init.files;
+				this.cw = init.cw;
+				this.useCw = init.cw != null;
+				if (init.poll) {
+					this.poll = init.poll;
+				}
+				this.visibility = init.visibility;
+				this.localOnly = init.localOnly;
+				this.quoteId = init.renote ? init.renote.id : null;
+			}
+
+			this.$nextTick(() => this.watch());
+		});
+	},
+
+	methods: {
+		watch() {
+			this.$watch('text', () => this.saveDraft());
+			this.$watch('useCw', () => this.saveDraft());
+			this.$watch('cw', () => this.saveDraft());
+			this.$watch('poll', () => this.saveDraft());
+			this.$watch('files', () => this.saveDraft(), { deep: true });
+			this.$watch('visibility', () => this.saveDraft());
+			this.$watch('localOnly', () => this.saveDraft());
+		},
+
+		togglePoll() {
+			if (this.poll) {
+				this.poll = null;
+			} else {
+				this.poll = {
+					choices: ['', ''],
+					multiple: false,
+					expiresAt: null,
+					expiredAfter: null,
+				};
+			}
+		},
+
+		addTag(tag: string) {
+			insertTextAtCursor(this.$refs.text, ` #${tag} `);
+		},
+
+		focus() {
+			(this.$refs.text as any).focus();
+		},
+
+		chooseFileFrom(ev) {
+			selectFile(ev.currentTarget || ev.target, this.$ts.attachFile, true).then(files => {
+				for (const file of files) {
+					this.files.push(file);
+				}
+			});
+		},
+
+		detachFile(id) {
+			this.files = this.files.filter(x => x.id != id);
+		},
+
+		updateFiles(files) {
+			this.files = files;
+		},
+
+		updateFileSensitive(file, sensitive) {
+			this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
+		},
+
+		updateFileName(file, name) {
+			this.files[this.files.findIndex(x => x.id === file.id)].name = name;
+		},
+
+		upload(file: File, name?: string) {
+			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
+				this.files.push(res);
+			});
+		},
+
+		onPollUpdate(poll) {
+			this.poll = poll;
+			this.saveDraft();
+		},
+
+		setVisibility() {
+			if (this.channel) {
+				// TODO: information dialog
+				return;
+			}
+
+			os.popup(import('@/components/visibility-picker.vue'), {
+				currentVisibility: this.visibility,
+				currentLocalOnly: this.localOnly,
+				src: this.$refs.visibilityButton
+			}, {
+				changeVisibility: visibility => {
+					this.visibility = visibility;
+					if (this.$store.state.rememberNoteVisibility) {
+						this.$store.set('visibility', visibility);
+					}
+				},
+				changeLocalOnly: localOnly => {
+					this.localOnly = localOnly;
+					if (this.$store.state.rememberNoteVisibility) {
+						this.$store.set('localOnly', localOnly);
+					}
+				}
+			}, 'closed');
+		},
+
+		addVisibleUser() {
+			os.selectUser().then(user => {
+				this.visibleUsers.push(user);
+			});
+		},
+
+		removeVisibleUser(user) {
+			this.visibleUsers = erase(user, this.visibleUsers);
+		},
+
+		clear() {
+			this.text = '';
+			this.files = [];
+			this.poll = null;
+			this.quoteId = null;
+		},
+
+		onKeydown(e: KeyboardEvent) {
+			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
+			if (e.which === 27) this.$emit('esc');
+			this.typing();
+		},
+
+		onCompositionUpdate(e: CompositionEvent) {
+			this.imeText = e.data;
+			this.typing();
+		},
+
+		onCompositionEnd(e: CompositionEvent) {
+			this.imeText = '';
+		},
+
+		async onPaste(e: ClipboardEvent) {
+			for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
+				if (item.kind == 'file') {
+					const file = item.getAsFile();
+					const lio = file.name.lastIndexOf('.');
+					const ext = lio >= 0 ? file.name.slice(lio) : '';
+					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+					this.upload(file, formatted);
+				}
+			}
+
+			const paste = e.clipboardData.getData('text');
+
+			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
+				e.preventDefault();
+
+				os.dialog({
+					type: 'info',
+					text: this.$ts.quoteQuestion,
+					showCancelButton: true
+				}).then(({ canceled }) => {
+					if (canceled) {
+						insertTextAtCursor(this.$refs.text, paste);
+						return;
+					}
+
+					this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+				});
+			}
+		},
+
+		onDragover(e) {
+			if (!e.dataTransfer.items[0]) return;
+			const isFile = e.dataTransfer.items[0].kind == 'file';
+			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+			if (isFile || isDriveFile) {
+				e.preventDefault();
+				this.draghover = true;
+				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+			}
+		},
+
+		onDragenter(e) {
+			this.draghover = true;
+		},
+
+		onDragleave(e) {
+			this.draghover = false;
+		},
+
+		onDrop(e): void {
+			this.draghover = false;
+
+			// ファイルだったら
+			if (e.dataTransfer.files.length > 0) {
+				e.preventDefault();
+				for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
+				return;
+			}
+
+			//#region ドライブのファイル
+			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+			if (driveFile != null && driveFile != '') {
+				const file = JSON.parse(driveFile);
+				this.files.push(file);
+				e.preventDefault();
+			}
+			//#endregion
+		},
+
+		saveDraft() {
+			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+
+			data[this.draftKey] = {
+				updatedAt: new Date(),
+				data: {
+					text: this.text,
+					useCw: this.useCw,
+					cw: this.cw,
+					visibility: this.visibility,
+					localOnly: this.localOnly,
+					files: this.files,
+					poll: this.poll
+				}
+			};
+
+			localStorage.setItem('drafts', JSON.stringify(data));
+		},
+
+		deleteDraft() {
+			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+
+			delete data[this.draftKey];
+
+			localStorage.setItem('drafts', JSON.stringify(data));
+		},
+
+		async post() {
+			let data = {
+				text: this.text == '' ? undefined : this.text,
+				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+				replyId: this.reply ? this.reply.id : undefined,
+				renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
+				channelId: this.channel ? this.channel : undefined,
+				poll: this.poll,
+				cw: this.useCw ? this.cw || '' : undefined,
+				localOnly: this.localOnly,
+				visibility: this.visibility,
+				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
+				viaMobile: isMobile
+			};
+
+			// plugin
+			if (notePostInterruptors.length > 0) {
+				for (const interruptor of notePostInterruptors) {
+					data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
+				}
+			}
+
+			this.posting = true;
+			os.api('notes/create', data).then(() => {
+				this.clear();
+				this.$nextTick(() => {
+					this.deleteDraft();
+					this.$emit('posted');
+					if (this.text && this.text != '') {
+						const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
+						const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
+						localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
+					}
+					this.posting = false;
+				});
+			}).catch(err => {
+				this.posting = false;
+				os.dialog({
+					type: 'error',
+					text: err.message + '\n' + (err as any).id,
+				});
+			});
+		},
+
+		cancel() {
+			this.$emit('cancel');
+		},
+
+		insertMention() {
+			os.selectUser().then(user => {
+				insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' ');
+			});
+		},
+
+		async insertEmoji(ev) {
+			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
+		},
+
+		showActions(ev) {
+			os.popupMenu(postFormActions.map(action => ({
+				text: action.title,
+				action: () => {
+					action.handler({
+						text: this.text
+					}, (key, value) => {
+						if (key === 'text') { this.text = value; }
+					});
+				}
+			})), ev.currentTarget || ev.target);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.pxiwixjf {
+	position: relative;
+	border: solid 0.5px var(--divider);
+	border-radius: 8px;
+
+	> .form {
+		> .preview {
+			padding: 16px;
+		}
+
+		> .with-quote {
+			margin: 0 0 8px 0;
+			color: var(--accent);
+
+			> button {
+				padding: 4px 8px;
+				color: var(--accentAlpha04);
+
+				&:hover {
+					color: var(--accentAlpha06);
+				}
+
+				&:active {
+					color: var(--accentDarken30);
+				}
+			}
+		}
+
+		> .to-specified {
+			padding: 6px 24px;
+			margin-bottom: 8px;
+			overflow: auto;
+			white-space: nowrap;
+
+			> .visibleUsers {
+				display: inline;
+				top: -1px;
+				font-size: 14px;
+
+				> button {
+					padding: 4px;
+					border-radius: 8px;
+				}
+
+				> span {
+					margin-right: 14px;
+					padding: 8px 0 8px 8px;
+					border-radius: 8px;
+					background: var(--X4);
+
+					> button {
+						padding: 4px 8px;
+					}
+				}
+			}
+		}
+
+		> .cw,
+		> .text {
+			display: block;
+			box-sizing: border-box;
+			padding: 16px;
+			margin: 0;
+			width: 100%;
+			font-size: 16px;
+			border: none;
+			border-radius: 0;
+			background: transparent;
+			color: var(--fg);
+			font-family: inherit;
+
+			&:focus {
+				outline: none;
+			}
+
+			&:disabled {
+				opacity: 0.5;
+			}
+		}
+
+		> .cw {
+			z-index: 1;
+			padding-bottom: 8px;
+			border-bottom: solid 0.5px var(--divider);
+		}
+
+		> .text {
+			max-width: 100%;
+			min-width: 100%;
+			min-height: 60px;
+
+			&.withCw {
+				padding-top: 8px;
+			}
+		}
+
+		> footer {
+			$height: 44px;
+			display: flex;
+			padding: 0 8px 8px 8px;
+			line-height: $height;
+
+			> .left {
+				> button {
+					display: inline-block;
+					padding: 0;
+					margin: 0;
+					font-size: 16px;
+					width: $height;
+					height: $height;
+					border-radius: 6px;
+
+					&:hover {
+						background: var(--X5);
+					}
+
+					&.active {
+						color: var(--accent);
+					}
+				}
+			}
+
+			> .right {
+				margin-left: auto;
+
+				> .text-count {
+					opacity: 0.7;
+				}
+
+				> .visibility {
+					width: $height;
+					margin: 0 8px;
+
+					& + .localOnly {
+						margin-left: 0 !important;
+					}
+				}
+				
+				> .local-only {
+					margin: 0 0 0 12px;
+					opacity: 0.7;
+				}
+
+				> .submit {
+					margin: 0;
+					padding: 0 12px;
+					line-height: 34px;
+					font-weight: bold;
+					border-radius: 4px;
+
+					&:disabled {
+						opacity: 0.7;
+					}
+
+					> i {
+						margin-left: 6px;
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/side.vue b/packages/client/src/ui/chat/side.vue
new file mode 100644
index 0000000000..73881b23c0
--- /dev/null
+++ b/packages/client/src/ui/chat/side.vue
@@ -0,0 +1,157 @@
+<template>
+<div class="mrajymqm _narrow_" v-if="component">
+	<header class="header" @contextmenu.prevent.stop="onContextmenu">
+		<MkHeader class="title" :info="pageInfo" :center="false"/>
+	</header>
+	<component :is="component" v-bind="props" :ref="changePage" class="body"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { resolve } from '@/router';
+import { url } from '@/config';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+	},
+
+	provide() {
+		return {
+			navHook: (path) => {
+				this.navigate(path);
+			}
+		};
+	},
+
+	data() {
+		return {
+			path: null,
+			component: null,
+			props: {},
+			pageInfo: null,
+			history: [],
+		};
+	},
+
+	computed: {
+		url(): string {
+			return url + this.path;
+		}
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+			}
+		},
+
+		navigate(path, record = true) {
+			if (record && this.path) this.history.push(this.path);
+			this.path = path;
+			const { component, props } = resolve(path);
+			this.component = component;
+			this.props = props;
+			this.$emit('open');
+		},
+
+		back() {
+			this.navigate(this.history.pop(), false);
+		},
+
+		close() {
+			this.path = null;
+			this.component = null;
+			this.props = {};
+			this.$emit('close');
+		},
+
+		onContextmenu(e) {
+			os.contextMenu([{
+				type: 'label',
+				text: this.path,
+			}, {
+				icon: 'fas fa-expand-alt',
+				text: this.$ts.showInPage,
+				action: () => {
+					this.$router.push(this.path);
+					this.close();
+				}
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(this.path);
+					this.close();
+				}
+			}, null, {
+				icon: 'fas fa-external-link-alt',
+				text: this.$ts.openInNewTab,
+				action: () => {
+					window.open(this.url, '_blank');
+					this.close();
+				}
+			}, {
+				icon: 'fas fa-link',
+				text: this.$ts.copyLink,
+				action: () => {
+					copyToClipboard(this.url);
+				}
+			}], e);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mrajymqm {
+	$header-height: 54px; // TODO: どこかに集約したい
+
+	--root-margin: 16px;
+	--margin: var(--marginHalf);
+
+	height: 100%;
+	overflow: auto;
+	box-sizing: border-box;
+
+	> .header {
+		display: flex;
+		position: sticky;
+		z-index: 1000;
+		top: 0;
+		height: $header-height;
+		width: 100%;
+		font-weight: bold;
+		//background-color: var(--panel);
+		-webkit-backdrop-filter: var(--blur, blur(32px));
+		backdrop-filter: var(--blur, blur(32px));
+		background-color: var(--header);
+		border-bottom: solid 0.5px var(--divider);
+		box-sizing: border-box;
+
+		> ._button {
+			height: $header-height;
+			width: $header-height;
+
+			&:hover {
+				color: var(--fgHighlighted);
+			}
+		}
+
+		> .title {
+			flex: 1;
+			position: relative;
+		}
+	}
+
+	> .body {
+
+	}
+}
+</style>
+
diff --git a/packages/client/src/ui/chat/store.ts b/packages/client/src/ui/chat/store.ts
new file mode 100644
index 0000000000..389d56afb6
--- /dev/null
+++ b/packages/client/src/ui/chat/store.ts
@@ -0,0 +1,17 @@
+import { markRaw } from 'vue';
+import { Storage } from '../../pizzax';
+
+export const store = markRaw(new Storage('chatUi', {
+	widgets: {
+		where: 'account',
+		default: [] as {
+			name: string;
+			id: string;
+			data: Record<string, any>;
+		}[]
+	},
+	tl: {
+		where: 'deviceAccount',
+		default: 'home'
+	},
+}));
diff --git a/packages/client/src/ui/chat/sub-note-content.vue b/packages/client/src/ui/chat/sub-note-content.vue
new file mode 100644
index 0000000000..9c169ea546
--- /dev/null
+++ b/packages/client/src/ui/chat/sub-note-content.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="wrmlmaau">
+	<div class="body">
+		<span v-if="note.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
+		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $ts.deleted }})</span>
+		<MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
+		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
+		<MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
+	</div>
+	<details v-if="note.files.length > 0">
+		<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
+		<XMediaList :media-list="note.files"/>
+	</details>
+	<details v-if="note.poll">
+		<summary>{{ $ts.poll }}</summary>
+		<XPoll :note="note"/>
+	</details>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XPoll from '@/components/poll.vue';
+import XMediaList from '@/components/media-list.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XPoll,
+		XMediaList,
+	},
+	props: {
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+		};
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wrmlmaau {
+	overflow-wrap: break-word;
+
+	> .body {
+		> .reply {
+			margin-right: 6px;
+			color: var(--accent);
+		}
+
+		> .rp {
+			margin-left: 4px;
+			font-style: oblique;
+			color: var(--renote);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/chat/widgets.vue b/packages/client/src/ui/chat/widgets.vue
new file mode 100644
index 0000000000..6b12f9dac9
--- /dev/null
+++ b/packages/client/src/ui/chat/widgets.vue
@@ -0,0 +1,62 @@
+<template>
+<div class="qydbhufi">
+	<XWidgets :edit="edit" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
+
+	<button v-if="edit" @click="edit = false" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgetsExit }}</button>
+	<button v-else @click="edit = true" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgets }}</button>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import XWidgets from '@/components/widgets.vue';
+import { store } from './store';
+
+export default defineComponent({
+	components: {
+		XWidgets,
+	},
+
+	data() {
+		return {
+			edit: false,
+			widgets: store.reactiveState.widgets
+		};
+	},
+
+	methods: {
+		addWidget(widget) {
+			store.set('widgets', [widget, ...store.state.widgets]);
+		},
+
+		removeWidget(widget) {
+			store.set('widgets', store.state.widgets.filter(w => w.id != widget.id));
+		},
+
+		updateWidget({ id, data }) {
+			// TODO: throttleしたい
+			store.set('widgets', store.state.widgets.map(w => w.id === id ? {
+				...w,
+				data: data
+			} : w));
+		},
+
+		updateWidgets(widgets) {
+			store.set('widgets', widgets);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qydbhufi {
+	height: 100%;
+	box-sizing: border-box;
+	overflow: auto;
+	padding: var(--margin);
+
+	::v-deep(._panel) {
+		box-shadow: none;
+	}
+}
+</style>
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
new file mode 100644
index 0000000000..c2e3bbd69b
--- /dev/null
+++ b/packages/client/src/ui/classic.header.vue
@@ -0,0 +1,210 @@
+<template>
+<div class="azykntjl">
+	<div class="body">
+		<div class="left">
+			<MkA class="item index" active-class="active" to="/" exact v-click-anime v-tooltip="$ts.timeline">
+				<i class="fas fa-home fa-fw"></i>
+			</MkA>
+			<template v-for="item in menu">
+				<div v-if="item === '-'" class="divider"></div>
+				<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime v-tooltip="$ts[menuDef[item].title]">
+					<i class="fa-fw" :class="menuDef[item].icon"></i>
+					<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+				</component>
+			</template>
+			<div class="divider"></div>
+			<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.controlPanel">
+				<i class="fas fa-door-open fa-fw"></i>
+			</MkA>
+			<button class="item _button" @click="more" v-click-anime>
+				<i class="fas fa-ellipsis-h fa-fw"></i>
+				<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
+			</button>
+		</div>
+		<div class="right">
+			<MkA class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.settings">
+				<i class="fas fa-cog fa-fw"></i>
+			</MkA>
+			<button class="item _button account" @click="openAccountMenu" v-click-anime>
+				<MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/>
+			</button>
+			<div class="post" @click="post">
+				<MkButton class="button" gradate full rounded>
+					<i class="fas fa-pencil-alt fa-fw"></i>
+				</MkButton>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { host } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import { openAccountMenu } from '@/account';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+	components: {
+		MkButton,
+	},
+
+	data() {
+		return {
+			host: host,
+			accounts: [],
+			connection: null,
+			menuDef: menuDef,
+			settingsWindowed: false,
+		};
+	},
+
+	computed: {
+		menu(): string[] {
+			return this.$store.state.menu;
+		},
+
+		otherNavItemIndicated(): boolean {
+			for (const def in this.menuDef) {
+				if (this.menu.includes(def)) continue;
+				if (this.menuDef[def].indicated) return true;
+			}
+			return false;
+		},
+	},
+
+	watch: {
+		'$store.reactiveState.menuDisplay.value'() {
+			this.calcViewState();
+		},
+	},
+
+	created() {
+		window.addEventListener('resize', this.calcViewState);
+		this.calcViewState();
+	},
+
+	methods: {
+		calcViewState() {
+			this.settingsWindowed = (window.innerWidth > 1400);
+		},
+
+		post() {
+			os.post();
+		},
+
+		search() {
+			search();
+		},
+
+		more(ev) {
+			os.popup(import('@/components/launch-pad.vue'), {}, {
+			}, 'closed');
+		},
+
+		openAccountMenu,
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.azykntjl {
+	$height: 60px;
+	$avatar-size: 32px;
+	$avatar-margin: 8px;
+
+	position: sticky;
+	top: 0;
+	z-index: 1000;
+	width: 100%;
+	height: $height;
+	background-color: var(--bg);
+
+	> .body {
+		max-width: 1380px;
+		margin: 0 auto;
+		display: flex;
+
+		> .right,
+		> .left {
+
+			> .item {
+				position: relative;
+				font-size: 0.9em;
+				display: inline-block;
+				padding: 0 12px;
+				line-height: $height;
+
+				> i,
+				> .avatar {
+					margin-right: 0;
+				}
+
+				> i {
+					left: 10px;
+				}
+
+				> .avatar {
+					width: $avatar-size;
+					height: $avatar-size;
+					vertical-align: middle;
+				}
+
+				> .indicator {
+					position: absolute;
+					top: 0;
+					left: 0;
+					color: var(--navIndicator);
+					font-size: 8px;
+					animation: blink 1s infinite;
+				}
+
+				&:hover {
+					text-decoration: none;
+					color: var(--navHoverFg);
+				}
+
+				&.active {
+					color: var(--navActive);
+				}
+			}
+
+			> .divider {
+				display: inline-block;
+				height: 16px;
+				margin: 0 10px;
+				border-right: solid 0.5px var(--divider);
+			}
+
+			> .post {
+				display: inline-block;
+			
+				> .button {
+					width: 40px;
+					height: 40px;
+					padding: 0;
+					min-width: 0;
+				}
+			}
+
+			> .account {
+				display: inline-flex;
+				align-items: center;
+				vertical-align: top;
+				margin-right: 8px;
+
+				> .acct {
+					margin-left: 8px;
+				}
+			}
+		}
+
+		> .right {
+			margin-left: auto;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/classic.side.vue b/packages/client/src/ui/classic.side.vue
new file mode 100644
index 0000000000..38087cebb8
--- /dev/null
+++ b/packages/client/src/ui/classic.side.vue
@@ -0,0 +1,158 @@
+<template>
+<div class="qvzfzxam _narrow_" v-if="component">
+	<div class="container">
+		<header class="header" @contextmenu.prevent.stop="onContextmenu">
+			<button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
+			<button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
+			<span class="title">{{ pageInfo.title }}</span>
+			<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
+		</header>
+		<MkHeader class="pageHeader" :info="pageInfo"/>
+		<component :is="component" v-bind="props" :ref="changePage"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { resolve } from '@/router';
+import { url } from '@/config';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	provide() {
+		return {
+			navHook: (path) => {
+				this.navigate(path);
+			}
+		};
+	},
+
+	data() {
+		return {
+			path: null,
+			component: null,
+			props: {},
+			pageInfo: null,
+			history: [],
+		};
+	},
+
+	computed: {
+		url(): string {
+			return url + this.path;
+		}
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+			}
+		},
+
+		navigate(path, record = true) {
+			if (record && this.path) this.history.push(this.path);
+			this.path = path;
+			const { component, props } = resolve(path);
+			this.component = component;
+			this.props = props;
+		},
+
+		back() {
+			this.navigate(this.history.pop(), false);
+		},
+
+		close() {
+			this.path = null;
+			this.component = null;
+			this.props = {};
+		},
+
+		onContextmenu(e) {
+			os.contextMenu([{
+				type: 'label',
+				text: this.path,
+			}, {
+				icon: 'fas fa-expand-alt',
+				text: this.$ts.showInPage,
+				action: () => {
+					this.$router.push(this.path);
+					this.close();
+				}
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(this.path);
+					this.close();
+				}
+			}, null, {
+				icon: 'fas fa-external-link-alt',
+				text: this.$ts.openInNewTab,
+				action: () => {
+					window.open(this.url, '_blank');
+					this.close();
+				}
+			}, {
+				icon: 'fas fa-link',
+				text: this.$ts.copyLink,
+				action: () => {
+					copyToClipboard(this.url);
+				}
+			}], e);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.qvzfzxam {
+	$header-height: 58px; // TODO: どこかに集約したい
+
+	--root-margin: 16px;
+	--margin: var(--marginHalf);
+
+	> .container {
+		position: fixed;
+		width: 370px;
+		height: 100vh;
+		overflow: auto;
+		box-sizing: border-box;
+
+		> .header {
+			display: flex;
+			position: sticky;
+			z-index: 1000;
+			top: 0;
+			height: $header-height;
+			width: 100%;
+			line-height: $header-height;
+			text-align: center;
+			font-weight: bold;
+			//background-color: var(--panel);
+			-webkit-backdrop-filter: var(--blur, blur(32px));
+			backdrop-filter: var(--blur, blur(32px));
+			background-color: var(--header);
+
+			> ._button {
+				height: $header-height;
+				width: $header-height;
+
+				&:hover {
+					color: var(--fgHighlighted);
+				}
+			}
+
+			> .title {
+				flex: 1;
+				position: relative;
+			}
+		}
+	}
+}
+</style>
+
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
new file mode 100644
index 0000000000..5e4b6ae28f
--- /dev/null
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -0,0 +1,263 @@
+<template>
+<div class="npcljfve" :class="{ iconOnly }">
+	<button class="item _button account" @click="openAccountMenu" v-click-anime>
+		<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
+	</button>
+	<div class="post" @click="post" data-cy-open-post-form>
+		<MkButton class="button" gradate full rounded>
+			<i class="fas fa-pencil-alt fa-fw"></i><span class="text" v-if="!iconOnly">{{ $ts.note }}</span>
+		</MkButton>
+	</div>
+	<div class="divider"></div>
+	<MkA class="item index" active-class="active" to="/" exact v-click-anime>
+		<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
+	</MkA>
+	<template v-for="item in menu">
+		<div v-if="item === '-'" class="divider"></div>
+		<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime>
+			<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
+			<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
+		</component>
+	</template>
+	<div class="divider"></div>
+	<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
+		<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
+	</MkA>
+	<button class="item _button" @click="more" v-click-anime>
+		<i class="fas fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
+		<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
+	</button>
+	<MkA class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
+		<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
+	</MkA>
+	<div class="divider"></div>
+	<div class="about">
+		<MkA class="link" to="/about" v-click-anime>
+			<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" class="_ghost"/>
+		</MkA>
+	</div>
+	<!--<MisskeyLogo class="misskey"/>-->
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { host } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import { openAccountMenu } from '@/account';
+import MkButton from '@/components/ui/button.vue';
+import { StickySidebar } from '@/scripts/sticky-sidebar';
+//import MisskeyLogo from '@assets/client/misskey.svg';
+
+export default defineComponent({
+	components: {
+		MkButton,
+		//MisskeyLogo,
+	},
+
+	data() {
+		return {
+			host: host,
+			accounts: [],
+			connection: null,
+			menuDef: menuDef,
+			iconOnly: false,
+			settingsWindowed: false,
+		};
+	},
+
+	computed: {
+		menu(): string[] {
+			return this.$store.state.menu;
+		},
+
+		otherNavItemIndicated(): boolean {
+			for (const def in this.menuDef) {
+				if (this.menu.includes(def)) continue;
+				if (this.menuDef[def].indicated) return true;
+			}
+			return false;
+		},
+	},
+
+	watch: {
+		'$store.reactiveState.menuDisplay.value'() {
+			this.calcViewState();
+		},
+
+		iconOnly() {
+			this.$nextTick(() => {
+				this.$emit('change-view-mode');
+			});
+		},
+	},
+
+	created() {
+		window.addEventListener('resize', this.calcViewState);
+		this.calcViewState();
+	},
+
+	mounted() {
+		const sticky = new StickySidebar(this.$el.parentElement, 16);
+		window.addEventListener('scroll', () => {
+			sticky.calc(window.scrollY);
+		}, { passive: true });
+	},
+
+	methods: {
+		calcViewState() {
+			this.iconOnly = (window.innerWidth <= 1400) || (this.$store.state.menuDisplay === 'sideIcon');
+			this.settingsWindowed = (window.innerWidth > 1400);
+		},
+
+		post() {
+			os.post();
+		},
+
+		search() {
+			search();
+		},
+
+		more(ev) {
+			os.popup(import('@/components/launch-pad.vue'), {}, {
+			}, 'closed');
+		},
+
+		openAccountMenu,
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.npcljfve {
+	$ui-font-size: 1em; // TODO: どこかに集約したい
+	$nav-icon-only-width: 78px; // TODO: どこかに集約したい
+	$avatar-size: 32px;
+	$avatar-margin: 8px;
+
+	padding: 0 16px;
+	box-sizing: border-box;
+	width: 260px;
+
+	&.iconOnly {
+		flex: 0 0 $nav-icon-only-width;
+		width: $nav-icon-only-width !important;
+
+		> .divider {
+			margin: 8px auto;
+			width: calc(100% - 32px);
+		}
+
+		> .post {
+			> .button {
+				width: 46px;
+				height: 46px;
+				padding: 0;
+			}
+		}
+
+		> .item {
+			padding-left: 0;
+			width: 100%;
+			text-align: center;
+			font-size: $ui-font-size * 1.1;
+			line-height: 3.7rem;
+
+			> i,
+			> .avatar {
+				margin-right: 0;
+			}
+
+			> i {
+				left: 10px;
+			}
+
+			> .text {
+				display: none;
+			}
+		}
+	}
+
+	> .divider {
+		margin: 10px 0;
+		border-top: solid 0.5px var(--divider);
+	}
+
+	> .post {
+		position: sticky;
+		top: 0;
+		z-index: 1;
+		padding: 16px 0;
+		background: var(--bg);
+
+		> .button {
+			min-width: 0;
+		}
+	}
+
+	> .about {
+		fill: currentColor;
+		padding: 8px 0 16px 0;
+		text-align: center;
+
+		> .link {
+			display: block;
+			width: 32px;
+			margin: 0 auto;
+
+			img {
+				display: block;
+				width: 100%;
+			}
+		}
+	}
+
+	> .item {
+		position: relative;
+		display: block;
+		font-size: $ui-font-size;
+		line-height: 2.6rem;
+		text-overflow: ellipsis;
+		overflow: hidden;
+		white-space: nowrap;
+		width: 100%;
+		text-align: left;
+		box-sizing: border-box;
+
+		> i {
+			width: 32px;
+		}
+
+		> i,
+		> .avatar {
+			margin-right: $avatar-margin;
+		}
+
+		> .avatar {
+			width: $avatar-size;
+			height: $avatar-size;
+			vertical-align: middle;
+		}
+
+		> .indicator {
+			position: absolute;
+			top: 0;
+			left: 0;
+			color: var(--navIndicator);
+			font-size: 8px;
+			animation: blink 1s infinite;
+		}
+
+		&:hover {
+			text-decoration: none;
+			color: var(--navHoverFg);
+		}
+
+		&.active {
+			color: var(--navActive);
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
new file mode 100644
index 0000000000..5d7c79a0e2
--- /dev/null
+++ b/packages/client/src/ui/classic.vue
@@ -0,0 +1,471 @@
+<template>
+<div class="mk-app" :class="{ wallpaper, isMobile }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
+	<XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/>
+
+	<div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }">
+		<template v-if="!isMobile">
+			<div class="sidebar" v-if="!showMenuOnTop">
+				<XSidebar/>
+			</div>
+			<div class="widgets left" ref="widgetsLeft" v-else>
+				<XWidgets @mounted="attachSticky('widgetsLeft')" :place="'left'"/>
+			</div>
+		</template>
+
+		<main class="main" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }">
+			<div class="content">
+				<MkStickyContainer>
+					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
+					<router-view v-slot="{ Component }">
+						<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+							<keep-alive :include="['timeline']">
+								<component :is="Component" :ref="changePage"/>
+							</keep-alive>
+						</transition>
+					</router-view>
+				</MkStickyContainer>
+			</div>
+		</main>
+
+		<div v-if="isDesktop" class="widgets right" ref="widgetsRight">
+			<XWidgets @mounted="attachSticky('widgetsRight')" :place="null"/>
+		</div>
+	</div>
+
+	<div class="buttons" v-if="isMobile">
+		<button class="button nav _button" @click="showDrawerNav" ref="navButton"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
+		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
+		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
+		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
+		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
+	</div>
+
+	<XDrawerSidebar ref="drawerNav" class="sidebar" v-if="isMobile"/>
+
+	<transition name="tray-back">
+		<div class="tray-back _modalBg"
+			v-if="widgetsShowing"
+			@click="widgetsShowing = false"
+			@touchstart.passive="widgetsShowing = false"
+		></div>
+	</transition>
+
+	<transition name="tray">
+		<XWidgets v-if="widgetsShowing" class="tray"/>
+	</transition>
+
+	<iframe v-if="$store.state.aiChanMode" class="ivnzpscs" ref="live2d" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe>
+
+	<XCommon/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
+import { instanceName } from '@/config';
+import { StickySidebar } from '@/scripts/sticky-sidebar';
+import XSidebar from './classic.sidebar.vue';
+import XDrawerSidebar from '@/ui/_common_/sidebar.vue';
+import XCommon from './_common_/common.vue';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import * as symbols from '@/symbols';
+
+const DESKTOP_THRESHOLD = 1100;
+const MOBILE_THRESHOLD = 600;
+
+export default defineComponent({
+	components: {
+		XCommon,
+		XSidebar,
+		XDrawerSidebar,
+		XHeaderMenu: defineAsyncComponent(() => import('./classic.header.vue')),
+		XWidgets: defineAsyncComponent(() => import('./classic.widgets.vue')),
+	},
+
+	provide() {
+		return {
+			shouldHeaderThin: this.showMenuOnTop,
+		};
+	},
+
+	data() {
+		return {
+			pageInfo: null,
+			menuDef: menuDef,
+			globalHeaderHeight: 0,
+			isMobile: window.innerWidth <= MOBILE_THRESHOLD,
+			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
+			widgetsShowing: false,
+			fullView: false,
+			wallpaper: localStorage.getItem('wallpaper') != null,
+		};
+	},
+
+	computed: {
+		navIndicated(): boolean {
+			for (const def in this.menuDef) {
+				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
+				if (this.menuDef[def].indicated) return true;
+			}
+			return false;
+		},
+
+		showMenuOnTop(): boolean {
+			return !this.isMobile && this.$store.state.menuDisplay === 'top';
+		}
+	},
+
+	created() {
+		document.documentElement.style.overflowY = 'scroll';
+
+		if (this.$store.state.widgets.length === 0) {
+			this.$store.set('widgets', [{
+				name: 'calendar',
+				id: 'a', place: null, data: {}
+			}, {
+				name: 'notifications',
+				id: 'b', place: null, data: {}
+			}, {
+				name: 'trends',
+				id: 'c', place: null, data: {}
+			}]);
+		}
+	},
+
+	mounted() {
+		window.addEventListener('resize', () => {
+			this.isMobile = (window.innerWidth <= MOBILE_THRESHOLD);
+			this.isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD);
+		}, { passive: true });
+
+		if (this.$store.state.aiChanMode) {
+			const iframeRect = this.$refs.live2d.getBoundingClientRect();
+			window.addEventListener('mousemove', ev => {
+				this.$refs.live2d.contentWindow.postMessage({
+					type: 'moveCursor',
+					body: {
+						x: ev.clientX - iframeRect.left,
+						y: ev.clientY - iframeRect.top,
+					}
+				}, '*');
+			}, { passive: true });
+			window.addEventListener('touchmove', ev => {
+				this.$refs.live2d.contentWindow.postMessage({
+					type: 'moveCursor',
+					body: {
+						x: ev.touches[0].clientX - iframeRect.left,
+						y: ev.touches[0].clientY - iframeRect.top,
+					}
+				}, '*');
+			}, { passive: true });
+		}
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+				document.title = `${this.pageInfo.title} | ${instanceName}`;
+			}
+		},
+
+		attachSticky(ref) {
+			const sticky = new StickySidebar(this.$refs[ref], this.$store.state.menuDisplay === 'top' ? 0 : 16, this.$store.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
+			window.addEventListener('scroll', () => {
+				sticky.calc(window.scrollY);
+			}, { passive: true });
+		},
+
+		post() {
+			os.post();
+		},
+
+		top() {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		},
+
+		back() {
+			history.back();
+		},
+
+		showDrawerNav() {
+			this.$refs.drawerNav.show();
+		},
+
+		onTransition() {
+			if (window._scroll) window._scroll();
+		},
+
+		onContextmenu(e) {
+			const isLink = (el: HTMLElement) => {
+				if (el.tagName === 'A') return true;
+				if (el.parentElement) {
+					return isLink(el.parentElement);
+				}
+			};
+			if (isLink(e.target)) return;
+			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+			if (window.getSelection().toString() !== '') return;
+			const path = this.$route.path;
+			os.contextMenu([{
+				type: 'label',
+				text: path,
+			}, {
+				icon: this.fullView ? 'fas fa-compress' : 'fas fa-expand',
+				text: this.fullView ? this.$ts.quitFullView : this.$ts.fullView,
+				action: () => {
+					this.fullView = !this.fullView;
+				}
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(path);
+				}
+			}], e);
+		},
+
+		onAiClick(ev) {
+			//if (this.live2d) this.live2d.click(ev);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.tray-enter-active,
+.tray-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.tray-enter-from,
+.tray-leave-active {
+	opacity: 0;
+	transform: translateX(240px);
+}
+
+.tray-back-enter-active,
+.tray-back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.tray-back-enter-from,
+.tray-back-leave-active {
+	opacity: 0;
+}
+
+.mk-app {
+	$ui-font-size: 1em;
+	$widgets-hide-threshold: 1200px;
+	$nav-icon-only-width: 78px; // TODO: どこかに集約したい
+
+	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+	min-height: calc(var(--vh, 1vh) * 100);
+	box-sizing: border-box;
+
+	&.wallpaper {
+		background: var(--wallpaperOverlay);
+		//backdrop-filter: var(--blur, blur(4px));
+	}
+
+	&.isMobile {
+		> .columns {
+			display: block;
+			margin: 0;
+
+			> .main {
+				margin: 0;
+				padding-bottom: 92px;
+				border: none;
+				width: 100%;
+				border-radius: 0;
+			}
+		}
+	}
+
+	> .columns {
+		display: flex;
+		justify-content: center;
+		max-width: 100%;
+		//margin: 32px 0;
+
+		&.fullView {
+			margin: 0;
+		
+			> .sidebar {
+				display: none;
+			}
+
+			> .widgets {
+				display: none;
+			}
+
+			> .main {
+				margin: 0;
+				border-radius: 0;
+				box-shadow: none;
+				width: 100%;
+			}
+		}
+
+		> .main {
+			min-width: 0;
+			width: 750px;
+			margin: 0 16px 0 0;
+			background: var(--panel);
+			border-left: solid 1px var(--divider);
+			border-right: solid 1px var(--divider);
+			border-radius: 0;
+			overflow: clip;
+			--margin: 12px;
+		}
+
+		> .widgets {
+			//--panelBorder: none;
+			width: 300px;
+			margin-top: 16px;
+
+			@media (max-width: $widgets-hide-threshold) {
+				display: none;
+			}
+
+			&.left {
+				margin-right: 16px;
+			}
+		}
+
+		> .sidebar {
+			margin-top: 16px;
+		}
+
+		&.withGlobalHeader {
+			> .main {
+				margin-top: 0;
+				border: solid 1px var(--divider);
+				border-radius: var(--radius);
+				--stickyTop: var(--globalHeaderHeight);
+			}
+
+			> .widgets {
+				--stickyTop: var(--globalHeaderHeight);
+				margin-top: 0;
+			}
+		}
+
+		@media (max-width: 850px) {
+			margin: 0;
+
+			> .sidebar {
+				border-right: solid 0.5px var(--divider);
+			}
+
+			> .main {
+				margin: 0;
+				border-radius: 0;
+				box-shadow: none;
+				width: 100%;
+			}
+		}
+	}
+
+	> .buttons {
+		position: fixed;
+		z-index: 1000;
+		bottom: 0;
+		padding: 16px;
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		-webkit-backdrop-filter: var(--blur, blur(32px));
+		backdrop-filter: var(--blur, blur(32px));
+		background-color: var(--header);
+		border-top: solid 0.5px var(--divider);
+
+		> .button {
+			position: relative;
+			flex: 1;
+			padding: 0;
+			margin: auto;
+			height: 64px;
+			border-radius: 8px;
+			background: var(--panel);
+			color: var(--fg);
+
+			&:not(:last-child) {
+				margin-right: 12px;
+			}
+
+			@media (max-width: 400px) {
+				height: 60px;
+
+				&:not(:last-child) {
+					margin-right: 8px;
+				}
+			}
+
+			&:hover {
+				background: var(--X2);
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 0;
+				left: 0;
+				color: var(--indicator);
+				font-size: 16px;
+				animation: blink 1s infinite;
+			}
+
+			&:first-child {
+				margin-left: 0;
+			}
+
+			&:last-child {
+				margin-right: 0;
+			}
+
+			> * {
+				font-size: 22px;
+			}
+
+			&:disabled {
+				cursor: default;
+
+				> * {
+					opacity: 0.5;
+				}
+			}
+		}
+	}
+
+	> .tray-back {
+		z-index: 1001;
+	}
+
+	> .tray {
+		position: fixed;
+		top: 0;
+		right: 0;
+		z-index: 1001;
+		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+		height: calc(var(--vh, 1vh) * 100);
+		padding: var(--margin);
+		box-sizing: border-box;
+		overflow: auto;
+		background: var(--bg);
+	}
+
+	> .ivnzpscs {
+		position: fixed;
+		bottom: 0;
+		right: 0;
+		width: 300px;
+		height: 600px;
+		border: none;
+		pointer-events: none;
+	}
+}
+</style>
diff --git a/packages/client/src/ui/classic.widgets.vue b/packages/client/src/ui/classic.widgets.vue
new file mode 100644
index 0000000000..562c2eeb2c
--- /dev/null
+++ b/packages/client/src/ui/classic.widgets.vue
@@ -0,0 +1,84 @@
+<template>
+<div class="ddiqwdnk">
+	<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value.filter(w => w.place === place)" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
+	<MkAd class="a" :prefer="['square']"/>
+
+	<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
+	<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import XWidgets from '@/components/widgets.vue';
+
+export default defineComponent({
+	components: {
+		XWidgets
+	},
+
+	props: {
+		place: {
+			type: String,
+		}
+	},
+
+	emits: ['mounted'],
+
+	data() {
+		return {
+			editMode: false,
+		};
+	},
+
+	mounted() {
+		this.$emit('mounted', this.$el);
+	},
+
+	methods: {
+		addWidget(widget) {
+			this.$store.set('widgets', [{
+				...widget,
+				place: this.place,
+			}, ...this.$store.state.widgets]);
+		},
+
+		removeWidget(widget) {
+			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
+		},
+
+		updateWidget({ id, data }) {
+			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? {
+				...w,
+				data: data
+			} : w));
+		},
+
+		updateWidgets(widgets) {
+			this.$store.set('widgets', [
+				...this.$store.state.widgets.filter(w => w.place !== this.place),
+				...widgets
+			]);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ddiqwdnk {
+	position: sticky;
+	height: min-content;
+	box-sizing: border-box;
+	padding-bottom: 8px;
+
+	> .widgets,
+	> .a {
+		width: 300px;
+	}
+
+	> .edit {
+		display: block;
+		margin: 16px auto;
+	}
+}
+</style>
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
new file mode 100644
index 0000000000..cc8bf5a511
--- /dev/null
+++ b/packages/client/src/ui/deck.vue
@@ -0,0 +1,229 @@
+<template>
+<div class="mk-deck" :class="`${deckStore.reactiveState.columnAlign.value}`" @contextmenu.self.prevent="onContextmenu"
+	:style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
+>
+	<XSidebar ref="nav"/>
+
+	<template v-for="ids in layout">
+		<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
+		<section v-if="ids.length > 1"
+			class="folder column"
+			:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
+		>
+			<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
+		</section>
+		<DeckColumnCore v-else
+			class="column"
+			:ref="ids[0]"
+			:key="ids[0]"
+			:column="columns.find(c => c.id === ids[0])"
+			@parent-focus="moveFocus(ids[0], $event)"
+			:style="columns.find(c => c.id === ids[0]).flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0]).width + 'px' }"
+		/>
+	</template>
+
+	<button v-if="$i" class="nav _button" @click="showNav()"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
+	<button v-if="$i" class="post _buttonPrimary" @click="post()"><i class="fas fa-pencil-alt"></i></button>
+
+	<XCommon/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+import { host } from '@/config';
+import DeckColumnCore from '@/ui/deck/column-core.vue';
+import XSidebar from '@/ui/_common_/sidebar.vue';
+import { getScrollContainer } from '@/scripts/scroll';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import XCommon from './_common_/common.vue';
+import { deckStore, addColumn, loadDeck } from './deck/deck-store';
+
+export default defineComponent({
+	components: {
+		XCommon,
+		XSidebar,
+		DeckColumnCore,
+	},
+
+	provide() {
+		return deckStore.state.navWindow ? {
+			navHook: (url) => {
+				os.pageWindow(url);
+			}
+		} : {};
+	},
+
+	data() {
+		return {
+			deckStore,
+			host: host,
+			menuDef: menuDef,
+			wallpaper: localStorage.getItem('wallpaper') != null,
+		};
+	},
+
+	computed: {
+		columns() {
+			return deckStore.reactiveState.columns.value;
+		},
+		layout() {
+			return deckStore.reactiveState.layout.value;
+		},
+		navIndicated(): boolean {
+			if (!this.$i) return false;
+			for (const def in this.menuDef) {
+				if (this.menuDef[def].indicated) return true;
+			}
+			return false;
+		},
+	},
+
+	created() {
+		document.documentElement.style.overflowY = 'hidden';
+		document.documentElement.style.scrollBehavior = 'auto';
+		window.addEventListener('wheel', this.onWheel);
+		loadDeck();
+	},
+
+	mounted() {
+	},
+
+	methods: {
+		onWheel(e) {
+			if (getScrollContainer(e.target) == null) {
+				document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
+			}
+		},
+
+		showNav() {
+			this.$refs.nav.show();
+		},
+
+		post() {
+			os.post();
+		},
+
+		async addColumn(ev) {
+			const columns = [
+				'main',
+				'widgets',
+				'notifications',
+				'tl',
+				'antenna',
+				'list',
+				'mentions',
+				'direct',
+			];
+
+			const { canceled, result: column } = await os.dialog({
+				title: this.$ts._deck.addColumn,
+				type: null,
+				select: {
+					items: columns.map(column => ({
+						value: column, text: this.$t('_deck._columns.' + column)
+					}))
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+
+			addColumn({
+				type: column,
+				id: uuid(),
+				name: this.$t('_deck._columns.' + column),
+				width: 330,
+			});
+		},
+
+		onContextmenu(e) {
+			os.contextMenu([{
+				text: this.$ts._deck.addColumn,
+				icon: null,
+				action: this.addColumn
+			}], e);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-deck {
+	$nav-hide-threshold: 650px; // TODO: どこかに集約したい
+
+	// TODO: ここではなくて、各カラムで自身の幅に応じて上書きするようにしたい
+	--margin: var(--marginHalf);
+
+	display: flex;
+	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+	height: calc(var(--vh, 1vh) * 100);
+	box-sizing: border-box;
+	flex: 1;
+	padding: var(--deckMargin);
+
+	&.center {
+		> .column:first-of-type {
+			margin-left: auto;
+		}
+
+		> .column:last-of-type {
+			margin-right: auto;
+		}
+	}
+
+	> .column {
+		flex-shrink: 0;
+		margin-right: var(--deckMargin);
+
+		&.folder {
+			display: flex;
+			flex-direction: column;
+
+			> *:not(:last-child) {
+				margin-bottom: var(--deckMargin);
+			}
+		}
+	}
+
+	> .post,
+	> .nav {
+		position: fixed;
+		z-index: 1000;
+		bottom: 32px;
+		width: 64px;
+		height: 64px;
+		border-radius: 100%;
+		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
+		font-size: 22px;
+
+		@media (min-width: ($nav-hide-threshold + 1px)) {
+			display: none;
+		}
+	}
+
+	> .post {
+		right: 32px;
+	}
+
+	> .nav {
+		left: 32px;
+		background: var(--panel);
+		color: var(--fg);
+
+		&:hover {
+			background: var(--X2);
+		}
+
+		> .indicator {
+			position: absolute;
+			top: 0;
+			left: 0;
+			color: var(--indicator);
+			font-size: 16px;
+			animation: blink 1s infinite;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/deck/antenna-column.vue b/packages/client/src/ui/deck/antenna-column.vue
new file mode 100644
index 0000000000..d42b8a5a10
--- /dev/null
+++ b/packages/client/src/ui/deck/antenna-column.vue
@@ -0,0 +1,80 @@
+<template>
+<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked">
+	<template #header>
+		<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+	</template>
+
+	<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
+</XColumn>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XColumn from './column.vue';
+import XTimeline from '@/components/timeline.vue';
+import * as os from '@/os';
+import { updateColumn } from './deck-store';
+
+export default defineComponent({
+	components: {
+		XColumn,
+		XTimeline,
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true
+		},
+		isStacked: {
+			type: Boolean,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	watch: {
+		mediaOnly() {
+			(this.$refs.timeline as any).reload();
+		}
+	},
+
+	mounted() {
+		if (this.column.antennaId == null) {
+			this.setAntenna();
+		}
+	},
+
+	methods: {
+		async setAntenna() {
+			const antennas = await os.api('antennas/list');
+			const { canceled, result: antenna } = await os.dialog({
+				title: this.$ts.selectAntenna,
+				type: null,
+				select: {
+					items: antennas.map(x => ({
+						value: x, text: x.name
+					})),
+				default: this.column.antennaId
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			updateColumn(this.column.id, {
+				antennaId: antenna.id
+			});
+		},
+
+		focus() {
+			(this.$refs.timeline as any).focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/packages/client/src/ui/deck/column-core.vue b/packages/client/src/ui/deck/column-core.vue
new file mode 100644
index 0000000000..5393bac736
--- /dev/null
+++ b/packages/client/src/ui/deck/column-core.vue
@@ -0,0 +1,52 @@
+<template>
+<!-- TODO: リファクタの余地がありそう -->
+<XMainColumn v-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XMainColumn from './main-column.vue';
+import XTlColumn from './tl-column.vue';
+import XAntennaColumn from './antenna-column.vue';
+import XListColumn from './list-column.vue';
+import XNotificationsColumn from './notifications-column.vue';
+import XWidgetsColumn from './widgets-column.vue';
+import XMentionsColumn from './mentions-column.vue';
+import XDirectColumn from './direct-column.vue';
+
+export default defineComponent({
+	components: {
+		XMainColumn,
+		XTlColumn,
+		XAntennaColumn,
+		XListColumn,
+		XNotificationsColumn,
+		XWidgetsColumn,
+		XMentionsColumn,
+		XDirectColumn
+	},
+	props: {
+		column: {
+			type: Object,
+			required: true
+		},
+		isStacked: {
+			type: Boolean,
+			required: false,
+			default: false
+		}
+	},
+	methods: {
+		focus() {
+			this.$children[0].focus();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue
new file mode 100644
index 0000000000..fe112e3039
--- /dev/null
+++ b/packages/client/src/ui/deck/column.vue
@@ -0,0 +1,408 @@
+<template>
+<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
+<section class="dnpfarvg _panel _narrow_" :class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
+	@dragover.prevent.stop="onDragover"
+	@dragleave="onDragleave"
+	@drop.prevent.stop="onDrop"
+	v-hotkey="keymap"
+	:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
+>
+	<header :class="{ indicated }"
+		draggable="true"
+		@click="goTop"
+		@dragstart="onDragstart"
+		@dragend="onDragend"
+		@contextmenu.prevent.stop="onContextmenu"
+	>
+		<button class="toggleActive _button" @click="toggleActive" v-if="isStacked && !isMainColumn">
+			<template v-if="active"><i class="fas fa-angle-up"></i></template>
+			<template v-else><i class="fas fa-angle-down"></i></template>
+		</button>
+		<div class="action">
+			<slot name="action"></slot>
+		</div>
+		<span class="header"><slot name="header"></slot></span>
+		<button v-if="func" class="menu _button" v-tooltip="func.title" @click.stop="func.handler"><i :class="func.icon || 'fas fa-cog'"></i></button>
+	</header>
+	<div ref="body" v-show="active">
+		<slot></slot>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from './deck-store';
+import { deckStore } from './deck-store';
+
+export default defineComponent({
+	provide: {
+		shouldHeaderThin: true,
+		shouldOmitHeaderTitle: true,
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: false,
+			default: null
+		},
+		isStacked: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		func: {
+			type: Object,
+			required: false,
+			default: null
+		},
+		naked: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		indicated: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+
+	data() {
+		return {
+			deckStore,
+			dragging: false,
+			draghover: false,
+			dropready: false,
+		};
+	},
+
+	computed: {
+		isMainColumn(): boolean {
+			return this.column.type === 'main';
+		},
+
+		active(): boolean {
+			return this.column.active !== false;
+		},
+
+		keymap(): any {
+			return {
+				'shift+up': () => this.$parent.$emit('parent-focus', 'up'),
+				'shift+down': () => this.$parent.$emit('parent-focus', 'down'),
+				'shift+left': () => this.$parent.$emit('parent-focus', 'left'),
+				'shift+right': () => this.$parent.$emit('parent-focus', 'right'),
+			};
+		}
+	},
+
+	watch: {
+		active(v) {
+			this.$emit('change-active-state', v);
+		},
+
+		dragging(v) {
+			os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd');
+		}
+	},
+
+	mounted() {
+		os.deckGlobalEvents.on('column.dragStart', this.onOtherDragStart);
+		os.deckGlobalEvents.on('column.dragEnd', this.onOtherDragEnd);
+	},
+
+	beforeUnmount() {
+		os.deckGlobalEvents.off('column.dragStart', this.onOtherDragStart);
+		os.deckGlobalEvents.off('column.dragEnd', this.onOtherDragEnd);
+	},
+
+	methods: {
+		onOtherDragStart() {
+			this.dropready = true;
+		},
+
+		onOtherDragEnd() {
+			this.dropready = false;
+		},
+
+		toggleActive() {
+			if (!this.isStacked) return;
+			updateColumn(this.column.id, {
+				active: !this.column.active
+			});
+		},
+
+		getMenu() {
+			const items = [{
+				icon: 'fas fa-pencil-alt',
+				text: this.$ts.edit,
+				action: async () => {
+					const { canceled, result } = await os.form(this.column.name, {
+						name: {
+							type: 'string',
+							label: this.$ts.name,
+							default: this.column.name
+						},
+						width: {
+							type: 'number',
+							label: this.$ts.width,
+							default: this.column.width
+						},
+						flexible: {
+							type: 'boolean',
+							label: this.$ts.flexible,
+							default: this.column.flexible
+						}
+					});
+					if (canceled) return;
+					updateColumn(this.column.id, result);
+				}
+			}, null, {
+				icon: 'fas fa-arrow-left',
+				text: this.$ts._deck.swapLeft,
+				action: () => {
+					swapLeftColumn(this.column.id);
+				}
+			}, {
+				icon: 'fas fa-arrow-right',
+				text: this.$ts._deck.swapRight,
+				action: () => {
+					swapRightColumn(this.column.id);
+				}
+			}, this.isStacked ? {
+				icon: 'fas fa-arrow-up',
+				text: this.$ts._deck.swapUp,
+				action: () => {
+					swapUpColumn(this.column.id);
+				}
+			} : undefined, this.isStacked ? {
+				icon: 'fas fa-arrow-down',
+				text: this.$ts._deck.swapDown,
+				action: () => {
+					swapDownColumn(this.column.id);
+				}
+			} : undefined, null, {
+				icon: 'fas fa-window-restore',
+				text: this.$ts._deck.stackLeft,
+				action: () => {
+					stackLeftColumn(this.column.id);
+				}
+			}, this.isStacked ? {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts._deck.popRight,
+				action: () => {
+					popRightColumn(this.column.id);
+				}
+			} : undefined, null, {
+				icon: 'fas fa-trash-alt',
+				text: this.$ts.remove,
+				danger: true,
+				action: () => {
+					removeColumn(this.column.id);
+				}
+			}];
+
+			return items;
+		},
+
+		onContextmenu(e) {
+			os.contextMenu(this.getMenu(), e);
+		},
+
+		goTop() {
+			this.$refs.body.scrollTo({
+				top: 0,
+				behavior: 'smooth'
+			});
+		},
+
+		onDragstart(e) {
+			e.dataTransfer.effectAllowed = 'move';
+			e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, this.column.id);
+
+			// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
+			// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
+			setTimeout(() => {
+				this.dragging = true;
+			}, 10);
+		},
+
+		onDragend(e) {
+			this.dragging = false;
+		},
+
+		onDragover(e) {
+			// 自分自身がドラッグされている場合
+			if (this.dragging) {
+				// 自分自身にはドロップさせない
+				e.dataTransfer.dropEffect = 'none';
+				return;
+			}
+
+			const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_;
+
+			e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
+
+			if (!this.dragging && isDeckColumn) this.draghover = true;
+		},
+
+		onDragleave() {
+			this.draghover = false;
+		},
+
+		onDrop(e) {
+			this.draghover = false;
+			os.deckGlobalEvents.emit('column.dragEnd');
+
+			const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
+			if (id != null && id != '') {
+				swapColumn(this.column.id, id);
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.dnpfarvg {
+	--root-margin: 10px;
+
+	height: 100%;
+	overflow: hidden;
+	contain: content;
+	box-shadow: 0 0 8px 0 var(--shadow);
+
+	&.draghover {
+		box-shadow: 0 0 0 2px var(--focus);
+
+		&:after {
+			content: "";
+			display: block;
+			position: absolute;
+			z-index: 1000;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			background: var(--focus);
+		}
+	}
+
+	&.dragging {
+		box-shadow: 0 0 0 2px var(--focus);
+	}
+
+	&.dropready {
+		* {
+			pointer-events: none;
+		}
+	}
+
+	&:not(.active) {
+		flex-basis: var(--deckColumnHeaderHeight);
+		min-height: var(--deckColumnHeaderHeight);
+
+		> header.indicated {
+			box-shadow: 4px 0px var(--accent) inset;
+		}
+	}
+
+	&.naked {
+		background: var(--acrylicBg) !important;
+		-webkit-backdrop-filter: var(--blur, blur(10px));
+		backdrop-filter: var(--blur, blur(10px));
+
+		> header {
+			background: transparent;
+			box-shadow: none;
+
+			> button {
+				color: var(--fg);
+			}
+		}
+	}
+
+	&.paged {
+		background: var(--bg) !important;
+	}
+
+	> header {
+		position: relative;
+		display: flex;
+		z-index: 2;
+		line-height: var(--deckColumnHeaderHeight);
+		height: var(--deckColumnHeaderHeight);
+		padding: 0 16px;
+		font-size: 0.9em;
+		color: var(--panelHeaderFg);
+		background: var(--panelHeaderBg);
+		box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
+		cursor: pointer;
+
+		&, * {
+			user-select: none;
+		}
+
+		&.indicated {
+			box-shadow: 0 3px 0 0 var(--accent);
+		}
+
+		> .header {
+			display: inline-block;
+			align-items: center;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+
+		> span:only-of-type {
+			width: 100%;
+		}
+
+		> .toggleActive,
+		> .action > ::v-deep(*),
+		> .menu {
+			z-index: 1;
+			width: var(--deckColumnHeaderHeight);
+			line-height: var(--deckColumnHeaderHeight);
+			font-size: 16px;
+			color: var(--faceTextButton);
+
+			&:hover {
+				color: var(--faceTextButtonHover);
+			}
+
+			&:active {
+				color: var(--faceTextButtonActive);
+			}
+		}
+
+		> .toggleActive, > .action {
+			margin-left: -16px;
+		}
+
+		> .action {
+			z-index: 1;
+		}
+
+		> .action:empty {
+			display: none;
+		}
+
+		> .menu {
+			margin-left: auto;
+			margin-right: -16px;
+		}
+	}
+
+	> div {
+		height: calc(100% - var(--deckColumnHeaderHeight));
+		overflow: auto;
+		overflow-x: hidden;
+		-webkit-overflow-scrolling: touch;
+		box-sizing: border-box;
+	}
+}
+</style>
diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts
new file mode 100644
index 0000000000..6b6b02f3f9
--- /dev/null
+++ b/packages/client/src/ui/deck/deck-store.ts
@@ -0,0 +1,298 @@
+import { throttle } from 'throttle-debounce';
+import { i18n } from '@/i18n';
+import { api } from '@/os';
+import { markRaw, watch } from 'vue';
+import { Storage } from '../../pizzax';
+
+type ColumnWidget = {
+	name: string;
+	id: string;
+	data: Record<string, any>;
+};
+
+type Column = {
+	id: string;
+	type: string;
+	name: string | null;
+	width: number;
+	widgets?: ColumnWidget[];
+	active?: boolean;
+};
+
+function copy<T>(x: T): T {
+	return JSON.parse(JSON.stringify(x));
+}
+
+export const deckStore = markRaw(new Storage('deck', {
+	profile: {
+		where: 'deviceAccount',
+		default: 'default'
+	},
+	columns: {
+		where: 'deviceAccount',
+		default: [] as Column[]
+	},
+	layout: {
+		where: 'deviceAccount',
+		default: [] as Column['id'][][]
+	},
+	columnAlign: {
+		where: 'deviceAccount',
+		default: 'left' as 'left' | 'right' | 'center'
+	},
+	alwaysShowMainColumn: {
+		where: 'deviceAccount',
+		default: true
+	},
+	navWindow: {
+		where: 'deviceAccount',
+		default: true
+	},
+	columnMargin: {
+		where: 'deviceAccount',
+		default: 16
+	},
+	columnHeaderHeight: {
+		where: 'deviceAccount',
+		default: 42
+	},
+}));
+
+export const loadDeck = async () => {
+	let deck;
+
+	try {
+		deck = await api('i/registry/get', {
+			scope: ['client', 'deck', 'profiles'],
+			key: deckStore.state.profile,
+		});
+	} catch (e) {
+		if (e.code === 'NO_SUCH_KEY') {
+			// 後方互換性のため
+			if (deckStore.state.profile === 'default') {
+				saveDeck();
+				return;
+			}
+
+			deckStore.set('columns', [{
+				id: 'a',
+				type: 'main',
+				name: i18n.locale._deck._columns.main,
+				width: 350,
+			}, {
+				id: 'b',
+				type: 'notifications',
+				name: i18n.locale._deck._columns.notifications,
+				width: 330,
+			}]);
+			deckStore.set('layout', [['a'], ['b']]);
+			return;
+		}
+		throw e;
+	}
+
+	deckStore.set('columns', deck.columns);
+	deckStore.set('layout', deck.layout);
+};
+
+// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
+export const saveDeck = throttle(1000, () => {
+	api('i/registry/set', {
+		scope: ['client', 'deck', 'profiles'],
+		key: deckStore.state.profile,
+		value: {
+			columns: deckStore.reactiveState.columns.value,
+			layout: deckStore.reactiveState.layout.value,
+		}
+	});
+});
+
+export function addColumn(column: Column) {
+	if (column.name == undefined) column.name = null;
+	deckStore.push('columns', column);
+	deckStore.push('layout', [column.id]);
+	saveDeck();
+}
+
+export function removeColumn(id: Column['id']) {
+	deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id));
+	deckStore.set('layout', deckStore.state.layout
+		.map(ids => ids.filter(_id => _id !== id))
+		.filter(ids => ids.length > 0));
+	saveDeck();
+}
+
+export function swapColumn(a: Column['id'], b: Column['id']) {
+	const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) != -1);
+	const aY = deckStore.state.layout[aX].findIndex(id => id == a);
+	const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) != -1);
+	const bY = deckStore.state.layout[bX].findIndex(id => id == b);
+	const layout = copy(deckStore.state.layout);
+	layout[aX][aY] = b;
+	layout[bX][bY] = a;
+	deckStore.set('layout', layout);
+	saveDeck();
+}
+
+export function swapLeftColumn(id: Column['id']) {
+	const layout = copy(deckStore.state.layout);
+	deckStore.state.layout.some((ids, i) => {
+		if (ids.includes(id)) {
+			const left = deckStore.state.layout[i - 1];
+			if (left) {
+				layout[i - 1] = deckStore.state.layout[i];
+				layout[i] = left;
+				deckStore.set('layout', layout);
+			}
+			return true;
+		}
+	});
+	saveDeck();
+}
+
+export function swapRightColumn(id: Column['id']) {
+	const layout = copy(deckStore.state.layout);
+	deckStore.state.layout.some((ids, i) => {
+		if (ids.includes(id)) {
+			const right = deckStore.state.layout[i + 1];
+			if (right) {
+				layout[i + 1] = deckStore.state.layout[i];
+				layout[i] = right;
+				deckStore.set('layout', layout);
+			}
+			return true;
+		}
+	});
+	saveDeck();
+}
+
+export function swapUpColumn(id: Column['id']) {
+	const layout = copy(deckStore.state.layout);
+	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
+	const ids = copy(deckStore.state.layout[idsIndex]);
+	ids.some((x, i) => {
+		if (x === id) {
+			const up = ids[i - 1];
+			if (up) {
+				ids[i - 1] = id;
+				ids[i] = up;
+
+				layout[idsIndex] = ids;
+				deckStore.set('layout', layout);
+			}
+			return true;
+		}
+	});
+	saveDeck();
+}
+
+export function swapDownColumn(id: Column['id']) {
+	const layout = copy(deckStore.state.layout);
+	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
+	const ids = copy(deckStore.state.layout[idsIndex]);
+	ids.some((x, i) => {
+		if (x === id) {
+			const down = ids[i + 1];
+			if (down) {
+				ids[i + 1] = id;
+				ids[i] = down;
+
+				layout[idsIndex] = ids;
+				deckStore.set('layout', layout);
+			}
+			return true;
+		}
+	});
+	saveDeck();
+}
+
+export function stackLeftColumn(id: Column['id']) {
+	let layout = copy(deckStore.state.layout);
+	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
+	layout = layout.map(ids => ids.filter(_id => _id !== id));
+	layout[i - 1].push(id);
+	layout = layout.filter(ids => ids.length > 0);
+	deckStore.set('layout', layout);
+	saveDeck();
+}
+
+export function popRightColumn(id: Column['id']) {
+	let layout = copy(deckStore.state.layout);
+	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
+	const affected = layout[i];
+	layout = layout.map(ids => ids.filter(_id => _id !== id));
+	layout.splice(i + 1, 0, [id]);
+	layout = layout.filter(ids => ids.length > 0);
+	deckStore.set('layout', layout);
+
+	const columns = copy(deckStore.state.columns);
+	for (const column of columns) {
+		if (affected.includes(column.id)) {
+			column.active = true;
+		}
+	}
+	deckStore.set('columns', columns);
+
+	saveDeck();
+}
+
+export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
+	column.widgets.unshift(widget);
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+	saveDeck();
+}
+
+export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	column.widgets = column.widgets.filter(w => w.id != widget.id);
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+	saveDeck();
+}
+
+export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	column.widgets = widgets;
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+	saveDeck();
+}
+
+export function updateColumnWidget(id: Column['id'], widgetId: string, data: any) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const column = copy(deckStore.state.columns[columnIndex]);
+	if (column == null) return;
+	column.widgets = column.widgets.map(w => w.id === widgetId ? {
+		...w,
+		data: data
+	} : w);
+	columns[columnIndex] = column;
+	deckStore.set('columns', columns);
+	saveDeck();
+}
+
+export function updateColumn(id: Column['id'], column: Partial<Column>) {
+	const columns = copy(deckStore.state.columns);
+	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
+	const currentColumn = copy(deckStore.state.columns[columnIndex]);
+	if (currentColumn == null) return;
+	for (const [k, v] of Object.entries(column)) {
+		currentColumn[k] = v;
+	}
+	columns[columnIndex] = currentColumn;
+	deckStore.set('columns', columns);
+	saveDeck();
+}
diff --git a/packages/client/src/ui/deck/direct-column.vue b/packages/client/src/ui/deck/direct-column.vue
new file mode 100644
index 0000000000..a11b2e82ed
--- /dev/null
+++ b/packages/client/src/ui/deck/direct-column.vue
@@ -0,0 +1,55 @@
+<template>
+<XColumn :column="column" :is-stacked="isStacked">
+	<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
+
+	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
+</XColumn>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XColumn from './column.vue';
+import XNotes from '@/components/notes.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XColumn,
+		XNotes
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true
+		},
+		isStacked: {
+			type: Boolean,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'notes/mentions',
+				limit: 10,
+				params: () => ({
+					visibility: 'specified'
+				})
+			},
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/ui/deck/list-column.vue b/packages/client/src/ui/deck/list-column.vue
new file mode 100644
index 0000000000..3ebba8032f
--- /dev/null
+++ b/packages/client/src/ui/deck/list-column.vue
@@ -0,0 +1,80 @@
+<template>
+<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked">
+	<template #header>
+		<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
+	</template>
+
+	<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
+</XColumn>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XColumn from './column.vue';
+import XTimeline from '@/components/timeline.vue';
+import * as os from '@/os';
+import { updateColumn } from './deck-store';
+
+export default defineComponent({
+	components: {
+		XColumn,
+		XTimeline,
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true
+		},
+		isStacked: {
+			type: Boolean,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	watch: {
+		mediaOnly() {
+			(this.$refs.timeline as any).reload();
+		}
+	},
+
+	mounted() {
+		if (this.column.listId == null) {
+			this.setList();
+		}
+	},
+
+	methods: {
+		async setList() {
+			const lists = await os.api('users/lists/list');
+			const { canceled, result: list } = await os.dialog({
+				title: this.$ts.selectList,
+				type: null,
+				select: {
+					items: lists.map(x => ({
+						value: x, text: x.name
+					})),
+					default: this.column.listId
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			updateColumn(this.column.id, {
+				listId: list.id
+			});
+		},
+
+		focus() {
+			(this.$refs.timeline as any).focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue
new file mode 100644
index 0000000000..744056881c
--- /dev/null
+++ b/packages/client/src/ui/deck/main-column.vue
@@ -0,0 +1,91 @@
+<template>
+<XColumn v-if="deckStore.state.alwaysShowMainColumn || $route.name !== 'index'" :column="column" :is-stacked="isStacked">
+	<template #header>
+		<template v-if="pageInfo">
+			<i :class="pageInfo.icon"></i>
+			{{ pageInfo.title }}
+		</template>
+	</template>
+
+	<MkStickyContainer>
+		<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
+		<router-view v-slot="{ Component }">
+			<transition>
+				<keep-alive :include="['timeline']">
+					<component :is="Component" :ref="changePage" @contextmenu.stop="onContextmenu"/>
+				</keep-alive>
+			</transition>
+		</router-view>
+	</MkStickyContainer>
+</XColumn>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XColumn from './column.vue';
+import XNotes from '@/components/notes.vue';
+import { deckStore } from '@/ui/deck/deck-store';
+import * as os from '@/os';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XColumn,
+		XNotes
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true
+		},
+		isStacked: {
+			type: Boolean,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			deckStore,
+			pageInfo: null,
+		}
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+			}
+		},
+
+		back() {
+			history.back();
+		},
+
+		onContextmenu(e) {
+			const isLink = (el: HTMLElement) => {
+				if (el.tagName === 'A') return true;
+				if (el.parentElement) {
+					return isLink(el.parentElement);
+				}
+			};
+			if (isLink(e.target)) return;
+			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+			if (window.getSelection().toString() !== '') return;
+			const path = this.$route.path;
+			os.contextMenu([{
+				type: 'label',
+				text: path,
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(path);
+				}
+			}], e);
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/ui/deck/mentions-column.vue b/packages/client/src/ui/deck/mentions-column.vue
new file mode 100644
index 0000000000..7dd06989cb
--- /dev/null
+++ b/packages/client/src/ui/deck/mentions-column.vue
@@ -0,0 +1,52 @@
+<template>
+<XColumn :column="column" :is-stacked="isStacked">
+	<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
+
+	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
+</XColumn>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import Progress from '@/scripts/loading';
+import XColumn from './column.vue';
+import XNotes from '@/components/notes.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XColumn,
+		XNotes
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true
+		},
+		isStacked: {
+			type: Boolean,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'notes/mentions',
+				limit: 10,
+			},
+		}
+	},
+
+	methods: {
+		before() {
+			Progress.start();
+		},
+
+		after() {
+			Progress.done();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/ui/deck/notifications-column.vue b/packages/client/src/ui/deck/notifications-column.vue
new file mode 100644
index 0000000000..f8f406cdd1
--- /dev/null
+++ b/packages/client/src/ui/deck/notifications-column.vue
@@ -0,0 +1,53 @@
+<template>
+<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }">
+	<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
+
+	<XNotifications :include-types="column.includingTypes"/>
+</XColumn>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XColumn from './column.vue';
+import XNotifications from '@/components/notifications.vue';
+import * as os from '@/os';
+import { updateColumn } from './deck-store';
+
+export default defineComponent({
+	components: {
+		XColumn,
+		XNotifications
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true
+		},
+		isStacked: {
+			type: Boolean,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+		}
+	},
+
+	methods: {
+		func() {
+			os.popup(import('@/components/notification-setting-window.vue'), {
+				includingTypes: this.column.includingTypes,
+			}, {
+				done: async (res) => {
+					const { includingTypes } = res;
+					updateColumn(this.column.id, {
+						includingTypes: includingTypes
+					});
+				},
+			}, 'closed');
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/ui/deck/tl-column.vue b/packages/client/src/ui/deck/tl-column.vue
new file mode 100644
index 0000000000..faf692c447
--- /dev/null
+++ b/packages/client/src/ui/deck/tl-column.vue
@@ -0,0 +1,137 @@
+<template>
+<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
+	<template #header>
+		<i v-if="column.tl === 'home'" class="fas fa-home"></i>
+		<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
+		<i v-else-if="column.tl === 'social'" class="fas fa-share-alt"></i>
+		<i v-else-if="column.tl === 'global'" class="fas fa-globe"></i>
+		<span style="margin-left: 8px;">{{ column.name }}</span>
+	</template>
+
+	<div class="iwaalbte" v-if="disabled">
+		<p>
+			<i class="fas fa-minus-circle"></i>
+			{{ $t('disabled-timeline.title') }}
+		</p>
+		<p class="desc">{{ $t('disabled-timeline.description') }}</p>
+	</div>
+	<XTimeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/>
+</XColumn>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XColumn from './column.vue';
+import XTimeline from '@/components/timeline.vue';
+import * as os from '@/os';
+import { removeColumn, updateColumn } from './deck-store';
+
+export default defineComponent({
+	components: {
+		XColumn,
+		XTimeline,
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true
+		},
+		isStacked: {
+			type: Boolean,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			disabled: false,
+			indicated: false,
+			columnActive: true,
+		};
+	},
+
+	watch: {
+		mediaOnly() {
+			(this.$refs.timeline as any).reload();
+		}
+	},
+
+	mounted() {
+		if (this.column.tl == null) {
+			this.setType();
+		} else {
+			this.disabled = !this.$i.isModerator && !this.$i.isAdmin && (
+				this.$instance.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) ||
+				this.$instance.disableGlobalTimeline && ['global'].includes(this.column.tl));
+		}
+	},
+
+	methods: {
+		async setType() {
+			const { canceled, result: src } = await os.dialog({
+				title: this.$ts.timeline,
+				type: null,
+				select: {
+					items: [{
+						value: 'home', text: this.$ts._timelines.home
+					}, {
+						value: 'local', text: this.$ts._timelines.local
+					}, {
+						value: 'social', text: this.$ts._timelines.social
+					}, {
+						value: 'global', text: this.$ts._timelines.global
+					}]
+				},
+			});
+			if (canceled) {
+				if (this.column.tl == null) {
+					removeColumn(this.column.id);
+				}
+				return;
+			}
+			updateColumn(this.column.id, {
+				tl: src
+			});
+		},
+
+		queueUpdated(q) {
+			if (this.columnActive) {
+				this.indicated = q !== 0;
+			}
+		},
+
+		onNote() {
+			if (!this.columnActive) {
+				this.indicated = true;
+			}
+		},
+
+		onChangeActiveState(state) {
+			this.columnActive = state;
+
+			if (this.columnActive) {
+				this.indicated = false;
+			}
+		},
+
+		focus() {
+			(this.$refs.timeline as any).focus();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.iwaalbte {
+	text-align: center;
+
+	> p {
+		margin: 16px;
+
+		&.desc {
+			font-size: 14px;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/deck/widgets-column.vue b/packages/client/src/ui/deck/widgets-column.vue
new file mode 100644
index 0000000000..8c3a95ac2b
--- /dev/null
+++ b/packages/client/src/ui/deck/widgets-column.vue
@@ -0,0 +1,71 @@
+<template>
+<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked">
+	<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
+
+	<div class="wtdtxvec">
+		<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
+	</div>
+</XColumn>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import XWidgets from '@/components/widgets.vue';
+import XColumn from './column.vue';
+import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
+
+export default defineComponent({
+	components: {
+		XColumn,
+		XWidgets,
+	},
+
+	props: {
+		column: {
+			type: Object,
+			required: true,
+		},
+		isStacked: {
+			type: Boolean,
+			required: true,
+		},
+	},
+
+	data() {
+		return {
+			edit: false,
+		};
+	},
+
+	methods: {
+		addWidget(widget) {
+			addColumnWidget(this.column.id, widget);
+		},
+
+		removeWidget(widget) {
+			removeColumnWidget(this.column.id, widget);
+		},
+
+		updateWidget({ id, data }) {
+			updateColumnWidget(this.column.id, id, data);
+		},
+
+		updateWidgets(widgets) {
+			setColumnWidgets(this.column.id, widgets);
+		},
+
+		func() {
+			this.edit = !this.edit;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wtdtxvec {
+	--margin: 8px;
+	--panelBorder: none;
+
+	padding: 0 var(--margin);
+}
+</style>
diff --git a/packages/client/src/ui/desktop.vue b/packages/client/src/ui/desktop.vue
new file mode 100644
index 0000000000..17783c58e3
--- /dev/null
+++ b/packages/client/src/ui/desktop.vue
@@ -0,0 +1,70 @@
+<template>
+<div class="mk-app" :class="{ wallpaper }" @contextmenu.prevent="() => {}">
+	<XSidebar ref="nav" class="sidebar"/>
+
+	<XCommon/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { host } from '@/config';
+import { search } from '@/scripts/search';
+import XCommon from './_common_/common.vue';
+import * as os from '@/os';
+import XSidebar from '@/ui/_common_/sidebar.vue';
+import { menuDef } from '@/menu';
+import { ColdDeviceStorage } from '@/store';
+
+export default defineComponent({
+	components: {
+		XCommon,
+		XSidebar
+	},
+
+	provide() {
+		return {
+			navHook: (url) => {
+				os.pageWindow(url);
+			}
+		};
+	},
+
+	data() {
+		return {
+			host: host,
+			menuDef: menuDef,
+			wallpaper: localStorage.getItem('wallpaper') != null,
+		};
+	},
+
+	computed: {
+		menu(): string[] {
+			return this.$store.state.menu;
+		},
+	},
+
+	created() {
+		if (window.innerWidth < 1024) {
+			localStorage.setItem('ui', 'default');
+			location.reload();
+		}
+	},
+
+	methods: {
+		help() {
+			window.open(`https://misskey-hub.net/docs/keyboard-shortcut.md`, '_blank');
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-app {
+	height: 100vh;
+	width: 100vw;
+}
+</style>
+
+<style lang="scss">
+</style>
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
new file mode 100644
index 0000000000..6d00bb6048
--- /dev/null
+++ b/packages/client/src/ui/universal.vue
@@ -0,0 +1,402 @@
+<template>
+<div class="mk-app" :class="{ wallpaper }">
+	<XSidebar ref="nav" class="sidebar"/>
+
+	<div class="contents" ref="contents" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }">
+		<main ref="main">
+			<div class="content">
+				<MkStickyContainer>
+					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
+					<router-view v-slot="{ Component }">
+						<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+							<keep-alive :include="['timeline']">
+								<component :is="Component" :ref="changePage"/>
+							</keep-alive>
+						</transition>
+					</router-view>
+				</MkStickyContainer>
+			</div>
+			<div class="spacer"></div>
+		</main>
+	</div>
+
+	<XSide v-if="isDesktop" class="side" ref="side"/>
+
+	<div v-if="isDesktop" class="widgets" ref="widgets">
+		<XWidgets @mounted="attachSticky"/>
+	</div>
+
+	<div class="buttons" :class="{ navHidden }">
+		<button class="button nav _button" @click="showNav" ref="navButton"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
+		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
+		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
+		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
+		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
+	</div>
+
+	<button class="widgetButton _button" :class="{ navHidden }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
+
+	<transition name="tray-back">
+		<div class="tray-back _modalBg"
+			v-if="widgetsShowing"
+			@click="widgetsShowing = false"
+			@touchstart.passive="widgetsShowing = false"
+		></div>
+	</transition>
+
+	<transition name="tray">
+		<XWidgets v-if="widgetsShowing" class="tray"/>
+	</transition>
+
+	<XCommon/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { instanceName } from '@/config';
+import { StickySidebar } from '@/scripts/sticky-sidebar';
+import XSidebar from '@/ui/_common_/sidebar.vue';
+import XCommon from './_common_/common.vue';
+import XSide from './classic.side.vue';
+import * as os from '@/os';
+import { menuDef } from '@/menu';
+import * as symbols from '@/symbols';
+
+const DESKTOP_THRESHOLD = 1100;
+
+export default defineComponent({
+	components: {
+		XCommon,
+		XSidebar,
+		XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
+		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
+	},
+
+	provide() {
+		return {
+			sideViewHook: this.isDesktop ? (url) => {
+				this.$refs.side.navigate(url);
+			} : null
+		};
+	},
+
+	data() {
+		return {
+			pageInfo: null,
+			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
+			menuDef: menuDef,
+			navHidden: false,
+			widgetsShowing: false,
+			wallpaper: localStorage.getItem('wallpaper') != null,
+		};
+	},
+
+	computed: {
+		navIndicated(): boolean {
+			for (const def in this.menuDef) {
+				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
+				if (this.menuDef[def].indicated) return true;
+			}
+			return false;
+		}
+	},
+
+	created() {
+		document.documentElement.style.overflowY = 'scroll';
+
+		if (this.$store.state.widgets.length === 0) {
+			this.$store.set('widgets', [{
+				name: 'calendar',
+				id: 'a', place: 'right', data: {}
+			}, {
+				name: 'notifications',
+				id: 'b', place: 'right', data: {}
+			}, {
+				name: 'trends',
+				id: 'c', place: 'right', data: {}
+			}]);
+		}
+	},
+
+	mounted() {
+		this.adjustUI();
+
+		const ro = new ResizeObserver((entries, observer) => {
+			this.adjustUI();
+		});
+
+		ro.observe(this.$refs.contents);
+
+		window.addEventListener('resize', this.adjustUI, { passive: true });
+
+		if (!this.isDesktop) {
+			window.addEventListener('resize', () => {
+				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
+			}, { passive: true });
+		}
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+				document.title = `${this.pageInfo.title} | ${instanceName}`;
+			}
+		},
+
+		adjustUI() {
+			const navWidth = this.$refs.nav.$el.offsetWidth;
+			this.navHidden = navWidth === 0;
+		},
+
+		showNav() {
+			this.$refs.nav.show();
+		},
+
+		attachSticky(el) {
+			const sticky = new StickySidebar(this.$refs.widgets);
+			window.addEventListener('scroll', () => {
+				sticky.calc(window.scrollY);
+			}, { passive: true });
+		},
+
+		post() {
+			os.post();
+		},
+
+		top() {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		},
+
+		back() {
+			history.back();
+		},
+
+		onTransition() {
+			if (window._scroll) window._scroll();
+		},
+
+		onContextmenu(e) {
+			const isLink = (el: HTMLElement) => {
+				if (el.tagName === 'A') return true;
+				if (el.parentElement) {
+					return isLink(el.parentElement);
+				}
+			};
+			if (isLink(e.target)) return;
+			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+			if (window.getSelection().toString() !== '') return;
+			const path = this.$route.path;
+			os.contextMenu([{
+				type: 'label',
+				text: path,
+			}, {
+				icon: 'fas fa-columns',
+				text: this.$ts.openInSideView,
+				action: () => {
+					this.$refs.side.navigate(path);
+				}
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(path);
+				}
+			}], e);
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.tray-enter-active,
+.tray-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.tray-enter-from,
+.tray-leave-active {
+	opacity: 0;
+	transform: translateX(240px);
+}
+
+.tray-back-enter-active,
+.tray-back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.tray-back-enter-from,
+.tray-back-leave-active {
+	opacity: 0;
+}
+
+.mk-app {
+	$ui-font-size: 1em; // TODO: どこかに集約したい
+	$widgets-hide-threshold: 1090px;
+
+	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+	min-height: calc(var(--vh, 1vh) * 100);
+	box-sizing: border-box;
+	display: flex;
+
+	&.wallpaper {
+		background: var(--wallpaperOverlay);
+		//backdrop-filter: var(--blur, blur(4px));
+	}
+
+	> .sidebar {
+	}
+
+	> .contents {
+		width: 100%;
+		min-width: 0;
+		background: var(--panel);
+
+		> main {
+			min-width: 0;
+
+			> .spacer {
+				height: 82px;
+
+				@media (min-width: ($widgets-hide-threshold + 1px)) {
+					display: none;
+				}
+			}
+		}
+	}
+
+	> .side {
+		min-width: 370px;
+		max-width: 370px;
+		border-left: solid 0.5px var(--divider);
+	}
+
+	> .widgets {
+		padding: 0 var(--margin);
+		border-left: solid 0.5px var(--divider);
+		background: var(--bg);
+
+		@media (max-width: $widgets-hide-threshold) {
+			display: none;
+		}
+	}
+
+	> .widgetButton {
+		display: block;
+		position: fixed;
+		z-index: 1000;
+		bottom: 32px;
+		right: 32px;
+		width: 64px;
+		height: 64px;
+		border-radius: 100%;
+		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
+		font-size: 22px;
+		background: var(--panel);
+
+		&.navHidden {
+			display: none;
+		}
+
+		@media (min-width: ($widgets-hide-threshold + 1px)) {
+			display: none;
+		}
+	}
+
+	> .buttons {
+		position: fixed;
+		z-index: 1000;
+		bottom: 0;
+		padding: 16px;
+		display: flex;
+		width: 100%;
+		box-sizing: border-box;
+		-webkit-backdrop-filter: var(--blur, blur(32px));
+		backdrop-filter: var(--blur, blur(32px));
+		background-color: var(--header);
+
+		&:not(.navHidden) {
+			display: none;
+		}
+
+		> .button {
+			position: relative;
+			flex: 1;
+			padding: 0;
+			margin: auto;
+			height: 64px;
+			border-radius: 8px;
+			background: var(--panel);
+			color: var(--fg);
+
+			&:not(:last-child) {
+				margin-right: 12px;
+			}
+
+			@media (max-width: 400px) {
+				height: 60px;
+
+				&:not(:last-child) {
+					margin-right: 8px;
+				}
+			}
+
+			&:hover {
+				background: var(--X2);
+			}
+
+			> .indicator {
+				position: absolute;
+				top: 0;
+				left: 0;
+				color: var(--indicator);
+				font-size: 16px;
+				animation: blink 1s infinite;
+			}
+
+			&:first-child {
+				margin-left: 0;
+			}
+
+			&:last-child {
+				margin-right: 0;
+			}
+
+			> * {
+				font-size: 22px;
+			}
+
+			&:disabled {
+				cursor: default;
+
+				> * {
+					opacity: 0.5;
+				}
+			}
+		}
+	}
+
+	> .tray-back {
+		z-index: 1001;
+	}
+
+	> .tray {
+		position: fixed;
+		top: 0;
+		right: 0;
+		z-index: 1001;
+		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+		height: calc(var(--vh, 1vh) * 100);
+		padding: var(--margin);
+		box-sizing: border-box;
+		overflow: auto;
+		background: var(--bg);
+	}
+}
+</style>
+
+<style lang="scss">
+</style>
diff --git a/packages/client/src/ui/universal.widgets.vue b/packages/client/src/ui/universal.widgets.vue
new file mode 100644
index 0000000000..37911d6624
--- /dev/null
+++ b/packages/client/src/ui/universal.widgets.vue
@@ -0,0 +1,79 @@
+<template>
+<div class="efzpzdvf">
+	<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
+
+	<button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
+	<button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import XWidgets from '@/components/widgets.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	components: {
+		XWidgets
+	},
+
+	emits: ['mounted'],
+
+	data() {
+		return {
+			editMode: false,
+		};
+	},
+
+	mounted() {
+		this.$emit('mounted', this.$el);
+	},
+
+	methods: {
+		addWidget(widget) {
+			this.$store.set('widgets', [{
+				...widget,
+				place: null,
+			}, ...this.$store.state.widgets]);
+		},
+
+		removeWidget(widget) {
+			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
+		},
+
+		updateWidget({ id, data }) {
+			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? {
+				...w,
+				data: data
+			} : w));
+		},
+
+		updateWidgets(widgets) {
+			this.$store.set('widgets', widgets);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.efzpzdvf {
+	position: sticky;
+	height: min-content;
+	min-height: 100vh;
+	padding: var(--margin) 0;
+	box-sizing: border-box;
+
+	> * {
+		margin: var(--margin) 0;
+		width: 300px;
+
+		&:first-child {
+			margin-top: 0;
+		}
+	}
+
+	> .add {
+		margin: 0 auto;
+	}
+}
+</style>
diff --git a/packages/client/src/ui/visitor.vue b/packages/client/src/ui/visitor.vue
new file mode 100644
index 0000000000..ec9150d346
--- /dev/null
+++ b/packages/client/src/ui/visitor.vue
@@ -0,0 +1,19 @@
+<template>
+<DesignB/>
+<XCommon/>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import DesignA from './visitor/a.vue';
+import DesignB from './visitor/b.vue';
+import XCommon from './_common_/common.vue';
+
+export default defineComponent({
+	components: {
+		XCommon,
+		DesignA,
+		DesignB,
+	},
+});
+</script>
diff --git a/packages/client/src/ui/visitor/a.vue b/packages/client/src/ui/visitor/a.vue
new file mode 100644
index 0000000000..d7098f94b3
--- /dev/null
+++ b/packages/client/src/ui/visitor/a.vue
@@ -0,0 +1,260 @@
+<template>
+<div class="mk-app">
+	<div class="banner" v-if="$route.path === '/'" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
+		<div>
+			<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
+			<div class="about" v-if="meta">
+				<div class="desc" v-html="meta.description || $ts.introMisskey"></div>
+			</div>
+			<div class="action">
+				<button class="_button primary" @click="signup()">{{ $ts.signup }}</button>
+				<button class="_button" @click="signin()">{{ $ts.login }}</button>
+			</div>
+		</div>
+	</div>
+	<div class="banner-mini" v-else :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
+		<div>
+			<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
+		</div>
+	</div>
+
+	<div class="main">
+		<div class="contents" ref="contents" :class="{ wallpaper }">
+			<header class="header" ref="header" v-show="$route.path !== '/'">
+				<XHeader :info="pageInfo"/>
+			</header>
+			<main ref="main">
+				<router-view v-slot="{ Component }">
+					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+						<component :is="Component" :ref="changePage"/>
+					</transition>
+				</router-view>
+			</main>
+			<div class="powered-by">
+				<b><MkA to="/">{{ host }}</MkA></b>
+				<small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { host, instanceName } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import MkPagination from '@/components/ui/pagination.vue';
+import MkButton from '@/components/ui/button.vue';
+import XHeader from './header.vue';
+import { ColdDeviceStorage } from '@/store';
+import * as symbols from '@/symbols';
+
+const DESKTOP_THRESHOLD = 1100;
+
+export default defineComponent({
+	components: {
+		XHeader,
+		MkPagination,
+		MkButton,
+	},
+
+	data() {
+		return {
+			host,
+			instanceName,
+			pageInfo: null,
+			meta: null,
+			narrow: window.innerWidth < 1280,
+			announcements: {
+				endpoint: 'announcements',
+				limit: 10,
+			},
+			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				'd': () => {
+					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
+					this.$store.set('darkMode', !this.$store.state.darkMode);
+				},
+				's': search,
+				'h|/': this.help
+			};
+		},
+	},
+
+	created() {
+		document.documentElement.style.overflowY = 'scroll';
+
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+	},
+
+	mounted() {
+		if (!this.isDesktop) {
+			window.addEventListener('resize', () => {
+				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
+			}, { passive: true });
+		}
+	},
+
+	methods: {
+		setParallax(el) {
+			//new simpleParallax(el);
+		},
+
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+			}
+		},
+
+		top() {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		},
+
+		help() {
+			window.open(`https://misskey-hub.net/docs/keyboard-shortcut.md`, '_blank');
+		},
+
+		onTransition() {
+			if (window._scroll) window._scroll();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-app {
+	min-height: 100vh;
+
+	> .banner {
+		position: relative;
+		width: 100%;
+		text-align: center;
+		background-position: center;
+		background-size: cover;
+
+		> div {
+			height: 100%;
+			background: rgba(0, 0, 0, 0.3);
+
+			* {
+				color: #fff;
+			}
+					
+			> h1 {
+				margin: 0;
+				padding: 96px 32px 0 32px;
+				text-shadow: 0 0 8px black;
+
+				> .logo {
+					vertical-align: bottom;
+					max-height: 150px;
+				}
+			}
+
+			> .about {
+				padding: 32px;
+				max-width: 580px;
+				margin: 0 auto;
+				box-sizing: border-box;
+				text-shadow: 0 0 8px black;
+			}
+
+			> .action {
+				padding-bottom: 64px;
+				
+				> button {
+					display: inline-block;
+					padding: 10px 20px;
+					box-sizing: border-box;
+					text-align: center;
+					border-radius: 999px;
+					background: var(--panel);
+					color: var(--fg);
+
+					&.primary {
+						background: var(--accent);
+						color: #fff;
+					}
+
+					&:first-child {
+						margin-right: 16px;
+					}
+				}
+			}
+		}
+	}
+
+	> .banner-mini {
+		position: relative;
+		width: 100%;
+		text-align: center;
+		background-position: center;
+		background-size: cover;
+
+		> div {
+			position: relative;
+			z-index: 1;
+			height: 100%;
+			background: rgba(0, 0, 0, 0.3);
+
+			* {
+				color: #fff !important;
+			}
+
+			> header {
+				
+			}
+					
+			> h1 {
+				margin: 0;
+				padding: 32px;
+				text-shadow: 0 0 8px black;
+
+				> .logo {
+					vertical-align: bottom;
+					max-height: 100px;
+				}
+			}
+		}
+	}
+
+	> .main {
+		> .contents {
+			position: relative;
+			z-index: 1;
+
+			> .header {
+				position: sticky;
+				top: 0;
+				left: 0;
+				z-index: 1000;
+			}
+
+			> .powered-by {
+				padding: 28px;
+				font-size: 14px;
+				text-align: center;
+				border-top: 1px solid var(--divider);
+
+				> small {
+					display: block;
+					margin-top: 8px;
+					opacity: 0.5;
+				}
+			}
+		}
+	}
+}
+</style>
+
+<style lang="scss">
+</style>
diff --git a/packages/client/src/ui/visitor/b.vue b/packages/client/src/ui/visitor/b.vue
new file mode 100644
index 0000000000..d662187aae
--- /dev/null
+++ b/packages/client/src/ui/visitor/b.vue
@@ -0,0 +1,282 @@
+<template>
+<div class="mk-app">
+	<a v-if="root" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
+
+	<div class="side" v-if="!narrow && !root">
+		<XKanban class="kanban" full/>
+	</div>
+
+	<div class="main">
+		<XKanban class="banner" :powered-by="root" v-if="narrow && !root"/>
+
+		<div class="contents">
+			<XHeader class="header" :info="pageInfo" v-if="!root"/>
+			<main>
+				<router-view v-slot="{ Component }">
+					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+						<component :is="Component" :ref="changePage"/>
+					</transition>
+				</router-view>
+			</main>
+			<div class="powered-by" v-if="!root">
+				<b><MkA to="/">{{ host }}</MkA></b>
+				<small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small>
+			</div>
+		</div>
+	</div>
+
+	<transition name="tray-back">
+		<div class="menu-back _modalBg"
+			v-if="showMenu"
+			@click="showMenu = false"
+			@touchstart.passive="showMenu = false"
+		></div>
+	</transition>
+
+	<transition name="tray">
+		<div v-if="showMenu" class="menu">
+			<MkA to="/" class="link" active-class="active"><i class="fas fa-home icon"></i>{{ $ts.home }}</MkA>
+			<MkA to="/explore" class="link" active-class="active"><i class="fas fa-hashtag icon"></i>{{ $ts.explore }}</MkA>
+			<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
+			<MkA to="/channels" class="link" active-class="active"><i class="fas fa-satellite-dish icon"></i>{{ $ts.channel }}</MkA>
+			<div class="action">
+				<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
+				<button class="_button" @click="signin()">{{ $ts.login }}</button>
+			</div>
+		</div>
+	</transition>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { host, instanceName } from '@/config';
+import { search } from '@/scripts/search';
+import * as os from '@/os';
+import MkPagination from '@/components/ui/pagination.vue';
+import XSigninDialog from '@/components/signin-dialog.vue';
+import XSignupDialog from '@/components/signup-dialog.vue';
+import MkButton from '@/components/ui/button.vue';
+import XHeader from './header.vue';
+import XKanban from './kanban.vue';
+import { ColdDeviceStorage } from '@/store';
+import * as symbols from '@/symbols';
+
+const DESKTOP_THRESHOLD = 1100;
+
+export default defineComponent({
+	components: {
+		XHeader,
+		XKanban,
+		MkPagination,
+		MkButton,
+	},
+
+	data() {
+		return {
+			host,
+			instanceName,
+			pageInfo: null,
+			meta: null,
+			showMenu: false,
+			narrow: window.innerWidth < 1280,
+			announcements: {
+				endpoint: 'announcements',
+				limit: 10,
+			},
+			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
+		};
+	},
+
+	computed: {
+		keymap(): any {
+			return {
+				'd': () => {
+					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
+					this.$store.set('darkMode', !this.$store.state.darkMode);
+				},
+				's': search,
+				'h|/': this.help
+			};
+		},
+
+		root(): boolean {
+			return this.$route.path === '/';
+		},
+	},
+
+	created() {
+		//document.documentElement.style.overflowY = 'scroll';
+
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+	},
+
+	mounted() {
+		if (!this.isDesktop) {
+			window.addEventListener('resize', () => {
+				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
+			}, { passive: true });
+		}
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+			}
+		},
+
+		top() {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		},
+
+		help() {
+			window.open(`https://misskey-hub.net/docs/keyboard-shortcut.md`, '_blank');
+		},
+
+		onTransition() {
+			if (window._scroll) window._scroll();
+		},
+
+		signin() {
+			os.popup(XSigninDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		signup() {
+			os.popup(XSignupDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		}
+	}
+});
+</script>
+
+<style>
+.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}
+</style>
+
+<style lang="scss" scoped>
+.tray-enter-active,
+.tray-leave-active {
+	opacity: 1;
+	transform: translateX(0);
+	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.tray-enter-from,
+.tray-leave-active {
+	opacity: 0;
+	transform: translateX(-240px);
+}
+
+.tray-back-enter-active,
+.tray-back-leave-active {
+	opacity: 1;
+	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.tray-back-enter-from,
+.tray-back-leave-active {
+	opacity: 0;
+}
+
+.mk-app {
+	display: flex;
+	min-height: 100vh;
+	background-position: center;
+	background-size: cover;
+	background-attachment: fixed;
+
+	> .side {
+		width: 500px;
+		height: 100vh;
+
+		> .kanban {
+			position: fixed;
+			top: 0;
+			left: 0;
+			width: 500px;
+			height: 100vh;
+			overflow: auto;
+		}
+	}
+
+	> .main {
+		flex: 1;
+		min-width: 0;
+
+		> .banner {
+		}
+
+		> .contents {
+			position: relative;
+			z-index: 1;
+
+			> .powered-by {
+				padding: 28px;
+				font-size: 14px;
+				text-align: center;
+				border-top: 1px solid var(--divider);
+
+				> small {
+					display: block;
+					margin-top: 8px;
+					opacity: 0.5;
+				}
+			}
+		}
+	}
+
+	> .menu-back {
+		position: fixed;
+		z-index: 1001;
+		top: 0;
+		left: 0;
+		width: 100vw;
+		height: 100vh;
+	}
+
+	> .menu {
+		position: fixed;
+		z-index: 1001;
+		top: 0;
+		left: 0;
+		width: 240px;
+		height: 100vh;
+		background: var(--panel);
+
+		> .link {
+			display: block;
+			padding: 16px;
+
+			> .icon {
+				margin-right: 1em;
+			}
+		}
+
+		> .action {
+			padding: 16px;
+
+			> button {
+				display: block;
+				width: 100%;
+				padding: 10px;
+				box-sizing: border-box;
+				text-align: center;
+				border-radius: 999px;
+
+				&._button {
+					background: var(--panel);
+				}
+
+				&:first-child {
+					margin-bottom: 16px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/visitor/header.vue b/packages/client/src/ui/visitor/header.vue
new file mode 100644
index 0000000000..5caef1cdd6
--- /dev/null
+++ b/packages/client/src/ui/visitor/header.vue
@@ -0,0 +1,228 @@
+<template>
+<div class="sqxihjet">
+	<div class="wide" v-if="narrow === false">
+		<div class="content">
+			<MkA to="/" class="link" active-class="active"><i class="fas fa-home icon"></i>{{ $ts.home }}</MkA>
+			<MkA to="/explore" class="link" active-class="active"><i class="fas fa-hashtag icon"></i>{{ $ts.explore }}</MkA>
+			<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
+			<MkA to="/channels" class="link" active-class="active"><i class="fas fa-satellite-dish icon"></i>{{ $ts.channel }}</MkA>
+			<div class="page active link" v-if="info">
+				<div class="title">
+					<i v-if="info.icon" class="icon" :class="info.icon"></i>
+					<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
+					<span v-if="info.title" class="text">{{ info.title }}</span>
+					<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
+				</div>
+				<button class="_button action" v-if="info.action" @click.stop="info.action.handler"><!-- TODO --></button>
+			</div>
+			<div class="right">
+				<button class="_button search" @click="search()"><i class="fas fa-search icon"></i><span>{{ $ts.search }}</span></button>
+				<button class="_buttonPrimary signup" @click="signup()">{{ $ts.signup }}</button>
+				<button class="_button login" @click="signin()">{{ $ts.login }}</button>
+			</div>
+		</div>
+	</div>
+	<div class="narrow" v-else-if="narrow === true">
+		<button class="menu _button" @click="$parent.showMenu = true">
+			<i class="fas fa-bars icon"></i>
+		</button>
+		<div class="title" v-if="info">
+			<i v-if="info.icon" class="icon" :class="info.icon"></i>
+			<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
+			<span v-if="info.title" class="text">{{ info.title }}</span>
+			<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
+		</div>
+		<button class="action _button" v-if="info && info.action" @click.stop="info.action.handler">
+			<!-- TODO -->
+		</button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XSigninDialog from '@/components/signin-dialog.vue';
+import XSignupDialog from '@/components/signup-dialog.vue';
+import * as os from '@/os';
+import { search } from '@/scripts/search';
+
+export default defineComponent({
+	props: {
+		info: {
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			narrow: null,
+			showMenu: false,
+		};
+	},
+
+	mounted() {
+		this.narrow = this.$el.clientWidth < 1300;
+	},
+
+	methods: {
+		signin() {
+			os.popup(XSigninDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		signup() {
+			os.popup(XSignupDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		search
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.sqxihjet {
+	$height: 60px;
+	position: sticky;
+	top: 0;
+	left: 0;
+	z-index: 1000;
+	line-height: $height;
+	-webkit-backdrop-filter: var(--blur, blur(32px));
+	backdrop-filter: var(--blur, blur(32px));
+	background-color: var(--X16);
+
+	> .wide {
+		> .content {
+			max-width: 1400px;
+			margin: 0 auto;
+			display: flex;
+			align-items: center;
+
+			> .link {
+				$line: 3px;
+				display: inline-block;
+				padding: 0 16px;
+				line-height: $height - ($line * 2);
+				border-top: solid $line transparent;
+				border-bottom: solid $line transparent;
+
+				> .icon {
+					margin-right: 0.5em;
+				}
+
+				&.page {
+					border-bottom-color: var(--accent);
+				}
+			}
+
+			> .page {
+				> .title {
+					display: inline-block;
+					vertical-align: bottom;
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					position: relative;
+
+					> .icon + .text {
+						margin-left: 8px;
+					}
+
+					> .avatar {
+						$size: 32px;
+						display: inline-block;
+						width: $size;
+						height: $size;
+						vertical-align: middle;
+						margin-right: 8px;
+						pointer-events: none;
+					}
+
+					&._button {
+						&:hover {
+							color: var(--fgHighlighted);
+						}
+					}
+
+					&.selected {
+						box-shadow: 0 -2px 0 0 var(--accent) inset;
+						color: var(--fgHighlighted);
+					}
+				}
+
+				> .action {
+					padding: 0 0 0 16px;
+				}
+			}
+
+			> .right {
+				margin-left: auto;
+
+				> .search {
+					background: var(--bg);
+					border-radius: 999px;
+					width: 230px;
+					line-height: $height - 20px;
+					margin-right: 16px;
+					text-align: left;
+
+					> * {
+						opacity: 0.7;
+					}
+
+					> .icon {
+						padding: 0 16px;
+					}
+				}
+
+				> .signup {
+					border-radius: 999px;
+					padding: 0 24px;
+					line-height: $height - 20px;
+				}
+
+				> .login {
+					padding: 0 16px;
+				}
+			}
+		}
+	}
+
+	> .narrow {
+		display: flex;
+
+		> .menu,
+		> .action {
+			width: $height;
+			height: $height;
+			font-size: 20px;
+		}
+
+		> .title {
+			flex: 1;
+			white-space: nowrap;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			position: relative;
+			text-align: center;
+
+			> .icon + .text {
+				margin-left: 8px;
+			}
+
+			> .avatar {
+				$size: 32px;
+				display: inline-block;
+				width: $size;
+				height: $size;
+				vertical-align: middle;
+				margin-right: 8px;
+				pointer-events: none;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/visitor/kanban.vue b/packages/client/src/ui/visitor/kanban.vue
new file mode 100644
index 0000000000..97d210d7e0
--- /dev/null
+++ b/packages/client/src/ui/visitor/kanban.vue
@@ -0,0 +1,256 @@
+<template>
+<div class="rwqkcmrc" :style="{ backgroundImage: transparent ? 'none' : `url(${ $instance.backgroundImageUrl })` }">
+	<div class="back" :class="{ transparent }"></div>
+	<div class="contents">
+		<div class="wrapper">
+			<h1 v-if="meta" :class="{ full }">
+				<MkA to="/" class="link"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></MkA>
+			</h1>
+			<template v-if="full">
+				<div class="about" v-if="meta">
+					<div class="desc" v-html="meta.description || $ts.introMisskey"></div>
+				</div>
+				<div class="action">
+					<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
+					<button class="_button" @click="signin()">{{ $ts.login }}</button>
+				</div>
+				<div class="announcements panel">
+					<header>{{ $ts.announcements }}</header>
+					<MkPagination :pagination="announcements" #default="{items}" class="list">
+						<section class="item" v-for="(announcement, i) in items" :key="announcement.id">
+							<div class="title">{{ announcement.title }}</div>
+							<div class="content">
+								<Mfm :text="announcement.text"/>
+								<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
+							</div>
+						</section>
+					</MkPagination>
+				</div>
+				<div class="powered-by" v-if="poweredBy">
+					<b><MkA to="/">{{ host }}</MkA></b>
+					<small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small>
+				</div>
+			</template>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { host, instanceName } from '@/config';
+import * as os from '@/os';
+import MkPagination from '@/components/ui/pagination.vue';
+import XSigninDialog from '@/components/signin-dialog.vue';
+import XSignupDialog from '@/components/signup-dialog.vue';
+import MkButton from '@/components/ui/button.vue';
+
+export default defineComponent({
+	components: {
+		MkPagination,
+		MkButton,
+	},
+
+	props: {
+		full: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		transparent: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+		poweredBy: {
+			type: Boolean,
+			required: false,
+			default: false,
+		},
+	},
+
+	data() {
+		return {
+			host,
+			instanceName,
+			pageInfo: null,
+			meta: null,
+			narrow: window.innerWidth < 1280,
+			announcements: {
+				endpoint: 'announcements',
+				limit: 10,
+			},
+		};
+	},
+
+	created() {
+		os.api('meta', { detail: true }).then(meta => {
+			this.meta = meta;
+		});
+	},
+
+	methods: {
+		signin() {
+			os.popup(XSigninDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		},
+
+		signup() {
+			os.popup(XSignupDialog, {
+				autoSet: true
+			}, {}, 'closed');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.rwqkcmrc {
+	position: relative;
+	text-align: center;
+	background-position: center;
+	background-size: cover;
+	// TODO: パララックスにしたい
+
+	> .back {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		background: rgba(0, 0, 0, 0.3);
+
+		&.transparent {
+			-webkit-backdrop-filter: var(--blur, blur(12px));
+			backdrop-filter: var(--blur, blur(12px));
+		}
+	}
+
+	> .contents {
+		position: relative;
+		z-index: 1;
+		height: inherit;
+		overflow: auto;
+
+		> .wrapper {
+			max-width: 380px;
+			padding: 0 16px;
+			box-sizing: border-box;
+			margin: 0 auto;
+
+			> .panel {
+				-webkit-backdrop-filter: var(--blur, blur(8px));
+				backdrop-filter: var(--blur, blur(8px));
+				background: rgba(0, 0, 0, 0.5);
+				border-radius: var(--radius);
+
+				&, * {
+					color: #fff !important;
+				}
+			}
+
+			> h1 {
+				display: block;
+				margin: 0;
+				padding: 32px 0 32px 0;
+				color: #fff;
+
+				&.full {
+					padding: 64px 0 0 0;
+
+					> .link {
+						> ::v-deep(.logo) {
+							max-height: 130px;
+						}
+					}
+				}
+
+				> .link {
+					display: block;
+
+					> ::v-deep(.logo) {
+						vertical-align: bottom;
+						max-height: 100px;
+					}
+				}
+			}
+
+			> .about {
+				display: block;
+				margin: 24px 0;
+				text-align: center;
+				box-sizing: border-box;
+				text-shadow: 0 0 8px black;
+				color: #fff;
+			}
+
+			> .action {
+				> button {
+					display: block;
+					width: 100%;
+					padding: 10px;
+					box-sizing: border-box;
+					text-align: center;
+					border-radius: 999px;
+
+					&._button {
+						background: var(--panel);
+					}
+
+					&:first-child {
+						margin-bottom: 16px;
+					}
+				}
+			}
+
+			> .announcements {
+				margin: 32px 0;
+				text-align: left;
+
+				> header {
+					padding: 12px 16px;
+					border-bottom: solid 1px rgba(255, 255, 255, 0.5);
+				}
+
+				> .list {
+					max-height: 300px;
+					overflow: auto;
+
+					> .item {
+						padding: 12px 16px;
+
+						& + .item {
+							border-top: solid 1px rgba(255, 255, 255, 0.5);
+						}
+
+						> .title {
+							font-weight: bold;
+						}
+
+						> .content {
+							> img {
+								max-width: 100%;
+							}
+						}
+					}
+				}
+			}
+
+			> .powered-by {
+				padding: 28px;
+				font-size: 14px;
+				text-align: center;
+				border-top: 1px solid rgba(255, 255, 255, 0.5);
+				color: #fff;
+
+				> small {
+					display: block;
+					margin-top: 8px;
+					opacity: 0.5;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/ui/zen.vue b/packages/client/src/ui/zen.vue
new file mode 100644
index 0000000000..7c72232cfd
--- /dev/null
+++ b/packages/client/src/ui/zen.vue
@@ -0,0 +1,106 @@
+<template>
+<div class="mk-app">
+	<div class="contents">
+		<header class="header">
+			<MkHeader :info="pageInfo"/>
+		</header>
+		<main ref="main">
+			<div class="content">
+				<router-view v-slot="{ Component }">
+					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+						<keep-alive :include="['timeline']">
+							<component :is="Component" :ref="changePage"/>
+						</keep-alive>
+					</transition>
+				</router-view>
+			</div>
+		</main>
+	</div>
+
+	<XCommon/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent } from 'vue';
+import { host } from '@/config';
+import XCommon from './_common_/common.vue';
+import * as symbols from '@/symbols';
+
+export default defineComponent({
+	components: {
+		XCommon,
+	},
+
+	data() {
+		return {
+			host: host,
+			pageInfo: null,
+		};
+	},
+
+	created() {
+		document.documentElement.style.overflowY = 'scroll';
+	},
+
+	methods: {
+		changePage(page) {
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+			}
+		},
+
+		top() {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		},
+
+		help() {
+			window.open(`https://misskey-hub.net/docs/keyboard-shortcut.md`, '_blank');
+		},
+
+		onTransition() {
+			if (window._scroll) window._scroll();
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-app {
+	$header-height: 52px;
+	$ui-font-size: 1em; // TODO: どこかに集約したい
+
+	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+	min-height: calc(var(--vh, 1vh) * 100);
+	box-sizing: border-box;
+
+	> .contents {
+		padding-top: $header-height;
+
+		> .header {
+			position: fixed;
+			z-index: 1000;
+			top: 0;
+			height: $header-height;
+			width: 100%;
+			line-height: $header-height;
+			text-align: center;
+			//background-color: var(--panel);
+			-webkit-backdrop-filter: var(--blur, blur(32px));
+			backdrop-filter: var(--blur, blur(32px));
+			background-color: var(--header);
+			border-bottom: solid 0.5px var(--divider);
+		}
+
+		> main {
+			> .content {
+				> * {
+					// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
+					min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/activity.calendar.vue b/packages/client/src/widgets/activity.calendar.vue
new file mode 100644
index 0000000000..b833bd65ca
--- /dev/null
+++ b/packages/client/src/widgets/activity.calendar.vue
@@ -0,0 +1,85 @@
+<template>
+<svg viewBox="0 0 21 7">
+	<rect v-for="record in data" class="day"
+		width="1" height="1"
+		:x="record.x" :y="record.date.weekday"
+		rx="1" ry="1"
+		fill="transparent">
+		<title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title>
+	</rect>
+	<rect v-for="record in data" class="day"
+		:width="record.v" :height="record.v"
+		:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
+		rx="1" ry="1"
+		:fill="record.color"
+		style="pointer-events: none;"/>
+	<rect class="today"
+		width="1" height="1"
+		:x="data[0].x" :y="data[0].date.weekday"
+		rx="1" ry="1"
+		fill="none"
+		stroke-width="0.1"
+		stroke="#f73520"/>
+</svg>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+export default defineComponent({
+	props: ['data'],
+	created() {
+		for (const d of this.data) {
+			d.total = d.notes + d.replies + d.renotes;
+		}
+		const peak = Math.max.apply(null, this.data.map(d => d.total));
+
+		const now = new Date();
+		const year = now.getFullYear();
+		const month = now.getMonth();
+		const day = now.getDate();
+
+		let x = 20;
+		this.data.slice().forEach((d, i) => {
+			d.x = x;
+
+			const date = new Date(year, month, day - i);
+			d.date = {
+				year: date.getFullYear(),
+				month: date.getMonth(),
+				day: date.getDate(),
+				weekday: date.getDay()
+			};
+
+			d.v = peak === 0 ? 0 : d.total / (peak / 2);
+			if (d.v > 1) d.v = 1;
+			const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170;
+			const cs = d.v * 100;
+			const cl = 15 + ((1 - d.v) * 80);
+			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
+
+			if (d.date.weekday === 0) x--;
+		});
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+svg {
+	display: block;
+	padding: 16px;
+	width: 100%;
+	box-sizing: border-box;
+
+	> rect {
+		transform-origin: center;
+
+		&.day {
+			&:hover {
+				fill: rgba(#000, 0.05);
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/activity.chart.vue b/packages/client/src/widgets/activity.chart.vue
new file mode 100644
index 0000000000..9702d66663
--- /dev/null
+++ b/packages/client/src/widgets/activity.chart.vue
@@ -0,0 +1,107 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
+	<polyline
+		:points="pointsNote"
+		fill="none"
+		stroke-width="1"
+		stroke="#41ddde"/>
+	<polyline
+		:points="pointsReply"
+		fill="none"
+		stroke-width="1"
+		stroke="#f7796c"/>
+	<polyline
+		:points="pointsRenote"
+		fill="none"
+		stroke-width="1"
+		stroke="#a1de41"/>
+	<polyline
+		:points="pointsTotal"
+		fill="none"
+		stroke-width="1"
+		stroke="#555"
+		stroke-dasharray="2 2"/>
+</svg>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@/os';
+
+function dragListen(fn) {
+	window.addEventListener('mousemove',  fn);
+	window.addEventListener('mouseleave', dragClear.bind(null, fn));
+	window.addEventListener('mouseup',    dragClear.bind(null, fn));
+}
+
+function dragClear(fn) {
+	window.removeEventListener('mousemove',  fn);
+	window.removeEventListener('mouseleave', dragClear);
+	window.removeEventListener('mouseup',    dragClear);
+}
+
+export default defineComponent({
+	props: ['data'],
+	data() {
+		return {
+			viewBoxX: 147,
+			viewBoxY: 60,
+			zoom: 1,
+			pos: 0,
+			pointsNote: null,
+			pointsReply: null,
+			pointsRenote: null,
+			pointsTotal: null
+		};
+	},
+	created() {
+		for (const d of this.data) {
+			d.total = d.notes + d.replies + d.renotes;
+		}
+
+		this.render();
+	},
+	methods: {
+		render() {
+			const peak = Math.max.apply(null, this.data.map(d => d.total));
+			if (peak != 0) {
+				const data = this.data.slice().reverse();
+				this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
+				this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+			}
+		},
+		onMousedown(e) {
+			const clickX = e.clientX;
+			const clickY = e.clientY;
+			const baseZoom = this.zoom;
+			const basePos = this.pos;
+
+			// 動かした時
+			dragListen(me => {
+				let moveLeft = me.clientX - clickX;
+				let moveTop = me.clientY - clickY;
+
+				this.zoom = baseZoom + (-moveTop / 20);
+				this.pos = basePos + moveLeft;
+				if (this.zoom < 1) this.zoom = 1;
+				if (this.pos > 0) this.pos = 0;
+				if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
+
+				this.render();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+svg {
+	display: block;
+	padding: 16px;
+	width: 100%;
+	box-sizing: border-box;
+	cursor: all-scroll;
+}
+</style>
diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue
new file mode 100644
index 0000000000..eaac1455ad
--- /dev/null
+++ b/packages/client/src/widgets/activity.vue
@@ -0,0 +1,82 @@
+<template>
+<MkContainer :show-header="props.showHeader" :naked="props.transparent">
+	<template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template>
+	<template #func><button @click="toggleView()" class="_button"><i class="fas fa-sort"></i></button></template>
+
+	<div>
+		<MkLoading v-if="fetching"/>
+		<template v-else>
+			<XCalendar v-show="props.view === 0" :data="[].concat(activity)"/>
+			<XChart v-show="props.view === 1" :data="[].concat(activity)"/>
+		</template>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import define from './define';
+import XCalendar from './activity.calendar.vue';
+import XChart from './activity.chart.vue';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'activity',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+		transparent: {
+			type: 'boolean',
+			default: false,
+		},
+		view: {
+			type: 'number',
+			default: 0,
+			hidden: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer,
+		XCalendar,
+		XChart,
+	},
+	data() {
+		return {
+			fetching: true,
+			activity: null,
+		};
+	},
+	mounted() {
+		os.api('charts/user/notes', {
+			userId: this.$i.id,
+			span: 'day',
+			limit: 7 * 21
+		}).then(activity => {
+			this.activity = activity.diffs.normal.map((_, i) => ({
+				total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i],
+				notes: activity.diffs.normal[i],
+				replies: activity.diffs.reply[i],
+				renotes: activity.diffs.renote[i]
+			}));
+			this.fetching = false;
+		});
+	},
+	methods: {
+		toggleView() {
+			if (this.props.view === 1) {
+				this.props.view = 0;
+			} else {
+				this.props.view++;
+			}
+			this.save();
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/widgets/aichan.vue b/packages/client/src/widgets/aichan.vue
new file mode 100644
index 0000000000..8196bff8fa
--- /dev/null
+++ b/packages/client/src/widgets/aichan.vue
@@ -0,0 +1,59 @@
+<template>
+<MkContainer :naked="props.transparent" :show-header="false">
+	<iframe class="dedjhjmo" ref="live2d" @click="touched" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100"></iframe>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import define from './define';
+import MkContainer from '@/components/ui/container.vue';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'ai',
+	props: () => ({
+		transparent: {
+			type: 'boolean',
+			default: false,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer,
+	},
+	data() {
+		return {
+		};
+	},
+	mounted() {
+		window.addEventListener('mousemove', ev => {
+			const iframeRect = this.$refs.live2d.getBoundingClientRect();
+			this.$refs.live2d.contentWindow.postMessage({
+				type: 'moveCursor',
+				body: {
+					x: ev.clientX - iframeRect.left,
+					y: ev.clientY - iframeRect.top,
+				}
+			}, '*');
+		}, { passive: true });
+	},
+	methods: {
+		touched() {
+			//if (this.live2d) this.live2d.changeExpression('gurugurume');
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.dedjhjmo {
+	width: 100%;
+	height: 350px;
+	border: none;
+	pointer-events: none;
+}
+</style>
diff --git a/packages/client/src/widgets/aiscript.vue b/packages/client/src/widgets/aiscript.vue
new file mode 100644
index 0000000000..992ec2f8a1
--- /dev/null
+++ b/packages/client/src/widgets/aiscript.vue
@@ -0,0 +1,163 @@
+<template>
+<MkContainer :show-header="props.showHeader">
+	<template #header><i class="fas fa-terminal"></i>{{ $ts._widgets.aiscript }}</template>
+
+	<div class="uylguesu _monospace">
+		<textarea v-model="props.script" placeholder="(1 + 1)"></textarea>
+		<button @click="run" class="_buttonPrimary">RUN</button>
+		<div class="logs">
+			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div>
+		</div>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import define from './define';
+import * as os from '@/os';
+import { AiScript, parse, utils } from '@syuilo/aiscript';
+import { createAiScriptEnv } from '@/scripts/aiscript/api';
+
+const widget = define({
+	name: 'aiscript',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+		script: {
+			type: 'string',
+			multiline: true,
+			default: '(1 + 1)',
+			hidden: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer
+	},
+
+	data() {
+		return {
+			logs: [],
+		};
+	},
+
+	methods: {
+		async run() {
+			this.logs = [];
+			const aiscript = new AiScript(createAiScriptEnv({
+				storageKey: 'widget',
+				token: this.$i?.token,
+			}), {
+				in: (q) => {
+					return new Promise(ok => {
+						os.dialog({
+							title: q,
+							input: {}
+						}).then(({ canceled, result: a }) => {
+							ok(a);
+						});
+					});
+				},
+				out: (value) => {
+					this.logs.push({
+						id: Math.random(),
+						text: value.type === 'str' ? value.value : utils.valToString(value),
+						print: true
+					});
+				},
+				log: (type, params) => {
+					switch (type) {
+						case 'end': this.logs.push({
+							id: Math.random(),
+							text: utils.valToString(params.val, true),
+							print: false
+						}); break;
+						default: break;
+					}
+				}
+			});
+
+			let ast;
+			try {
+				ast = parse(this.props.script);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: 'Syntax error :('
+				});
+				return;
+			}
+			try {
+				await aiscript.exec(ast);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: e
+				});
+			}
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.uylguesu {
+	text-align: right;
+
+	> textarea {
+		display: block;
+		width: 100%;
+		max-width: 100%;
+		min-width: 100%;
+		padding: 16px;
+		color: var(--fg);
+		background: transparent;
+		border: none;
+		border-bottom: solid 0.5px var(--divider);
+		border-radius: 0;
+		box-sizing: border-box;
+		font: inherit;
+
+		&:focus-visible {
+			outline: none;
+		}
+	}
+
+	> button {
+		display: inline-block;
+		margin: 8px;
+		padding: 0 10px;
+		height: 28px;
+		outline: none;
+		border-radius: 4px;
+
+		&:disabled {
+			opacity: 0.7;
+			cursor: default;
+		}
+	}
+
+	> .logs {
+		border-top: solid 0.5px var(--divider);
+		text-align: left;
+		padding: 16px;
+
+		&:empty {
+			display: none;
+		}
+
+		> .log {
+			&:not(.print) {
+				opacity: 0.7;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/button.vue b/packages/client/src/widgets/button.vue
new file mode 100644
index 0000000000..3417181d0c
--- /dev/null
+++ b/packages/client/src/widgets/button.vue
@@ -0,0 +1,95 @@
+<template>
+<div class="mkw-button">
+	<MkButton :primary="props.colored" full @click="run">
+		{{ props.label }}
+	</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import define from './define';
+import MkButton from '@/components/ui/button.vue';
+import * as os from '@/os';
+import { AiScript, parse, utils } from '@syuilo/aiscript';
+import { createAiScriptEnv } from '@/scripts/aiscript/api';
+
+const widget = define({
+	name: 'button',
+	props: () => ({
+		label: {
+			type: 'string',
+			default: 'BUTTON',
+		},
+		colored: {
+			type: 'boolean',
+			default: true,
+		},
+		script: {
+			type: 'string',
+			multiline: true,
+			default: 'Mk:dialog("hello" "world")',
+		},
+	})
+});
+
+export default defineComponent({
+	components: {
+		MkButton
+	},
+	extends: widget,
+	data() {
+		return {
+		};
+	},
+	methods: {
+		async run() {
+			const aiscript = new AiScript(createAiScriptEnv({
+				storageKey: 'widget',
+				token: this.$i?.token,
+			}), {
+				in: (q) => {
+					return new Promise(ok => {
+						os.dialog({
+							title: q,
+							input: {}
+						}).then(({ canceled, result: a }) => {
+							ok(a);
+						});
+					});
+				},
+				out: (value) => {
+					// nop
+				},
+				log: (type, params) => {
+					// nop
+				}
+			});
+
+			let ast;
+			try {
+				ast = parse(this.props.script);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: 'Syntax error :('
+				});
+				return;
+			}
+			try {
+				await aiscript.exec(ast);
+			} catch (e) {
+				os.dialog({
+					type: 'error',
+					text: e
+				});
+			}
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mkw-button {
+}
+</style>
diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue
new file mode 100644
index 0000000000..545072e87b
--- /dev/null
+++ b/packages/client/src/widgets/calendar.vue
@@ -0,0 +1,204 @@
+<template>
+<div class="mkw-calendar" :class="{ _panel: !props.transparent }">
+	<div class="calendar" :class="{ isHoliday }">
+		<p class="month-and-year">
+			<span class="year">{{ $t('yearX', { year }) }}</span>
+			<span class="month">{{ $t('monthX', { month }) }}</span>
+		</p>
+		<p class="day">{{ $t('dayX', { day }) }}</p>
+		<p class="week-day">{{ weekDay }}</p>
+	</div>
+	<div class="info">
+		<div>
+			<p>{{ $ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${dayP}%` }"></div>
+			</div>
+		</div>
+		<div>
+			<p>{{ $ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${monthP}%` }"></div>
+			</div>
+		</div>
+		<div>
+			<p>{{ $ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p>
+			<div class="meter">
+				<div class="val" :style="{ width: `${yearP}%` }"></div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import define from './define';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'calendar',
+	props: () => ({
+		transparent: {
+			type: 'boolean',
+			default: false,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	data() {
+		return {
+			now: new Date(),
+			year: null,
+			month: null,
+			day: null,
+			weekDay: null,
+			yearP: null,
+			dayP: null,
+			monthP: null,
+			isHoliday: null,
+			clock: null
+		};
+	},
+	created() {
+		this.tick();
+		this.clock = setInterval(this.tick, 1000);
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		tick() {
+			const now = new Date();
+			const nd = now.getDate();
+			const nm = now.getMonth();
+			const ny = now.getFullYear();
+
+			this.year = ny;
+			this.month = nm + 1;
+			this.day = nd;
+			this.weekDay = [
+				this.$ts._weekday.sunday,
+				this.$ts._weekday.monday,
+				this.$ts._weekday.tuesday,
+				this.$ts._weekday.wednesday,
+				this.$ts._weekday.thursday,
+				this.$ts._weekday.friday,
+				this.$ts._weekday.saturday
+			][now.getDay()];
+
+			const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime();
+			const dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
+			const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
+			const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
+			const yearNumer  = now.getTime() - new Date(ny, 0, 1).getTime();
+			const yearDenom  = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
+
+			this.dayP   = dayNumer   / dayDenom   * 100;
+			this.monthP = monthNumer / monthDenom * 100;
+			this.yearP  = yearNumer  / yearDenom  * 100;
+
+			this.isHoliday = now.getDay() === 0 || now.getDay() === 6;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mkw-calendar {
+	padding: 16px 0;
+
+	&:after {
+		content: "";
+		display: block;
+		clear: both;
+	}
+
+	> .calendar {
+		float: left;
+		width: 60%;
+		text-align: center;
+
+		&.isHoliday {
+			> .day {
+				color: #ef95a0;
+			}
+		}
+
+		> p {
+			margin: 0;
+			line-height: 18px;
+			font-size: 0.9em;
+
+			> span {
+				margin: 0 4px;
+			}
+		}
+
+		> .day {
+			margin: 10px 0;
+			line-height: 32px;
+			font-size: 1.75em;
+		}
+	}
+
+	> .info {
+		display: block;
+		float: left;
+		width: 40%;
+		padding: 0 16px 0 0;
+		box-sizing: border-box;
+
+		> div {
+			margin-bottom: 8px;
+
+			&:last-child {
+				margin-bottom: 4px;
+			}
+
+			> p {
+				margin: 0 0 2px 0;
+				font-size: 0.75em;
+				line-height: 18px;
+				opacity: 0.8;
+
+				> b {
+					margin-left: 2px;
+				}
+			}
+
+			> .meter {
+				width: 100%;
+				overflow: hidden;
+				background: var(--X11);
+				border-radius: 8px;
+
+				> .val {
+					height: 4px;
+					transition: width .3s cubic-bezier(0.23, 1, 0.32, 1);
+				}
+			}
+
+			&:nth-child(1) {
+				> .meter > .val {
+					background: #f7796c;
+				}
+			}
+
+			&:nth-child(2) {
+				> .meter > .val {
+					background: #a1de41;
+				}
+			}
+
+			&:nth-child(3) {
+				> .meter > .val {
+					background: #41ddde;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/clock.vue b/packages/client/src/widgets/clock.vue
new file mode 100644
index 0000000000..da0cd65c96
--- /dev/null
+++ b/packages/client/src/widgets/clock.vue
@@ -0,0 +1,55 @@
+<template>
+<MkContainer :naked="props.transparent" :show-header="false">
+	<div class="vubelbmv">
+		<MkAnalogClock class="clock" :thickness="props.thickness"/>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import define from './define';
+import MkContainer from '@/components/ui/container.vue';
+import MkAnalogClock from '@/components/analog-clock.vue';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'clock',
+	props: () => ({
+		transparent: {
+			type: 'boolean',
+			default: false,
+		},
+		thickness: {
+			type: 'radio',
+			default: 0.1,
+			options: [{
+				value: 0.1, label: 'thin'
+			}, {
+				value: 0.2, label: 'medium'
+			}, {
+				value: 0.3, label: 'thick'
+			}]
+		}
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer,
+		MkAnalogClock
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.vubelbmv {
+	padding: 8px;
+
+	> .clock {
+		height: 150px;
+		margin: auto;
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/define.ts b/packages/client/src/widgets/define.ts
new file mode 100644
index 0000000000..08a346d97c
--- /dev/null
+++ b/packages/client/src/widgets/define.ts
@@ -0,0 +1,75 @@
+import { defineComponent } from 'vue';
+import { throttle } from 'throttle-debounce';
+import { Form } from '@/scripts/form';
+import * as os from '@/os';
+
+export default function <T extends Form>(data: {
+	name: string;
+	props?: () => T;
+}) {
+	return defineComponent({
+		props: {
+			widget: {
+				type: Object,
+				required: false
+			},
+			settingCallback: {
+				required: false
+			}
+		},
+
+		emits: ['updateProps'],
+
+		data() {
+			return {
+				props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {},
+				save: throttle(3000, () => {
+					this.$emit('updateProps', this.props);
+				}),
+			};
+		},
+
+		computed: {
+			id(): string {
+				return this.widget ? this.widget.id : null;
+			},
+		},
+
+		created() {
+			this.mergeProps();
+
+			this.$watch('props', () => {
+				this.mergeProps();
+			}, { deep: true });
+
+			if (this.settingCallback) this.settingCallback(this.setting);
+		},
+
+		methods: {
+			mergeProps() {
+				if (data.props) {
+					const defaultProps = data.props();
+					for (const prop of Object.keys(defaultProps)) {
+						if (this.props.hasOwnProperty(prop)) continue;
+						this.props[prop] = defaultProps[prop].default;
+					}
+				}
+			},
+
+			async setting() {
+				const form = data.props();
+				for (const item of Object.keys(form)) {
+					form[item].default = this.props[item];
+				}
+				const { canceled, result } = await os.form(data.name, form);
+				if (canceled) return;
+
+				for (const key of Object.keys(result)) {
+					this.props[key] = result[key];
+				}
+
+				this.save();
+			},
+		}
+	});
+}
diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue
new file mode 100644
index 0000000000..9d32e8b9fe
--- /dev/null
+++ b/packages/client/src/widgets/digital-clock.vue
@@ -0,0 +1,79 @@
+<template>
+<div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
+	<span>
+		<span v-text="hh"></span>
+		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
+		<span v-text="mm"></span>
+		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
+		<span v-text="ss"></span>
+		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }" v-if="props.showMs">:</span>
+		<span v-text="ms" v-if="props.showMs"></span>
+	</span>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import define from './define';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'digitalClock',
+	props: () => ({
+		transparent: {
+			type: 'boolean',
+			default: false,
+		},
+		fontSize: {
+			type: 'number',
+			default: 1.5,
+			step: 0.1,
+		},
+		showMs: {
+			type: 'boolean',
+			default: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	data() {
+		return {
+			clock: null,
+			hh: null,
+			mm: null,
+			ss: null,
+			ms: null,
+			showColon: true,
+		};
+	},
+	created() {
+		this.tick();
+		this.$watch(() => this.props.showMs, () => {
+			if (this.clock) clearInterval(this.clock);
+			this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000);
+		}, { immediate: true });
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		tick() {
+			const now = new Date();
+			this.hh = now.getHours().toString().padStart(2, '0');
+			this.mm = now.getMinutes().toString().padStart(2, '0');
+			this.ss = now.getSeconds().toString().padStart(2, '0');
+			this.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
+			this.showColon = now.getSeconds() % 2 === 0;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mkw-digitalClock {
+	padding: 16px 0;
+	text-align: center;
+}
+</style>
diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue
new file mode 100644
index 0000000000..85cfb8b845
--- /dev/null
+++ b/packages/client/src/widgets/federation.vue
@@ -0,0 +1,145 @@
+<template>
+<MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable">
+	<template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template>
+
+	<div class="wbrkwalb">
+		<MkLoading v-if="fetching"/>
+		<transition-group tag="div" name="chart" class="instances" v-else>
+			<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
+				<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
+				<div class="body">
+					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
+					<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
+				</div>
+				<MkMiniChart class="chart" :src="charts[i].requests.received"/>
+			</div>
+		</transition-group>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import define from './define';
+import MkMiniChart from '@/components/mini-chart.vue';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'federation',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer, MkMiniChart
+	},
+	props: {
+		foldable: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+		scrollable: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+	},
+	data() {
+		return {
+			instances: [],
+			charts: [],
+			fetching: true,
+		};
+	},
+	mounted() {
+		this.fetch();
+		this.clock = setInterval(this.fetch, 1000 * 60);
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		async fetch() {
+			const instances = await os.api('federation/instances', {
+				sort: '+lastCommunicatedAt',
+				limit: 5
+			});
+			const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
+			this.instances = instances;
+			this.charts = charts;
+			this.fetching = false;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wbrkwalb {
+	$bodyTitleHieght: 18px;
+	$bodyInfoHieght: 16px;
+
+	height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
+	overflow: hidden;
+
+	> .instances {
+		.chart-move {
+			transition: transform 1s ease;
+		}
+
+		> .instance {
+			display: flex;
+			align-items: center;
+			padding: 14px 16px;
+			border-bottom: solid 0.5px var(--divider);
+
+			> img {
+				display: block;
+				width: ($bodyTitleHieght + $bodyInfoHieght);
+				height: ($bodyTitleHieght + $bodyInfoHieght);
+				object-fit: cover;
+				border-radius: 4px;
+				margin-right: 8px;
+			}
+
+			> .body {
+				flex: 1;
+				overflow: hidden;
+				font-size: 0.9em;
+				color: var(--fg);
+				padding-right: 8px;
+
+				> .a {
+					display: block;
+					width: 100%;
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					line-height: $bodyTitleHieght;
+				}
+
+				> p {
+					margin: 0;
+					font-size: 75%;
+					opacity: 0.7;
+					line-height: $bodyInfoHieght;
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+				}
+			}
+
+			> .chart {
+				height: 30px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/index.ts b/packages/client/src/widgets/index.ts
new file mode 100644
index 0000000000..51a82af080
--- /dev/null
+++ b/packages/client/src/widgets/index.ts
@@ -0,0 +1,45 @@
+import { App, defineAsyncComponent } from 'vue';
+
+export default function(app: App) {
+	app.component('MkwMemo', defineAsyncComponent(() => import('./memo.vue')));
+	app.component('MkwNotifications', defineAsyncComponent(() => import('./notifications.vue')));
+	app.component('MkwTimeline', defineAsyncComponent(() => import('./timeline.vue')));
+	app.component('MkwCalendar', defineAsyncComponent(() => import('./calendar.vue')));
+	app.component('MkwRss', defineAsyncComponent(() => import('./rss.vue')));
+	app.component('MkwTrends', defineAsyncComponent(() => import('./trends.vue')));
+	app.component('MkwClock', defineAsyncComponent(() => import('./clock.vue')));
+	app.component('MkwActivity', defineAsyncComponent(() => import('./activity.vue')));
+	app.component('MkwPhotos', defineAsyncComponent(() => import('./photos.vue')));
+	app.component('MkwDigitalClock', defineAsyncComponent(() => import('./digital-clock.vue')));
+	app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue')));
+	app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue')));
+	app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue')));
+	app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue')));
+	app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
+	app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue')));
+	app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
+	app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
+	app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
+}
+
+export const widgets = [
+	'memo',
+	'notifications',
+	'timeline',
+	'calendar',
+	'rss',
+	'trends',
+	'clock',
+	'activity',
+	'photos',
+	'digitalClock',
+	'federation',
+	'postForm',
+	'slideshow',
+	'serverMetric',
+	'onlineUsers',
+	'jobQueue',
+	'button',
+	'aiscript',
+	'aichan',
+];
diff --git a/packages/client/src/widgets/job-queue.vue b/packages/client/src/widgets/job-queue.vue
new file mode 100644
index 0000000000..ef440881e5
--- /dev/null
+++ b/packages/client/src/widgets/job-queue.vue
@@ -0,0 +1,183 @@
+<template>
+<div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }">
+	<div class="inbox">
+		<div class="label">Inbox queue<i v-if="inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
+		<div class="values">
+			<div>
+				<div>Process</div>
+				<div :class="{ inc: inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(inbox.activeSincePrevTick) }}</div>
+			</div>
+			<div>
+				<div>Active</div>
+				<div :class="{ inc: inbox.active > prev.inbox.active, dec: inbox.active < prev.inbox.active }">{{ number(inbox.active) }}</div>
+			</div>
+			<div>
+				<div>Delayed</div>
+				<div :class="{ inc: inbox.delayed > prev.inbox.delayed, dec: inbox.delayed < prev.inbox.delayed }">{{ number(inbox.delayed) }}</div>
+			</div>
+			<div>
+				<div>Waiting</div>
+				<div :class="{ inc: inbox.waiting > prev.inbox.waiting, dec: inbox.waiting < prev.inbox.waiting }">{{ number(inbox.waiting) }}</div>
+			</div>
+		</div>
+	</div>
+	<div class="deliver">
+		<div class="label">Deliver queue<i v-if="deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
+		<div class="values">
+			<div>
+				<div>Process</div>
+				<div :class="{ inc: deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(deliver.activeSincePrevTick) }}</div>
+			</div>
+			<div>
+				<div>Active</div>
+				<div :class="{ inc: deliver.active > prev.deliver.active, dec: deliver.active < prev.deliver.active }">{{ number(deliver.active) }}</div>
+			</div>
+			<div>
+				<div>Delayed</div>
+				<div :class="{ inc: deliver.delayed > prev.deliver.delayed, dec: deliver.delayed < prev.deliver.delayed }">{{ number(deliver.delayed) }}</div>
+			</div>
+			<div>
+				<div>Waiting</div>
+				<div :class="{ inc: deliver.waiting > prev.deliver.waiting, dec: deliver.waiting < prev.deliver.waiting }">{{ number(deliver.waiting) }}</div>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import define from './define';
+import * as os from '@/os';
+import number from '@/filters/number';
+import * as sound from '@/scripts/sound';
+
+const widget = define({
+	name: 'jobQueue',
+	props: () => ({
+		transparent: {
+			type: 'boolean',
+			default: false,
+		},
+		sound: {
+			type: 'boolean',
+			default: false,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	data() {
+		return {
+			connection: markRaw(os.stream.useChannel('queueStats')),
+			inbox: {
+				activeSincePrevTick: 0,
+				active: 0,
+				waiting: 0,
+				delayed: 0,
+			},
+			deliver: {
+				activeSincePrevTick: 0,
+				active: 0,
+				waiting: 0,
+				delayed: 0,
+			},
+			prev: {},
+			sound: sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1)
+		};
+	},
+	created() {
+		for (const domain of ['inbox', 'deliver']) {
+			this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
+		}
+	
+		this.connection.on('stats', this.onStats);
+		this.connection.on('statsLog', this.onStatsLog);
+
+		this.connection.send('requestLog', {
+			id: Math.random().toString().substr(2, 8),
+			length: 1
+		});
+	},
+	beforeUnmount() {
+		this.connection.off('stats', this.onStats);
+		this.connection.off('statsLog', this.onStatsLog);
+		this.connection.dispose();
+	},
+	methods: {
+		onStats(stats) {
+			for (const domain of ['inbox', 'deliver']) {
+				this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
+				this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
+				this[domain].active = stats[domain].active;
+				this[domain].waiting = stats[domain].waiting;
+				this[domain].delayed = stats[domain].delayed;
+
+				if (this[domain].waiting > 0 && this.props.sound && this.sound.paused) {
+					this.sound.play();
+				}
+			}
+		},
+
+		onStatsLog(statsLog) {
+			for (const stats of [...statsLog].reverse()) {
+				this.onStats(stats);
+			}
+		},
+
+		number
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+@keyframes warnBlink {
+	0% { opacity: 1; }
+	50% { opacity: 0; }
+}
+
+.mkw-jobQueue {
+	font-size: 0.9em;
+
+	> div {
+		padding: 16px;
+
+		&:not(:first-child) {
+			border-top: solid 0.5px var(--divider);
+		}
+
+		> .label {
+			display: flex;
+
+			> .icon {
+				color: var(--warn);
+				margin-left: auto;
+				animation: warnBlink 1s infinite;
+			}
+		}
+
+		> .values {
+			display: flex;
+
+			> div {
+				flex: 1;
+
+				> div:first-child {
+					opacity: 0.7;
+				}
+
+				> div:last-child {
+					&.inc {
+						color: var(--warn);
+					}
+
+					&.dec {
+						color: var(--success);
+					}
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue
new file mode 100644
index 0000000000..100f1b2934
--- /dev/null
+++ b/packages/client/src/widgets/memo.vue
@@ -0,0 +1,106 @@
+<template>
+<MkContainer :show-header="props.showHeader">
+	<template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template>
+
+	<div class="otgbylcu">
+		<textarea v-model="text" :placeholder="$ts.placeholder" @input="onChange"></textarea>
+		<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $ts.save }}</button>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import define from './define';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'memo',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer
+	},
+
+	data() {
+		return {
+			text: null,
+			changed: false,
+			timeoutId: null,
+		};
+	},
+
+	created() {
+		this.text = this.$store.state.memo;
+
+		this.$watch(() => this.$store.reactiveState.memo, text => {
+			this.text = text;
+		});
+	},
+
+	methods: {
+		onChange() {
+			this.changed = true;
+			clearTimeout(this.timeoutId);
+			this.timeoutId = setTimeout(this.saveMemo, 1000);
+		},
+
+		saveMemo() {
+			this.$store.set('memo', this.text);
+			this.changed = false;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.otgbylcu {
+	padding-bottom: 28px + 16px;
+
+	> textarea {
+		display: block;
+		width: 100%;
+		max-width: 100%;
+		min-width: 100%;
+		padding: 16px;
+		color: var(--fg);
+		background: transparent;
+		border: none;
+		border-bottom: solid 0.5px var(--divider);
+		border-radius: 0;
+		box-sizing: border-box;
+		font: inherit;
+		font-size: 0.9em;
+
+		&:focus-visible {
+			outline: none;
+		}
+	}
+
+	> button {
+		display: block;
+		position: absolute;
+		bottom: 8px;
+		right: 8px;
+		margin: 0;
+		padding: 0 10px;
+		height: 28px;
+		outline: none;
+		border-radius: 4px;
+
+		&:disabled {
+			opacity: 0.7;
+			cursor: default;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/notifications.vue b/packages/client/src/widgets/notifications.vue
new file mode 100644
index 0000000000..462f39a339
--- /dev/null
+++ b/packages/client/src/widgets/notifications.vue
@@ -0,0 +1,65 @@
+<template>
+<MkContainer :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
+	<template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template>
+	<template #func><button @click="configure()" class="_button"><i class="fas fa-cog"></i></button></template>
+
+	<div>
+		<XNotifications :include-types="props.includingTypes"/>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import XNotifications from '@/components/notifications.vue';
+import define from './define';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'notifications',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+		height: {
+			type: 'number',
+			default: 300,
+		},
+		includingTypes: {
+			type: 'array',
+			hidden: true,
+			default: null,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+
+	components: {
+		MkContainer,
+		XNotifications,
+	},
+
+	data() {
+		return {
+		};
+	},
+
+	methods: {
+		configure() {
+			os.popup(import('@/components/notification-setting-window.vue'), {
+				includingTypes: this.props.includingTypes,
+			}, {
+				done: async (res) => {
+					const { includingTypes } = res;
+					this.props.includingTypes = includingTypes;
+					this.save();
+				}
+			}, 'closed');
+		}
+	}
+});
+</script>
diff --git a/packages/client/src/widgets/online-users.vue b/packages/client/src/widgets/online-users.vue
new file mode 100644
index 0000000000..5b889f4816
--- /dev/null
+++ b/packages/client/src/widgets/online-users.vue
@@ -0,0 +1,67 @@
+<template>
+<div class="mkw-onlineUsers" :class="{ _panel: !props.transparent, pad: !props.transparent }">
+	<I18n v-if="onlineUsersCount" :src="$ts.onlineUsersCount" text-tag="span" class="text">
+		<template #n><b>{{ onlineUsersCount }}</b></template>
+	</I18n>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import define from './define';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'onlineUsers',
+	props: () => ({
+		transparent: {
+			type: 'boolean',
+			default: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	data() {
+		return {
+			onlineUsersCount: null,
+			clock: null,
+		};
+	},
+	created() {
+		this.tick();
+		this.clock = setInterval(this.tick, 1000 * 15);
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		tick() {
+			os.api('get-online-users-count').then(res => {
+				this.onlineUsersCount = res.count;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mkw-onlineUsers {
+	text-align: center;
+
+	&.pad {
+		padding: 16px 0;
+	}
+
+	> .text {
+		::v-deep(b) {
+			color: #41b781;
+		}
+
+		::v-deep(span) {
+			opacity: 0.7;
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/photos.vue b/packages/client/src/widgets/photos.vue
new file mode 100644
index 0000000000..0c919526b0
--- /dev/null
+++ b/packages/client/src/widgets/photos.vue
@@ -0,0 +1,113 @@
+<template>
+<MkContainer :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent ? true : null">
+	<template #header><i class="fas fa-camera"></i>{{ $ts._widgets.photos }}</template>
+
+	<div class="">
+		<MkLoading v-if="fetching"/>
+		<div v-else :class="$style.stream">
+			<div v-for="(image, i) in images" :key="i"
+				:class="$style.img"
+				:style="`background-image: url(${thumbnail(image)})`"
+			></div>
+		</div>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import define from './define';
+import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'photos',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+		transparent: {
+			type: 'boolean',
+			default: false,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer,
+	},
+	data() {
+		return {
+			images: [],
+			fetching: true,
+			connection: null,
+		};
+	},
+	mounted() {
+		this.connection = markRaw(os.stream.useChannel('main'));
+
+		this.connection.on('driveFileCreated', this.onDriveFileCreated);
+
+		os.api('drive/stream', {
+			type: 'image/*',
+			limit: 9
+		}).then(images => {
+			this.images = images;
+			this.fetching = false;
+		});
+	},
+	beforeUnmount() {
+		this.connection.dispose();
+	},
+	methods: {
+		onDriveFileCreated(file) {
+			if (/^image\/.+$/.test(file.type)) {
+				this.images.unshift(file);
+				if (this.images.length > 9) this.images.pop();
+			}
+		},
+
+		thumbnail(image: any): string {
+			return this.$store.state.disableShowingAnimatedImages
+				? getStaticImageUrl(image.thumbnailUrl)
+				: image.thumbnailUrl;
+		},
+	}
+});
+</script>
+
+<style lang="scss" module>
+.root[data-transparent] {
+	.stream {
+		padding: 0;
+	}
+
+	.img {
+		border: solid 4px transparent;
+		border-radius: 8px;
+	}
+}
+
+.stream {
+	display: flex;
+	justify-content: center;
+	flex-wrap: wrap;
+	padding: 8px;
+
+	.img {
+		flex: 1 1 33%;
+		width: 33%;
+		height: 80px;
+		box-sizing: border-box;
+		background-position: center center;
+		background-size: cover;
+		background-clip: content-box;
+		border: solid 2px transparent;
+		border-radius: 4px;
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/post-form.vue b/packages/client/src/widgets/post-form.vue
new file mode 100644
index 0000000000..5ecaa67b5a
--- /dev/null
+++ b/packages/client/src/widgets/post-form.vue
@@ -0,0 +1,23 @@
+<template>
+<XPostForm class="_panel" :fixed="true" :autofocus="false"/>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XPostForm from '@/components/post-form.vue';
+import define from './define';
+
+const widget = define({
+	name: 'postForm',
+	props: () => ({
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+
+	components: {
+		XPostForm,
+	},
+});
+</script>
diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue
new file mode 100644
index 0000000000..235fce574a
--- /dev/null
+++ b/packages/client/src/widgets/rss.vue
@@ -0,0 +1,89 @@
+<template>
+<MkContainer :show-header="props.showHeader">
+	<template #header><i class="fas fa-rss-square"></i>RSS</template>
+	<template #func><button class="_button" @click="setting"><i class="fas fa-cog"></i></button></template>
+
+	<div class="ekmkgxbj">
+		<MkLoading v-if="fetching"/>
+		<div class="feed" v-else>
+			<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
+		</div>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import define from './define';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'rss',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+		url: {
+			type: 'string',
+			default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer
+	},
+	data() {
+		return {
+			items: [],
+			fetching: true,
+			clock: null,
+		};
+	},
+	mounted() {
+		this.fetch();
+		this.clock = setInterval(this.fetch, 60000);
+		this.$watch(() => this.props.url, this.fetch);
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		fetch() {
+			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
+			}).then(res => {
+				res.json().then(feed => {
+					this.items = feed.items;
+					this.fetching = false;
+				});
+			});
+		},
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.ekmkgxbj {
+	> .feed {
+		padding: 0;
+		font-size: 0.9em;
+
+		> a {
+			display: block;
+			padding: 8px 16px;
+			color: var(--fg);
+			white-space: nowrap;
+			text-overflow: ellipsis;
+			overflow: hidden;
+
+			&:nth-child(even) {
+				background: rgba(#000, 0.05);
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/server-metric/cpu-mem.vue b/packages/client/src/widgets/server-metric/cpu-mem.vue
new file mode 100644
index 0000000000..ad9e6a8b0f
--- /dev/null
+++ b/packages/client/src/widgets/server-metric/cpu-mem.vue
@@ -0,0 +1,174 @@
+<template>
+<div class="lcfyofjk">
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+		<defs>
+			<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
+				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
+				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
+			</linearGradient>
+			<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+				<polygon
+					:points="cpuPolygonPoints"
+					fill="#fff"
+					fill-opacity="0.5"
+				/>
+				<polyline
+					:points="cpuPolylinePoints"
+					fill="none"
+					stroke="#fff"
+					stroke-width="1"
+				/>
+				<circle
+					:cx="cpuHeadX"
+					:cy="cpuHeadY"
+					r="1.5"
+					fill="#fff"
+				/>
+			</mask>
+		</defs>
+		<rect
+			x="-2" y="-2"
+			:width="viewBoxX + 4" :height="viewBoxY + 4"
+			:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"
+		/>
+		<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
+	</svg>
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+		<defs>
+			<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
+				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
+				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
+			</linearGradient>
+			<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
+				<polygon
+					:points="memPolygonPoints"
+					fill="#fff"
+					fill-opacity="0.5"
+				/>
+				<polyline
+					:points="memPolylinePoints"
+					fill="none"
+					stroke="#fff"
+					stroke-width="1"
+				/>
+				<circle
+					:cx="memHeadX"
+					:cy="memHeadY"
+					r="1.5"
+					fill="#fff"
+				/>
+			</mask>
+		</defs>
+		<rect
+			x="-2" y="-2"
+			:width="viewBoxX + 4" :height="viewBoxY + 4"
+			:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"
+		/>
+		<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
+	</svg>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { v4 as uuid } from 'uuid';
+
+export default defineComponent({
+	props: {
+		connection: {
+			required: true,
+		},
+		meta: {
+			required: true,
+		}
+	},
+	data() {
+		return {
+			viewBoxX: 50,
+			viewBoxY: 30,
+			stats: [],
+			cpuGradientId: uuid(),
+			cpuMaskId: uuid(),
+			memGradientId: uuid(),
+			memMaskId: uuid(),
+			cpuPolylinePoints: '',
+			memPolylinePoints: '',
+			cpuPolygonPoints: '',
+			memPolygonPoints: '',
+			cpuHeadX: null,
+			cpuHeadY: null,
+			memHeadX: null,
+			memHeadY: null,
+			cpuP: '',
+			memP: ''
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+		this.connection.on('statsLog', this.onStatsLog);
+		this.connection.send('requestLog', {
+			id: Math.random().toString().substr(2, 8)
+		});
+	},
+	beforeUnmount() {
+		this.connection.off('stats', this.onStats);
+		this.connection.off('statsLog', this.onStatsLog);
+	},
+	methods: {
+		onStats(stats) {
+			this.stats.push(stats);
+			if (this.stats.length > 50) this.stats.shift();
+
+			const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu) * this.viewBoxY]);
+			const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.active / this.meta.mem.total)) * this.viewBoxY]);
+			this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+			this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+
+			this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
+			this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
+
+			this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
+			this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
+			this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
+			this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
+
+			this.cpuP = (stats.cpu * 100).toFixed(0);
+			this.memP = (stats.mem.active / this.meta.mem.total * 100).toFixed(0);
+		},
+		onStatsLog(statsLog) {
+			for (const stats of [...statsLog].reverse()) {
+				this.onStats(stats);
+			}
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.lcfyofjk {
+	display: flex;
+
+	> svg {
+		display: block;
+		padding: 10px;
+		width: 50%;
+
+		&:first-child {
+			padding-right: 5px;
+		}
+
+		&:last-child {
+			padding-left: 5px;
+		}
+
+		> text {
+			font-size: 5px;
+			fill: currentColor;
+
+			> tspan {
+				opacity: 0.5;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/server-metric/cpu.vue b/packages/client/src/widgets/server-metric/cpu.vue
new file mode 100644
index 0000000000..4478ee3065
--- /dev/null
+++ b/packages/client/src/widgets/server-metric/cpu.vue
@@ -0,0 +1,76 @@
+<template>
+<div class="vrvdvrys">
+	<XPie class="pie" :value="usage"/>
+	<div>
+		<p><i class="fas fa-microchip"></i>CPU</p>
+		<p>{{ meta.cpu.cores }} Logical cores</p>
+		<p>{{ meta.cpu.model }}</p>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XPie from './pie.vue';
+
+export default defineComponent({
+	components: {
+		XPie
+	},
+	props: {
+		connection: {
+			required: true,
+		},
+		meta: {
+			required: true,
+		}
+	},
+	data() {
+		return {
+			usage: 0,
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+	},
+	beforeUnmount() {
+		this.connection.off('stats', this.onStats);
+	},
+	methods: {
+		onStats(stats) {
+			this.usage = stats.cpu;
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.vrvdvrys {
+	display: flex;
+	padding: 16px;
+
+	> .pie {
+		height: 82px;
+		flex-shrink: 0;
+		margin-right: 16px;
+	}
+
+	> div {
+		flex: 1;
+
+		> p {
+			margin: 0;
+			font-size: 0.8em;
+
+			&:first-child {
+				font-weight: bold;
+				margin-bottom: 4px;
+
+				> i {
+					margin-right: 4px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/server-metric/disk.vue b/packages/client/src/widgets/server-metric/disk.vue
new file mode 100644
index 0000000000..650101b0ee
--- /dev/null
+++ b/packages/client/src/widgets/server-metric/disk.vue
@@ -0,0 +1,70 @@
+<template>
+<div class="zbwaqsat">
+	<XPie class="pie" :value="usage"/>
+	<div>
+		<p><i class="fas fa-hdd"></i>Disk</p>
+		<p>Total: {{ bytes(total, 1) }}</p>
+		<p>Free: {{ bytes(available, 1) }}</p>
+		<p>Used: {{ bytes(used, 1) }}</p>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XPie from './pie.vue';
+import bytes from '@/filters/bytes';
+
+export default defineComponent({
+	components: {
+		XPie
+	},
+	props: {
+		meta: {
+			required: true,
+		}
+	},
+	data() {
+		return {
+			usage: this.meta.fs.used / this.meta.fs.total,
+			total: this.meta.fs.total,
+			used: this.meta.fs.used,
+			available: this.meta.fs.total - this.meta.fs.used,
+		};
+	},
+	methods: {
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.zbwaqsat {
+	display: flex;
+	padding: 16px;
+
+	> .pie {
+		height: 82px;
+		flex-shrink: 0;
+		margin-right: 16px;
+	}
+
+	> div {
+		flex: 1;
+
+		> p {
+			margin: 0;
+			font-size: 0.8em;
+
+			&:first-child {
+				font-weight: bold;
+				margin-bottom: 4px;
+
+				> i {
+					margin-right: 4px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/server-metric/index.vue b/packages/client/src/widgets/server-metric/index.vue
new file mode 100644
index 0000000000..cfe3c15df7
--- /dev/null
+++ b/packages/client/src/widgets/server-metric/index.vue
@@ -0,0 +1,82 @@
+<template>
+<MkContainer :show-header="props.showHeader" :naked="props.transparent">
+	<template #header><i class="fas fa-server"></i>{{ $ts._widgets.serverMetric }}</template>
+	<template #func><button @click="toggleView()" class="_button"><i class="fas fa-sort"></i></button></template>
+
+	<div class="mkw-serverMetric" v-if="meta">
+		<XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/>
+		<XNet v-if="props.view === 1" :connection="connection" :meta="meta"/>
+		<XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/>
+		<XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/>
+		<XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent, markRaw } from 'vue';
+import define from '../define';
+import MkContainer from '@/components/ui/container.vue';
+import XCpuMemory from './cpu-mem.vue';
+import XNet from './net.vue';
+import XCpu from './cpu.vue';
+import XMemory from './mem.vue';
+import XDisk from './disk.vue';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'serverMetric',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+		transparent: {
+			type: 'boolean',
+			default: false,
+		},
+		view: {
+			type: 'number',
+			default: 0,
+			hidden: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer,
+		XCpuMemory,
+		XNet,
+		XCpu,
+		XMemory,
+		XDisk,
+	},
+	data() {
+		return {
+			meta: null,
+			connection: null,
+		};
+	},
+	created() {
+		os.api('server-info', {}).then(res => {
+			this.meta = res;
+		});
+		this.connection = markRaw(os.stream.useChannel('serverStats'));
+	},
+	unmounted() {
+		this.connection.dispose();
+	},
+	methods: {
+		toggleView() {
+			if (this.props.view == 4) {
+				this.props.view = 0;
+			} else {
+				this.props.view++;
+			}
+			this.save();
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/widgets/server-metric/mem.vue b/packages/client/src/widgets/server-metric/mem.vue
new file mode 100644
index 0000000000..a6ca7b1175
--- /dev/null
+++ b/packages/client/src/widgets/server-metric/mem.vue
@@ -0,0 +1,85 @@
+<template>
+<div class="zlxnikvl">
+	<XPie class="pie" :value="usage"/>
+	<div>
+		<p><i class="fas fa-memory"></i>RAM</p>
+		<p>Total: {{ bytes(total, 1) }}</p>
+		<p>Used: {{ bytes(used, 1) }}</p>
+		<p>Free: {{ bytes(free, 1) }}</p>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XPie from './pie.vue';
+import bytes from '@/filters/bytes';
+
+export default defineComponent({
+	components: {
+		XPie
+	},
+	props: {
+		connection: {
+			required: true,
+		},
+		meta: {
+			required: true,
+		}
+	},
+	data() {
+		return {
+			usage: 0,
+			total: 0,
+			used: 0,
+			free: 0,
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+	},
+	beforeUnmount() {
+		this.connection.off('stats', this.onStats);
+	},
+	methods: {
+		onStats(stats) {
+			this.usage = stats.mem.active / this.meta.mem.total;
+			this.total = this.meta.mem.total;
+			this.used = stats.mem.active;
+			this.free = this.meta.mem.total - stats.mem.active;
+		},
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.zlxnikvl {
+	display: flex;
+	padding: 16px;
+
+	> .pie {
+		height: 82px;
+		flex-shrink: 0;
+		margin-right: 16px;
+	}
+
+	> div {
+		flex: 1;
+
+		> p {
+			margin: 0;
+			font-size: 0.8em;
+
+			&:first-child {
+				font-weight: bold;
+				margin-bottom: 4px;
+
+				> i {
+					margin-right: 4px;
+				}
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/server-metric/net.vue b/packages/client/src/widgets/server-metric/net.vue
new file mode 100644
index 0000000000..23c148eeb6
--- /dev/null
+++ b/packages/client/src/widgets/server-metric/net.vue
@@ -0,0 +1,148 @@
+<template>
+<div class="oxxrhrto">
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+		<polygon
+			:points="inPolygonPoints"
+			fill="#94a029"
+			fill-opacity="0.5"
+		/>
+		<polyline
+			:points="inPolylinePoints"
+			fill="none"
+			stroke="#94a029"
+			stroke-width="1"
+		/>
+		<circle
+			:cx="inHeadX"
+			:cy="inHeadY"
+			r="1.5"
+			fill="#94a029"
+		/>
+		<text x="1" y="5">NET rx <tspan>{{ bytes(inRecent) }}</tspan></text>
+	</svg>
+	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+		<polygon
+			:points="outPolygonPoints"
+			fill="#ff9156"
+			fill-opacity="0.5"
+		/>
+		<polyline
+			:points="outPolylinePoints"
+			fill="none"
+			stroke="#ff9156"
+			stroke-width="1"
+		/>
+		<circle
+			:cx="outHeadX"
+			:cy="outHeadY"
+			r="1.5"
+			fill="#ff9156"
+		/>
+		<text x="1" y="5">NET tx <tspan>{{ bytes(outRecent) }}</tspan></text>
+	</svg>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import bytes from '@/filters/bytes';
+
+export default defineComponent({
+	props: {
+		connection: {
+			required: true,
+		},
+		meta: {
+			required: true,
+		}
+	},
+	data() {
+		return {
+			viewBoxX: 50,
+			viewBoxY: 30,
+			stats: [],
+			inPolylinePoints: '',
+			outPolylinePoints: '',
+			inPolygonPoints: '',
+			outPolygonPoints: '',
+			inHeadX: null,
+			inHeadY: null,
+			outHeadX: null,
+			outHeadY: null,
+			inRecent: 0,
+			outRecent: 0
+		};
+	},
+	mounted() {
+		this.connection.on('stats', this.onStats);
+		this.connection.on('statsLog', this.onStatsLog);
+		this.connection.send('requestLog', {
+			id: Math.random().toString().substr(2, 8)
+		});
+	},
+	beforeUnmount() {
+		this.connection.off('stats', this.onStats);
+		this.connection.off('statsLog', this.onStatsLog);
+	},
+	methods: {
+		onStats(stats) {
+			this.stats.push(stats);
+			if (this.stats.length > 50) this.stats.shift();
+
+			const inPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.rx)));
+			const outPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.tx)));
+
+			const inPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * this.viewBoxY]);
+			const outPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * this.viewBoxY]);
+			this.inPolylinePoints = inPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+			this.outPolylinePoints = outPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+
+			this.inPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.inPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
+			this.outPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.outPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
+
+			this.inHeadX = inPolylinePoints[inPolylinePoints.length - 1][0];
+			this.inHeadY = inPolylinePoints[inPolylinePoints.length - 1][1];
+			this.outHeadX = outPolylinePoints[outPolylinePoints.length - 1][0];
+			this.outHeadY = outPolylinePoints[outPolylinePoints.length - 1][1];
+
+			this.inRecent = stats.net.rx;
+			this.outRecent = stats.net.tx;
+		},
+		onStatsLog(statsLog) {
+			for (const stats of [...statsLog].reverse()) {
+				this.onStats(stats);
+			}
+		},
+		bytes
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.oxxrhrto {
+	display: flex;
+
+	> svg {
+		display: block;
+		padding: 10px;
+		width: 50%;
+
+		&:first-child {
+			padding-right: 5px;
+		}
+
+		&:last-child {
+			padding-left: 5px;
+		}
+
+		> text {
+			font-size: 5px;
+			fill: currentColor;
+
+			> tspan {
+				opacity: 0.5;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/server-metric/pie.vue b/packages/client/src/widgets/server-metric/pie.vue
new file mode 100644
index 0000000000..38dcf6fcd9
--- /dev/null
+++ b/packages/client/src/widgets/server-metric/pie.vue
@@ -0,0 +1,65 @@
+<template>
+<svg class="hsalcinq" viewBox="0 0 1 1" preserveAspectRatio="none">
+	<circle
+		:r="r"
+		cx="50%" cy="50%"
+		fill="none"
+		stroke-width="0.1"
+		stroke="rgba(0, 0, 0, 0.05)"
+	/>
+	<circle
+		:r="r"
+		cx="50%" cy="50%"
+		:stroke-dasharray="Math.PI * (r * 2)"
+		:stroke-dashoffset="strokeDashoffset"
+		fill="none"
+		stroke-width="0.1"
+		:stroke="color"
+	/>
+	<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text>
+</svg>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+	props: {
+		value: {
+			type: Number,
+			required: true
+		}
+	},
+	data() {
+		return {
+			r: 0.45
+		};
+	},
+	computed: {
+		color(): string {
+			return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
+		},
+		strokeDashoffset(): number {
+			return (1 - this.value) * (Math.PI * (this.r * 2));
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hsalcinq {
+	display: block;
+	height: 100%;
+
+	> circle {
+		transform-origin: center;
+		transform: rotate(-90deg);
+		transition: stroke-dashoffset 0.5s ease;
+	}
+
+	> text {
+		font-size: 0.15px;
+		fill: currentColor;
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue
new file mode 100644
index 0000000000..0909bda67c
--- /dev/null
+++ b/packages/client/src/widgets/slideshow.vue
@@ -0,0 +1,167 @@
+<template>
+<div class="kvausudm _panel">
+	<div @click="choose">
+		<p v-if="props.folderId == null">
+			<template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template>
+			<template v-else>{{ $ts.folder }}</template>
+		</p>
+		<p v-if="props.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
+		<div ref="slideA" class="slide a"></div>
+		<div ref="slideB" class="slide b"></div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import define from './define';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'slideshow',
+	props: () => ({
+		height: {
+			type: 'number',
+			default: 300,
+		},
+		folderId: {
+			type: 'string',
+			default: null,
+			hidden: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	data() {
+		return {
+			images: [],
+			fetching: true,
+			clock: null
+		};
+	},
+	mounted() {
+		this.$nextTick(() => {
+			this.applySize();
+		});
+
+		if (this.props.folderId != null) {
+			this.fetch();
+		}
+
+		this.clock = setInterval(this.change, 10000);
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		applySize() {
+			let h;
+
+			if (this.props.size == 1) {
+				h = 250;
+			} else {
+				h = 170;
+			}
+
+			this.$el.style.height = `${h}px`;
+		},
+		resize() {
+			if (this.props.size == 1) {
+				this.props.size = 0;
+			} else {
+				this.props.size++;
+			}
+			this.save();
+
+			this.applySize();
+		},
+		change() {
+			if (this.images.length == 0) return;
+
+			const index = Math.floor(Math.random() * this.images.length);
+			const img = `url(${ this.images[index].url })`;
+
+			(this.$refs.slideB as any).style.backgroundImage = img;
+
+			this.$refs.slideB.classList.add('anime');
+			setTimeout(() => {
+				// 既にこのウィジェットがunmountされていたら要素がない
+				if ((this.$refs.slideA as any) == null) return;
+
+				(this.$refs.slideA as any).style.backgroundImage = img;
+
+				this.$refs.slideB.classList.remove('anime');
+			}, 1000);
+		},
+		fetch() {
+			this.fetching = true;
+
+			os.api('drive/files', {
+				folderId: this.props.folderId,
+				type: 'image/*',
+				limit: 100
+			}).then(images => {
+				this.images = images;
+				this.fetching = false;
+				(this.$refs.slideA as any).style.backgroundImage = '';
+				(this.$refs.slideB as any).style.backgroundImage = '';
+				this.change();
+			});
+		},
+		choose() {
+			os.selectDriveFolder(false).then(folder => {
+				if (folder == null) {
+					return;
+				}
+				this.props.folderId = folder.id;
+				this.save();
+				this.fetch();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.kvausudm {
+	position: relative;
+
+	> div {
+		width: 100%;
+		height: 100%;
+		cursor: pointer;
+
+		> p {
+			display: block;
+			margin: 1em;
+			text-align: center;
+			color: #888;
+		}
+
+		> * {
+			pointer-events: none;
+		}
+
+		> .slide {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			background-size: cover;
+			background-position: center;
+
+			&.b {
+				opacity: 0;
+			}
+
+			&.anime {
+				transition: opacity 1s;
+				opacity: 1;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue
new file mode 100644
index 0000000000..0d0629abe2
--- /dev/null
+++ b/packages/client/src/widgets/timeline.vue
@@ -0,0 +1,116 @@
+<template>
+<MkContainer :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true">
+	<template #header>
+		<button @click="choose" class="_button">
+			<i v-if="props.src === 'home'" class="fas fa-home"></i>
+			<i v-else-if="props.src === 'local'" class="fas fa-comments"></i>
+			<i v-else-if="props.src === 'social'" class="fas fa-share-alt"></i>
+			<i v-else-if="props.src === 'global'" class="fas fa-globe"></i>
+			<i v-else-if="props.src === 'list'" class="fas fa-list-ul"></i>
+			<i v-else-if="props.src === 'antenna'" class="fas fa-satellite"></i>
+			<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
+			<i :class="menuOpened ? 'fas fa-angle-up' : 'fas fa-angle-down'" style="margin-left: 8px;"></i>
+		</button>
+	</template>
+
+	<div>
+		<XTimeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import XTimeline from '@/components/timeline.vue';
+import define from './define';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'timeline',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+		height: {
+			type: 'number',
+			default: 300,
+		},
+		src: {
+			type: 'string',
+			default: 'home',
+			hidden: true,
+		},
+		list: {
+			type: 'object',
+			default: null,
+			hidden: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer,
+		XTimeline,
+	},
+
+	data() {
+		return {
+			menuOpened: false,
+		};
+	},
+
+	methods: {
+		async choose(ev) {
+			this.menuOpened = true;
+			const [antennas, lists] = await Promise.all([
+				os.api('antennas/list'),
+				os.api('users/lists/list')
+			]);
+			const antennaItems = antennas.map(antenna => ({
+				text: antenna.name,
+				icon: 'fas fa-satellite',
+				action: () => {
+					this.props.antenna = antenna;
+					this.setSrc('antenna');
+				}
+			}));
+			const listItems = lists.map(list => ({
+				text: list.name,
+				icon: 'fas fa-list-ul',
+				action: () => {
+					this.props.list = list;
+					this.setSrc('list');
+				}
+			}));
+			os.popupMenu([{
+				text: this.$ts._timelines.home,
+				icon: 'fas fa-home',
+				action: () => { this.setSrc('home') }
+			}, {
+				text: this.$ts._timelines.local,
+				icon: 'fas fa-comments',
+				action: () => { this.setSrc('local') }
+			}, {
+				text: this.$ts._timelines.social,
+				icon: 'fas fa-share-alt',
+				action: () => { this.setSrc('social') }
+			}, {
+				text: this.$ts._timelines.global,
+				icon: 'fas fa-globe',
+				action: () => { this.setSrc('global') }
+			}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {
+				this.menuOpened = false;
+			});
+		},
+
+		setSrc(src) {
+			this.props.src = src;
+			this.save();
+		},
+	}
+});
+</script>
diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue
new file mode 100644
index 0000000000..dba3392618
--- /dev/null
+++ b/packages/client/src/widgets/trends.vue
@@ -0,0 +1,111 @@
+<template>
+<MkContainer :show-header="props.showHeader">
+	<template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template>
+
+	<div class="wbrkwala">
+		<MkLoading v-if="fetching"/>
+		<transition-group tag="div" name="chart" class="tags" v-else>
+			<div v-for="stat in stats" :key="stat.tag">
+				<div class="tag">
+					<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
+					<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
+				</div>
+				<MkMiniChart class="chart" :src="stat.chart"/>
+			</div>
+		</transition-group>
+	</div>
+</MkContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkContainer from '@/components/ui/container.vue';
+import define from './define';
+import MkMiniChart from '@/components/mini-chart.vue';
+import * as os from '@/os';
+
+const widget = define({
+	name: 'hashtags',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+	})
+});
+
+export default defineComponent({
+	extends: widget,
+	components: {
+		MkContainer, MkMiniChart
+	},
+	data() {
+		return {
+			stats: [],
+			fetching: true,
+		};
+	},
+	mounted() {
+		this.fetch();
+		this.clock = setInterval(this.fetch, 1000 * 60);
+	},
+	beforeUnmount() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		fetch() {
+			os.api('hashtags/trend').then(stats => {
+				this.stats = stats;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wbrkwala {
+	height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
+	overflow: hidden;
+
+	> .tags {
+		.chart-move {
+			transition: transform 1s ease;
+		}
+
+		> div {
+			display: flex;
+			align-items: center;
+			padding: 14px 16px;
+			border-bottom: solid 0.5px var(--divider);
+
+			> .tag {
+				flex: 1;
+				overflow: hidden;
+				font-size: 0.9em;
+				color: var(--fg);
+
+				> .a {
+					display: block;
+					width: 100%;
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					line-height: 18px;
+				}
+
+				> p {
+					margin: 0;
+					font-size: 75%;
+					opacity: 0.7;
+					line-height: 16px;
+				}
+			}
+
+			> .chart {
+				height: 30px;
+			}
+		}
+	}
+}
+</style>
diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json
new file mode 100644
index 0000000000..e6d3e5f1d3
--- /dev/null
+++ b/packages/client/tsconfig.json
@@ -0,0 +1,39 @@
+{
+	"compilerOptions": {
+		"allowJs": true,
+		"noEmitOnError": false,
+		"noImplicitAny": false,
+		"noImplicitReturns": true,
+		"noUnusedParameters": false,
+		"noUnusedLocals": true,
+		"noFallthroughCasesInSwitch": true,
+		"declaration": false,
+		"sourceMap": false,
+		"target": "es2017",
+		"module": "esnext",
+		"moduleResolution": "node",
+		"removeComments": false,
+		"noLib": false,
+		"strict": true,
+		"strictNullChecks": true,
+		"experimentalDecorators": true,
+		"resolveJsonModule": true,
+		"baseUrl": ".",
+		"paths": {
+			"@/*": ["./src/*"],
+		},
+		"typeRoots": [
+			"node_modules/@types",
+			"@types",
+		],
+		"lib": [
+			"esnext",
+			"dom",
+			"webworker"
+		]
+	},
+	"compileOnSave": false,
+	"include": [
+		"./**/*.ts"
+	]
+}
diff --git a/packages/client/webpack.config.js b/packages/client/webpack.config.js
new file mode 100644
index 0000000000..7bcfdcb15d
--- /dev/null
+++ b/packages/client/webpack.config.js
@@ -0,0 +1,192 @@
+/**
+ * webpack configuration
+ */
+
+const fs = require('fs');
+const webpack = require('webpack');
+const { VueLoaderPlugin } = require('vue-loader');
+
+class WebpackOnBuildPlugin {
+	constructor(callback) {
+		this.callback = callback;
+	}
+
+	apply(compiler) {
+		compiler.hooks.done.tap('WebpackOnBuildPlugin', this.callback);
+	}
+}
+
+const isProduction = process.env.NODE_ENV === 'production';
+
+const locales = require('../../locales');
+const meta = require('../../package.json');
+
+const postcss = {
+	loader: 'postcss-loader',
+	options: {
+		postcssOptions: {
+			plugins: [
+				require('cssnano')({
+					preset: 'default'
+				})
+			]
+		}
+	},
+};
+
+module.exports = {
+	entry: {
+		app: './src/init.ts',
+		sw: './src/sw/sw.ts'
+	},
+	module: {
+		rules: [{
+			test: /\.vue$/,
+			exclude: /node_modules/,
+			use: [{
+				loader: 'vue-loader',
+				options: {
+					cssSourceMap: false,
+					compilerOptions: {
+						preserveWhitespace: false
+					}
+				}
+			}]
+		}, {
+			test: /\.scss?$/,
+			exclude: /node_modules/,
+			oneOf: [{
+				resourceQuery: /module/,
+				use: [{
+					loader: 'vue-style-loader'
+				}, {
+					loader: 'css-loader',
+					options: {
+						modules: true,
+						esModule: false, // TODO: trueにすると壊れる。Vue3移行の折にはtrueにできるかもしれない
+						url: false,
+					}
+				}, postcss, {
+					loader: 'sass-loader',
+					options: {
+						implementation: require('sass'),
+						sassOptions: {
+							fiber: false
+						}
+					}
+				}]
+			}, {
+				use: [{
+					loader: 'vue-style-loader'
+				}, {
+					loader: 'css-loader',
+					options: {
+						url: false,
+						esModule: false, // TODO: trueにすると壊れる。Vue3移行の折にはtrueにできるかもしれない
+					}
+				}, postcss, {
+					loader: 'sass-loader',
+					options: {
+						implementation: require('sass'),
+						sassOptions: {
+							fiber: false
+						}
+					}
+				}]
+			}]
+		}, {
+			test: /\.css$/,
+			oneOf: [{
+				resourceQuery: /module/,
+				use: [{
+					loader: 'vue-style-loader'
+				}, {
+					loader: 'css-loader',
+					options: {
+						modules: true,
+						esModule: false, // TODO: trueにすると壊れる。Vue3移行の折にはtrueにできるかもしれない
+					}
+				}, postcss]
+			}, {
+				use: [{
+					loader: 'vue-style-loader'
+				}, {
+					loader: 'css-loader',
+					options: {
+						esModule: false, // TODO: trueにすると壊れる。Vue3移行の折にはtrueにできるかもしれない
+					}
+				}, postcss]
+			}]
+		}, {
+			test: /\.svg$/,
+			use: [
+				'vue-loader',
+				'vue-svg-loader',
+			],
+		}, {
+			test: /\.(eot|woff|woff2|svg|ttf)([?]?.*)$/,
+			type: 'asset/resource'
+		}, {
+			test: /\.json5$/,
+			loader: 'json5-loader',
+			options: {
+				esModule: false,
+			},
+			type: 'javascript/auto'
+		}, {
+			test: /\.ts$/,
+			exclude: /node_modules/,
+			use: [{
+				loader: 'ts-loader',
+				options: {
+					happyPackMode: true,
+					transpileOnly: true,
+					configFile: __dirname + '/tsconfig.json',
+					appendTsSuffixTo: [/\.vue$/]
+				}
+			}]
+		}]
+	},
+	plugins: [
+		new webpack.ProgressPlugin({}),
+		new webpack.DefinePlugin({
+			_VERSION_: JSON.stringify(meta.version),
+			_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])),
+			_ENV_: JSON.stringify(process.env.NODE_ENV),
+			_DEV_: process.env.NODE_ENV !== 'production',
+			_PERF_PREFIX_: JSON.stringify('Misskey:'),
+			_DATA_TRANSFER_DRIVE_FILE_: JSON.stringify('mk_drive_file'),
+			_DATA_TRANSFER_DRIVE_FOLDER_: JSON.stringify('mk_drive_folder'),
+			_DATA_TRANSFER_DECK_COLUMN_: JSON.stringify('mk_deck_column'),
+			__VUE_OPTIONS_API__: true,
+			__VUE_PROD_DEVTOOLS__: false,
+		}),
+		new VueLoaderPlugin(),
+		new WebpackOnBuildPlugin(() => {
+			fs.mkdirSync(__dirname + '/../../built', { recursive: true });
+			fs.writeFileSync(__dirname + '/../../built/meta.json', JSON.stringify({ version: meta.version }), 'utf-8');
+		}),
+	],
+	output: {
+		path: __dirname + '/../../built/_client_dist_',
+		filename: `[name].${meta.version}.js`,
+		publicPath: `/assets/`,
+		pathinfo: false,
+	},
+	resolve: {
+		extensions: [
+			'.js', '.ts', '.json'
+		],
+		alias: {
+			'@': __dirname + '/src/',
+		}
+	},
+	resolveLoader: {
+		modules: ['node_modules']
+	},
+	experiments: {
+		topLevelAwait: true
+	},
+	devtool: false, //'source-map',
+	mode: isProduction ? 'production' : 'development'
+};
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
new file mode 100644
index 0000000000..8dd3579bb2
--- /dev/null
+++ b/packages/client/yarn.lock
@@ -0,0 +1,6874 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+  version "7.12.13"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
+  integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
+  dependencies:
+    "@babel/highlight" "^7.12.13"
+
+"@babel/helper-validator-identifier@^7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
+  integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
+
+"@babel/highlight@^7.12.13":
+  version "7.12.13"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c"
+  integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.12.11"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/parser@^7.15.0":
+  version "7.15.6"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.6.tgz#043b9aa3c303c0722e5377fef9197f4cf1796549"
+  integrity sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q==
+
+"@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
+  version "7.13.9"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99"
+  integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==
+
+"@babel/runtime@^7.16.0":
+  version "7.16.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
+  integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/runtime@^7.6.2":
+  version "7.12.13"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d"
+  integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/types@^7.6.1", "@babel/types@^7.9.6":
+  version "7.13.0"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80"
+  integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.12.11"
+    lodash "^4.17.19"
+    to-fast-properties "^2.0.0"
+
+"@cspotcode/source-map-consumer@0.8.0":
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
+  integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
+
+"@cspotcode/source-map-support@0.7.0":
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5"
+  integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==
+  dependencies:
+    "@cspotcode/source-map-consumer" "0.8.0"
+
+"@cypress/request@^2.88.6":
+  version "2.88.6"
+  resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.6.tgz#a970dd675befc6bdf8a8921576c01f51cc5798e9"
+  integrity sha512-z0UxBE/+qaESAHY9p9sM2h8Y4XqtsbDCt0/DPOrqA/RZgKi4PkxdpXyK4wCCnSk1xHqWHZZAE+gV6aDAR6+caQ==
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^8.3.2"
+
+"@cypress/xvfb@^1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a"
+  integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==
+  dependencies:
+    debug "^3.1.0"
+    lodash.once "^4.1.1"
+
+"@discordapp/twemoji@13.1.0":
+  version "13.1.0"
+  resolved "https://registry.yarnpkg.com/@discordapp/twemoji/-/twemoji-13.1.0.tgz#6b25f3958fa8fd68692248c87776bc737fd009a9"
+  integrity sha512-KEw/te+ylD2MHutzigafyptv0kdTU05Dbgxr9Y5J9IAQw8PbFz16nKtlPnJtA23BLp9fZQeNXzUmegkRi7fpDA==
+  dependencies:
+    fs-extra "^8.0.1"
+    jsonfile "^5.0.0"
+    twemoji-parser "13.1.0"
+    universalify "^0.1.2"
+
+"@discoveryjs/json-ext@^0.5.0":
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"
+  integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==
+
+"@elastic/elasticsearch@7.11.0":
+  version "7.11.0"
+  resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.11.0.tgz#e196243d0ed026742fc160d72cc5b4b5b6c7807d"
+  integrity sha512-AFVVuANIdbV1qYjuOi4hnsX/DehWYG+bbhQO4amq9K4/NnzU7mpGWOPgVlRQTiX+vBfBkx7SL6h4QEjIlM3ztA==
+  dependencies:
+    debug "^4.1.1"
+    hpagent "^0.1.1"
+    ms "^2.1.1"
+    pump "^3.0.0"
+    secure-json-parse "^2.1.0"
+
+"@eslint/eslintrc@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.4.tgz#dfe0ff7ba270848d10c5add0715e04964c034b31"
+  integrity sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==
+  dependencies:
+    ajv "^6.12.4"
+    debug "^4.3.2"
+    espree "^9.0.0"
+    globals "^13.9.0"
+    ignore "^4.0.6"
+    import-fresh "^3.2.1"
+    js-yaml "^4.1.0"
+    minimatch "^3.0.4"
+    strip-json-comments "^3.1.1"
+
+"@hapi/hoek@^9.0.0":
+  version "9.2.0"
+  resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
+  integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
+
+"@hapi/topo@^5.0.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
+  integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
+  dependencies:
+    "@hapi/hoek" "^9.0.0"
+
+"@humanwhocodes/config-array@^0.6.0":
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.6.0.tgz#b5621fdb3b32309d2d16575456cbc277fa8f021a"
+  integrity sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==
+  dependencies:
+    "@humanwhocodes/object-schema" "^1.2.0"
+    debug "^4.1.1"
+    minimatch "^3.0.4"
+
+"@humanwhocodes/object-schema@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
+  integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
+
+"@jfonx/console-utils@^1.0.3":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@jfonx/console-utils/-/console-utils-1.0.3.tgz#cbb7f911e4191a4a2fe1ba4807d29f100b5d099f"
+  integrity sha512-/XbnqjWc7yNZVLAJJO9rimfIz9DYte+cj3EF9hwhIv7vw6ok2t3cjl0huYsmD89srKH03vWjeqAcIH86CuYj3g==
+  dependencies:
+    colors "^1.3.3"
+
+"@jfonx/file-utils@^3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@jfonx/file-utils/-/file-utils-3.0.1.tgz#8d3d6e931a283420fe29802ea71c28dd397cd8d3"
+  integrity sha512-qwH0CuzWmghtTHGMyuPHj6SJPQgWeiXFJBfrxCWMbzxVCa3aLZPEfzSdlSnC/UABsk6feRkNdHXw59rVshNPqw==
+  dependencies:
+    "@jfonx/console-utils" "^1.0.3"
+    comment-json "^4.1.0"
+    find-up "^4.1.0"
+
+"@nodelib/fs.scandir@2.1.3":
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
+  integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.3"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3"
+  integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
+
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976"
+  integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.3"
+    fastq "^1.6.0"
+
+"@redocly/ajv@^8.6.2":
+  version "8.6.2"
+  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.2.tgz#8c4e485e72f7864f91fae40093bed548ec2619b2"
+  integrity sha512-tU8fQs0D76ZKhJ2cWtnfQthWqiZgGBx0gH0+5D8JvaBEBaqA8foPPBt3Nonwr3ygyv5xrw2IzKWgIY86BlGs+w==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+    uri-js "^4.2.2"
+
+"@redocly/openapi-core@1.0.0-beta.54":
+  version "1.0.0-beta.54"
+  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.54.tgz#42575a849c4dd54b9d0c6413fb8ca547e087cd11"
+  integrity sha512-uYs0N1Trjkh7u8IMIuCU2VxCXhMyGWSZUkP/WNdTR1OgBUtvNdF9C32zoQV+hyCIH4gVu42ROHkjisy333ZX+w==
+  dependencies:
+    "@redocly/ajv" "^8.6.2"
+    "@types/node" "^14.11.8"
+    colorette "^1.2.0"
+    js-levenshtein "^1.1.6"
+    js-yaml "^3.14.1"
+    lodash.isequal "^4.5.0"
+    minimatch "^3.0.4"
+    node-fetch "^2.6.1"
+    yaml-ast-parser "0.0.43"
+
+"@sentry/browser@5.29.2":
+  version "5.29.2"
+  resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.29.2.tgz#51adb4005511b1a4a70e4571880fc6653d651f91"
+  integrity sha512-uxZ7y7rp85tJll+RZtXRhXPbnFnOaxZqJEv05vJlXBtBNLQtlczV5iCtU9mZRLVHDtmZ5VVKUV8IKXntEqqDpQ==
+  dependencies:
+    "@sentry/core" "5.29.2"
+    "@sentry/types" "5.29.2"
+    "@sentry/utils" "5.29.2"
+    tslib "^1.9.3"
+
+"@sentry/core@5.29.2":
+  version "5.29.2"
+  resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.29.2.tgz#9e05fe197234161d57aabaf52fab336a7c520d81"
+  integrity sha512-7WYkoxB5IdlNEbwOwqSU64erUKH4laavPsM0/yQ+jojM76ErxlgEF0u//p5WaLPRzh3iDSt6BH+9TL45oNZeZw==
+  dependencies:
+    "@sentry/hub" "5.29.2"
+    "@sentry/minimal" "5.29.2"
+    "@sentry/types" "5.29.2"
+    "@sentry/utils" "5.29.2"
+    tslib "^1.9.3"
+
+"@sentry/hub@5.29.2":
+  version "5.29.2"
+  resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.29.2.tgz#208f10fe6674695575ad74182a1151f71d6df00a"
+  integrity sha512-LaAIo2hwUk9ykeh9RF0cwLy6IRw+DjEee8l1HfEaDFUM6TPGlNNGObMJNXb9/95jzWp7jWwOpQjoIE3jepdQJQ==
+  dependencies:
+    "@sentry/types" "5.29.2"
+    "@sentry/utils" "5.29.2"
+    tslib "^1.9.3"
+
+"@sentry/minimal@5.29.2":
+  version "5.29.2"
+  resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.29.2.tgz#420bebac8d03d30980fdb05c72d7b253d8aa541b"
+  integrity sha512-0aINSm8fGA1KyM7PavOBe1GDZDxrvnKt+oFnU0L+bTcw8Lr+of+v6Kwd97rkLRNOLw621xP076dL/7LSIzMuhw==
+  dependencies:
+    "@sentry/hub" "5.29.2"
+    "@sentry/types" "5.29.2"
+    tslib "^1.9.3"
+
+"@sentry/tracing@5.29.2":
+  version "5.29.2"
+  resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.29.2.tgz#6012788547d2ab7893799d82c4941bda145dcd47"
+  integrity sha512-iumYbVRpvoU3BUuIooxibydeaOOjl5ysc+mzsqhRs2NGW/C3uKAsFXdvyNfqt3bxtRQwJEhwJByLP2u3pLThpw==
+  dependencies:
+    "@sentry/hub" "5.29.2"
+    "@sentry/minimal" "5.29.2"
+    "@sentry/types" "5.29.2"
+    "@sentry/utils" "5.29.2"
+    tslib "^1.9.3"
+
+"@sentry/types@5.29.2":
+  version "5.29.2"
+  resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.29.2.tgz#ac87383df1222c2d9b9f8f9ed7a6b86ea41a098a"
+  integrity sha512-dM9wgt8wy4WRty75QkqQgrw9FV9F+BOMfmc0iaX13Qos7i6Qs2Q0dxtJ83SoR4YGtW8URaHzlDtWlGs5egBiMA==
+
+"@sentry/utils@5.29.2":
+  version "5.29.2"
+  resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.29.2.tgz#99a5cdda2ea19d34a41932f138d470adcb3ee673"
+  integrity sha512-nEwQIDjtFkeE4k6yIk4Ka5XjGRklNLThWLs2xfXlL7uwrYOH2B9UBBOOIRUraBm/g/Xrra3xsam/kRxuiwtXZQ==
+  dependencies:
+    "@sentry/types" "5.29.2"
+    tslib "^1.9.3"
+
+"@sideway/address@^4.1.0":
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.2.tgz#811b84333a335739d3969cfc434736268170cad1"
+  integrity sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA==
+  dependencies:
+    "@hapi/hoek" "^9.0.0"
+
+"@sideway/formula@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
+  integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
+
+"@sideway/pinpoint@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
+  integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
+
+"@sindresorhus/is@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4"
+  integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==
+
+"@sinonjs/commons@^1.7.0":
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
+  integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==
+  dependencies:
+    type-detect "4.0.8"
+
+"@sinonjs/fake-timers@7.1.2":
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5"
+  integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+
+"@syuilo/aiscript@0.11.1":
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.11.1.tgz#52c14692113c58d1d62e6ae696352ba49abdf2eb"
+  integrity sha512-chwOIA3yLUKvOB0G611hjLArKTeOWNmTm3lHERSaDW1d+dS6do56naX6Lkwy2UpnwWC0qzeNSgg35elk6t2gZg==
+  dependencies:
+    autobind-decorator "2.4.0"
+    chalk "4.0.0"
+    seedrandom "3.0.5"
+    stringz "2.1.0"
+    uuid "7.0.3"
+
+"@szmarczak/http-timer@^4.0.5":
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152"
+  integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==
+  dependencies:
+    defer-to-connect "^2.0.0"
+
+"@trysound/sax@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
+  integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
+
+"@tsconfig/node10@^1.0.7":
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.7.tgz#1eb1de36c73478a2479cc661ef5af1c16d86d606"
+  integrity sha512-aBvUmXLQbayM4w3A8TrjwrXs4DZ8iduJnuJLLRGdkWlyakCf1q6uHZJBzXoRA/huAEknG5tcUyQxN3A+In5euQ==
+
+"@tsconfig/node12@^1.0.7":
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.7.tgz#677bd9117e8164dc319987dd6ff5fc1ba6fbf18b"
+  integrity sha512-dgasobK/Y0wVMswcipr3k0HpevxFJLijN03A8mYfEPvWvOs14v0ZlYTR4kIgMx8g4+fTyTFv8/jLCIfRqLDJ4A==
+
+"@tsconfig/node14@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.0.tgz#5bd046e508b1ee90bc091766758838741fdefd6e"
+  integrity sha512-RKkL8eTdPv6t5EHgFKIVQgsDapugbuOptNd9OOunN/HAkzmmTnZELx1kNCK0rSdUYGmiFMM3rRQMAWiyp023LQ==
+
+"@tsconfig/node16@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
+  integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
+
+"@types/anymatch@*":
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
+  integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
+
+"@types/cacheable-request@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
+  integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==
+  dependencies:
+    "@types/http-cache-semantics" "*"
+    "@types/keyv" "*"
+    "@types/node" "*"
+    "@types/responselike" "*"
+
+"@types/color-name@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+
+"@types/dateformat@3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
+  integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
+
+"@types/disposable-email-domains@^1.0.1":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/disposable-email-domains/-/disposable-email-domains-1.0.2.tgz#0280f6b38fa7f14e54b056a434135ecd254483b1"
+  integrity sha512-SDKwyYTjk3y5aZBxxc38yRecpJPjsqn57STz1bNxYYlv4k11bBe7QB8w4llXDTmQXKT1mFvgGmJv+8Zdu3YmJw==
+
+"@types/escape-regexp@0.0.0":
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.0.tgz#bff0225f9ef30d0dbdbe0e2a24283ee5342990c3"
+  integrity sha512-HTansGo4tJ7K7W9I9LBdQqnHtPB/Y7tlS+EMrkboaAQLsRPhRpHaqAHe01K1HVXM5e1u1IplRd8EBh+pJrp7Dg==
+
+"@types/eslint-scope@^3.7.0":
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86"
+  integrity sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==
+  dependencies:
+    "@types/eslint" "*"
+    "@types/estree" "*"
+
+"@types/eslint@*":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.0.tgz#eb5c5b575237334df24c53195e37b53d66478d7b"
+  integrity sha512-LpUXkr7fnmPXWGxB0ZuLEzNeTURuHPavkC5zuU4sg62/TgL5ZEjamr5Y8b6AftwHtx2bPJasI+CL0TT2JwQ7aA==
+  dependencies:
+    "@types/estree" "*"
+    "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^0.0.46":
+  version "0.0.46"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe"
+  integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==
+
+"@types/estree@^0.0.50":
+  version "0.0.50"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
+  integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
+
+"@types/events@*":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
+  integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
+
+"@types/expect@^1.20.4":
+  version "1.20.4"
+  resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
+  integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
+
+"@types/fluent-ffmpeg@2.1.17":
+  version "2.1.17"
+  resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.17.tgz#6958dda400fe1b33c21f3683db76905cb210d053"
+  integrity sha512-/bdvjKw/mtBHlJ2370d04nt4CsWqU5MrwS/NtO96V01jxitJ4+iq8OFNcqc5CegeV3TQOK3uueK02kvRK+zjUg==
+  dependencies:
+    "@types/node" "*"
+
+"@types/glob-stream@*":
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc"
+  integrity sha512-RHv6ZQjcTncXo3thYZrsbAVwoy4vSKosSWhuhuQxLOTv74OJuFQxXkmUuZCr3q9uNBEVCvIzmZL/FeRNbHZGUg==
+  dependencies:
+    "@types/glob" "*"
+    "@types/node" "*"
+
+"@types/glob@*":
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
+  integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
+  dependencies:
+    "@types/events" "*"
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/glob@7.2.0":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
+  integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/gulp-rename@2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@types/gulp-rename/-/gulp-rename-2.0.1.tgz#c8228fc2c5c4a7500346ea9ce18f27fa988caef5"
+  integrity sha512-9ZjeS2RHEnmBmTcyi2+oeye3BgCsWhvi4uv3qCnAg8i6plOuRdaeNxjOves0ELysEXYLBl7bCl5fbVs7AZtgTA==
+  dependencies:
+    "@types/node" "*"
+    "@types/vinyl" "*"
+
+"@types/gulp@4.0.9":
+  version "4.0.9"
+  resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.9.tgz#a2f9667bcc26bc72b4899dd16216d6584a12346c"
+  integrity sha512-zzT+wfQ8uwoXjDhRK9Zkmmk09/fbLLmN/yDHFizJiEKIve85qutOnXcP/TM2sKPBTU+Jc16vfPbOMkORMUBN7Q==
+  dependencies:
+    "@types/undertaker" "*"
+    "@types/vinyl-fs" "*"
+    chokidar "^3.3.1"
+
+"@types/http-cache-semantics@*":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a"
+  integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==
+
+"@types/is-url@1.2.30":
+  version "1.2.30"
+  resolved "https://registry.yarnpkg.com/@types/is-url/-/is-url-1.2.30.tgz#85567e8bee4fee69202bc3448f9fb34b0d56c50a"
+  integrity sha512-AnlNFwjzC8XLda5VjRl4ItSd8qp8pSNowvsut0WwQyBWHpOxjxRJm8iO6uETWqEyLdYdb9/1j+Qd9gQ4l5I4fw==
+
+"@types/js-yaml@4.0.4":
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.4.tgz#cc38781257612581a1a0eb25f1709d2b06812fce"
+  integrity sha512-AuHubXUmg0AzkXH0Mx6sIxeY/1C110mm/EkE/gB1sTRz3h2dao2W/63q42SlVST+lICxz5Oki2hzYA6+KnnieQ==
+
+"@types/json-schema@*":
+  version "7.0.5"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
+  integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
+
+"@types/json-schema@^7.0.6":
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
+  integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
+
+"@types/json-schema@^7.0.7":
+  version "7.0.8"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818"
+  integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==
+
+"@types/json5@^0.0.29":
+  version "0.0.29"
+  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+  integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+
+"@types/katex@0.11.1":
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.1.tgz#34de04477dcf79e2ef6c8d23b41a3d81f9ebeaf5"
+  integrity sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==
+
+"@types/keyv@*":
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"
+  integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/matter-js@0.17.6":
+  version "0.17.6"
+  resolved "https://registry.yarnpkg.com/@types/matter-js/-/matter-js-0.17.6.tgz#525bb33a7289105e1981ef51b987972fa9739011"
+  integrity sha512-i6WLNuM7/89SLqO2aOyaUkom9tc3B/qo4ekh7BD99xQ8+wOVVZO0F4RzKNYZCaFwr+xp3pK3oIb6sSVjLpz+pA==
+
+"@types/minimatch@*":
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
+  integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+
+"@types/mocha@8.2.3":
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
+  integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
+
+"@types/node-fetch@2.5.12":
+  version "2.5.12"
+  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66"
+  integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==
+  dependencies:
+    "@types/node" "*"
+    form-data "^3.0.0"
+
+"@types/node@*":
+  version "16.6.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
+  integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
+
+"@types/node@16.11.7":
+  version "16.11.7"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
+  integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==
+
+"@types/node@^14.11.8", "@types/node@^14.14.31":
+  version "14.17.9"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd"
+  integrity sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==
+
+"@types/nodemailer@6.4.4":
+  version "6.4.4"
+  resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
+  integrity sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/nprogress@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f"
+  integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A==
+
+"@types/oauth@0.9.1":
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.1.tgz#e17221e7f7936b0459ae7d006255dff61adca305"
+  integrity sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==
+  dependencies:
+    "@types/node" "*"
+
+"@types/parse-json@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+
+"@types/parse5@6.0.2":
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.2.tgz#99f6b72d82e34cea03a4d8f2ed72114d909c1c61"
+  integrity sha512-+hQX+WyJAOne7Fh3zF5CxPemILIbuhNcqHHodzK9caYOLnC8pD5efmPleRnw0z++LfKUC/sVNMwk0Gap+B0baA==
+
+"@types/parsimmon@1.10.6":
+  version "1.10.6"
+  resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.6.tgz#8fcf95990514d2a7624aa5f630c13bf2427f9cdd"
+  integrity sha512-FwAQwMRbkhx0J6YELkwIpciVzCcgEqXEbIrIn3a2P5d3kGEHQ3wVhlN3YdVepYP+bZzCYO6OjmD4o9TGOZ40rA==
+
+"@types/portscanner@2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@types/portscanner/-/portscanner-2.1.1.tgz#89d5094e16f3d941f20f3889dfa5d3a164b3dd3b"
+  integrity sha512-1NsVIbgBKvrqxwtMN0V6CLji1ERwKSI/RWz0J3y++CzSwYNGBStCfpIFgxV3ZwxsDR5PoZqoUWhwraDm+Ztn0Q==
+
+"@types/pug@2.0.5":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.5.tgz#69bc700934dd473c7ab97270bd2dbacefe562231"
+  integrity sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==
+
+"@types/punycode@2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83"
+  integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g==
+
+"@types/q@^1.5.1":
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
+  integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
+
+"@types/qrcode@1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.1.tgz#0689f400c3a95d2db040c99c99834faa09ee9dc1"
+  integrity sha512-vxMyr7JM7tYPxu8vUE83NiosWX5DZieCyYeJRoOIg0pAkyofCBzknJ2ycUZkPGDFis2RS8GN/BeJLnRnAPxeCA==
+  dependencies:
+    "@types/node" "*"
+
+"@types/random-seed@0.3.3":
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/@types/random-seed/-/random-seed-0.3.3.tgz#7741f7b0a4513198a9396ce4ad25832f799a6727"
+  integrity sha512-kHsCbIRHNXJo6EN5W8EA5b4i1hdT6jaZke5crBPLUcLqaLdZ0QBq8QVMbafHzhjFF83Cl9qlee2dChD18d/kPg==
+
+"@types/rename@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@types/rename/-/rename-1.0.4.tgz#30c6f0306042591a560361ea02639e89647dd173"
+  integrity sha512-eV81+6bVv2mdCBahkMefjEUwAjKDAP3AuyhqWCWRxcRaeVdUeHUBaoq2zSz+5HNHF2jzTajMcfLvJsy4K3cbwA==
+
+"@types/request-stats@3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/request-stats/-/request-stats-3.0.0.tgz#d3909a9f778b8ae0b42fb8c1ed20cb936ed95f99"
+  integrity sha512-POsDF7nETH8up49iBNvbZuO0pEk9F+TG0rXCkvjxCClcOS99xfF+mKmJteYlwKYpuRKkixzysKlL8rwN1hU2lw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/responselike@*", "@types/responselike@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
+  integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
+  dependencies:
+    "@types/node" "*"
+
+"@types/rimraf@3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8"
+  integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==
+  dependencies:
+    "@types/glob" "*"
+    "@types/node" "*"
+
+"@types/seedrandom@2.4.28":
+  version "2.4.28"
+  resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f"
+  integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA==
+
+"@types/sinonjs__fake-timers@6.0.4":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
+  integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
+
+"@types/sinonjs__fake-timers@^6.0.2":
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.3.tgz#79df6f358ae8f79e628fe35a63608a0ea8e7cf08"
+  integrity sha512-E1dU4fzC9wN2QK2Cr1MLCfyHM8BoNnRFvuf45LYMPNDA+WqbNzC45S4UzPxvp1fFJ1rvSGU0bPvdd35VLmXG8g==
+
+"@types/sizzle@^2.3.2":
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
+  integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
+
+"@types/source-list-map@*":
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
+  integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
+
+"@types/speakeasy@2.0.6":
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/@types/speakeasy/-/speakeasy-2.0.6.tgz#12540f7b64d08180393ae2c5c8c280866a85da61"
+  integrity sha512-2wIXZp5yJUddhsSZarYCZIakCvzwQgTVdtT29DYVdFzc0cHttanaQx9THRhtjY4kDqVaF2jhyFOEofozOioFdQ==
+  dependencies:
+    "@types/node" "*"
+
+"@types/tapable@^1":
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4"
+  integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ==
+
+"@types/throttle-debounce@2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776"
+  integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
+
+"@types/tinycolor2@1.4.3":
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706"
+  integrity sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==
+
+"@types/tmp@0.2.2":
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.2.tgz#424537a3b91828cb26aaf697f21ae3cd1b69f7e7"
+  integrity sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==
+
+"@types/uglify-js@*":
+  version "3.9.0"
+  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.0.tgz#4490a140ca82aa855ad68093829e7fd6ae94ea87"
+  integrity sha512-3ZcoyPYHVOCcLpnfZwD47KFLr8W/mpUcgjpf1M4Q78TMJIw7KMAHSjiCLJp1z3ZrBR9pTLbe191O0TldFK5zcw==
+  dependencies:
+    source-map "^0.6.1"
+
+"@types/undertaker-registry@*":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz#4306d4a03d7acedb974b66530832b90729e1d1da"
+  integrity sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ==
+
+"@types/undertaker@*":
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/@types/undertaker/-/undertaker-1.2.2.tgz#927da24d0d3279830af96386862b035e040ead74"
+  integrity sha512-j4iepCSuY2JGW/hShVtUBagic0klYNFIXP7VweavnYnNC2EjiKxJFeaS9uaJmAT0ty9sQSqTS1aagWMZMV0HyA==
+  dependencies:
+    "@types/undertaker-registry" "*"
+
+"@types/uuid@8.3.1":
+  version "8.3.1"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
+  integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
+
+"@types/vinyl-fs@*":
+  version "2.4.11"
+  resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-2.4.11.tgz#b98119b8bb2494141eaf649b09fbfeb311161206"
+  integrity sha512-2OzQSfIr9CqqWMGqmcERE6Hnd2KY3eBVtFaulVo3sJghplUcaeMdL9ZjEiljcQQeHjheWY9RlNmumjIAvsBNaA==
+  dependencies:
+    "@types/glob-stream" "*"
+    "@types/node" "*"
+    "@types/vinyl" "*"
+
+"@types/vinyl@*":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.4.tgz#9a7a8071c8d14d3a95d41ebe7135babe4ad5995a"
+  integrity sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ==
+  dependencies:
+    "@types/expect" "^1.20.4"
+    "@types/node" "*"
+
+"@types/web-push@3.3.2":
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.3.2.tgz#8c32434147c0396415862e86405c9edc9c50fc15"
+  integrity sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==
+  dependencies:
+    "@types/node" "*"
+
+"@types/webpack-sources@*":
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.7.tgz#0a330a9456113410c74a5d64180af0cbca007141"
+  integrity sha512-XyaHrJILjK1VHVC4aVlKsdNN5KBTwufMb43cQs+flGxtPAf/1Qwl8+Q0tp5BwEGaI8D6XT1L+9bSWXckgkjTLw==
+  dependencies:
+    "@types/node" "*"
+    "@types/source-list-map" "*"
+    source-map "^0.6.1"
+
+"@types/webpack-stream@3.2.12":
+  version "3.2.12"
+  resolved "https://registry.yarnpkg.com/@types/webpack-stream/-/webpack-stream-3.2.12.tgz#cf13e64067a662a7acd8cd0524b3f64c86b0ecb6"
+  integrity sha512-znMUl4kKT0V0SwkUgRgwUNSAO7J5I/jdTCBNy3utkCsgMJ3IHp4FBTDwsQC+tfQ73TWeKIH05QNmbUYmeGThGw==
+  dependencies:
+    "@types/node" "*"
+    "@types/webpack" "^4"
+
+"@types/webpack@5.28.0":
+  version "5.28.0"
+  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03"
+  integrity sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==
+  dependencies:
+    "@types/node" "*"
+    tapable "^2.2.0"
+    webpack "^5"
+
+"@types/webpack@^4":
+  version "4.41.27"
+  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.27.tgz#f47da488c8037e7f1b2dbf2714fbbacb61ec0ffc"
+  integrity sha512-wK/oi5gcHi72VMTbOaQ70VcDxSQ1uX8S2tukBK9ARuGXrYM/+u4ou73roc7trXDNmCxCoerE8zruQqX/wuHszA==
+  dependencies:
+    "@types/anymatch" "*"
+    "@types/node" "*"
+    "@types/tapable" "^1"
+    "@types/uglify-js" "*"
+    "@types/webpack-sources" "*"
+    source-map "^0.6.0"
+
+"@types/websocket@1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8"
+  integrity sha512-qn1LkcFEKK8RPp459jkjzsfpbsx36BBt3oC3pITYtkoBw/aVX+EZFa5j3ThCRTNpLFvIMr5dSTD4RaMdilIOpA==
+  dependencies:
+    "@types/node" "*"
+
+"@types/ws@8.2.0":
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.0.tgz#75faefbe2328f3b833cb8dc640658328990d04f3"
+  integrity sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==
+  dependencies:
+    "@types/node" "*"
+
+"@types/yauzl@^2.9.1":
+  version "2.9.2"
+  resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a"
+  integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==
+  dependencies:
+    "@types/node" "*"
+
+"@typescript-eslint/parser@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.1.0.tgz#6c7f837d210d2bc0a811e7ea742af414f4e00908"
+  integrity sha512-vx1P+mhCtYw3+bRHmbalq/VKP2Y3gnzNgxGxfEWc6OFpuEL7iQdAeq11Ke3Rhy8NjgB+AHsIWEwni3e+Y7djKA==
+  dependencies:
+    "@typescript-eslint/scope-manager" "5.1.0"
+    "@typescript-eslint/types" "5.1.0"
+    "@typescript-eslint/typescript-estree" "5.1.0"
+    debug "^4.3.2"
+
+"@typescript-eslint/scope-manager@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.1.0.tgz#6f1f26ad66a8f71bbb33b635e74fec43f76b44df"
+  integrity sha512-yYlyVjvn5lvwCL37i4hPsa1s0ORsjkauhTqbb8MnpvUs7xykmcjGqwlNZ2Q5QpoqkJ1odlM2bqHqJwa28qV6Tw==
+  dependencies:
+    "@typescript-eslint/types" "5.1.0"
+    "@typescript-eslint/visitor-keys" "5.1.0"
+
+"@typescript-eslint/types@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.1.0.tgz#a8a75ddfc611660de6be17d3ad950302385607a9"
+  integrity sha512-sEwNINVxcB4ZgC6Fe6rUyMlvsB2jvVdgxjZEjQUQVlaSPMNamDOwO6/TB98kFt4sYYfNhdhTPBEQqNQZjMMswA==
+
+"@typescript-eslint/typescript-estree@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.1.0.tgz#132aea34372df09decda961cb42457433aa6e83d"
+  integrity sha512-SSz+l9YrIIsW4s0ZqaEfnjl156XQ4VRmJsbA0ZE1XkXrD3cRpzuZSVCyqeCMR3EBjF27IisWakbBDGhGNIOvfQ==
+  dependencies:
+    "@typescript-eslint/types" "5.1.0"
+    "@typescript-eslint/visitor-keys" "5.1.0"
+    debug "^4.3.2"
+    globby "^11.0.4"
+    is-glob "^4.0.3"
+    semver "^7.3.5"
+    tsutils "^3.21.0"
+
+"@typescript-eslint/visitor-keys@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.1.0.tgz#e01a01b27eb173092705ae983aa1451bd1842630"
+  integrity sha512-uqNXepKBg81JVwjuqAxYrXa1Ql/YDzM+8g/pS+TCPxba0wZttl8m5DkrasbfnmJGHs4lQ2jTbcZ5azGhI7kK+w==
+  dependencies:
+    "@typescript-eslint/types" "5.1.0"
+    eslint-visitor-keys "^3.0.0"
+
+"@ungap/promise-all-settled@1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
+  integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
+
+"@vue/compiler-core@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.21.tgz#26566c32b2ad838199d471ef5df620a83846f24e"
+  integrity sha512-NhhiQZNG71KNq1h5pMW/fAXdTF7lJRaSI7LDm2edhHXVz1ROMICo8SreUmQnSf4Fet0UPBVqJ988eF4+936iDQ==
+  dependencies:
+    "@babel/parser" "^7.15.0"
+    "@vue/shared" "3.2.21"
+    estree-walker "^2.0.2"
+    source-map "^0.6.1"
+
+"@vue/compiler-dom@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.21.tgz#d6f6c85364ef8888f9c4e9122bfba11e78fb398c"
+  integrity sha512-gsJD3DpYZSYquiA7UIPsMDSlAooYWDvHPq9VRsqzJEk2PZtFvLvHPb4aaMD8Ufd62xzYn32cnnkzsEOJhyGilA==
+  dependencies:
+    "@vue/compiler-core" "3.2.21"
+    "@vue/shared" "3.2.21"
+
+"@vue/compiler-sfc@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.21.tgz#42639ee49e725afb7d8f1d1940e75dc17a56002c"
+  integrity sha512-+yDlUSebKpz/ovxM2vLRRx7w/gVfY767pOfYTgbIhAs+ogvIV2BsIt4fpxlThnlCNChJ+yE0ERUNoROv2kEGEQ==
+  dependencies:
+    "@babel/parser" "^7.15.0"
+    "@vue/compiler-core" "3.2.21"
+    "@vue/compiler-dom" "3.2.21"
+    "@vue/compiler-ssr" "3.2.21"
+    "@vue/ref-transform" "3.2.21"
+    "@vue/shared" "3.2.21"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
+    postcss "^8.1.10"
+    source-map "^0.6.1"
+
+"@vue/compiler-ssr@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.21.tgz#37d124f89e8adef9fd56b85775de4b5310a0436e"
+  integrity sha512-eU+A0iWYy+1zAo2CRIJ0zSVlv1iuGAIbNRCnllSJ31pV1lX3jypJYzGbJlSRAbB7VP6E+tYveVT1Oq8JKewa3g==
+  dependencies:
+    "@vue/compiler-dom" "3.2.21"
+    "@vue/shared" "3.2.21"
+
+"@vue/reactivity@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.21.tgz#073ad144192ed78a07e151e95a3baa515e4099a2"
+  integrity sha512-7C57zFm/5E3SSTUhVuYj1InDwuJ+GIVQ/z+H43C9sST85gIThGXVhksl1yWTAadf8Yz4T5lSbqi5Ds8U/ueWcw==
+  dependencies:
+    "@vue/shared" "3.2.21"
+
+"@vue/ref-transform@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/ref-transform/-/ref-transform-3.2.21.tgz#b0c554c9f640c3f005f77e676066aa0faba90984"
+  integrity sha512-uiEWWBsrGeun9O7dQExYWzXO3rHm/YdtFNXDVqCSoPypzOVxWxdiL+8hHeWzxMB58fVuV2sT80aUtIVyaBVZgQ==
+  dependencies:
+    "@babel/parser" "^7.15.0"
+    "@vue/compiler-core" "3.2.21"
+    "@vue/shared" "3.2.21"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
+
+"@vue/runtime-core@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.21.tgz#e12dac8c3893b7aebfc37e32066718d8aa686ac5"
+  integrity sha512-7oOxKaU0D2IunOAMOOHZgJVrHg63xwng8BZx3fbgmakqEIMwHhQcp+5GV1sOg/sWW7R4UhaRDIUCukO2GRVK2Q==
+  dependencies:
+    "@vue/reactivity" "3.2.21"
+    "@vue/shared" "3.2.21"
+
+"@vue/runtime-dom@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.21.tgz#33dd15bc85281e773177a30dc6931c294bd77aa1"
+  integrity sha512-apBdriD6QsI4ywbllY8kjr9/0scGuStDuvLbJULPQkFPtHzntd51bP5PQTQVAEIc9kwnTozmj6x6ZdX/cwo7xA==
+  dependencies:
+    "@vue/runtime-core" "3.2.21"
+    "@vue/shared" "3.2.21"
+    csstype "^2.6.8"
+
+"@vue/server-renderer@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.21.tgz#887d0a44de76f72313cff2686a24c0315231d634"
+  integrity sha512-QBgYqVgI7XCSBCqGa4LduV9vpfQFdZBOodFmq5Txk5W/v1KrJ1LoOh2Q0RHiRgtoK/UR9uyvRVcYqOmwHkZNEg==
+  dependencies:
+    "@vue/compiler-ssr" "3.2.21"
+    "@vue/shared" "3.2.21"
+
+"@vue/shared@3.2.21":
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.21.tgz#4cd80c0e62cf65a7adab2449e86b6f0cb33a130b"
+  integrity sha512-5EQmIPK6gw4UVYUbM959B0uPsJ58+xoMESCZs3N89XyvJ9e+fX4pqEPrOGV8OroIk3SbEvJcC+eYc8BH9JQrHA==
+
+"@webassemblyjs/ast@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f"
+  integrity sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==
+  dependencies:
+    "@webassemblyjs/helper-numbers" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+
+"@webassemblyjs/ast@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
+  integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==
+  dependencies:
+    "@webassemblyjs/helper-numbers" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz#34d62052f453cd43101d72eab4966a022587947c"
+  integrity sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==
+
+"@webassemblyjs/floating-point-hex-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f"
+  integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==
+
+"@webassemblyjs/helper-api-error@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz#aaea8fb3b923f4aaa9b512ff541b013ffb68d2d4"
+  integrity sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==
+
+"@webassemblyjs/helper-api-error@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16"
+  integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==
+
+"@webassemblyjs/helper-buffer@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz#d026c25d175e388a7dbda9694e91e743cbe9b642"
+  integrity sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==
+
+"@webassemblyjs/helper-buffer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5"
+  integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==
+
+"@webassemblyjs/helper-numbers@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz#7ab04172d54e312cc6ea4286d7d9fa27c88cd4f9"
+  integrity sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==
+  dependencies:
+    "@webassemblyjs/floating-point-hex-parser" "1.11.0"
+    "@webassemblyjs/helper-api-error" "1.11.0"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-numbers@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae"
+  integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==
+  dependencies:
+    "@webassemblyjs/floating-point-hex-parser" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz#85fdcda4129902fe86f81abf7e7236953ec5a4e1"
+  integrity sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1"
+  integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==
+
+"@webassemblyjs/helper-wasm-section@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz#9ce2cc89300262509c801b4af113d1ca25c1a75b"
+  integrity sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-buffer" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+    "@webassemblyjs/wasm-gen" "1.11.0"
+
+"@webassemblyjs/helper-wasm-section@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a"
+  integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+
+"@webassemblyjs/ieee754@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz#46975d583f9828f5d094ac210e219441c4e6f5cf"
+  integrity sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/ieee754@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614"
+  integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.0.tgz#f7353de1df38aa201cba9fb88b43f41f75ff403b"
+  integrity sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/leb128@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5"
+  integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.0.tgz#86e48f959cf49e0e5091f069a709b862f5a2cadf"
+  integrity sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==
+
+"@webassemblyjs/utf8@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff"
+  integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==
+
+"@webassemblyjs/wasm-edit@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz#ee4a5c9f677046a210542ae63897094c2027cb78"
+  integrity sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-buffer" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+    "@webassemblyjs/helper-wasm-section" "1.11.0"
+    "@webassemblyjs/wasm-gen" "1.11.0"
+    "@webassemblyjs/wasm-opt" "1.11.0"
+    "@webassemblyjs/wasm-parser" "1.11.0"
+    "@webassemblyjs/wast-printer" "1.11.0"
+
+"@webassemblyjs/wasm-edit@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6"
+  integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/helper-wasm-section" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-opt" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    "@webassemblyjs/wast-printer" "1.11.1"
+
+"@webassemblyjs/wasm-gen@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe"
+  integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+    "@webassemblyjs/ieee754" "1.11.0"
+    "@webassemblyjs/leb128" "1.11.0"
+    "@webassemblyjs/utf8" "1.11.0"
+
+"@webassemblyjs/wasm-gen@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76"
+  integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wasm-opt@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz#1638ae188137f4bb031f568a413cd24d32f92978"
+  integrity sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-buffer" "1.11.0"
+    "@webassemblyjs/wasm-gen" "1.11.0"
+    "@webassemblyjs/wasm-parser" "1.11.0"
+
+"@webassemblyjs/wasm-opt@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2"
+  integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+
+"@webassemblyjs/wasm-parser@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz#3e680b8830d5b13d1ec86cc42f38f3d4a7700754"
+  integrity sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/helper-api-error" "1.11.0"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
+    "@webassemblyjs/ieee754" "1.11.0"
+    "@webassemblyjs/leb128" "1.11.0"
+    "@webassemblyjs/utf8" "1.11.0"
+
+"@webassemblyjs/wasm-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199"
+  integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wast-printer@1.11.0":
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz#680d1f6a5365d6d401974a8e949e05474e1fab7e"
+  integrity sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.0"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/wast-printer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0"
+  integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.1.0.tgz#8342bef0badfb7dfd3b576f2574ab80c725be043"
+  integrity sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==
+
+"@webpack-cli/info@^1.4.0":
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.4.0.tgz#b9179c3227ab09cbbb149aa733475fcf99430223"
+  integrity sha512-F6b+Man0rwE4n0409FyAJHStYA5OIZERxmnUfLVwv0mc0V1wLad3V7jqRlMkgKBeAq07jUvglacNaa6g9lOpuw==
+  dependencies:
+    envinfo "^7.7.3"
+
+"@webpack-cli/serve@^1.6.0":
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.0.tgz#2c275aa05c895eccebbfc34cfb223c6e8bd591a2"
+  integrity sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==
+
+"@xtuc/ieee754@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+  integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+  integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+abort-controller@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
+  integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+  dependencies:
+    event-target-shim "^5.0.0"
+
+acorn-import-assertions@^1.7.6:
+  version "1.7.6"
+  resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz#580e3ffcae6770eebeec76c3b9723201e9d01f78"
+  integrity sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==
+
+acorn-jsx@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
+  integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
+
+acorn-walk@^8.1.1:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.1.tgz#3ddab7f84e4a7e2313f6c414c5b7dac85f4e3ebc"
+  integrity sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==
+
+acorn@^7.1.1:
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+
+acorn@^8.0.4:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe"
+  integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==
+
+acorn@^8.4.1:
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
+  integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
+
+acorn@^8.5.0:
+  version "8.5.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
+  integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
+
+agent-base@6:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+  dependencies:
+    debug "4"
+
+aggregate-error@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
+  integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
+  dependencies:
+    clean-stack "^2.0.0"
+    indent-string "^4.0.0"
+
+ajv-keywords@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5:
+  version "6.12.5"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
+  integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+alphanum-sort@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+  integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
+ansi-colors@4.1.1, ansi-colors@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
+ansi-escapes@^4.3.0:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
+  integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+  dependencies:
+    type-fest "^0.21.3"
+
+ansi-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+  integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-regex@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+
+ansi-regex@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
+  integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  dependencies:
+    "@types/color-name" "^1.1.1"
+    color-convert "^2.0.1"
+
+anymatch@~3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+  integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+arch@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
+  integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
+
+arg@^4.1.0:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+  integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+  dependencies:
+    sprintf-js "~1.0.2"
+
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+array-timsort@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926"
+  integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==
+
+array-union@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
+asap@~2.0.3:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+  integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+
+asn1.js@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.3.0.tgz#439099fe9174e09cff5a54a9dda70260517e8689"
+  integrity sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==
+  dependencies:
+    bn.js "^4.0.0"
+    inherits "^2.0.1"
+    minimalistic-assert "^1.0.0"
+    safer-buffer "^2.1.0"
+
+asn1@~0.2.3:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+  integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+  dependencies:
+    safer-buffer "~2.1.0"
+
+assert-never@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe"
+  integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+astral-regex@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
+  integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
+
+async@^2.6.0:
+  version "2.6.3"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
+  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
+  dependencies:
+    lodash "^4.17.14"
+
+async@^3.2.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
+  integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+at-least-node@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
+  integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
+
+autobind-decorator@2.4.0, autobind-decorator@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.4.0.tgz#ea9e1c98708cf3b5b356f7cf9f10f265ff18239c"
+  integrity sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw==
+
+autosize@4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03"
+  integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==
+
+autwh@0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/autwh/-/autwh-0.1.0.tgz#24a5300923309d105133401a2568f9c8ab7d7e03"
+  integrity sha512-IkGZ4kjVlZMkEmDiVtZpGG3lDGHPqsMBIh4IpQKN7idYOJ5EGedqKPO+ychNqh8zrJEEqYsN0NcBkcmoE2uFAw==
+  dependencies:
+    oauth "0.9.15"
+
+aws-sign2@~0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
+  integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+
+axios@^0.19.2:
+  version "0.19.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
+  integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+  dependencies:
+    follow-redirects "1.5.10"
+
+axios@^0.21.1:
+  version "0.21.1"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
+  integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
+  dependencies:
+    follow-redirects "^1.10.0"
+
+babel-walk@3.0.0-canary-5:
+  version "3.0.0-canary-5"
+  resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11"
+  integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==
+  dependencies:
+    "@babel/types" "^7.9.6"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base32.js@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.0.1.tgz#d045736a57b1f6c139f0c7df42518a84e91bb2ba"
+  integrity sha1-0EVzalex9sE58MffQlGKhOkbsro=
+
+base64-js@^1.0.2:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
+  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+  integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+  dependencies:
+    tweetnacl "^0.14.3"
+
+big-integer@^1.6.16:
+  version "1.6.48"
+  resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
+  integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
+
+big.js@^5.2.2:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+  integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
+binary-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
+  integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
+
+blob-util@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
+  integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
+
+bluebird@3.7.2, bluebird@^3.7.2:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+blurhash@1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244"
+  integrity sha512-MXIPz6zwYUKayju+Uidf83KhH0vodZfeRl6Ich8Gu+KGl0JgKiFq9LsfqV7cVU5fKD/AotmduZqvOfrGKOfTaA==
+
+bn.js@^4.0.0:
+  version "4.11.8"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+  integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+
+boolbase@^1.0.0, boolbase@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^3.0.1, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+broadcast-channel@4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.5.0.tgz#d4717c493e219908fcb7f2f9078fe0baf95b77c1"
+  integrity sha512-jp+VPlQ1HyR0CM3uIYUrdpXupBvhTMFRkjR6mEmt5W4HaGDPFEzrO2Jqvi2PZ6zCC4zwLeco7CC5EUJPrVH8Tw==
+  dependencies:
+    "@babel/runtime" "^7.16.0"
+    detect-node "^2.1.0"
+    microseconds "0.2.0"
+    nano-time "1.0.0"
+    oblivious-set "1.0.0"
+    rimraf "3.0.2"
+    unload "2.3.1"
+
+browser-stdout@1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+  integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+
+browserslist@^4.0.0, browserslist@^4.14.5:
+  version "4.16.3"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
+  integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
+  dependencies:
+    caniuse-lite "^1.0.30001181"
+    colorette "^1.2.1"
+    electron-to-chromium "^1.3.649"
+    escalade "^3.1.1"
+    node-releases "^1.1.70"
+
+browserslist@^4.16.0:
+  version "4.16.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.4.tgz#7ebf913487f40caf4637b892b268069951c35d58"
+  integrity sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==
+  dependencies:
+    caniuse-lite "^1.0.30001208"
+    colorette "^1.2.2"
+    electron-to-chromium "^1.3.712"
+    escalade "^3.1.1"
+    node-releases "^1.1.71"
+
+browserslist@^4.16.6:
+  version "4.16.6"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
+  integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
+  dependencies:
+    caniuse-lite "^1.0.30001219"
+    colorette "^1.2.2"
+    electron-to-chromium "^1.3.723"
+    escalade "^3.1.1"
+    node-releases "^1.1.71"
+
+bs-logger@0.x:
+  version "0.2.6"
+  resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
+  integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
+  dependencies:
+    fast-json-stable-stringify "2.x"
+
+buffer-alloc-unsafe@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
+  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+
+buffer-alloc@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
+  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+  dependencies:
+    buffer-alloc-unsafe "^1.1.0"
+    buffer-fill "^1.0.0"
+
+buffer-crc32@~0.2.3:
+  version "0.2.13"
+  resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+  integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
+
+buffer-equal-constant-time@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+  integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
+
+buffer-fill@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
+  integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
+
+buffer-from@1.x:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+buffer-from@^1.0.0, buffer-from@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+buffer@^5.4.3:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
+  integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+
+bufferutil@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
+  integrity sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==
+  dependencies:
+    node-gyp-build "~3.7.0"
+
+builtin-modules@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+  integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+
+cacheable-lookup@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3"
+  integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==
+
+cacheable-request@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58"
+  integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==
+  dependencies:
+    clone-response "^1.0.2"
+    get-stream "^5.1.0"
+    http-cache-semantics "^4.0.0"
+    keyv "^4.0.0"
+    lowercase-keys "^2.0.0"
+    normalize-url "^4.1.0"
+    responselike "^2.0.0"
+
+cachedir@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
+  integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+camelcase@^6.0.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
+  integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
+
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001208, caniuse-lite@^1.0.30001219:
+  version "1.0.30001279"
+  resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001279.tgz"
+  integrity sha512-VfEHpzHEXj6/CxggTwSFoZBBYGQfQv9Cf42KPlO79sWXCD1QNKWKsKzFeWL7QpZHJQYAvocqV6Rty1yJMkqWLQ==
+
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+  integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
+  integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^4.0.0, chalk@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
+  integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+char-regex@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
+  integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
+
+character-parser@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
+  integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A=
+  dependencies:
+    is-regex "^1.0.3"
+
+chart.js@3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.6.0.tgz#a87fce8431d4e7c5523d721f487f53aada1e42fe"
+  integrity sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ==
+
+chartjs-adapter-date-fns@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
+  integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
+
+chartjs-plugin-zoom@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.1.1.tgz#8a28923a17fcb5eb57a0dc94c5113bf402677647"
+  integrity sha512-1q54WOzK7FtAjkbemQeqvmFUV0btNYIQny2HbQ6Awq9wUtCz7Zmj6vIgp3C1DYMQwN0nqgpC3vnApqiwI7cSdQ==
+  dependencies:
+    hammerjs "^2.0.8"
+
+check-more-types@2.24.0, check-more-types@^2.24.0:
+  version "2.24.0"
+  resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
+  integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
+
+chokidar@3.5.1, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.3.1, chokidar@^3.5.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
+  integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.3.0"
+  optionalDependencies:
+    fsevents "~2.1.2"
+
+chrome-trace-event@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
+  integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==
+  dependencies:
+    tslib "^1.9.0"
+
+ci-info@^3.1.1:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6"
+  integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==
+
+clean-stack@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
+  integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+
+cli-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
+  integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
+  dependencies:
+    restore-cursor "^3.1.0"
+
+cli-table3@~0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
+  integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
+  dependencies:
+    object-assign "^4.1.0"
+    string-width "^4.2.0"
+  optionalDependencies:
+    colors "^1.1.2"
+
+cli-truncate@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
+  integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
+  dependencies:
+    slice-ansi "^3.0.0"
+    string-width "^4.2.0"
+
+cliui@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+  dependencies:
+    string-width "^3.1.0"
+    strip-ansi "^5.2.0"
+    wrap-ansi "^5.1.0"
+
+cliui@^7.0.2:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^7.0.0"
+
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
+clone-response@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
+  integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
+  dependencies:
+    mimic-response "^1.0.0"
+
+coa@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
+  integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
+  dependencies:
+    "@types/q" "^1.5.1"
+    chalk "^2.4.1"
+    q "^1.1.2"
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+colord@^2.9.1:
+  version "2.9.1"
+  resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e"
+  integrity sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw==
+
+colorette@^1.2.0, colorette@^1.2.1, colorette@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
+  integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
+
+colorette@^2.0.14:
+  version "2.0.15"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.15.tgz#8e634aa0429b110d24be82eac4d42f5ea65ab2d5"
+  integrity sha512-lIFQhufWaVvwi4wOlX9Gx5b0Nmw3XAZ8HzHNH9dfxhe+JaKNTmX6QLk4o7UHyI+tUY8ClvyfaHUm5bf61O3psA==
+
+colors@^1.1.2, colors@^1.3.3:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+
+combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+commander@^2.12.1, commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
+  integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
+
+commander@^6.0.0, commander@^6.2.1:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+  integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
+
+commander@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.0.0.tgz#3e2bbfd8bb6724760980988fb5b22b7ee6b71ab2"
+  integrity sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA==
+
+commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+comment-json@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.1.1.tgz#49df4948704bebb1cc0ffa6910e25669b668b7c5"
+  integrity sha512-v8gmtPvxhBlhdRBLwdHSjGy9BgA23t9H1FctdQKyUrErPjSrJcdDMqBq9B4Irtm7w3TNYLQJNH6ARKnpyag1sA==
+  dependencies:
+    array-timsort "^1.0.3"
+    core-util-is "^1.0.2"
+    esprima "^4.0.1"
+    has-own-prop "^2.0.0"
+    repeat-string "^1.6.1"
+
+common-tags@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
+  integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==
+
+compare-versions@3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
+  integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concurrently@6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.3.0.tgz#63128cb4a6ed54d3c0ed8528728590a5fe54582a"
+  integrity sha512-k4k1jQGHHKsfbqzkUszVf29qECBrkvBKkcPJEUDTyVR7tZd1G/JOfnst4g1sYbFvJ4UjHZisj1aWQR8yLKpGPw==
+  dependencies:
+    chalk "^4.1.0"
+    date-fns "^2.16.1"
+    lodash "^4.17.21"
+    rxjs "^6.6.3"
+    spawn-command "^0.0.2-1"
+    supports-color "^8.1.0"
+    tree-kill "^1.2.2"
+    yargs "^16.2.0"
+
+constantinople@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151"
+  integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==
+  dependencies:
+    "@babel/parser" "^7.6.0"
+    "@babel/types" "^7.6.1"
+
+content-disposition@0.5.3:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
+  integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+  dependencies:
+    safe-buffer "5.1.2"
+
+core-util-is@1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+core-util-is@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+cosmiconfig@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
+  integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.2.1"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.10.0"
+
+crc-32@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
+  integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==
+  dependencies:
+    exit-on-epipe "~1.0.1"
+    printj "~1.1.0"
+
+create-require@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
+  integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+
+cross-env@7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
+  integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
+  dependencies:
+    cross-spawn "^7.0.1"
+
+cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+css-color-names@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67"
+  integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==
+
+css-declaration-sorter@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz#9dfd8ea0df4cc7846827876fafb52314890c21a9"
+  integrity sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==
+  dependencies:
+    timsort "^0.3.0"
+
+css-loader@6.5.1:
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.5.1.tgz#0c43d4fbe0d97f699c91e9818cb585759091d1b1"
+  integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==
+  dependencies:
+    icss-utils "^5.1.0"
+    postcss "^8.2.15"
+    postcss-modules-extract-imports "^3.0.0"
+    postcss-modules-local-by-default "^4.0.0"
+    postcss-modules-scope "^3.0.0"
+    postcss-modules-values "^4.0.0"
+    postcss-value-parser "^4.1.0"
+    semver "^7.3.5"
+
+css-select-base-adapter@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
+  integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
+
+css-select@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef"
+  integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^3.2.1"
+    domutils "^1.7.0"
+    nth-check "^1.0.2"
+
+css-select@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
+  integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^5.0.0"
+    domhandler "^4.2.0"
+    domutils "^2.6.0"
+    nth-check "^2.0.0"
+
+css-tree@1.0.0-alpha.37:
+  version "1.0.0-alpha.37"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
+  integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
+  dependencies:
+    mdn-data "2.0.4"
+    source-map "^0.6.1"
+
+css-tree@1.0.0-alpha.39:
+  version "1.0.0-alpha.39"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
+  integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
+  dependencies:
+    mdn-data "2.0.6"
+    source-map "^0.6.1"
+
+css-tree@^1.1.2, css-tree@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+  dependencies:
+    mdn-data "2.0.14"
+    source-map "^0.6.1"
+
+css-what@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1"
+  integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==
+
+css-what@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
+  integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^5.1.6:
+  version "5.1.6"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.6.tgz#1bdb83be6a6b1fee6dc5e9ec2e61286bcadcc7a6"
+  integrity sha512-X2nDeNGBXc0486oHjT2vSj+TdeyVsxRvJUxaOH50hOM6vSDLkKd0+59YXpSZRInJ4sNtBOykS4KsPfhdrU/35w==
+  dependencies:
+    css-declaration-sorter "^6.0.3"
+    cssnano-utils "^2.0.1"
+    postcss-calc "^8.0.0"
+    postcss-colormin "^5.2.1"
+    postcss-convert-values "^5.0.2"
+    postcss-discard-comments "^5.0.1"
+    postcss-discard-duplicates "^5.0.1"
+    postcss-discard-empty "^5.0.1"
+    postcss-discard-overridden "^5.0.1"
+    postcss-merge-longhand "^5.0.3"
+    postcss-merge-rules "^5.0.2"
+    postcss-minify-font-values "^5.0.1"
+    postcss-minify-gradients "^5.0.3"
+    postcss-minify-params "^5.0.1"
+    postcss-minify-selectors "^5.1.0"
+    postcss-normalize-charset "^5.0.1"
+    postcss-normalize-display-values "^5.0.1"
+    postcss-normalize-positions "^5.0.1"
+    postcss-normalize-repeat-style "^5.0.1"
+    postcss-normalize-string "^5.0.1"
+    postcss-normalize-timing-functions "^5.0.1"
+    postcss-normalize-unicode "^5.0.1"
+    postcss-normalize-url "^5.0.2"
+    postcss-normalize-whitespace "^5.0.1"
+    postcss-ordered-values "^5.0.2"
+    postcss-reduce-initial "^5.0.1"
+    postcss-reduce-transforms "^5.0.1"
+    postcss-svgo "^5.0.3"
+    postcss-unique-selectors "^5.0.1"
+
+cssnano-utils@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2"
+  integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==
+
+cssnano@5.0.10:
+  version "5.0.10"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.10.tgz#92207eb7c9c6dc08d318050726f9fad0adf7220b"
+  integrity sha512-YfNhVJJ04imffOpbPbXP2zjIoByf0m8E2c/s/HnvSvjXgzXMfgopVjAEGvxYOjkOpWuRQDg/OZFjO7WW94Ri8w==
+  dependencies:
+    cssnano-preset-default "^5.1.6"
+    is-resolvable "^1.1.0"
+    lilconfig "^2.0.3"
+    yaml "^1.10.2"
+
+csso@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903"
+  integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==
+  dependencies:
+    css-tree "1.0.0-alpha.39"
+
+csso@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
+  dependencies:
+    css-tree "^1.1.2"
+
+csstype@^2.6.8:
+  version "2.6.13"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f"
+  integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==
+
+cypress@8.5.0:
+  version "8.5.0"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.5.0.tgz#5712ca170913f8344bf167301205c4217c1eb9bd"
+  integrity sha512-MMkXIS+Ro2KETn4gAlG3tIc/7FiljuuCZP0zpd9QsRG6MZSyZW/l1J3D4iQM6WHsVxuX4rFChn5jPFlC2tNSvQ==
+  dependencies:
+    "@cypress/request" "^2.88.6"
+    "@cypress/xvfb" "^1.2.4"
+    "@types/node" "^14.14.31"
+    "@types/sinonjs__fake-timers" "^6.0.2"
+    "@types/sizzle" "^2.3.2"
+    arch "^2.2.0"
+    blob-util "^2.0.2"
+    bluebird "^3.7.2"
+    cachedir "^2.3.0"
+    chalk "^4.1.0"
+    check-more-types "^2.24.0"
+    cli-cursor "^3.1.0"
+    cli-table3 "~0.6.0"
+    commander "^5.1.0"
+    common-tags "^1.8.0"
+    dayjs "^1.10.4"
+    debug "^4.3.2"
+    enquirer "^2.3.6"
+    eventemitter2 "^6.4.3"
+    execa "4.1.0"
+    executable "^4.1.1"
+    extract-zip "2.0.1"
+    figures "^3.2.0"
+    fs-extra "^9.1.0"
+    getos "^3.2.1"
+    is-ci "^3.0.0"
+    is-installed-globally "~0.4.0"
+    lazy-ass "^1.6.0"
+    listr2 "^3.8.3"
+    lodash "^4.17.21"
+    log-symbols "^4.0.0"
+    minimist "^1.2.5"
+    ospath "^1.2.2"
+    pretty-bytes "^5.6.0"
+    proxy-from-env "1.0.0"
+    ramda "~0.27.1"
+    request-progress "^3.0.0"
+    supports-color "^8.1.1"
+    tmp "~0.2.1"
+    untildify "^4.0.0"
+    url "^0.11.0"
+    yauzl "^2.10.0"
+
+d@1, d@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
+  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+  dependencies:
+    es5-ext "^0.10.50"
+    type "^1.0.1"
+
+dashdash@^1.12.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+  integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+  dependencies:
+    assert-plus "^1.0.0"
+
+date-fns@2.25.0:
+  version "2.25.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
+  integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
+
+date-fns@^2.16.1:
+  version "2.19.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
+  integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg==
+
+dateformat@4.5.1:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.5.1.tgz#c20e7a9ca77d147906b6dc2261a8be0a5bd2173c"
+  integrity sha512-OD0TZ+B7yP7ZgpJf5K2DIbj3FZvFvxgFUuaqA/V5zTjAtAAXZ1E8bktHxmAGs4x5b7PflqA9LeQ84Og7wYtF7Q==
+
+dayjs@^1.10.4:
+  version "1.10.6"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.6.tgz#288b2aa82f2d8418a6c9d4df5898c0737ad02a63"
+  integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==
+
+debug@2, debug@^2.2.0, debug@^2.5.2:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
+
+debug@4, debug@4.3.1, debug@^4.1.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+  dependencies:
+    ms "2.1.2"
+
+debug@4.3.2, debug@^4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
+  dependencies:
+    ms "2.1.2"
+
+debug@=3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+  dependencies:
+    ms "2.0.0"
+
+debug@^3.1.0:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+  dependencies:
+    ms "^2.1.1"
+
+debug@^3.2.6:
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+  dependencies:
+    ms "^2.1.1"
+
+decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decamelize@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
+  integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
+
+decompress-response@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
+  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
+  dependencies:
+    mimic-response "^3.1.0"
+
+deep-email-validator@0.1.18:
+  version "0.1.18"
+  resolved "https://registry.yarnpkg.com/deep-email-validator/-/deep-email-validator-0.1.18.tgz#a072a93f28e11863cc6b9ca3ae964e0e45b3ece8"
+  integrity sha512-eo2WEUidQvppg6Qdek8iwOqmXvaxRJ2D2VJKbIOwUgLZNFveDDdJMBsFc+yq0S+lILEUcmzrJRrCWbyoe7QUzQ==
+  dependencies:
+    "@types/disposable-email-domains" "^1.0.1"
+    axios "^0.19.2"
+    disposable-email-domains "^1.0.53"
+    lodash "^4.17.15"
+    mailcheck "^1.1.1"
+    ts-jest "^25.2.1"
+
+deep-is@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+defer-to-connect@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1"
+  integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+  dependencies:
+    object-keys "^1.0.12"
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+detect-file@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
+  integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
+
+detect-node@2.1.0, detect-node@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
+  integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
+
+diff@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
+  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
+
+diff@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
+dijkstrajs@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
+  integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
+
+dir-glob@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+  integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+  dependencies:
+    path-type "^4.0.0"
+
+disposable-email-domains@^1.0.53:
+  version "1.0.58"
+  resolved "https://registry.yarnpkg.com/disposable-email-domains/-/disposable-email-domains-1.0.58.tgz#ac9c879c02c4f0898bfb6c0c80b959c0b0b7bc51"
+  integrity sha512-frnNCPqTjk6t/sosPoco6EIFHbP9SazHQkeltJNfZeUyNgewaVf+kFjEfVkVDVd436Vln43YElJPb8JozhBs7Q==
+
+doctrine@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+  dependencies:
+    esutils "^2.0.2"
+
+doctypes@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
+  integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
+
+dom-serializer@0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
+  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+  dependencies:
+    domelementtype "^2.0.1"
+    entities "^2.0.0"
+
+dom-serializer@^1.0.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be"
+  integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+    entities "^2.0.0"
+
+domelementtype@1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domelementtype@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
+  integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
+
+domelementtype@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
+  integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
+
+domhandler@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.1.0.tgz#c1d8d494d5ec6db22de99e46a149c2a4d23ddd43"
+  integrity sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domhandler@^4.2.0:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
+  integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domutils@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+domutils@^2.6.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.0"
+
+duplexer@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
+  integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
+
+ecc-jsbn@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+  integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+  dependencies:
+    jsbn "~0.1.0"
+    safer-buffer "^2.1.0"
+
+ecdsa-sig-formatter@1.0.11:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
+  integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
+  dependencies:
+    safe-buffer "^5.0.1"
+
+electron-to-chromium@^1.3.649:
+  version "1.3.672"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.672.tgz#3a6e335016dab4bc584d5292adc4f98f54541f6a"
+  integrity sha512-gFQe7HBb0lbOMqK2GAS5/1F+B0IMdYiAgB9OT/w1F4M7lgJK2aNOMNOM622aEax+nS1cTMytkiT0uMOkbtFmHw==
+
+electron-to-chromium@^1.3.712:
+  version "1.3.717"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz#78d4c857070755fb58ab64bcc173db1d51cbc25f"
+  integrity sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==
+
+electron-to-chromium@^1.3.723:
+  version "1.3.742"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.742.tgz#7223215acbbd3a5284962ebcb6df85d88b95f200"
+  integrity sha512-ihL14knI9FikJmH2XUIDdZFWJxvr14rPSdOhJ7PpS27xbz8qmaRwCwyg/bmFwjWKmWK9QyamiCZVCvXm5CH//Q==
+
+emoji-regex@^7.0.1:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+emojis-list@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
+  integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+
+end-of-stream@^1.1.0:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+  dependencies:
+    once "^1.4.0"
+
+enhanced-resolve@^5.0.0:
+  version "5.8.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.0.tgz#d9deae58f9d3773b6a111a5a46831da5be5c9ac0"
+  integrity sha512-Sl3KRpJA8OpprrtaIswVki3cWPiPKxXuFxJXBp+zNb6s6VwNWwFRUdtmzd2ReUut8n+sCPx7QCtQ7w5wfJhSgQ==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+enhanced-resolve@^5.7.0:
+  version "5.7.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz#525c5d856680fbd5052de453ac83e32049958b5c"
+  integrity sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+enhanced-resolve@^5.8.3:
+  version "5.8.3"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0"
+  integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+enquirer@^2.3.5, enquirer@^2.3.6:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+  integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+  dependencies:
+    ansi-colors "^4.1.1"
+
+entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
+  integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+
+envinfo@^7.7.3:
+  version "7.7.3"
+  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.7.3.tgz#4b2d8622e3e7366afb8091b23ed95569ea0208cc"
+  integrity sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA==
+
+error-ex@^1.3.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+  dependencies:
+    is-arrayish "^0.2.1"
+
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
+  version "1.17.5"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
+  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
+  dependencies:
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+    is-callable "^1.1.5"
+    is-regex "^1.0.5"
+    object-inspect "^1.7.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.0"
+    string.prototype.trimleft "^2.1.1"
+    string.prototype.trimright "^2.1.1"
+
+es-module-lexer@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.4.0.tgz#21f4181cc8b7eee06855f1c59e6087c7bc4f77b0"
+  integrity sha512-iuEGihqqhKWFgh72Q/Jtch7V2t/ft8w8IPP2aEN8ArYKO+IWyo6hsi96hCdgyeEDQIV3InhYQ9BlwUFPGXrbEQ==
+
+es-module-lexer@^0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.0.tgz#fe4c4621977bc668e285c5f1f70ca3b451095fda"
+  integrity sha512-qU2eN/XHsrl3E4y7mK1wdWnyy5c8gXtCbfP6Xcsemm7fPUR1PIV1JhZfP7ojcN0Fzp69CfrS3u76h2tusvfKiQ==
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+es5-ext@^0.10.35, es5-ext@^0.10.50:
+  version "0.10.53"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
+  integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
+  dependencies:
+    es6-iterator "~2.0.3"
+    es6-symbol "~3.1.3"
+    next-tick "~1.0.0"
+
+es6-iterator@~2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
+  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
+  dependencies:
+    d "^1.0.1"
+    ext "^1.1.2"
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-regexp@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/escape-regexp/-/escape-regexp-0.0.1.tgz#f44bda12d45bbdf9cb7f862ee7e4827b3dd32254"
+  integrity sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ=
+
+escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+eslint-plugin-vue@8.0.3:
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-8.0.3.tgz#791cc4543940319e612ea61a1d779e8c87cf749a"
+  integrity sha512-Rlhhy5ltzde0sRwSkqHuNePTXLMMaJ5+qsQubM4RYloYsQ8cXlnJT5MDaCzSirkGADipOHtmQXIbbPFAzUrADg==
+  dependencies:
+    eslint-utils "^3.0.0"
+    natural-compare "^1.4.0"
+    semver "^7.3.5"
+    vue-eslint-parser "^8.0.1"
+
+eslint-scope@5.1.1, eslint-scope@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
+
+eslint-scope@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-6.0.0.tgz#9cf45b13c5ac8f3d4c50f46a5121f61b3e318978"
+  integrity sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^5.2.0"
+
+eslint-utils@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
+  integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
+  dependencies:
+    eslint-visitor-keys "^2.0.0"
+
+eslint-visitor-keys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
+  integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
+
+eslint-visitor-keys@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186"
+  integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==
+
+eslint@8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.2.0.tgz#44d3fb506d0f866a506d97a0fc0e90ee6d06a815"
+  integrity sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==
+  dependencies:
+    "@eslint/eslintrc" "^1.0.4"
+    "@humanwhocodes/config-array" "^0.6.0"
+    ajv "^6.10.0"
+    chalk "^4.0.0"
+    cross-spawn "^7.0.2"
+    debug "^4.3.2"
+    doctrine "^3.0.0"
+    enquirer "^2.3.5"
+    escape-string-regexp "^4.0.0"
+    eslint-scope "^6.0.0"
+    eslint-utils "^3.0.0"
+    eslint-visitor-keys "^3.0.0"
+    espree "^9.0.0"
+    esquery "^1.4.0"
+    esutils "^2.0.2"
+    fast-deep-equal "^3.1.3"
+    file-entry-cache "^6.0.1"
+    functional-red-black-tree "^1.0.1"
+    glob-parent "^6.0.1"
+    globals "^13.6.0"
+    ignore "^4.0.6"
+    import-fresh "^3.0.0"
+    imurmurhash "^0.1.4"
+    is-glob "^4.0.0"
+    js-yaml "^4.1.0"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.4.1"
+    lodash.merge "^4.6.2"
+    minimatch "^3.0.4"
+    natural-compare "^1.4.0"
+    optionator "^0.9.1"
+    progress "^2.0.0"
+    regexpp "^3.2.0"
+    semver "^7.2.1"
+    strip-ansi "^6.0.1"
+    strip-json-comments "^3.1.0"
+    text-table "^0.2.0"
+    v8-compile-cache "^2.0.3"
+
+espree@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.0.0.tgz#e90a2965698228502e771c7a58489b1a9d107090"
+  integrity sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==
+  dependencies:
+    acorn "^8.5.0"
+    acorn-jsx "^5.3.1"
+    eslint-visitor-keys "^3.0.0"
+
+esprima@^4.0.0, esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+esquery@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
+  integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
+  dependencies:
+    estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
+  integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
+
+estraverse@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
+  integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+event-stream@=3.3.4:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
+  integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=
+  dependencies:
+    duplexer "~0.1.1"
+    from "~0"
+    map-stream "~0.1.0"
+    pause-stream "0.0.11"
+    split "0.3"
+    stream-combiner "~0.0.4"
+    through "~2.3.1"
+
+event-target-shim@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
+  integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
+eventemitter2@^6.4.3:
+  version "6.4.4"
+  resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b"
+  integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==
+
+eventemitter3@4.0.7, eventemitter3@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+events@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
+  integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
+
+execa@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
+  integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
+  dependencies:
+    cross-spawn "^7.0.0"
+    get-stream "^5.0.0"
+    human-signals "^1.1.1"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.0"
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+    strip-final-newline "^2.0.0"
+
+execa@5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+  integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+  dependencies:
+    cross-spawn "^7.0.3"
+    get-stream "^6.0.0"
+    human-signals "^2.1.0"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.1"
+    onetime "^5.1.2"
+    signal-exit "^3.0.3"
+    strip-final-newline "^2.0.0"
+
+execa@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376"
+  integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==
+  dependencies:
+    cross-spawn "^7.0.3"
+    get-stream "^6.0.0"
+    human-signals "^2.1.0"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.1"
+    onetime "^5.1.2"
+    signal-exit "^3.0.3"
+    strip-final-newline "^2.0.0"
+
+executable@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
+  integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==
+  dependencies:
+    pify "^2.2.0"
+
+exit-on-epipe@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
+  integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
+
+expand-tilde@^2.0.0, expand-tilde@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
+  integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
+  dependencies:
+    homedir-polyfill "^1.0.1"
+
+ext@^1.1.2:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
+  integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
+  dependencies:
+    type "^2.0.0"
+
+extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extract-zip@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
+  integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
+  dependencies:
+    debug "^4.1.1"
+    get-stream "^5.1.0"
+    yauzl "^2.10.0"
+  optionalDependencies:
+    "@types/yauzl" "^2.9.1"
+
+extsprintf@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+  integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+  integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+fast-deep-equal@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
+  integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+
+fast-deep-equal@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-glob@^3.1.1:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
+  integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.0"
+    merge2 "^1.3.0"
+    micromatch "^4.0.2"
+    picomatch "^2.2.1"
+
+fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fast-xml-parser@^3.19.0:
+  version "3.19.0"
+  resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01"
+  integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==
+
+fastest-levenshtein@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
+  integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
+
+fastq@^1.6.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481"
+  integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==
+  dependencies:
+    reusify "^1.0.4"
+
+fd-slicer@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
+  integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
+  dependencies:
+    pend "~1.2.0"
+
+feed@4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
+  integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==
+  dependencies:
+    xml-js "^1.6.11"
+
+figures@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+  integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
+  dependencies:
+    escape-string-regexp "^1.0.5"
+
+file-entry-cache@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+  dependencies:
+    flat-cache "^3.0.4"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-node-modules@^2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-2.1.2.tgz#57565a3455baf671b835bc6b2134a9b938b9c53c"
+  integrity sha512-x+3P4mbtRPlSiVE1Qco0Z4YLU8WFiFcuWTf3m75OV9Uzcfs2Bg+O9N+r/K0AnmINBW06KpfqKwYJbFlFq4qNug==
+  dependencies:
+    findup-sync "^4.0.0"
+    merge "^2.1.0"
+
+find-up@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+  dependencies:
+    locate-path "^6.0.0"
+    path-exists "^4.0.0"
+
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
+find-up@^4.0.0, find-up@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+findup-sync@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0"
+  integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==
+  dependencies:
+    detect-file "^1.0.0"
+    is-glob "^4.0.0"
+    micromatch "^4.0.2"
+    resolve-dir "^1.0.1"
+
+flat-cache@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+  dependencies:
+    flatted "^3.1.0"
+    rimraf "^3.0.2"
+
+flat@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
+  integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
+
+flatted@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067"
+  integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==
+
+follow-redirects@1.5.10:
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+  dependencies:
+    debug "=3.1.0"
+
+follow-redirects@^1.10.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
+  integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
+
+forever-agent@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+  integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
+  integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+form-data@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
+from@~0:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
+  integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
+
+fs-extra@^8.0.1:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+  dependencies:
+    graceful-fs "^4.2.0"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
+fs-extra@^9.1.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
+  integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
+  dependencies:
+    at-least-node "^1.0.0"
+    graceful-fs "^4.2.0"
+    jsonfile "^6.0.1"
+    universalify "^2.0.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@~2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
+  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+functional-red-black-tree@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+  integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
+get-caller-file@^2.0.1, get-caller-file@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-stream@^5.0.0, get-stream@^5.1.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
+  dependencies:
+    pump "^3.0.0"
+
+get-stream@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.0.tgz#3e0012cb6827319da2706e601a1583e8629a6718"
+  integrity sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==
+
+getos@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5"
+  integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==
+  dependencies:
+    async "^3.2.0"
+
+getpass@^0.1.1:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+  integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+  dependencies:
+    assert-plus "^1.0.0"
+
+glob-parent@^5.1.0, glob-parent@~5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-parent@^6.0.1:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+  integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+  dependencies:
+    is-glob "^4.0.3"
+
+glob-to-regexp@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+glob@7.1.6, glob@^7.1.1, glob@^7.1.3:
+  version "7.1.6"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+global-dirs@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
+  integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==
+  dependencies:
+    ini "2.0.0"
+
+global-modules@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
+  integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
+  dependencies:
+    global-prefix "^1.0.1"
+    is-windows "^1.0.1"
+    resolve-dir "^1.0.0"
+
+global-prefix@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
+  integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
+  dependencies:
+    expand-tilde "^2.0.2"
+    homedir-polyfill "^1.0.1"
+    ini "^1.3.4"
+    is-windows "^1.0.1"
+    which "^1.2.14"
+
+globals@^13.6.0:
+  version "13.7.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.7.0.tgz#aed3bcefd80ad3ec0f0be2cf0c895110c0591795"
+  integrity sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA==
+  dependencies:
+    type-fest "^0.20.2"
+
+globals@^13.9.0:
+  version "13.9.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb"
+  integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==
+  dependencies:
+    type-fest "^0.20.2"
+
+globby@^11.0.2, globby@^11.0.4:
+  version "11.0.4"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
+  integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
+  dependencies:
+    array-union "^2.1.0"
+    dir-glob "^3.0.1"
+    fast-glob "^3.1.1"
+    ignore "^5.1.4"
+    merge2 "^1.3.0"
+    slash "^3.0.0"
+
+got@11.8.2:
+  version "11.8.2"
+  resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
+  integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
+  dependencies:
+    "@sindresorhus/is" "^4.0.0"
+    "@szmarczak/http-timer" "^4.0.5"
+    "@types/cacheable-request" "^6.0.1"
+    "@types/responselike" "^1.0.0"
+    cacheable-lookup "^5.0.3"
+    cacheable-request "^7.0.1"
+    decompress-response "^6.0.0"
+    http2-wrapper "^1.0.0-beta.5.2"
+    lowercase-keys "^2.0.0"
+    p-cancelable "^2.0.0"
+    responselike "^2.0.0"
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
+  integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+
+graceful-fs@^4.2.0:
+  version "4.2.8"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
+  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
+
+growl@1.10.5:
+  version "1.10.5"
+  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
+  integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
+
+hammerjs@^2.0.8:
+  version "2.0.8"
+  resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
+  integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
+
+har-schema@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+  integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+  dependencies:
+    ajv "^6.5.5"
+    har-schema "^2.0.0"
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-own-prop@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af"
+  integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==
+
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+hash-sum@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
+  integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=
+
+hash-sum@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a"
+  integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==
+
+he@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+homedir-polyfill@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
+  integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
+  dependencies:
+    parse-passwd "^1.0.0"
+
+hpagent@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.1.tgz#66f67f16e5c7a8b59a068e40c2658c2c749ad5e2"
+  integrity sha512-IxJWQiY0vmEjetHdoE9HZjD4Cx+mYTr25tR7JCxXaiI3QxW0YqYyM11KyZbHufoa/piWhMb2+D3FGpMgmA2cFQ==
+
+http-cache-semantics@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
+  integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
+
+http-headers@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/http-headers/-/http-headers-3.0.2.tgz#5147771292f0b39d6778d930a3a59a76fc7ef44d"
+  integrity sha512-87E1I+2Wg4dxxz4rcxElo3dxO/w1ZtgL1yA0Sb6vH3qU16vRKq1NjWQv9SCY3ly2OQROcoxHZOUpmelS+k6wOw==
+  dependencies:
+    next-line "^1.1.0"
+
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+http2-wrapper@^1.0.0-beta.5.2:
+  version "1.0.0-beta.5.2"
+  resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3"
+  integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==
+  dependencies:
+    quick-lru "^5.1.1"
+    resolve-alpn "^1.0.0"
+
+http_ece@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75"
+  integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==
+  dependencies:
+    urlsafe-base64 "~1.0.0"
+
+https-proxy-agent@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
+  integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
+  dependencies:
+    agent-base "6"
+    debug "4"
+
+human-signals@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
+  integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
+
+human-signals@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+  integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
+iconv-lite@^0.4.4:
+  version "0.4.24"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
+icss-utils@^5.0.0, icss-utils@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
+  integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
+
+idb-keyval@5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.1.3.tgz#6ef5dff371897c23f144322dc6374eadd6a345d9"
+  integrity sha512-N9HbCK/FaXSRVI+k6Xq4QgWxbcZRUv+SfG1y7HJ28JdV8yEJu6k+C/YLea7npGckX2DQJeEVuMc4bKOBeU/2LQ==
+  dependencies:
+    safari-14-idb-fix "^1.0.4"
+
+ieee754@^1.1.4:
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
+  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+
+ignore@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+  integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
+ignore@^5.1.4:
+  version "5.1.8"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
+  integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
+
+immutable@^3.8.2:
+  version "3.8.2"
+  resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
+  integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
+
+import-fresh@^3.0.0, import-fresh@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
+  integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+import-local@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
+  integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
+  dependencies:
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+  integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+indent-string@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
+indexes-of@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+  integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@^2.0.1:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ini@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
+  integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
+
+ini@^1.3.4:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
+  integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
+
+insert-text-at-cursor@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/insert-text-at-cursor/-/insert-text-at-cursor-0.3.0.tgz#1819607680ec1570618347c4cd475e791faa25da"
+  integrity sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==
+
+interpret@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
+  integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+
+ip-address@^7.1.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-7.1.0.tgz#4a9c699e75b51cbeb18b38de8ed216efa1a490c5"
+  integrity sha512-V9pWC/VJf2lsXqP7IWJ+pe3P1/HCYGBMZrrnT62niLGjAfCbeiwXMUxaeHvnVlz19O27pvXP4azs+Pj/A0x+SQ==
+  dependencies:
+    jsbn "1.1.0"
+    sprintf-js "1.1.2"
+
+ip-cidr@3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.4.tgz#a915c47e00f47ea8d5f8ed662ea6161471c44375"
+  integrity sha512-pKNiqmBlTvEkhaLAa3+FOmYSY0/jjADVxxjA3NbujZZTT8mjLI90Q+6mwg6kd0fNm0RuAOkWJ1u1a/ETmlrPNQ==
+  dependencies:
+    ip-address "^7.1.0"
+    jsbn "^1.1.0"
+
+ip-regex@^4.0.0, ip-regex@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
+  integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
+
+ipaddr.js@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
+  integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
+
+is-absolute-url@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
+  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-callable@^1.1.4, is-callable@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
+  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
+
+is-ci@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994"
+  integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==
+  dependencies:
+    ci-info "^3.1.1"
+
+is-core-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.0.0.tgz#58531b70aed1db7c0e8d4eb1a0a2d1ddd64bd12d"
+  integrity sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw==
+  dependencies:
+    has "^1.0.3"
+
+is-core-module@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
+  integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
+  dependencies:
+    has "^1.0.3"
+
+is-date-object@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-expression@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab"
+  integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==
+  dependencies:
+    acorn "^7.1.1"
+    object-assign "^4.1.1"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-glob@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-installed-globally@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
+  integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==
+  dependencies:
+    global-dirs "^3.0.0"
+    is-path-inside "^3.0.2"
+
+is-ip@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8"
+  integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==
+  dependencies:
+    ip-regex "^4.0.0"
+
+is-number-like@^1.0.3:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3"
+  integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==
+  dependencies:
+    lodash.isfinite "^3.3.2"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-path-inside@^3.0.2:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
+  integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
+
+is-plain-obj@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+  integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
+is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+  dependencies:
+    isobject "^3.0.1"
+
+is-promise@^2.0.0:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
+  integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
+is-regex@^1.0.3, is-regex@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
+  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
+  dependencies:
+    has "^1.0.3"
+
+is-resolvable@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+
+is-svg@4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.1.tgz#8c63ec8c67c8c7f0a8de0a71c8c7d58eccf4406b"
+  integrity sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==
+  dependencies:
+    fast-xml-parser "^3.19.0"
+
+is-symbol@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+  dependencies:
+    has-symbols "^1.0.1"
+
+is-typedarray@^1.0.0, is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+  integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-unicode-supported@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
+  integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
+
+is-windows@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+  integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+isarray@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+jest-worker@^26.6.2:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
+  integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^7.0.0"
+
+jest-worker@^27.0.2:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed"
+  integrity sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^8.0.0"
+
+joi@^17.4.0:
+  version "17.4.2"
+  resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.2.tgz#02f4eb5cf88e515e614830239379dcbbe28ce7f7"
+  integrity sha512-Lm56PP+n0+Z2A2rfRvsfWVDXGEWjXxatPopkQ8qQ5mxCEhwHG+Ettgg5o98FFaxilOxozoa14cFhrE/hOzh/Nw==
+  dependencies:
+    "@hapi/hoek" "^9.0.0"
+    "@hapi/topo" "^5.0.0"
+    "@sideway/address" "^4.1.0"
+    "@sideway/formula" "^3.0.0"
+    "@sideway/pinpoint" "^2.0.0"
+
+jpeg-js@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.1.tgz#937a3ae911eb6427f151760f8123f04c8bfe6ef7"
+  integrity sha512-jA55yJiB5tCXEddos8JBbvW+IMrqY0y1tjjx9KNVtA+QPmu7ND5j0zkKopClpUTsaETL135uOM2XfcYG4XRjmw==
+
+js-levenshtein@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
+  integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
+
+js-stringify@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
+  integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds=
+
+js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
+  integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
+  dependencies:
+    argparse "^2.0.1"
+
+js-yaml@4.1.0, js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
+js-yaml@^3.13.1, js-yaml@^3.14.1:
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+jsbn@1.1.0, jsbn@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
+  integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA=
+
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+  integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+json-buffer@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
+json-parse-better-errors@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema-traverse@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+  integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
+json-schema@0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stable-stringify-without-jsonify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+  integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+
+json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+  integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5-loader@4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/json5-loader/-/json5-loader-4.0.1.tgz#6d17a1181e8f3c3d9204dca2a4ce4627306c8498"
+  integrity sha512-c9viNZlZTz0MTIcf/4qvek5Dz1/PU3DNCB4PwUhlEZIV3qb1bSD6vQQymlV17/Wm6ncra1aCvmIPsuRj+KfEEg==
+  dependencies:
+    json5 "^2.1.3"
+    loader-utils "^2.0.0"
+    schema-utils "^3.0.0"
+
+json5@2.2.0, json5@2.x:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
+  integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
+  dependencies:
+    minimist "^1.2.5"
+
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
+json5@^2.1.2, json5@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
+  integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
+  dependencies:
+    minimist "^1.2.5"
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsonfile@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
+  integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
+  dependencies:
+    universalify "^0.1.2"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsonfile@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+  integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+  dependencies:
+    universalify "^2.0.0"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsprim@^1.2.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+  dependencies:
+    assert-plus "1.0.0"
+    extsprintf "1.3.0"
+    json-schema "0.2.3"
+    verror "1.10.0"
+
+jstransformer@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
+  integrity sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=
+  dependencies:
+    is-promise "^2.0.0"
+    promise "^7.0.1"
+
+jwa@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
+  integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
+  dependencies:
+    buffer-equal-constant-time "1.0.1"
+    ecdsa-sig-formatter "1.0.11"
+    safe-buffer "^5.0.1"
+
+jws@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
+  integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
+  dependencies:
+    jwa "^2.0.0"
+    safe-buffer "^5.0.1"
+
+katex@0.13.18:
+  version "0.13.18"
+  resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.18.tgz#ba89e8e4b70cc2325e25e019a62b9fe71e5c2931"
+  integrity sha512-a3dC4NSVSDU3O1WZbTnOiA8rVNJ2lSiomOl0kmckCIGObccIHXof7gAseIY0o1gjEspe+34ZeSEX2D1ChFKIvA==
+  dependencies:
+    commander "^6.0.0"
+
+keyv@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254"
+  integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==
+  dependencies:
+    json-buffer "3.0.1"
+
+kind-of@^6.0.2:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+klona@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
+  integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
+
+langmap@0.0.16:
+  version "0.0.16"
+  resolved "https://registry.yarnpkg.com/langmap/-/langmap-0.0.16.tgz#2fe3e98a531fec0fec546624ebe168c2855bab56"
+  integrity sha512-AtYvBK7BsDvWwnSfmO7CfgeUy7GUT1wK3QX8eKH/Ey/eXodqoHuAtvdQ82hmWD9QVFVKnuiNjym9fGY4qSJeLA==
+
+lazy-ass@1.6.0, lazy-ass@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
+  integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM=
+
+levn@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+  dependencies:
+    prelude-ls "^1.2.1"
+    type-check "~0.4.0"
+
+lilconfig@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
+  integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
+
+lines-and-columns@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
+listr2@^3.8.3:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.11.0.tgz#9771b02407875aa78e73d6e0ff6541bbec0aaee9"
+  integrity sha512-XLJVe2JgXCyQTa3FbSv11lkKExYmEyA4jltVo8z4FX10Vt1Yj8IMekBfwim0BSOM9uj1QMTJvDQQpHyuPbB/dQ==
+  dependencies:
+    cli-truncate "^2.1.0"
+    colorette "^1.2.2"
+    log-update "^4.0.0"
+    p-map "^4.0.0"
+    rxjs "^6.6.7"
+    through "^2.3.8"
+    wrap-ansi "^7.0.0"
+
+loader-runner@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
+  integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
+
+loader-utils@^1.0.2:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
+  integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^1.0.1"
+
+loader-utils@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
+  integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^2.1.2"
+
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
+locate-path@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+  dependencies:
+    p-locate "^5.0.0"
+
+lodash.isequal@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.isfinite@^3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
+  integrity sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=
+
+lodash.memoize@4.x, lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.merge@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+lodash.once@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+  integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+log-symbols@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
+  integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
+  dependencies:
+    chalk "^4.0.0"
+
+log-symbols@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
+  integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
+  dependencies:
+    chalk "^4.1.0"
+    is-unicode-supported "^0.1.0"
+
+log-update@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
+  integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
+  dependencies:
+    ansi-escapes "^4.3.0"
+    cli-cursor "^3.1.0"
+    slice-ansi "^4.0.0"
+    wrap-ansi "^6.2.0"
+
+lowercase-keys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
+  integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+magic-string@^0.25.7:
+  version "0.25.7"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
+  integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
+  dependencies:
+    sourcemap-codec "^1.4.4"
+
+mailcheck@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/mailcheck/-/mailcheck-1.1.1.tgz#d87cf6ba0b64ba512199dbf93f1489f479591e34"
+  integrity sha1-2Hz2ugtkulEhmdv5PxSJ9HlZHjQ=
+
+make-error@1.x, make-error@^1.1.1:
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+  integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
+map-stream@~0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
+  integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=
+
+matter-js@0.17.1:
+  version "0.17.1"
+  resolved "https://registry.yarnpkg.com/matter-js/-/matter-js-0.17.1.tgz#b30ac4c708116258fbcaacd2efd8a94e34a91c7f"
+  integrity sha512-pSquoENJgvSAlQGcA0s5UkmEohGXZaUww2g3B6qG87x0iEcVf+aigMXn5UkFPdnh6w3B+C4vXSLaYqhHwKrOLA==
+
+mdn-data@2.0.14:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
+mdn-data@2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
+  integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+
+mdn-data@2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
+  integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
+
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+merge@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98"
+  integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==
+
+mfm-js@0.20.0:
+  version "0.20.0"
+  resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.20.0.tgz#3afdcd7959461fd825aa8af9b9e8a57cdbddc290"
+  integrity sha512-1+3tV3nWUKQNh/ztX3wXu5iLBtdsg6q3wUhl+XyOhc2H3sQdG+sih/w2c0nR9TIawjN+Z1/pvgGzxMJHfmKQmA==
+  dependencies:
+    twemoji-parser "13.1.x"
+
+micromatch@4.x:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
+  integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.2.3"
+
+micromatch@^4.0.0, micromatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.0.5"
+
+microseconds@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
+  integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
+
+mime-db@1.44.0:
+  version "1.44.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
+  integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
+
+mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19:
+  version "2.1.27"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
+  integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
+  dependencies:
+    mime-db "1.44.0"
+
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+mimic-response@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
+  integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
+
+mimic-response@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
+  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
+
+minimalistic-assert@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimatch@3.0.4, minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.2.0, minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+misskey-js@0.0.10:
+  version "0.0.10"
+  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.10.tgz#f305dd37cecfbaeb7a277d5e0c769ca12c6eb9a6"
+  integrity sha512-2rdqFrCOwggMKYitsUPyupesqCNpNooqEHQQRfdjttbhiqLbNFJE1UuWQ04ffmiJ08UJt+1ZN2kVAYNEN3HRsg==
+  dependencies:
+    autobind-decorator "^2.4.0"
+    eventemitter3 "^4.0.7"
+    reconnecting-websocket "^4.4.0"
+
+mkdirp@0.x, mkdirp@^0.5.3, mkdirp@~0.5.1:
+  version "0.5.5"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
+  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
+  dependencies:
+    minimist "^1.2.5"
+
+mocha@8.4.0:
+  version "8.4.0"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.4.0.tgz#677be88bf15980a3cae03a73e10a0fc3997f0cff"
+  integrity sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==
+  dependencies:
+    "@ungap/promise-all-settled" "1.1.2"
+    ansi-colors "4.1.1"
+    browser-stdout "1.3.1"
+    chokidar "3.5.1"
+    debug "4.3.1"
+    diff "5.0.0"
+    escape-string-regexp "4.0.0"
+    find-up "5.0.0"
+    glob "7.1.6"
+    growl "1.10.5"
+    he "1.2.0"
+    js-yaml "4.0.0"
+    log-symbols "4.0.0"
+    minimatch "3.0.4"
+    ms "2.1.3"
+    nanoid "3.1.20"
+    serialize-javascript "5.0.1"
+    strip-json-comments "3.1.1"
+    supports-color "8.1.1"
+    which "2.0.2"
+    wide-align "1.1.3"
+    workerpool "6.1.0"
+    yargs "16.2.0"
+    yargs-parser "20.2.4"
+    yargs-unparser "2.0.0"
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@2.1.3, ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+nano-time@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
+  integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
+  dependencies:
+    big-integer "^1.6.16"
+
+nanoid@3.1.20, nanoid@^3.1.20:
+  version "3.1.20"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
+  integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
+
+nanoid@^3.1.23:
+  version "3.1.23"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
+  integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
+
+nanoid@^3.1.30:
+  version "3.1.30"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
+  integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
+
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+needle@^2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.2.tgz#cf1a8fce382b5a280108bba90a14993c00e4010a"
+  integrity sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==
+  dependencies:
+    debug "^3.2.6"
+    iconv-lite "^0.4.4"
+    sax "^1.2.4"
+
+neo-async@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+nested-property@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-4.0.0.tgz#a67b5a31991e701e03cdbaa6453bc5b1011bb88d"
+  integrity sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==
+
+netmask@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
+  integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
+
+next-line@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603"
+  integrity sha1-/K5XhTBStqm66CCOQN19PC0wRgM=
+
+next-tick@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+  integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
+
+node-fetch@2.6.1, node-fetch@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
+  integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+
+node-gyp-build@~3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
+  integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
+
+node-releases@^1.1.70, node-releases@^1.1.71:
+  version "1.1.71"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb"
+  integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-url@^4.1.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
+  integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
+
+normalize-url@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
+  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
+
+npm-run-path@^4.0.0, npm-run-path@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
+
+nth-check@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+  dependencies:
+    boolbase "~1.0.0"
+
+nth-check@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
+  integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
+  dependencies:
+    boolbase "^1.0.0"
+
+oauth@0.9.15:
+  version "0.9.15"
+  resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
+  integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
+
+object-assign@^4.1.0, object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-inspect@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+  integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+  dependencies:
+    define-properties "^1.1.2"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    object-keys "^1.0.11"
+
+object.getownpropertydescriptors@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+object.values@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
+  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+oblivious-set@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
+  integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^5.1.0, onetime@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+  dependencies:
+    mimic-fn "^2.1.0"
+
+opentype.js@^0.4.3:
+  version "0.4.11"
+  resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.4.11.tgz#281a2390639cc15931c955d8d63c14a7c7772b41"
+  integrity sha1-KBojkGOcwVkxyVXY1jwUp8d3K0E=
+
+optionator@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
+  dependencies:
+    deep-is "^0.1.3"
+    fast-levenshtein "^2.0.6"
+    levn "^0.4.1"
+    prelude-ls "^1.2.1"
+    type-check "^0.4.0"
+    word-wrap "^1.2.3"
+
+ospath@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
+  integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=
+
+p-cancelable@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e"
+  integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==
+
+p-limit@^2.0.0, p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe"
+  integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
+p-locate@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+  dependencies:
+    p-limit "^3.0.2"
+
+p-map@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
+  integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
+  dependencies:
+    aggregate-error "^3.0.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-json@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646"
+  integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
+parse-passwd@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+  integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
+
+parse5@6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+  integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+pause-stream@0.0.11:
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
+  integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=
+  dependencies:
+    through "~2.3"
+
+pend@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+  integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+"photoswipe@git://github.com/dimsemenov/photoswipe#v5-beta":
+  version "5.1.7"
+  resolved "git://github.com/dimsemenov/photoswipe#60040164333bd257409669e715e4327afdb3aec7"
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7, picomatch@^2.2.1:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+  integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
+picomatch@^2.2.3:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
+  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
+
+pify@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+  integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pkg-dir@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
+pngjs@^3.3.0, pngjs@^3.3.1:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
+  integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
+
+portscanner@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1"
+  integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==
+  dependencies:
+    async "^2.6.0"
+    is-number-like "^1.0.3"
+
+postcss-calc@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a"
+  integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==
+  dependencies:
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.2"
+
+postcss-colormin@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.1.tgz#6e444a806fd3c578827dbad022762df19334414d"
+  integrity sha512-VVwMrEYLcHYePUYV99Ymuoi7WhKrMGy/V9/kTS0DkCoJYmmjdOMneyhzYUxcNgteKDVbrewOkSM7Wje/MFwxzA==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    colord "^2.9.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-convert-values@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz#879b849dc3677c7d6bc94b6a2c1a3f0808798059"
+  integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-discard-comments@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe"
+  integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==
+
+postcss-discard-duplicates@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d"
+  integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==
+
+postcss-discard-empty@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8"
+  integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==
+
+postcss-discard-overridden@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6"
+  integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==
+
+postcss-loader@6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.0.tgz#714370a3f567141cf4cadcdf9575f5234d186bc5"
+  integrity sha512-H9hv447QjQJVDbHj3OUdciyAXY3v5+UDduzEytAlZCVHCpNAAg/mCSwhYYqZr9BiGYhmYspU8QXxZwiHTLn3yA==
+  dependencies:
+    cosmiconfig "^7.0.0"
+    klona "^2.0.4"
+    semver "^7.3.5"
+
+postcss-merge-longhand@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.3.tgz#42194a5ffbaa5513edbf606ef79c44958564658b"
+  integrity sha512-kmB+1TjMTj/bPw6MCDUiqSA5e/x4fvLffiAdthra3a0m2/IjTrWsTmD3FdSskzUjEwkj5ZHBDEbv5dOcqD7CMQ==
+  dependencies:
+    css-color-names "^1.0.1"
+    postcss-value-parser "^4.1.0"
+    stylehacks "^5.0.1"
+
+postcss-merge-rules@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a"
+  integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    cssnano-utils "^2.0.1"
+    postcss-selector-parser "^6.0.5"
+    vendors "^1.0.3"
+
+postcss-minify-font-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf"
+  integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-minify-gradients@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.3.tgz#f970a11cc71e08e9095e78ec3a6b34b91c19550e"
+  integrity sha512-Z91Ol22nB6XJW+5oe31+YxRsYooxOdFKcbOqY/V8Fxse1Y3vqlNRpi1cxCqoACZTQEhl+xvt4hsbWiV5R+XI9Q==
+  dependencies:
+    colord "^2.9.1"
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-minify-params@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c"
+  integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    browserslist "^4.16.0"
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+    uniqs "^2.0.0"
+
+postcss-minify-selectors@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54"
+  integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    postcss-selector-parser "^6.0.5"
+
+postcss-modules-extract-imports@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
+  integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
+
+postcss-modules-local-by-default@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
+  integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
+  dependencies:
+    icss-utils "^5.0.0"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.1.0"
+
+postcss-modules-scope@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
+  integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
+  dependencies:
+    postcss-selector-parser "^6.0.4"
+
+postcss-modules-values@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
+  integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
+  dependencies:
+    icss-utils "^5.0.0"
+
+postcss-normalize-charset@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0"
+  integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==
+
+postcss-normalize-display-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd"
+  integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-positions@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5"
+  integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-repeat-style@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5"
+  integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-string@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0"
+  integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-timing-functions@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c"
+  integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-unicode@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37"
+  integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==
+  dependencies:
+    browserslist "^4.16.0"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-url@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz#ddcdfb7cede1270740cf3e4dfc6008bd96abc763"
+  integrity sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==
+  dependencies:
+    is-absolute-url "^3.0.3"
+    normalize-url "^6.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-whitespace@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a"
+  integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-ordered-values@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044"
+  integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-reduce-initial@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946"
+  integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==
+  dependencies:
+    browserslist "^4.16.0"
+    caniuse-api "^3.0.0"
+
+postcss-reduce-transforms@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640"
+  integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
+  integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
+  dependencies:
+    cssesc "^3.0.0"
+    indexes-of "^1.0.1"
+    uniq "^1.0.1"
+    util-deprecate "^1.0.2"
+
+postcss-selector-parser@^6.0.5:
+  version "6.0.6"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea"
+  integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-svgo@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30"
+  integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+    svgo "^2.7.0"
+
+postcss-unique-selectors@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc"
+  integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    postcss-selector-parser "^6.0.5"
+    uniqs "^2.0.0"
+
+postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
+  integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
+
+postcss@8.3.11:
+  version "8.3.11"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858"
+  integrity sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==
+  dependencies:
+    nanoid "^3.1.30"
+    picocolors "^1.0.0"
+    source-map-js "^0.6.2"
+
+postcss@^8.1.10:
+  version "8.2.8"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
+  integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
+  dependencies:
+    colorette "^1.2.2"
+    nanoid "^3.1.20"
+    source-map "^0.6.1"
+
+postcss@^8.2.15:
+  version "8.3.0"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f"
+  integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==
+  dependencies:
+    colorette "^1.2.2"
+    nanoid "^3.1.23"
+    source-map-js "^0.6.2"
+
+prelude-ls@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+pretty-bytes@^5.6.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
+  integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
+
+printj@~1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
+  integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
+
+prismjs@1.25.0:
+  version "1.25.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
+  integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
+
+private-ip@2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/private-ip/-/private-ip-2.3.3.tgz#1e80ff8443e5ac78f555631aec3ea6ff027fa6aa"
+  integrity sha512-5zyFfekIVUOTVbL92hc8LJOtE/gyGHeREHkJ2yTyByP8Q2YZVoBqLg3EfYLeF0oVvGqtaEX2t2Qovja0/gStXw==
+  dependencies:
+    ip-regex "^4.3.0"
+    ipaddr.js "^2.0.1"
+    is-ip "^3.1.0"
+    netmask "^2.0.2"
+
+probe-image-size@7.2.1:
+  version "7.2.1"
+  resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.1.tgz#df0c924e67e247bc94f8fcb0fad7f0081061fc44"
+  integrity sha512-d+6L3NvQBCNt4peRDoEfA7r9bPm6/qy18FnLKwg4NWBC5JrJm0pMLRg1kF4XNsPe1bUdt3WIMonPJzQWN2HXjQ==
+  dependencies:
+    lodash.merge "^4.6.2"
+    needle "^2.5.2"
+    stream-parser "~0.3.1"
+
+progress@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
+promise-limit@2.7.0:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/promise-limit/-/promise-limit-2.7.0.tgz#eb5737c33342a030eaeaecea9b3d3a93cb592b26"
+  integrity sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==
+
+promise@^7.0.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+  integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+  dependencies:
+    asap "~2.0.3"
+
+proxy-from-env@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
+  integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
+
+ps-tree@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.2.0.tgz#5e7425b89508736cdd4f2224d028f7bb3f722ebd"
+  integrity sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==
+  dependencies:
+    event-stream "=3.3.4"
+
+psl@^1.1.28:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+  integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+
+pug-attrs@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41"
+  integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==
+  dependencies:
+    constantinople "^4.0.1"
+    js-stringify "^1.0.2"
+    pug-runtime "^3.0.0"
+
+pug-code-gen@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce"
+  integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==
+  dependencies:
+    constantinople "^4.0.1"
+    doctypes "^1.1.0"
+    js-stringify "^1.0.2"
+    pug-attrs "^3.0.0"
+    pug-error "^2.0.0"
+    pug-runtime "^3.0.0"
+    void-elements "^3.1.0"
+    with "^7.0.0"
+
+pug-error@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5"
+  integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==
+
+pug-filters@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e"
+  integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==
+  dependencies:
+    constantinople "^4.0.1"
+    jstransformer "1.0.0"
+    pug-error "^2.0.0"
+    pug-walk "^2.0.0"
+    resolve "^1.15.1"
+
+pug-lexer@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5"
+  integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==
+  dependencies:
+    character-parser "^2.2.0"
+    is-expression "^4.0.0"
+    pug-error "^2.0.0"
+
+pug-linker@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708"
+  integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==
+  dependencies:
+    pug-error "^2.0.0"
+    pug-walk "^2.0.0"
+
+pug-load@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662"
+  integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==
+  dependencies:
+    object-assign "^4.1.1"
+    pug-walk "^2.0.0"
+
+pug-parser@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260"
+  integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==
+  dependencies:
+    pug-error "^2.0.0"
+    token-stream "1.0.0"
+
+pug-runtime@^3.0.0, pug-runtime@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7"
+  integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==
+
+pug-strip-comments@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e"
+  integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==
+  dependencies:
+    pug-error "^2.0.0"
+
+pug-walk@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe"
+  integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==
+
+pug@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535"
+  integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==
+  dependencies:
+    pug-code-gen "^3.0.2"
+    pug-filters "^4.0.0"
+    pug-lexer "^5.0.1"
+    pug-linker "^4.0.0"
+    pug-load "^3.0.0"
+    pug-parser "^6.0.0"
+    pug-runtime "^3.0.1"
+    pug-strip-comments "^2.0.0"
+
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+punycode@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+  integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@2.1.1, punycode@^2.1.0, punycode@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+pureimage@0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/pureimage/-/pureimage-0.3.5.tgz#cd5e91f7b6409fcf4880297aaa3e7fc0afc24d5e"
+  integrity sha512-+CFUEpoX6GemlKlHihI7Ii4IqKqF5KZjd682sAxwzbc4t4zU4Gwhxd4W3UMZW94nJzf0n4nA9zJrwTR4jZB4TA==
+  dependencies:
+    jpeg-js "^0.4.1"
+    opentype.js "^0.4.3"
+    pngjs "^3.3.1"
+
+q@^1.1.2:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+  integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
+qrcode@1.4.4:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
+  integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
+  dependencies:
+    buffer "^5.4.3"
+    buffer-alloc "^1.2.0"
+    buffer-from "^1.1.1"
+    dijkstrajs "^1.0.1"
+    isarray "^2.0.1"
+    pngjs "^3.3.0"
+    yargs "^13.2.4"
+
+qs@~6.5.2:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystring@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+  integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+quick-lru@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
+  integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+
+ramda@~0.27.1:
+  version "0.27.1"
+  resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
+  integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
+
+random-seed@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/random-seed/-/random-seed-0.3.0.tgz#d945f2e1f38f49e8d58913431b8bf6bb937556cd"
+  integrity sha1-2UXy4fOPSejViRNDG4v2u5N1Vs0=
+  dependencies:
+    json-stringify-safe "^5.0.1"
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+rangestr@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/rangestr/-/rangestr-0.0.1.tgz#f72ff9246f10f2a7d7c16e14616f617be2c2635a"
+  integrity sha1-9y/5JG8Q8qfXwW4UYW9he+LCY1o=
+
+ratelimiter@3.4.1:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/ratelimiter/-/ratelimiter-3.4.1.tgz#fa69e94937413382a926aaa17aaeaa6263af4659"
+  integrity sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==
+
+readdirp@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17"
+  integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==
+  dependencies:
+    picomatch "^2.0.7"
+
+rechoir@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca"
+  integrity sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==
+  dependencies:
+    resolve "^1.9.0"
+
+reconnecting-websocket@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
+  integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
+
+reflect-metadata@0.1.13:
+  version "0.1.13"
+  resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
+  integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
+
+regenerator-runtime@^0.13.4:
+  version "0.13.7"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+  integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
+
+regexpp@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
+  integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
+
+rename@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/rename/-/rename-1.0.4.tgz#a0f25078fa4195e650f73050c7c12ccf689f430b"
+  integrity sha1-oPJQePpBleZQ9zBQx8Esz2ifQws=
+  dependencies:
+    debug "^2.5.2"
+
+repeat-string@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+  integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+request-progress@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
+  integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=
+  dependencies:
+    throttleit "^1.0.0"
+
+request-stats@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/request-stats/-/request-stats-3.0.0.tgz#769155dc8974d78d4a1cb87bbf14eaab985afe25"
+  integrity sha1-dpFV3Il0141KHLh7vxTqq5ha/iU=
+  dependencies:
+    http-headers "^3.0.1"
+    once "^1.4.0"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+require-main-filename@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+resolve-alpn@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c"
+  integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==
+
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+  dependencies:
+    resolve-from "^5.0.0"
+
+resolve-dir@^1.0.0, resolve-dir@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
+  integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
+  dependencies:
+    expand-tilde "^2.0.0"
+    global-modules "^1.0.0"
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.15.1:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+  integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
+  dependencies:
+    is-core-module "^2.2.0"
+    path-parse "^1.0.6"
+
+resolve@^1.3.2, resolve@^1.9.0:
+  version "1.18.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130"
+  integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==
+  dependencies:
+    is-core-module "^2.0.0"
+    path-parse "^1.0.6"
+
+responselike@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723"
+  integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==
+  dependencies:
+    lowercase-keys "^2.0.0"
+
+restore-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
+  integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+  dependencies:
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+  dependencies:
+    glob "^7.1.3"
+
+rndstr@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/rndstr/-/rndstr-1.0.0.tgz#77e66fa8f9b4836853fdd91e50719591bb67d349"
+  integrity sha1-d+ZvqPm0g2hT/dkeUHGVkbtn00k=
+  dependencies:
+    rangestr "0.0.1"
+    seedrandom "2.4.2"
+
+run-parallel@^1.1.9:
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
+  integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
+
+rxjs@^6.6.3:
+  version "6.6.6"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70"
+  integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==
+  dependencies:
+    tslib "^1.9.0"
+
+rxjs@^6.6.7:
+  version "6.6.7"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+  integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+  dependencies:
+    tslib "^1.9.0"
+
+rxjs@^7.1.0:
+  version "7.3.0"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.3.0.tgz#39fe4f3461dc1e50be1475b2b85a0a88c1e938c6"
+  integrity sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==
+  dependencies:
+    tslib "~2.1.0"
+
+s-age@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/s-age/-/s-age-1.1.2.tgz#c0cf15233ccc93f41de92ea42c36d957977d1ea2"
+  integrity sha512-aSN2TlF39WLoZA/6cgYSJZhKt63kJ4EaadejPWjWY9/h4rksIqvfWY3gfd+3uAegSM1IXsA9aWeEhJtkxkFQtA==
+
+safari-14-idb-fix@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-1.0.4.tgz#5c68ba63e2a8ae0d89a0aa1e13fe89e3aef7da19"
+  integrity sha512-4+Y2baQdgJpzu84d0QjySl70Kyygzf0pepVg8NVg4NnQEPpfC91fAn0baNvtStlCjUUxxiu0BOMiafa98fRRuA==
+
+safe-buffer@5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sass-loader@12.3.0:
+  version "12.3.0"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.3.0.tgz#93278981c189c36a58cbfc37d4b9cef0cdc02871"
+  integrity sha512-6l9qwhdOb7qSrtOu96QQ81LVl8v6Dp9j1w3akOm0aWHyrTYtagDt5+kS32N4yq4hHk3M+rdqoRMH+lIdqvW6HA==
+  dependencies:
+    klona "^2.0.4"
+    neo-async "^2.6.2"
+
+sass@1.43.4:
+  version "1.43.4"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.4.tgz#68c7d6a1b004bef49af0d9caf750e9b252105d1f"
+  integrity sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==
+  dependencies:
+    chokidar ">=3.0.0 <4.0.0"
+
+sax@^1.2.4, sax@~1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+schema-utils@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
+  integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
+  dependencies:
+    "@types/json-schema" "^7.0.6"
+    ajv "^6.12.5"
+    ajv-keywords "^3.5.2"
+
+schema-utils@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.0.tgz#95986eb604f66daadeed56e379bfe7a7f963cdb9"
+  integrity sha512-tTEaeYkyIhEZ9uWgAjDerWov3T9MgX8dhhy2r0IGeeX4W8ngtGl1++dUve/RUqzuaASSh7shwCDJjEzthxki8w==
+  dependencies:
+    "@types/json-schema" "^7.0.7"
+    ajv "^6.12.5"
+    ajv-keywords "^3.5.2"
+
+secure-json-parse@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20"
+  integrity sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA==
+
+seedrandom@2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.2.tgz#18d78c41287d13aff8eadb29e235938b248aa9ff"
+  integrity sha1-GNeMQSh9E6/46tsp4jWTiySKqf8=
+
+seedrandom@3.0.5:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
+  integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
+
+semver@6.x:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+semver@^5.3.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
+  version "7.3.4"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
+  integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
+  dependencies:
+    lru-cache "^6.0.0"
+
+semver@^7.3.5:
+  version "7.3.5"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
+serialize-javascript@5.0.1, serialize-javascript@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
+  integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
+  dependencies:
+    randombytes "^2.1.0"
+
+serialize-javascript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+  dependencies:
+    randombytes "^2.1.0"
+
+set-blocking@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+signal-exit@^3.0.2, signal-exit@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
+  integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+
+slash@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+slice-ansi@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
+  integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
+  dependencies:
+    ansi-styles "^4.0.0"
+    astral-regex "^2.0.0"
+    is-fullwidth-code-point "^3.0.0"
+
+slice-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
+  integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
+  dependencies:
+    ansi-styles "^4.0.0"
+    astral-regex "^2.0.0"
+    is-fullwidth-code-point "^3.0.0"
+
+sortablejs@1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
+  integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
+
+source-list-map@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
+  integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+
+source-map-js@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
+  integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
+
+source-map-support@~0.5.19:
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@~0.7.2:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
+sourcemap-codec@^1.4.4:
+  version "1.4.8"
+  resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
+  integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
+
+spawn-command@^0.0.2-1:
+  version "0.0.2-1"
+  resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
+  integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=
+
+speakeasy@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/speakeasy/-/speakeasy-2.0.0.tgz#85c91a071b09a5cb8642590d983566165f57613a"
+  integrity sha1-hckaBxsJpcuGQlkNmDVmFl9XYTo=
+  dependencies:
+    base32.js "0.0.1"
+
+split@0.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
+  integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=
+  dependencies:
+    through "2"
+
+sprintf-js@1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
+  integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.7.0:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+  integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+  dependencies:
+    asn1 "~0.2.3"
+    assert-plus "^1.0.0"
+    bcrypt-pbkdf "^1.0.0"
+    dashdash "^1.12.0"
+    ecc-jsbn "~0.1.1"
+    getpass "^0.1.1"
+    jsbn "~0.1.0"
+    safer-buffer "^2.0.2"
+    tweetnacl "~0.14.0"
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+start-server-and-test@1.14.0:
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/start-server-and-test/-/start-server-and-test-1.14.0.tgz#c57f04f73eac15dd51733b551d775b40837fdde3"
+  integrity sha512-on5ELuxO2K0t8EmNj9MtVlFqwBMxfWOhu4U7uZD1xccVpFlOQKR93CSe0u98iQzfNxRyaNTb/CdadbNllplTsw==
+  dependencies:
+    bluebird "3.7.2"
+    check-more-types "2.24.0"
+    debug "4.3.2"
+    execa "5.1.1"
+    lazy-ass "1.6.0"
+    ps-tree "1.2.0"
+    wait-on "6.0.0"
+
+stream-combiner@~0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
+  integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=
+  dependencies:
+    duplexer "~0.1.1"
+
+stream-parser@~0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
+  integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=
+  dependencies:
+    debug "2"
+
+strict-event-emitter-types@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f"
+  integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==
+
+"string-width@^1.0.2 || 2":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+  integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+  dependencies:
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^4.0.0"
+
+string-width@^3.0.0, string-width@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
+  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+  dependencies:
+    emoji-regex "^7.0.1"
+    is-fullwidth-code-point "^2.0.0"
+    strip-ansi "^5.1.0"
+
+string-width@^4.1.0, string-width@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
+  integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.0"
+
+string.prototype.trimend@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
+  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+
+string.prototype.trimleft@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc"
+  integrity sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+    string.prototype.trimstart "^1.0.0"
+
+string.prototype.trimright@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3"
+  integrity sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+    string.prototype.trimend "^1.0.0"
+
+string.prototype.trimstart@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
+  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
+
+stringz@2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/stringz/-/stringz-2.1.0.tgz#5896b4713eac31157556040fb90258fb02c1630c"
+  integrity sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==
+  dependencies:
+    char-regex "^1.0.2"
+
+strip-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+  integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+  dependencies:
+    ansi-regex "^3.0.0"
+
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+  dependencies:
+    ansi-regex "^4.1.0"
+
+strip-ansi@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
+  integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
+  dependencies:
+    ansi-regex "^5.0.0"
+
+strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+  integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+style-loader@3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
+  integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
+
+stylehacks@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb"
+  integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==
+  dependencies:
+    browserslist "^4.16.0"
+    postcss-selector-parser "^6.0.4"
+
+supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.0, supports-color@^8.1.1:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.0.0, supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+svgo@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
+  integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
+  dependencies:
+    chalk "^2.4.1"
+    coa "^2.0.2"
+    css-select "^2.0.0"
+    css-select-base-adapter "^0.1.1"
+    css-tree "1.0.0-alpha.37"
+    csso "^4.0.2"
+    js-yaml "^3.13.1"
+    mkdirp "~0.5.1"
+    object.values "^1.1.0"
+    sax "~1.2.4"
+    stable "^0.1.8"
+    unquote "~1.1.1"
+    util.promisify "~1.0.0"
+
+svgo@^2.7.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
+  integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
+  dependencies:
+    "@trysound/sax" "0.2.0"
+    commander "^7.2.0"
+    css-select "^4.1.3"
+    css-tree "^1.1.3"
+    csso "^4.2.0"
+    picocolors "^1.0.0"
+    stable "^0.1.8"
+
+syuilo-password-strength@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/syuilo-password-strength/-/syuilo-password-strength-0.0.1.tgz#08f71a8f0ecb77db649f3d9a6424510d9d945f52"
+  integrity sha1-CPcajw7Ld9tknz2aZCRRDZ2UX1I=
+
+tapable@^2.1.1, tapable@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
+  integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
+
+terser-webpack-plugin@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz#7effadee06f7ecfa093dbbd3e9ab23f5f3ed8673"
+  integrity sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q==
+  dependencies:
+    jest-worker "^26.6.2"
+    p-limit "^3.1.0"
+    schema-utils "^3.0.0"
+    serialize-javascript "^5.0.1"
+    source-map "^0.6.1"
+    terser "^5.5.1"
+
+terser-webpack-plugin@^5.1.3:
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1"
+  integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==
+  dependencies:
+    jest-worker "^27.0.2"
+    p-limit "^3.1.0"
+    schema-utils "^3.0.0"
+    serialize-javascript "^6.0.0"
+    source-map "^0.6.1"
+    terser "^5.7.0"
+
+terser@^5.5.1:
+  version "5.5.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289"
+  integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.7.2"
+    source-map-support "~0.5.19"
+
+terser@^5.7.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.1.tgz#2dc7a61009b66bb638305cb2a824763b116bf784"
+  integrity sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.7.2"
+    source-map-support "~0.5.19"
+
+text-table@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+  integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+
+textarea-caret@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.1.0.tgz#5d5a35bb035fd06b2ff0e25d5359e97f2655087f"
+  integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==
+
+three@0.117.1:
+  version "0.117.1"
+  resolved "https://registry.yarnpkg.com/three/-/three-0.117.1.tgz#a49bcb1a6ddea2f250003e42585dc3e78e92b9d3"
+  integrity sha512-t4zeJhlNzUIj9+ub0l6nICVimSuRTZJOqvk3Rmlu+YGdTOJ49Wna8p7aumpkXJakJfITiybfpYE1XN1o1Z34UQ==
+
+throttle-debounce@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
+  integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
+
+throttleit@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
+  integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=
+
+through@2, through@^2.3.8, through@~2.3, through@~2.3.1:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+  integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
+tinycolor2@1.4.2:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
+  integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
+
+tmp@0.2.1, tmp@~0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
+  integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
+  dependencies:
+    rimraf "^3.0.0"
+
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+  integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+token-stream@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
+  integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=
+
+tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tree-kill@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
+  integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
+
+ts-jest@^25.2.1:
+  version "25.5.1"
+  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7"
+  integrity sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==
+  dependencies:
+    bs-logger "0.x"
+    buffer-from "1.x"
+    fast-json-stable-stringify "2.x"
+    json5 "2.x"
+    lodash.memoize "4.x"
+    make-error "1.x"
+    micromatch "4.x"
+    mkdirp "0.x"
+    semver "6.x"
+    yargs-parser "18.x"
+
+ts-loader@9.2.6:
+  version "9.2.6"
+  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74"
+  integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==
+  dependencies:
+    chalk "^4.1.0"
+    enhanced-resolve "^5.0.0"
+    micromatch "^4.0.0"
+    semver "^7.3.4"
+
+ts-node@10.4.0:
+  version "10.4.0"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7"
+  integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==
+  dependencies:
+    "@cspotcode/source-map-support" "0.7.0"
+    "@tsconfig/node10" "^1.0.7"
+    "@tsconfig/node12" "^1.0.7"
+    "@tsconfig/node14" "^1.0.0"
+    "@tsconfig/node16" "^1.0.2"
+    acorn "^8.4.1"
+    acorn-walk "^8.1.1"
+    arg "^4.1.0"
+    create-require "^1.1.0"
+    diff "^4.0.1"
+    make-error "^1.1.1"
+    yn "3.1.1"
+
+tsc-alias@1.3.10:
+  version "1.3.10"
+  resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.3.10.tgz#6ccf81c644092387ab9be3a3a75549a95eeffd80"
+  integrity sha512-7SF56qiV7Oh/bON+XjF/uAzEFqbmwCuEIHQyoTyVJAK80WnxaIyhO9TBwD/x8InIMU8lnvExQBOrgKkRPsHH+w==
+  dependencies:
+    "@jfonx/console-utils" "^1.0.3"
+    "@jfonx/file-utils" "^3.0.1"
+    chokidar "^3.5.0"
+    commander "^6.2.1"
+    find-node-modules "^2.1.0"
+    globby "^11.0.2"
+    normalize-path "^3.0.0"
+
+tsconfig-paths@3.11.0:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
+  integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==
+  dependencies:
+    "@types/json5" "^0.0.29"
+    json5 "^1.0.1"
+    minimist "^1.2.0"
+    strip-bom "^3.0.0"
+
+tslib@^1.13.0:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
+  integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
+
+tslib@^1.8.1, tslib@^1.9.0:
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
+  integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
+
+tslib@^1.9.3:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tslib@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
+  integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
+
+tslint-sonarts@1.9.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/tslint-sonarts/-/tslint-sonarts-1.9.0.tgz#feb593e92db328c0328b430b838adbe65d504de9"
+  integrity sha512-CJWt+IiYI8qggb2O/JPkS6CkC5DY1IcqRsm9EHJ+AxoWK70lvtP7jguochyNDMP2vIz/giGdWCfEM39x/I/Vnw==
+  dependencies:
+    immutable "^3.8.2"
+
+tslint@6.1.3:
+  version "6.1.3"
+  resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904"
+  integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    builtin-modules "^1.1.1"
+    chalk "^2.3.0"
+    commander "^2.12.1"
+    diff "^4.0.1"
+    glob "^7.1.1"
+    js-yaml "^3.13.1"
+    minimatch "^3.0.4"
+    mkdirp "^0.5.3"
+    resolve "^1.3.2"
+    semver "^5.3.0"
+    tslib "^1.13.0"
+    tsutils "^2.29.0"
+
+tsutils@^2.29.0:
+  version "2.29.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
+  integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==
+  dependencies:
+    tslib "^1.8.1"
+
+tsutils@^3.21.0:
+  version "3.21.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
+  dependencies:
+    tslib "^1.8.1"
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+  dependencies:
+    safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+twemoji-parser@13.1.0, twemoji-parser@13.1.x:
+  version "13.1.0"
+  resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4"
+  integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg==
+
+type-check@^0.4.0, type-check@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+  dependencies:
+    prelude-ls "^1.2.1"
+
+type-detect@4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+  integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
+type-fest@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+type-fest@^0.21.3:
+  version "0.21.3"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+  integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
+type@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
+  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
+
+type@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
+  integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
+
+typedarray-to-buffer@^3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+  integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+  dependencies:
+    is-typedarray "^1.0.0"
+
+typescript@4.4.4:
+  version "4.4.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
+  integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
+
+uniq@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+  integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+
+uniqs@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+  integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
+universalify@^0.1.0, universalify@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+universalify@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
+  integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+
+unload@2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/unload/-/unload-2.3.1.tgz#9d16862d372a5ce5cb630ad1309c2fd6e35dacfe"
+  integrity sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA==
+  dependencies:
+    "@babel/runtime" "^7.6.2"
+    detect-node "2.1.0"
+
+unquote@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
+  integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
+
+untildify@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
+  integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
+
+uri-js@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+  dependencies:
+    punycode "^2.1.0"
+
+url@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+  integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+  dependencies:
+    punycode "1.3.2"
+    querystring "0.2.0"
+
+urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6"
+  integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY=
+
+utf-8-validate@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.2.tgz#63cfbccd85dc1f2b66cf7a1d0eebc08ed056bfb3"
+  integrity sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==
+  dependencies:
+    node-gyp-build "~3.7.0"
+
+util-deprecate@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
+  integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.2"
+    has-symbols "^1.0.1"
+    object.getownpropertydescriptors "^2.1.0"
+
+uuid@7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
+  integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
+
+uuid@8.3.2, uuid@^8.3.2:
+  version "8.3.2"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+  integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+v-debounce@0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/v-debounce/-/v-debounce-0.1.2.tgz#cab75df7def2783215bf449ef85c69c2decf0a55"
+  integrity sha1-yrdd997yeDIVv0Se+Fxpwt7PClU=
+
+v8-compile-cache@^2.0.3:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
+  integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
+
+vanilla-tilt@1.7.2:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/vanilla-tilt/-/vanilla-tilt-1.7.2.tgz#59a5565d9f1f6d392a36969f223fb600dd101a81"
+  integrity sha512-arf2wY2Y65rP6Zxve9PnUUnRl9nQ1KenPNae6QRaVq/PEvaIto2bC4jYirNJ19U7nLkzI1H9O+nYtcQlX7BTsA==
+
+vendors@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
+  integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+
+verror@1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+  integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+  dependencies:
+    assert-plus "^1.0.0"
+    core-util-is "1.0.2"
+    extsprintf "^1.2.0"
+
+void-elements@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
+  integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
+
+vue-eslint-parser@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-8.0.1.tgz#25e08b20a414551531f3e19f999902e1ecf45f13"
+  integrity sha512-lhWjDXJhe3UZw2uu3ztX51SJAPGPey1Tff2RK3TyZURwbuI4vximQLzz4nQfCv8CZq4xx7uIiogHMMoSJPr33A==
+  dependencies:
+    debug "^4.3.2"
+    eslint-scope "^6.0.0"
+    eslint-visitor-keys "^3.0.0"
+    espree "^9.0.0"
+    esquery "^1.4.0"
+    lodash "^4.17.21"
+    semver "^7.3.5"
+
+vue-loader@16.7.0:
+  version "16.7.0"
+  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.7.0.tgz#ee161b2f6c27b0b459264c3c23d34068b471d574"
+  integrity sha512-43I0grWtwSCE8fiH/hAwFK+6sNlmvDuHhXScYH8HVSVAMS81IM66tgUOcxbPCeqhhz/1BE51YPxX59eZKGallQ==
+  dependencies:
+    chalk "^4.1.0"
+    hash-sum "^2.0.0"
+    loader-utils "^2.0.0"
+
+vue-prism-editor@2.0.0-alpha.2:
+  version "2.0.0-alpha.2"
+  resolved "https://registry.yarnpkg.com/vue-prism-editor/-/vue-prism-editor-2.0.0-alpha.2.tgz#aa53a88efaaed628027cbb282c2b1d37fc7c5c69"
+  integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
+
+vue-router@4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.5.tgz#dd0a4134bc950c37aef64b973e9ee1008428d8fa"
+  integrity sha512-AQq+pllb6FCc7rS6vh4PPcce3XA1jgK3hKNkQ4hXHwoVN7jOeAOMKCnX7XAX3etV9rmN7iNW8iIwgPk95ckBjw==
+
+vue-style-loader@4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
+  integrity sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==
+  dependencies:
+    hash-sum "^1.0.2"
+    loader-utils "^1.0.2"
+
+vue-svg-loader@0.17.0-beta.2:
+  version "0.17.0-beta.2"
+  resolved "https://registry.yarnpkg.com/vue-svg-loader/-/vue-svg-loader-0.17.0-beta.2.tgz#954b2a08b5488998dd81ec371ab5fb5ea4182ef7"
+  integrity sha512-iMUGJTKEcuNAG8VXOchjA8443IqEmEi2Aw6EVIHWma2cC4TUQ7Oet5Yry9IFfqXQXXvyzXz5EyttVvfRGTNH4Q==
+  dependencies:
+    loader-utils "^2.0.0"
+    semver "^7.3.2"
+    svgo "^1.3.2"
+
+vue@3.2.21:
+  version "3.2.21"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.21.tgz#55f5665172d95cf97e806b9aad0a375180be23a1"
+  integrity sha512-jpy7ckXdyclfRzqLjL4mtq81AkzQleE54KjZsJg/9OorNVurAxdlU5XpD49GpjKdnftuffKUvx2C5jDOrgc/zg==
+  dependencies:
+    "@vue/compiler-dom" "3.2.21"
+    "@vue/compiler-sfc" "3.2.21"
+    "@vue/runtime-dom" "3.2.21"
+    "@vue/server-renderer" "3.2.21"
+    "@vue/shared" "3.2.21"
+
+vuedraggable@4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.0.1.tgz#3bcaab0808b7944030b7d9a29f9a63d59dfa12c5"
+  integrity sha512-7qN5jhB1SLfx5P+HCm3JUW+pvgA1bSLgYLSVOeLWBDH9z+zbaEH0OlyZBVMLOxFR+JUHJjwDD0oy7T4r9TEgDA==
+  dependencies:
+    sortablejs "1.10.2"
+
+wait-on@6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.0.tgz#7e9bf8e3d7fe2daecbb7a570ac8ca41e9311c7e7"
+  integrity sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw==
+  dependencies:
+    axios "^0.21.1"
+    joi "^17.4.0"
+    lodash "^4.17.21"
+    minimist "^1.2.5"
+    rxjs "^7.1.0"
+
+watchpack@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.0.0.tgz#b12248f32f0fd4799b7be0802ad1f6573a45955c"
+  integrity sha512-xSdCxxYZWNk3VK13bZRYhsQpfa8Vg63zXG+3pyU8ouqSLRCv4IGXIp9Kr226q6GBkGRlZrST2wwKtjfKz2m7Cg==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"
+
+watchpack@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"
+  integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"
+
+web-push@3.4.5:
+  version "3.4.5"
+  resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.5.tgz#f94074ff150538872c7183e4d8881c8305920cf1"
+  integrity sha512-2njbTqZ6Q7ZqqK14YpK1GGmaZs3NmuGYF5b7abCXulUIWFSlSYcZ3NBJQRFcMiQDceD7vQknb8FUuvI1F7Qe/g==
+  dependencies:
+    asn1.js "^5.3.0"
+    http_ece "1.1.0"
+    https-proxy-agent "^5.0.0"
+    jws "^4.0.0"
+    minimist "^1.2.5"
+    urlsafe-base64 "^1.0.0"
+
+webpack-cli@4.9.1:
+  version "4.9.1"
+  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.9.1.tgz#b64be825e2d1b130f285c314caa3b1ba9a4632b3"
+  integrity sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==
+  dependencies:
+    "@discoveryjs/json-ext" "^0.5.0"
+    "@webpack-cli/configtest" "^1.1.0"
+    "@webpack-cli/info" "^1.4.0"
+    "@webpack-cli/serve" "^1.6.0"
+    colorette "^2.0.14"
+    commander "^7.0.0"
+    execa "^5.0.0"
+    fastest-levenshtein "^1.0.12"
+    import-local "^3.0.2"
+    interpret "^2.2.0"
+    rechoir "^0.7.0"
+    webpack-merge "^5.7.3"
+
+webpack-merge@^5.7.3:
+  version "5.7.3"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213"
+  integrity sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==
+  dependencies:
+    clone-deep "^4.0.1"
+    wildcard "^2.0.0"
+
+webpack-sources@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac"
+  integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==
+  dependencies:
+    source-list-map "^2.0.1"
+    source-map "^0.6.1"
+
+webpack-sources@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d"
+  integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==
+
+webpack@5.63.0:
+  version "5.63.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.63.0.tgz#4b074115800e0526d85112985e46c64b95e04aaf"
+  integrity sha512-HYrw6bkj/MDmphAXvqLEvn2fVoDZsYu6O638WjK6lSNgIpjb5jl/KtOrqJyU9EC/ZV9mLUmZW5h4mASB+CVA4A==
+  dependencies:
+    "@types/eslint-scope" "^3.7.0"
+    "@types/estree" "^0.0.50"
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/wasm-edit" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    acorn "^8.4.1"
+    acorn-import-assertions "^1.7.6"
+    browserslist "^4.14.5"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^5.8.3"
+    es-module-lexer "^0.9.0"
+    eslint-scope "5.1.1"
+    events "^3.2.0"
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.2.4"
+    json-parse-better-errors "^1.0.2"
+    loader-runner "^4.2.0"
+    mime-types "^2.1.27"
+    neo-async "^2.6.2"
+    schema-utils "^3.1.0"
+    tapable "^2.1.1"
+    terser-webpack-plugin "^5.1.3"
+    watchpack "^2.2.0"
+    webpack-sources "^3.2.0"
+
+webpack@^5:
+  version "5.33.2"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.33.2.tgz#c049717c9b038febf5a72fd2f53319ad59a8c1fc"
+  integrity sha512-X4b7F1sYBmJx8mlh2B7mV5szEkE0jYNJ2y3akgAP0ERi0vLCG1VvdsIxt8lFd4st6SUy0lf7W0CCQS566MBpJg==
+  dependencies:
+    "@types/eslint-scope" "^3.7.0"
+    "@types/estree" "^0.0.46"
+    "@webassemblyjs/ast" "1.11.0"
+    "@webassemblyjs/wasm-edit" "1.11.0"
+    "@webassemblyjs/wasm-parser" "1.11.0"
+    acorn "^8.0.4"
+    browserslist "^4.14.5"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^5.7.0"
+    es-module-lexer "^0.4.0"
+    eslint-scope "^5.1.1"
+    events "^3.2.0"
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.2.4"
+    json-parse-better-errors "^1.0.2"
+    loader-runner "^4.2.0"
+    mime-types "^2.1.27"
+    neo-async "^2.6.2"
+    schema-utils "^3.0.0"
+    tapable "^2.1.1"
+    terser-webpack-plugin "^5.1.1"
+    watchpack "^2.0.0"
+    webpack-sources "^2.1.1"
+
+websocket@1.0.34:
+  version "1.0.34"
+  resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111"
+  integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==
+  dependencies:
+    bufferutil "^4.0.1"
+    debug "^2.2.0"
+    es5-ext "^0.10.50"
+    typedarray-to-buffer "^3.1.5"
+    utf-8-validate "^5.0.2"
+    yaeti "^0.0.6"
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@2.0.2, which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+which@^1.2.14:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+wide-align@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+  integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+  dependencies:
+    string-width "^1.0.2 || 2"
+
+wildcard@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
+  integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+
+with@^7.0.0:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac"
+  integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==
+  dependencies:
+    "@babel/parser" "^7.9.6"
+    "@babel/types" "^7.9.6"
+    assert-never "^1.2.1"
+    babel-walk "3.0.0-canary-5"
+
+word-wrap@^1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+workerpool@6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b"
+  integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==
+
+wrap-ansi@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+  dependencies:
+    ansi-styles "^3.2.0"
+    string-width "^3.0.0"
+    strip-ansi "^5.0.0"
+
+wrap-ansi@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+  integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+ws@8.2.3:
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
+xml-js@^1.6.11:
+  version "1.6.11"
+  resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
+  integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
+  dependencies:
+    sax "^1.2.4"
+
+y18n@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
+  integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
+
+y18n@^5.0.5:
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
+  integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
+
+yaeti@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
+  integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yaml-ast-parser@0.0.43:
+  version "0.0.43"
+  resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb"
+  integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
+
+yaml@^1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
+  integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
+
+yaml@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yargs-parser@18.x:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-parser@20.2.4, yargs-parser@^20.2.2:
+  version "20.2.4"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
+  integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
+
+yargs-parser@^13.1.2:
+  version "13.1.2"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
+  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-unparser@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
+  integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
+  dependencies:
+    camelcase "^6.0.0"
+    decamelize "^4.0.0"
+    flat "^5.0.2"
+    is-plain-obj "^2.1.0"
+
+yargs@16.2.0, yargs@^16.2.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
+yargs@^13.2.4:
+  version "13.3.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
+  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+  dependencies:
+    cliui "^5.0.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^3.0.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^13.1.2"
+
+yauzl@^2.10.0:
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
+  integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
+  dependencies:
+    buffer-crc32 "~0.2.3"
+    fd-slicer "~1.1.0"
+
+yn@3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+  integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
diff --git a/packages/meta.json b/packages/meta.json
new file mode 100644
index 0000000000..9afdfcf1e1
--- /dev/null
+++ b/packages/meta.json
@@ -0,0 +1,3 @@
+{
+	"version": "unknown"
+}
diff --git a/src/.eslintrc b/src/.eslintrc
deleted file mode 100644
index d54e20f6b6..0000000000
--- a/src/.eslintrc
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-	"env": {
-		"node": true,
-		"commonjs": true
-	}
-}
diff --git a/src/@types/hcaptcha.d.ts b/src/@types/hcaptcha.d.ts
deleted file mode 100644
index afed587560..0000000000
--- a/src/@types/hcaptcha.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-declare module 'hcaptcha' {
-	interface IVerifyResponse {
-		success: boolean;
-		challenge_ts: string;
-		hostname: string;
-		credit?: boolean;
-		'error-codes'?: unknown[];
-	}
-
-	export function verify(secret: string, token: string): Promise<IVerifyResponse>;
-}
diff --git a/src/@types/http-signature.d.ts b/src/@types/http-signature.d.ts
deleted file mode 100644
index 8d484312dc..0000000000
--- a/src/@types/http-signature.d.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-declare module 'http-signature' {
-	import { IncomingMessage, ClientRequest } from 'http';
-
-	interface ISignature {
-		keyId: string;
-		algorithm: string;
-		headers: string[];
-		signature: string;
-	}
-
-	interface IOptions {
-		headers?: string[];
-		algorithm?: string;
-		strict?: boolean;
-		authorizationHeaderName?: string;
-	}
-
-	interface IParseRequestOptions extends IOptions {
-		clockSkew?: number;
-	}
-
-	interface IParsedSignature {
-		scheme: string;
-		params: ISignature;
-		signingString: string;
-		algorithm: string;
-		keyId: string;
-	}
-
-	type RequestSignerConstructorOptions =
-		IRequestSignerConstructorOptionsFromProperties |
-		IRequestSignerConstructorOptionsFromFunction;
-
-	interface IRequestSignerConstructorOptionsFromProperties {
-		keyId: string;
-		key: string | Buffer;
-		algorithm?: string;
-	}
-
-	interface IRequestSignerConstructorOptionsFromFunction {
-		sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void;
-	}
-
-	class RequestSigner {
-		constructor(options: RequestSignerConstructorOptions);
-
-		public writeHeader(header: string, value: string): string;
-
-		public writeDateHeader(): string;
-
-		public writeTarget(method: string, path: string): void;
-
-		public sign(cb: (err: any, authz: string) => void): void;
-	}
-
-	interface ISignRequestOptions extends IOptions {
-		keyId: string;
-		key: string;
-		httpVersion?: string;
-	}
-
-	export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
-	export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
-
-	export function sign(request: ClientRequest, options: ISignRequestOptions): boolean;
-	export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean;
-	export function createSigner(): RequestSigner;
-	export function isSigner(obj: any): obj is RequestSigner;
-
-	export function sshKeyToPEM(key: string): string;
-	export function sshKeyFingerprint(key: string): string;
-	export function pemToRsaSSHKey(pem: string, comment: string): string;
-
-	export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
-	export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
-	export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean;
-}
diff --git a/src/@types/jsrsasign.d.ts b/src/@types/jsrsasign.d.ts
deleted file mode 100644
index bc9d746f7e..0000000000
--- a/src/@types/jsrsasign.d.ts
+++ /dev/null
@@ -1,800 +0,0 @@
-// Attention: Partial Type Definition
-
-declare module 'jsrsasign' {
-	//// HELPER TYPES
-
-	/**
-	 * Attention: The value might be changed by the function.
-	 */
-	type Mutable<T> = T;
-
-	/**
-	 * Deprecated: The function might be deleted in future release.
-	 */
-	type Deprecated<T> = T;
-
-	//// COMMON TYPES
-
-	/**
-	 * byte number
-	 */
-	type ByteNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255;
-
-	/**
-	 * hexadecimal string /[0-9A-F]/
-	 */
-	type HexString = string;
-
-	/**
-	 * binary string /[01]/
-	 */
-	type BinString = string;
-
-	/**
-	 * base64 string /[A-Za-z0-9+/]=+/
-	 */
-	type Base64String = string;
-
-	/**
-	 * base64 URL encoded string /[A-Za-z0-9_-]/
-	 */
-	type Base64URLString = string;
-
-	/**
-	 * time value (ex. "151231235959Z")
-	 */
-	type TimeValue = string;
-
-	/**
-	 * OID string (ex. '1.2.3.4.567')
-	 */
-	type OID = string;
-
-	/**
-	 * OID name
-	 */
-	type OIDName = string;
-
-	/**
-	 * PEM formatted string
-	 */
-	type PEM = string;
-
-	//// ASN1 TYPES
-
-	class ASN1Object {
-		public isModified: boolean;
-
-		public hTLV: ASN1TLV;
-
-		public hT: ASN1T;
-
-		public hL: ASN1L;
-
-		public hV: ASN1V;
-
-		public getLengthHexFromValue(): HexString;
-
-		public getEncodedHex(): ASN1TLV;
-
-		public getValueHex(): ASN1V;
-
-		public getFreshValueHex(): ASN1V;
-	}
-
-	class DERAbstractStructured extends ASN1Object {
-		constructor(params?: Partial<Record<'array', ASN1Object[]>>);
-
-		public setByASN1ObjectArray(asn1ObjectArray: ASN1Object[]): void;
-
-		public appendASN1Object(asn1Object: ASN1Object): void;
-	}
-
-	class DERSequence extends DERAbstractStructured {
-		constructor(params?: Partial<Record<'array', ASN1Object[]>>);
-
-		public getFreshValueHex(): ASN1V;
-	}
-
-	//// ASN1HEX TYPES
-
-	/**
-	 * ASN.1 DER encoded data (hexadecimal string)
-	 */
-	type ASN1S = HexString;
-
-	/**
-	 * index of something
-	 */
-	type Idx<T extends { [idx: string]: unknown } | { [idx: number]: unknown }> = ASN1S extends { [idx: string]: unknown } ? string : ASN1S extends { [idx: number]: unknown } ? number : never;
-
-	/**
-	 * byte length of something
-	 */
-	type ByteLength<T extends { length: unknown }> = T['length'];
-
-	/**
-	 * ASN.1 L(length) (hexadecimal string)
-	 */
-	type ASN1L = HexString;
-
-	/**
-	 * ASN.1 T(tag) (hexadecimal string)
-	 */
-	type ASN1T = HexString;
-
-	/**
-	 * ASN.1 V(value) (hexadecimal string)
-	 */
-	type ASN1V = HexString;
-
-	/**
-	 * ASN.1 TLV (hexadecimal string)
-	 */
-	type ASN1TLV = HexString;
-
-	/**
-	 * ASN.1 object string
-	 */
-	type ASN1ObjectString = string;
-
-	/**
-	 * nth
-	 */
-	type Nth = number;
-
-	/**
-	 * ASN.1 DER encoded OID value (hexadecimal string)
-	 */
-	type ASN1OIDV = HexString;
-
-	class ASN1HEX {
-		public static getLblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1L>;
-
-		public static getL(s: ASN1S, idx: Idx<ASN1S>): ASN1L;
-
-		public static getVblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1V>;
-
-		public static getVidx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1V>;
-
-		public static getV(s: ASN1S, idx: Idx<ASN1S>): ASN1V;
-
-		public static getTLV(s: ASN1S, idx: Idx<ASN1S>): ASN1TLV;
-
-		public static getNextSiblingIdx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1ObjectString>;
-
-		public static getChildIdx(h: ASN1S, pos: Idx<ASN1S>): Idx<ASN1ObjectString>[];
-
-		public static getNthChildIdx(h: ASN1S, idx: Idx<ASN1S>, nth: Nth): Idx<ASN1ObjectString>;
-
-		public static getIdxbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): Idx<Mutable<Nth[]>>;
-
-		public static getTLVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): ASN1TLV;
-
-		// tslint:disable-next-line:bool-param-default
-		public static getVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string, removeUnusedbits?: boolean): ASN1V;
-
-		public static hextooidstr(hex: ASN1OIDV): OID;
-
-		public static dump(hexOrObj: ASN1S | ASN1Object, flags?: Record<string, unknown>, idx?: Idx<ASN1S>, indent?: string): string;
-
-		public static isASN1HEX(hex: string): hex is HexString;
-
-		public static oidname(oidDotOrHex: OID | ASN1OIDV): OIDName;
-	}
-
-	//// BIG INTEGER TYPES (PARTIAL)
-
-	class BigInteger {
-		constructor(a: null);
-
-		constructor(a: number, b: SecureRandom);
-
-		constructor(a: number, b: number, c: SecureRandom);
-
-		constructor(a: unknown);
-
-		constructor(a: string, b: number);
-
-		public am(i: number, x: number, w: number, j: number, c: number, n: number): number;
-
-		public DB: number;
-
-		public DM: number;
-
-		public DV: number;
-
-		public FV: number;
-
-		public F1: number;
-
-		public F2: number;
-
-		protected copyTo(r: Mutable<BigInteger>): void;
-
-		protected fromInt(x: number): void;
-
-		protected fromString(s: string, b: number): void;
-
-		protected clamp(): void;
-
-		public toString(b: number): string;
-
-		public negate(): BigInteger;
-
-		public abs(): BigInteger;
-
-		public compareTo(a: BigInteger): number;
-
-		public bitLength(): number;
-
-		protected dlShiftTo(n: number, r: Mutable<BigInteger>): void;
-
-		protected drShiftTo(n: number, r: Mutable<BigInteger>): void;
-
-		protected lShiftTo(n: number, r: Mutable<BigInteger>): void;
-
-		protected rShiftTo(n: number, r: Mutable<BigInteger>): void;
-
-		protected subTo(a: BigInteger, r: Mutable<BigInteger>): void;
-
-		protected multiplyTo(a: BigInteger, r: Mutable<BigInteger>): void;
-
-		protected squareTo(r: Mutable<BigInteger>): void;
-
-		protected divRemTo(m: BigInteger, q: Mutable<BigInteger>, r: Mutable<BigInteger>): void;
-
-		public mod(a: BigInteger): BigInteger;
-
-		protected invDigit(): number;
-
-		protected isEven(): boolean;
-
-		protected exp(e: number, z: Classic | Montgomery): BigInteger;
-
-		public modPowInt(e: number, m: BigInteger): BigInteger;
-
-		public static ZERO: BigInteger;
-
-		public static ONE: BigInteger;
-	}
-
-	class Classic {
-		constructor(m: BigInteger);
-
-		public convert(x: BigInteger): BigInteger;
-
-		public revert(x: BigInteger): BigInteger;
-
-		public reduce(x: Mutable<BigInteger>): void;
-
-		public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
-
-		public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
-	}
-
-	class Montgomery {
-		constructor(m: BigInteger);
-
-		public convert(x: BigInteger): BigInteger;
-
-		public revert(x: BigInteger): BigInteger;
-
-		public reduce(x: Mutable<BigInteger>): void;
-
-		public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
-
-		public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
-	}
-
-	//// KEYUTIL TYPES
-
-	type DecryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
-
-	type Decrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
-
-	type DecryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
-
-	type EncryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
-
-	type Encrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
-
-	type EncryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
-
-	type AlgList = {
-		'AES-256-CBC':  { 'proc': DecryptAES;  'eproc': EncryptAES;  keylen: 32; ivlen: 16; };
-		'AES-192-CBC':  { 'proc': DecryptAES;  'eproc': EncryptAES;  keylen: 24; ivlen: 16; };
-		'AES-128-CBC':  { 'proc': DecryptAES;  'eproc': EncryptAES;  keylen: 16; ivlen: 16; };
-		'DES-EDE3-CBC': { 'proc': Decrypt3DES; 'eproc': Encrypt3DES; keylen: 24; ivlen: 8;  };
-		'DES-CBC':      { 'proc': DecryptDES;  'eproc': EncryptDES;  keylen: 8;  ivlen: 8;  };
-	};
-
-	type AlgName = keyof AlgList;
-
-	type PEMHeadAlgName = 'RSA' | 'EC' | 'DSA';
-
-	type GetKeyRSAParam = RSAKey | {
-		n: BigInteger;
-		e: number;
-	} | Record<'n' | 'e', HexString> | Record<'n' | 'e', HexString> & Record<'d' | 'p' | 'q' | 'dp' | 'dq' | 'co', HexString | null> | {
-		n: BigInteger;
-		e: number;
-		d: BigInteger;
-	} | {
-		kty: 'RSA';
-	} & Record<'n' | 'e', Base64URLString> | {
-		kty: 'RSA';
-	} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
-		kty: 'RSA';
-	} & Record<'n' | 'e' | 'd', Base64URLString>;
-
-	type GetKeyECDSAParam = KJUR.crypto.ECDSA | {
-		curve: KJUR.crypto.CurveName;
-		xy: HexString;
-	} | {
-		curve: KJUR.crypto.CurveName;
-		d: HexString;
-	} | {
-		kty: 'EC';
-		crv: KJUR.crypto.CurveName;
-		x: Base64URLString;
-		y: Base64URLString;
-	} | {
-		kty: 'EC';
-		crv: KJUR.crypto.CurveName;
-		x: Base64URLString;
-		y: Base64URLString;
-		d: Base64URLString;
-	};
-
-	type GetKeyDSAParam = KJUR.crypto.DSA | Record<'p' | 'q' | 'g', BigInteger> & Record<'y', BigInteger | null> | Record<'p' | 'q' | 'g' | 'x', BigInteger> & Record<'y', BigInteger | null>;
-
-	type GetKeyParam = GetKeyRSAParam | GetKeyECDSAParam | GetKeyDSAParam | string;
-
-	class KEYUTIL {
-		public version: '1.0.0';
-
-		public parsePKCS5PEM(sPKCS5PEM: PEM): Partial<Record<'type' | 's', string>> & (Record<'cipher' | 'ivsalt', string> | Record<'cipher' | 'ivsalt', undefined>);
-
-		public getKeyAndUnusedIvByPasscodeAndIvsalt(algName: AlgName, passcode: string, ivsaltHex: HexString): Record<'keyhex' | 'ivhex', HexString>;
-
-		public decryptKeyB64(privateKeyB64: Base64String, sharedKeyAlgName: AlgName, sharedKeyHex: HexString, ivsaltHex: HexString): Base64String;
-
-		public getDecryptedKeyHex(sEncryptedPEM: PEM, passcode: string): HexString;
-
-		public getEncryptedPKCS5PEMFromPrvKeyHex(pemHeadAlg: PEMHeadAlgName, hPrvKey: string, passcode: string, sharedKeyAlgName?: AlgName | null, ivsaltHex?: HexString | null): PEM;
-
-		public parseHexOfEncryptedPKCS8(sHEX: HexString): {
-			ciphertext: ASN1V;
-			encryptionSchemeAlg: 'TripleDES';
-			encryptionSchemeIV: ASN1V;
-			pbkdf2Salt: ASN1V;
-			pbkdf2Iter: number;
-		};
-
-		public getPBKDF2KeyHexFromParam(info: ReturnType<this['parseHexOfEncryptedPKCS8']>, passcode: string): HexString;
-
-		private _getPlainPKCS8HexFromEncryptedPKCS8PEM(pkcs8PEM: PEM, passcode: string): HexString;
-
-		public getKeyFromEncryptedPKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
-
-		public parsePlainPrivatePKCS8Hex(pkcs8PrvHex: HexString): {
-			algparam: ASN1V | null;
-			algoid: ASN1V;
-			keyidx: Idx<ASN1V>;
-		};
-
-		public getKeyFromPlainPrivatePKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
-
-		public getKeyFromPlainPrivatePKCS8Hex(prvKeyHex: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
-
-		private _getKeyFromPublicPKCS8Hex(h: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
-
-		public parsePublicRawRSAKeyHex(pubRawRSAHex: HexString): Record<'n' | 'e', ASN1V>;
-
-		public parsePublicPKCS8Hex(pkcs8PubHex: HexString): {
-			algparam: ASN1V | Record<'p' | 'q' | 'g', ASN1V> | null;
-			algoid: ASN1V;
-			key: ASN1V;
-		};
-
-		public static getKey(param: GetKeyRSAParam): RSAKey;
-
-		public static getKey(param: GetKeyECDSAParam): KJUR.crypto.ECDSA;
-
-		public static getKey(param: GetKeyDSAParam): KJUR.crypto.DSA;
-
-		public static getKey(param: string, passcode?: string, hextype?: string): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
-
-		public static generateKeypair(alg: 'RSA', keylen: number): Record<'prvKeyObj' | 'pubKeyObj', RSAKey>;
-
-		public static generateKeypair(alg: 'EC', curve: KJUR.crypto.CurveName): Record<'prvKeyObj' | 'pubKeyObj', KJUR.crypto.ECDSA>;
-
-		public static getPEM(keyObjOrHex: RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA, formatType?: 'PKCS1PRV' | 'PKCS5PRV' | 'PKCS8PRV', passwd?: string, encAlg?: 'DES-CBC' | 'DES-EDE3-CBC' | 'AES-128-CBC' | 'AES-192-CBC' | 'AES-256-CBC', hexType?: string, ivsaltHex?: HexString): object; // To Do
-
-		public static getKeyFromCSRPEM(csrPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
-
-		public static getKeyFromCSRHex(csrHex: HexString): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
-
-		public static parseCSRHex(csrHex: HexString): Record<'p8pubkeyhex', ASN1TLV>;
-
-		public static getJWKFromKey(keyObj: RSAKey): {
-			kty: 'RSA';
-		} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
-			kty: 'RSA';
-		} & Record<'n' | 'e', Base64URLString>;
-
-		public static getJWKFromKey(keyObj: KJUR.crypto.ECDSA): {
-			kty: 'EC';
-			crv: KJUR.crypto.CurveName;
-			x: Base64URLString;
-			y: Base64URLString;
-			d: Base64URLString;
-		} | {
-			kty: 'EC';
-			crv: KJUR.crypto.CurveName;
-			x: Base64URLString;
-			y: Base64URLString;
-		};
-	}
-
-	//// KJUR NAMESPACE (PARTIAL)
-
-	namespace KJUR {
-		namespace crypto {
-			type CurveName = 'secp128r1' | 'secp160k1' | 'secp160r1' | 'secp192k1' | 'secp192r1' | 'secp224r1' | 'secp256k1' | 'secp256r1' | 'secp384r1' | 'secp521r1';
-
-			class DSA {
-				public p: BigInteger | null;
-
-				public q: BigInteger | null;
-
-				public g: BigInteger | null;
-
-				public y: BigInteger | null;
-
-				public x: BigInteger | null;
-
-				public type: 'DSA';
-
-				public isPrivate: boolean;
-
-				public isPublic: boolean;
-
-				public setPrivate(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger | null, x: BigInteger): void;
-
-				public setPrivateHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString | null, hX: HexString): void;
-
-				public setPublic(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger): void;
-
-				public setPublicHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString): void;
-
-				public signWithMessageHash(sHashHex: HexString): HexString;
-
-				public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
-
-				public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
-
-				public readPKCS5PrvKeyHex(h: HexString): void;
-
-				public readPKCS8PrvKeyHex(h: HexString): void;
-
-				public readPKCS8PubKeyHex(h: HexString): void;
-
-				public readCertPubKeyHex(h: HexString, nthPKI: number): void;
-			}
-
-			class ECDSA {
-				constructor(params?: {
-					curve?: CurveName;
-					prv?: HexString;
-					pub?: HexString;
-				});
-
-				public p: BigInteger | null;
-
-				public q: BigInteger | null;
-
-				public g: BigInteger | null;
-
-				public y: BigInteger | null;
-
-				public x: BigInteger | null;
-
-				public type: 'EC';
-
-				public isPrivate: boolean;
-
-				public isPublic: boolean;
-
-				public getBigRandom(limit: BigInteger): BigInteger;
-
-				public setNamedCurve(curveName: CurveName): void;
-
-				public setPrivateKeyHex(prvKeyHex: HexString): void;
-
-				public setPublicKeyHex(pubKeyHex: HexString): void;
-
-				public getPublicKeyXYHex(): Record<'x' | 'y', HexString>;
-
-				public getShortNISTPCurveName(): 'P-256' | 'P-384' | null;
-
-				public generateKeyPairHex(): Record<'ecprvhex' | 'ecpubhex', HexString>;
-
-				public signWithMessageHash(hashHex: HexString): HexString;
-
-				public signHex(hashHex: HexString, privHex: HexString): HexString;
-
-				public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
-
-				public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
-
-				public readPKCS5PrvKeyHex(h: HexString): void;
-
-				public readPKCS8PrvKeyHex(h: HexString): void;
-
-				public readPKCS8PubKeyHex(h: HexString): void;
-
-				public readCertPubKeyHex(h: HexString, nthPKI: number): void;
-
-				public static parseSigHex(sigHex: HexString): Record<'r' | 's', BigInteger>;
-
-				public static parseSigHexInHexRS(sigHex: HexString): Record<'r' | 's', ASN1V>;
-
-				public static asn1SigToConcatSig(asn1Sig: HexString): HexString;
-
-				public static concatSigToASN1Sig(concatSig: HexString): ASN1TLV;
-
-				public static hexRSSigToASN1Sig(hR: HexString, hS: HexString): ASN1TLV;
-
-				public static biRSSigToASN1Sig(biR: BigInteger, biS: BigInteger): ASN1TLV;
-
-				public static getName(s: CurveName | HexString): 'secp256r1' | 'secp256k1' | 'secp384r1' | null;
-			}
-
-			class Signature {
-				constructor(params?: ({
-					alg: string;
-					prov?: string;
-				} | {}) & ({
-					psssaltlen: number;
-				} | {}) & ({
-					prvkeypem: PEM;
-					prvkeypas?: never;
-				} | {}));
-
-				private _setAlgNames(): void;
-
-				private _zeroPaddingOfSignature(hex: HexString, bitLength: number): HexString;
-
-				public setAlgAndProvider(alg: string, prov: string): void;
-
-				public init(key: GetKeyParam, pass?: string): void;
-
-				public updateString(str: string): void;
-
-				public updateHex(hex: HexString): void;
-
-				public sign(): HexString;
-
-				public signString(str: string): HexString;
-
-				public signHex(hex: HexString): HexString;
-
-				public verify(hSigVal: string): boolean | 0;
-			}
-		}
-	}
-
-	//// RSAKEY TYPES
-
-	class RSAKey {
-		public n: BigInteger | null;
-
-		public e: number;
-
-		public d: BigInteger | null;
-
-		public p: BigInteger | null;
-
-		public q: BigInteger | null;
-
-		public dmp1: BigInteger | null;
-
-		public dmq1: BigInteger | null;
-
-		public coeff: BigInteger | null;
-
-		public type: 'RSA';
-
-		public isPrivate?: boolean;
-
-		public isPublic?: boolean;
-
-		//// RSA PUBLIC
-
-		protected doPublic(x: BigInteger): BigInteger;
-
-		public setPublic(N: BigInteger, E: number): void;
-
-		public setPublic(N: HexString, E: HexString): void;
-
-		public encrypt(text: string): HexString | null;
-
-		public encryptOAEP(text: string, hash?: string | ((s: string) => string), hashLen?: number): HexString | null;
-
-		//// RSA PRIVATE
-
-		protected doPrivate(x: BigInteger): BigInteger;
-
-		public setPrivate(N: BigInteger, E: number, D: BigInteger): void;
-
-		public setPrivate(N: HexString, E: HexString, D: HexString): void;
-
-		public setPrivateEx(N: HexString, E: HexString, D?: HexString | null, P?: HexString | null, Q?: HexString | null, DP?: HexString | null, DQ?: HexString | null, C?: HexString | null): void;
-
-		public generate(B: number, E: HexString): void;
-
-		public decrypt(ctext: HexString): string;
-
-		public decryptOAEP(ctext: HexString, hash?: string | ((s: string) => string), hashLen?: number): string | null;
-
-		//// RSA PEM
-
-		public getPosArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
-
-		public getHexValueArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
-
-		public readPrivateKeyFromPEMString(keyPEM: PEM): void;
-
-		public readPKCS5PrvKeyHex(h: HexString): void;
-
-		public readPKCS8PrvKeyHex(h: HexString): void;
-
-		public readPKCS5PubKeyHex(h: HexString): void;
-
-		public readPKCS8PubKeyHex(h: HexString): void;
-
-		public readCertPubKeyHex(h: HexString, nthPKI: Nth): void;
-
-		//// RSA SIGN
-
-		public sign(s: string, hashAlg: string): HexString;
-
-		public signWithMessageHash(sHashHex: HexString, hashAlg: string): HexString;
-
-		public signPSS(s: string, hashAlg: string, sLen: number): HexString;
-
-		public signWithMessageHashPSS(hHash: HexString, hashAlg: string, sLen: number): HexString;
-
-		public verify(sMsg: string, hSig: HexString): boolean | 0;
-
-		public verifyWithMessageHash(sHashHex: HexString, hSig: HexString): boolean | 0;
-
-		public verifyPSS(sMsg: string, hSig: HexString, hashAlg: string, sLen: number): boolean;
-
-		public verifyWithMessageHashPSS(hHash: HexString, hSig: HexString, hashAlg: string, sLen: number): boolean;
-
-		public static SALT_LEN_HLEN: -1;
-
-		public static SALT_LEN_MAX: -2;
-
-		public static SALT_LEN_RECOVER: -2;
-	}
-
-	/// RNG TYPES
-	class SecureRandom {
-		public nextBytes(ba: Mutable<ByteNumber[]>): void;
-	}
-
-	//// X509 TYPES
-
-	type ExtInfo = {
-		critical: boolean;
-		oid: OID;
-		vidx: Idx<ASN1V>;
-	};
-
-	type ExtAIAInfo = Record<'ocsp' | 'caissuer', string>;
-
-	type ExtCertificatePolicy = {
-		id: OIDName;
-	} & Partial<{
-		cps: string;
-	} | {
-		unotice: string;
-	}>;
-
-	class X509 {
-		public hex: HexString | null;
-
-		public version: number;
-
-		public foffset: number;
-
-		public aExtInfo: null;
-
-		public getVersion(): number;
-
-		public getSerialNumberHex(): ASN1V;
-
-		public getSignatureAlgorithmField(): OIDName;
-
-		public getIssuerHex(): ASN1TLV;
-
-		public getIssuerString(): HexString;
-
-		public getSubjectHex(): ASN1TLV;
-
-		public getSubjectString(): HexString;
-
-		public getNotBefore(): TimeValue;
-
-		public getNotAfter(): TimeValue;
-
-		public getPublicKeyHex(): ASN1TLV;
-
-		public getPublicKeyIdx(): Idx<Mutable<Nth[]>>;
-
-		public getPublicKeyContentIdx(): Idx<Mutable<Nth[]>>;
-
-		public getPublicKey(): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
-
-		public getSignatureAlgorithmName(): OIDName;
-
-		public getSignatureValueHex(): ASN1V;
-
-		public verifySignature(pubKey: GetKeyParam): boolean | 0;
-
-		public parseExt(): void;
-
-		public getExtInfo(oidOrName: OID | string): ExtInfo | undefined;
-
-		public getExtBasicConstraints(): ExtInfo | {} | {
-			cA: true;
-			pathLen?: number;
-		};
-
-		public getExtKeyUsageBin(): BinString;
-
-		public getExtKeyUsageString(): string;
-
-		public getExtSubjectKeyIdentifier(): ASN1V | undefined;
-
-		public getExtAuthorityKeyIdentifier(): {
-			kid: ASN1V;
-		} | undefined;
-
-		public getExtExtKeyUsageName(): OIDName[] | undefined;
-
-		public getExtSubjectAltName(): Deprecated<string[]>;
-
-		public getExtSubjectAltName2(): ['MAIL' | 'DNS' | 'DN' | 'URI' | 'IP', string][] | undefined;
-
-		public getExtCRLDistributionPointsURI(): string[] | undefined;
-
-		public getExtAIAInfo(): ExtAIAInfo | undefined;
-
-		public getExtCertificatePolicies(): ExtCertificatePolicy[] | undefined;
-
-		public readCertPEM(sCertPEM: PEM): void;
-
-		public readCertHex(sCertHex: HexString): void;
-
-		public getInfo(): string;
-
-		public static hex2dn(hex: HexString, idx?: Idx<HexString>): string;
-
-		public static hex2rdn(hex: HexString, idx?: Idx<HexString>): string;
-
-		public static hex2attrTypeValue(hex: HexString, idx?: Idx<HexString>): string;
-
-		public static getPublicKeyFromCertPEM(sCertPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
-
-		public static getPublicKeyInfoPropOfCertPEM(sCertPEM: PEM): {
-			algparam: ASN1V | null;
-			leyhex: ASN1V;
-			algoid: ASN1V;
-		};
-	}
-}
diff --git a/src/@types/koa-json-body.d.ts b/src/@types/koa-json-body.d.ts
deleted file mode 100644
index 5aa8179c5b..0000000000
--- a/src/@types/koa-json-body.d.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-declare module 'koa-json-body' {
-	import { Middleware } from 'koa';
-
-	interface IKoaJsonBodyOptions {
-		strict: boolean;
-		limit: string;
-		fallback: boolean;
-	}
-
-	function koaJsonBody(opt?: IKoaJsonBodyOptions): Middleware;
-
-	namespace koaJsonBody {} // Hack
-
-	export = koaJsonBody;
-}
diff --git a/src/@types/koa-slow.d.ts b/src/@types/koa-slow.d.ts
deleted file mode 100644
index e748e2cc98..0000000000
--- a/src/@types/koa-slow.d.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-declare module 'koa-slow' {
-	import { Middleware } from 'koa';
-
-	interface ISlowOptions {
-		url?: RegExp;
-		delay?: number;
-	}
-
-	function slow(options?: ISlowOptions): Middleware;
-
-	namespace slow {} // Hack
-
-	export = slow;
-}
diff --git a/src/@types/langmap.d.ts b/src/@types/langmap.d.ts
deleted file mode 100644
index a0f99028ab..0000000000
--- a/src/@types/langmap.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-declare module 'langmap' {
-	type Lang = {
-		nativeName: string;
-		englishName: string;
-	};
-
-	const langmap: { [lang: string]: Lang };
-
-	export = langmap;
-}
diff --git a/src/@types/ms.d.ts b/src/@types/ms.d.ts
deleted file mode 100644
index 2f0156d104..0000000000
--- a/src/@types/ms.d.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-declare module 'ms' {
-	interface IMSOptions {
-		long: boolean;
-	}
-
-	function ms(value: string): number;
-	function ms(value: number, options?: IMSOptions): string;
-
-	namespace ms {} // Hack
-
-	export = ms;
-}
diff --git a/src/@types/os-utils.d.ts b/src/@types/os-utils.d.ts
deleted file mode 100644
index 390df17d39..0000000000
--- a/src/@types/os-utils.d.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-declare module 'os-utils' {
-	type FreeCommandCallback = (usedmem: number) => void;
-
-	type HarddriveCallback = (total: number, free: number, used: number) => void;
-
-	type GetProcessesCallback = (result: string) => void;
-
-	type CPUCallback = (perc: number) => void;
-
-	export function platform(): NodeJS.Platform;
-	export function cpuCount(): number;
-	export function sysUptime(): number;
-	export function processUptime(): number;
-
-	export function freemem(): number;
-	export function totalmem(): number;
-	export function freememPercentage(): number;
-	export function freeCommand(callback: FreeCommandCallback): void;
-
-	export function harddrive(callback: HarddriveCallback): void;
-
-	export function getProcesses(callback: GetProcessesCallback): void;
-	export function getProcesses(nProcess: number, callback: GetProcessesCallback): void;
-
-	export function allLoadavg(): string;
-	export function loadavg(_time?: number): number;
-
-	export function cpuFree(callback: CPUCallback): void;
-	export function cpuUsage(callback: CPUCallback): void;
-}
diff --git a/src/@types/package.json.d.ts b/src/@types/package.json.d.ts
deleted file mode 100644
index abe5fae687..0000000000
--- a/src/@types/package.json.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-declare module '*/package.json' {
-	interface IRepository {
-		type: string;
-		url: string;
-	}
-
-	export const name: string;
-	export const version: string;
-	export const repository: IRepository;
-}
diff --git a/src/@types/probe-image-size.d.ts b/src/@types/probe-image-size.d.ts
deleted file mode 100644
index 665edcf2e7..0000000000
--- a/src/@types/probe-image-size.d.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-declare module 'probe-image-size' {
-	import { ReadStream } from 'fs';
-
-	type ProbeOptions = {
-		retries: 1;
-		timeout: 30000;
-	};
-
-	type ProbeResult = {
-		width: number;
-		height: number;
-		length?: number;
-		type: string;
-		mime: string;
-		wUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex';
-		hUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex';
-		url?: string;
-	};
-
-	function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise<ProbeResult>;
-	function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void;
-	function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void;
-
-	namespace probeImageSize {} // Hack
-
-	export = probeImageSize;
-}
diff --git a/src/boot/index.ts b/src/boot/index.ts
deleted file mode 100644
index cb4c8536db..0000000000
--- a/src/boot/index.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import * as cluster from 'cluster';
-import * as chalk from 'chalk';
-import Xev from 'xev';
-
-import Logger from '@/services/logger';
-import { envOption } from '../env';
-
-// for typeorm
-import 'reflect-metadata';
-import { masterMain } from './master';
-import { workerMain } from './worker';
-
-const logger = new Logger('core', 'cyan');
-const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
-const ev = new Xev();
-
-/**
- * Init process
- */
-export default async function() {
-	process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
-
-	if (cluster.isMaster || envOption.disableClustering) {
-		await masterMain();
-
-		if (cluster.isMaster) {
-			ev.mount();
-		}
-	}
-
-	if (cluster.isWorker || envOption.disableClustering) {
-		await workerMain();
-	}
-
-	// ユニットテスト時にMisskeyが子プロセスで起動された時のため
-	// それ以外のときは process.send は使えないので弾く
-	if (process.send) {
-		process.send('ok');
-	}
-}
-
-//#region Events
-
-// Listen new workers
-cluster.on('fork', worker => {
-	clusterLogger.debug(`Process forked: [${worker.id}]`);
-});
-
-// Listen online workers
-cluster.on('online', worker => {
-	clusterLogger.debug(`Process is now online: [${worker.id}]`);
-});
-
-// Listen for dying workers
-cluster.on('exit', worker => {
-	// Replace the dead worker,
-	// we're not sentimental
-	clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
-	cluster.fork();
-});
-
-// Display detail of unhandled promise rejection
-if (!envOption.quiet) {
-	process.on('unhandledRejection', console.dir);
-}
-
-// Display detail of uncaught exception
-process.on('uncaughtException', err => {
-	try {
-		logger.error(err);
-	} catch { }
-});
-
-// Dying away...
-process.on('exit', code => {
-	logger.info(`The process is going to exit with code ${code}`);
-});
-
-//#endregion
diff --git a/src/boot/master.ts b/src/boot/master.ts
deleted file mode 100644
index 071b37b76d..0000000000
--- a/src/boot/master.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-import * as fs from 'fs';
-import { fileURLToPath } from 'url';
-import { dirname } from 'path';
-import * as os from 'os';
-import * as cluster from 'cluster';
-import * as chalk from 'chalk';
-import * as portscanner from 'portscanner';
-import { getConnection } from 'typeorm';
-
-import Logger from '@/services/logger';
-import loadConfig from '@/config/load';
-import { Config } from '@/config/types';
-import { lessThan } from '@/prelude/array';
-import { envOption } from '../env';
-import { showMachineInfo } from '@/misc/show-machine-info';
-import { initDb } from '../db/postgre';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-const meta = JSON.parse(fs.readFileSync(`${_dirname}/../meta.json`, 'utf-8'));
-
-const logger = new Logger('core', 'cyan');
-const bootLogger = logger.createSubLogger('boot', 'magenta', false);
-
-function greet() {
-	if (!envOption.quiet) {
-		//#region Misskey logo
-		const v = `v${meta.version}`;
-		console.log('  _____ _         _           ');
-		console.log(' |     |_|___ ___| |_ ___ _ _ ');
-		console.log(' | | | | |_ -|_ -| \'_| -_| | |');
-		console.log(' |_|_|_|_|___|___|_,_|___|_  |');
-		console.log(' ' + chalk.gray(v) + ('                        |___|\n'.substr(v.length)));
-		//#endregion
-
-		console.log(' Misskey is an open-source decentralized microblogging platform.');
-		console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
-
-		console.log('');
-		console.log(chalk`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`);
-	}
-
-	bootLogger.info('Welcome to Misskey!');
-	bootLogger.info(`Misskey v${meta.version}`, null, true);
-}
-
-function isRoot() {
-	// maybe process.getuid will be undefined under not POSIX environment (e.g. Windows)
-	return process.getuid != null && process.getuid() === 0;
-}
-
-/**
- * Init master process
- */
-export async function masterMain() {
-	let config!: Config;
-
-	// initialize app
-	try {
-		greet();
-		showEnvironment();
-		await showMachineInfo(bootLogger);
-		showNodejsVersion();
-		config = loadConfigBoot();
-		await connectDb();
-		await validatePort(config);
-	} catch (e) {
-		bootLogger.error('Fatal error occurred during initialization', null, true);
-		process.exit(1);
-	}
-
-	bootLogger.succ('Misskey initialized');
-
-	if (!envOption.disableClustering) {
-		await spawnWorkers(config.clusterLimit);
-	}
-
-	bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
-
-	if (!envOption.noDaemons) {
-		require('../daemons/server-stats').default();
-		require('../daemons/queue-stats').default();
-		require('../daemons/janitor').default();
-	}
-}
-
-const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
-const requiredNodejsVersion = [11, 7, 0];
-const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
-
-function showEnvironment(): void {
-	const env = process.env.NODE_ENV;
-	const logger = bootLogger.createSubLogger('env');
-	logger.info(typeof env === 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
-
-	if (env !== 'production') {
-		logger.warn('The environment is not in production mode.');
-		logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
-	}
-
-	logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
-}
-
-function showNodejsVersion(): void {
-	const nodejsLogger = bootLogger.createSubLogger('nodejs');
-
-	nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`);
-
-	if (!satisfyNodejsVersion) {
-		nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true);
-		process.exit(1);
-	}
-}
-
-function loadConfigBoot(): Config {
-	const configLogger = bootLogger.createSubLogger('config');
-	let config;
-
-	try {
-		config = loadConfig();
-	} catch (exception) {
-		if (typeof exception === 'string') {
-			configLogger.error(exception);
-			process.exit(1);
-		}
-		if (exception.code === 'ENOENT') {
-			configLogger.error('Configuration file not found', null, true);
-			process.exit(1);
-		}
-		throw exception;
-	}
-
-	configLogger.succ('Loaded');
-
-	return config;
-}
-
-async function connectDb(): Promise<void> {
-	const dbLogger = bootLogger.createSubLogger('db');
-
-	// Try to connect to DB
-	try {
-		dbLogger.info('Connecting...');
-		await initDb();
-		const v = await getConnection().query('SHOW server_version').then(x => x[0].server_version);
-		dbLogger.succ(`Connected: v${v}`);
-	} catch (e) {
-		dbLogger.error('Cannot connect', null, true);
-		dbLogger.error(e);
-		process.exit(1);
-	}
-}
-
-async function validatePort(config: Config): Promise<void> {
-	const isWellKnownPort = (port: number) => port < 1024;
-
-	async function isPortAvailable(port: number): Promise<boolean> {
-		return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
-	}
-
-	if (config.port == null || Number.isNaN(config.port)) {
-		bootLogger.error('The port is not configured. Please configure port.', null, true);
-		process.exit(1);
-	}
-
-	if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
-		bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
-		process.exit(1);
-	}
-
-	if (!await isPortAvailable(config.port)) {
-		bootLogger.error(`Port ${config.port} is already in use`, null, true);
-		process.exit(1);
-	}
-}
-
-async function spawnWorkers(limit: number = 1) {
-	const workers = Math.min(limit, os.cpus().length);
-	bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
-	await Promise.all([...Array(workers)].map(spawnWorker));
-	bootLogger.succ('All workers started');
-}
-
-function spawnWorker(): Promise<void> {
-	return new Promise(res => {
-		const worker = cluster.fork();
-		worker.on('message', message => {
-			if (message !== 'ready') return;
-			res();
-		});
-	});
-}
diff --git a/src/boot/worker.ts b/src/boot/worker.ts
deleted file mode 100644
index 362fa3f26b..0000000000
--- a/src/boot/worker.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as cluster from 'cluster';
-import { initDb } from '../db/postgre';
-
-/**
- * Init worker process
- */
-export async function workerMain() {
-	await initDb();
-
-	// start server
-	await require('../server').default();
-
-	// start job queue
-	require('../queue').default();
-
-	if (cluster.isWorker) {
-		// Send a 'ready' message to parent process
-		process.send!('ready');
-	}
-}
diff --git a/src/client/.eslintrc b/src/client/.eslintrc
deleted file mode 100644
index fffa28d9e4..0000000000
--- a/src/client/.eslintrc
+++ /dev/null
@@ -1,32 +0,0 @@
-{
-	"env": {
-		"node": false,
-	},
-	"extends": [
-		"eslint:recommended",
-		"plugin:vue/recommended"
-	],
-	"rules": {
-		"vue/require-v-for-key": 0,
-		"vue/max-attributes-per-line": 0,
-		"vue/html-indent": 0,
-		"vue/html-self-closing": 0,
-		"vue/no-unused-vars": 0,
-		"vue/attributes-order": 0,
-		"vue/require-prop-types": 0,
-		"vue/require-default-prop": 0,
-		"vue/html-closing-bracket-spacing": 0,
-		"vue/singleline-html-element-content-newline": 0,
-		"vue/no-v-html": 0
-	},
-	"globals": {
-		"_DEV_": false,
-		"_LANGS_": false,
-		"_VERSION_": false,
-		"_ENV_": false,
-		"_PERF_PREFIX_": false,
-		"_DATA_TRANSFER_DRIVE_FILE_": false,
-		"_DATA_TRANSFER_DRIVE_FOLDER_": false,
-		"_DATA_TRANSFER_DECK_COLUMN_": false
-	}
-}
diff --git a/src/client/@types/global.d.ts b/src/client/@types/global.d.ts
deleted file mode 100644
index 84dde63b22..0000000000
--- a/src/client/@types/global.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-declare const _LANGS_: string[][];
-declare const _VERSION_: string;
-declare const _ENV_: string;
-declare const _DEV_: boolean;
-declare const _PERF_PREFIX_: string;
-declare const _DATA_TRANSFER_DRIVE_FILE_: string;
-declare const _DATA_TRANSFER_DRIVE_FOLDER_: string;
-declare const _DATA_TRANSFER_DECK_COLUMN_: string;
diff --git a/src/client/@types/vue.d.ts b/src/client/@types/vue.d.ts
deleted file mode 100644
index 8cb6130629..0000000000
--- a/src/client/@types/vue.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-declare module '*.vue' {
-	import type { DefineComponent } from 'vue';
-	const component: DefineComponent<{}, {}, any>;
-	export default component;
-}
diff --git a/src/client/account.ts b/src/client/account.ts
deleted file mode 100644
index a2165ebed1..0000000000
--- a/src/client/account.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-import { del, get, set } from '@client/scripts/idb-proxy';
-import { reactive } from 'vue';
-import { apiUrl } from '@client/config';
-import { waiting, api, popup, popupMenu, success } from '@client/os';
-import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
-import { showSuspendedDialog } from './scripts/show-suspended-dialog';
-import { i18n } from './i18n';
-
-// TODO: 他のタブと永続化されたstateを同期
-
-type Account = {
-	id: string;
-	token: string;
-	isModerator: boolean;
-	isAdmin: boolean;
-	isDeleted: boolean;
-};
-
-const data = localStorage.getItem('account');
-
-// TODO: 外部からはreadonlyに
-export const $i = data ? reactive(JSON.parse(data) as Account) : null;
-
-export async function signout() {
-	waiting();
-	localStorage.removeItem('account');
-
-	//#region Remove account
-	const accounts = await getAccounts();
-	accounts.splice(accounts.findIndex(x => x.id === $i.id), 1);
-
-	if (accounts.length > 0) await set('accounts', accounts);
-	else await del('accounts');
-	//#endregion
-
-	//#region Remove service worker registration
-	try {
-		if (navigator.serviceWorker.controller) {
-			const registration = await navigator.serviceWorker.ready;
-			const push = await registration.pushManager.getSubscription();
-			if (push) {
-				await fetch(`${apiUrl}/sw/unregister`, {
-					method: 'POST',
-					body: JSON.stringify({
-						i: $i.token,
-						endpoint: push.endpoint,
-					}),
-				});
-			}
-		}
-
-		if (accounts.length === 0) {
-			await navigator.serviceWorker.getRegistrations()
-				.then(registrations => {
-					return Promise.all(registrations.map(registration => registration.unregister()));
-				});
-		}
-	} catch (e) {}
-	//#endregion
-
-	document.cookie = `igi=; path=/`;
-
-	if (accounts.length > 0) login(accounts[0].token);
-	else unisonReload('/');
-}
-
-export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
-	return (await get('accounts')) || [];
-}
-
-export async function addAccount(id: Account['id'], token: Account['token']) {
-	const accounts = await getAccounts();
-	if (!accounts.some(x => x.id === id)) {
-		await set('accounts', accounts.concat([{ id, token }]));
-	}
-}
-
-function fetchAccount(token): Promise<Account> {
-	return new Promise((done, fail) => {
-		// Fetch user
-		fetch(`${apiUrl}/i`, {
-			method: 'POST',
-			body: JSON.stringify({
-				i: token
-			})
-		})
-		.then(res => res.json())
-		.then(res => {
-			if (res.error) {
-				if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
-					showSuspendedDialog().then(() => {
-						signout();
-					});
-				} else {
-					signout();
-				}
-			} else {
-				res.token = token;
-				done(res);
-			}
-		})
-		.catch(fail);
-	});
-}
-
-export function updateAccount(data) {
-	for (const [key, value] of Object.entries(data)) {
-		$i[key] = value;
-	}
-	localStorage.setItem('account', JSON.stringify($i));
-}
-
-export function refreshAccount() {
-	return fetchAccount($i.token).then(updateAccount);
-}
-
-export async function login(token: Account['token'], redirect?: string) {
-	waiting();
-	if (_DEV_) console.log('logging as token ', token);
-	const me = await fetchAccount(token);
-	localStorage.setItem('account', JSON.stringify(me));
-	await addAccount(me.id, token);
-
-	if (redirect) {
-		// 他のタブは再読み込みするだけ
-		reloadChannel.postMessage(null);
-		// このページはredirectで指定された先に移動
-		location.href = redirect;
-		return;
-	}
-
-	unisonReload();
-}
-
-export async function openAccountMenu(ev: MouseEvent) {
-	function showSigninDialog() {
-		popup(import('@client/components/signin-dialog.vue'), {}, {
-			done: res => {
-				addAccount(res.id, res.i);
-				success();
-			},
-		}, 'closed');
-	}
-
-	function createAccount() {
-		popup(import('@client/components/signup-dialog.vue'), {}, {
-			done: res => {
-				addAccount(res.id, res.i);
-				switchAccountWithToken(res.i);
-			},
-		}, 'closed');
-	}
-
-	async function switchAccount(account: any) {
-		const storedAccounts = await getAccounts();
-		const token = storedAccounts.find(x => x.id === account.id).token;
-		switchAccountWithToken(token);
-	}
-
-	function switchAccountWithToken(token: string) {
-		login(token);
-	}
-
-	const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
-	const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
-
-	const accountItemPromises = storedAccounts.map(a => new Promise(res => {
-		accountsPromise.then(accounts => {
-			const account = accounts.find(x => x.id === a.id);
-			if (account == null) return res(null);
-			res({
-				type: 'user',
-				user: account,
-				action: () => { switchAccount(account); }
-			});
-		});
-	}));
-
-	popupMenu([...[{
-		type: 'link',
-		text: i18n.locale.profile,
-		to: `/@${ $i.username }`,
-		avatar: $i,
-	}, null, ...accountItemPromises, {
-		icon: 'fas fa-plus',
-		text: i18n.locale.addAccount,
-		action: () => {
-			popupMenu([{
-				text: i18n.locale.existingAccount,
-				action: () => { showSigninDialog(); },
-			}, {
-				text: i18n.locale.createAccount,
-				action: () => { createAccount(); },
-			}], ev.currentTarget || ev.target);
-		},
-	}, {
-		type: 'link',
-		icon: 'fas fa-users',
-		text: i18n.locale.manageAccounts,
-		to: `/settings/accounts`,
-	}]], ev.currentTarget || ev.target, {
-		align: 'left'
-	});
-}
-
-// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
-declare module '@vue/runtime-core' {
-	interface ComponentCustomProperties {
-		$i: typeof $i;
-	}
-}
diff --git a/src/client/components/abuse-report-window.vue b/src/client/components/abuse-report-window.vue
deleted file mode 100644
index 21a19385ae..0000000000
--- a/src/client/components/abuse-report-window.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<template>
-<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')">
-	<template #header>
-		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
-		<I18n :src="$ts.reportAbuseOf" tag="span">
-			<template #name>
-				<b><MkAcct :user="user"/></b>
-			</template>
-		</I18n>
-	</template>
-	<div class="dpvffvvy _monolithic_">
-		<div class="_section">
-			<MkTextarea v-model="comment">
-				<template #label>{{ $ts.details }}</template>
-				<template #caption>{{ $ts.fillAbuseReportDescription }}</template>
-			</MkTextarea>
-		</div>
-		<div class="_section">
-			<MkButton @click="send" primary full :disabled="comment.length === 0">{{ $ts.send }}</MkButton>
-		</div>
-	</div>
-</XWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import XWindow from '@client/components/ui/window.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XWindow,
-		MkTextarea,
-		MkButton,
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true,
-		},
-		initialComment: {
-			type: String,
-			required: false,
-		},
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			comment: this.initialComment || '',
-		};
-	},
-
-	methods: {
-		send() {
-			os.apiWithDialog('users/report-abuse', {
-				userId: this.user.id,
-				comment: this.comment,
-			}, undefined, res => {
-				os.dialog({
-					type: 'success',
-					text: this.$ts.abuseReported
-				});
-				this.$refs.window.close();
-			});
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.dpvffvvy {
-	--root-margin: 16px;
-}
-</style>
diff --git a/src/client/components/analog-clock.vue b/src/client/components/analog-clock.vue
deleted file mode 100644
index bc572e5fff..0000000000
--- a/src/client/components/analog-clock.vue
+++ /dev/null
@@ -1,150 +0,0 @@
-<template>
-<svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none">
-	<circle v-for="(angle, i) in graduations"
-					:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
-					:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
-					:r="i % 5 == 0 ? 0.125 : 0.05"
-					:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"
-					:key="i"
-	/>
-
-	<line
-		:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))"
-		:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))"
-		:x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
-		:y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
-		:stroke="sHandColor"
-		:stroke-width="thickness / 2"
-		stroke-linecap="round"
-	/>
-
-	<line
-		:x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))"
-		:y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))"
-		:x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
-		:y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
-		:stroke="mHandColor"
-		:stroke-width="thickness"
-		stroke-linecap="round"
-	/>
-
-	<line
-		:x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))"
-		:y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))"
-		:x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
-		:y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
-		:stroke="hHandColor"
-		:stroke-width="thickness"
-		stroke-linecap="round"
-	/>
-</svg>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as tinycolor from 'tinycolor2';
-
-export default defineComponent({
-	props: {
-		thickness: {
-			type: Number,
-			default: 0.1
-		}
-	},
-
-	data() {
-		return {
-			now: new Date(),
-			enabled: true,
-
-			graduationsPadding: 0.5,
-			handsPadding: 1,
-			handsTailLength: 0.7,
-			hHandLengthRatio: 0.75,
-			mHandLengthRatio: 1,
-			sHandLengthRatio: 1,
-
-			computedStyle: getComputedStyle(document.documentElement)
-		};
-	},
-
-	computed: {
-		dark(): boolean {
-			return tinycolor(this.computedStyle.getPropertyValue('--bg')).isDark();
-		},
-
-		majorGraduationColor(): string {
-			return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
-		},
-		minorGraduationColor(): string {
-			return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
-		},
-
-		sHandColor(): string {
-			return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
-		},
-		mHandColor(): string {
-			return tinycolor(this.computedStyle.getPropertyValue('--fg')).toHexString();
-		},
-		hHandColor(): string {
-			return tinycolor(this.computedStyle.getPropertyValue('--accent')).toHexString();
-		},
-
-		s(): number {
-			return this.now.getSeconds();
-		},
-		m(): number {
-			return this.now.getMinutes();
-		},
-		h(): number {
-			return this.now.getHours();
-		},
-
-		hAngle(): number {
-			return Math.PI * (this.h % 12 + (this.m + this.s / 60) / 60) / 6;
-		},
-		mAngle(): number {
-			return Math.PI * (this.m + this.s / 60) / 30;
-		},
-		sAngle(): number {
-			return Math.PI * this.s / 30;
-		},
-
-		graduations(): any {
-			const angles = [];
-			for (let i = 0; i < 60; i++) {
-				const angle = Math.PI * i / 30;
-				angles.push(angle);
-			}
-
-			return angles;
-		}
-	},
-
-	mounted() {
-		const update = () => {
-			if (this.enabled) {
-				this.tick();
-				setTimeout(update, 1000);
-			}
-		};
-		update();
-	},
-
-	beforeUnmount() {
-		this.enabled = false;
-	},
-
-	methods: {
-		tick() {
-			this.now = new Date();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mbcofsoe {
-	display: block;
-}
-</style>
diff --git a/src/client/components/autocomplete.vue b/src/client/components/autocomplete.vue
deleted file mode 100644
index e621b26229..0000000000
--- a/src/client/components/autocomplete.vue
+++ /dev/null
@@ -1,502 +0,0 @@
-<template>
-<div class="swhvrteh _popup _shadow" @contextmenu.prevent="() => {}">
-	<ol class="users" ref="suggests" v-if="type === 'user'">
-		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user">
-			<img class="avatar" :src="user.avatarUrl"/>
-			<span class="name">
-				<MkUserName :user="user" :key="user.id"/>
-			</span>
-			<span class="username">@{{ acct(user) }}</span>
-		</li>
-		<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li>
-	</ol>
-	<ol class="hashtags" ref="suggests" v-else-if="hashtags.length > 0">
-		<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
-			<span class="name">{{ hashtag }}</span>
-		</li>
-	</ol>
-	<ol class="emojis" ref="suggests" v-else-if="emojis.length > 0">
-		<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
-			<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
-			<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
-			<span class="emoji" v-else>{{ emoji.emoji }}</span>
-			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
-			<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
-		</li>
-	</ol>
-	<ol class="mfmTags" ref="suggests" v-else-if="mfmTags.length > 0">
-		<li v-for="tag in mfmTags" @click="complete(type, tag)" @keydown="onKeydown" tabindex="-1">
-			<span class="tag">{{ tag }}</span>
-		</li>
-	</ol>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { emojilist } from '@/misc/emojilist';
-import contains from '@client/scripts/contains';
-import { twemojiSvgBase } from '@/misc/twemoji-base';
-import { getStaticImageUrl } from '@client/scripts/get-static-image-url';
-import { acct } from '@client/filters/user';
-import * as os from '@client/os';
-import { instance } from '@client/instance';
-
-type EmojiDef = {
-	emoji: string;
-	name: string;
-	aliasOf?: string;
-	url?: string;
-	isCustomEmoji?: boolean;
-};
-
-const lib = emojilist.filter(x => x.category !== 'flags');
-
-const char2file = (char: string) => {
-	let codes = Array.from(char).map(x => x.codePointAt(0).toString(16));
-	if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
-	codes = codes.filter(x => x && x.length);
-	return codes.join('-');
-};
-
-const emjdb: EmojiDef[] = lib.map(x => ({
-	emoji: x.char,
-	name: x.name,
-	aliasOf: null,
-	url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
-}));
-
-for (const x of lib) {
-	if (x.keywords) {
-		for (const k of x.keywords) {
-			emjdb.push({
-				emoji: x.char,
-				name: k,
-				aliasOf: x.name,
-				url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
-			});
-		}
-	}
-}
-
-emjdb.sort((a, b) => a.name.length - b.name.length);
-
-//#region Construct Emoji DB
-const customEmojis = instance.emojis;
-const emojiDefinitions: EmojiDef[] = [];
-
-for (const x of customEmojis) {
-	emojiDefinitions.push({
-		name: x.name,
-		emoji: `:${x.name}:`,
-		url: x.url,
-		isCustomEmoji: true
-	});
-
-	if (x.aliases) {
-		for (const alias of x.aliases) {
-			emojiDefinitions.push({
-				name: alias,
-				aliasOf: x.name,
-				emoji: `:${x.name}:`,
-				url: x.url,
-				isCustomEmoji: true
-			});
-		}
-	}
-}
-
-emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
-
-const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
-//#endregion
-
-const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle'];
-
-export default defineComponent({
-	props: {
-		type: {
-			type: String,
-			required: true,
-		},
-
-		q: {
-			type: String,
-			required: false,
-		},
-
-		textarea: {
-			type: HTMLTextAreaElement,
-			required: true,
-		},
-
-		close: {
-			type: Function,
-			required: true,
-		},
-
-		x: {
-			type: Number,
-			required: true,
-		},
-
-		y: {
-			type: Number,
-			required: true,
-		},
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			getStaticImageUrl,
-			fetching: true,
-			users: [],
-			hashtags: [],
-			emojis: [],
-			items: [],
-			mfmTags: [],
-			select: -1,
-		}
-	},
-
-	updated() {
-		this.setPosition();
-		this.items = (this.$refs.suggests as Element | undefined)?.children || [];
-	},
-
-	mounted() {
-		this.setPosition();
-
-		this.textarea.addEventListener('keydown', this.onKeydown);
-
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
-			el.addEventListener('mousedown', this.onMousedown);
-		}
-
-		this.$nextTick(() => {
-			this.exec();
-
-			this.$watch('q', () => {
-				this.$nextTick(() => {
-					this.exec();
-				});
-			});
-		});
-	},
-
-	beforeUnmount() {
-		this.textarea.removeEventListener('keydown', this.onKeydown);
-
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
-			el.removeEventListener('mousedown', this.onMousedown);
-		}
-	},
-
-	methods: {
-		complete(type, value) {
-			this.$emit('done', { type, value });
-			this.$emit('closed');
-
-			if (type === 'emoji') {
-				let recents = this.$store.state.recentlyUsedEmojis;
-				recents = recents.filter((e: any) => e !== value);
-				recents.unshift(value);
-				this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
-			}
-		},
-
-		setPosition() {
-			if (this.x + this.$el.offsetWidth > window.innerWidth) {
-				this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
-			} else {
-				this.$el.style.left = this.x + 'px';
-			}
-
-			if (this.y + this.$el.offsetHeight > window.innerHeight) {
-				this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px';
-				this.$el.style.marginTop = '0';
-			} else {
-				this.$el.style.top = this.y + 'px';
-				this.$el.style.marginTop = 'calc(1em + 8px)';
-			}
-		},
-
-		exec() {
-			this.select = -1;
-			if (this.$refs.suggests) {
-				for (const el of Array.from(this.items)) {
-					el.removeAttribute('data-selected');
-				}
-			}
-
-			if (this.type === 'user') {
-				if (this.q == null) {
-					this.users = [];
-					this.fetching = false;
-					return;
-				}
-
-				const cacheKey = `autocomplete:user:${this.q}`;
-				const cache = sessionStorage.getItem(cacheKey);
-				if (cache) {
-					const users = JSON.parse(cache);
-					this.users = users;
-					this.fetching = false;
-				} else {
-					os.api('users/search-by-username-and-host', {
-						username: this.q,
-						limit: 10,
-						detail: false
-					}).then(users => {
-						this.users = users;
-						this.fetching = false;
-
-						// キャッシュ
-						sessionStorage.setItem(cacheKey, JSON.stringify(users));
-					});
-				}
-			} else if (this.type === 'hashtag') {
-				if (this.q == null || this.q == '') {
-					this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
-					this.fetching = false;
-				} else {
-					const cacheKey = `autocomplete:hashtag:${this.q}`;
-					const cache = sessionStorage.getItem(cacheKey);
-					if (cache) {
-						const hashtags = JSON.parse(cache);
-						this.hashtags = hashtags;
-						this.fetching = false;
-					} else {
-						os.api('hashtags/search', {
-							query: this.q,
-							limit: 30
-						}).then(hashtags => {
-							this.hashtags = hashtags;
-							this.fetching = false;
-
-							// キャッシュ
-							sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
-						});
-					}
-				}
-			} else if (this.type === 'emoji') {
-				if (this.q == null || this.q == '') {
-					// 最近使った絵文字をサジェスト
-					this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
-					return;
-				}
-
-				const matched = [];
-				const max = 30;
-
-				emojiDb.some(x => {
-					if (x.name.startsWith(this.q) && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-					return matched.length == max;
-				});
-				if (matched.length < max) {
-					emojiDb.some(x => {
-						if (x.name.startsWith(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-						return matched.length == max;
-					});
-				}
-				if (matched.length < max) {
-					emojiDb.some(x => {
-						if (x.name.includes(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
-						return matched.length == max;
-					});
-				}
-
-				this.emojis = matched;
-			} else if (this.type === 'mfmTag') {
-				if (this.q == null || this.q == '') {
-					this.mfmTags = MFM_TAGS;
-					return;
-				}
-
-				this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
-			}
-		},
-
-		onMousedown(e) {
-			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
-		},
-
-		onKeydown(e) {
-			const cancel = () => {
-				e.preventDefault();
-				e.stopPropagation();
-			};
-
-			switch (e.which) {
-				case 10: // [ENTER]
-				case 13: // [ENTER]
-					if (this.select !== -1) {
-						cancel();
-						(this.items[this.select] as any).click();
-					} else {
-						this.close();
-					}
-					break;
-
-				case 27: // [ESC]
-					cancel();
-					this.close();
-					break;
-
-				case 38: // [↑]
-					if (this.select !== -1) {
-						cancel();
-						this.selectPrev();
-					} else {
-						this.close();
-					}
-					break;
-
-				case 9: // [TAB]
-				case 40: // [↓]
-					cancel();
-					this.selectNext();
-					break;
-
-				default:
-					e.stopPropagation();
-					this.textarea.focus();
-			}
-		},
-
-		selectNext() {
-			if (++this.select >= this.items.length) this.select = 0;
-			if (this.items.length === 0) this.select = -1;
-			this.applySelect();
-		},
-
-		selectPrev() {
-			if (--this.select < 0) this.select = this.items.length - 1;
-			this.applySelect();
-		},
-
-		applySelect() {
-			for (const el of Array.from(this.items)) {
-				el.removeAttribute('data-selected');
-			}
-
-			if (this.select !== -1) {
-				this.items[this.select].setAttribute('data-selected', 'true');
-				(this.items[this.select] as any).focus();
-			}
-		},
-
-		chooseUser() {
-			this.close();
-			os.selectUser().then(user => {
-				this.complete('user', user);
-				this.textarea.focus();
-			});
-		},
-
-		acct
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.swhvrteh {
-	position: fixed;
-	z-index: 65535;
-	max-width: 100%;
-	margin-top: calc(1em + 8px);
-	overflow: hidden;
-	transition: top 0.1s ease, left 0.1s ease;
-
-	> ol {
-		display: block;
-		margin: 0;
-		padding: 4px 0;
-		max-height: 190px;
-		max-width: 500px;
-		overflow: auto;
-		list-style: none;
-
-		> li {
-			display: flex;
-			align-items: center;
-			padding: 4px 12px;
-			white-space: nowrap;
-			overflow: hidden;
-			font-size: 0.9em;
-			cursor: default;
-
-			&, * {
-				user-select: none;
-			}
-
-			* {
-				overflow: hidden;
-				text-overflow: ellipsis;
-			}
-
-			&:hover {
-				background: var(--X3);
-			}
-
-			&[data-selected='true'] {
-				background: var(--accent);
-
-				&, * {
-					color: #fff !important;
-				}
-			}
-
-			&:active {
-				background: var(--accentDarken);
-
-				&, * {
-					color: #fff !important;
-				}
-			}
-		}
-	}
-
-	> .users > li {
-
-		.avatar {
-			min-width: 28px;
-			min-height: 28px;
-			max-width: 28px;
-			max-height: 28px;
-			margin: 0 8px 0 0;
-			border-radius: 100%;
-		}
-
-		.name {
-			margin: 0 8px 0 0;
-		}
-	}
-
-	> .emojis > li {
-
-		.emoji {
-			display: inline-block;
-			margin: 0 4px 0 0;
-			width: 24px;
-
-			> img {
-				width: 24px;
-				vertical-align: bottom;
-			}
-		}
-
-		.alias {
-			margin: 0 0 0 8px;
-		}
-	}
-
-	> .mfmTags > li {
-
-		.name {
-		}
-	}
-}
-</style>
diff --git a/src/client/components/avatars.vue b/src/client/components/avatars.vue
deleted file mode 100644
index da862967dd..0000000000
--- a/src/client/components/avatars.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
-<div>
-	<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
-		<MkAvatar :user="user" style="width:32px;height:32px;" :show-indicator="true"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		userIds: {
-			required: true
-		},
-	},
-	data() {
-		return {
-			us: []
-		};
-	},
-	async created() {
-		this.us = await os.api('users/show', {
-			userIds: this.userIds
-		});
-	}
-});
-</script>
diff --git a/src/client/components/captcha.vue b/src/client/components/captcha.vue
deleted file mode 100644
index baa922506e..0000000000
--- a/src/client/components/captcha.vue
+++ /dev/null
@@ -1,123 +0,0 @@
-<template>
-<div>
-	<span v-if="!available">{{ $ts.waiting }}<MkEllipsis/></span>
-	<div ref="captcha"></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-
-type Captcha = {
-	render(container: string | Node, options: {
-		readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
-	}): string;
-	remove(id: string): void;
-	execute(id: string): void;
-	reset(id: string): void;
-	getResponse(id: string): string;
-};
-
-type CaptchaProvider = 'hcaptcha' | 'recaptcha';
-
-type CaptchaContainer = {
-	readonly [_ in CaptchaProvider]?: Captcha;
-};
-
-declare global {
-	interface Window extends CaptchaContainer {
-	}
-}
-
-export default defineComponent({
-	props: {
-		provider: {
-			type: String as PropType<CaptchaProvider>,
-			required: true,
-		},
-		sitekey: {
-			type: String,
-			required: true,
-		},
-		modelValue: {
-			type: String,
-		},
-	},
-
-	data() {
-		return {
-			available: false,
-		};
-	},
-
-	computed: {
-		variable(): string {
-			switch (this.provider) {
-				case 'hcaptcha': return 'hcaptcha';
-				case 'recaptcha': return 'grecaptcha';
-			}
-		},
-		loaded(): boolean {
-			return !!window[this.variable];
-		},
-		src(): string {
-			const endpoint = ({
-				hcaptcha: 'https://hcaptcha.com/1',
-				recaptcha: 'https://www.recaptcha.net/recaptcha',
-			} as Record<CaptchaProvider, string>)[this.provider];
-
-			return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
-		},
-		captcha(): Captcha {
-			return window[this.variable] || {} as unknown as Captcha;
-		},
-	},
-
-	created() {
-		if (this.loaded) {
-			this.available = true;
-		} else {
-			(document.getElementById(this.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
-				async: true,
-				id: this.provider,
-				src: this.src,
-			})))
-				.addEventListener('load', () => this.available = true);
-		}
-	},
-
-	mounted() {
-		if (this.available) {
-			this.requestRender();
-		} else {
-			this.$watch('available', this.requestRender);
-		}
-	},
-
-	beforeUnmount() {
-		this.reset();
-	},
-
-	methods: {
-		reset() {
-			if (this.captcha?.reset) this.captcha.reset();
-		},
-		requestRender() {
-			if (this.captcha.render && this.$refs.captcha instanceof Element) {
-				this.captcha.render(this.$refs.captcha, {
-					sitekey: this.sitekey,
-					theme: this.$store.state.darkMode ? 'dark' : 'light',
-					callback: this.callback,
-					'expired-callback': this.callback,
-					'error-callback': this.callback,
-				});
-			} else {
-				setTimeout(this.requestRender.bind(this), 1);
-			}
-		},
-		callback(response?: string) {
-			this.$emit('update:modelValue', typeof response == 'string' ? response : null);
-		},
-	},
-});
-</script>
diff --git a/src/client/components/channel-follow-button.vue b/src/client/components/channel-follow-button.vue
deleted file mode 100644
index bd8627f6e8..0000000000
--- a/src/client/components/channel-follow-button.vue
+++ /dev/null
@@ -1,140 +0,0 @@
-<template>
-<button class="hdcaacmi _button"
-	:class="{ wait, active: isFollowing, full }"
-	@click="onClick"
-	:disabled="wait"
->
-	<template v-if="!wait">
-		<template v-if="isFollowing">
-			<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
-		</template>
-		<template v-else>
-			<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
-		</template>
-	</template>
-	<template v-else>
-		<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
-	</template>
-</button>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		channel: {
-			type: Object,
-			required: true
-		},
-		full: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-
-	data() {
-		return {
-			isFollowing: this.channel.isFollowing,
-			wait: false,
-		};
-	},
-
-	methods: {
-		async onClick() {
-			this.wait = true;
-
-			try {
-				if (this.isFollowing) {
-					await os.api('channels/unfollow', {
-						channelId: this.channel.id
-					});
-					this.isFollowing = false;
-				} else {
-					await os.api('channels/follow', {
-						channelId: this.channel.id
-					});
-					this.isFollowing = true;
-				}
-			} catch (e) {
-				console.error(e);
-			} finally {
-				this.wait = false;
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hdcaacmi {
-	position: relative;
-	display: inline-block;
-	font-weight: bold;
-	color: var(--accent);
-	background: transparent;
-	border: solid 1px var(--accent);
-	padding: 0;
-	height: 31px;
-	font-size: 16px;
-	border-radius: 32px;
-	background: #fff;
-
-	&.full {
-		padding: 0 8px 0 12px;
-		font-size: 14px;
-	}
-
-	&:not(.full) {
-		width: 31px;
-	}
-
-	&:focus-visible {
-		&:after {
-			content: "";
-			pointer-events: none;
-			position: absolute;
-			top: -5px;
-			right: -5px;
-			bottom: -5px;
-			left: -5px;
-			border: 2px solid var(--focus);
-			border-radius: 32px;
-		}
-	}
-
-	&:hover {
-		//background: mix($primary, #fff, 20);
-	}
-
-	&:active {
-		//background: mix($primary, #fff, 40);
-	}
-
-	&.active {
-		color: #fff;
-		background: var(--accent);
-
-		&:hover {
-			background: var(--accentLighten);
-			border-color: var(--accentLighten);
-		}
-
-		&:active {
-			background: var(--accentDarken);
-			border-color: var(--accentDarken);
-		}
-	}
-
-	&.wait {
-		cursor: wait !important;
-		opacity: 0.7;
-	}
-
-	> span {
-		margin-right: 6px;
-	}
-}
-</style>
diff --git a/src/client/components/channel-preview.vue b/src/client/components/channel-preview.vue
deleted file mode 100644
index eb00052a78..0000000000
--- a/src/client/components/channel-preview.vue
+++ /dev/null
@@ -1,165 +0,0 @@
-<template>
-<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
-	<div class="banner" :style="bannerStyle">
-		<div class="fade"></div>
-		<div class="name"><i class="fas fa-satellite-dish"></i> {{ channel.name }}</div>
-		<div class="status">
-			<div>
-				<i class="fas fa-users fa-fw"></i>
-				<I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;">
-					<template #n>
-						<b>{{ channel.usersCount }}</b>
-					</template>
-				</I18n>
-			</div>
-			<div>
-				<i class="fas fa-pencil-alt fa-fw"></i>
-				<I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;">
-					<template #n>
-						<b>{{ channel.notesCount }}</b>
-					</template>
-				</I18n>
-			</div>
-		</div>
-	</div>
-	<article v-if="channel.description">
-		<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
-	</article>
-	<footer>
-		<span v-if="channel.lastNotedAt">
-			{{ $ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
-		</span>
-	</footer>
-</MkA>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		channel: {
-			type: Object,
-			required: true
-		},
-	},
-
-	computed: {
-		bannerStyle() {
-			if (this.channel.bannerUrl) {
-				return { backgroundImage: `url(${this.channel.bannerUrl})` };
-			} else {
-				return { backgroundColor: '#4c5e6d' };
-			}
-		}
-	},
-
-	data() {
-		return {
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.eftoefju {
-	display: block;
-	overflow: hidden;
-	width: 100%;
-
-	&:hover {
-		text-decoration: none;
-	}
-
-	> .banner {
-		position: relative;
-		width: 100%;
-		height: 200px;
-		background-position: center;
-		background-size: cover;
-
-		> .fade {
-			position: absolute;
-			bottom: 0;
-			left: 0;
-			width: 100%;
-			height: 64px;
-			background: linear-gradient(0deg, var(--panel), var(--X15));
-		}
-
-		> .name {
-			position: absolute;
-			top: 16px;
-			left: 16px;
-			padding: 12px 16px;
-			background: rgba(0, 0, 0, 0.7);
-			color: #fff;
-			font-size: 1.2em;
-		}
-
-		> .status {
-			position: absolute;
-			z-index: 1;
-			bottom: 16px;
-			right: 16px;
-			padding: 8px 12px;
-			font-size: 80%;
-			background: rgba(0, 0, 0, 0.7);
-			border-radius: 6px;
-			color: #fff;
-		}
-	}
-
-	> article {
-		padding: 16px;
-
-		> p {
-			margin: 0;
-			font-size: 1em;
-		}
-	}
-
-	> footer {
-		padding: 12px 16px;
-		border-top: solid 0.5px var(--divider);
-
-		> span {
-			opacity: 0.7;
-			font-size: 0.9em;
-		}
-	}
-
-	@media (max-width: 550px) {
-		font-size: 0.9em;
-
-		> .banner {
-			height: 80px;
-
-			> .status {
-				display: none;
-			}
-		}
-
-		> article {
-			padding: 12px;
-		}
-
-		> footer {
-			display: none;
-		}
-	}
-
-	@media (max-width: 500px) {
-		font-size: 0.8em;
-
-		> .banner {
-			height: 70px;
-		}
-
-		> article {
-			padding: 8px;
-		}
-	}
-}
-
-</style>
diff --git a/src/client/components/chart.vue b/src/client/components/chart.vue
deleted file mode 100644
index ae9a5e79b1..0000000000
--- a/src/client/components/chart.vue
+++ /dev/null
@@ -1,691 +0,0 @@
-<template>
-<div class="cbbedffa">
-	<canvas ref="chartEl"></canvas>
-	<div v-if="fetching" class="fetching">
-		<MkLoading/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
-import {
-	Chart,
-	ArcElement,
-	LineElement,
-	BarElement,
-	PointElement,
-	BarController,
-	LineController,
-	CategoryScale,
-	LinearScale,
-	TimeScale,
-	Legend,
-	Title,
-	Tooltip,
-	SubTitle,
-	Filler,
-} from 'chart.js';
-import 'chartjs-adapter-date-fns';
-import { enUS } from 'date-fns/locale';
-import zoomPlugin from 'chartjs-plugin-zoom';
-import * as os from '@client/os';
-import { defaultStore } from '@client/store';
-
-Chart.register(
-	ArcElement,
-	LineElement,
-	BarElement,
-	PointElement,
-	BarController,
-	LineController,
-	CategoryScale,
-	LinearScale,
-	TimeScale,
-	Legend,
-	Title,
-	Tooltip,
-	SubTitle,
-	Filler,
-	zoomPlugin,
-);
-
-const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
-const negate = arr => arr.map(x => -x);
-const alpha = (hex, a) => {
-	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
-	return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
-
-const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
-const getColor = (i) => {
-	return colors[i % colors.length];
-};
-
-export default defineComponent({
-	props: {
-		src: {
-			type: String,
-			required: true,
-		},
-		args: {
-			type: Object,
-			required: false,
-		},
-		limit: {
-			type: Number,
-			required: false,
-			default: 90
-		},
-		span: {
-			type: String as PropType<'hour' | 'day'>,
-			required: true,
-		},
-		detailed: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		stacked: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		aspectRatio: {
-			type: Number,
-			required: false,
-			default: null
-		},
-	},
-
-	setup(props) {
-		const now = new Date();
-		let chartInstance: Chart = null;
-		let data: {
-			series: {
-				name: string;
-				type: 'line' | 'area';
-				color?: string;
-				borderDash?: number[];
-				hidden?: boolean;
-				data: {
-					x: number;
-					y: number;
-				}[];
-			}[];
-		} = null;
-
-		const chartEl = ref<HTMLCanvasElement>(null);
-		const fetching = ref(true);
-
-		const getDate = (ago: number) => {
-			const y = now.getFullYear();
-			const m = now.getMonth();
-			const d = now.getDate();
-			const h = now.getHours();
-
-			return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
-		};
-
-		const format = (arr) => {
-			return arr.map((v, i) => ({
-				x: getDate(i).getTime(),
-				y: v
-			}));
-		};
-
-		const render = () => {
-			if (chartInstance) {
-				chartInstance.destroy();
-			}
-
-			const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
-			// フォントカラー
-			Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
-			chartInstance = new Chart(chartEl.value, {
-				type: 'line',
-				data: {
-					labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
-					datasets: data.series.map((x, i) => ({
-						parsing: false,
-						label: x.name,
-						data: x.data.slice().reverse(),
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: x.color ? x.color : getColor(i),
-						borderDash: x.borderDash || [],
-						borderJoinStyle: 'round',
-						backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
-						fill: x.type === 'area',
-						hidden: !!x.hidden,
-					})),
-				},
-				options: {
-					aspectRatio: props.aspectRatio || 2.5,
-					layout: {
-						padding: {
-							left: 16,
-							right: 16,
-							top: 16,
-							bottom: 8,
-						},
-					},
-					scales: {
-						x: {
-							type: 'time',
-							time: {
-								stepSize: 1,
-								unit: props.span === 'day' ? 'month' : 'day',
-							},
-							grid: {
-								color: gridColor,
-								borderColor: 'rgb(0, 0, 0, 0)',
-							},
-							ticks: {
-								display: props.detailed,
-							},
-							adapters: {
-								date: {
-									locale: enUS,
-								},
-							},
-							min: getDate(props.limit).getTime(),
-						},
-						y: {
-							position: 'left',
-							stacked: props.stacked,
-							grid: {
-								color: gridColor,
-								borderColor: 'rgb(0, 0, 0, 0)',
-							},
-							ticks: {
-								display: props.detailed,
-							},
-						},
-					},
-					interaction: {
-						intersect: false,
-					},
-					plugins: {
-						legend: {
-							display: props.detailed,
-							position: 'bottom',
-							labels: {
-								boxWidth: 16,
-							},
-						},
-						tooltip: {
-							mode: 'index',
-							animation: {
-								duration: 0,
-							},
-						},
-						zoom: {
-							pan: {
-								enabled: true,
-							},
-							zoom: {
-								wheel: {
-									enabled: true,
-								},
-								pinch: {
-									enabled: true,
-								},
-								drag: {
-									enabled: false,
-								},
-								mode: 'x',
-							},
-							limits: {
-								x: {
-									min: 'original',
-									max: 'original',
-								},
-								y: {
-									min: 'original',
-									max: 'original',
-								},
-							}
-						},
-					},
-				},
-			});
-		};
-
-		const exportData = () => {
-			// TODO
-		};
-
-		const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
-			const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Instances',
-					type: 'area',
-					data: format(total
-						? raw.instance.total
-						: sum(raw.instance.inc, negate(raw.instance.dec))
-					),
-				}],
-			};
-		};
-
-		const fetchNotesChart = async (type: string): Promise<typeof data> => {
-			const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'All',
-					type: 'line',
-					borderDash: [5, 5],
-					data: format(type == 'combined'
-						? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
-						: sum(raw[type].inc, negate(raw[type].dec))
-					),
-				}, {
-					name: 'Renotes',
-					type: 'area',
-					data: format(type == 'combined'
-						? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
-						: raw[type].diffs.renote
-					),
-				}, {
-					name: 'Replies',
-					type: 'area',
-					data: format(type == 'combined'
-						? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
-						: raw[type].diffs.reply
-					),
-				}, {
-					name: 'Normal',
-					type: 'area',
-					data: format(type == 'combined'
-						? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
-						: raw[type].diffs.normal
-					),
-				}],
-			};
-		};
-
-		const fetchNotesTotalChart = async (): Promise<typeof data> => {
-			const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Combined',
-					type: 'line',
-					data: format(sum(raw.local.total, raw.remote.total)),
-				}, {
-					name: 'Local',
-					type: 'area',
-					data: format(raw.local.total),
-				}, {
-					name: 'Remote',
-					type: 'area',
-					data: format(raw.remote.total),
-				}],
-			};
-		};
-
-		const fetchUsersChart = async (total: boolean): Promise<typeof data> => {
-			const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Combined',
-					type: 'line',
-					data: format(total
-						? sum(raw.local.total, raw.remote.total)
-						: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
-					),
-				}, {
-					name: 'Local',
-					type: 'area',
-					data: format(total
-						? raw.local.total
-						: sum(raw.local.inc, negate(raw.local.dec))
-					),
-				}, {
-					name: 'Remote',
-					type: 'area',
-					data: format(total
-						? raw.remote.total
-						: sum(raw.remote.inc, negate(raw.remote.dec))
-					),
-				}],
-			};
-		};
-
-		const fetchActiveUsersChart = async (): Promise<typeof data> => {
-			const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Combined',
-					type: 'line',
-					data: format(sum(raw.local.users, raw.remote.users)),
-				}, {
-					name: 'Local',
-					type: 'area',
-					data: format(raw.local.users),
-				}, {
-					name: 'Remote',
-					type: 'area',
-					data: format(raw.remote.users),
-				}],
-			};
-		};
-
-		const fetchDriveChart = async (): Promise<typeof data> => {
-			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
-			return {
-				bytes: true,
-				series: [{
-					name: 'All',
-					type: 'line',
-					borderDash: [5, 5],
-					data: format(
-						sum(
-							raw.local.incSize,
-							negate(raw.local.decSize),
-							raw.remote.incSize,
-							negate(raw.remote.decSize)
-						)
-					),
-				}, {
-					name: 'Local +',
-					type: 'area',
-					data: format(raw.local.incSize),
-				}, {
-					name: 'Local -',
-					type: 'area',
-					data: format(negate(raw.local.decSize)),
-				}, {
-					name: 'Remote +',
-					type: 'area',
-					data: format(raw.remote.incSize),
-				}, {
-					name: 'Remote -',
-					type: 'area',
-					data: format(negate(raw.remote.decSize)),
-				}],
-			};
-		};
-
-		const fetchDriveTotalChart = async (): Promise<typeof data> => {
-			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
-			return {
-				bytes: true,
-				series: [{
-					name: 'Combined',
-					type: 'line',
-					data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
-				}, {
-					name: 'Local',
-					type: 'area',
-					data: format(raw.local.totalSize),
-				}, {
-					name: 'Remote',
-					type: 'area',
-					data: format(raw.remote.totalSize),
-				}],
-			};
-		};
-
-		const fetchDriveFilesChart = async (): Promise<typeof data> => {
-			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'All',
-					type: 'line',
-					borderDash: [5, 5],
-					data: format(
-						sum(
-							raw.local.incCount,
-							negate(raw.local.decCount),
-							raw.remote.incCount,
-							negate(raw.remote.decCount)
-						)
-					),
-				}, {
-					name: 'Local +',
-					type: 'area',
-					data: format(raw.local.incCount),
-				}, {
-					name: 'Local -',
-					type: 'area',
-					data: format(negate(raw.local.decCount)),
-				}, {
-					name: 'Remote +',
-					type: 'area',
-					data: format(raw.remote.incCount),
-				}, {
-					name: 'Remote -',
-					type: 'area',
-					data: format(negate(raw.remote.decCount)),
-				}],
-			};
-		};
-
-		const fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
-			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Combined',
-					type: 'line',
-					data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
-				}, {
-					name: 'Local',
-					type: 'area',
-					data: format(raw.local.totalCount),
-				}, {
-					name: 'Remote',
-					type: 'area',
-					data: format(raw.remote.totalCount),
-				}],
-			};
-		};
-
-		const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
-			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'In',
-					type: 'area',
-					color: '#008FFB',
-					data: format(raw.requests.received)
-				}, {
-					name: 'Out (succ)',
-					type: 'area',
-					color: '#00E396',
-					data: format(raw.requests.succeeded)
-				}, {
-					name: 'Out (fail)',
-					type: 'area',
-					color: '#FEB019',
-					data: format(raw.requests.failed)
-				}]
-			};
-		};
-
-		const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => {
-			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Users',
-					type: 'area',
-					color: '#008FFB',
-					data: format(total
-						? raw.users.total
-						: sum(raw.users.inc, negate(raw.users.dec))
-					)
-				}]
-			};
-		};
-
-		const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => {
-			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Notes',
-					type: 'area',
-					color: '#008FFB',
-					data: format(total
-						? raw.notes.total
-						: sum(raw.notes.inc, negate(raw.notes.dec))
-					)
-				}]
-			};
-		};
-
-		const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => {
-			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Following',
-					type: 'area',
-					color: '#008FFB',
-					data: format(total
-						? raw.following.total
-						: sum(raw.following.inc, negate(raw.following.dec))
-					)
-				}, {
-					name: 'Followers',
-					type: 'area',
-					color: '#00E396',
-					data: format(total
-						? raw.followers.total
-						: sum(raw.followers.inc, negate(raw.followers.dec))
-					)
-				}]
-			};
-		};
-
-		const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => {
-			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
-			return {
-				bytes: true,
-				series: [{
-					name: 'Drive usage',
-					type: 'area',
-					color: '#008FFB',
-					data: format(total
-						? raw.drive.totalUsage
-						: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
-					)
-				}]
-			};
-		};
-
-		const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => {
-			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
-			return {
-				series: [{
-					name: 'Drive files',
-					type: 'area',
-					color: '#008FFB',
-					data: format(total
-						? raw.drive.totalFiles
-						: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
-					)
-				}]
-			};
-		};
-
-		const fetchPerUserNotesChart = async (): Promise<typeof data> => {
-			const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
-			return {
-				series: [...(props.args.withoutAll ? [] : [{
-					name: 'All',
-					type: 'line',
-					borderDash: [5, 5],
-					data: format(sum(raw.inc, negate(raw.dec))),
-				}]), {
-					name: 'Renotes',
-					type: 'area',
-					data: format(raw.diffs.renote),
-				}, {
-					name: 'Replies',
-					type: 'area',
-					data: format(raw.diffs.reply),
-				}, {
-					name: 'Normal',
-					type: 'area',
-					data: format(raw.diffs.normal),
-				}],
-			};
-		};
-
-		const fetchAndRender = async () => {
-			const fetchData = () => {
-				switch (props.src) {
-					case 'federation-instances': return fetchFederationInstancesChart(false);
-					case 'federation-instances-total': return fetchFederationInstancesChart(true);
-					case 'users': return fetchUsersChart(false);
-					case 'users-total': return fetchUsersChart(true);
-					case 'active-users': return fetchActiveUsersChart();
-					case 'notes': return fetchNotesChart('combined');
-					case 'local-notes': return fetchNotesChart('local');
-					case 'remote-notes': return fetchNotesChart('remote');
-					case 'notes-total': return fetchNotesTotalChart();
-					case 'drive': return fetchDriveChart();
-					case 'drive-total': return fetchDriveTotalChart();
-					case 'drive-files': return fetchDriveFilesChart();
-					case 'drive-files-total': return fetchDriveFilesTotalChart();
-					
-					case 'instance-requests': return fetchInstanceRequestsChart();
-					case 'instance-users': return fetchInstanceUsersChart(false);
-					case 'instance-users-total': return fetchInstanceUsersChart(true);
-					case 'instance-notes': return fetchInstanceNotesChart(false);
-					case 'instance-notes-total': return fetchInstanceNotesChart(true);
-					case 'instance-ff': return fetchInstanceFfChart(false);
-					case 'instance-ff-total': return fetchInstanceFfChart(true);
-					case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false);
-					case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true);
-					case 'instance-drive-files': return fetchInstanceDriveFilesChart(false);
-					case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
-
-					case 'per-user-notes': return fetchPerUserNotesChart();
-				}
-			};
-			fetching.value = true;
-			data = await fetchData();
-			fetching.value = false;
-			render();
-		};
-
-		watch(() => [props.src, props.span], fetchAndRender);
-
-		onMounted(() => {
-			fetchAndRender();
-		});
-
-		return {
-			chartEl,
-			fetching,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.cbbedffa {
-	position: relative;
-
-	> .fetching {
-		position: absolute;
-		top: 0;
-		left: 0;
-		width: 100%;
-		height: 100%;
-		-webkit-backdrop-filter: var(--blur, blur(12px));
-		backdrop-filter: var(--blur, blur(12px));
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		cursor: wait;
-	}
-}
-</style>
diff --git a/src/client/components/code-core.vue b/src/client/components/code-core.vue
deleted file mode 100644
index 9cff7b4448..0000000000
--- a/src/client/components/code-core.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<template>
-<code v-if="inline" v-html="html" :class="`language-${prismLang}`"></code>
-<pre v-else :class="`language-${prismLang}`"><code v-html="html" :class="`language-${prismLang}`"></code></pre>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import 'prismjs';
-import 'prismjs/themes/prism-okaidia.css';
-
-export default defineComponent({
-	props: {
-		code: {
-			type: String,
-			required: true
-		},
-		lang: {
-			type: String,
-			required: false
-		},
-		inline: {
-			type: Boolean,
-			required: false
-		}
-	},
-	computed: {
-		prismLang() {
-			return Prism.languages[this.lang] ? this.lang : 'js';
-		},
-		html() {
-			return Prism.highlight(this.code, Prism.languages[this.prismLang], this.prismLang);
-		}
-	}
-});
-</script>
diff --git a/src/client/components/code.vue b/src/client/components/code.vue
deleted file mode 100644
index f5d6c5673a..0000000000
--- a/src/client/components/code.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<template>
-<XCode :code="code" :lang="lang" :inline="inline"/>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-
-export default defineComponent({
-	components: {
-		XCode: defineAsyncComponent(() => import('./code-core.vue'))
-	},
-	props: {
-		code: {
-			type: String,
-			required: true
-		},
-		lang: {
-			type: String,
-			required: false
-		},
-		inline: {
-			type: Boolean,
-			required: false
-		}
-	}
-});
-</script>
diff --git a/src/client/components/cw-button.vue b/src/client/components/cw-button.vue
deleted file mode 100644
index 3a172f5d5e..0000000000
--- a/src/client/components/cw-button.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<button class="nrvgflfu _button" @click="toggle">
-	<b>{{ modelValue ? $ts._cw.hide : $ts._cw.show }}</b>
-	<span v-if="!modelValue">{{ label }}</span>
-</button>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { length } from 'stringz';
-import { concat } from '../../prelude/array';
-
-export default defineComponent({
-	props: {
-		modelValue: {
-			type: Boolean,
-			required: true
-		},
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	computed: {
-		label(): string {
-			return concat([
-				this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [],
-				this.note.files && this.note.files.length !== 0 ? [this.$t('_cw.files', { count: this.note.files.length }) ] : [],
-				this.note.poll != null ? [this.$ts.poll] : []
-			] as string[][]).join(' / ');
-		}
-	},
-
-	methods: {
-		length,
-
-		toggle() {
-			this.$emit('update:modelValue', !this.modelValue);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.nrvgflfu {
-	display: inline-block;
-	padding: 4px 8px;
-	font-size: 0.7em;
-	color: var(--cwFg);
-	background: var(--cwBg);
-	border-radius: 2px;
-
-	&:hover {
-		background: var(--cwHoverBg);
-	}
-
-	> span {
-		margin-left: 4px;
-
-		&:before {
-			content: '(';
-		}
-
-		&:after {
-			content: ')';
-		}
-	}
-}
-</style>
diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue
deleted file mode 100644
index fa0b6d669c..0000000000
--- a/src/client/components/date-separated-list.vue
+++ /dev/null
@@ -1,188 +0,0 @@
-<script lang="ts">
-import { defineComponent, h, PropType, TransitionGroup } from 'vue';
-import MkAd from '@client/components/global/ad.vue';
-
-export default defineComponent({
-	props: {
-		items: {
-			type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
-			required: true,
-		},
-		direction: {
-			type: String,
-			required: false,
-			default: 'down'
-		},
-		reversed: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		noGap: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		ad: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	methods: {
-		focus() {
-			this.$slots.default[0].elm.focus();
-		},
-
-		getDateText(time: string) {
-			const date = new Date(time).getDate();
-			const month = new Date(time).getMonth() + 1;
-			return this.$t('monthAndDay', {
-				month: month.toString(),
-				day: date.toString()
-			});
-		}
-	},
-
-	render() {
-		if (this.items.length === 0) return;
-
-		const renderChildren = () => this.items.map((item, i) => {
-			const el = this.$slots.default({
-				item: item
-			})[0];
-			if (el.key == null && item.id) el.key = item.id;
-
-			if (
-				i != this.items.length - 1 &&
-				new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
-			) {
-				const separator = h('div', {
-					class: 'separator',
-					key: item.id + ':separator',
-				}, h('p', {
-					class: 'date'
-				}, [
-					h('span', [
-						h('i', {
-							class: 'fas fa-angle-up icon',
-						}),
-						this.getDateText(item.createdAt)
-					]),
-					h('span', [
-						this.getDateText(this.items[i + 1].createdAt),
-						h('i', {
-							class: 'fas fa-angle-down icon',
-						})
-					])
-				]));
-
-				return [el, separator];
-			} else {
-				if (this.ad && item._shouldInsertAd_) {
-					return [h(MkAd, {
-						class: 'a', // advertiseの意(ブロッカー対策)
-						key: item.id + ':ad',
-						prefer: ['horizontal', 'horizontal-big'],
-					}), el];
-				} else {
-					return el;
-				}
-			}
-		});
-
-		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? {
-			class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''),
-			name: 'list',
-			tag: 'div',
-			'data-direction': this.direction,
-			'data-reversed': this.reversed ? 'true' : 'false',
-		} : {
-			class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''),
-		}, {
-			default: renderChildren
-		});
-	},
-});
-</script>
-
-<style lang="scss">
-.sqadhkmv {
-	> *:empty {
-		display: none;
-	}
-
-	> *:not(:last-child) {
-		margin-bottom: var(--margin);
-	}
-
-	> .list-move {
-		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
-	}
-
-	> .list-enter-active {
-		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
-	}
-
-	&[data-direction="up"] {
-		> .list-enter-from {
-			opacity: 0;
-			transform: translateY(64px);
-		}
-	}
-
-	&[data-direction="down"] {
-		> .list-enter-from {
-			opacity: 0;
-			transform: translateY(-64px);
-		}
-	}
-
-	> .separator {
-		text-align: center;
-
-		> .date {
-			display: inline-block;
-			position: relative;
-			margin: 0;
-			padding: 0 16px;
-			line-height: 32px;
-			text-align: center;
-			font-size: 12px;
-			color: var(--dateLabelFg);
-
-			> span {
-				&:first-child {
-					margin-right: 8px;
-
-					> .icon {
-						margin-right: 8px;
-					}
-				}
-
-				&:last-child {
-					margin-left: 8px;
-
-					> .icon {
-						margin-left: 8px;
-					}
-				}
-			}
-		}
-	}
-
-	&.noGap {
-		> * {
-			margin: 0 !important;
-			border: none;
-			border-radius: 0;
-			box-shadow: none;
-
-			&:not(:last-child) {
-				border-bottom: solid 0.5px var(--divider);
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/base.vue b/src/client/components/debobigego/base.vue
deleted file mode 100644
index f551a3478b..0000000000
--- a/src/client/components/debobigego/base.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div class="rbusrurv" :class="{ wide: forceWide }" v-size="{ max: [400] }">
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		forceWide: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rbusrurv {
-	// 他のCSSからも参照されるので消さないように
-	--debobigegoXPadding: 32px;
-	--debobigegoYPadding: 32px;
-
-	--debobigegoContentHMargin: 16px;
-
-	font-size: 95%;
-	line-height: 1.3em;
-	background: var(--bg);
-	padding: var(--debobigegoYPadding) var(--debobigegoXPadding);
-	max-width: 750px;
-	margin: 0 auto;
-
-	&:not(.wide).max-width_400px {
-		--debobigegoXPadding: 0px;
-
-		> ::v-deep(*) {
-			._debobigegoPanel {
-				border: solid 0.5px var(--divider);
-				border-radius: 0;
-				border-left: none;
-				border-right: none;
-			}
-
-			._debobigego_group {
-				> *:not(._debobigegoNoConcat) {
-					&:not(:last-child):not(._debobigegoNoConcatPrev) {
-						&._debobigegoPanel, ._debobigegoPanel {
-							border-bottom: solid 0.5px var(--divider);
-						}
-					}
-
-					&:not(:first-child):not(._debobigegoNoConcatNext) {
-						&._debobigegoPanel, ._debobigegoPanel {
-							border-top: none;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/button.vue b/src/client/components/debobigego/button.vue
deleted file mode 100644
index b883e817a4..0000000000
--- a/src/client/components/debobigego/button.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<template>
-<div class="yzpgjkxe _debobigegoItem">
-	<div class="_debobigegoLabel"><slot name="label"></slot></div>
-	<button class="main _button _debobigegoPanel _debobigegoClickable" :class="{ center, primary, danger }">
-		<slot></slot>
-		<div class="suffix">
-			<slot name="suffix"></slot>
-			<div class="icon">
-				<slot name="suffixIcon"></slot>
-			</div>
-		</div>
-	</button>
-	<div class="_debobigegoCaption"><slot name="desc"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import './debobigego.scss';
-
-export default defineComponent({
-	props: {
-		primary: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		danger: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		disabled: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		center: {
-			type: Boolean,
-			required: false,
-			default: true,
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.yzpgjkxe {
-	> .main {
-		display: flex;
-		width: 100%;
-		box-sizing: border-box;
-		padding: 14px 16px;
-		text-align: left;
-		align-items: center;
-
-		&.center {
-			display: block;
-			text-align: center;
-		}
-
-		&.primary {
-			color: var(--accent);
-		}
-
-		&.danger {
-			color: #ff2a2a;
-		}
-
-		> .suffix {
-			display: inline-flex;
-			margin-left: auto;
-			opacity: 0.7;
-
-			> .icon {
-				margin-left: 1em;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/debobigego.scss b/src/client/components/debobigego/debobigego.scss
deleted file mode 100644
index 833b656b66..0000000000
--- a/src/client/components/debobigego/debobigego.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-._debobigegoPanel {
-	background: var(--panel);
-	border-radius: var(--radius);
-	transition: background 0.2s ease;
-
-	&._debobigegoClickable {
-		&:hover {
-			//background: var(--panelHighlight);
-		}
-
-		&:active {
-			background: var(--panelHighlight);
-			transition: background 0s;
-		}
-	}
-}
-
-._debobigegoLabel,
-._debobigegoCaption {
-	font-size: 80%;
-	color: var(--fgTransparentWeak);
-
-	&:empty {
-		display: none;
-	}
-}
-
-._debobigegoLabel {
-	position: sticky;
-	top: var(--stickyTop, 0px);
-	z-index: 2;
-	margin: -8px calc(var(--debobigegoXPadding) * -1) 0 calc(var(--debobigegoXPadding) * -1);
-	padding: 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding)) 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding));
-	background: var(--X17);
-	-webkit-backdrop-filter: var(--blur, blur(10px));
-	backdrop-filter: var(--blur, blur(10px));
-}
-
-._themeChanging_ ._debobigegoLabel {
-	transition: none !important;
-	background: transparent;
-}
-
-._debobigegoCaption {
-	padding: 8px var(--debobigegoContentHMargin) 0 var(--debobigegoContentHMargin);
-}
-
-._debobigegoItem {
-	& + ._debobigegoItem {
-		margin-top: 24px;
-	}
-}
diff --git a/src/client/components/debobigego/group.vue b/src/client/components/debobigego/group.vue
deleted file mode 100644
index cba2c6ec94..0000000000
--- a/src/client/components/debobigego/group.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-<template>
-<div class="vrtktovg _debobigegoItem _debobigegoNoConcat" v-size="{ max: [500] }" v-sticky-container>
-	<div class="_debobigegoLabel"><slot name="label"></slot></div>
-	<div class="main _debobigego_group" ref="child">
-		<slot></slot>
-	</div>
-	<div class="_debobigegoCaption"><slot name="caption"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, ref } from 'vue';
-
-export default defineComponent({
-	setup(props, context) {
-		const child = ref<HTMLElement | null>(null);
-
-		const scanChild = () => {
-			if (child.value == null) return;
-			const els = Array.from(child.value.children);
-			for (let i = 0; i < els.length; i++) {
-				const el = els[i];
-				if (el.classList.contains('_debobigegoNoConcat')) {
-					if (els[i - 1]) els[i - 1].classList.add('_debobigegoNoConcatPrev');
-					if (els[i + 1]) els[i + 1].classList.add('_debobigegoNoConcatNext');
-				}
-			}
-		};
-
-		onMounted(() => {
-			scanChild();
-
-			const observer = new MutationObserver(records => {
-				scanChild();
-			});
-
-			observer.observe(child.value, {
-				childList: true,
-				subtree: false,
-				attributes: false,
-				characterData: false,
-			});
-		});
-
-		return {
-			child
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.vrtktovg {
-	> .main {
-		> ::v-deep(*):not(._debobigegoNoConcat) {
-			&:not(._debobigegoNoConcatNext) {
-				margin: 0;
-			}
-
-			&:not(:last-child):not(._debobigegoNoConcatPrev) {
-				&._debobigegoPanel, ._debobigegoPanel {
-					border-bottom: solid 0.5px var(--divider);
-					border-bottom-left-radius: 0;
-					border-bottom-right-radius: 0;
-				}
-			}
-
-			&:not(:first-child):not(._debobigegoNoConcatNext) {
-				&._debobigegoPanel, ._debobigegoPanel {
-					border-top: none;
-					border-top-left-radius: 0;
-					border-top-right-radius: 0;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/info.vue b/src/client/components/debobigego/info.vue
deleted file mode 100644
index 41afb03304..0000000000
--- a/src/client/components/debobigego/info.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<div class="fzenkabp _debobigegoItem">
-	<div class="_debobigegoPanel" :class="{ warn }">
-		<i v-if="warn" class="fas fa-exclamation-triangle"></i>
-		<i v-else class="fas fa-info-circle"></i>
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		warn: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	data() {
-		return {
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fzenkabp {
-	> div {
-		padding: 14px 16px;
-		font-size: 90%;
-		background: var(--infoBg);
-		color: var(--infoFg);
-
-		&.warn {
-			background: var(--infoWarnBg);
-			color: var(--infoWarnFg);
-		}
-
-		> i {
-			margin-right: 4px;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/input.vue b/src/client/components/debobigego/input.vue
deleted file mode 100644
index d113f04d27..0000000000
--- a/src/client/components/debobigego/input.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-<template>
-<FormGroup class="_debobigegoItem">
-	<template #label><slot></slot></template>
-	<div class="ztzhwixg _debobigegoItem" :class="{ inline, disabled }">
-		<div class="icon" ref="icon"><slot name="icon"></slot></div>
-		<div class="input _debobigegoPanel">
-			<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
-			<input ref="inputEl"
-				:type="type"
-				v-model="v"
-				:disabled="disabled"
-				:required="required"
-				:readonly="readonly"
-				:placeholder="placeholder"
-				:pattern="pattern"
-				:autocomplete="autocomplete"
-				:spellcheck="spellcheck"
-				:step="step"
-				@focus="focused = true"
-				@blur="focused = false"
-				@keydown="onKeydown($event)"
-				@input="onInput"
-				:list="id"
-			>
-			<datalist :id="id" v-if="datalist">
-				<option v-for="data in datalist" :value="data"/>
-			</datalist>
-			<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
-		</div>
-	</div>
-	<template #caption><slot name="desc"></slot></template>
-
-	<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-</FormGroup>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import './debobigego.scss';
-import FormButton from './button.vue';
-import FormGroup from './group.vue';
-
-export default defineComponent({
-	components: {
-		FormGroup,
-		FormButton,
-	},
-	props: {
-		modelValue: {
-			required: false
-		},
-		type: {
-			type: String,
-			required: false
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		readonly: {
-			type: Boolean,
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			required: false
-		},
-		pattern: {
-			type: String,
-			required: false
-		},
-		placeholder: {
-			type: String,
-			required: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autocomplete: {
-			required: false
-		},
-		spellcheck: {
-			required: false
-		},
-		step: {
-			required: false
-		},
-		datalist: {
-			type: Array,
-			required: false,
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		manualSave: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	emits: ['change', 'keydown', 'enter', 'update:modelValue'],
-	setup(props, context) {
-		const { modelValue, type, autofocus } = toRefs(props);
-		const v = ref(modelValue.value);
-		const id = Math.random().toString(); // TODO: uuid?
-		const focused = ref(false);
-		const changed = ref(false);
-		const invalid = ref(false);
-		const filled = computed(() => v.value !== '' && v.value != null);
-		const inputEl = ref(null);
-		const prefixEl = ref(null);
-		const suffixEl = ref(null);
-
-		const focus = () => inputEl.value.focus();
-		const onInput = (ev) => {
-			changed.value = true;
-			context.emit('change', ev);
-		};
-		const onKeydown = (ev: KeyboardEvent) => {
-			context.emit('keydown', ev);
-
-			if (ev.code === 'Enter') {
-				context.emit('enter');
-			}
-		};
-
-		const updated = () => {
-			changed.value = false;
-			if (type?.value === 'number') {
-				context.emit('update:modelValue', parseFloat(v.value));
-			} else {
-				context.emit('update:modelValue', v.value);
-			}
-		};
-
-		watch(modelValue.value, newValue => {
-			v.value = newValue;
-		});
-
-		watch(v, newValue => {
-			if (!props.manualSave) {
-				updated();
-			}
-
-			invalid.value = inputEl.value.validity.badInput;
-		});
-
-		onMounted(() => {
-			nextTick(() => {
-				if (autofocus.value) {
-					focus();
-				}
-
-				// このコンポーネントが作成された時、非表示状態である場合がある
-				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
-				const clock = setInterval(() => {
-					if (prefixEl.value) {
-						if (prefixEl.value.offsetWidth) {
-							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
-						}
-					}
-					if (suffixEl.value) {
-						if (suffixEl.value.offsetWidth) {
-							inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
-						}
-					}
-				}, 100);
-
-				onUnmounted(() => {
-					clearInterval(clock);
-				});
-			});
-		});
-
-		return {
-			id,
-			v,
-			focused,
-			invalid,
-			changed,
-			filled,
-			inputEl,
-			prefixEl,
-			suffixEl,
-			focus,
-			onInput,
-			onKeydown,
-			updated,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ztzhwixg {
-	position: relative;
-
-	> .icon {
-		position: absolute;
-		top: 0;
-		left: 0;
-		width: 24px;
-		text-align: center;
-		line-height: 32px;
-
-		&:not(:empty) + .input {
-			margin-left: 28px;
-		}
-	}
-
-	> .input {
-		$height: 48px;
-		position: relative;
-
-		> input {
-			display: block;
-			height: $height;
-			width: 100%;
-			margin: 0;
-			padding: 0 16px;
-			font: inherit;
-			font-weight: normal;
-			font-size: 1em;
-			line-height: $height;
-			color: var(--inputText);
-			background: transparent;
-			border: none;
-			border-radius: 0;
-			outline: none;
-			box-shadow: none;
-			box-sizing: border-box;
-
-			&[type='file'] {
-				display: none;
-			}
-		}
-
-		> .prefix,
-		> .suffix {
-			display: block;
-			position: absolute;
-			z-index: 1;
-			top: 0;
-			padding: 0 16px;
-			font-size: 1em;
-			line-height: $height;
-			color: var(--inputLabel);
-			pointer-events: none;
-
-			&:empty {
-				display: none;
-			}
-
-			> * {
-				display: inline-block;
-				min-width: 16px;
-				max-width: 150px;
-				overflow: hidden;
-				white-space: nowrap;
-				text-overflow: ellipsis;
-			}
-		}
-
-		> .prefix {
-			left: 0;
-			padding-right: 8px;
-		}
-
-		> .suffix {
-			right: 0;
-			padding-left: 8px;
-		}
-	}
-
-	&.inline {
-		display: inline-block;
-		margin: 0;
-	}
-
-	&.disabled {
-		opacity: 0.7;
-
-		&, * {
-			cursor: not-allowed !important;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/key-value-view.vue b/src/client/components/debobigego/key-value-view.vue
deleted file mode 100644
index 0e034a2d54..0000000000
--- a/src/client/components/debobigego/key-value-view.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<div class="_debobigegoItem">
-	<div class="_debobigegoPanel anocepby">
-		<span class="key"><slot name="key"></slot></span>
-		<span class="value"><slot name="value"></slot></span>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import './debobigego.scss';
-
-export default defineComponent({
-
-});
-</script>
-
-<style lang="scss" scoped>
-.anocepby {
-	display: flex;
-	align-items: center;
-	padding: 14px var(--debobigegoContentHMargin);
-
-	> .key {
-		margin-right: 12px;
-		white-space: nowrap;
-	}
-
-	> .value {
-		margin-left: auto;
-		opacity: 0.7;
-		text-overflow: ellipsis;
-		white-space: nowrap;
-		overflow: hidden;
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/link.vue b/src/client/components/debobigego/link.vue
deleted file mode 100644
index 885579eadf..0000000000
--- a/src/client/components/debobigego/link.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<template>
-<div class="qmfkfnzi _debobigegoItem">
-	<a class="main _button _debobigegoPanel _debobigegoClickable" :href="to" target="_blank" v-if="external">
-		<span class="icon"><slot name="icon"></slot></span>
-		<span class="text"><slot></slot></span>
-		<span class="right">
-			<span class="text"><slot name="suffix"></slot></span>
-			<i class="fas fa-external-link-alt icon"></i>
-		</span>
-	</a>
-	<MkA class="main _button _debobigegoPanel _debobigegoClickable" :class="{ active }" :to="to" :behavior="behavior" v-else>
-		<span class="icon"><slot name="icon"></slot></span>
-		<span class="text"><slot></slot></span>
-		<span class="right">
-			<span class="text"><slot name="suffix"></slot></span>
-			<i class="fas fa-chevron-right icon"></i>
-		</span>
-	</MkA>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import './debobigego.scss';
-
-export default defineComponent({
-	props: {
-		to: {
-			type: String,
-			required: true
-		},
-		active: {
-			type: Boolean,
-			required: false
-		},
-		external: {
-			type: Boolean,
-			required: false
-		},
-		behavior: {
-			type: String,
-			required: false,
-		},
-	},
-	data() {
-		return {
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.qmfkfnzi {
-	> .main {
-		display: flex;
-		align-items: center;
-		width: 100%;
-		box-sizing: border-box;
-		padding: 14px 16px 14px 14px;
-
-		&:hover {
-			text-decoration: none;
-		}
-
-		&.active {
-			color: var(--accent);
-			background: var(--panelHighlight);
-		}
-
-		> .icon {
-			width: 32px;
-			margin-right: 2px;
-			flex-shrink: 0;
-			text-align: center;
-			opacity: 0.8;
-
-			&:empty {
-				display: none;
-
-				& + .text {
-					padding-left: 4px;
-				}
-			}
-		}
-
-		> .text {
-			white-space: nowrap;
-			text-overflow: ellipsis;
-			overflow: hidden;
-			padding-right: 12px;
-		}
-
-		> .right {
-			margin-left: auto;
-			opacity: 0.7;
-
-			> .text:not(:empty) {
-				margin-right: 0.75em;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/object-view.vue b/src/client/components/debobigego/object-view.vue
deleted file mode 100644
index ea79daa915..0000000000
--- a/src/client/components/debobigego/object-view.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-<FormGroup class="_debobigegoItem">
-	<template #label><slot></slot></template>
-	<div class="drooglns _debobigegoItem" :class="{ tall }">
-		<div class="input _debobigegoPanel">
-			<textarea class="_monospace"
-				v-model="v"
-				readonly
-				:spellcheck="false"
-			></textarea>
-		</div>
-	</div>
-	<template #caption><slot name="desc"></slot></template>
-</FormGroup>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref, toRefs, watch } from 'vue';
-import * as JSON5 from 'json5';
-import './debobigego.scss';
-import FormGroup from './group.vue';
-
-export default defineComponent({
-	components: {
-		FormGroup,
-	},
-	props: {
-		value: {
-			required: false
-		},
-		tall: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		pre: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		manualSave: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	setup(props, context) {
-		const { value } = toRefs(props);
-		const v = ref('');
-
-		watch(() => value, newValue => {
-			v.value = JSON5.stringify(newValue.value, null, '\t');
-		}, {
-			immediate: true
-		});
-
-		return {
-			v,
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.drooglns {
-	position: relative;
-
-	> .input {
-		position: relative;
-	
-		> textarea {
-			display: block;
-			width: 100%;
-			min-width: 100%;
-			max-width: 100%;
-			min-height: 130px;
-			margin: 0;
-			padding: 16px var(--debobigegoContentHMargin);
-			box-sizing: border-box;
-			font: inherit;
-			font-weight: normal;
-			font-size: 1em;
-			background: transparent;
-			border: none;
-			border-radius: 0;
-			outline: none;
-			box-shadow: none;
-			color: var(--fg);
-			tab-size: 2;
-			white-space: pre;
-		}
-	}
-
-	&.tall {
-		> .input {
-			> textarea {
-				min-height: 200px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/pagination.vue b/src/client/components/debobigego/pagination.vue
deleted file mode 100644
index 2166f5065f..0000000000
--- a/src/client/components/debobigego/pagination.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<FormGroup class="uljviswt _debobigegoItem">
-	<template #label><slot name="label"></slot></template>
-	<slot :items="items"></slot>
-	<div class="empty" v-if="empty" key="_empty_">
-		<slot name="empty"></slot>
-	</div>
-	<FormButton v-show="more" class="button" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
-		<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-		<template v-if="moreFetching"><MkLoading inline/></template>
-	</FormButton>
-</FormGroup>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormButton from './button.vue';
-import FormGroup from './group.vue';
-import paging from '@client/scripts/paging';
-
-export default defineComponent({
-	components: {
-		FormButton,
-		FormGroup,
-	},
-
-	mixins: [
-		paging({}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.uljviswt {
-}
-</style>
diff --git a/src/client/components/debobigego/radios.vue b/src/client/components/debobigego/radios.vue
deleted file mode 100644
index 071c013afb..0000000000
--- a/src/client/components/debobigego/radios.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<script lang="ts">
-import { defineComponent, h } from 'vue';
-import MkRadio from '@client/components/form/radio.vue';
-import './debobigego.scss';
-
-export default defineComponent({
-	components: {
-		MkRadio
-	},
-	props: {
-		modelValue: {
-			required: false
-		},
-	},
-	data() {
-		return {
-			value: this.modelValue,
-		}
-	},
-	watch: {
-		modelValue() {
-			this.value = this.modelValue;
-		},
-		value() {
-			this.$emit('update:modelValue', this.value);
-		}
-	},
-	render() {
-		const label = this.$slots.desc();
-		let options = this.$slots.default();
-
-		// なぜかFragmentになることがあるため
-		if (options.length === 1 && options[0].props == null) options = options[0].children;
-
-		return h('div', {
-			class: 'cnklmpwm _debobigegoItem'
-		}, [
-			h('div', {
-				class: '_debobigegoLabel',
-			}, label),
-			...options.map(option => h('button', {
-				class: '_button _debobigegoPanel _debobigegoClickable',
-				key: option.key,
-				onClick: () => this.value = option.props.value,
-			}, [h('span', {
-				class: ['check', { checked: this.value === option.props.value }],
-			}), option.children]))
-		]);
-	}
-});
-</script>
-
-<style lang="scss">
-.cnklmpwm {
-	> button {
-		display: block;
-		width: 100%;
-		box-sizing: border-box;
-		padding: 14px 18px;
-		text-align: left;
-
-		&:not(:first-of-type) {
-			border-top: none !important;
-			border-top-left-radius: 0;
-			border-top-right-radius: 0;
-		}
-
-		&:not(:last-of-type) {
-			border-bottom: solid 0.5px var(--divider);
-			border-bottom-left-radius: 0;
-			border-bottom-right-radius: 0;
-		}
-
-		> .check {
-			display: inline-block;
-			vertical-align: bottom;
-			position: relative;
-			width: 16px;
-			height: 16px;
-			margin-right: 8px;
-			background: none;
-			border: 2px solid var(--inputBorder);
-			border-radius: 100%;
-			transition: inherit;
-
-			&:after {
-				content: "";
-				display: block;
-				position: absolute;
-				top: 3px;
-				right: 3px;
-				bottom: 3px;
-				left: 3px;
-				border-radius: 100%;
-				opacity: 0;
-				transform: scale(0);
-				transition: .4s cubic-bezier(.25,.8,.25,1);
-			}
-
-			&.checked {
-				border-color: var(--accent);
-
-				&:after {
-					background-color: var(--accent);
-					transform: scale(1);
-					opacity: 1;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/range.vue b/src/client/components/debobigego/range.vue
deleted file mode 100644
index 26fb0f37c6..0000000000
--- a/src/client/components/debobigego/range.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-<div class="ifitouly _debobigegoItem" :class="{ focused, disabled }">
-	<div class="_debobigegoLabel"><slot name="label"></slot></div>
-	<div class="_debobigegoPanel main">
-		<input
-			type="range"
-			ref="input"
-			v-model="v"
-			:disabled="disabled"
-			:min="min"
-			:max="max"
-			:step="step"
-			@focus="focused = true"
-			@blur="focused = false"
-			@input="$emit('update:value', $event.target.value)"
-		/>
-	</div>
-	<div class="_debobigegoCaption"><slot name="caption"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		value: {
-			type: Number,
-			required: false,
-			default: 0
-		},
-		disabled: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		min: {
-			type: Number,
-			required: false,
-			default: 0
-		},
-		max: {
-			type: Number,
-			required: false,
-			default: 100
-		},
-		step: {
-			type: Number,
-			required: false,
-			default: 1
-		},
-	},
-	data() {
-		return {
-			v: this.value,
-			focused: false
-		};
-	},
-	watch: {
-		value(v) {
-			this.v = parseFloat(v);
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ifitouly {
-	position: relative;
-
-	> .main {
-		padding: 22px 16px;
-
-		> input {
-			display: block;
-			-webkit-appearance: none;
-			-moz-appearance: none;
-			appearance: none;
-			background: var(--X10);
-			height: 4px;
-			width: 100%;
-			box-sizing: border-box;
-			margin: 0;
-			outline: 0;
-			border: 0;
-			border-radius: 7px;
-
-			&.disabled {
-				opacity: 0.6;
-				cursor: not-allowed;
-			}
-
-			&::-webkit-slider-thumb {
-				-webkit-appearance: none;
-				appearance: none;
-				cursor: pointer;
-				width: 20px;
-				height: 20px;
-				display: block;
-				border-radius: 50%;
-				border: none;
-				background: var(--accent);
-				box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
-				box-sizing: content-box;
-			}
-
-			&::-moz-range-thumb {
-				-moz-appearance: none;
-				appearance: none;
-				cursor: pointer;
-				width: 20px;
-				height: 20px;
-				display: block;
-				border-radius: 50%;
-				border: none;
-				background: var(--accent);
-				box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/select.vue b/src/client/components/debobigego/select.vue
deleted file mode 100644
index 7a31371afc..0000000000
--- a/src/client/components/debobigego/select.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-<template>
-<div class="yrtfrpux _debobigegoItem" :class="{ disabled, inline }">
-	<div class="_debobigegoLabel"><slot name="label"></slot></div>
-	<div class="icon" ref="icon"><slot name="icon"></slot></div>
-	<div class="input _debobigegoPanel _debobigegoClickable" @click="focus">
-		<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
-		<select ref="input"
-			v-model="v"
-			:required="required"
-			:disabled="disabled"
-			@focus="focused = true"
-			@blur="focused = false"
-		>
-			<slot></slot>
-		</select>
-		<div class="suffix">
-			<i class="fas fa-chevron-down"></i>
-		</div>
-	</div>
-	<div class="_debobigegoCaption"><slot name="caption"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import './debobigego.scss';
-
-export default defineComponent({
-	props: {
-		modelValue: {
-			required: false
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			required: false
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	data() {
-		return {
-		};
-	},
-	computed: {
-		v: {
-			get() {
-				return this.modelValue;
-			},
-			set(v) {
-				this.$emit('update:modelValue', v);
-			}
-		},
-	},
-	methods: {
-		focus() {
-			this.$refs.input.focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.yrtfrpux {
-	position: relative;
-
-	> .icon {
-		position: absolute;
-		top: 0;
-		left: 0;
-		width: 24px;
-		text-align: center;
-		line-height: 32px;
-
-		&:not(:empty) + .input {
-			margin-left: 28px;
-		}
-	}
-
-	> .input {
-		display: flex;
-		position: relative;
-
-		> select {
-			display: block;
-			flex: 1;
-			width: 100%;
-			padding: 0 16px;
-			font: inherit;
-			font-weight: normal;
-			font-size: 1em;
-			height: 48px;
-			background: none;
-			border: none;
-			border-radius: 0;
-			outline: none;
-			box-shadow: none;
-			appearance: none;
-			-webkit-appearance: none;
-			color: var(--fg);
-
-			option,
-			optgroup {
-				color: var(--fg);
-				background: var(--bg);
-			}
-		}
-
-		> .prefix,
-		> .suffix {
-			display: block;
-			align-self: center;
-			justify-self: center;
-			font-size: 1em;
-			line-height: 32px;
-			color: var(--inputLabel);
-			pointer-events: none;
-
-			&:empty {
-				display: none;
-			}
-
-			> * {
-				display: block;
-				min-width: 16px;
-			}
-		}
-
-		> .prefix {
-			padding-right: 4px;
-		}
-
-		> .suffix {
-			padding: 0 16px 0 0;
-			opacity: 0.7;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/suspense.vue b/src/client/components/debobigego/suspense.vue
deleted file mode 100644
index e59e0ba12d..0000000000
--- a/src/client/components/debobigego/suspense.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<template>
-<transition name="fade" mode="out-in">
-	<div class="_debobigegoItem" v-if="pending">
-		<div class="_debobigegoPanel">
-			<MkLoading/>
-		</div>
-	</div>
-	<div v-else-if="resolved" class="_debobigegoItem">
-		<slot :result="result"></slot>
-	</div>
-	<div class="_debobigegoItem" v-else>
-		<div class="_debobigegoPanel eiurkvay">
-			<div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
-			<MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
-		</div>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType, ref, watch } from 'vue';
-import './debobigego.scss';
-import MkButton from '@client/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	props: {
-		p: {
-			type: Function as PropType<() => Promise<any>>,
-			required: true,
-		}
-	},
-
-	setup(props, context) {
-		const pending = ref(true);
-		const resolved = ref(false);
-		const rejected = ref(false);
-		const result = ref(null);
-
-		const process = () => {
-			if (props.p == null) {
-				return;
-			}
-			const promise = props.p();
-			pending.value = true;
-			resolved.value = false;
-			rejected.value = false;
-			promise.then((_result) => {
-				pending.value = false;
-				resolved.value = true;
-				result.value = _result;
-			});
-			promise.catch(() => {
-				pending.value = false;
-				rejected.value = true;
-			});
-		};
-
-		watch(() => props.p, () => {
-			process();
-		}, {
-			immediate: true
-		});
-
-		const retry = () => {
-			process();
-		};
-
-		return {
-			pending,
-			resolved,
-			rejected,
-			result,
-			retry,
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.eiurkvay {
-	padding: 16px;
-	text-align: center;
-
-	> .retry {
-		margin-top: 16px;
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/switch.vue b/src/client/components/debobigego/switch.vue
deleted file mode 100644
index 9a69e18302..0000000000
--- a/src/client/components/debobigego/switch.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<template>
-<div class="ijnpvmgr _debobigegoItem">
-	<div class="main _debobigegoPanel _debobigegoClickable"
-		:class="{ disabled, checked }"
-		:aria-checked="checked"
-		:aria-disabled="disabled"
-		@click.prevent="toggle"
-	>
-		<input
-			type="checkbox"
-			ref="input"
-			:disabled="disabled"
-			@keydown.enter="toggle"
-		>
-		<span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
-			<span class="handle"></span>
-		</span>
-		<span class="label">
-			<span><slot></slot></span>
-		</span>
-	</div>
-	<div class="_debobigegoCaption"><slot name="desc"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import './debobigego.scss';
-
-export default defineComponent({
-	props: {
-		modelValue: {
-			type: Boolean,
-			default: false
-		},
-		disabled: {
-			type: Boolean,
-			default: false
-		}
-	},
-	computed: {
-		checked(): boolean {
-			return this.modelValue;
-		}
-	},
-	methods: {
-		toggle() {
-			if (this.disabled) return;
-			this.$emit('update:modelValue', !this.checked);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ijnpvmgr {
-	> .main {
-		position: relative;
-		display: flex;
-		padding: 14px 16px;
-		cursor: pointer;
-
-		> * {
-			user-select: none;
-		}
-
-		> input {
-			position: absolute;
-			width: 0;
-			height: 0;
-			opacity: 0;
-			margin: 0;
-		}
-
-		> .button {
-			position: relative;
-			display: inline-block;
-			flex-shrink: 0;
-			margin: 0;
-			width: 34px;
-			height: 22px;
-			background: var(--switchBg);
-			outline: none;
-			border-radius: 999px;
-			transition: all 0.3s;
-			cursor: pointer;
-
-			> .handle {
-				position: absolute;
-				top: 0;
-				left: 3px;
-				bottom: 0;
-				margin: auto 0;
-				border-radius: 100%;
-				transition: background-color 0.3s, transform 0.3s;
-				width: 16px;
-				height: 16px;
-				background-color: #fff;
-				pointer-events: none;
-			}
-		}
-
-		> .label {
-			margin-left: 12px;
-			display: block;
-			transition: inherit;
-			color: var(--fg);
-
-			> span {
-				display: block;
-				line-height: 20px;
-				transition: inherit;
-			}
-		}
-
-		&.disabled {
-			opacity: 0.6;
-			cursor: not-allowed;
-		}
-
-		&.checked {
-			> .button {
-				background-color: var(--accent);
-
-				> .handle {
-					transform: translateX(12px);
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/textarea.vue b/src/client/components/debobigego/textarea.vue
deleted file mode 100644
index 64e8d47126..0000000000
--- a/src/client/components/debobigego/textarea.vue
+++ /dev/null
@@ -1,161 +0,0 @@
-<template>
-<FormGroup class="_debobigegoItem">
-	<template #label><slot></slot></template>
-	<div class="rivhosbp _debobigegoItem" :class="{ tall, pre }">
-		<div class="input _debobigegoPanel">
-			<textarea ref="input" :class="{ code, _monospace: code }"
-				v-model="v"
-				:required="required"
-				:readonly="readonly"
-				:pattern="pattern"
-				:autocomplete="autocomplete"
-				:spellcheck="!code"
-				@input="onInput"
-				@focus="focused = true"
-				@blur="focused = false"
-			></textarea>
-		</div>
-	</div>
-	<template #caption><slot name="desc"></slot></template>
-
-	<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-</FormGroup>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref, toRefs, watch } from 'vue';
-import './debobigego.scss';
-import FormButton from './button.vue';
-import FormGroup from './group.vue';
-
-export default defineComponent({
-	components: {
-		FormGroup,
-		FormButton,
-	},
-	props: {
-		modelValue: {
-			required: false
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		readonly: {
-			type: Boolean,
-			required: false
-		},
-		pattern: {
-			type: String,
-			required: false
-		},
-		autocomplete: {
-			type: String,
-			required: false
-		},
-		code: {
-			type: Boolean,
-			required: false
-		},
-		tall: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		pre: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		manualSave: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	setup(props, context) {
-		const { modelValue } = toRefs(props);
-		const v = ref(modelValue.value);
-		const changed = ref(false);
-		const inputEl = ref(null);
-		const focus = () => inputEl.value.focus();
-		const onInput = (ev) => {
-			changed.value = true;
-			context.emit('change', ev);
-		};
-
-		const updated = () => {
-			changed.value = false;
-			context.emit('update:modelValue', v.value);
-		};
-
-		watch(modelValue.value, newValue => {
-			v.value = newValue;
-		});
-
-		watch(v, newValue => {
-			if (!props.manualSave) {
-				updated();
-			}
-		});
-		
-		return {
-			v,
-			updated,
-			changed,
-			focus,
-			onInput,
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rivhosbp {
-	position: relative;
-
-	> .input {
-		position: relative;
-	
-		> textarea {
-			display: block;
-			width: 100%;
-			min-width: 100%;
-			max-width: 100%;
-			min-height: 130px;
-			margin: 0;
-			padding: 16px;
-			box-sizing: border-box;
-			font: inherit;
-			font-weight: normal;
-			font-size: 1em;
-			background: transparent;
-			border: none;
-			border-radius: 0;
-			outline: none;
-			box-shadow: none;
-			color: var(--fg);
-
-			&.code {
-				tab-size: 2;
-			}
-		}
-	}
-
-	&.tall {
-		> .input {
-			> textarea {
-				min-height: 200px;
-			}
-		}
-	}
-
-	&.pre {
-		> .input {
-			> textarea {
-				white-space: pre;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/debobigego/tuple.vue b/src/client/components/debobigego/tuple.vue
deleted file mode 100644
index 8a4599fd64..0000000000
--- a/src/client/components/debobigego/tuple.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-<div class="wthhikgt _debobigegoItem" v-size="{ max: [500] }">
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-});
-</script>
-
-<style lang="scss" scoped>
-.wthhikgt {
-	position: relative;
-	display: flex;
-
-	> ::v-deep(*) {
-		flex: 1;
-		margin: 0;
-
-		&:not(:last-child) {
-			margin-right: 16px;
-		}
-	}
-
-	&.max-width_500px {
-		display: block;
-
-		> ::v-deep(*) {
-			margin: inherit;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/dialog.vue b/src/client/components/dialog.vue
deleted file mode 100644
index dd4932f61f..0000000000
--- a/src/client/components/dialog.vue
+++ /dev/null
@@ -1,212 +0,0 @@
-<template>
-<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
-	<div class="mk-dialog">
-		<div class="icon" v-if="icon">
-			<i :class="icon"></i>
-		</div>
-		<div class="icon" v-else-if="!input && !select" :class="type">
-			<i v-if="type === 'success'" class="fas fa-check"></i>
-			<i v-else-if="type === 'error'" class="fas fa-times-circle"></i>
-			<i v-else-if="type === 'warning'" class="fas fa-exclamation-triangle"></i>
-			<i v-else-if="type === 'info'" class="fas fa-info-circle"></i>
-			<i v-else-if="type === 'question'" class="fas fa-question-circle"></i>
-			<i v-else-if="type === 'waiting'" class="fas fa-spinner fa-pulse"></i>
-		</div>
-		<header v-if="title"><Mfm :text="title"/></header>
-		<div class="body" v-if="text"><Mfm :text="text"/></div>
-		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput>
-		<MkSelect v-if="select" v-model="selectedValue" autofocus>
-			<template v-if="select.items">
-				<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
-			</template>
-			<template v-else>
-				<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
-					<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
-				</optgroup>
-			</template>
-		</MkSelect>
-		<div class="buttons" v-if="(showOkButton || showCancelButton) && !actions">
-			<MkButton inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select">{{ (showCancelButton || input || select) ? $ts.ok : $ts.gotIt }}</MkButton>
-			<MkButton inline @click="cancel" v-if="showCancelButton || input || select">{{ $ts.cancel }}</MkButton>
-		</div>
-		<div class="buttons" v-if="actions">
-			<MkButton v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</MkButton>
-		</div>
-	</div>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkModal from '@client/components/ui/modal.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-
-export default defineComponent({
-	components: {
-		MkModal,
-		MkButton,
-		MkInput,
-		MkSelect,
-	},
-
-	props: {
-		type: {
-			type: String,
-			required: false,
-			default: 'info'
-		},
-		title: {
-			type: String,
-			required: false
-		},
-		text: {
-			type: String,
-			required: false
-		},
-		input: {
-			required: false
-		},
-		select: {
-			required: false
-		},
-		icon: {
-			required: false
-		},
-		actions: {
-			required: false
-		},
-		showOkButton: {
-			type: Boolean,
-			default: true
-		},
-		showCancelButton: {
-			type: Boolean,
-			default: false
-		},
-		cancelableByBgClick: {
-			type: Boolean,
-			default: true
-		},
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			inputValue: this.input && this.input.default ? this.input.default : null,
-			selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
-		};
-	},
-
-	mounted() {
-		document.addEventListener('keydown', this.onKeydown);
-	},
-
-	beforeUnmount() {
-		document.removeEventListener('keydown', this.onKeydown);
-	},
-
-	methods: {
-		done(canceled, result?) {
-			this.$emit('done', { canceled, result });
-			this.$refs.modal.close();
-		},
-
-		async ok() {
-			if (!this.showOkButton) return;
-
-			const result =
-				this.input ? this.inputValue :
-				this.select ? this.selectedValue :
-				true;
-			this.done(false, result);
-		},
-
-		cancel() {
-			this.done(true);
-		},
-
-		onBgClick() {
-			if (this.cancelableByBgClick) {
-				this.cancel();
-			}
-		},
-
-		onKeydown(e) {
-			if (e.which === 27) { // ESC
-				this.cancel();
-			}
-		},
-
-		onInputKeydown(e) {
-			if (e.which === 13) { // Enter
-				e.preventDefault();
-				e.stopPropagation();
-				this.ok();
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-dialog {
-	position: relative;
-	padding: 32px;
-	min-width: 320px;
-	max-width: 480px;
-	box-sizing: border-box;
-	text-align: center;
-	background: var(--panel);
-	border-radius: var(--radius);
-
-	> .icon {
-		font-size: 32px;
-
-		&.success {
-			color: var(--success);
-		}
-
-		&.error {
-			color: var(--error);
-		}
-
-		&.warning {
-			color: var(--warn);
-		}
-
-		> * {
-			display: block;
-			margin: 0 auto;
-		}
-
-		& + header {
-			margin-top: 16px;
-		}
-	}
-
-	> header {
-		margin: 0 0 8px 0;
-		font-weight: bold;
-		font-size: 20px;
-
-		& + .body {
-			margin-top: 8px;
-		}
-	}
-
-	> .body {
-		margin: 16px 0 0 0;
-	}
-
-	> .buttons {
-		margin-top: 16px;
-
-		> * {
-			margin: 0 8px;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/drive-file-thumbnail.vue b/src/client/components/drive-file-thumbnail.vue
deleted file mode 100644
index 2cb1d98618..0000000000
--- a/src/client/components/drive-file-thumbnail.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<template>
-<div class="zdjebgpv" ref="thumbnail">
-	<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
-	<i v-else-if="is === 'image'" class="fas fa-file-image icon"></i>
-	<i v-else-if="is === 'video'" class="fas fa-file-video icon"></i>
-	<i v-else-if="is === 'audio' || is === 'midi'" class="fas fa-music icon"></i>
-	<i v-else-if="is === 'csv'" class="fas fa-file-csv icon"></i>
-	<i v-else-if="is === 'pdf'" class="fas fa-file-pdf icon"></i>
-	<i v-else-if="is === 'textfile'" class="fas fa-file-alt icon"></i>
-	<i v-else-if="is === 'archive'" class="fas fa-file-archive icon"></i>
-	<i v-else class="fas fa-file icon"></i>
-
-	<i v-if="isThumbnailAvailable && is === 'video'" class="fas fa-film icon-sub"></i>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import ImgWithBlurhash from '@client/components/img-with-blurhash.vue';
-import { ColdDeviceStorage } from '@client/store';
-
-export default defineComponent({
-	components: {
-		ImgWithBlurhash
-	},
-	props: {
-		file: {
-			type: Object,
-			required: true
-		},
-		fit: {
-			type: String,
-			required: false,
-			default: 'cover'
-		},
-	},
-	data() {
-		return {
-			isContextmenuShowing: false,
-			isDragging: false,
-
-		};
-	},
-	computed: {
-		is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
-			if (this.file.type.startsWith('image/')) return 'image';
-			if (this.file.type.startsWith('video/')) return 'video';
-			if (this.file.type === 'audio/midi') return 'midi';
-			if (this.file.type.startsWith('audio/')) return 'audio';
-			if (this.file.type.endsWith('/csv')) return 'csv';
-			if (this.file.type.endsWith('/pdf')) return 'pdf';
-			if (this.file.type.startsWith('text/')) return 'textfile';
-			if ([
-					"application/zip",
-					"application/x-cpio",
-					"application/x-bzip",
-					"application/x-bzip2",
-					"application/java-archive",
-					"application/x-rar-compressed",
-					"application/x-tar",
-					"application/gzip",
-					"application/x-7z-compressed"
-				].some(e => e === this.file.type)) return 'archive';
-			return 'unknown';
-		},
-		isThumbnailAvailable(): boolean {
-			return this.file.thumbnailUrl
-				? (this.is === 'image' || this.is === 'video')
-				: false;
-		},
-	},
-	mounted() {
-		const audioTag = this.$refs.volumectrl as HTMLAudioElement;
-		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
-	},
-	methods: {
-		volumechange() {
-			const audioTag = this.$refs.volumectrl as HTMLAudioElement;
-			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.zdjebgpv {
-	position: relative;
-
-	> .icon-sub {
-		position: absolute;
-		width: 30%;
-		height: auto;
-		margin: 0;
-		right: 4%;
-		bottom: 4%;
-	}
-
-	> * {
-		margin: auto;
-	}
-
-	> .icon {
-		pointer-events: none;
-		height: 65%;
-		width: 65%;
-	}
-}
-</style>
diff --git a/src/client/components/drive-select-dialog.vue b/src/client/components/drive-select-dialog.vue
deleted file mode 100644
index ce6e2fa789..0000000000
--- a/src/client/components/drive-select-dialog.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="800"
-	:height="500"
-	:with-ok-button="true"
-	:ok-button-disabled="(type === 'file') && (selected.length === 0)"
-	@click="cancel()"
-	@close="cancel()"
-	@ok="ok()"
-	@closed="$emit('closed')"
->
-	<template #header>
-		{{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }}
-		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
-	</template>
-	<XDrive :multiple="multiple" @changeSelection="onChangeSelection" @selected="ok()" :select="type"/>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XDrive from './drive.vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import number from '@client/filters/number';
-
-export default defineComponent({
-	components: {
-		XDrive,
-		XModalWindow,
-	},
-
-	props: {
-		type: {
-			type: String,
-			required: false,
-			default: 'file'
-		},
-		multiple: {
-			type: Boolean,
-			default: false
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			selected: []
-		};
-	},
-
-	methods: {
-		ok() {
-			this.$emit('done', this.selected);
-			this.$refs.dialog.close();
-		},
-
-		cancel() {
-			this.$emit('done');
-			this.$refs.dialog.close();
-		},
-
-		onChangeSelection(xs) {
-			this.selected = xs;
-		},
-
-		number
-	}
-});
-</script>
diff --git a/src/client/components/drive-window.vue b/src/client/components/drive-window.vue
deleted file mode 100644
index 30b04091be..0000000000
--- a/src/client/components/drive-window.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<XWindow ref="window"
-	:initial-width="800"
-	:initial-height="500"
-	:can-resize="true"
-	@closed="$emit('closed')"
->
-	<template #header>
-		{{ $ts.drive }}
-	</template>
-	<XDrive :initial-folder="initialFolder"/>
-</XWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XDrive from './drive.vue';
-import XWindow from '@client/components/ui/window.vue';
-
-export default defineComponent({
-	components: {
-		XDrive,
-		XWindow,
-	},
-
-	props: {
-		initialFolder: {
-			type: Object,
-			required: false
-		},
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-
-	}
-});
-</script>
diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue
deleted file mode 100644
index b1be3d0cab..0000000000
--- a/src/client/components/drive.file.vue
+++ /dev/null
@@ -1,374 +0,0 @@
-<template>
-<div class="ncvczrfv"
-	:class="{ isSelected }"
-	@click="onClick"
-	@contextmenu.stop="onContextmenu"
-	draggable="true"
-	@dragstart="onDragstart"
-	@dragend="onDragend"
-	:title="title"
->
-	<div class="label" v-if="$i.avatarId == file.id">
-		<img src="/static-assets/client/label.svg"/>
-		<p>{{ $ts.avatar }}</p>
-	</div>
-	<div class="label" v-if="$i.bannerId == file.id">
-		<img src="/static-assets/client/label.svg"/>
-		<p>{{ $ts.banner }}</p>
-	</div>
-	<div class="label red" v-if="file.isSensitive">
-		<img src="/static-assets/client/label-red.svg"/>
-		<p>{{ $ts.nsfw }}</p>
-	</div>
-
-	<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
-
-	<p class="name">
-		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
-		<span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
-	</p>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
-import bytes from '@client/filters/bytes';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkDriveFileThumbnail
-	},
-
-	props: {
-		file: {
-			type: Object,
-			required: true,
-		},
-		isSelected: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		selectMode: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['chosen'],
-
-	data() {
-		return {
-			isDragging: false
-		};
-	},
-
-	computed: {
-		// TODO: parentへの参照を無くす
-		browser(): any {
-			return this.$parent;
-		},
-		title(): string {
-			return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
-		}
-	},
-
-	methods: {
-		getMenu() {
-			return [{
-				text: this.$ts.rename,
-				icon: 'fas fa-i-cursor',
-				action: this.rename
-			}, {
-				text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
-				icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
-				action: this.toggleSensitive
-			}, {
-				text: this.$ts.describeFile,
-				icon: 'fas fa-i-cursor',
-				action: this.describe
-			}, null, {
-				text: this.$ts.copyUrl,
-				icon: 'fas fa-link',
-				action: this.copyUrl
-			}, {
-				type: 'a',
-				href: this.file.url,
-				target: '_blank',
-				text: this.$ts.download,
-				icon: 'fas fa-download',
-				download: this.file.name
-			}, null, {
-				text: this.$ts.delete,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: this.deleteFile
-			}];
-		},
-
-		onClick(ev) {
-			if (this.selectMode) {
-				this.$emit('chosen', this.file);
-			} else {
-				os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
-			}
-		},
-
-		onContextmenu(e) {
-			os.contextMenu(this.getMenu(), e);
-		},
-
-		onDragstart(e) {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
-			this.isDragging = true;
-
-			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
-			// (=あなたの子供が、ドラッグを開始しましたよ)
-			this.browser.isDragSource = true;
-		},
-
-		onDragend(e) {
-			this.isDragging = false;
-			this.browser.isDragSource = false;
-		},
-
-		rename() {
-			os.dialog({
-				title: this.$ts.renameFile,
-				input: {
-					placeholder: this.$ts.inputNewFileName,
-					default: this.file.name,
-					allowEmpty: false
-				}
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				os.api('drive/files/update', {
-					fileId: this.file.id,
-					name: name
-				});
-			});
-		},
-
-		describe() {
-			os.popup(import('@client/components/media-caption.vue'), {
-				title: this.$ts.describeFile,
-				input: {
-					placeholder: this.$ts.inputNewDescription,
-					default: this.file.comment !== null ? this.file.comment : '',
-				},
-				image: this.file
-			}, {
-				done: result => {
-					if (!result || result.canceled) return;
-					let comment = result.result;
-					os.api('drive/files/update', {
-						fileId: this.file.id,
-						comment: comment.length == 0 ? null : comment
-					});
-				}
-			}, 'closed');
-		},
-
-		toggleSensitive() {
-			os.api('drive/files/update', {
-				fileId: this.file.id,
-				isSensitive: !this.file.isSensitive
-			});
-		},
-
-		copyUrl() {
-			copyToClipboard(this.file.url);
-			os.success();
-		},
-
-		setAsAvatar() {
-			os.updateAvatar(this.file);
-		},
-
-		setAsBanner() {
-			os.updateBanner(this.file);
-		},
-
-		addApp() {
-			alert('not implemented yet');
-		},
-
-		async deleteFile() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			os.api('drive/files/delete', {
-				fileId: this.file.id
-			});
-		},
-
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ncvczrfv {
-	position: relative;
-	padding: 8px 0 0 0;
-	min-height: 180px;
-	border-radius: 4px;
-
-	&, * {
-		cursor: pointer;
-	}
-
-	> * {
-		pointer-events: none;
-	}
-
-	&:hover {
-		background: rgba(#000, 0.05);
-
-		> .label {
-			&:before,
-			&:after {
-				background: #0b65a5;
-			}
-
-			&.red {
-				&:before,
-				&:after {
-					background: #c12113;
-				}
-			}
-		}
-	}
-
-	&:active {
-		background: rgba(#000, 0.1);
-
-		> .label {
-			&:before,
-			&:after {
-				background: #0b588c;
-			}
-
-			&.red {
-				&:before,
-				&:after {
-					background: #ce2212;
-				}
-			}
-		}
-	}
-
-	&.isSelected {
-		background: var(--accent);
-
-		&:hover {
-			background: var(--accentLighten);
-		}
-
-		&:active {
-			background: var(--accentDarken);
-		}
-
-		> .label {
-			&:before,
-			&:after {
-				display: none;
-			}
-		}
-
-		> .name {
-			color: #fff;
-		}
-
-		> .thumbnail {
-			color: #fff;
-		}
-	}
-
-	> .label {
-		position: absolute;
-		top: 0;
-		left: 0;
-		pointer-events: none;
-
-		&:before,
-		&:after {
-			content: "";
-			display: block;
-			position: absolute;
-			z-index: 1;
-			background: #0c7ac9;
-		}
-
-		&:before {
-			top: 0;
-			left: 57px;
-			width: 28px;
-			height: 8px;
-		}
-
-		&:after {
-			top: 57px;
-			left: 0;
-			width: 8px;
-			height: 28px;
-		}
-
-		&.red {
-			&:before,
-			&:after {
-				background: #c12113;
-			}
-		}
-
-		> img {
-			position: absolute;
-			z-index: 2;
-			top: 0;
-			left: 0;
-		}
-
-		> p {
-			position: absolute;
-			z-index: 3;
-			top: 19px;
-			left: -28px;
-			width: 120px;
-			margin: 0;
-			text-align: center;
-			line-height: 28px;
-			color: #fff;
-			transform: rotate(-45deg);
-		}
-	}
-
-	> .thumbnail {
-		width: 110px;
-		height: 110px;
-		margin: auto;
-	}
-
-	> .name {
-		display: block;
-		margin: 4px 0 0 0;
-		font-size: 0.8em;
-		text-align: center;
-		word-break: break-all;
-		color: var(--fg);
-		overflow: hidden;
-
-		> .ext {
-			opacity: 0.5;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/drive.folder.vue b/src/client/components/drive.folder.vue
deleted file mode 100644
index 4c09e7775a..0000000000
--- a/src/client/components/drive.folder.vue
+++ /dev/null
@@ -1,326 +0,0 @@
-<template>
-<div class="rghtznwe"
-	:class="{ draghover }"
-	@click="onClick"
-	@contextmenu.stop="onContextmenu"
-	@mouseover="onMouseover"
-	@mouseout="onMouseout"
-	@dragover.prevent.stop="onDragover"
-	@dragenter.prevent="onDragenter"
-	@dragleave="onDragleave"
-	@drop.prevent.stop="onDrop"
-	draggable="true"
-	@dragstart="onDragstart"
-	@dragend="onDragend"
-	:title="title"
->
-	<p class="name">
-		<template v-if="hover"><i class="fas fa-folder-open fa-fw"></i></template>
-		<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
-		{{ folder.name }}
-	</p>
-	<p class="upload" v-if="$store.state.uploadFolder == folder.id">
-		{{ $ts.uploadFolder }}
-	</p>
-	<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		folder: {
-			type: Object,
-			required: true,
-		},
-		isSelected: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		selectMode: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['chosen'],
-
-	data() {
-		return {
-			hover: false,
-			draghover: false,
-			isDragging: false,
-		};
-	},
-
-	computed: {
-		browser(): any {
-			return this.$parent;
-		},
-		title(): string {
-			return this.folder.name;
-		}
-	},
-
-	methods: {
-		checkboxClicked(e) {
-			this.$emit('chosen', this.folder);
-		},
-
-		onClick() {
-			this.browser.move(this.folder);
-		},
-
-		onMouseover() {
-			this.hover = true;
-		},
-
-		onMouseout() {
-			this.hover = false
-		},
-
-		onDragover(e) {
-			// 自分自身がドラッグされている場合
-			if (this.isDragging) {
-				// 自分自身にはドロップさせない
-				e.dataTransfer.dropEffect = 'none';
-				return;
-			}
-
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
-
-			if (isFile || isDriveFile || isDriveFolder) {
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			} else {
-				e.dataTransfer.dropEffect = 'none';
-			}
-		},
-
-		onDragenter() {
-			if (!this.isDragging) this.draghover = true;
-		},
-
-		onDragleave() {
-			this.draghover = false;
-		},
-
-		onDrop(e) {
-			this.draghover = false;
-
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				for (const file of Array.from(e.dataTransfer.files)) {
-					this.browser.upload(file, this.folder);
-				}
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.browser.removeFile(file.id);
-				os.api('drive/files/update', {
-					fileId: file.id,
-					folderId: this.folder.id
-				});
-			}
-			//#endregion
-
-			//#region ドライブのフォルダ
-			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
-			if (driveFolder != null && driveFolder != '') {
-				const folder = JSON.parse(driveFolder);
-
-				// 移動先が自分自身ならreject
-				if (folder.id == this.folder.id) return;
-
-				this.browser.removeFolder(folder.id);
-				os.api('drive/folders/update', {
-					folderId: folder.id,
-					parentId: this.folder.id
-				}).then(() => {
-					// noop
-				}).catch(err => {
-					switch (err) {
-						case 'detected-circular-definition':
-							os.dialog({
-								title: this.$ts.unableToProcess,
-								text: this.$ts.circularReferenceFolder
-							});
-							break;
-						default:
-							os.dialog({
-								type: 'error',
-								text: this.$ts.somethingHappened
-							});
-					}
-				});
-			}
-			//#endregion
-		},
-
-		onDragstart(e) {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
-			this.isDragging = true;
-
-			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
-			// (=あなたの子供が、ドラッグを開始しましたよ)
-			this.browser.isDragSource = true;
-		},
-
-		onDragend() {
-			this.isDragging = false;
-			this.browser.isDragSource = false;
-		},
-
-		go() {
-			this.browser.move(this.folder.id);
-		},
-
-		newWindow() {
-			this.browser.newWindow(this.folder);
-		},
-
-		rename() {
-			os.dialog({
-				title: this.$ts.renameFolder,
-				input: {
-					placeholder: this.$ts.inputNewFolderName,
-					default: this.folder.name
-				}
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				os.api('drive/folders/update', {
-					folderId: this.folder.id,
-					name: name
-				});
-			});
-		},
-
-		deleteFolder() {
-			os.api('drive/folders/delete', {
-				folderId: this.folder.id
-			}).then(() => {
-				if (this.$store.state.uploadFolder === this.folder.id) {
-					this.$store.set('uploadFolder', null);
-				}
-			}).catch(err => {
-				switch(err.id) {
-					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
-						os.dialog({
-							type: 'error',
-							title: this.$ts.unableToDelete,
-							text: this.$ts.hasChildFilesOrFolders
-						});
-						break;
-					default:
-						os.dialog({
-							type: 'error',
-							text: this.$ts.unableToDelete
-						});
-				}
-			});
-		},
-
-		setAsUploadFolder() {
-			this.$store.set('uploadFolder', this.folder.id);
-		},
-
-		onContextmenu(e) {
-			os.contextMenu([{
-				text: this.$ts.openInWindow,
-				icon: 'fas fa-window-restore',
-				action: () => {
-					os.popup(import('./drive-window.vue'), {
-						initialFolder: this.folder
-					}, {
-					}, 'closed');
-				}
-			}, null, {
-				text: this.$ts.rename,
-				icon: 'fas fa-i-cursor',
-				action: this.rename
-			}, null, {
-				text: this.$ts.delete,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: this.deleteFolder
-			}], e);
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rghtznwe {
-	position: relative;
-	padding: 8px;
-	height: 64px;
-	background: var(--driveFolderBg);
-	border-radius: 4px;
-
-	&, * {
-		cursor: pointer;
-	}
-
-	*:not(.checkbox) {
-		pointer-events: none;
-	}
-
-	> .checkbox {
-		position: absolute;
-		bottom: 8px;
-		right: 8px;
-		width: 16px;
-		height: 16px;
-		background: #fff;
-		border: solid 1px #000;
-
-		&.checked {
-			background: var(--accent);
-		}
-	}
-
-	&.draghover {
-		&:after {
-			content: "";
-			pointer-events: none;
-			position: absolute;
-			top: -4px;
-			right: -4px;
-			bottom: -4px;
-			left: -4px;
-			border: 2px dashed var(--focus);
-			border-radius: 4px;
-		}
-	}
-
-	> .name {
-		margin: 0;
-		font-size: 0.9em;
-		color: var(--desktopDriveFolderFg);
-
-		> i {
-			margin-right: 4px;
-			margin-left: 2px;
-			text-align: left;
-		}
-	}
-
-	> .upload {
-		margin: 4px 4px;
-		font-size: 0.8em;
-		text-align: right;
-		color: var(--desktopDriveFolderFg);
-	}
-}
-</style>
diff --git a/src/client/components/drive.nav-folder.vue b/src/client/components/drive.nav-folder.vue
deleted file mode 100644
index 913a1b5f92..0000000000
--- a/src/client/components/drive.nav-folder.vue
+++ /dev/null
@@ -1,135 +0,0 @@
-<template>
-<div class="drylbebk"
-	:class="{ draghover }"
-	@click="onClick"
-	@dragover.prevent.stop="onDragover"
-	@dragenter="onDragenter"
-	@dragleave="onDragleave"
-	@drop.stop="onDrop"
->
-	<i v-if="folder == null" class="fas fa-cloud"></i>
-	<span>{{ folder == null ? $ts.drive : folder.name }}</span>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		folder: {
-			type: Object,
-			required: false,
-		}
-	},
-
-	data() {
-		return {
-			hover: false,
-			draghover: false,
-		};
-	},
-
-	computed: {
-		browser(): any {
-			return this.$parent;
-		}
-	},
-
-	methods: {
-		onClick() {
-			this.browser.move(this.folder);
-		},
-
-		onMouseover() {
-			this.hover = true;
-		},
-
-		onMouseout() {
-			this.hover = false;
-		},
-
-		onDragover(e) {
-			// このフォルダがルートかつカレントディレクトリならドロップ禁止
-			if (this.folder == null && this.browser.folder == null) {
-				e.dataTransfer.dropEffect = 'none';
-			}
-
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
-
-			if (isFile || isDriveFile || isDriveFolder) {
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			} else {
-				e.dataTransfer.dropEffect = 'none';
-			}
-
-			return false;
-		},
-
-		onDragenter() {
-			if (this.folder || this.browser.folder) this.draghover = true;
-		},
-
-		onDragleave() {
-			if (this.folder || this.browser.folder) this.draghover = false;
-		},
-
-		onDrop(e) {
-			this.draghover = false;
-
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				for (const file of Array.from(e.dataTransfer.files)) {
-					this.browser.upload(file, this.folder);
-				}
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.browser.removeFile(file.id);
-				os.api('drive/files/update', {
-					fileId: file.id,
-					folderId: this.folder ? this.folder.id : null
-				});
-			}
-			//#endregion
-
-			//#region ドライブのフォルダ
-			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
-			if (driveFolder != null && driveFolder != '') {
-				const folder = JSON.parse(driveFolder);
-				// 移動先が自分自身ならreject
-				if (this.folder && folder.id == this.folder.id) return;
-				this.browser.removeFolder(folder.id);
-				os.api('drive/folders/update', {
-					folderId: folder.id,
-					parentId: this.folder ? this.folder.id : null
-				});
-			}
-			//#endregion
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.drylbebk {
-	> * {
-		pointer-events: none;
-	}
-
-	&.draghover {
-		background: #eee;
-	}
-
-	> i {
-		margin-right: 4px;
-	}
-}
-</style>
diff --git a/src/client/components/drive.vue b/src/client/components/drive.vue
deleted file mode 100644
index 5dadf9a11f..0000000000
--- a/src/client/components/drive.vue
+++ /dev/null
@@ -1,784 +0,0 @@
-<template>
-<div class="yfudmmck">
-	<nav>
-		<div class="path" @contextmenu.prevent.stop="() => {}">
-			<XNavFolder :class="{ current: folder == null }"/>
-			<template v-for="f in hierarchyFolders">
-				<span class="separator"><i class="fas fa-angle-right"></i></span>
-				<XNavFolder :folder="f"/>
-			</template>
-			<span class="separator" v-if="folder != null"><i class="fas fa-angle-right"></i></span>
-			<span class="folder current" v-if="folder != null">{{ folder.name }}</span>
-		</div>
-		<button @click="showMenu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
-	</nav>
-	<div class="main" :class="{ uploading: uploadings.length > 0, fetching }"
-		ref="main"
-		@dragover.prevent.stop="onDragover"
-		@dragenter="onDragenter"
-		@dragleave="onDragleave"
-		@drop.prevent.stop="onDrop"
-		@contextmenu.stop="onContextmenu"
-	>
-		<div class="contents" ref="contents">
-			<div class="folders" ref="foldersContainer" v-show="folders.length > 0">
-				<XFolder v-for="(f, i) in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder" v-anim="i"/>
-				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
-				<div class="padding" v-for="(n, i) in 16" :key="i"></div>
-				<MkButton ref="moreFolders" v-if="moreFolders">{{ $ts.loadMore }}</MkButton>
-			</div>
-			<div class="files" ref="filesContainer" v-show="files.length > 0">
-				<XFile v-for="(file, i) in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile" v-anim="i"/>
-				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
-				<div class="padding" v-for="(n, i) in 16" :key="i"></div>
-				<MkButton ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $ts.loadMore }}</MkButton>
-			</div>
-			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
-				<p v-if="draghover">{{ $t('empty-draghover') }}</p>
-				<p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p>
-				<p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p>
-			</div>
-		</div>
-		<MkLoading v-if="fetching"/>
-	</div>
-	<div class="dropzone" v-if="draghover"></div>
-	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import XNavFolder from './drive.nav-folder.vue';
-import XFolder from './drive.folder.vue';
-import XFile from './drive.file.vue';
-import MkButton from './ui/button.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XNavFolder,
-		XFolder,
-		XFile,
-		MkButton,
-	},
-
-	props: {
-		initialFolder: {
-			type: Object,
-			required: false
-		},
-		type: {
-			type: String,
-			required: false,
-			default: undefined
-		},
-		multiple: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		select: {
-			type: String,
-			required: false,
-			default: null
-		}
-	},
-
-	emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],
-
-	data() {
-		return {
-			/**
-			 * 現在の階層(フォルダ)
-			 * * null でルートを表す
-			 */
-			folder: null,
-
-			files: [],
-			folders: [],
-			moreFiles: false,
-			moreFolders: false,
-			hierarchyFolders: [],
-			selectedFiles: [],
-			selectedFolders: [],
-			uploadings: os.uploads,
-			connection: null,
-
-			/**
-			 * ドロップされようとしているか
-			 */
-			draghover: false,
-
-			/**
-			 * 自信の所有するアイテムがドラッグをスタートさせたか
-			 * (自分自身の階層にドロップできないようにするためのフラグ)
-			 */
-			isDragSource: false,
-
-			fetching: true,
-
-			ilFilesObserver: new IntersectionObserver(
-				(entries) => entries.some((entry) => entry.isIntersecting)
-				&& !this.fetching && this.moreFiles &&
-					this.fetchMoreFiles()
-			),
-			moreFilesElement: null as Element,
-
-		};
-	},
-
-	watch: {
-		folder() {
-			this.$emit('cd', this.folder);
-		}
-	},
-
-	mounted() {
-		if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
-			this.$nextTick(() => {
-				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
-			});
-		}
-
-		this.connection = markRaw(os.stream.useChannel('drive'));
-
-		this.connection.on('fileCreated', this.onStreamDriveFileCreated);
-		this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
-		this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
-		this.connection.on('folderCreated', this.onStreamDriveFolderCreated);
-		this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated);
-		this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted);
-
-		if (this.initialFolder) {
-			this.move(this.initialFolder);
-		} else {
-			this.fetch();
-		}
-	},
-
-	activated() {
-		if (this.$store.state.enableInfiniteScroll) {
-			this.$nextTick(() => {
-				this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
-			});
-		}
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-		this.ilFilesObserver.disconnect();
-	},
-
-	methods: {
-		onStreamDriveFileCreated(file) {
-			this.addFile(file, true);
-		},
-
-		onStreamDriveFileUpdated(file) {
-			const current = this.folder ? this.folder.id : null;
-			if (current != file.folderId) {
-				this.removeFile(file);
-			} else {
-				this.addFile(file, true);
-			}
-		},
-
-		onStreamDriveFileDeleted(fileId) {
-			this.removeFile(fileId);
-		},
-
-		onStreamDriveFolderCreated(folder) {
-			this.addFolder(folder, true);
-		},
-
-		onStreamDriveFolderUpdated(folder) {
-			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parentId) {
-				this.removeFolder(folder);
-			} else {
-				this.addFolder(folder, true);
-			}
-		},
-
-		onStreamDriveFolderDeleted(folderId) {
-			this.removeFolder(folderId);
-		},
-
-		onDragover(e): any {
-			// ドラッグ元が自分自身の所有するアイテムだったら
-			if (this.isDragSource) {
-				// 自分自身にはドロップさせない
-				e.dataTransfer.dropEffect = 'none';
-				return;
-			}
-
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
-
-			if (isFile || isDriveFile || isDriveFolder) {
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			} else {
-				e.dataTransfer.dropEffect = 'none';
-			}
-
-			return false;
-		},
-
-		onDragenter(e) {
-			if (!this.isDragSource) this.draghover = true;
-		},
-
-		onDragleave(e) {
-			this.draghover = false;
-		},
-
-		onDrop(e): any {
-			this.draghover = false;
-
-			// ドロップされてきたものがファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				for (const file of Array.from(e.dataTransfer.files)) {
-					this.upload(file, this.folder);
-				}
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				if (this.files.some(f => f.id == file.id)) return;
-				this.removeFile(file.id);
-				os.api('drive/files/update', {
-					fileId: file.id,
-					folderId: this.folder ? this.folder.id : null
-				});
-			}
-			//#endregion
-
-			//#region ドライブのフォルダ
-			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
-			if (driveFolder != null && driveFolder != '') {
-				const folder = JSON.parse(driveFolder);
-
-				// 移動先が自分自身ならreject
-				if (this.folder && folder.id == this.folder.id) return false;
-				if (this.folders.some(f => f.id == folder.id)) return false;
-				this.removeFolder(folder.id);
-				os.api('drive/folders/update', {
-					folderId: folder.id,
-					parentId: this.folder ? this.folder.id : null
-				}).then(() => {
-					// noop
-				}).catch(err => {
-					switch (err) {
-						case 'detected-circular-definition':
-							os.dialog({
-								title: this.$ts.unableToProcess,
-								text: this.$ts.circularReferenceFolder
-							});
-							break;
-						default:
-							os.dialog({
-								type: 'error',
-								text: this.$ts.somethingHappened
-							});
-					}
-				});
-			}
-			//#endregion
-		},
-
-		selectLocalFile() {
-			(this.$refs.fileInput as any).click();
-		},
-
-		urlUpload() {
-			os.dialog({
-				title: this.$ts.uploadFromUrl,
-				input: {
-					placeholder: this.$ts.uploadFromUrlDescription
-				}
-			}).then(({ canceled, result: url }) => {
-				if (canceled) return;
-				os.api('drive/files/upload-from-url', {
-					url: url,
-					folderId: this.folder ? this.folder.id : undefined
-				});
-
-				os.dialog({
-					title: this.$ts.uploadFromUrlRequested,
-					text: this.$ts.uploadFromUrlMayTakeTime
-				});
-			});
-		},
-
-		createFolder() {
-			os.dialog({
-				title: this.$ts.createFolder,
-				input: {
-					placeholder: this.$ts.folderName
-				}
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				os.api('drive/folders/create', {
-					name: name,
-					parentId: this.folder ? this.folder.id : undefined
-				}).then(folder => {
-					this.addFolder(folder, true);
-				});
-			});
-		},
-
-		renameFolder(folder) {
-			os.dialog({
-				title: this.$ts.renameFolder,
-				input: {
-					placeholder: this.$ts.inputNewFolderName,
-					default: folder.name
-				}
-			}).then(({ canceled, result: name }) => {
-				if (canceled) return;
-				os.api('drive/folders/update', {
-					folderId: folder.id,
-					name: name
-				}).then(folder => {
-					// FIXME: 画面を更新するために自分自身に移動
-					this.move(folder);
-				});
-			});
-		},
-
-		deleteFolder(folder) {
-			os.api('drive/folders/delete', {
-				folderId: folder.id
-			}).then(() => {
-				// 削除時に親フォルダに移動
-				this.move(folder.parentId);
-			}).catch(err => {
-				switch(err.id) {
-					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
-						os.dialog({
-							type: 'error',
-							title: this.$ts.unableToDelete,
-							text: this.$ts.hasChildFilesOrFolders
-						});
-						break;
-					default:
-						os.dialog({
-							type: 'error',
-							text: this.$ts.unableToDelete
-						});
-					}
-			});
-		},
-
-		onChangeFileInput() {
-			for (const file of Array.from((this.$refs.fileInput as any).files)) {
-				this.upload(file, this.folder);
-			}
-		},
-
-		upload(file, folder) {
-			if (folder && typeof folder == 'object') folder = folder.id;
-			os.upload(file, folder).then(res => {
-				this.addFile(res, true);
-			});
-		},
-
-		chooseFile(file) {
-			const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
-			if (this.multiple) {
-				if (isAlreadySelected) {
-					this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
-				} else {
-					this.selectedFiles.push(file);
-				}
-				this.$emit('change-selection', this.selectedFiles);
-			} else {
-				if (isAlreadySelected) {
-					this.$emit('selected', file);
-				} else {
-					this.selectedFiles = [file];
-					this.$emit('change-selection', [file]);
-				}
-			}
-		},
-
-		chooseFolder(folder) {
-			const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id);
-			if (this.multiple) {
-				if (isAlreadySelected) {
-					this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id);
-				} else {
-					this.selectedFolders.push(folder);
-				}
-				this.$emit('change-selection', this.selectedFolders);
-			} else {
-				if (isAlreadySelected) {
-					this.$emit('selected', folder);
-				} else {
-					this.selectedFolders = [folder];
-					this.$emit('change-selection', [folder]);
-				}
-			}
-		},
-
-		move(target) {
-			if (target == null) {
-				this.goRoot();
-				return;
-			} else if (typeof target == 'object') {
-				target = target.id;
-			}
-
-			this.fetching = true;
-
-			os.api('drive/folders/show', {
-				folderId: target
-			}).then(folder => {
-				this.folder = folder;
-				this.hierarchyFolders = [];
-
-				const dive = folder => {
-					this.hierarchyFolders.unshift(folder);
-					if (folder.parent) dive(folder.parent);
-				};
-
-				if (folder.parent) dive(folder.parent);
-
-				this.$emit('open-folder', folder);
-				this.fetch();
-			});
-		},
-
-		addFolder(folder, unshift = false) {
-			const current = this.folder ? this.folder.id : null;
-			if (current != folder.parentId) return;
-
-			if (this.folders.some(f => f.id == folder.id)) {
-				const exist = this.folders.map(f => f.id).indexOf(folder.id);
-				this.folders[exist] = folder;
-				return;
-			}
-
-			if (unshift) {
-				this.folders.unshift(folder);
-			} else {
-				this.folders.push(folder);
-			}
-		},
-
-		addFile(file, unshift = false) {
-			const current = this.folder ? this.folder.id : null;
-			if (current != file.folderId) return;
-
-			if (this.files.some(f => f.id == file.id)) {
-				const exist = this.files.map(f => f.id).indexOf(file.id);
-				this.files[exist] = file;
-				return;
-			}
-
-			if (unshift) {
-				this.files.unshift(file);
-			} else {
-				this.files.push(file);
-			}
-		},
-
-		removeFolder(folder) {
-			if (typeof folder == 'object') folder = folder.id;
-			this.folders = this.folders.filter(f => f.id != folder);
-		},
-
-		removeFile(file) {
-			if (typeof file == 'object') file = file.id;
-			this.files = this.files.filter(f => f.id != file);
-		},
-
-		appendFile(file) {
-			this.addFile(file);
-		},
-
-		appendFolder(folder) {
-			this.addFolder(folder);
-		},
-
-		prependFile(file) {
-			this.addFile(file, true);
-		},
-
-		prependFolder(folder) {
-			this.addFolder(folder, true);
-		},
-
-		goRoot() {
-			// 既にrootにいるなら何もしない
-			if (this.folder == null) return;
-
-			this.folder = null;
-			this.hierarchyFolders = [];
-			this.$emit('move-root');
-			this.fetch();
-		},
-
-		fetch() {
-			this.folders = [];
-			this.files = [];
-			this.moreFolders = false;
-			this.moreFiles = false;
-			this.fetching = true;
-
-			let fetchedFolders = null;
-			let fetchedFiles = null;
-
-			const foldersMax = 30;
-			const filesMax = 30;
-
-			// フォルダ一覧取得
-			os.api('drive/folders', {
-				folderId: this.folder ? this.folder.id : null,
-				limit: foldersMax + 1
-			}).then(folders => {
-				if (folders.length == foldersMax + 1) {
-					this.moreFolders = true;
-					folders.pop();
-				}
-				fetchedFolders = folders;
-				complete();
-			});
-
-			// ファイル一覧取得
-			os.api('drive/files', {
-				folderId: this.folder ? this.folder.id : null,
-				type: this.type,
-				limit: filesMax + 1
-			}).then(files => {
-				if (files.length == filesMax + 1) {
-					this.moreFiles = true;
-					files.pop();
-				}
-				fetchedFiles = files;
-				complete();
-			});
-
-			let flag = false;
-			const complete = () => {
-				if (flag) {
-					for (const x of fetchedFolders) this.appendFolder(x);
-					for (const x of fetchedFiles) this.appendFile(x);
-					this.fetching = false;
-				} else {
-					flag = true;
-				}
-			};
-		},
-
-		fetchMoreFiles() {
-			this.fetching = true;
-
-			const max = 30;
-
-			// ファイル一覧取得
-			os.api('drive/files', {
-				folderId: this.folder ? this.folder.id : null,
-				type: this.type,
-				untilId: this.files[this.files.length - 1].id,
-				limit: max + 1
-			}).then(files => {
-				if (files.length == max + 1) {
-					this.moreFiles = true;
-					files.pop();
-				} else {
-					this.moreFiles = false;
-				}
-				for (const x of files) this.appendFile(x);
-				this.fetching = false;
-			});
-		},
-
-		getMenu() {
-			return [{
-				text: this.$ts.addFile,
-				type: 'label'
-			}, {
-				text: this.$ts.upload,
-				icon: 'fas fa-upload',
-				action: () => { this.selectLocalFile(); }
-			}, {
-				text: this.$ts.fromUrl,
-				icon: 'fas fa-link',
-				action: () => { this.urlUpload(); }
-			}, null, {
-				text: this.folder ? this.folder.name : this.$ts.drive,
-				type: 'label'
-			}, this.folder ? {
-				text: this.$ts.renameFolder,
-				icon: 'fas fa-i-cursor',
-				action: () => { this.renameFolder(this.folder); }
-			} : undefined, this.folder ? {
-				text: this.$ts.deleteFolder,
-				icon: 'fas fa-trash-alt',
-				action: () => { this.deleteFolder(this.folder); }
-			} : undefined, {
-				text: this.$ts.createFolder,
-				icon: 'fas fa-folder-plus',
-				action: () => { this.createFolder(); }
-			}];
-		},
-
-		showMenu(ev) {
-			os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
-		},
-
-		onContextmenu(ev) {
-			os.contextMenu(this.getMenu(), ev);
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.yfudmmck {
-	display: flex;
-	flex-direction: column;
-	height: 100%;
-
-	> nav {
-		display: flex;
-		z-index: 2;
-		width: 100%;
-		padding: 0 8px;
-		box-sizing: border-box;
-		overflow: auto;
-		font-size: 0.9em;
-		box-shadow: 0 1px 0 var(--divider);
-
-		&, * {
-			user-select: none;
-		}
-
-		> .path {
-			display: inline-block;
-			vertical-align: bottom;
-			line-height: 38px;
-			white-space: nowrap;
-
-			> * {
-				display: inline-block;
-				margin: 0;
-				padding: 0 8px;
-				line-height: 38px;
-				cursor: pointer;
-
-				* {
-					pointer-events: none;
-				}
-
-				&:hover {
-					text-decoration: underline;
-				}
-
-				&.current {
-					font-weight: bold;
-					cursor: default;
-
-					&:hover {
-						text-decoration: none;
-					}
-				}
-
-				&.separator {
-					margin: 0;
-					padding: 0;
-					opacity: 0.5;
-					cursor: default;
-
-					> i {
-						margin: 0;
-					}
-				}
-			}
-		}
-
-		> .menu {
-			margin-left: auto;
-		}
-	}
-
-	> .main {
-		flex: 1;
-		overflow: auto;
-		padding: var(--margin);
-
-		&, * {
-			user-select: none;
-		}
-
-		&.fetching {
-			cursor: wait !important;
-
-			* {
-				pointer-events: none;
-			}
-
-			> .contents {
-				opacity: 0.5;
-			}
-		}
-
-		&.uploading {
-			height: calc(100% - 38px - 100px);
-		}
-
-		> .contents {
-
-			> .folders,
-			> .files {
-				display: flex;
-				flex-wrap: wrap;
-
-				> .folder,
-				> .file {
-					flex-grow: 1;
-					width: 128px;
-					margin: 4px;
-					box-sizing: border-box;
-				}
-
-				> .padding {
-					flex-grow: 1;
-					pointer-events: none;
-					width: 128px + 8px;
-				}
-			}
-
-			> .empty {
-				padding: 16px;
-				text-align: center;
-				pointer-events: none;
-				opacity: 0.5;
-
-				> p {
-					margin: 0;
-				}
-			}
-		}
-	}
-
-	> .dropzone {
-		position: absolute;
-		left: 0;
-		top: 38px;
-		width: 100%;
-		height: calc(100% - 38px);
-		border: dashed 2px var(--focus);
-		pointer-events: none;
-	}
-
-	> input {
-		display: none;
-	}
-}
-</style>
diff --git a/src/client/components/emoji-picker-dialog.vue b/src/client/components/emoji-picker-dialog.vue
deleted file mode 100644
index aa17b8b250..0000000000
--- a/src/client/components/emoji-picker-dialog.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<MkPopup ref="popup" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.popup.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')" #default="{point}">
-	<MkEmojiPicker class="ryghynhb _popup _shadow" :class="{ pointer: point === 'top' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker"/>
-</MkPopup>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import MkPopup from '@client/components/ui/popup.vue';
-import MkEmojiPicker from '@client/components/emoji-picker.vue';
-
-export default defineComponent({
-	components: {
-		MkPopup,
-		MkEmojiPicker,
-	},
-
-	props: {
-		manualShowing: {
-			type: Boolean,
-			required: false,
-			default: null,
-		},
-		src: {
-			required: false
-		},
-		showPinned: {
-			required: false,
-			default: true
-		},
-		asReactionPicker: {
-			required: false
-		},
-	},
-
-	emits: ['done', 'close', 'closed'],
-
-	data() {
-		return {
-
-		};
-	},
-
-	methods: {
-		chosen(emoji: any) {
-			this.$emit('done', emoji);
-			this.$refs.popup.close();
-		},
-
-		opening() {
-			this.$refs.picker.reset();
-			this.$refs.picker.focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ryghynhb {
-	&.pointer {
-		&:before {
-			--size: 8px;
-			content: '';
-			display: block;
-			position: absolute;
-			top: calc(0px - (var(--size) * 2));
-			left: 0;
-			right: 0;
-			width: 0;
-			margin: auto;
-			border: solid var(--size) transparent;
-			border-bottom-color: var(--popup);
-		}
-	}
-}
-</style>
diff --git a/src/client/components/emoji-picker-window.vue b/src/client/components/emoji-picker-window.vue
deleted file mode 100644
index b7b884565b..0000000000
--- a/src/client/components/emoji-picker-window.vue
+++ /dev/null
@@ -1,197 +0,0 @@
-<template>
-<MkWindow ref="window"
-	:initial-width="null"
-	:initial-height="null"
-	:can-resize="false"
-	:mini="true"
-	:front="true"
-	@closed="$emit('closed')"
->
-	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
-</MkWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import MkWindow from '@client/components/ui/window.vue';
-import MkEmojiPicker from '@client/components/emoji-picker.vue';
-
-export default defineComponent({
-	components: {
-		MkWindow,
-		MkEmojiPicker,
-	},
-
-	props: {
-		src: {
-			required: false
-		},
-		showPinned: {
-			required: false,
-			default: true
-		},
-		asReactionPicker: {
-			required: false
-		},
-	},
-
-	emits: ['chosen', 'closed'],
-
-	data() {
-		return {
-
-		};
-	},
-
-	methods: {
-		chosen(emoji: any) {
-			this.$emit('chosen', emoji);
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.omfetrab {
-	$pad: 8px;
-	--eachSize: 40px;
-
-	display: flex;
-	flex-direction: column;
-	contain: content;
-
-	&.big {
-		--eachSize: 44px;
-	}
-
-	&.w1 {
-		width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
-	}
-
-	&.w2 {
-		width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
-	}
-
-	&.w3 {
-		width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
-	}
-
-	&.h1 {
-		--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
-	}
-
-	&.h2 {
-		--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
-	}
-
-	&.h3 {
-		--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
-	}
-
-	> .search {
-		width: 100%;
-		padding: 12px;
-		box-sizing: border-box;
-		font-size: 1em;
-		outline: none;
-		border: none;
-		background: transparent;
-		color: var(--fg);
-
-		&:not(.filled) {
-			order: 1;
-			z-index: 2;
-			box-shadow: 0px -1px 0 0px var(--divider);
-		}
-	}
-
-	> .emojis {
-		height: var(--height);
-		overflow-y: auto;
-		overflow-x: hidden;
-
-		scrollbar-width: none;
-
-		&::-webkit-scrollbar {
-			display: none;
-		}
-
-		> .index {
-			min-height: var(--height);
-			position: relative;
-			border-bottom: solid 0.5px var(--divider);
-				
-			> .arrow {
-				position: absolute;
-				bottom: 0;
-				left: 0;
-				width: 100%;
-				padding: 16px 0;
-				text-align: center;
-				opacity: 0.5;
-				pointer-events: none;
-			}
-		}
-
-		section {
-			> header {
-				position: sticky;
-				top: 0;
-				left: 0;
-				z-index: 1;
-				padding: 8px;
-				font-size: 12px;
-			}
-
-			> div {
-				padding: $pad;
-
-				> button {
-					position: relative;
-					padding: 0;
-					width: var(--eachSize);
-					height: var(--eachSize);
-					border-radius: 4px;
-
-					&:focus-visible {
-						outline: solid 2px var(--focus);
-						z-index: 1;
-					}
-
-					&:hover {
-						background: rgba(0, 0, 0, 0.05);
-					}
-
-					&:active {
-						background: var(--accent);
-						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
-					}
-
-					> * {
-						font-size: 24px;
-						height: 1.25em;
-						vertical-align: -.25em;
-						pointer-events: none;
-					}
-				}
-			}
-
-			&.result {
-				border-bottom: solid 0.5px var(--divider);
-
-				&:empty {
-					display: none;
-				}
-			}
-
-			&.unicode {
-				min-height: 384px;
-			}
-
-			&.custom {
-				min-height: 64px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/emoji-picker.section.vue b/src/client/components/emoji-picker.section.vue
deleted file mode 100644
index 0ea3761429..0000000000
--- a/src/client/components/emoji-picker.section.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<section>
-	<header class="_acrylic" @click="shown = !shown">
-		<i class="toggle fa-fw" :class="shown ? 'fas fa-chevron-down' : 'fas fa-chevron-up'"></i> <slot></slot> ({{ emojis.length }})
-	</header>
-	<div v-if="shown">
-		<button v-for="emoji in emojis"
-			class="_button"
-			@click="chosen(emoji, $event)"
-			:key="emoji"
-		>
-			<MkEmoji :emoji="emoji" :normal="true"/>
-		</button>
-	</div>
-</section>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { getStaticImageUrl } from '@client/scripts/get-static-image-url';
-
-export default defineComponent({
-	props: {
-		emojis: {
-			required: true,
-		},
-		initialShown: {
-			required: false
-		}
-	},
-
-	emits: ['chosen'],
-
-	data() {
-		return {
-			getStaticImageUrl,
-			shown: this.initialShown,
-		};
-	},
-
-	methods: {
-		chosen(emoji: any, ev) {
-			this.$parent.chosen(emoji, ev);
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
deleted file mode 100644
index 85a12a08e6..0000000000
--- a/src/client/components/emoji-picker.vue
+++ /dev/null
@@ -1,501 +0,0 @@
-<template>
-<div class="omfetrab" :class="['w' + width, 'h' + height, { big }]">
-	<input ref="search" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" v-model.trim="q" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
-	<div class="emojis" ref="emojis">
-		<section class="result">
-			<div v-if="searchResultCustom.length > 0">
-				<button v-for="emoji in searchResultCustom"
-					class="_button"
-					:title="emoji.name"
-					@click="chosen(emoji, $event)"
-					:key="emoji"
-					tabindex="0"
-				>
-					<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
-					<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
-				</button>
-			</div>
-			<div v-if="searchResultUnicode.length > 0">
-				<button v-for="emoji in searchResultUnicode"
-					class="_button"
-					:title="emoji.name"
-					@click="chosen(emoji, $event)"
-					:key="emoji.name"
-					tabindex="0"
-				>
-					<MkEmoji :emoji="emoji.char"/>
-				</button>
-			</div>
-		</section>
-
-		<div class="index" v-if="tab === 'index'">
-			<section v-if="showPinned">
-				<div>
-					<button v-for="emoji in pinned"
-						class="_button"
-						@click="chosen(emoji, $event)"
-						tabindex="0"
-						:key="emoji"
-					>
-						<MkEmoji :emoji="emoji" :normal="true"/>
-					</button>
-				</div>
-			</section>
-
-			<section>
-				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header>
-				<div>
-					<button v-for="emoji in $store.state.recentlyUsedEmojis"
-						class="_button"
-						@click="chosen(emoji, $event)"
-						:key="emoji"
-					>
-						<MkEmoji :emoji="emoji" :normal="true"/>
-					</button>
-				</div>
-			</section>
-		</div>
-		<div>
-			<header class="_acrylic">{{ $ts.customEmojis }}</header>
-			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
-		</div>
-		<div>
-			<header class="_acrylic">{{ $ts.emoji }}</header>
-			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
-		</div>
-	</div>
-	<div class="tabs">
-		<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="fas fa-asterisk fa-fw"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="fas fa-laugh fa-fw"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="fas fa-leaf fa-fw"></i></button>
-		<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="fas fa-hashtag fa-fw"></i></button>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { emojilist } from '@/misc/emojilist';
-import { getStaticImageUrl } from '@client/scripts/get-static-image-url';
-import Particle from '@client/components/particle.vue';
-import * as os from '@client/os';
-import { isDeviceTouch } from '@client/scripts/is-device-touch';
-import { isMobile } from '@client/scripts/is-mobile';
-import { emojiCategories } from '@client/instance';
-import XSection from './emoji-picker.section.vue';
-
-export default defineComponent({
-	components: {
-		XSection
-	},
-
-	props: {
-		showPinned: {
-			required: false,
-			default: true
-		},
-		asReactionPicker: {
-			required: false
-		},
-	},
-
-	emits: ['chosen'],
-
-	data() {
-		return {
-			emojilist: markRaw(emojilist),
-			getStaticImageUrl,
-			pinned: this.$store.reactiveState.reactions,
-			width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
-			height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
-			big: this.asReactionPicker ? isDeviceTouch : false,
-			customEmojiCategories: emojiCategories,
-			customEmojis: this.$instance.emojis,
-			q: null,
-			searchResultCustom: [],
-			searchResultUnicode: [],
-			tab: 'index',
-			categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
-		};
-	},
-
-	watch: {
-		q() {
-			this.$refs.emojis.scrollTop = 0;
-
-			if (this.q == null || this.q === '') {
-				this.searchResultCustom = [];
-				this.searchResultUnicode = [];
-				return;
-			}
-
-			const q = this.q.replace(/:/g, '');
-
-			const searchCustom = () => {
-				const max = 8;
-				const emojis = this.customEmojis;
-				const matches = new Set();
-
-				const exactMatch = emojis.find(e => e.name === q);
-				if (exactMatch) matches.add(exactMatch);
-
-				if (q.includes(' ')) { // AND検索
-					const keywords = q.split(' ');
-
-					// 名前にキーワードが含まれている
-					for (const emoji of emojis) {
-						if (keywords.every(keyword => emoji.name.includes(keyword))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					// 名前またはエイリアスにキーワードが含まれている
-					for (const emoji of emojis) {
-						if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-				} else {
-					for (const emoji of emojis) {
-						if (emoji.name.startsWith(q)) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.aliases.some(alias => alias.startsWith(q))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.name.includes(q)) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.aliases.some(alias => alias.includes(q))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-				}
-
-				return matches;
-			};
-
-			const searchUnicode = () => {
-				const max = 8;
-				const emojis = this.emojilist;
-				const matches = new Set();
-
-				const exactMatch = emojis.find(e => e.name === q);
-				if (exactMatch) matches.add(exactMatch);
-
-				if (q.includes(' ')) { // AND検索
-					const keywords = q.split(' ');
-
-					// 名前にキーワードが含まれている
-					for (const emoji of emojis) {
-						if (keywords.every(keyword => emoji.name.includes(keyword))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					// 名前またはエイリアスにキーワードが含まれている
-					for (const emoji of emojis) {
-						if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-				} else {
-					for (const emoji of emojis) {
-						if (emoji.name.startsWith(q)) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.name.includes(q)) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-					if (matches.size >= max) return matches;
-
-					for (const emoji of emojis) {
-						if (emoji.keywords.some(keyword => keyword.includes(q))) {
-							matches.add(emoji);
-							if (matches.size >= max) break;
-						}
-					}
-				}
-
-				return matches;
-			};
-
-			this.searchResultCustom = Array.from(searchCustom());
-			this.searchResultUnicode = Array.from(searchUnicode());
-		}
-	},
-
-	mounted() {
-		this.focus();
-	},
-
-	methods: {
-		focus() {
-			if (!isMobile && !isDeviceTouch) {
-				this.$refs.search.focus({
-					preventScroll: true
-				});
-			}
-		},
-
-		reset() {
-			this.$refs.emojis.scrollTop = 0;
-			this.q = '';
-		},
-
-		getKey(emoji: any) {
-			return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
-		},
-
-		chosen(emoji: any, ev) {
-			if (ev) {
-				const el = ev.currentTarget || ev.target;
-				const rect = el.getBoundingClientRect();
-				const x = rect.left + (el.clientWidth / 2);
-				const y = rect.top + (el.clientHeight / 2);
-				os.popup(Particle, { x, y }, {}, 'end');
-			}
-
-			const key = this.getKey(emoji);
-			this.$emit('chosen', key);
-
-			// 最近使った絵文字更新
-			if (!this.pinned.includes(key)) {
-				let recents = this.$store.state.recentlyUsedEmojis;
-				recents = recents.filter((e: any) => e !== key);
-				recents.unshift(key);
-				this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
-			}
-		},
-
-		paste(event) {
-			const paste = (event.clipboardData || window.clipboardData).getData('text');
-			if (this.done(paste)) {
-				event.preventDefault();
-			}
-		},
-
-		done(query) {
-			if (query == null) query = this.q;
-			if (query == null) return;
-			const q = query.replace(/:/g, '');
-			const exactMatchCustom = this.customEmojis.find(e => e.name === q);
-			if (exactMatchCustom) {
-				this.chosen(exactMatchCustom);
-				return true;
-			}
-			const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q);
-			if (exactMatchUnicode) {
-				this.chosen(exactMatchUnicode);
-				return true;
-			}
-			if (this.searchResultCustom.length > 0) {
-				this.chosen(this.searchResultCustom[0]);
-				return true;
-			}
-			if (this.searchResultUnicode.length > 0) {
-				this.chosen(this.searchResultUnicode[0]);
-				return true;
-			}
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.omfetrab {
-	$pad: 8px;
-	--eachSize: 40px;
-
-	display: flex;
-	flex-direction: column;
-
-	&.big {
-		--eachSize: 44px;
-	}
-
-	&.w1 {
-		width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
-	}
-
-	&.w2 {
-		width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
-	}
-
-	&.w3 {
-		width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
-	}
-
-	&.h1 {
-		--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
-	}
-
-	&.h2 {
-		--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
-	}
-
-	&.h3 {
-		--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
-	}
-
-	> .search {
-		width: 100%;
-		padding: 12px;
-		box-sizing: border-box;
-		font-size: 1em;
-		outline: none;
-		border: none;
-		background: transparent;
-		color: var(--fg);
-
-		&:not(.filled) {
-			order: 1;
-			z-index: 2;
-			box-shadow: 0px -1px 0 0px var(--divider);
-		}
-	}
-
-	> .tabs {
-		display: flex;
-		display: none;
-
-		> .tab {
-			flex: 1;
-			height: 38px;
-			border-top: solid 0.5px var(--divider);
-
-			&.active {
-				border-top: solid 1px var(--accent);
-				color: var(--accent);
-			}
-		}
-	}
-
-	> .emojis {
-		height: var(--height);
-		overflow-y: auto;
-		overflow-x: hidden;
-
-		scrollbar-width: none;
-
-		&::-webkit-scrollbar {
-			display: none;
-		}
-
-		> div {
-			&:not(.index) {
-				padding: 4px 0 8px 0;
-				border-top: solid 0.5px var(--divider);
-			}
-
-			> header {
-				/*position: sticky;
-				top: 0;
-				left: 0;*/
-				height: 32px;
-				line-height: 32px;
-				z-index: 2;
-				padding: 0 8px;
-				font-size: 12px;
-			}
-		}
-
-		::v-deep(section) {
-			> header {
-				position: sticky;
-				top: 0;
-				left: 0;
-				height: 32px;
-				line-height: 32px;
-				z-index: 1;
-				padding: 0 8px;
-				font-size: 12px;
-				cursor: pointer;
-
-				&:hover {
-					color: var(--accent);
-				}
-			}
-
-			> div {
-				position: relative;
-				padding: $pad;
-
-				> button {
-					position: relative;
-					padding: 0;
-					width: var(--eachSize);
-					height: var(--eachSize);
-					border-radius: 4px;
-
-					&:focus-visible {
-						outline: solid 2px var(--focus);
-						z-index: 1;
-					}
-
-					&:hover {
-						background: rgba(0, 0, 0, 0.05);
-					}
-
-					&:active {
-						background: var(--accent);
-						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
-					}
-
-					> * {
-						font-size: 24px;
-						height: 1.25em;
-						vertical-align: -.25em;
-						pointer-events: none;
-					}
-				}
-			}
-
-			&.result {
-				border-bottom: solid 0.5px var(--divider);
-
-				&:empty {
-					display: none;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/featured-photos.vue b/src/client/components/featured-photos.vue
deleted file mode 100644
index c992a5d1fb..0000000000
--- a/src/client/components/featured-photos.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<template>
-<div class="xfbouadm" v-if="meta" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-	},
-
-	data() {
-		return {
-			meta: null,
-		};
-	},
-
-	created() {
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.xfbouadm {
-	background-position: center;
-	background-size: cover;
-}
-</style>
diff --git a/src/client/components/file-type-icon.vue b/src/client/components/file-type-icon.vue
deleted file mode 100644
index 95200b98c2..0000000000
--- a/src/client/components/file-type-icon.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<template>
-<span class="mk-file-type-icon">
-	<template v-if="kind == 'image'"><i class="fas fa-file-image"></i></template>
-</span>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		type: {
-			type: String,
-			required: true,
-		}
-	},
-	data() {
-		return {
-		};
-	},
-	computed: {
-		kind(): string {
-			return this.type.split('/')[0];
-		}
-	}
-});
-</script>
diff --git a/src/client/components/follow-button.vue b/src/client/components/follow-button.vue
deleted file mode 100644
index 5eba9b1f6b..0000000000
--- a/src/client/components/follow-button.vue
+++ /dev/null
@@ -1,210 +0,0 @@
-<template>
-<button class="kpoogebi _button"
-	:class="{ wait, active: isFollowing || hasPendingFollowRequestFromYou, full, large }"
-	@click="onClick"
-	:disabled="wait"
->
-	<template v-if="!wait">
-		<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
-			<span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
-		</template>
-		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
-			<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
-		</template>
-		<template v-else-if="isFollowing">
-			<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
-		</template>
-		<template v-else-if="!isFollowing && user.isLocked">
-			<span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i>
-		</template>
-		<template v-else-if="!isFollowing && !user.isLocked">
-			<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
-		</template>
-	</template>
-	<template v-else>
-		<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
-	</template>
-</button>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		full: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		large: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-
-	data() {
-		return {
-			isFollowing: this.user.isFollowing,
-			hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
-			wait: false,
-			connection: null,
-		};
-	},
-
-	created() {
-		// 渡されたユーザー情報が不完全な場合
-		if (this.user.isFollowing == null) {
-			os.api('users/show', {
-				userId: this.user.id
-			}).then(u => {
-				this.isFollowing = u.isFollowing;
-				this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou;
-			});
-		}
-	},
-
-	mounted() {
-		this.connection = markRaw(os.stream.useChannel('main'));
-
-		this.connection.on('follow', this.onFollowChange);
-		this.connection.on('unfollow', this.onFollowChange);
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onFollowChange(user) {
-			if (user.id == this.user.id) {
-				this.isFollowing = user.isFollowing;
-				this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
-			}
-		},
-
-		async onClick() {
-			this.wait = true;
-
-			try {
-				if (this.isFollowing) {
-					const { canceled } = await os.dialog({
-						type: 'warning',
-						text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
-						showCancelButton: true
-					});
-
-					if (canceled) return;
-
-					await os.api('following/delete', {
-						userId: this.user.id
-					});
-				} else {
-					if (this.hasPendingFollowRequestFromYou) {
-						await os.api('following/requests/cancel', {
-							userId: this.user.id
-						});
-					} else if (this.user.isLocked) {
-						await os.api('following/create', {
-							userId: this.user.id
-						});
-						this.hasPendingFollowRequestFromYou = true;
-					} else {
-						await os.api('following/create', {
-							userId: this.user.id
-						});
-						this.hasPendingFollowRequestFromYou = true;
-					}
-				}
-			} catch (e) {
-				console.error(e);
-			} finally {
-				this.wait = false;
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.kpoogebi {
-	position: relative;
-	display: inline-block;
-	font-weight: bold;
-	color: var(--accent);
-	background: transparent;
-	border: solid 1px var(--accent);
-	padding: 0;
-	height: 31px;
-	font-size: 16px;
-	border-radius: 32px;
-	background: #fff;
-
-	&.full {
-		padding: 0 8px 0 12px;
-		font-size: 14px;
-	}
-
-	&.large {
-		font-size: 16px;
-		height: 38px;
-		padding: 0 12px 0 16px;
-	}
-
-	&:not(.full) {
-		width: 31px;
-	}
-
-	&:focus-visible {
-		&:after {
-			content: "";
-			pointer-events: none;
-			position: absolute;
-			top: -5px;
-			right: -5px;
-			bottom: -5px;
-			left: -5px;
-			border: 2px solid var(--focus);
-			border-radius: 32px;
-		}
-	}
-
-	&:hover {
-		//background: mix($primary, #fff, 20);
-	}
-
-	&:active {
-		//background: mix($primary, #fff, 40);
-	}
-
-	&.active {
-		color: #fff;
-		background: var(--accent);
-
-		&:hover {
-			background: var(--accentLighten);
-			border-color: var(--accentLighten);
-		}
-
-		&:active {
-			background: var(--accentDarken);
-			border-color: var(--accentDarken);
-		}
-	}
-
-	&.wait {
-		cursor: wait !important;
-		opacity: 0.7;
-	}
-
-	> span {
-		margin-right: 6px;
-	}
-}
-</style>
diff --git a/src/client/components/forgot-password.vue b/src/client/components/forgot-password.vue
deleted file mode 100644
index 7fcf9aa720..0000000000
--- a/src/client/components/forgot-password.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="370"
-	:height="400"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
->
-	<template #header>{{ $ts.forgotPassword }}</template>
-
-	<form class="bafeceda" @submit.prevent="onSubmit" v-if="$instance.enableEmail">
-		<div class="main _formRoot">
-			<MkInput class="_formBlock" v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
-				<template #label>{{ $ts.username }}</template>
-				<template #prefix>@</template>
-			</MkInput>
-
-			<MkInput class="_formBlock" v-model="email" type="email" spellcheck="false" required>
-				<template #label>{{ $ts.emailAddress }}</template>
-				<template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
-			</MkInput>
-
-			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
-		</div>
-		<div class="sub">
-			<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
-		</div>
-	</form>
-	<div v-else>
-		{{ $ts._forgotPassword.contactAdmin }}
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XModalWindow,
-		MkButton,
-		MkInput,
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			username: '',
-			email: '',
-			processing: false,
-		};
-	},
-
-	methods: {
-		async onSubmit() {
-			this.processing = true;
-			await os.apiWithDialog('request-reset-password', {
-				username: this.username,
-				email: this.email,
-			});
-
-			this.$emit('done');
-			this.$refs.dialog.close();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.bafeceda {
-	> .main {
-		padding: 24px;
-	}
-
-	> .sub {
-		border-top: solid 0.5px var(--divider);
-		padding: 24px;
-	}
-}
-</style>
diff --git a/src/client/components/form-dialog.vue b/src/client/components/form-dialog.vue
deleted file mode 100644
index 6353b7287e..0000000000
--- a/src/client/components/form-dialog.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="450"
-	:can-close="false"
-	:with-ok-button="true"
-	:ok-button-disabled="false"
-	@click="cancel()"
-	@ok="ok()"
-	@close="cancel()"
-	@closed="$emit('closed')"
->
-	<template #header>
-		{{ title }}
-	</template>
-	<FormBase class="xkpnjxcv">
-		<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
-			<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
-				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormInput>
-			<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
-				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormInput>
-			<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
-				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormTextarea>
-			<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
-				<span v-text="form[item].label || item"></span>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormSwitch>
-			<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
-				<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
-				<option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
-			</FormSelect>
-			<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
-				<template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
-				<option v-for="item in form[item].options" :value="item.value" :key="item.value">{{ item.label }}</option>
-			</FormRadios>
-			<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
-				<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
-				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
-			</FormRange>
-			<FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
-				<span v-text="form[item].content || item"></span>
-			</FormButton>
-		</template>
-	</FormBase>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import FormBase from './debobigego/base.vue';
-import FormInput from './debobigego/input.vue';
-import FormTextarea from './debobigego/textarea.vue';
-import FormSwitch from './debobigego/switch.vue';
-import FormSelect from './debobigego/select.vue';
-import FormRange from './debobigego/range.vue';
-import FormButton from './debobigego/button.vue';
-import FormRadios from './debobigego/radios.vue';
-
-export default defineComponent({
-	components: {
-		XModalWindow,
-		FormBase,
-		FormInput,
-		FormTextarea,
-		FormSwitch,
-		FormSelect,
-		FormRange,
-		FormButton,
-		FormRadios,
-	},
-
-	props: {
-		title: {
-			type: String,
-			required: true,
-		},
-		form: {
-			type: Object,
-			required: true,
-		},
-	},
-
-	emits: ['done'],
-
-	data() {
-		return {
-			values: {}
-		};
-	},
-
-	created() {
-		for (const item in this.form) {
-			this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null;
-		}
-	},
-
-	methods: {
-		ok() {
-			this.$emit('done', {
-				result: this.values
-			});
-			this.$refs.dialog.close();
-		},
-
-		cancel() {
-			this.$emit('done', {
-				canceled: true
-			});
-			this.$refs.dialog.close();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xkpnjxcv {
-
-}
-</style>
diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue
deleted file mode 100644
index 591eda9ed5..0000000000
--- a/src/client/components/form/input.vue
+++ /dev/null
@@ -1,315 +0,0 @@
-<template>
-<div class="matxzzsk">
-	<div class="label" @click="focus"><slot name="label"></slot></div>
-	<div class="input" :class="{ inline, disabled, focused }">
-		<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
-		<input ref="inputEl"
-			:type="type"
-			v-model="v"
-			:disabled="disabled"
-			:required="required"
-			:readonly="readonly"
-			:placeholder="placeholder"
-			:pattern="pattern"
-			:autocomplete="autocomplete"
-			:spellcheck="spellcheck"
-			:step="step"
-			@focus="focused = true"
-			@blur="focused = false"
-			@keydown="onKeydown($event)"
-			@input="onInput"
-			:list="id"
-		>
-		<datalist :id="id" v-if="datalist">
-			<option v-for="data in datalist" :value="data"/>
-		</datalist>
-		<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
-	</div>
-	<div class="caption"><slot name="caption"></slot></div>
-
-	<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import { debounce } from 'throttle-debounce';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-
-	props: {
-		modelValue: {
-			required: true
-		},
-		type: {
-			type: String,
-			required: false
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		readonly: {
-			type: Boolean,
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			required: false
-		},
-		pattern: {
-			type: String,
-			required: false
-		},
-		placeholder: {
-			type: String,
-			required: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autocomplete: {
-			required: false
-		},
-		spellcheck: {
-			required: false
-		},
-		step: {
-			required: false
-		},
-		datalist: {
-			type: Array,
-			required: false,
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		debounce: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		manualSave: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['change', 'keydown', 'enter', 'update:modelValue'],
-
-	setup(props, context) {
-		const { modelValue, type, autofocus } = toRefs(props);
-		const v = ref(modelValue.value);
-		const id = Math.random().toString(); // TODO: uuid?
-		const focused = ref(false);
-		const changed = ref(false);
-		const invalid = ref(false);
-		const filled = computed(() => v.value !== '' && v.value != null);
-		const inputEl = ref(null);
-		const prefixEl = ref(null);
-		const suffixEl = ref(null);
-
-		const focus = () => inputEl.value.focus();
-		const onInput = (ev) => {
-			changed.value = true;
-			context.emit('change', ev);
-		};
-		const onKeydown = (ev: KeyboardEvent) => {
-			context.emit('keydown', ev);
-
-			if (ev.code === 'Enter') {
-				context.emit('enter');
-			}
-		};
-
-		const updated = () => {
-			changed.value = false;
-			if (type?.value === 'number') {
-				context.emit('update:modelValue', parseFloat(v.value));
-			} else {
-				context.emit('update:modelValue', v.value);
-			}
-		};
-
-		const debouncedUpdated = debounce(1000, updated);
-
-		watch(modelValue, newValue => {
-			v.value = newValue;
-		});
-
-		watch(v, newValue => {
-			if (!props.manualSave) {
-				if (props.debounce) {
-					debouncedUpdated();
-				} else {
-					updated();
-				}
-			}
-
-			invalid.value = inputEl.value.validity.badInput;
-		});
-
-		onMounted(() => {
-			nextTick(() => {
-				if (autofocus.value) {
-					focus();
-				}
-
-				// このコンポーネントが作成された時、非表示状態である場合がある
-				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
-				const clock = setInterval(() => {
-					if (prefixEl.value) {
-						if (prefixEl.value.offsetWidth) {
-							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
-						}
-					}
-					if (suffixEl.value) {
-						if (suffixEl.value.offsetWidth) {
-							inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
-						}
-					}
-				}, 100);
-
-				onUnmounted(() => {
-					clearInterval(clock);
-				});
-			});
-		});
-
-		return {
-			id,
-			v,
-			focused,
-			invalid,
-			changed,
-			filled,
-			inputEl,
-			prefixEl,
-			suffixEl,
-			focus,
-			onInput,
-			onKeydown,
-			updated,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.matxzzsk {
-	> .label {
-		font-size: 0.85em;
-		padding: 0 0 8px 12px;
-		user-select: none;
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .caption {
-		font-size: 0.8em;
-		padding: 8px 0 0 12px;
-		color: var(--fgTransparentWeak);
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .input {
-		$height: 42px;
-		position: relative;
-
-		> input {
-			appearance: none;
-			-webkit-appearance: none;
-			display: block;
-			height: $height;
-			width: 100%;
-			margin: 0;
-			padding: 0 12px;
-			font: inherit;
-			font-weight: normal;
-			font-size: 1em;
-			color: var(--fg);
-			background: var(--panel);
-			border: solid 0.5px var(--inputBorder);
-			border-radius: 6px;
-			outline: none;
-			box-shadow: none;
-			box-sizing: border-box;
-			transition: border-color 0.1s ease-out;
-
-			&:hover {
-				border-color: var(--inputBorderHover);
-			}
-		}
-
-		> .prefix,
-		> .suffix {
-			display: flex;
-			align-items: center;
-			position: absolute;
-			z-index: 1;
-			top: 0;
-			padding: 0 12px;
-			font-size: 1em;
-			height: $height;
-			pointer-events: none;
-
-			&:empty {
-				display: none;
-			}
-
-			> * {
-				display: inline-block;
-				min-width: 16px;
-				max-width: 150px;
-				overflow: hidden;
-				white-space: nowrap;
-				text-overflow: ellipsis;
-			}
-		}
-
-		> .prefix {
-			left: 0;
-			padding-right: 6px;
-		}
-
-		> .suffix {
-			right: 0;
-			padding-left: 6px;
-		}
-
-		&.inline {
-			display: inline-block;
-			margin: 0;
-		}
-
-		&.focused {
-			> input {
-				border-color: var(--accent);
-				//box-shadow: 0 0 0 4px var(--focus);
-			}
-		}
-
-		&.disabled {
-			opacity: 0.7;
-
-			&, * {
-				cursor: not-allowed !important;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/form/radio.vue b/src/client/components/form/radio.vue
deleted file mode 100644
index 0f31d8fa0a..0000000000
--- a/src/client/components/form/radio.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-<div
-	class="novjtctn"
-	:class="{ disabled, checked }"
-	:aria-checked="checked"
-	:aria-disabled="disabled"
-	@click="toggle"
->
-	<input type="radio"
-		:disabled="disabled"
-	>
-	<span class="button">
-		<span></span>
-	</span>
-	<span class="label"><slot></slot></span>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		modelValue: {
-			required: false
-		},
-		value: {
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			default: false
-		}
-	},
-	computed: {
-		checked(): boolean {
-			return this.modelValue === this.value;
-		}
-	},
-	methods: {
-		toggle() {
-			if (this.disabled) return;
-			this.$emit('update:modelValue', this.value);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.novjtctn {
-	position: relative;
-	display: inline-block;
-	margin: 8px 20px 0 0;
-	text-align: left;
-	cursor: pointer;
-	transition: all 0.3s;
-
-	> * {
-		user-select: none;
-	}
-
-	&.disabled {
-		opacity: 0.6;
-
-		&, * {
-			cursor: not-allowed !important;
-		}
-	}
-
-	&.checked {
-		> .button {
-			border-color: var(--accent);
-
-			&:after {
-				background-color: var(--accent);
-				transform: scale(1);
-				opacity: 1;
-			}
-		}
-	}
-
-	> input {
-		position: absolute;
-		width: 0;
-		height: 0;
-		opacity: 0;
-		margin: 0;
-	}
-
-	> .button {
-		position: absolute;
-		width: 20px;
-		height: 20px;
-		background: none;
-		border: solid 2px var(--inputBorder);
-		border-radius: 100%;
-		transition: inherit;
-
-		&:after {
-			content: '';
-			display: block;
-			position: absolute;
-			top: 3px;
-			right: 3px;
-			bottom: 3px;
-			left: 3px;
-			border-radius: 100%;
-			opacity: 0;
-			transform: scale(0);
-			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
-		}
-	}
-
-	> .label {
-		margin-left: 28px;
-		display: block;
-		font-size: 16px;
-		line-height: 20px;
-		cursor: pointer;
-	}
-}
-</style>
diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue
deleted file mode 100644
index 998a738202..0000000000
--- a/src/client/components/form/radios.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<script lang="ts">
-import { defineComponent, h } from 'vue';
-import MkRadio from './radio.vue';
-
-export default defineComponent({
-	components: {
-		MkRadio
-	},
-	props: {
-		modelValue: {
-			required: false
-		},
-	},
-	data() {
-		return {
-			value: this.modelValue,
-		}
-	},
-	watch: {
-		value() {
-			this.$emit('update:modelValue', this.value);
-		}
-	},
-	render() {
-		let options = this.$slots.default();
-
-		// なぜかFragmentになることがあるため
-		if (options.length === 1 && options[0].props == null) options = options[0].children;
-
-		return h('div', {
-			class: 'novjtcto'
-		}, [
-			...options.map(option => h(MkRadio, {
-				key: option.key,
-				value: option.props.value,
-				modelValue: this.value,
-				'onUpdate:modelValue': value => this.value = value,
-			}, option.children))
-		]);
-	}
-});
-</script>
-
-<style lang="scss">
-.novjtcto {
-	&:first-child {
-		margin-top: 0;
-	}
-
-	&:last-child {
-		margin-bottom: 0;
-	}
-}
-</style>
diff --git a/src/client/components/form/range.vue b/src/client/components/form/range.vue
deleted file mode 100644
index 4cfe66a8fc..0000000000
--- a/src/client/components/form/range.vue
+++ /dev/null
@@ -1,139 +0,0 @@
-<template>
-<div class="timctyfi" :class="{ focused, disabled }">
-	<div class="icon"><slot name="icon"></slot></div>
-	<span class="label"><slot name="label"></slot></span>
-	<input
-		type="range"
-		ref="input"
-		v-model="v"
-		:disabled="disabled"
-		:min="min"
-		:max="max"
-		:step="step"
-		:autofocus="autofocus"
-		@focus="focused = true"
-		@blur="focused = false"
-		@input="$emit('update:value', $event.target.value)"
-	/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		value: {
-			type: Number,
-			required: false,
-			default: 0
-		},
-		disabled: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		min: {
-			type: Number,
-			required: false,
-			default: 0
-		},
-		max: {
-			type: Number,
-			required: false,
-			default: 100
-		},
-		step: {
-			type: Number,
-			required: false,
-			default: 1
-		},
-		autofocus: {
-			type: Boolean,
-			required: false
-		}
-	},
-	data() {
-		return {
-			v: this.value,
-			focused: false
-		};
-	},
-	watch: {
-		value(v) {
-			this.v = parseFloat(v);
-		}
-	},
-	mounted() {
-		if (this.autofocus) {
-			this.$nextTick(() => {
-				this.$refs.input.focus();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.timctyfi {
-	position: relative;
-	margin: 8px;
-
-	> .icon {
-		display: inline-block;
-		width: 24px;
-		text-align: center;
-	}
-
-	> .title {
-		pointer-events: none;
-		font-size: 16px;
-		color: var(--inputLabel);
-		overflow: hidden;
-	}
-
-	> input {
-		-webkit-appearance: none;
-		-moz-appearance: none;
-		appearance: none;
-		background: var(--X10);
-		height: 7px;
-		margin: 0 8px;
-		outline: 0;
-		border: 0;
-		border-radius: 7px;
-
-		&.disabled {
-			opacity: 0.6;
-			cursor: not-allowed;
-		}
-
-		&::-webkit-slider-thumb {
-			-webkit-appearance: none;
-			appearance: none;
-			cursor: pointer;
-			width: 20px;
-			height: 20px;
-			display: block;
-			border-radius: 50%;
-			border: none;
-			background: var(--accent);
-			box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
-			box-sizing: content-box;
-		}
-
-		&::-moz-range-thumb {
-			-moz-appearance: none;
-			appearance: none;
-			cursor: pointer;
-			width: 20px;
-			height: 20px;
-			display: block;
-			border-radius: 50%;
-			border: none;
-			background: var(--accent);
-			box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
-		}
-	}
-}
-</style>
diff --git a/src/client/components/form/section.vue b/src/client/components/form/section.vue
deleted file mode 100644
index 8eac40a0db..0000000000
--- a/src/client/components/form/section.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<div class="vrtktovh" v-size="{ max: [500] }" v-sticky-container>
-	<div class="label"><slot name="label"></slot></div>
-	<div class="main">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-
-});
-</script>
-
-<style lang="scss" scoped>
-.vrtktovh {
-	border-top: solid 0.5px var(--divider);
-
-	> .label {
-		font-weight: bold;
-		padding: 24px 0 16px 0;
-	}
-
-	> .main {
-		margin-bottom: 32px;
-	}
-}
-</style>
diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue
deleted file mode 100644
index 363b3515fa..0000000000
--- a/src/client/components/form/select.vue
+++ /dev/null
@@ -1,312 +0,0 @@
-<template>
-<div class="vblkjoeq">
-	<div class="label" @click="focus"><slot name="label"></slot></div>
-	<div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container">
-		<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
-		<select class="select" ref="inputEl"
-			v-model="v"
-			:disabled="disabled"
-			:required="required"
-			:readonly="readonly"
-			:placeholder="placeholder"
-			@focus="focused = true"
-			@blur="focused = false"
-			@input="onInput"
-		>
-			<slot></slot>
-		</select>
-		<div class="suffix" ref="suffixEl"><i class="fas fa-chevron-down"></i></div>
-	</div>
-	<div class="caption"><slot name="caption"></slot></div>
-
-	<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, VNode } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-
-	props: {
-		modelValue: {
-			required: true
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		readonly: {
-			type: Boolean,
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			required: false
-		},
-		placeholder: {
-			type: String,
-			required: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		manualSave: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['change', 'update:modelValue'],
-
-	setup(props, context) {
-		const { modelValue, autofocus } = toRefs(props);
-		const v = ref(modelValue.value);
-		const focused = ref(false);
-		const changed = ref(false);
-		const invalid = ref(false);
-		const filled = computed(() => v.value !== '' && v.value != null);
-		const inputEl = ref(null);
-		const prefixEl = ref(null);
-		const suffixEl = ref(null);
-		const container = ref(null);
-
-		const focus = () => inputEl.value.focus();
-		const onInput = (ev) => {
-			changed.value = true;
-			context.emit('change', ev);
-		};
-
-		const updated = () => {
-			changed.value = false;
-			context.emit('update:modelValue', v.value);
-		};
-
-		watch(modelValue, newValue => {
-			v.value = newValue;
-		});
-
-		watch(v, newValue => {
-			if (!props.manualSave) {
-				updated();
-			}
-
-			invalid.value = inputEl.value.validity.badInput;
-		});
-
-		onMounted(() => {
-			nextTick(() => {
-				if (autofocus.value) {
-					focus();
-				}
-
-				// このコンポーネントが作成された時、非表示状態である場合がある
-				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
-				const clock = setInterval(() => {
-					if (prefixEl.value) {
-						if (prefixEl.value.offsetWidth) {
-							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
-						}
-					}
-					if (suffixEl.value) {
-						if (suffixEl.value.offsetWidth) {
-							inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
-						}
-					}
-				}, 100);
-
-				onUnmounted(() => {
-					clearInterval(clock);
-				});
-			});
-		});
-
-		const onClick = (ev: MouseEvent) => {
-			focused.value = true;
-
-			const menu = [];
-			let options = context.slots.default();
-
-			const pushOption = (option: VNode) => {
-				menu.push({
-					text: option.children,
-					active: v.value === option.props.value,
-					action: () => {
-						v.value = option.props.value;
-					},
-				});
-			};
-
-			const scanOptions = (options: VNode[]) => {
-				for (const vnode of options) {
-					if (vnode.type === 'optgroup') {
-						const optgroup = vnode;
-						menu.push({
-							type: 'label',
-							text: optgroup.props.label,
-						});
-						scanOptions(optgroup.children);
-					} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
-						const fragment = vnode;
-						scanOptions(fragment.children);
-					} else {
-						const option = vnode;
-						pushOption(option);
-					}
-				}
-			};
-
-			scanOptions(options);
-
-			os.popupMenu(menu, container.value, {
-				width: container.value.offsetWidth,
-			}).then(() => {
-				focused.value = false;
-			});
-		};
-
-		return {
-			v,
-			focused,
-			invalid,
-			changed,
-			filled,
-			inputEl,
-			prefixEl,
-			suffixEl,
-			container,
-			focus,
-			onInput,
-			onClick,
-			updated,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.vblkjoeq {
-	> .label {
-		font-size: 0.85em;
-		padding: 0 0 8px 12px;
-		user-select: none;
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .caption {
-		font-size: 0.8em;
-		padding: 8px 0 0 12px;
-		color: var(--fgTransparentWeak);
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .input {
-		$height: 42px;
-		position: relative;
-		cursor: pointer;
-
-		&:hover {
-			> .select {
-				border-color: var(--inputBorderHover);
-			}
-		}
-
-		> .select {
-			appearance: none;
-			-webkit-appearance: none;
-			display: block;
-			height: $height;
-			width: 100%;
-			margin: 0;
-			padding: 0 12px;
-			font: inherit;
-			font-weight: normal;
-			font-size: 1em;
-			color: var(--fg);
-			background: var(--panel);
-			border: solid 1px var(--inputBorder);
-			border-radius: 6px;
-			outline: none;
-			box-shadow: none;
-			box-sizing: border-box;
-			cursor: pointer;
-			transition: border-color 0.1s ease-out;
-			pointer-events: none;
-		}
-
-		> .prefix,
-		> .suffix {
-			display: flex;
-			align-items: center;
-			position: absolute;
-			z-index: 1;
-			top: 0;
-			padding: 0 12px;
-			font-size: 1em;
-			height: $height;
-			pointer-events: none;
-
-			&:empty {
-				display: none;
-			}
-
-			> * {
-				display: inline-block;
-				min-width: 16px;
-				max-width: 150px;
-				overflow: hidden;
-				white-space: nowrap;
-				text-overflow: ellipsis;
-			}
-		}
-
-		> .prefix {
-			left: 0;
-			padding-right: 6px;
-		}
-
-		> .suffix {
-			right: 0;
-			padding-left: 6px;
-		}
-
-		&.inline {
-			display: inline-block;
-			margin: 0;
-		}
-
-		&.focused {
-			> select {
-				border-color: var(--accent);
-			}
-		}
-
-		&.disabled {
-			opacity: 0.7;
-
-			&, * {
-				cursor: not-allowed !important;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/form/slot.vue b/src/client/components/form/slot.vue
deleted file mode 100644
index 8580c1307d..0000000000
--- a/src/client/components/form/slot.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<div class="adhpbeou">
-	<div class="label" @click="focus"><slot name="label"></slot></div>
-	<div class="content">
-		<slot></slot>
-	</div>
-	<div class="caption"><slot name="caption"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-
-});
-</script>
-
-<style lang="scss" scoped>
-.adhpbeou {
-	margin: 1.5em 0;
-
-	> .label {
-		font-size: 0.85em;
-		padding: 0 0 8px 12px;
-		user-select: none;
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .caption {
-		font-size: 0.8em;
-		padding: 8px 0 0 12px;
-		color: var(--fgTransparentWeak);
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .content {
-		position: relative;
-		background: var(--panel);
-		border: solid 0.5px var(--inputBorder);
-		border-radius: 6px;
-	}
-}
-</style>
diff --git a/src/client/components/form/switch.vue b/src/client/components/form/switch.vue
deleted file mode 100644
index 85f8b7c870..0000000000
--- a/src/client/components/form/switch.vue
+++ /dev/null
@@ -1,150 +0,0 @@
-<template>
-<div
-	class="ziffeoms"
-	:class="{ disabled, checked }"
-	role="switch"
-	:aria-checked="checked"
-	:aria-disabled="disabled"
-	@click.prevent="toggle"
->
-	<input
-		type="checkbox"
-		ref="input"
-		:disabled="disabled"
-		@keydown.enter="toggle"
-	>
-	<span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
-		<span class="handle"></span>
-	</span>
-	<span class="label">
-		<span><slot></slot></span>
-		<p><slot name="caption"></slot></p>
-	</span>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		modelValue: {
-			type: Boolean,
-			default: false
-		},
-		disabled: {
-			type: Boolean,
-			default: false
-		}
-	},
-	computed: {
-		checked(): boolean {
-			return this.modelValue;
-		}
-	},
-	methods: {
-		toggle() {
-			if (this.disabled) return;
-			this.$emit('update:modelValue', !this.checked);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ziffeoms {
-	position: relative;
-	display: flex;
-	cursor: pointer;
-	transition: all 0.3s;
-
-	&:first-child {
-		margin-top: 0;
-	}
-
-	&:last-child {
-		margin-bottom: 0;
-	}
-
-	> * {
-		user-select: none;
-	}
-
-	> input {
-		position: absolute;
-		width: 0;
-		height: 0;
-		opacity: 0;
-		margin: 0;
-	}
-
-	> .button {
-		position: relative;
-		display: inline-block;
-		flex-shrink: 0;
-		margin: 0;
-		width: 36px;
-		height: 26px;
-		background: var(--switchBg);
-		outline: none;
-		border-radius: 999px;
-		transition: inherit;
-
-		> .handle {
-			position: absolute;
-			top: 0;
-			bottom: 0;
-			left: 5px;
-			margin: auto 0;
-			border-radius: 100%;
-			transition: background-color 0.3s, transform 0.3s;
-			width: 16px;
-			height: 16px;
-			background-color: #fff;
-		}
-	}
-
-	> .label {
-		margin-left: 16px;
-		margin-top: 2px;
-		display: block;
-		cursor: pointer;
-		transition: inherit;
-		color: var(--fg);
-
-		> span {
-			display: block;
-			line-height: 20px;
-			transition: inherit;
-		}
-
-		> p {
-			margin: 0;
-			color: var(--fgTransparentWeak);
-			font-size: 90%;
-		}
-	}
-
-	&:hover {
-		> .button {
-			background-color: var(--accentedBg);
-		}
-	}
-
-	&.disabled {
-		opacity: 0.6;
-		cursor: not-allowed;
-	}
-
-	&.checked {
-		> .button {
-			background-color: var(--accent);
-			border-color: var(--accent);
-
-			> .handle {
-				transform: translateX(10px);
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue
deleted file mode 100644
index 048e9032df..0000000000
--- a/src/client/components/form/textarea.vue
+++ /dev/null
@@ -1,252 +0,0 @@
-<template>
-<div class="adhpbeos">
-	<div class="label" @click="focus"><slot name="label"></slot></div>
-	<div class="input" :class="{ disabled, focused, tall, pre }">
-		<textarea ref="inputEl"
-			:class="{ code, _monospace: code }"
-			v-model="v"
-			:disabled="disabled"
-			:required="required"
-			:readonly="readonly"
-			:placeholder="placeholder"
-			:pattern="pattern"
-			:autocomplete="autocomplete"
-			:spellcheck="spellcheck"
-			@focus="focused = true"
-			@blur="focused = false"
-			@keydown="onKeydown($event)"
-			@input="onInput"
-		></textarea>
-	</div>
-	<div class="caption"><slot name="caption"></slot></div>
-
-	<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import { debounce } from 'throttle-debounce';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-
-	props: {
-		modelValue: {
-			required: true
-		},
-		type: {
-			type: String,
-			required: false
-		},
-		required: {
-			type: Boolean,
-			required: false
-		},
-		readonly: {
-			type: Boolean,
-			required: false
-		},
-		disabled: {
-			type: Boolean,
-			required: false
-		},
-		pattern: {
-			type: String,
-			required: false
-		},
-		placeholder: {
-			type: String,
-			required: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autocomplete: {
-			required: false
-		},
-		spellcheck: {
-			required: false
-		},
-		code: {
-			type: Boolean,
-			required: false
-		},
-		tall: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		pre: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		debounce: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		manualSave: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['change', 'keydown', 'enter', 'update:modelValue'],
-
-	setup(props, context) {
-		const { modelValue, autofocus } = toRefs(props);
-		const v = ref(modelValue.value);
-		const focused = ref(false);
-		const changed = ref(false);
-		const invalid = ref(false);
-		const filled = computed(() => v.value !== '' && v.value != null);
-		const inputEl = ref(null);
-
-		const focus = () => inputEl.value.focus();
-		const onInput = (ev) => {
-			changed.value = true;
-			context.emit('change', ev);
-		};
-		const onKeydown = (ev: KeyboardEvent) => {
-			context.emit('keydown', ev);
-
-			if (ev.code === 'Enter') {
-				context.emit('enter');
-			}
-		};
-
-		const updated = () => {
-			changed.value = false;
-			context.emit('update:modelValue', v.value);
-		};
-
-		const debouncedUpdated = debounce(1000, updated);
-
-		watch(modelValue, newValue => {
-			v.value = newValue;
-		});
-
-		watch(v, newValue => {
-			if (!props.manualSave) {
-				if (props.debounce) {
-					debouncedUpdated();
-				} else {
-					updated();
-				}
-			}
-
-			invalid.value = inputEl.value.validity.badInput;
-		});
-
-		onMounted(() => {
-			nextTick(() => {
-				if (autofocus.value) {
-					focus();
-				}
-			});
-		});
-
-		return {
-			v,
-			focused,
-			invalid,
-			changed,
-			filled,
-			inputEl,
-			focus,
-			onInput,
-			onKeydown,
-			updated,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.adhpbeos {
-	> .label {
-		font-size: 0.85em;
-		padding: 0 0 8px 12px;
-		user-select: none;
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .caption {
-		font-size: 0.8em;
-		padding: 8px 0 0 12px;
-		color: var(--fgTransparentWeak);
-
-		&:empty {
-			display: none;
-		}
-	}
-
-	> .input {
-		position: relative;
-
-		> textarea {
-			appearance: none;
-			-webkit-appearance: none;
-			display: block;
-			width: 100%;
-			min-width: 100%;
-			max-width: 100%;
-			min-height: 130px;
-			margin: 0;
-			padding: 12px;
-			font: inherit;
-			font-weight: normal;
-			font-size: 1em;
-			color: var(--fg);
-			background: var(--panel);
-			border: solid 0.5px var(--inputBorder);
-			border-radius: 6px;
-			outline: none;
-			box-shadow: none;
-			box-sizing: border-box;
-			transition: border-color 0.1s ease-out;
-
-			&:hover {
-				border-color: var(--inputBorderHover);
-			}
-		}
-
-		&.focused {
-			> textarea {
-				border-color: var(--accent);
-			}
-		}
-
-		&.disabled {
-			opacity: 0.7;
-
-			&, * {
-				cursor: not-allowed !important;
-			}
-		}
-
-		&.tall {
-			> textarea {
-				min-height: 200px;
-			}
-		}
-
-		&.pre {
-			> textarea {
-				white-space: pre;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/formula-core.vue b/src/client/components/formula-core.vue
deleted file mode 100644
index 6e35295ff5..0000000000
--- a/src/client/components/formula-core.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-
-<template>
-<div v-if="block" v-html="compiledFormula"></div>
-<span v-else v-html="compiledFormula"></span>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as katex from 'katex';import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		formula: {
-			type: String,
-			required: true
-		},
-		block: {
-			type: Boolean,
-			required: true
-		}
-	},
-	computed: {
-		compiledFormula(): any {
-			return katex.renderToString(this.formula, {
-				throwOnError: false
-			} as any);
-		}
-	}
-});
-</script>
-
-<style>
-@import "../../../node_modules/katex/dist/katex.min.css";
-</style>
diff --git a/src/client/components/formula.vue b/src/client/components/formula.vue
deleted file mode 100644
index 6722ce38a1..0000000000
--- a/src/client/components/formula.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-<template>
-<XFormula :formula="formula" :block="block" />
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XFormula: defineAsyncComponent(() => import('./formula-core.vue'))
-	},
-	props: {
-		formula: {
-			type: String,
-			required: true
-		},
-		block: {
-			type: Boolean,
-			required: true
-		}
-	}
-});
-</script>
diff --git a/src/client/components/gallery-post-preview.vue b/src/client/components/gallery-post-preview.vue
deleted file mode 100644
index 5c3bdb1349..0000000000
--- a/src/client/components/gallery-post-preview.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<template>
-<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1">
-	<div class="thumbnail">
-		<ImgWithBlurhash class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
-	</div>
-	<article>
-		<header>
-			<MkAvatar :user="post.user" class="avatar"/>
-		</header>
-		<footer>
-			<span class="title">{{ post.title }}</span>
-		</footer>
-	</article>
-</MkA>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { userName } from '@client/filters/user';
-import ImgWithBlurhash from '@client/components/img-with-blurhash.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		ImgWithBlurhash
-	},
-	props: {
-		post: {
-			type: Object,
-			required: true
-		},
-	},
-	methods: {
-		userName
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ttasepnz {
-	display: block;
-	position: relative;
-	height: 200px;
-
-	&:hover {
-		text-decoration: none;
-		color: var(--accent);
-
-		> .thumbnail {
-			transform: scale(1.1);
-		}
-
-		> article {
-			> footer {
-				&:before {
-					opacity: 1;
-				}
-			}
-		}
-	}
-
-	> .thumbnail {
-		width: 100%;
-		height: 100%;
-		position: absolute;
-		transition: all 0.5s ease;
-
-		> .img {
-			width: 100%;
-			height: 100%;
-			object-fit: cover;
-		}
-	}
-
-	> article {
-		position: absolute;
-		z-index: 1;
-		width: 100%;
-		height: 100%;
-
-		> header {
-			position: absolute;
-			top: 0;
-			width: 100%;
-			padding: 12px;
-			box-sizing: border-box;
-			display: flex;
-
-			> .avatar {
-				margin-left: auto;
-				width: 32px;
-				height: 32px;
-			}
-		}
-
-		> footer {
-			position: absolute;
-			bottom: 0;
-			width: 100%;
-			padding: 16px;
-			box-sizing: border-box;
-			color: #fff;
-			text-shadow: 0 0 8px #000;
-			background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
-
-			&:before {
-				content: "";
-				display: block;
-				position: absolute;
-				z-index: -1;
-				top: 0;
-				left: 0;
-				width: 100%;
-				height: 100%;
-				background: linear-gradient(rgba(0, 0, 0, 0.4), transparent);
-				opacity: 0;
-				transition: opacity 0.5s ease;
-			}
-
-			> .title {
-				font-weight: bold;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/global/a.vue b/src/client/components/global/a.vue
deleted file mode 100644
index 952dfb1841..0000000000
--- a/src/client/components/global/a.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<template>
-<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
-	<slot></slot>
-</a>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { router } from '@client/router';
-import { url } from '@client/config';
-import { popout } from '@client/scripts/popout';
-import { ColdDeviceStorage } from '@client/store';
-
-export default defineComponent({
-	inject: {
-		navHook: {
-			default: null
-		},
-		sideViewHook: {
-			default: null
-		}
-	},
-
-	props: {
-		to: {
-			type: String,
-			required: true,
-		},
-		activeClass: {
-			type: String,
-			required: false,
-		},
-		behavior: {
-			type: String,
-			required: false,
-		},
-	},
-
-	computed: {
-		active() {
-			if (this.activeClass == null) return false;
-			const resolved = router.resolve(this.to);
-			if (resolved.path == this.$route.path) return true;
-			if (resolved.name == null) return false;
-			if (this.$route.name == null) return false;
-			return resolved.name == this.$route.name;
-		}
-	},
-
-	methods: {
-		onContextmenu(e) {
-			if (window.getSelection().toString() !== '') return;
-			os.contextMenu([{
-				type: 'label',
-				text: this.to,
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(this.to);
-				}
-			}, this.sideViewHook ? {
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.sideViewHook(this.to);
-				}
-			} : undefined, {
-				icon: 'fas fa-expand-alt',
-				text: this.$ts.showInPage,
-				action: () => {
-					this.$router.push(this.to);
-				}
-			}, null, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.openInNewTab,
-				action: () => {
-					window.open(this.to, '_blank');
-				}
-			}, {
-				icon: 'fas fa-link',
-				text: this.$ts.copyLink,
-				action: () => {
-					copyToClipboard(`${url}${this.to}`);
-				}
-			}], e);
-		},
-
-		window() {
-			os.pageWindow(this.to);
-		},
-
-		modalWindow() {
-			os.modalPageWindow(this.to);
-		},
-
-		popout() {
-			popout(this.to);
-		},
-
-		nav() {
-			if (this.behavior === 'browser') {
-				location.href = this.to;
-				return;
-			}
-
-			if (this.to.startsWith('/my/messaging')) {
-				if (ColdDeviceStorage.get('chatOpenBehavior') === 'window') return this.window();
-				if (ColdDeviceStorage.get('chatOpenBehavior') === 'popout') return this.popout();
-			}
-
-			if (this.behavior) {
-				if (this.behavior === 'window') {
-					return this.window();
-				} else if (this.behavior === 'modalWindow') {
-					return this.modalWindow();
-				}
-			}
-
-			if (this.navHook) {
-				this.navHook(this.to);
-			} else {
-				if (this.$store.state.defaultSideView && this.sideViewHook && this.to !== '/') {
-					return this.sideViewHook(this.to);
-				}
-
-				if (this.$router.currentRoute.value.path === this.to) {
-					window.scroll({ top: 0, behavior: 'smooth' });
-				} else {
-					this.$router.push(this.to);
-				}
-			}
-		}
-	}
-});
-</script>
diff --git a/src/client/components/global/acct.vue b/src/client/components/global/acct.vue
deleted file mode 100644
index 70f2954cb0..0000000000
--- a/src/client/components/global/acct.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<span class="mk-acct">
-	<span class="name">@{{ user.username }}</span>
-	<span class="host" v-if="user.host || detail || $store.state.showFullAcct">@{{ user.host || host }}</span>
-</span>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { toUnicode } from 'punycode/';
-import { host } from '@client/config';
-
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		detail: {
-			type: Boolean,
-			default: false
-		},
-	},
-	data() {
-		return {
-			host: toUnicode(host),
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-acct {
-	> .host {
-		opacity: 0.5;
-	}
-}
-</style>
diff --git a/src/client/components/global/ad.vue b/src/client/components/global/ad.vue
deleted file mode 100644
index 8397b2229e..0000000000
--- a/src/client/components/global/ad.vue
+++ /dev/null
@@ -1,200 +0,0 @@
-<template>
-<div class="qiivuoyo" v-if="ad">
-	<div class="main" :class="ad.place" v-if="!showMenu">
-		<a :href="ad.url" target="_blank">
-			<img :src="ad.imageUrl">
-			<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button>
-		</a>
-	</div>
-	<div class="menu" v-else>
-		<div class="body">
-			<div>Ads by {{ host }}</div>
-			<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
-			<MkButton v-if="ad.ratio !== 0" class="button" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
-			<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
-		</div>
-	</div>
-</div>
-<div v-else></div>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref } from 'vue';
-import { Instance, instance } from '@client/instance';
-import { host } from '@client/config';
-import MkButton from '@client/components/ui/button.vue';
-import { defaultStore } from '@client/store';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	props: {
-		prefer: {
-			type: Array,
-			required: true
-		},
-		specify: {
-			type: Object,
-			required: false
-		},
-	},
-
-	setup(props) {
-		const showMenu = ref(false);
-		const toggleMenu = () => {
-			showMenu.value = !showMenu.value;
-		};
-
-		const choseAd = (): Instance['ads'][number] | null => {
-			if (props.specify) {
-				return props.specify as Instance['ads'][number];
-			}
-
-			const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
-				...ad,
-				ratio: 0
-			} : ad);
-
-			let ads = allAds.filter(ad => props.prefer.includes(ad.place));
-
-			if (ads.length === 0) {
-				ads = allAds.filter(ad => ad.place === 'square');
-			}
-
-			const lowPriorityAds = ads.filter(ad => ad.ratio === 0);
-			ads = ads.filter(ad => ad.ratio !== 0);
-
-			if (ads.length === 0) {
-				if (lowPriorityAds.length !== 0) {
-					return lowPriorityAds[Math.floor(Math.random() * lowPriorityAds.length)];
-				} else {
-					return null;
-				}
-			}
-
-			const totalFactor = ads.reduce((a, b) => a + b.ratio, 0);
-			const r = Math.random() * totalFactor;
-
-			let stackedFactor = 0;
-			for (const ad of ads) {
-				if (r >= stackedFactor && r <= stackedFactor + ad.ratio) {
-					return ad;
-				} else {
-					stackedFactor += ad.ratio;
-				}
-			}
-
-			return null;
-		};
-
-		const chosen = ref(choseAd());
-
-		const reduceFrequency = () => {
-			if (chosen.value == null) return;
-			if (defaultStore.state.mutedAds.includes(chosen.value.id)) return;
-			defaultStore.push('mutedAds', chosen.value.id);
-			os.success();
-			chosen.value = choseAd();
-			showMenu.value = false;
-		};
-
-		return {
-			ad: chosen,
-			showMenu,
-			toggleMenu,
-			host,
-			reduceFrequency,
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.qiivuoyo {
-	background-size: auto auto;
-	background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
-
-	> .main {
-		text-align: center;
-
-		> a {
-			display: inline-block;
-			position: relative;
-			vertical-align: bottom;
-
-			&:hover {
-				> img {
-					filter: contrast(120%);
-				}
-			}
-
-			> img {
-				display: block;
-				object-fit: contain;
-				margin: auto;
-			}
-
-			> .menu {
-				position: absolute;
-				top: 0;
-				right: 0;
-				background: var(--panel);
-			}
-		}
-
-		&.square {
-			> a ,
-			> a > img {
-				max-width: min(300px, 100%);
-				max-height: 300px;
-			}
-		}
-
-		&.horizontal {
-			padding: 8px;
-
-			> a ,
-			> a > img {
-				max-width: min(600px, 100%);
-				max-height: 80px;
-			}
-		}
-
-		&.horizontal-big {
-			padding: 8px;
-
-			> a ,
-			> a > img {
-				max-width: min(600px, 100%);
-				max-height: 250px;
-			}
-		}
-
-		&.vertical {
-			> a ,
-			> a > img {
-				max-width: min(100px, 100%);
-			}
-		}
-	}
-
-	> .menu {
-		padding: 8px;
-		text-align: center;
-
-		> .body {
-			padding: 8px;
-			margin: 0 auto;
-			max-width: 400px;
-			border: solid 1px var(--divider);
-
-			> .button {
-				margin: 8px auto;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/global/avatar.vue b/src/client/components/global/avatar.vue
deleted file mode 100644
index 395ed5d8ce..0000000000
--- a/src/client/components/global/avatar.vue
+++ /dev/null
@@ -1,163 +0,0 @@
-<template>
-<span class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
-	<img class="inner" :src="url" decoding="async"/>
-	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
-</span>
-<MkA class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
-	<img class="inner" :src="url" decoding="async"/>
-	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
-</MkA>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { getStaticImageUrl } from '@client/scripts/get-static-image-url';
-import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash';
-import { acct, userPage } from '@client/filters/user';
-import MkUserOnlineIndicator from '@client/components/user-online-indicator.vue';
-
-export default defineComponent({
-	components: {
-		MkUserOnlineIndicator
-	},
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		target: {
-			required: false,
-			default: null
-		},
-		disableLink: {
-			required: false,
-			default: false
-		},
-		disablePreview: {
-			required: false,
-			default: false
-		},
-		showIndicator: {
-			required: false,
-			default: false
-		}
-	},
-	emits: ['click'],
-	computed: {
-		cat(): boolean {
-			return this.user.isCat;
-		},
-		url(): string {
-			return this.$store.state.disableShowingAnimatedImages
-				? getStaticImageUrl(this.user.avatarUrl)
-				: this.user.avatarUrl;
-		},
-	},
-	watch: {
-		'user.avatarBlurhash'() {
-			if (this.$el == null) return;
-			this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
-		}
-	},
-	mounted() {
-		this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
-	},
-	methods: {
-		onClick(e) {
-			this.$emit('click', e);
-		},
-		acct,
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-@keyframes earwiggleleft {
-	from { transform: rotate(37.6deg) skew(30deg); }
-	25% { transform: rotate(10deg) skew(30deg); }
-	50% { transform: rotate(20deg) skew(30deg); }
-	75% { transform: rotate(0deg) skew(30deg); }
-	to { transform: rotate(37.6deg) skew(30deg); }
-}
-
-@keyframes earwiggleright {
-	from { transform: rotate(-37.6deg) skew(-30deg); }
-	30% { transform: rotate(-10deg) skew(-30deg); }
-	55% { transform: rotate(-20deg) skew(-30deg); }
-	75% { transform: rotate(0deg) skew(-30deg); }
-	to { transform: rotate(-37.6deg) skew(-30deg); }
-}
-
-.eiwwqkts {
-	position: relative;
-	display: inline-block;
-	vertical-align: bottom;
-	flex-shrink: 0;
-	border-radius: 100%;
-	line-height: 16px;
-
-	> .inner {
-		position: absolute;
-		bottom: 0;
-		left: 0;
-		right: 0;
-		top: 0;
-		border-radius: 100%;
-		z-index: 1;
-		overflow: hidden;
-		object-fit: cover;
-		width: 100%;
-		height: 100%;
-	}
-
-	> .indicator {
-		position: absolute;
-		z-index: 1;
-		bottom: 0;
-		left: 0;
-		width: 20%;
-		height: 20%;
-	}
-
-	&.square {
-		border-radius: 20%;
-
-		> .inner {
-			border-radius: 20%;
-		}
-	}
-
-	&.cat {
-		&:before, &:after {
-			background: #df548f;
-			border: solid 4px currentColor;
-			box-sizing: border-box;
-			content: '';
-			display: inline-block;
-			height: 50%;
-			width: 50%;
-		}
-
-		&:before {
-			border-radius: 0 75% 75%;
-			transform: rotate(37.5deg) skew(30deg);
-		}
-
-		&:after {
-			border-radius: 75% 0 75% 75%;
-			transform: rotate(-37.5deg) skew(-30deg);
-		}
-
-		&:hover {
-			&:before {
-				animation: earwiggleleft 1s infinite;
-			}
-
-			&:after {
-				animation: earwiggleright 1s infinite;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/global/ellipsis.vue b/src/client/components/global/ellipsis.vue
deleted file mode 100644
index 0a46f486d6..0000000000
--- a/src/client/components/global/ellipsis.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-	<span class="mk-ellipsis">
-		<span>.</span><span>.</span><span>.</span>
-	</span>
-</template>
-
-<style lang="scss" scoped>
-.mk-ellipsis {
-	> span {
-		animation: ellipsis 1.4s infinite ease-in-out both;
-
-		&:nth-child(1) {
-			animation-delay: 0s;
-		}
-
-		&:nth-child(2) {
-			animation-delay: 0.16s;
-		}
-
-		&:nth-child(3) {
-			animation-delay: 0.32s;
-		}
-	}
-}
-
-@keyframes ellipsis {
-	0%, 80%, 100% {
-		opacity: 1;
-	}
-	40% {
-		opacity: 0;
-	}
-}
-</style>
diff --git a/src/client/components/global/emoji.vue b/src/client/components/global/emoji.vue
deleted file mode 100644
index f92e35c38f..0000000000
--- a/src/client/components/global/emoji.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<template>
-<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/>
-<img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt" decoding="async"/>
-<span v-else-if="char && useOsNativeEmojis">{{ char }}</span>
-<span v-else>{{ emoji }}</span>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { getStaticImageUrl } from '@client/scripts/get-static-image-url';
-import { twemojiSvgBase } from '@client/../misc/twemoji-base';
-
-export default defineComponent({
-	props: {
-		emoji: {
-			type: String,
-			required: true
-		},
-		normal: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		noStyle: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		customEmojis: {
-			required: false
-		},
-		isReaction: {
-			type: Boolean,
-			default: false
-		},
-	},
-
-	data() {
-		return {
-			url: null,
-			char: null,
-			customEmoji: null
-		}
-	},
-
-	computed: {
-		isCustom(): boolean {
-			return this.emoji.startsWith(':');
-		},
-
-		alt(): string {
-			return this.customEmoji ? `:${this.customEmoji.name}:` : this.char;
-		},
-
-		useOsNativeEmojis(): boolean {
-			return this.$store.state.useOsNativeEmojis && !this.isReaction;
-		},
-
-		ce() {
-			return this.customEmojis || this.$instance?.emojis || [];
-		}
-	},
-
-	watch: {
-		ce: {
-			handler() {
-				if (this.isCustom) {
-					const customEmoji = this.ce.find(x => x.name === this.emoji.substr(1, this.emoji.length - 2));
-					if (customEmoji) {
-						this.customEmoji = customEmoji;
-						this.url = this.$store.state.disableShowingAnimatedImages
-							? getStaticImageUrl(customEmoji.url)
-							: customEmoji.url;
-					}
-				}
-			},
-			immediate: true
-		},
-	},
-
-	created() {
-		if (!this.isCustom) {
-			this.char = this.emoji;
-		}
-
-		if (this.char) {
-			let codes = Array.from(this.char).map(x => x.codePointAt(0).toString(16));
-			if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
-			codes = codes.filter(x => x && x.length);
-
-			this.url = `${twemojiSvgBase}/${codes.join('-')}.svg`;
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-emoji {
-	height: 1.25em;
-	vertical-align: -0.25em;
-
-	&.custom {
-		height: 2.5em;
-		vertical-align: middle;
-		transition: transform 0.2s ease;
-
-		&:hover {
-			transform: scale(1.2);
-		}
-
-		&.normal {
-			height: 1.25em;
-			vertical-align: -0.25em;
-
-			&:hover {
-				transform: none;
-			}
-		}
-	}
-
-	&.noStyle {
-		height: auto !important;
-	}
-}
-</style>
diff --git a/src/client/components/global/error.vue b/src/client/components/global/error.vue
deleted file mode 100644
index 05a508a653..0000000000
--- a/src/client/components/global/error.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<template>
-<transition :name="$store.state.animation ? 'zoom' : ''" appear>
-	<div class="mjndxjcg">
-		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
-		<p><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</p>
-		<MkButton @click="() => $emit('retry')" class="button">{{ $ts.retry }}</MkButton>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-	data() {
-		return {
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.mjndxjcg {
-	padding: 32px;
-	text-align: center;
-
-	> p {
-		margin: 0 0 8px 0;
-	}
-
-	> .button {
-		margin: 0 auto;
-	}
-
-	> img {
-		vertical-align: bottom;
-		height: 128px;
-		margin-bottom: 16px;
-		border-radius: 16px;
-	}
-}
-</style>
diff --git a/src/client/components/global/header.vue b/src/client/components/global/header.vue
deleted file mode 100644
index 526db07fd3..0000000000
--- a/src/client/components/global/header.vue
+++ /dev/null
@@ -1,360 +0,0 @@
-<template>
-<div class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick" ref="el">
-	<template v-if="info">
-		<div class="titleContainer" @click="showTabsPopup" v-if="!hideTitle">
-			<MkAvatar v-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
-			<i v-else-if="info.icon" class="icon" :class="info.icon"></i>
-
-			<div class="title">
-				<MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="title"/>
-				<div v-else-if="info.title" class="title">{{ info.title }}</div>
-				<div class="subtitle" v-if="!narrow && info.subtitle">
-					{{ info.subtitle }}
-				</div>
-				<div class="subtitle activeTab" v-if="narrow && hasTabs">
-					{{ info.tabs.find(tab => tab.active)?.title }}
-					<i class="chevron fas fa-chevron-down"></i>
-				</div>
-			</div>
-		</div>
-		<div class="tabs" v-if="!narrow || hideTitle">
-			<button class="tab _button" v-for="tab in info.tabs" :class="{ active: tab.active }" @click="tab.onClick" v-tooltip="tab.title">
-				<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
-				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
-			</button>
-		</div>
-	</template>
-	<div class="buttons right">
-		<template v-if="info && info.actions && !narrow">
-			<template v-for="action in info.actions">
-				<MkButton class="fullButton" v-if="action.asFullButton" @click.stop="action.handler" primary><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
-				<button v-else class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button>
-			</template>
-		</template>
-		<button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue';
-import * as tinycolor from 'tinycolor2';
-import { popupMenu } from '@client/os';
-import { url } from '@client/config';
-import { scrollToTop } from '@client/scripts/scroll';
-import MkButton from '@client/components/ui/button.vue';
-import { i18n } from '@client/i18n';
-import { globalEvents } from '@client/events';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	props: {
-		info: {
-			type: Object as PropType<{
-				actions?: {}[];
-				tabs?: {}[];
-			}>,
-			required: true
-		},
-		menu: {
-			required: false
-		},
-		thin: {
-			required: false,
-			default: false
-		},
-	},
-
-	setup(props) {
-		const el = ref<HTMLElement>(null);
-		const bg = ref(null);
-		const narrow = ref(false);
-		const height = ref(0);
-		const hasTabs = computed(() => {
-			return props.info.tabs && props.info.tabs.length > 0;
-		});
-		const shouldShowMenu = computed(() => {
-			if (props.info == null) return false;
-			if (props.info.actions != null && narrow.value) return true;
-			if (props.info.menu != null) return true;
-			if (props.info.share != null) return true;
-			if (props.menu != null) return true;
-			return false;
-		});
-
-		const share = () => {
-			navigator.share({
-				url: url + props.info.path,
-				...props.info.share,
-			});
-		};
-
-		const showMenu = (ev: MouseEvent) => {
-			let menu = props.info.menu ? props.info.menu() : [];
-			if (narrow.value && props.info.actions) {
-				menu = [...props.info.actions.map(x => ({
-					text: x.text,
-					icon: x.icon,
-					action: x.handler
-				})), menu.length > 0 ? null : undefined, ...menu];
-			}
-			if (props.info.share) {
-				if (menu.length > 0) menu.push(null);
-				menu.push({
-					text: i18n.locale.share,
-					icon: 'fas fa-share-alt',
-					action: share
-				});
-			}
-			if (props.menu) {
-				if (menu.length > 0) menu.push(null);
-				menu = menu.concat(props.menu);
-			}
-			popupMenu(menu, ev.currentTarget || ev.target);
-		};
-
-		const showTabsPopup = (ev: MouseEvent) => {
-			if (!hasTabs.value) return;
-			if (!narrow.value) return;
-			ev.preventDefault();
-			ev.stopPropagation();
-			const menu = props.info.tabs.map(tab => ({
-				text: tab.title,
-				icon: tab.icon,
-				action: tab.onClick,
-			}));
-			popupMenu(menu, ev.currentTarget || ev.target);
-		};
-
-		const preventDrag = (ev: TouchEvent) => {
-			ev.stopPropagation();
-		};
-
-		const onClick = () => {
-			scrollToTop(el.value, { behavior: 'smooth' });
-		};
-
-		const calcBg = () => {
-			const rawBg = props.info?.bg || 'var(--bg)';
-			const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
-			tinyBg.setAlpha(0.85);
-			bg.value = tinyBg.toRgbString();
-		};
-
-		onMounted(() => {
-			calcBg();
-			globalEvents.on('themeChanged', calcBg);
-			onUnmounted(() => {
-				globalEvents.off('themeChanged', calcBg);
-			});
-		
-			if (el.value.parentElement) {
-				narrow.value = el.value.parentElement.offsetWidth < 500;
-				const ro = new ResizeObserver((entries, observer) => {
-					if (el.value) {
-						narrow.value = el.value.parentElement.offsetWidth < 500;
-					}
-				});
-				ro.observe(el.value.parentElement);
-				onUnmounted(() => {
-					ro.disconnect();
-				});
-			}
-		});
-
-		return {
-			el,
-			bg,
-			narrow,
-			height,
-			hasTabs,
-			shouldShowMenu,
-			share,
-			showMenu,
-			showTabsPopup,
-			preventDrag,
-			onClick,
-			hideTitle: inject('shouldOmitHeaderTitle', false),
-			thin_: props.thin || inject('shouldHeaderThin', false)
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.fdidabkb {
-	--height: 60px;
-	display: flex;
-	position: sticky;
-	top: var(--stickyTop, 0);
-	z-index: 1000;
-	width: 100%;
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-	border-bottom: solid 0.5px var(--divider);
-
-	&.thin {
-		--height: 50px;
-
-		> .buttons {
-			> .button {
-				font-size: 0.9em;
-			}
-		}
-	}
-
-	&.slim {
-		text-align: center;
-
-		> .titleContainer {
-			flex: 1;
-			margin: 0 auto;
-			margin-left: var(--height);
-
-			> *:first-child {
-				margin-left: auto;
-			}
-
-			> *:last-child {
-				margin-right: auto;
-			}
-		}
-	}
-
-	> .buttons {
-		--margin: 8px;
-		display: flex;
-    align-items: center;
-		height: var(--height);
-		margin: 0 var(--margin);
-
-		&.right {
-			margin-left: auto;
-		}
-
-		&:empty {
-			width: var(--height);
-		}
-
-		> .button {
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			height: calc(var(--height) - (var(--margin) * 2));
-			width: calc(var(--height) - (var(--margin) * 2));
-			box-sizing: border-box;
-			position: relative;
-			border-radius: 5px;
-
-			&:hover {
-				background: rgba(0, 0, 0, 0.05);
-			}
-
-			&.highlighted {
-				color: var(--accent);
-			}
-		}
-
-		> .fullButton {
-			& + .fullButton {
-				margin-left: 12px;
-			}
-		}
-	}
-
-	> .titleContainer {
-		display: flex;
-		align-items: center;
-		overflow: auto;
-		white-space: nowrap;
-		text-align: left;
-		font-weight: bold;
-		flex-shrink: 0;
-		margin-left: 24px;
-
-		> .avatar {
-			$size: 32px;
-			display: inline-block;
-			width: $size;
-			height: $size;
-			vertical-align: bottom;
-			margin: 0 8px;
-			pointer-events: none;
-		}
-
-		> .icon {
-			margin-right: 8px;
-		}
-
-		> .title {
-			min-width: 0;
-			overflow: hidden;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-			line-height: 1.1;
-
-			> .subtitle {
-				opacity: 0.6;
-				font-size: 0.8em;
-				font-weight: normal;
-				white-space: nowrap;
-				overflow: hidden;
-				text-overflow: ellipsis;
-
-				&.activeTab {
-					text-align: center;
-
-					> .chevron {
-						display: inline-block;
-						margin-left: 6px;
-					}
-				}
-			}
-		}
-	}
-
-	> .tabs {
-		margin-left: 16px;
-		font-size: 0.8em;
-		overflow: auto;
-		white-space: nowrap;
-
-		> .tab {
-			display: inline-block;
-			position: relative;
-			padding: 0 10px;
-			height: 100%;
-			font-weight: normal;
-			opacity: 0.7;
-
-			&:hover {
-				opacity: 1;
-			}
-
-			&.active {
-				opacity: 1;
-
-				&:after {
-					content: "";
-					display: block;
-					position: absolute;
-					bottom: 0;
-					left: 0;
-					right: 0;
-					margin: 0 auto;
-					width: 100%;
-					height: 3px;
-					background: var(--accent);
-				}
-			}
-
-			> .icon + .title {
-				margin-left: 8px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/global/i18n.ts b/src/client/components/global/i18n.ts
deleted file mode 100644
index abf0c96856..0000000000
--- a/src/client/components/global/i18n.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { h, defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		src: {
-			type: String,
-			required: true,
-		},
-		tag: {
-			type: String,
-			required: false,
-			default: 'span',
-		},
-		textTag: {
-			type: String,
-			required: false,
-			default: null,
-		},
-	},
-	render() {
-		let str = this.src;
-		const parsed = [] as (string | { arg: string; })[];
-		while (true) {
-			const nextBracketOpen = str.indexOf('{');
-			const nextBracketClose = str.indexOf('}');
-
-			if (nextBracketOpen === -1) {
-				parsed.push(str);
-				break;
-			} else {
-				if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
-				parsed.push({
-					arg: str.substring(nextBracketOpen + 1, nextBracketClose)
-				});
-			}
-
-			str = str.substr(nextBracketClose + 1);
-		}
-
-		return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]()));
-	}
-});
diff --git a/src/client/components/global/loading.vue b/src/client/components/global/loading.vue
deleted file mode 100644
index 7bde53c12e..0000000000
--- a/src/client/components/global/loading.vue
+++ /dev/null
@@ -1,92 +0,0 @@
-<template>
-<div class="yxspomdl" :class="{ inline, colored, mini }">
-	<div class="ring"></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		colored: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-		mini: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-@keyframes ring {
-	0% {
-		transform: rotate(0deg);
-	}
-	100% {
-		transform: rotate(360deg);
-	}
-}
-
-.yxspomdl {
-	padding: 32px;
-	text-align: center;
-	cursor: wait;
-
-	--size: 48px;
-
-	&.colored {
-		color: var(--accent);
-	}
-
-	&.inline {
-		display: inline;
-		padding: 0;
-		--size: 32px;
-	}
-
-	&.mini {
-		padding: 16px;
-		--size: 32px;
-	}
-
-	> .ring {
-		position: relative;
-		display: inline-block;
-		vertical-align: middle;
-
-		&:before,
-		&:after {
-			content: " ";
-			display: block;
-			box-sizing: border-box;
-			width: var(--size);
-			height: var(--size);
-			border-radius: 50%;
-			border: solid 4px;
-		}
-
-		&:before {
-			border-color: currentColor;
-			opacity: 0.3;
-		}
-
-		&:after {
-			position: absolute;
-			top: 0;
-			border-color: currentColor transparent transparent transparent;
-			animation: ring 0.5s linear infinite;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/global/misskey-flavored-markdown.vue b/src/client/components/global/misskey-flavored-markdown.vue
deleted file mode 100644
index c4f75bee93..0000000000
--- a/src/client/components/global/misskey-flavored-markdown.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<template>
-<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MfmCore from '@client/components/mfm';
-
-export default defineComponent({
-	components: {
-		MfmCore
-	}
-});
-</script>
-
-<style lang="scss">
-._mfm_blur_ {
-	filter: blur(6px);
-	transition: filter 0.3s;
-
-	&:hover {
-		filter: blur(0px);
-	}
-}
-
-@keyframes mfm-spin {
-	0% { transform: rotate(0deg); }
-	100% { transform: rotate(360deg); }
-}
-
-@keyframes mfm-spinX {
-	0% { transform: perspective(128px) rotateX(0deg); }
-	100% { transform: perspective(128px) rotateX(360deg); }
-}
-
-@keyframes mfm-spinY {
-	0% { transform: perspective(128px) rotateY(0deg); }
-	100% { transform: perspective(128px) rotateY(360deg); }
-}
-
-@keyframes mfm-jump {
-	0% { transform: translateY(0); }
-	25% { transform: translateY(-16px); }
-	50% { transform: translateY(0); }
-	75% { transform: translateY(-8px); }
-	100% { transform: translateY(0); }
-}
-
-@keyframes mfm-bounce {
-	0% { transform: translateY(0) scale(1, 1); }
-	25% { transform: translateY(-16px) scale(1, 1); }
-	50% { transform: translateY(0) scale(1, 1); }
-	75% { transform: translateY(0) scale(1.5, 0.75); }
-	100% { transform: translateY(0) scale(1, 1); }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-twitch {
-	0% { transform: translate(7px, -2px) }
-	5% { transform: translate(-3px, 1px) }
-	10% { transform: translate(-7px, -1px) }
-	15% { transform: translate(0px, -1px) }
-	20% { transform: translate(-8px, 6px) }
-	25% { transform: translate(-4px, -3px) }
-	30% { transform: translate(-4px, -6px) }
-	35% { transform: translate(-8px, -8px) }
-	40% { transform: translate(4px, 6px) }
-	45% { transform: translate(-3px, 1px) }
-	50% { transform: translate(2px, -10px) }
-	55% { transform: translate(-7px, 0px) }
-	60% { transform: translate(-2px, 4px) }
-	65% { transform: translate(3px, -8px) }
-	70% { transform: translate(6px, 7px) }
-	75% { transform: translate(-7px, -2px) }
-	80% { transform: translate(-7px, -8px) }
-	85% { transform: translate(9px, 3px) }
-	90% { transform: translate(-3px, -2px) }
-	95% { transform: translate(-10px, 2px) }
-	100% { transform: translate(-2px, -6px) }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-shake {
-	0% { transform: translate(-3px, -1px) rotate(-8deg) }
-	5% { transform: translate(0px, -1px) rotate(-10deg) }
-	10% { transform: translate(1px, -3px) rotate(0deg) }
-	15% { transform: translate(1px, 1px) rotate(11deg) }
-	20% { transform: translate(-2px, 1px) rotate(1deg) }
-	25% { transform: translate(-1px, -2px) rotate(-2deg) }
-	30% { transform: translate(-1px, 2px) rotate(-3deg) }
-	35% { transform: translate(2px, 1px) rotate(6deg) }
-	40% { transform: translate(-2px, -3px) rotate(-9deg) }
-	45% { transform: translate(0px, -1px) rotate(-12deg) }
-	50% { transform: translate(1px, 2px) rotate(10deg) }
-	55% { transform: translate(0px, -3px) rotate(8deg) }
-	60% { transform: translate(1px, -1px) rotate(8deg) }
-	65% { transform: translate(0px, -1px) rotate(-7deg) }
-	70% { transform: translate(-1px, -3px) rotate(6deg) }
-	75% { transform: translate(0px, -2px) rotate(4deg) }
-	80% { transform: translate(-2px, -1px) rotate(3deg) }
-	85% { transform: translate(1px, -3px) rotate(-10deg) }
-	90% { transform: translate(1px, 0px) rotate(3deg) }
-	95% { transform: translate(-2px, 0px) rotate(-3deg) }
-	100% { transform: translate(2px, 1px) rotate(2deg) }
-}
-
-@keyframes mfm-rubberBand {
-	from { transform: scale3d(1, 1, 1); }
-	30% { transform: scale3d(1.25, 0.75, 1); }
-	40% { transform: scale3d(0.75, 1.25, 1); }
-	50% { transform: scale3d(1.15, 0.85, 1); }
-	65% { transform: scale3d(0.95, 1.05, 1); }
-	75% { transform: scale3d(1.05, 0.95, 1); }
-	to { transform: scale3d(1, 1, 1); }
-}
-
-@keyframes mfm-rainbow {
-	0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
-	100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
-}
-</style>
-
-<style lang="scss" scoped>
-.havbbuyv {
-	white-space: pre-wrap;
-
-	&.nowrap {
-		white-space: pre;
-		word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
-
-	::v-deep(.quote) {
-		display: block;
-		margin: 8px;
-		padding: 6px 0 6px 12px;
-		color: var(--fg);
-		border-left: solid 3px var(--fg);
-		opacity: 0.7;
-	}
-
-	::v-deep(pre) {
-		font-size: 0.8em;
-	}
-
-	> ::v-deep(code) {
-		font-size: 0.8em;
-		word-break: break-all;
-		padding: 4px 6px;
-	}
-}
-</style>
diff --git a/src/client/components/global/spacer.vue b/src/client/components/global/spacer.vue
deleted file mode 100644
index 1129d54c71..0000000000
--- a/src/client/components/global/spacer.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<div ref="root" :class="$style.root" :style="{ padding: margin + 'px' }">
-	<div ref="content" :class="$style.content">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
-
-export default defineComponent({
-	props: {
-		contentMax: {
-			type: Number,
-			required: false,
-			default: null,
-		}
-	},
-
-	setup(props, context) {
-		let ro: ResizeObserver;
-		const root = ref<HTMLElement>(null);
-		const content = ref<HTMLElement>(null);
-		const margin = ref(0);
-		const adjust = (rect: { width: number; height: number; }) => {
-			if (rect.width > (props.contentMax || 500)) {
-				margin.value = 32;
-			} else {
-				margin.value = 12;
-			}
-		};
-
-		onMounted(() => {
-			ro = new ResizeObserver((entries) => {
-				/* iOSが対応していない
-				adjust({
-					width: entries[0].borderBoxSize[0].inlineSize,
-					height: entries[0].borderBoxSize[0].blockSize,
-				});
-				*/
-				adjust({
-					width: root.value.offsetWidth,
-					height: root.value.offsetHeight,
-				});
-			});
-			ro.observe(root.value);
-
-			if (props.contentMax) {
-				content.value.style.maxWidth = `${props.contentMax}px`;
-			}
-		});
-
-		onUnmounted(() => {
-			ro.disconnect();
-		});
-
-		return {
-			root,
-			content,
-			margin,
-		};
-	},
-});
-</script>
-
-<style lang="scss" module>
-.root {
-	box-sizing: border-box;
-	width: 100%;
-}
-
-.content {
-	margin: 0 auto;
-}
-</style>
diff --git a/src/client/components/global/sticky-container.vue b/src/client/components/global/sticky-container.vue
deleted file mode 100644
index 859b2c1d73..0000000000
--- a/src/client/components/global/sticky-container.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<template>
-<div ref="rootEl">
-	<slot name="header"></slot>
-	<div ref="bodyEl">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
-
-export default defineComponent({
-	props: {
-		autoSticky: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-
-	setup(props, context) {
-		const rootEl = ref<HTMLElement>(null);
-		const bodyEl = ref<HTMLElement>(null);
-
-		const calc = () => {
-			const currentStickyTop = getComputedStyle(rootEl.value).getPropertyValue('--stickyTop') || '0px';
-
-			const header = rootEl.value.children[0];
-			if (header === bodyEl.value) {
-				bodyEl.value.style.setProperty('--stickyTop', currentStickyTop);
-			} else {
-				bodyEl.value.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
-
-				if (props.autoSticky) {
-					header.style.setProperty('--stickyTop', currentStickyTop);
-					header.style.position = 'sticky';
-					header.style.top = 'var(--stickyTop)';
-					header.style.zIndex = '1';
-				}
-			}
-		};
-
-		onMounted(() => {
-			calc();
-
-			const observer = new MutationObserver(() => {
-				setTimeout(() => {
-					calc();
-				}, 100);
-			});
-
-			observer.observe(rootEl.value, {
-				attributes: false,
-				childList: true,
-				subtree: false,
-			});
-
-			onUnmounted(() => {
-				observer.disconnect();
-			});
-		});
-
-		return {
-			rootEl,
-			bodyEl,
-		};
-	},
-});
-</script>
-
-<style lang="scss" module>
-
-</style>
diff --git a/src/client/components/global/time.vue b/src/client/components/global/time.vue
deleted file mode 100644
index 6a330a2307..0000000000
--- a/src/client/components/global/time.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-<time :title="absolute">
-	<template v-if="mode == 'relative'">{{ relative }}</template>
-	<template v-else-if="mode == 'absolute'">{{ absolute }}</template>
-	<template v-else-if="mode == 'detail'">{{ absolute }} ({{ relative }})</template>
-</time>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		time: {
-			type: [Date, String],
-			required: true
-		},
-		mode: {
-			type: String,
-			default: 'relative'
-		}
-	},
-	data() {
-		return {
-			tickId: null,
-			now: new Date()
-		};
-	},
-	computed: {
-		_time(): Date {
-			return typeof this.time == 'string' ? new Date(this.time) : this.time;
-		},
-		absolute(): string {
-			return this._time.toLocaleString();
-		},
-		relative(): string {
-			const time = this._time;
-			const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
-			return (
-				ago >= 31536000 ? this.$t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
-				ago >= 2592000  ? this.$t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
-				ago >= 604800   ? this.$t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
-				ago >= 86400    ? this.$t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
-				ago >= 3600     ? this.$t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
-				ago >= 60       ? this.$t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
-				ago >= 10       ? this.$t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
-				ago >= -1       ? this.$ts._ago.justNow :
-				ago <  -1       ? this.$ts._ago.future :
-				this.$ts._ago.unknown);
-		}
-	},
-	created() {
-		if (this.mode == 'relative' || this.mode == 'detail') {
-			this.tickId = window.requestAnimationFrame(this.tick);
-		}
-	},
-	unmounted() {
-		if (this.mode === 'relative' || this.mode === 'detail') {
-			window.clearTimeout(this.tickId);
-		}
-	},
-	methods: {
-		tick() {
-			// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
-			this.now = new Date();
-
-			this.tickId = setTimeout(() => {
-				window.requestAnimationFrame(this.tick);
-			}, 10000);
-		}
-	}
-});
-</script>
diff --git a/src/client/components/global/url.vue b/src/client/components/global/url.vue
deleted file mode 100644
index 218729882d..0000000000
--- a/src/client/components/global/url.vue
+++ /dev/null
@@ -1,142 +0,0 @@
-<template>
-<component :is="self ? 'MkA' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
-	@mouseover="onMouseover"
-	@mouseleave="onMouseleave"
-	@contextmenu.stop="() => {}"
->
-	<template v-if="!self">
-		<span class="schema">{{ schema }}//</span>
-		<span class="hostname">{{ hostname }}</span>
-		<span class="port" v-if="port != ''">:{{ port }}</span>
-	</template>
-	<template v-if="pathname === '/' && self">
-		<span class="self">{{ hostname }}</span>
-	</template>
-	<span class="pathname" v-if="pathname != ''">{{ self ? pathname.substr(1) : pathname }}</span>
-	<span class="query">{{ query }}</span>
-	<span class="hash">{{ hash }}</span>
-	<i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i>
-</component>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { toUnicode as decodePunycode } from 'punycode/';
-import { url as local } from '@client/config';
-import { isDeviceTouch } from '@client/scripts/is-device-touch';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		url: {
-			type: String,
-			required: true,
-		},
-		rel: {
-			type: String,
-			required: false,
-		}
-	},
-	data() {
-		const self = this.url.startsWith(local);
-		return {
-			local,
-			schema: null as string | null,
-			hostname: null as string | null,
-			port: null as string | null,
-			pathname: null as string | null,
-			query: null as string | null,
-			hash: null as string | null,
-			self: self,
-			attr: self ? 'to' : 'href',
-			target: self ? null : '_blank',
-			showTimer: null,
-			hideTimer: null,
-			checkTimer: null,
-			close: null,
-		};
-	},
-	created() {
-		const url = new URL(this.url);
-		this.schema = url.protocol;
-		this.hostname = decodePunycode(url.hostname);
-		this.port = url.port;
-		this.pathname = decodeURIComponent(url.pathname);
-		this.query = decodeURIComponent(url.search);
-		this.hash = decodeURIComponent(url.hash);
-	},
-	methods: {
-		async showPreview() {
-			if (!document.body.contains(this.$el)) return;
-			if (this.close) return;
-
-			const { dispose } = await os.popup(import('@client/components/url-preview-popup.vue'), {
-				url: this.url,
-				source: this.$el
-			});
-
-			this.close = () => {
-				dispose();
-			};
-
-			this.checkTimer = setInterval(() => {
-				if (!document.body.contains(this.$el)) this.closePreview();
-			}, 1000);
-		},
-		closePreview() {
-			if (this.close) {
-				clearInterval(this.checkTimer);
-				this.close();
-				this.close = null;
-			}
-		},
-		onMouseover() {
-			if (isDeviceTouch) return;
-			clearTimeout(this.showTimer);
-			clearTimeout(this.hideTimer);
-			this.showTimer = setTimeout(this.showPreview, 500);
-		},
-		onMouseleave() {
-			if (isDeviceTouch) return;
-			clearTimeout(this.showTimer);
-			clearTimeout(this.hideTimer);
-			this.hideTimer = setTimeout(this.closePreview, 500);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ieqqeuvs {
-	word-break: break-all;
-
-	> .icon {
-		padding-left: 2px;
-		font-size: .9em;
-	}
-
-	> .self {
-		font-weight: bold;
-	}
-
-	> .schema {
-		opacity: 0.5;
-	}
-
-	> .hostname {
-		font-weight: bold;
-	}
-
-	> .pathname {
-		opacity: 0.8;
-	}
-
-	> .query {
-		opacity: 0.5;
-	}
-
-	> .hash {
-		font-style: italic;
-	}
-}
-</style>
diff --git a/src/client/components/global/user-name.vue b/src/client/components/global/user-name.vue
deleted file mode 100644
index bc93a8ea30..0000000000
--- a/src/client/components/global/user-name.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<template>
-<Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		nowrap: {
-			type: Boolean,
-			default: true
-		},
-	}
-});
-</script>
diff --git a/src/client/components/google.vue b/src/client/components/google.vue
deleted file mode 100644
index be724f038d..0000000000
--- a/src/client/components/google.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<template>
-<div class="mk-google">
-	<input type="search" v-model="query" :placeholder="q">
-	<button @click="search"><i class="fas fa-search"></i> {{ $ts.search }}</button>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		q: {
-			type: String,
-			required: true,
-		}
-	},
-	data() {
-		return {
-			query: null,
-		};
-	},
-	mounted() {
-		this.query = this.q;
-	},
-	methods: {
-		search() {
-			window.open(`https://www.google.com/search?q=${this.query}`, '_blank');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-google {
-	display: flex;
-	margin: 8px 0;
-
-	> input {
-		flex-shrink: 1;
-		padding: 10px;
-		width: 100%;
-		height: 40px;
-		font-size: 16px;
-		border: solid 1px var(--divider);
-		border-radius: 4px 0 0 4px;
-		-webkit-appearance: textfield;
-	}
-
-	> button {
-		flex-shrink: 0;
-		margin: 0;
-		padding: 0 16px;
-		border: solid 1px var(--divider);
-		border-left: none;
-		border-radius: 0 4px 4px 0;
-
-		&:active {
-			box-shadow: 0 2px 4px rgba(#000, 0.15) inset;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/image-viewer.vue b/src/client/components/image-viewer.vue
deleted file mode 100644
index 7701ae926f..0000000000
--- a/src/client/components/image-viewer.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
-	<div class="xubzgfga">
-		<header>{{ image.name }}</header>
-		<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
-		<footer>
-			<span>{{ image.type }}</span>
-			<span>{{ bytes(image.size) }}</span>
-			<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
-		</footer>
-	</div>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-import MkModal from '@client/components/ui/modal.vue';
-
-export default defineComponent({
-	components: {
-		MkModal,
-	},
-
-	props: {
-		image: {
-			type: Object,
-			required: true
-		},
-	},
-
-	emits: ['closed'],
-
-	methods: {
-		bytes,
-		number,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xubzgfga {
-	display: flex;
-	flex-direction: column;
-	height: 100%;
-
-	> header,
-	> footer {
-		align-self: center;
-		display: inline-block;
-		padding: 6px 9px;
-		font-size: 90%;
-		background: rgba(0, 0, 0, 0.5);
-		border-radius: 6px;
-		color: #fff;
-	}
-
-	> header {
-		margin-bottom: 8px;
-		opacity: 0.9;
-	}
-
-	> img {
-		display: block;
-		flex: 1;
-		min-height: 0;
-		object-fit: contain;
-		width: 100%;
-		cursor: zoom-out;
-		image-orientation: from-image;
-	}
-
-	> footer {
-		margin-top: 8px;
-		opacity: 0.8;
-
-		> span + span {
-			margin-left: 0.5em;
-			padding-left: 0.5em;
-			border-left: solid 1px rgba(255, 255, 255, 0.5);
-		}
-	}
-}
-</style>
diff --git a/src/client/components/img-with-blurhash.vue b/src/client/components/img-with-blurhash.vue
deleted file mode 100644
index 7e80b00208..0000000000
--- a/src/client/components/img-with-blurhash.vue
+++ /dev/null
@@ -1,100 +0,0 @@
-<template>
-<div class="xubzgfgb" :class="{ cover }" :title="title">
-	<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/>
-	<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { decode } from 'blurhash';
-
-export default defineComponent({
-	props: {
-		src: {
-			type: String,
-			required: false,
-			default: null
-		},
-		hash: {
-			type: String,
-			required: true
-		},
-		alt: {
-			type: String,
-			required: false,
-			default: '',
-		},
-		title: {
-			type: String,
-			required: false,
-			default: null,
-		},
-		size: {
-			type: Number,
-			required: false,
-			default: 64
-		},
-		cover: {
-			type: Boolean,
-			required: false,
-			default: true,
-		}
-	},
-
-	data() {
-		return {
-			loaded: false,
-		};
-	},
-
-	mounted() {
-		this.draw();
-	},
-
-	methods: {
-		draw() {
-			if (this.hash == null) return;
-			const pixels = decode(this.hash, this.size, this.size);
-			const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
-			const imageData = ctx!.createImageData(this.size, this.size);
-			imageData.data.set(pixels);
-			ctx!.putImageData(imageData, 0, 0);
-		},
-
-		onLoad() {
-			this.loaded = true;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xubzgfgb {
-	position: relative;
-	width: 100%;
-	height: 100%;
-
-	> canvas,
-	> img {
-		display: block;
-		width: 100%;
-		height: 100%;
-	}
-
-	> canvas {
-		position: absolute;
-		object-fit: cover;
-	}
-
-	> img {
-		object-fit: contain;
-	}
-
-	&.cover {
-		> img {
-			object-fit: cover;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/index.ts b/src/client/components/index.ts
deleted file mode 100644
index 2340b228f8..0000000000
--- a/src/client/components/index.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { App } from 'vue';
-
-import mfm from './global/misskey-flavored-markdown.vue';
-import a from './global/a.vue';
-import acct from './global/acct.vue';
-import avatar from './global/avatar.vue';
-import emoji from './global/emoji.vue';
-import userName from './global/user-name.vue';
-import ellipsis from './global/ellipsis.vue';
-import time from './global/time.vue';
-import url from './global/url.vue';
-import i18n from './global/i18n';
-import loading from './global/loading.vue';
-import error from './global/error.vue';
-import ad from './global/ad.vue';
-import header from './global/header.vue';
-import spacer from './global/spacer.vue';
-import stickyContainer from './global/sticky-container.vue';
-
-export default function(app: App) {
-	app.component('I18n', i18n);
-	app.component('Mfm', mfm);
-	app.component('MkA', a);
-	app.component('MkAcct', acct);
-	app.component('MkAvatar', avatar);
-	app.component('MkEmoji', emoji);
-	app.component('MkUserName', userName);
-	app.component('MkEllipsis', ellipsis);
-	app.component('MkTime', time);
-	app.component('MkUrl', url);
-	app.component('MkLoading', loading);
-	app.component('MkError', error);
-	app.component('MkAd', ad);
-	app.component('MkHeader', header);
-	app.component('MkSpacer', spacer);
-	app.component('MkStickyContainer', stickyContainer);
-}
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
deleted file mode 100644
index fd0b75609f..0000000000
--- a/src/client/components/instance-stats.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-<div class="zbcjwnqg" style="margin-top: -8px;">
-	<div class="selects" style="display: flex;">
-		<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
-			<optgroup :label="$ts.federation">
-				<option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option>
-				<option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option>
-			</optgroup>
-			<optgroup :label="$ts.users">
-				<option value="users">{{ $ts._charts.usersIncDec }}</option>
-				<option value="users-total">{{ $ts._charts.usersTotal }}</option>
-				<option value="active-users">{{ $ts._charts.activeUsers }}</option>
-			</optgroup>
-			<optgroup :label="$ts.notes">
-				<option value="notes">{{ $ts._charts.notesIncDec }}</option>
-				<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
-				<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
-				<option value="notes-total">{{ $ts._charts.notesTotal }}</option>
-			</optgroup>
-			<optgroup :label="$ts.drive">
-				<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
-				<option value="drive-files-total">{{ $ts._charts.filesTotal }}</option>
-				<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
-				<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
-			</optgroup>
-		</MkSelect>
-		<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
-			<option value="hour">{{ $ts.perHour }}</option>
-			<option value="day">{{ $ts.perDay }}</option>
-		</MkSelect>
-	</div>
-	<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, ref, watch } from 'vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkChart from '@client/components/chart.vue';
-import * as os from '@client/os';
-import { defaultStore } from '@client/store';
-
-export default defineComponent({
-	components: {
-		MkSelect,
-		MkChart,
-	},
-
-	props: {
-		chartLimit: {
-			type: Number,
-			required: false,
-			default: 90
-		},
-		detailed: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	setup() {
-		const chartSpan = ref<'hour' | 'day'>('hour');
-		const chartSrc = ref('notes');
-
-		return {
-			chartSrc,
-			chartSpan,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.zbcjwnqg {
-	> .selects {
-		padding: 8px 16px 0 16px;
-	}
-}
-</style>
diff --git a/src/client/components/instance-ticker.vue b/src/client/components/instance-ticker.vue
deleted file mode 100644
index 5674174558..0000000000
--- a/src/client/components/instance-ticker.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="hpaizdrt" :style="bg">
-	<img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/>
-	<span class="name">{{ info.name }}</span>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { instanceName } from '@client/config';
-
-export default defineComponent({
-	props: {
-		instance: {
-			type: Object,
-			required: false
-		},
-	},
-
-	data() {
-		return {
-			info: this.instance || {
-				faviconUrl: '/favicon.ico',
-				name: instanceName,
-				themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
-			}
-		}
-	},
-
-	computed: {
-		bg(): any {
-			const themeColor = this.info.themeColor || '#777777';
-			return {
-				background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
-			};
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hpaizdrt {
-	$height: 1.1rem;
-
-	height: $height;
-	border-radius: 4px 0 0 4px;
-	overflow: hidden;
-	color: #fff;
-
-	> .icon {
-		height: 100%;
-	}
-
-	> .name {
-		margin-left: 4px;
-		line-height: $height;
-		font-size: 0.9em;
-		vertical-align: top;
-		font-weight: bold;
-	}
-}
-</style>
diff --git a/src/client/components/launch-pad.vue b/src/client/components/launch-pad.vue
deleted file mode 100644
index 9da62f1e0b..0000000000
--- a/src/client/components/launch-pad.vue
+++ /dev/null
@@ -1,152 +0,0 @@
-<template>
-<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
-	<div class="szkkfdyq _popup">
-		<div class="main">
-			<template v-for="item in items">
-				<button v-if="item.action" class="_button" @click="$event => { item.action($event); close(); }" v-click-anime>
-					<i class="icon" :class="item.icon"></i>
-					<div class="text">{{ item.text }}</div>
-					<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-				</button>
-				<MkA v-else :to="item.to" @click.passive="close()" v-click-anime>
-					<i class="icon" :class="item.icon"></i>
-					<div class="text">{{ item.text }}</div>
-					<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-				</MkA>
-			</template>
-		</div>
-		<div class="sub">
-			<a href="https://misskey-hub.net/help.html" target="_blank" @click.passive="close()" v-click-anime>
-				<i class="fas fa-question-circle icon"></i>
-				<div class="text">{{ $ts.help }}</div>
-			</a>
-			<MkA to="/about" @click.passive="close()" v-click-anime>
-				<i class="fas fa-info-circle icon"></i>
-				<div class="text">{{ $t('aboutX', { x: instanceName }) }}</div>
-			</MkA>
-			<MkA to="/about-misskey" @click.passive="close()" v-click-anime>
-				<img src="/static-assets/favicon.png" class="icon"/>
-				<div class="text">{{ $ts.aboutMisskey }}</div>
-			</MkA>
-		</div>
-	</div>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkModal from '@client/components/ui/modal.vue';
-import { menuDef } from '@client/menu';
-import { instanceName } from '@client/config';
-
-export default defineComponent({
-	components: {
-		MkModal,
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			menuDef: menuDef,
-			items: [],
-			instanceName,
-		};
-	},
-
-	computed: {
-		menu(): string[] {
-			return this.$store.state.menu;
-		},
-	},
-
-	created() {
-		this.items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
-			type: def.to ? 'link' : 'button',
-			text: this.$ts[def.title],
-			icon: def.icon,
-			to: def.to,
-			action: def.action,
-			indicate: def.indicated,
-		}));
-	},
-
-	methods: {
-		close() {
-			this.$refs.modal.close();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.szkkfdyq {
-	width: 100%;
-	max-height: 100%;
-	max-width: 800px;
-	padding: 32px;
-	box-sizing: border-box;
-	overflow: auto;
-	text-align: center;
-	border-radius: 16px;
-
-	@media (max-width: 500px) {
-		padding: 16px;
-	}
-	
-	> .main, > .sub {
-		> * {
-			position: relative;
-			display: inline-flex;
-			flex-direction: column;
-			align-items: center;
-			justify-content: center;
-			vertical-align: bottom;
-			width: 128px;
-			height: 128px;
-			border-radius: var(--radius);
-
-			@media (max-width: 500px) {
-				width: 100px;
-				height: 100px;
-			}
-
-			&:hover {
-				background: rgba(0, 0, 0, 0.05);
-				text-decoration: none;
-			}
-
-			> .icon {
-				font-size: 26px;
-				height: 32px;
-			}
-
-			> .text {
-				margin-top: 8px;
-				font-size: 0.9em;
-				line-height: 1.5em;
-			}
-
-			> .indicator {
-				position: absolute;
-				top: 32px;
-				left: 32px;
-				color: var(--indicator);
-				font-size: 8px;
-				animation: blink 1s infinite;
-
-				@media (max-width: 500px) {
-					top: 16px;
-					left: 16px;
-				}
-			}
-		}
-	}
-
-	> .sub {
-		margin-top: 8px;
-		padding-top: 8px;
-		border-top: solid 0.5px var(--divider);
-	}
-}
-</style>
diff --git a/src/client/components/link.vue b/src/client/components/link.vue
deleted file mode 100644
index a887410331..0000000000
--- a/src/client/components/link.vue
+++ /dev/null
@@ -1,92 +0,0 @@
-<template>
-<component :is="self ? 'MkA' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
-	@mouseover="onMouseover"
-	@mouseleave="onMouseleave"
-	:title="url"
->
-	<slot></slot>
-	<i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i>
-</component>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { url as local } from '@client/config';
-import { isDeviceTouch } from '@client/scripts/is-device-touch';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		url: {
-			type: String,
-			required: true,
-		},
-		rel: {
-			type: String,
-			required: false,
-		}
-	},
-	data() {
-		const self = this.url.startsWith(local);
-		return {
-			local,
-			self: self,
-			attr: self ? 'to' : 'href',
-			target: self ? null : '_blank',
-			showTimer: null,
-			hideTimer: null,
-			checkTimer: null,
-			close: null,
-		};
-	},
-	methods: {
-		async showPreview() {
-			if (!document.body.contains(this.$el)) return;
-			if (this.close) return;
-
-			const { dispose } = await os.popup(import('@client/components/url-preview-popup.vue'), {
-				url: this.url,
-				source: this.$el
-			});
-
-			this.close = () => {
-				dispose();
-			};
-
-			this.checkTimer = setInterval(() => {
-				if (!document.body.contains(this.$el)) this.closePreview();
-			}, 1000);
-		},
-		closePreview() {
-			if (this.close) {
-				clearInterval(this.checkTimer);
-				this.close();
-				this.close = null;
-			}
-		},
-		onMouseover() {
-			if (isDeviceTouch) return;
-			clearTimeout(this.showTimer);
-			clearTimeout(this.hideTimer);
-			this.showTimer = setTimeout(this.showPreview, 500);
-		},
-		onMouseleave() {
-			if (isDeviceTouch) return;
-			clearTimeout(this.showTimer);
-			clearTimeout(this.hideTimer);
-			this.hideTimer = setTimeout(this.closePreview, 500);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xlcxczvw {
-	word-break: break-all;
-
-	> .icon {
-		padding-left: 2px;
-		font-size: .9em;
-	}
-}
-</style>
diff --git a/src/client/components/media-banner.vue b/src/client/components/media-banner.vue
deleted file mode 100644
index 34065557bf..0000000000
--- a/src/client/components/media-banner.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<template>
-<div class="mk-media-banner">
-	<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
-		<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
-		<b>{{ $ts.sensitive }}</b>
-		<span>{{ $ts.clickToShow }}</span>
-	</div>
-	<div class="audio" v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'">
-		<audio class="audio"
-			:src="media.url"
-			:title="media.name"
-			controls
-			ref="audio"
-			@volumechange="volumechange"
-			preload="metadata" />
-	</div>
-	<a class="download" v-else
-		:href="media.url"
-		:title="media.name"
-		:download="media.name"
-	>
-		<span class="icon"><i class="fas fa-download"></i></span>
-		<b>{{ media.name }}</b>
-	</a>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-
-export default defineComponent({
-	props: {
-		media: {
-			type: Object,
-			required: true
-		}
-	},
-	data() {
-		return {
-			hide: true,
-		};
-	},
-	mounted() {
-		const audioTag = this.$refs.audio as HTMLAudioElement;
-		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
-	},
-	methods: {
-		volumechange() {
-			const audioTag = this.$refs.audio as HTMLAudioElement;
-			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
-		},
-	},
-})
-</script>
-
-<style lang="scss" scoped>
-.mk-media-banner {
-	width: 100%;
-	border-radius: 4px;
-	margin-top: 4px;
-	overflow: hidden;
-
-	> .download,
-	> .sensitive {
-		display: flex;
-		align-items: center;
-		font-size: 12px;
-		padding: 8px 12px;
-		white-space: nowrap;
-
-		> * {
-			display: block;
-		}
-
-		> b {
-			overflow: hidden;
-			text-overflow: ellipsis;
-		}
-
-		> *:not(:last-child) {
-			margin-right: .2em;
-		}
-
-		> .icon {
-			font-size: 1.6em;
-		}
-	}
-
-	> .download {
-		background: var(--noteAttachedFile);
-	}
-
-	> .sensitive {
-		background: #111;
-		color: #fff;
-	}
-
-	> .audio {
-		.audio {
-			display: block;
-			width: 100%;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/media-caption.vue b/src/client/components/media-caption.vue
deleted file mode 100644
index b35b101d06..0000000000
--- a/src/client/components/media-caption.vue
+++ /dev/null
@@ -1,260 +0,0 @@
-<template>
-	<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
-		<div class="container">
-			<div class="fullwidth top-caption">
-				<div class="mk-dialog">
-					<header>
-						<Mfm v-if="title" class="title" :text="title"/>
-						<span class="text-count" :class="{ over: remainingLength < 0 }">{{ remainingLength }}</span>
-					</header>
-					<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
-					<div class="buttons" v-if="(showOkButton || showCancelButton)">
-						<MkButton inline @click="ok" primary :disabled="remainingLength < 0">{{ $ts.ok }}</MkButton>
-						<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
-					</div>
-				</div>
-			</div>
-			<div class="hdrwpsaf fullwidth">
-				<header>{{ image.name }}</header>
-				<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
-				<footer>
-					<span>{{ image.type }}</span>
-					<span>{{ bytes(image.size) }}</span>
-					<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
-				</footer>
-			</div>
-		</div>
-	</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { length } from 'stringz';
-import MkModal from '@client/components/ui/modal.vue';
-import MkButton from '@client/components/ui/button.vue';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
-
-export default defineComponent({
-	components: {
-		MkModal,
-		MkButton,
-	},
-
-	props: {
-		image: {
-			type: Object,
-			required: true,
-		},
-		title: {
-			type: String,
-			required: false
-		},
-		input: {
-			required: true
-		},
-		showOkButton: {
-			type: Boolean,
-			default: true
-		},
-		showCancelButton: {
-			type: Boolean,
-			default: true
-		},
-		cancelableByBgClick: {
-			type: Boolean,
-			default: true
-		},
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			inputValue: this.input.default ? this.input.default : null
-		};
-	},
-
-	mounted() {
-		document.addEventListener('keydown', this.onKeydown);
-	},
-
-	beforeUnmount() {
-		document.removeEventListener('keydown', this.onKeydown);
-	},
-
-	computed: {
-		remainingLength(): number {
-			if (typeof this.inputValue != "string") return DB_MAX_IMAGE_COMMENT_LENGTH;
-			return DB_MAX_IMAGE_COMMENT_LENGTH - length(this.inputValue);
-		}
-	},
-
-	methods: {
-		bytes,
-		number,
-
-		done(canceled, result?) {
-			this.$emit('done', { canceled, result });
-			this.$refs.modal.close();
-		},
-
-		async ok() {
-			if (!this.showOkButton) return;
-
-			const result = this.inputValue;
-			this.done(false, result);
-		},
-
-		cancel() {
-			this.done(true);
-		},
-
-		onBgClick() {
-			if (this.cancelableByBgClick) {
-				this.cancel();
-			}
-		},
-
-		onKeydown(e) {
-			if (e.which === 27) { // ESC
-				this.cancel();
-			}
-		},
-
-		onInputKeydown(e) {
-			if (e.which === 13) { // Enter
-				if (e.ctrlKey) {
-					e.preventDefault();
-					e.stopPropagation();
-					this.ok();
-				}
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.container {
-	display: flex;
-	width: 100%;
-	height: 100%;
-	flex-direction: row;
-}
-@media (max-width: 850px) {
-	.container {
-		flex-direction: column;
-	}
-	.top-caption {
-		padding-bottom: 8px;
-	}
-}
-.fullwidth {
-	width: 100%;
-	margin: auto;
-}
-.mk-dialog {
-	position: relative;
-	padding: 32px;
-	min-width: 320px;
-	max-width: 480px;
-	box-sizing: border-box;
-	text-align: center;
-	background: var(--panel);
-	border-radius: var(--radius);
-	margin: auto;
-
-	> header {
-		margin: 0 0 8px 0;
-		position: relative;
-
-		> .title {
-			font-weight: bold;
-			font-size: 20px;
-		}
-
-		> .text-count {
-			opacity: 0.7;
-			position: absolute;
-			right: 0;
-		}
-	}
-
-	> .buttons {
-		margin-top: 16px;
-
-		> * {
-			margin: 0 8px;
-		}
-	}
-
-	> textarea {
-		display: block;
-		box-sizing: border-box;
-		padding: 0 24px;
-		margin: 0;
-		width: 100%;
-		font-size: 16px;
-		border: none;
-		border-radius: 0;
-		background: transparent;
-		color: var(--fg);
-		font-family: inherit;
-		max-width: 100%;
-		min-width: 100%;
-		min-height: 90px;
-
-		&:focus-visible {
-			outline: none;
-		}
-
-		&:disabled {
-			opacity: 0.5;
-		}
-	}
-}
-.hdrwpsaf {
-	display: flex;
-	flex-direction: column;
-	height: 100%;
-
-	> header,
-	> footer {
-		align-self: center;
-		display: inline-block;
-		padding: 6px 9px;
-		font-size: 90%;
-		background: rgba(0, 0, 0, 0.5);
-		border-radius: 6px;
-		color: #fff;
-	}
-
-	> header {
-		margin-bottom: 8px;
-		opacity: 0.9;
-	}
-
-	> img {
-		display: block;
-		flex: 1;
-		min-height: 0;
-		object-fit: contain;
-		width: 100%;
-		cursor: zoom-out;
-		image-orientation: from-image;
-	}
-
-	> footer {
-		margin-top: 8px;
-		opacity: 0.8;
-
-		> span + span {
-			margin-left: 0.5em;
-			padding-left: 0.5em;
-			border-left: solid 1px rgba(255, 255, 255, 0.5);
-		}
-	}
-}
-</style>
diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue
deleted file mode 100644
index fd5e0b5f9b..0000000000
--- a/src/client/components/media-image.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template>
-<div class="qjewsnkg" v-if="hide" @click="hide = false">
-	<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
-	<div class="text">
-		<div>
-			<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
-			<span>{{ $ts.clickToShow }}</span>
-		</div>
-	</div>
-</div>
-<div class="gqnyydlz" :style="{ background: color }" v-else>
-	<a
-		:href="image.url"
-		:title="image.name"
-	>
-		<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/>
-		<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
-	</a>
-	<i class="fas fa-eye-slash" @click="hide = true"></i>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { getStaticImageUrl } from '@client/scripts/get-static-image-url';
-import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash';
-import ImageViewer from './image-viewer.vue';
-import ImgWithBlurhash from '@client/components/img-with-blurhash.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		ImgWithBlurhash
-	},
-	props: {
-		image: {
-			type: Object,
-			required: true
-		},
-		raw: {
-			default: false
-		}
-	},
-	data() {
-		return {
-			hide: true,
-			color: null,
-		};
-	},
-	computed: {
-		url(): any {
-			let url = this.$store.state.disableShowingAnimatedImages
-				? getStaticImageUrl(this.image.thumbnailUrl)
-				: this.image.thumbnailUrl;
-
-			if (this.raw || this.$store.state.loadRawImages) {
-				url = this.image.url;
-			}
-
-			return url;
-		}
-	},
-	created() {
-		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
-		this.$watch('image', () => {
-			this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore');
-			if (this.image.blurhash) {
-				this.color = extractAvgColorFromBlurhash(this.image.blurhash);
-			}
-		}, {
-			deep: true,
-			immediate: true,
-		});
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.qjewsnkg {
-	position: relative;
-
-	> .bg {
-		filter: brightness(0.5);
-	}
-
-	> .text {
-		position: absolute;
-		left: 0;
-		top: 0;
-		width: 100%;
-		height: 100%;
-		z-index: 1;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-
-		> div {
-			display: table-cell;
-			text-align: center;
-			font-size: 0.8em;
-			color: #fff;
-
-			> * {
-				display: block;
-			}
-		}
-	}
-}
-
-.gqnyydlz {
-	position: relative;
-	border: solid 0.5px var(--divider);
-
-	> i {
-		display: block;
-		position: absolute;
-		border-radius: 6px;
-		background-color: var(--fg);
-		color: var(--accentLighten);
-		font-size: 14px;
-		opacity: .5;
-		padding: 3px 6px;
-		text-align: center;
-		cursor: pointer;
-		top: 12px;
-		right: 12px;
-	}
-
-	> a {
-		display: block;
-		cursor: zoom-in;
-		overflow: hidden;
-		width: 100%;
-		height: 100%;
-		background-position: center;
-		background-size: contain;
-		background-repeat: no-repeat;
-
-		> .gif {
-			background-color: var(--fg);
-			border-radius: 6px;
-			color: var(--accentLighten);
-			display: inline-block;
-			font-size: 14px;
-			font-weight: bold;
-			left: 12px;
-			opacity: .5;
-			padding: 0 6px;
-			text-align: center;
-			top: 12px;
-			pointer-events: none;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/media-list.vue b/src/client/components/media-list.vue
deleted file mode 100644
index c499525d84..0000000000
--- a/src/client/components/media-list.vue
+++ /dev/null
@@ -1,167 +0,0 @@
-<template>
-<div class="hoawjimk">
-	<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :media="media" :key="media.id"/>
-	<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
-		<div :data-count="mediaList.filter(media => previewable(media)).length" ref="gallery">
-			<template v-for="media in mediaList">
-				<XVideo :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
-				<XImage class="image" :data-id="media.id" :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
-			</template>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, PropType, ref } from 'vue';
-import * as misskey from 'misskey-js';
-import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js';
-import PhotoSwipe from 'photoswipe/dist/photoswipe.esm.js';
-import 'photoswipe/dist/photoswipe.css';
-import XBanner from './media-banner.vue';
-import XImage from './media-image.vue';
-import XVideo from './media-video.vue';
-import * as os from '@client/os';
-import { defaultStore } from '@client/store';
-
-export default defineComponent({
-	components: {
-		XBanner,
-		XImage,
-		XVideo,
-	},
-	props: {
-		mediaList: {
-			type: Array as PropType<misskey.entities.DriveFile[]>,
-			required: true,
-		},
-		raw: {
-			default: false
-		},
-	},
-	setup(props) {
-		const gallery = ref(null);
-
-		onMounted(() => {
-			const lightbox = new PhotoSwipeLightbox({
-				dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({
-					src: media.url,
-					w: media.properties.width,
-					h: media.properties.height,
-					alt: media.name,
-				})),
-				gallery: gallery.value,
-				children: '.image',
-				thumbSelector: '.image',
-				pswpModule: PhotoSwipe
-			});
-
-			lightbox.on('itemData', (e) => {
-				const { itemData } = e;
-
-				// element is children
-				const { element } = itemData;
-
-				const id = element.dataset.id;
-				const file = props.mediaList.find(media => media.id === id);
-
-				itemData.src = file.url;
-				itemData.w = Number(file.properties.width);
-				itemData.h = Number(file.properties.height);
-				itemData.msrc = file.thumbnailUrl;
-				itemData.thumbCropped = true;
-			});
-
-			lightbox.init();
-		});
-
-		const previewable = (file: misskey.entities.DriveFile): boolean => {
-			return file.type.startsWith('video') || file.type.startsWith('image');
-		};
-
-		return {
-			previewable,
-			gallery,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.hoawjimk {
-	> .gird-container {
-		position: relative;
-		width: 100%;
-		margin-top: 4px;
-
-		&:before {
-			content: '';
-			display: block;
-			padding-top: 56.25% // 16:9;
-		}
-
-		> div {
-			position: absolute;
-			top: 0;
-			right: 0;
-			bottom: 0;
-			left: 0;
-			display: grid;
-			grid-gap: 4px;
-
-			> * {
-				overflow: hidden;
-				border-radius: 6px;
-			}
-
-			&[data-count="1"] {
-				grid-template-rows: 1fr;
-			}
-
-			&[data-count="2"] {
-				grid-template-columns: 1fr 1fr;
-				grid-template-rows: 1fr;
-			}
-
-			&[data-count="3"] {
-				grid-template-columns: 1fr 0.5fr;
-				grid-template-rows: 1fr 1fr;
-
-				> *:nth-child(1) {
-					grid-row: 1 / 3;
-				}
-
-				> *:nth-child(3) {
-					grid-column: 2 / 3;
-					grid-row: 2 / 3;
-				}
-			}
-
-			&[data-count="4"] {
-				grid-template-columns: 1fr 1fr;
-				grid-template-rows: 1fr 1fr;
-			}
-
-			> *:nth-child(1) {
-				grid-column: 1 / 2;
-				grid-row: 1 / 2;
-			}
-
-			> *:nth-child(2) {
-				grid-column: 2 / 3;
-				grid-row: 1 / 2;
-			}
-
-			> *:nth-child(3) {
-				grid-column: 1 / 2;
-				grid-row: 2 / 3;
-			}
-
-			> *:nth-child(4) {
-				grid-column: 2 / 3;
-				grid-row: 2 / 3;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/media-video.vue b/src/client/components/media-video.vue
deleted file mode 100644
index 4d4a551653..0000000000
--- a/src/client/components/media-video.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-<template>
-<div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="hide" @click="hide = false">
-	<div>
-		<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
-		<span>{{ $ts.clickToShow }}</span>
-	</div>
-</div>
-<div class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else>
-	<video
-		:poster="video.thumbnailUrl"
-		:title="video.name"
-		preload="none"
-		controls
-		@contextmenu.stop
-	>
-		<source 
-			:src="video.url" 
-			:type="video.type"
-		>
-	</video>
-	<i class="fas fa-eye-slash" @click="hide = true"></i>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		video: {
-			type: Object,
-			required: true
-		}
-	},
-	data() {
-		return {
-			hide: true,
-		};
-	},
-	created() {
-		this.hide = (this.$store.state.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.nsfw !== 'ignore');
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.kkjnbbplepmiyuadieoenjgutgcmtsvu {
-	position: relative;
-
-	> i {
-		display: block;
-		position: absolute;
-		border-radius: 6px;
-		background-color: var(--fg);
-		color: var(--accentLighten);
-		font-size: 14px;
-		opacity: .5;
-		padding: 3px 6px;
-		text-align: center;
-		cursor: pointer;
-		top: 12px;
-		right: 12px;
-	}
-
-	> video {
-		display: flex;
-		justify-content: center;
-		align-items: center;
-
-		font-size: 3.5em;
-		overflow: hidden;
-		background-position: center;
-		background-size: cover;
-		width: 100%;
-		height: 100%;
-	}
-}
-
-.icozogqfvdetwohsdglrbswgrejoxbdj {
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	background: #111;
-	color: #fff;
-
-	> div {
-		display: table-cell;
-		text-align: center;
-		font-size: 12px;
-
-		> b {
-			display: block;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/mention.vue b/src/client/components/mention.vue
deleted file mode 100644
index 4c7030bf35..0000000000
--- a/src/client/components/mention.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-<template>
-<MkA v-if="url.startsWith('/')" class="ldlomzub" :class="{ isMe }" :to="url" v-user-preview="canonical" :style="{ background: bg }">
-	<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
-	<span class="main">
-		<span class="username">@{{ username }}</span>
-		<span class="host" v-if="(host != localHost) || $store.state.showFullAcct">@{{ toUnicode(host) }}</span>
-	</span>
-</MkA>
-<a v-else class="ldlomzub" :href="url" target="_blank" rel="noopener" :style="{ background: bg }">
-	<span class="main">
-		<span class="username">@{{ username }}</span>
-		<span class="host">@{{ toUnicode(host) }}</span>
-	</span>
-</a>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as tinycolor from 'tinycolor2';
-import { toUnicode } from 'punycode';
-import { host as localHost } from '@client/config';
-import { wellKnownServices } from '../../well-known-services';
-import { $i } from '@client/account';
-
-export default defineComponent({
-	props: {
-		username: {
-			type: String,
-			required: true
-		},
-		host: {
-			type: String,
-			required: true
-		}
-	},
-
-	setup(props) {
-		const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
-
-		const wellKnown = wellKnownServices.find(x => x[0] === props.host);
-		const url = wellKnown ? wellKnown[1](props.username) : `/${canonical}`;
-
-		const isMe = $i && (
-			`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
-		);
-
-		const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
-		bg.setAlpha(0.1);
-
-		return {
-			localHost,
-			isMe,
-			url,
-			canonical,
-			toUnicode,
-			bg: bg.toRgbString(),
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ldlomzub {
-	display: inline-block;
-	padding: 4px 8px 4px 4px;
-	border-radius: 999px;
-	color: var(--mention);
-
-	&.isMe {
-		color: var(--mentionMe);
-	}
-
-	> .icon {
-		width: 1.5em;
-		margin: 0 0.2em 0 0;
-		vertical-align: bottom;
-		border-radius: 100%;
-	}
-
-	> .main {
-		> .host {
-			opacity: 0.5;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
deleted file mode 100644
index ad6e711f6f..0000000000
--- a/src/client/components/mfm.ts
+++ /dev/null
@@ -1,321 +0,0 @@
-import { VNode, defineComponent, h } from 'vue';
-import * as mfm from 'mfm-js';
-import MkUrl from '@client/components/global/url.vue';
-import MkLink from '@client/components/link.vue';
-import MkMention from '@client/components/mention.vue';
-import MkEmoji from '@client/components/global/emoji.vue';
-import { concat } from '@client/../prelude/array';
-import MkFormula from '@client/components/formula.vue';
-import MkCode from '@client/components/code.vue';
-import MkGoogle from '@client/components/google.vue';
-import MkSparkle from '@client/components/sparkle.vue';
-import MkA from '@client/components/global/a.vue';
-import { host } from '@client/config';
-import { fnNameList } from '@/mfm/fn-name-list';
-
-export default defineComponent({
-	props: {
-		text: {
-			type: String,
-			required: true
-		},
-		plain: {
-			type: Boolean,
-			default: false
-		},
-		nowrap: {
-			type: Boolean,
-			default: false
-		},
-		author: {
-			type: Object,
-			default: null
-		},
-		i: {
-			type: Object,
-			default: null
-		},
-		customEmojis: {
-			required: false,
-		},
-		isNote: {
-			type: Boolean,
-			default: true
-		},
-	},
-
-	render() {
-		if (this.text == null || this.text == '') return;
-
-		const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text, { fnNameList });
-
-		const validTime = (t: string | null | undefined) => {
-			if (t == null) return null;
-			return t.match(/^[0-9.]+s$/) ? t : null;
-		};
-
-		const genEl = (ast: mfm.MfmNode[]) => concat(ast.map((token): VNode[] => {
-			switch (token.type) {
-				case 'text': {
-					const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
-
-					if (!this.plain) {
-						const res = [];
-						for (const t of text.split('\n')) {
-							res.push(h('br'));
-							res.push(t);
-						}
-						res.shift();
-						return res;
-					} else {
-						return [text.replace(/\n/g, ' ')];
-					}
-				}
-
-				case 'bold': {
-					return [h('b', genEl(token.children))];
-				}
-
-				case 'strike': {
-					return [h('del', genEl(token.children))];
-				}
-
-				case 'italic': {
-					return h('i', {
-						style: 'font-style: oblique;'
-					}, genEl(token.children));
-				}
-
-				case 'fn': {
-					// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
-					let style;
-					switch (token.props.name) {
-						case 'tada': {
-							style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : '');
-							break;
-						}
-						case 'jelly': {
-							const speed = validTime(token.props.args.speed) || '1s';
-							style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
-							break;
-						}
-						case 'twitch': {
-							const speed = validTime(token.props.args.speed) || '0.5s';
-							style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : '';
-							break;
-						}
-						case 'shake': {
-							const speed = validTime(token.props.args.speed) || '0.5s';
-							style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : '';
-							break;
-						}
-						case 'spin': {
-							const direction =
-								token.props.args.left ? 'reverse' :
-								token.props.args.alternate ? 'alternate' :
-								'normal';
-							const anime =
-								token.props.args.x ? 'mfm-spinX' :
-								token.props.args.y ? 'mfm-spinY' :
-								'mfm-spin';
-							const speed = validTime(token.props.args.speed) || '1.5s';
-							style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
-							break;
-						}
-						case 'jump': {
-							style = this.$store.state.animatedMfm ? 'animation: mfm-jump 0.75s linear infinite;' : '';
-							break;
-						}
-						case 'bounce': {
-							style = this.$store.state.animatedMfm ? 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;' : '';
-							break;
-						}
-						case 'flip': {
-							const transform =
-								(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
-								token.props.args.v ? 'scaleY(-1)' :
-								'scaleX(-1)';
-							style = `transform: ${transform};`;
-							break;
-						}
-						case 'x2': {
-							style = `font-size: 200%;`;
-							break;
-						}
-						case 'x3': {
-							style = `font-size: 400%;`;
-							break;
-						}
-						case 'x4': {
-							style = `font-size: 600%;`;
-							break;
-						}
-						case 'font': {
-							const family =
-								token.props.args.serif ? 'serif' :
-								token.props.args.monospace ? 'monospace' :
-								token.props.args.cursive ? 'cursive' :
-								token.props.args.fantasy ? 'fantasy' :
-								token.props.args.emoji ? 'emoji' :
-								token.props.args.math ? 'math' :
-								null;
-							if (family) style = `font-family: ${family};`;
-							break;
-						}
-						case 'blur': {
-							return h('span', {
-								class: '_mfm_blur_',
-							}, genEl(token.children));
-						}
-						case 'rainbow': {
-							style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : '';
-							break;
-						}
-						case 'sparkle': {
-							if (!this.$store.state.animatedMfm) {
-								return genEl(token.children);
-							}
-							let count = token.props.args.count ? parseInt(token.props.args.count) : 10;
-							if (count > 100) {
-								count = 100;
-							}
-							const speed = token.props.args.speed ? parseFloat(token.props.args.speed) : 1;
-							return h(MkSparkle, {
-								count, speed,
-							}, genEl(token.children));
-						}
-					}
-					if (style == null) {
-						return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
-					} else {
-						return h('span', {
-							style: 'display: inline-block;' + style,
-						}, genEl(token.children));
-					}
-				}
-
-				case 'small': {
-					return [h('small', {
-						style: 'opacity: 0.7;'
-					}, genEl(token.children))];
-				}
-
-				case 'center': {
-					return [h('div', {
-						style: 'text-align:center;'
-					}, genEl(token.children))];
-				}
-
-				case 'url': {
-					return [h(MkUrl, {
-						key: Math.random(),
-						url: token.props.url,
-						rel: 'nofollow noopener',
-					})];
-				}
-
-				case 'link': {
-					return [h(MkLink, {
-						key: Math.random(),
-						url: token.props.url,
-						rel: 'nofollow noopener',
-					}, genEl(token.children))];
-				}
-
-				case 'mention': {
-					return [h(MkMention, {
-						key: Math.random(),
-						host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
-						username: token.props.username
-					})];
-				}
-
-				case 'hashtag': {
-					return [h(MkA, {
-						key: Math.random(),
-						to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`,
-						style: 'color:var(--hashtag);'
-					}, `#${token.props.hashtag}`)];
-				}
-
-				case 'blockCode': {
-					return [h(MkCode, {
-						key: Math.random(),
-						code: token.props.code,
-						lang: token.props.lang,
-					})];
-				}
-
-				case 'inlineCode': {
-					return [h(MkCode, {
-						key: Math.random(),
-						code: token.props.code,
-						inline: true
-					})];
-				}
-
-				case 'quote': {
-					if (!this.nowrap) {
-						return [h('div', {
-							class: 'quote'
-						}, genEl(token.children))];
-					} else {
-						return [h('span', {
-							class: 'quote'
-						}, genEl(token.children))];
-					}
-				}
-
-				case 'emojiCode': {
-					return [h(MkEmoji, {
-						key: Math.random(),
-						emoji: `:${token.props.name}:`,
-						customEmojis: this.customEmojis,
-						normal: this.plain
-					})];
-				}
-
-				case 'unicodeEmoji': {
-					return [h(MkEmoji, {
-						key: Math.random(),
-						emoji: token.props.emoji,
-						customEmojis: this.customEmojis,
-						normal: this.plain
-					})];
-				}
-
-				case 'mathInline': {
-					return [h(MkFormula, {
-						key: Math.random(),
-						formula: token.props.formula,
-						block: false
-					})];
-				}
-
-				case 'mathBlock': {
-					return [h(MkFormula, {
-						key: Math.random(),
-						formula: token.props.formula,
-						block: true
-					})];
-				}
-
-				case 'search': {
-					return [h(MkGoogle, {
-						key: Math.random(),
-						q: token.props.query
-					})];
-				}
-
-				default: {
-					console.error('unrecognized ast type:', token.type);
-
-					return [];
-				}
-			}
-		}));
-
-		// Parse ast to DOM
-		return h('span', genEl(ast));
-	}
-});
diff --git a/src/client/components/mini-chart.vue b/src/client/components/mini-chart.vue
deleted file mode 100644
index 0d01e4e4b5..0000000000
--- a/src/client/components/mini-chart.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<template>
-<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" style="overflow:visible">
-	<defs>
-		<linearGradient :id="gradientId" x1="0" x2="0" y1="1" y2="0">
-			<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop>
-			<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop>
-		</linearGradient>
-		<mask :id="maskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
-			<polygon
-				:points="polygonPoints"
-				fill="#fff"
-				fill-opacity="0.5"/>
-			<polyline
-				:points="polylinePoints"
-				fill="none"
-				stroke="#fff"
-				stroke-width="2"/>
-			<circle
-				:cx="headX"
-				:cy="headY"
-				r="3"
-				fill="#fff"/>
-		</mask>
-	</defs>
-	<rect
-		x="-10" y="-10"
-		:width="viewBoxX + 20" :height="viewBoxY + 20"
-		:style="`stroke: none; fill: url(#${ gradientId }); mask: url(#${ maskId })`"/>
-</svg>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		src: {
-			type: Array,
-			required: true
-		}
-	},
-	data() {
-		return {
-			viewBoxX: 50,
-			viewBoxY: 30,
-			gradientId: uuid(),
-			maskId: uuid(),
-			polylinePoints: '',
-			polygonPoints: '',
-			headX: null,
-			headY: null,
-			clock: null
-		};
-	},
-	watch: {
-		src() {
-			this.draw();
-		}
-	},
-	created() {
-		this.draw();
-
-		// Vueが何故かWatchを発動させない場合があるので
-		this.clock = setInterval(this.draw, 1000);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		draw() {
-			const stats = this.src.slice().reverse();
-			const peak = Math.max.apply(null, stats) || 1;
-
-			const polylinePoints = stats.map((n, i) => [
-				i * (this.viewBoxX / (stats.length - 1)),
-				(1 - (n / peak)) * this.viewBoxY
-			]);
-
-			this.polylinePoints = polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-
-			this.polygonPoints = `0,${ this.viewBoxY } ${ this.polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
-
-			this.headX = polylinePoints[polylinePoints.length - 1][0];
-			this.headY = polylinePoints[polylinePoints.length - 1][1];
-		}
-	}
-});
-</script>
diff --git a/src/client/components/modal-page-window.vue b/src/client/components/modal-page-window.vue
deleted file mode 100644
index e47d3dc62c..0000000000
--- a/src/client/components/modal-page-window.vue
+++ /dev/null
@@ -1,223 +0,0 @@
-<template>
-<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
-	<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
-		<div class="header" @contextmenu="onContextmenu">
-			<button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
-			<span v-else style="display: inline-block; width: 20px"></span>
-			<span v-if="pageInfo" class="title">
-				<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i>
-				<span>{{ pageInfo.title }}</span>
-			</span>
-			<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
-		</div>
-		<div class="body">
-			<MkStickyContainer>
-				<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
-				<keep-alive>
-					<component :is="component" v-bind="props" :ref="changePage"/>
-				</keep-alive>
-			</MkStickyContainer>
-		</div>
-	</div>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkModal from '@client/components/ui/modal.vue';
-import { popout } from '@client/scripts/popout';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { resolve } from '@client/router';
-import { url } from '@client/config';
-import * as symbols from '@client/symbols';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkModal,
-	},
-
-	inject: {
-		sideViewHook: {
-			default: null
-		}
-	},
-
-	provide() {
-		return {
-			navHook: (path) => {
-				this.navigate(path);
-			},
-			shouldHeaderThin: true,
-		};
-	},
-
-	props: {
-		initialPath: {
-			type: String,
-			required: true,
-		},
-		initialComponent: {
-			type: Object,
-			required: true,
-		},
-		initialProps: {
-			type: Object,
-			required: false,
-			default: () => {},
-		},
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			width: 860,
-			height: 660,
-			pageInfo: null,
-			path: this.initialPath,
-			component: this.initialComponent,
-			props: this.initialProps,
-			history: [],
-		};
-	},
-
-	computed: {
-		url(): string {
-			return url + this.path;
-		},
-
-		contextmenu() {
-			return [{
-				type: 'label',
-				text: this.path,
-			}, {
-				icon: 'fas fa-expand-alt',
-				text: this.$ts.showInPage,
-				action: this.expand
-			}, this.sideViewHook ? {
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.sideViewHook(this.path);
-					this.$refs.window.close();
-				}
-			} : undefined, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.popout,
-				action: this.popout
-			}, null, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.openInNewTab,
-				action: () => {
-					window.open(this.url, '_blank');
-					this.$refs.window.close();
-				}
-			}, {
-				icon: 'fas fa-link',
-				text: this.$ts.copyLink,
-				action: () => {
-					copyToClipboard(this.url);
-				}
-			}];
-		},
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		navigate(path, record = true) {
-			if (record) this.history.push(this.path);
-			this.path = path;
-			const { component, props } = resolve(path);
-			this.component = component;
-			this.props = props;
-		},
-
-		back() {
-			this.navigate(this.history.pop(), false);
-		},
-
-		expand() {
-			this.$router.push(this.path);
-			this.$refs.window.close();
-		},
-
-		popout() {
-			popout(this.path, this.$el);
-			this.$refs.window.close();
-		},
-
-		onContextmenu(e) {
-			os.contextMenu(this.contextmenu, e);
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.hrmcaedk {
-	overflow: hidden;
-	display: flex;
-	flex-direction: column;
-	contain: content;
-
-	--root-margin: 24px;
-
-	@media (max-width: 500px) {
-		--root-margin: 16px;
-	}
-
-	> .header {
-		$height: 52px;
-		$height-narrow: 42px;
-		display: flex;
-		flex-shrink: 0;
-		height: $height;
-		line-height: $height;
-		font-weight: bold;
-		white-space: nowrap;
-		overflow: hidden;
-		text-overflow: ellipsis;
-		box-shadow: 0px 1px var(--divider);
-
-		> button {
-			height: $height;
-			width: $height;
-
-			&:hover {
-				color: var(--fgHighlighted);
-			}
-		}
-
-		@media (max-width: 500px) {
-			height: $height-narrow;
-			line-height: $height-narrow;
-			padding-left: 16px;
-
-			> button {
-				height: $height-narrow;
-				width: $height-narrow;
-			}
-		}
-
-		> .title {
-			flex: 1;
-
-			> .icon {
-				margin-right: 0.5em;
-			}
-		}
-	}
-
-	> .body {
-		overflow: auto;
-		background: var(--bg);
-	}
-}
-</style>
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
deleted file mode 100644
index 568a2360d1..0000000000
--- a/src/client/components/note-detailed.vue
+++ /dev/null
@@ -1,1229 +0,0 @@
-<template>
-<div
-	class="lxwezrsl _block"
-	v-if="!muted"
-	v-show="!isDeleted"
-	:tabindex="!isDeleted ? '-1' : null"
-	:class="{ renote: isRenote }"
-	v-hotkey="keymap"
-	v-size="{ max: [500, 450, 350, 300] }"
->
-	<XSub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/>
-	<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
-	<div class="renote" v-if="isRenote">
-		<MkAvatar class="avatar" :user="note.user"/>
-		<i class="fas fa-retweet"></i>
-		<I18n :src="$ts.renotedBy" tag="span">
-			<template #user>
-				<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
-					<MkUserName :user="note.user"/>
-				</MkA>
-			</template>
-		</I18n>
-		<div class="info">
-			<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
-				<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
-				<MkTime :time="note.createdAt"/>
-			</button>
-			<span class="visibility" v-if="note.visibility !== 'public'">
-				<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
-				<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
-				<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
-			</span>
-			<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
-		</div>
-	</div>
-	<article class="article" @contextmenu.stop="onContextmenu">
-		<header class="header">
-			<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
-			<div class="body">
-				<div class="top">
-					<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.user.id">
-						<MkUserName :user="appearNote.user"/>
-					</MkA>
-					<span class="is-bot" v-if="appearNote.user.isBot">bot</span>
-					<span class="admin" v-if="appearNote.user.isAdmin"><i class="fas fa-bookmark"></i></span>
-					<span class="moderator" v-if="!appearNote.user.isAdmin && appearNote.user.isModerator"><i class="far fa-bookmark"></i></span>
-					<span class="visibility" v-if="appearNote.visibility !== 'public'">
-						<i v-if="appearNote.visibility === 'home'" class="fas fa-home"></i>
-						<i v-else-if="appearNote.visibility === 'followers'" class="fas fa-unlock"></i>
-						<i v-else-if="appearNote.visibility === 'specified'" class="fas fa-envelope"></i>
-					</span>
-					<span class="localOnly" v-if="appearNote.localOnly"><i class="fas fa-biohazard"></i></span>
-				</div>
-				<div class="username"><MkAcct :user="appearNote.user"/></div>
-				<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
-			</div>
-		</header>
-		<div class="main">
-			<div class="body">
-				<p v-if="appearNote.cw != null" class="cw">
-					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
-					<XCwButton v-model="showContent" :note="appearNote"/>
-				</p>
-				<div class="content" v-show="appearNote.cw == null || showContent">
-					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
-						<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
-						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
-						<a class="rp" v-if="appearNote.renote != null">RN:</a>
-						<div class="translation" v-if="translating || translation">
-							<MkLoading v-if="translating" mini/>
-							<div class="translated" v-else>
-								<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}:</b>
-								{{ translation.text }}
-							</div>
-						</div>
-					</div>
-					<div class="files" v-if="appearNote.files.length > 0">
-						<XMediaList :media-list="appearNote.files"/>
-					</div>
-					<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
-					<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="true" class="url-preview"/>
-					<div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
-				</div>
-				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
-			</div>
-			<footer class="footer">
-				<div class="info">
-					<span class="mobile" v-if="appearNote.viaMobile"><i class="fas fa-mobile-alt"></i></span>
-					<MkTime class="created-at" :time="appearNote.createdAt" mode="detail"/>
-				</div>
-				<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
-				<button @click="reply()" class="button _button">
-					<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
-					<template v-else><i class="fas fa-reply"></i></template>
-					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
-				</button>
-				<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton">
-					<i class="fas fa-retweet"></i><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
-				</button>
-				<button v-else class="button _button">
-					<i class="fas fa-ban"></i>
-				</button>
-				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
-					<i class="fas fa-plus"></i>
-				</button>
-				<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
-					<i class="fas fa-minus"></i>
-				</button>
-				<button class="button _button" @click="menu()" ref="menuButton">
-					<i class="fas fa-ellipsis-h"></i>
-				</button>
-			</footer>
-		</div>
-	</article>
-	<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
-</div>
-<div v-else class="_panel muted" @click="muted = false">
-	<I18n :src="$ts.userSaysSomething" tag="small">
-		<template #name>
-			<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
-				<MkUserName :user="appearNote.user"/>
-			</MkA>
-		</template>
-	</I18n>
-</div>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
-import * as mfm from 'mfm-js';
-import { sum } from '../../prelude/array';
-import XSub from './note.sub.vue';
-import XNoteHeader from './note-header.vue';
-import XNoteSimple from './note-simple.vue';
-import XReactionsViewer from './reactions-viewer.vue';
-import XMediaList from './media-list.vue';
-import XCwButton from './cw-button.vue';
-import XPoll from './poll.vue';
-import { pleaseLogin } from '@client/scripts/please-login';
-import { focusPrev, focusNext } from '@client/scripts/focus';
-import { url } from '@client/config';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { checkWordMute } from '@client/scripts/check-word-mute';
-import { userPage } from '@client/filters/user';
-import * as os from '@client/os';
-import { noteActions, noteViewInterruptors } from '@client/store';
-import { reactionPicker } from '@client/scripts/reaction-picker';
-import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
-
-// TODO: note.vueとほぼ同じなので共通化したい
-export default defineComponent({
-	components: {
-		XSub,
-		XNoteHeader,
-		XNoteSimple,
-		XReactionsViewer,
-		XMediaList,
-		XCwButton,
-		XPoll,
-		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
-		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
-	},
-
-	inject: {
-		inChannel: {
-			default: null
-		},
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-
-	emits: ['update:note'],
-
-	data() {
-		return {
-			connection: null,
-			conversation: [],
-			replies: [],
-			showContent: false,
-			isDeleted: false,
-			muted: false,
-			translation: null,
-			translating: false,
-		};
-	},
-
-	computed: {
-		rs() {
-			return this.$store.state.reactions;
-		},
-		keymap(): any {
-			return {
-				'r': () => this.reply(true),
-				'e|a|plus': () => this.react(true),
-				'q': () => this.renote(true),
-				'f|b': this.favorite,
-				'delete|ctrl+d': this.del,
-				'ctrl+q': this.renoteDirectly,
-				'up|k|shift+tab': this.focusBefore,
-				'down|j|tab': this.focusAfter,
-				'esc': this.blur,
-				'm|o': () => this.menu(true),
-				's': this.toggleShowContent,
-				'1': () => this.reactDirectly(this.rs[0]),
-				'2': () => this.reactDirectly(this.rs[1]),
-				'3': () => this.reactDirectly(this.rs[2]),
-				'4': () => this.reactDirectly(this.rs[3]),
-				'5': () => this.reactDirectly(this.rs[4]),
-				'6': () => this.reactDirectly(this.rs[5]),
-				'7': () => this.reactDirectly(this.rs[6]),
-				'8': () => this.reactDirectly(this.rs[7]),
-				'9': () => this.reactDirectly(this.rs[8]),
-				'0': () => this.reactDirectly(this.rs[9]),
-			};
-		},
-
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.fileIds.length == 0 &&
-				this.note.poll == null);
-		},
-
-		appearNote(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-
-		isMyNote(): boolean {
-			return this.$i && (this.$i.id === this.appearNote.userId);
-		},
-
-		isMyRenote(): boolean {
-			return this.$i && (this.$i.id === this.note.userId);
-		},
-
-		canRenote(): boolean {
-			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
-		},
-
-		reactionsCount(): number {
-			return this.appearNote.reactions
-				? sum(Object.values(this.appearNote.reactions))
-				: 0;
-		},
-
-		urls(): string[] {
-			if (this.appearNote.text) {
-				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
-			} else {
-				return null;
-			}
-		},
-
-		showTicker() {
-			if (this.$store.state.instanceTicker === 'always') return true;
-			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
-			return false;
-		}
-	},
-
-	async created() {
-		if (this.$i) {
-			this.connection = os.stream;
-		}
-
-		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
-
-		// plugin
-		if (noteViewInterruptors.length > 0) {
-			let result = this.note;
-			for (const interruptor of noteViewInterruptors) {
-				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
-			}
-			this.$emit('update:note', Object.freeze(result));
-		}
-
-		os.api('notes/children', {
-			noteId: this.appearNote.id,
-			limit: 30
-		}).then(replies => {
-			this.replies = replies;
-		});
-
-		if (this.appearNote.replyId) {
-			os.api('notes/conversation', {
-				noteId: this.appearNote.replyId
-			}).then(conversation => {
-				this.conversation = conversation.reverse();
-			});
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$i) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-	},
-
-	beforeUnmount() {
-		this.decapture(true);
-
-		if (this.$i) {
-			this.connection.off('_connected_', this.onStreamConnected);
-		}
-	},
-
-	methods: {
-		updateAppearNote(v) {
-			this.$emit('update:note', Object.freeze(this.isRenote ? {
-				...this.note,
-				renote: {
-					...this.note.renote,
-					...v
-				}
-			} : {
-				...this.note,
-				...v
-			}));
-		},
-
-		readPromo() {
-			os.api('promo/read', {
-				noteId: this.appearNote.id
-			});
-			this.isDeleted = true;
-		},
-
-		capture(withHandler = false) {
-			if (this.$i) {
-				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
-				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
-				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$i) {
-				this.connection.send('un', {
-					id: this.appearNote.id
-				});
-				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		onStreamConnected() {
-			this.capture();
-		},
-
-		onStreamNoteUpdated(data) {
-			const { type, id, body } = data;
-
-			if (id !== this.appearNote.id) return;
-
-			switch (type) {
-				case 'reacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					if (body.emoji) {
-						const emojis = this.appearNote.emojis || [];
-						if (!emojis.includes(body.emoji)) {
-							n.emojis = [...emojis, body.emoji];
-						}
-					}
-
-					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Increment the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: currentCount + 1
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = reaction;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'unreacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Decrement the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: Math.max(0, currentCount - 1)
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = null;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'pollVoted': {
-					const choice = body.choice;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					const choices = [...this.appearNote.poll.choices];
-					choices[choice] = {
-						...choices[choice],
-						votes: choices[choice].votes + 1,
-						...(body.userId === this.$i.id ? {
-							isVoted: true
-						} : {})
-					};
-
-					n.poll = {
-						...this.appearNote.poll,
-						choices: choices
-					};
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'deleted': {
-					this.isDeleted = true;
-					break;
-				}
-			}
-		},
-
-		reply(viaKeyboard = false) {
-			pleaseLogin();
-			os.post({
-				reply: this.appearNote,
-				animation: !viaKeyboard,
-			}, () => {
-				this.focus();
-			});
-		},
-
-		renote(viaKeyboard = false) {
-			pleaseLogin();
-			this.blur();
-			os.popupMenu([{
-				text: this.$ts.renote,
-				icon: 'fas fa-retweet',
-				action: () => {
-					os.api('notes/create', {
-						renoteId: this.appearNote.id
-					});
-				}
-			}, {
-				text: this.$ts.quote,
-				icon: 'fas fa-quote-right',
-				action: () => {
-					os.post({
-						renote: this.appearNote,
-					});
-				}
-			}], this.$refs.renoteButton, {
-				viaKeyboard
-			});
-		},
-
-		renoteDirectly() {
-			os.apiWithDialog('notes/create', {
-				renoteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.dialog({
-					type: 'success',
-					text: this.$ts.renoted,
-				});
-			}, (e: Error) => {
-				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantRenote,
-					});
-				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantReRenote,
-					});
-				}
-			});
-		},
-
-		react(viaKeyboard = false) {
-			pleaseLogin();
-			this.blur();
-			reactionPicker.show(this.$refs.reactButton, reaction => {
-				os.api('notes/reactions/create', {
-					noteId: this.appearNote.id,
-					reaction: reaction
-				});
-			}, () => {
-				this.focus();
-			});
-		},
-
-		reactDirectly(reaction) {
-			os.api('notes/reactions/create', {
-				noteId: this.appearNote.id,
-				reaction: reaction
-			});
-		},
-
-		undoReact(note) {
-			const oldReaction = note.myReaction;
-			if (!oldReaction) return;
-			os.api('notes/reactions/delete', {
-				noteId: note.id
-			});
-		},
-
-		favorite() {
-			pleaseLogin();
-			os.apiWithDialog('notes/favorites/create', {
-				noteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.dialog({
-					type: 'success',
-					text: this.$ts.favorited,
-				});
-			}, (e: Error) => {
-				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.alreadyFavorited,
-					});
-				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantFavorite,
-					});
-				}
-			});
-		},
-
-		del() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.noteDeleteConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-			});
-		},
-
-		delEdit() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.deleteAndEditConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-
-				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
-			});
-		},
-
-		toggleFavorite(favorite: boolean) {
-			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleWatch(watch: boolean) {
-			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleThreadMute(mute: boolean) {
-			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		getMenu() {
-			let menu;
-			if (this.$i) {
-				const statePromise = os.api('notes/state', {
-					noteId: this.appearNote.id
-				});
-
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined,
-				{
-					icon: 'fas fa-share-alt',
-					text: this.$ts.share,
-					action: this.share
-				},
-				this.$instance.translatorAvailable ? {
-					icon: 'fas fa-language',
-					text: this.$ts.translate,
-					action: this.translate
-				} : undefined,
-				null,
-				statePromise.then(state => state.isFavorited ? {
-					icon: 'fas fa-star',
-					text: this.$ts.unfavorite,
-					action: () => this.toggleFavorite(false)
-				} : {
-					icon: 'fas fa-star',
-					text: this.$ts.favorite,
-					action: () => this.toggleFavorite(true)
-				}),
-				{
-					icon: 'fas fa-paperclip',
-					text: this.$ts.clip,
-					action: () => this.clip()
-				},
-				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
-					icon: 'fas fa-eye-slash',
-					text: this.$ts.unwatch,
-					action: () => this.toggleWatch(false)
-				} : {
-					icon: 'fas fa-eye',
-					text: this.$ts.watch,
-					action: () => this.toggleWatch(true)
-				}) : undefined,
-				statePromise.then(state => state.isMutedThread ? {
-					icon: 'fas fa-comment-slash',
-					text: this.$ts.unmuteThread,
-					action: () => this.toggleThreadMute(false)
-				} : {
-					icon: 'fas fa-comment-slash',
-					text: this.$ts.muteThread,
-					action: () => this.toggleThreadMute(true)
-				}),
-				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.unpin,
-					action: () => this.togglePin(false)
-				} : {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.pin,
-					action: () => this.togglePin(true)
-				} : undefined,
-				...(this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					{
-						icon: 'fas fa-bullhorn',
-						text: this.$ts.promote,
-						action: this.promote
-					}]
-					: []
-				),
-				...(this.appearNote.userId != this.$i.id ? [
-					null,
-					{
-						icon: 'fas fa-exclamation-circle',
-						text: this.$ts.reportAbuse,
-						action: () => {
-							const u = `${url}/notes/${this.appearNote.id}`;
-							os.popup(import('@client/components/abuse-report-window.vue'), {
-								user: this.appearNote.user,
-								initialComment: `Note: ${u}\n-----\n`
-							}, {}, 'closed');
-						}
-					}]
-					: []
-				),
-				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					this.appearNote.userId == this.$i.id ? {
-						icon: 'fas fa-edit',
-						text: this.$ts.deleteAndEdit,
-						action: this.delEdit
-					} : undefined,
-					{
-						icon: 'fas fa-trash-alt',
-						text: this.$ts.delete,
-						danger: true,
-						action: this.del
-					}]
-					: []
-				)]
-				.filter(x => x !== undefined);
-			} else {
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined]
-				.filter(x => x !== undefined);
-			}
-
-			if (noteActions.length > 0) {
-				menu = menu.concat([null, ...noteActions.map(action => ({
-					icon: 'fas fa-plug',
-					text: action.title,
-					action: () => {
-						action.handler(this.appearNote);
-					}
-				}))]);
-			}
-
-			return menu;
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (window.getSelection().toString() !== '') return;
-
-			if (this.$store.state.useReactionPickerForContextMenu) {
-				e.preventDefault();
-				this.react();
-			} else {
-				os.contextMenu(this.getMenu(), e).then(this.focus);
-			}
-		},
-
-		menu(viaKeyboard = false) {
-			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
-				viaKeyboard
-			}).then(this.focus);
-		},
-
-		showRenoteMenu(viaKeyboard = false) {
-			if (!this.isMyRenote) return;
-			os.popupMenu([{
-				text: this.$ts.unrenote,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: () => {
-					os.api('notes/delete', {
-						noteId: this.note.id
-					});
-					this.isDeleted = true;
-				}
-			}], this.$refs.renoteTime, {
-				viaKeyboard: viaKeyboard
-			});
-		},
-
-		toggleShowContent() {
-			this.showContent = !this.showContent;
-		},
-
-		copyContent() {
-			copyToClipboard(this.appearNote.text);
-			os.success();
-		},
-
-		copyLink() {
-			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
-			os.success();
-		},
-
-		togglePin(pin: boolean) {
-			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
-				noteId: this.appearNote.id
-			}, undefined, null, e => {
-				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.pinLimitExceeded
-					});
-				}
-			});
-		},
-
-		async clip() {
-			const clips = await os.api('clips/list');
-			os.popupMenu([{
-				icon: 'fas fa-plus',
-				text: this.$ts.createNew,
-				action: async () => {
-					const { canceled, result } = await os.form(this.$ts.createNewClip, {
-						name: {
-							type: 'string',
-							label: this.$ts.name
-						},
-						description: {
-							type: 'string',
-							required: false,
-							multiline: true,
-							label: this.$ts.description
-						},
-						isPublic: {
-							type: 'boolean',
-							label: this.$ts.public,
-							default: false
-						}
-					});
-					if (canceled) return;
-
-					const clip = await os.apiWithDialog('clips/create', result);
-
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}, null, ...clips.map(clip => ({
-				text: clip.name,
-				action: () => {
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}))], this.$refs.menuButton, {
-			}).then(this.focus);
-		},
-
-		async promote() {
-			const { canceled, result: days } = await os.dialog({
-				title: this.$ts.numberOfDays,
-				input: { type: 'number' }
-			});
-
-			if (canceled) return;
-
-			os.apiWithDialog('admin/promo/create', {
-				noteId: this.appearNote.id,
-				expiresAt: Date.now() + (86400000 * days)
-			});
-		},
-
-		share() {
-			navigator.share({
-				title: this.$t('noteOf', { user: this.appearNote.user.name }),
-				text: this.appearNote.text,
-				url: `${url}/notes/${this.appearNote.id}`
-			});
-		},
-
-		async translate() {
-			if (this.translation != null) return;
-			this.translating = true;
-			const res = await os.api('notes/translate', {
-				noteId: this.appearNote.id,
-				targetLang: localStorage.getItem('lang') || navigator.language,
-			});
-			this.translating = false;
-			this.translation = res;
-		},
-
-		focus() {
-			this.$el.focus();
-		},
-
-		blur() {
-			this.$el.blur();
-		},
-
-		focusBefore() {
-			focusPrev(this.$el);
-		},
-
-		focusAfter() {
-			focusNext(this.$el);
-		},
-
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.lxwezrsl {
-	position: relative;
-	transition: box-shadow 0.1s ease;
-	overflow: hidden;
-	contain: content;
-
-	&:focus-visible {
-		outline: none;
-
-		&:after {
-			content: "";
-			pointer-events: none;
-			display: block;
-			position: absolute;
-			z-index: 10;
-			top: 0;
-			left: 0;
-			right: 0;
-			bottom: 0;
-			margin: auto;
-			width: calc(100% - 8px);
-			height: calc(100% - 8px);
-			border: dashed 1px var(--focus);
-			border-radius: var(--radius);
-			box-sizing: border-box;
-		}
-	}
-
-	&:hover > .article > .main > .footer > .button {
-		opacity: 1;
-	}
-
-	> .reply-to {
-		opacity: 0.7;
-		padding-bottom: 0;
-	}
-
-	> .reply-to-more {
-		opacity: 0.7;
-	}
-
-	> .renote {
-		display: flex;
-		align-items: center;
-		padding: 16px 32px 8px 32px;
-		line-height: 28px;
-		white-space: pre;
-		color: var(--renote);
-
-		> .avatar {
-			flex-shrink: 0;
-			display: inline-block;
-			width: 28px;
-			height: 28px;
-			margin: 0 8px 0 0;
-			border-radius: 6px;
-		}
-
-		> i {
-			margin-right: 4px;
-		}
-
-		> span {
-			overflow: hidden;
-			flex-shrink: 1;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-
-			> .name {
-				font-weight: bold;
-			}
-		}
-
-		> .info {
-			margin-left: auto;
-			font-size: 0.9em;
-
-			> .time {
-				flex-shrink: 0;
-				color: inherit;
-
-				> .dropdownIcon {
-					margin-right: 4px;
-				}
-			}
-
-			> .visibility {
-				margin-left: 8px;
-			}
-
-			> .localOnly {
-				margin-left: 8px;
-			}
-		}
-	}
-
-	> .renote + .article {
-		padding-top: 8px;
-	}
-
-	> .article {
-		padding: 32px;
-		font-size: 1.1em;
-
-		> .header {
-			display: flex;
-			position: relative;
-			margin-bottom: 16px;
-
-			> .avatar {
-				display: block;
-				flex-shrink: 0;
-				width: 58px;
-				height: 58px;
-			}
-
-			> .body {
-				flex: 1;
-				display: flex;
-				flex-direction: column;
-				justify-content: center;
-				padding-left: 16px;
-				font-size: 0.95em;
-
-				> .top {
-					> .name {
-						font-weight: bold;
-					}
-
-					> .is-bot {
-						flex-shrink: 0;
-						align-self: center;
-						margin: 0 0.5em;
-						padding: 4px 6px;
-						font-size: 80%;
-						border: solid 0.5px var(--divider);
-						border-radius: 4px;
-					}
-
-					> .admin,
-					> .moderator {
-						margin-right: 0.5em;
-						color: var(--badge);
-					}
-				}
-			}
-		}
-
-		> .main {
-			> .body {
-				> .cw {
-					cursor: default;
-					display: block;
-					margin: 0;
-					padding: 0;
-					overflow-wrap: break-word;
-
-					> .text {
-						margin-right: 8px;
-					}
-				}
-
-				> .content {
-					> .text {
-						overflow-wrap: break-word;
-
-						> .reply {
-							color: var(--accent);
-							margin-right: 0.5em;
-						}
-
-						> .rp {
-							margin-left: 4px;
-							font-style: oblique;
-							color: var(--renote);
-						}
-
-						> .translation {
-							border: solid 0.5px var(--divider);
-							border-radius: var(--radius);
-							padding: 12px;
-							margin-top: 8px;
-						}
-					}
-
-					> .url-preview {
-						margin-top: 8px;
-					}
-
-					> .poll {
-						font-size: 80%;
-					}
-
-					> .renote {
-						padding: 8px 0;
-
-						> * {
-							padding: 16px;
-							border: dashed 1px var(--renote);
-							border-radius: 8px;
-						}
-					}
-				}
-
-				> .channel {
-					opacity: 0.7;
-					font-size: 80%;
-				}
-			}
-
-			> .footer {
-				> .info {
-					margin: 16px 0;
-					opacity: 0.7;
-					font-size: 0.9em;
-				}
-
-				> .button {
-					margin: 0;
-					padding: 8px;
-					opacity: 0.7;
-
-					&:not(:last-child) {
-						margin-right: 28px;
-					}
-
-					&:hover {
-						color: var(--fgHighlighted);
-					}
-
-					> .count {
-						display: inline;
-						margin: 0 0 0 8px;
-						opacity: 0.7;
-					}
-
-					&.reacted {
-						color: var(--accent);
-					}
-				}
-			}
-		}
-	}
-
-	> .reply {
-		border-top: solid 0.5px var(--divider);
-	}
-
-	&.max-width_500px {
-		font-size: 0.9em;
-	}
-
-	&.max-width_450px {
-		> .renote {
-			padding: 8px 16px 0 16px;
-		}
-
-		> .article {
-			padding: 16px;
-
-			> .header {
-				> .avatar {
-					width: 50px;
-					height: 50px;
-				}
-			}
-		}
-	}
-
-	&.max-width_350px {
-		> .article {
-			> .main {
-				> .footer {
-					> .button {
-						&:not(:last-child) {
-							margin-right: 18px;
-						}
-					}
-				}
-			}
-		}
-	}
-
-	&.max-width_300px {
-		font-size: 0.825em;
-
-		> .article {
-			> .header {
-				> .avatar {
-					width: 50px;
-					height: 50px;
-				}
-			}
-
-			> .main {
-				> .footer {
-					> .button {
-						&:not(:last-child) {
-							margin-right: 12px;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-
-.muted {
-	padding: 8px;
-	text-align: center;
-	opacity: 0.7;
-}
-</style>
diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue
deleted file mode 100644
index 80bfea9b07..0000000000
--- a/src/client/components/note-header.vue
+++ /dev/null
@@ -1,115 +0,0 @@
-<template>
-<header class="kkwtjztg">
-	<MkA class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
-		<MkUserName :user="note.user"/>
-	</MkA>
-	<div class="is-bot" v-if="note.user.isBot">bot</div>
-	<div class="username"><MkAcct :user="note.user"/></div>
-	<div class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></div>
-	<div class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></div>
-	<div class="info">
-		<span class="mobile" v-if="note.viaMobile"><i class="fas fa-mobile-alt"></i></span>
-		<MkA class="created-at" :to="notePage(note)">
-			<MkTime :time="note.createdAt"/>
-		</MkA>
-		<span class="visibility" v-if="note.visibility !== 'public'">
-			<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
-			<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
-			<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
-		</span>
-		<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
-	</div>
-</header>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import notePage from '@client/filters/note';
-import { userPage } from '@client/filters/user';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-		notePage,
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.kkwtjztg {
-	display: flex;
-	align-items: baseline;
-	white-space: nowrap;
-
-	> .name {
-		flex-shrink: 1;
-		display: block;
-		margin: 0 .5em 0 0;
-		padding: 0;
-		overflow: hidden;
-		font-size: 1em;
-		font-weight: bold;
-		text-decoration: none;
-		text-overflow: ellipsis;
-
-		&:hover {
-			text-decoration: underline;
-		}
-	}
-
-	> .is-bot {
-		flex-shrink: 0;
-		align-self: center;
-		margin: 0 .5em 0 0;
-		padding: 1px 6px;
-		font-size: 80%;
-		border: solid 0.5px var(--divider);
-		border-radius: 3px;
-	}
-
-	> .admin,
-	> .moderator {
-		flex-shrink: 0;
-		margin-right: 0.5em;
-		color: var(--badge);
-	}
-
-	> .username {
-		flex-shrink: 9999999;
-		margin: 0 .5em 0 0;
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
-
-	> .info {
-		flex-shrink: 0;
-		margin-left: auto;
-		font-size: 0.9em;
-
-		> .mobile {
-			margin-right: 8px;
-		}
-
-		> .visibility {
-			margin-left: 8px;
-		}
-
-		> .localOnly {
-			margin-left: 8px;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/note-preview.vue b/src/client/components/note-preview.vue
deleted file mode 100644
index a474a01341..0000000000
--- a/src/client/components/note-preview.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<template>
-<div class="fefdfafb" v-size="{ min: [350, 500] }">
-	<MkAvatar class="avatar" :user="$i"/>
-	<div class="main">
-		<div class="header">
-			<MkUserName :user="$i"/>
-		</div>
-		<div class="body">
-			<div class="content">
-				<Mfm :text="text" :author="$i" :i="$i"/>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	components: {
-	},
-
-	props: {
-		text: {
-			type: String,
-			required: true
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.fefdfafb {
-	display: flex;
-	margin: 0;
-	padding: 0;
-	overflow: clip;
-	font-size: 0.95em;
-
-	&.min-width_350px {
-		> .avatar {
-			margin: 0 10px 0 0;
-			width: 44px;
-			height: 44px;
-		}
-	}
-
-	&.min-width_500px {
-		> .avatar {
-			margin: 0 12px 0 0;
-			width: 48px;
-			height: 48px;
-		}
-	}
-
-	> .avatar {
-		flex-shrink: 0;
-		display: block;
-		margin: 0 10px 0 0;
-		width: 40px;
-		height: 40px;
-		border-radius: 8px;
-	}
-
-	> .main {
-		flex: 1;
-		min-width: 0;
-
-		> .header {
-			margin-bottom: 2px;
-		}
-
-		> .body {
-
-			> .cw {
-				cursor: default;
-				display: block;
-				margin: 0;
-				padding: 0;
-				overflow-wrap: break-word;
-
-				> .text {
-					margin-right: 8px;
-				}
-			}
-
-			> .content {
-				> .text {
-					cursor: default;
-					margin: 0;
-					padding: 0;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/note-simple.vue b/src/client/components/note-simple.vue
deleted file mode 100644
index 406a475cd9..0000000000
--- a/src/client/components/note-simple.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<template>
-<div class="yohlumlk" v-size="{ min: [350, 500] }">
-	<MkAvatar class="avatar" :user="note.user"/>
-	<div class="main">
-		<XNoteHeader class="header" :note="note" :mini="true"/>
-		<div class="body">
-			<p v-if="note.cw != null" class="cw">
-				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
-				<XCwButton v-model="showContent" :note="note"/>
-			</p>
-			<div class="content" v-show="note.cw == null || showContent">
-				<XSubNote-content class="text" :note="note"/>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
-import XCwButton from './cw-button.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XNoteHeader,
-		XSubNoteContent,
-		XCwButton,
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			showContent: false
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.yohlumlk {
-	display: flex;
-	margin: 0;
-	padding: 0;
-	overflow: clip;
-	font-size: 0.95em;
-
-	&.min-width_350px {
-		> .avatar {
-			margin: 0 10px 0 0;
-			width: 44px;
-			height: 44px;
-		}
-	}
-
-	&.min-width_500px {
-		> .avatar {
-			margin: 0 12px 0 0;
-			width: 48px;
-			height: 48px;
-		}
-	}
-
-	> .avatar {
-		flex-shrink: 0;
-		display: block;
-		margin: 0 10px 0 0;
-		width: 40px;
-		height: 40px;
-		border-radius: 8px;
-	}
-
-	> .main {
-		flex: 1;
-		min-width: 0;
-
-		> .header {
-			margin-bottom: 2px;
-		}
-
-		> .body {
-
-			> .cw {
-				cursor: default;
-				display: block;
-				margin: 0;
-				padding: 0;
-				overflow-wrap: break-word;
-
-				> .text {
-					margin-right: 8px;
-				}
-			}
-
-			> .content {
-				> .text {
-					cursor: default;
-					margin: 0;
-					padding: 0;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/note.sub.vue b/src/client/components/note.sub.vue
deleted file mode 100644
index 157b65ec5c..0000000000
--- a/src/client/components/note.sub.vue
+++ /dev/null
@@ -1,146 +0,0 @@
-<template>
-<div class="wrpstxzv" :class="{ children }" v-size="{ max: [450] }">
-	<div class="main">
-		<MkAvatar class="avatar" :user="note.user"/>
-		<div class="body">
-			<XNoteHeader class="header" :note="note" :mini="true"/>
-			<div class="body">
-				<p v-if="note.cw != null" class="cw">
-					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
-					<XCwButton v-model="showContent" :note="note"/>
-				</p>
-				<div class="content" v-show="note.cw == null || showContent">
-					<XSubNote-content class="text" :note="note"/>
-				</div>
-			</div>
-		</div>
-	</div>
-	<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
-import XCwButton from './cw-button.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	name: 'XSub',
-
-	components: {
-		XNoteHeader,
-		XSubNoteContent,
-		XCwButton,
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		children: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		// TODO
-		truncate: {
-			type: Boolean,
-			default: true
-		}
-	},
-
-	data() {
-		return {
-			showContent: false,
-			replies: [],
-		};
-	},
-
-	created() {
-		if (this.detail) {
-			os.api('notes/children', {
-				noteId: this.note.id,
-				limit: 5
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.wrpstxzv {
-	padding: 16px 32px;
-	font-size: 0.9em;
-
-	&.max-width_450px {
-		padding: 14px 16px;
-	}
-
-	&.children {
-		padding: 10px 0 0 16px;
-		font-size: 1em;
-
-		&.max-width_450px {
-			padding: 10px 0 0 8px;
-		}
-	}
-
-	> .main {
-		display: flex;
-
-		> .avatar {
-			flex-shrink: 0;
-			display: block;
-			margin: 0 8px 0 0;
-			width: 38px;
-			height: 38px;
-			border-radius: 8px;
-		}
-
-		> .body {
-			flex: 1;
-			min-width: 0;
-
-			> .header {
-				margin-bottom: 2px;
-			}
-
-			> .body {
-				> .cw {
-					cursor: default;
-					display: block;
-					margin: 0;
-					padding: 0;
-					overflow-wrap: break-word;
-
-					> .text {
-						margin-right: 8px;
-					}
-				}
-
-				> .content {
-					> .text {
-						margin: 0;
-						padding: 0;
-					}
-				}
-			}
-		}
-	}
-
-	> .reply {
-		border-left: solid 0.5px var(--divider);
-		margin-top: 10px;
-	}
-}
-</style>
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
deleted file mode 100644
index 681e819a22..0000000000
--- a/src/client/components/note.vue
+++ /dev/null
@@ -1,1228 +0,0 @@
-<template>
-<div
-	class="tkcbzcuz"
-	v-if="!muted"
-	v-show="!isDeleted"
-	:tabindex="!isDeleted ? '-1' : null"
-	:class="{ renote: isRenote }"
-	v-hotkey="keymap"
-	v-size="{ max: [500, 450, 350, 300] }"
->
-	<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
-	<div class="info" v-if="pinned"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
-	<div class="info" v-if="appearNote._prId_"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
-	<div class="info" v-if="appearNote._featuredId_"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
-	<div class="renote" v-if="isRenote">
-		<MkAvatar class="avatar" :user="note.user"/>
-		<i class="fas fa-retweet"></i>
-		<I18n :src="$ts.renotedBy" tag="span">
-			<template #user>
-				<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
-					<MkUserName :user="note.user"/>
-				</MkA>
-			</template>
-		</I18n>
-		<div class="info">
-			<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
-				<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
-				<MkTime :time="note.createdAt"/>
-			</button>
-			<span class="visibility" v-if="note.visibility !== 'public'">
-				<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
-				<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
-				<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
-			</span>
-			<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
-		</div>
-	</div>
-	<article class="article" @contextmenu.stop="onContextmenu">
-		<MkAvatar class="avatar" :user="appearNote.user"/>
-		<div class="main">
-			<XNoteHeader class="header" :note="appearNote" :mini="true"/>
-			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
-			<div class="body">
-				<p v-if="appearNote.cw != null" class="cw">
-					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
-					<XCwButton v-model="showContent" :note="appearNote"/>
-				</p>
-				<div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent">
-					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
-						<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
-						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
-						<a class="rp" v-if="appearNote.renote != null">RN:</a>
-						<div class="translation" v-if="translating || translation">
-							<MkLoading v-if="translating" mini/>
-							<div class="translated" v-else>
-								<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}:</b>
-								{{ translation.text }}
-							</div>
-						</div>
-					</div>
-					<div class="files" v-if="appearNote.files.length > 0">
-						<XMediaList :media-list="appearNote.files"/>
-					</div>
-					<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
-					<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
-					<div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
-					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
-						<span>{{ $ts.showMore }}</span>
-					</button>
-				</div>
-				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
-			</div>
-			<footer class="footer">
-				<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
-				<button @click="reply()" class="button _button">
-					<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
-					<template v-else><i class="fas fa-reply"></i></template>
-					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
-				</button>
-				<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton">
-					<i class="fas fa-retweet"></i><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
-				</button>
-				<button v-else class="button _button">
-					<i class="fas fa-ban"></i>
-				</button>
-				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
-					<i class="fas fa-plus"></i>
-				</button>
-				<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
-					<i class="fas fa-minus"></i>
-				</button>
-				<button class="button _button" @click="menu()" ref="menuButton">
-					<i class="fas fa-ellipsis-h"></i>
-				</button>
-			</footer>
-		</div>
-	</article>
-</div>
-<div v-else class="muted" @click="muted = false">
-	<I18n :src="$ts.userSaysSomething" tag="small">
-		<template #name>
-			<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
-				<MkUserName :user="appearNote.user"/>
-			</MkA>
-		</template>
-	</I18n>
-</div>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
-import * as mfm from 'mfm-js';
-import { sum } from '../../prelude/array';
-import XSub from './note.sub.vue';
-import XNoteHeader from './note-header.vue';
-import XNoteSimple from './note-simple.vue';
-import XReactionsViewer from './reactions-viewer.vue';
-import XMediaList from './media-list.vue';
-import XCwButton from './cw-button.vue';
-import XPoll from './poll.vue';
-import { pleaseLogin } from '@client/scripts/please-login';
-import { focusPrev, focusNext } from '@client/scripts/focus';
-import { url } from '@client/config';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { checkWordMute } from '@client/scripts/check-word-mute';
-import { userPage } from '@client/filters/user';
-import * as os from '@client/os';
-import { noteActions, noteViewInterruptors } from '@client/store';
-import { reactionPicker } from '@client/scripts/reaction-picker';
-import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
-
-export default defineComponent({
-	components: {
-		XSub,
-		XNoteHeader,
-		XNoteSimple,
-		XReactionsViewer,
-		XMediaList,
-		XCwButton,
-		XPoll,
-		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
-		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
-	},
-
-	inject: {
-		inChannel: {
-			default: null
-		},
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		pinned: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['update:note'],
-
-	data() {
-		return {
-			connection: null,
-			replies: [],
-			showContent: false,
-			collapsed: false,
-			isDeleted: false,
-			muted: false,
-			translation: null,
-			translating: false,
-		};
-	},
-
-	computed: {
-		rs() {
-			return this.$store.state.reactions;
-		},
-		keymap(): any {
-			return {
-				'r': () => this.reply(true),
-				'e|a|plus': () => this.react(true),
-				'q': () => this.renote(true),
-				'f|b': this.favorite,
-				'delete|ctrl+d': this.del,
-				'ctrl+q': this.renoteDirectly,
-				'up|k|shift+tab': this.focusBefore,
-				'down|j|tab': this.focusAfter,
-				'esc': this.blur,
-				'm|o': () => this.menu(true),
-				's': this.toggleShowContent,
-				'1': () => this.reactDirectly(this.rs[0]),
-				'2': () => this.reactDirectly(this.rs[1]),
-				'3': () => this.reactDirectly(this.rs[2]),
-				'4': () => this.reactDirectly(this.rs[3]),
-				'5': () => this.reactDirectly(this.rs[4]),
-				'6': () => this.reactDirectly(this.rs[5]),
-				'7': () => this.reactDirectly(this.rs[6]),
-				'8': () => this.reactDirectly(this.rs[7]),
-				'9': () => this.reactDirectly(this.rs[8]),
-				'0': () => this.reactDirectly(this.rs[9]),
-			};
-		},
-
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.fileIds.length == 0 &&
-				this.note.poll == null);
-		},
-
-		appearNote(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-
-		isMyNote(): boolean {
-			return this.$i && (this.$i.id === this.appearNote.userId);
-		},
-
-		isMyRenote(): boolean {
-			return this.$i && (this.$i.id === this.note.userId);
-		},
-
-		canRenote(): boolean {
-			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
-		},
-
-		reactionsCount(): number {
-			return this.appearNote.reactions
-				? sum(Object.values(this.appearNote.reactions))
-				: 0;
-		},
-
-		urls(): string[] {
-			if (this.appearNote.text) {
-				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
-			} else {
-				return null;
-			}
-		},
-
-		showTicker() {
-			if (this.$store.state.instanceTicker === 'always') return true;
-			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
-			return false;
-		}
-	},
-
-	async created() {
-		if (this.$i) {
-			this.connection = os.stream;
-		}
-
-		this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
-			(this.appearNote.text.split('\n').length > 9) ||
-			(this.appearNote.text.length > 500)
-		);
-		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
-
-		// plugin
-		if (noteViewInterruptors.length > 0) {
-			let result = this.note;
-			for (const interruptor of noteViewInterruptors) {
-				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
-			}
-			this.$emit('update:note', Object.freeze(result));
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$i) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-	},
-
-	beforeUnmount() {
-		this.decapture(true);
-
-		if (this.$i) {
-			this.connection.off('_connected_', this.onStreamConnected);
-		}
-	},
-
-	methods: {
-		updateAppearNote(v) {
-			this.$emit('update:note', Object.freeze(this.isRenote ? {
-				...this.note,
-				renote: {
-					...this.note.renote,
-					...v
-				}
-			} : {
-				...this.note,
-				...v
-			}));
-		},
-
-		readPromo() {
-			os.api('promo/read', {
-				noteId: this.appearNote.id
-			});
-			this.isDeleted = true;
-		},
-
-		capture(withHandler = false) {
-			if (this.$i) {
-				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
-				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
-				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$i) {
-				this.connection.send('un', {
-					id: this.appearNote.id
-				});
-				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		onStreamConnected() {
-			this.capture();
-		},
-
-		onStreamNoteUpdated(data) {
-			const { type, id, body } = data;
-
-			if (id !== this.appearNote.id) return;
-
-			switch (type) {
-				case 'reacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					if (body.emoji) {
-						const emojis = this.appearNote.emojis || [];
-						if (!emojis.includes(body.emoji)) {
-							n.emojis = [...emojis, body.emoji];
-						}
-					}
-
-					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Increment the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: currentCount + 1
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = reaction;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'unreacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Decrement the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: Math.max(0, currentCount - 1)
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = null;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'pollVoted': {
-					const choice = body.choice;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					const choices = [...this.appearNote.poll.choices];
-					choices[choice] = {
-						...choices[choice],
-						votes: choices[choice].votes + 1,
-						...(body.userId === this.$i.id ? {
-							isVoted: true
-						} : {})
-					};
-
-					n.poll = {
-						...this.appearNote.poll,
-						choices: choices
-					};
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'deleted': {
-					this.isDeleted = true;
-					break;
-				}
-			}
-		},
-
-		reply(viaKeyboard = false) {
-			pleaseLogin();
-			os.post({
-				reply: this.appearNote,
-				animation: !viaKeyboard,
-			}, () => {
-				this.focus();
-			});
-		},
-
-		renote(viaKeyboard = false) {
-			pleaseLogin();
-			this.blur();
-			os.popupMenu([{
-				text: this.$ts.renote,
-				icon: 'fas fa-retweet',
-				action: () => {
-					os.api('notes/create', {
-						renoteId: this.appearNote.id
-					});
-				}
-			}, {
-				text: this.$ts.quote,
-				icon: 'fas fa-quote-right',
-				action: () => {
-					os.post({
-						renote: this.appearNote,
-					});
-				}
-			}], this.$refs.renoteButton, {
-				viaKeyboard
-			});
-		},
-
-		renoteDirectly() {
-			os.apiWithDialog('notes/create', {
-				renoteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.dialog({
-					type: 'success',
-					text: this.$ts.renoted,
-				});
-			}, (e: Error) => {
-				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantRenote,
-					});
-				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantReRenote,
-					});
-				}
-			});
-		},
-
-		react(viaKeyboard = false) {
-			pleaseLogin();
-			this.blur();
-			reactionPicker.show(this.$refs.reactButton, reaction => {
-				os.api('notes/reactions/create', {
-					noteId: this.appearNote.id,
-					reaction: reaction
-				});
-			}, () => {
-				this.focus();
-			});
-		},
-
-		reactDirectly(reaction) {
-			os.api('notes/reactions/create', {
-				noteId: this.appearNote.id,
-				reaction: reaction
-			});
-		},
-
-		undoReact(note) {
-			const oldReaction = note.myReaction;
-			if (!oldReaction) return;
-			os.api('notes/reactions/delete', {
-				noteId: note.id
-			});
-		},
-
-		favorite() {
-			pleaseLogin();
-			os.apiWithDialog('notes/favorites/create', {
-				noteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.dialog({
-					type: 'success',
-					text: this.$ts.favorited,
-				});
-			}, (e: Error) => {
-				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.alreadyFavorited,
-					});
-				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantFavorite,
-					});
-				}
-			});
-		},
-
-		del() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.noteDeleteConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-			});
-		},
-
-		delEdit() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.deleteAndEditConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-
-				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
-			});
-		},
-
-		toggleFavorite(favorite: boolean) {
-			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleWatch(watch: boolean) {
-			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleThreadMute(mute: boolean) {
-			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		getMenu() {
-			let menu;
-			if (this.$i) {
-				const statePromise = os.api('notes/state', {
-					noteId: this.appearNote.id
-				});
-
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined,
-				{
-					icon: 'fas fa-share-alt',
-					text: this.$ts.share,
-					action: this.share
-				},
-				this.$instance.translatorAvailable ? {
-					icon: 'fas fa-language',
-					text: this.$ts.translate,
-					action: this.translate
-				} : undefined,
-				null,
-				statePromise.then(state => state.isFavorited ? {
-					icon: 'fas fa-star',
-					text: this.$ts.unfavorite,
-					action: () => this.toggleFavorite(false)
-				} : {
-					icon: 'fas fa-star',
-					text: this.$ts.favorite,
-					action: () => this.toggleFavorite(true)
-				}),
-				{
-					icon: 'fas fa-paperclip',
-					text: this.$ts.clip,
-					action: () => this.clip()
-				},
-				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
-					icon: 'fas fa-eye-slash',
-					text: this.$ts.unwatch,
-					action: () => this.toggleWatch(false)
-				} : {
-					icon: 'fas fa-eye',
-					text: this.$ts.watch,
-					action: () => this.toggleWatch(true)
-				}) : undefined,
-				statePromise.then(state => state.isMutedThread ? {
-					icon: 'fas fa-comment-slash',
-					text: this.$ts.unmuteThread,
-					action: () => this.toggleThreadMute(false)
-				} : {
-					icon: 'fas fa-comment-slash',
-					text: this.$ts.muteThread,
-					action: () => this.toggleThreadMute(true)
-				}),
-				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.unpin,
-					action: () => this.togglePin(false)
-				} : {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.pin,
-					action: () => this.togglePin(true)
-				} : undefined,
-				...(this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					{
-						icon: 'fas fa-bullhorn',
-						text: this.$ts.promote,
-						action: this.promote
-					}]
-					: []
-				),
-				...(this.appearNote.userId != this.$i.id ? [
-					null,
-					{
-						icon: 'fas fa-exclamation-circle',
-						text: this.$ts.reportAbuse,
-						action: () => {
-							const u = `${url}/notes/${this.appearNote.id}`;
-							os.popup(import('@client/components/abuse-report-window.vue'), {
-								user: this.appearNote.user,
-								initialComment: `Note: ${u}\n-----\n`
-							}, {}, 'closed');
-						}
-					}]
-					: []
-				),
-				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					this.appearNote.userId == this.$i.id ? {
-						icon: 'fas fa-edit',
-						text: this.$ts.deleteAndEdit,
-						action: this.delEdit
-					} : undefined,
-					{
-						icon: 'fas fa-trash-alt',
-						text: this.$ts.delete,
-						danger: true,
-						action: this.del
-					}]
-					: []
-				)]
-				.filter(x => x !== undefined);
-			} else {
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined]
-				.filter(x => x !== undefined);
-			}
-
-			if (noteActions.length > 0) {
-				menu = menu.concat([null, ...noteActions.map(action => ({
-					icon: 'fas fa-plug',
-					text: action.title,
-					action: () => {
-						action.handler(this.appearNote);
-					}
-				}))]);
-			}
-
-			return menu;
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (window.getSelection().toString() !== '') return;
-
-			if (this.$store.state.useReactionPickerForContextMenu) {
-				e.preventDefault();
-				this.react();
-			} else {
-				os.contextMenu(this.getMenu(), e).then(this.focus);
-			}
-		},
-
-		menu(viaKeyboard = false) {
-			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
-				viaKeyboard
-			}).then(this.focus);
-		},
-
-		showRenoteMenu(viaKeyboard = false) {
-			if (!this.isMyRenote) return;
-			os.popupMenu([{
-				text: this.$ts.unrenote,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: () => {
-					os.api('notes/delete', {
-						noteId: this.note.id
-					});
-					this.isDeleted = true;
-				}
-			}], this.$refs.renoteTime, {
-				viaKeyboard: viaKeyboard
-			});
-		},
-
-		toggleShowContent() {
-			this.showContent = !this.showContent;
-		},
-
-		copyContent() {
-			copyToClipboard(this.appearNote.text);
-			os.success();
-		},
-
-		copyLink() {
-			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
-			os.success();
-		},
-
-		togglePin(pin: boolean) {
-			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
-				noteId: this.appearNote.id
-			}, undefined, null, e => {
-				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.pinLimitExceeded
-					});
-				}
-			});
-		},
-
-		async clip() {
-			const clips = await os.api('clips/list');
-			os.popupMenu([{
-				icon: 'fas fa-plus',
-				text: this.$ts.createNew,
-				action: async () => {
-					const { canceled, result } = await os.form(this.$ts.createNewClip, {
-						name: {
-							type: 'string',
-							label: this.$ts.name
-						},
-						description: {
-							type: 'string',
-							required: false,
-							multiline: true,
-							label: this.$ts.description
-						},
-						isPublic: {
-							type: 'boolean',
-							label: this.$ts.public,
-							default: false
-						}
-					});
-					if (canceled) return;
-
-					const clip = await os.apiWithDialog('clips/create', result);
-
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}, null, ...clips.map(clip => ({
-				text: clip.name,
-				action: () => {
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}))], this.$refs.menuButton, {
-			}).then(this.focus);
-		},
-
-		async promote() {
-			const { canceled, result: days } = await os.dialog({
-				title: this.$ts.numberOfDays,
-				input: { type: 'number' }
-			});
-
-			if (canceled) return;
-
-			os.apiWithDialog('admin/promo/create', {
-				noteId: this.appearNote.id,
-				expiresAt: Date.now() + (86400000 * days)
-			});
-		},
-
-		share() {
-			navigator.share({
-				title: this.$t('noteOf', { user: this.appearNote.user.name }),
-				text: this.appearNote.text,
-				url: `${url}/notes/${this.appearNote.id}`
-			});
-		},
-
-		async translate() {
-			if (this.translation != null) return;
-			this.translating = true;
-			const res = await os.api('notes/translate', {
-				noteId: this.appearNote.id,
-				targetLang: localStorage.getItem('lang') || navigator.language,
-			});
-			this.translating = false;
-			this.translation = res;
-		},
-
-		focus() {
-			this.$el.focus();
-		},
-
-		blur() {
-			this.$el.blur();
-		},
-
-		focusBefore() {
-			focusPrev(this.$el);
-		},
-
-		focusAfter() {
-			focusNext(this.$el);
-		},
-
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.tkcbzcuz {
-	position: relative;
-	transition: box-shadow 0.1s ease;
-	overflow: clip;
-	contain: content;
-
-	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
-	// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
-	// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
-	// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
-	// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか？)
-	//content-visibility: auto;
-  //contain-intrinsic-size: 0 128px;
-
-	&:focus-visible {
-		outline: none;
-
-		&:after {
-			content: "";
-			pointer-events: none;
-			display: block;
-			position: absolute;
-			z-index: 10;
-			top: 0;
-			left: 0;
-			right: 0;
-			bottom: 0;
-			margin: auto;
-			width: calc(100% - 8px);
-			height: calc(100% - 8px);
-			border: dashed 1px var(--focus);
-			border-radius: var(--radius);
-			box-sizing: border-box;
-		}
-	}
-
-	&:hover > .article > .main > .footer > .button {
-		opacity: 1;
-	}
-
-	> .info {
-		display: flex;
-		align-items: center;
-		padding: 16px 32px 8px 32px;
-		line-height: 24px;
-		font-size: 90%;
-		white-space: pre;
-		color: #d28a3f;
-
-		> i {
-			margin-right: 4px;
-		}
-
-		> .hide {
-			margin-left: auto;
-			color: inherit;
-		}
-	}
-
-	> .info + .article {
-		padding-top: 8px;
-	}
-
-	> .reply-to {
-		opacity: 0.7;
-		padding-bottom: 0;
-	}
-
-	> .renote {
-		display: flex;
-		align-items: center;
-		padding: 16px 32px 8px 32px;
-		line-height: 28px;
-		white-space: pre;
-		color: var(--renote);
-
-		> .avatar {
-			flex-shrink: 0;
-			display: inline-block;
-			width: 28px;
-			height: 28px;
-			margin: 0 8px 0 0;
-			border-radius: 6px;
-		}
-
-		> i {
-			margin-right: 4px;
-		}
-
-		> span {
-			overflow: hidden;
-			flex-shrink: 1;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-
-			> .name {
-				font-weight: bold;
-			}
-		}
-
-		> .info {
-			margin-left: auto;
-			font-size: 0.9em;
-
-			> .time {
-				flex-shrink: 0;
-				color: inherit;
-
-				> .dropdownIcon {
-					margin-right: 4px;
-				}
-			}
-
-			> .visibility {
-				margin-left: 8px;
-			}
-
-			> .localOnly {
-				margin-left: 8px;
-			}
-		}
-	}
-
-	> .renote + .article {
-		padding-top: 8px;
-	}
-
-	> .article {
-		display: flex;
-		padding: 28px 32px 18px;
-
-		> .avatar {
-			flex-shrink: 0;
-			display: block;
-			margin: 0 14px 8px 0;
-			width: 58px;
-			height: 58px;
-			position: sticky;
-			top: calc(22px + var(--stickyTop, 0px));
-			left: 0;
-		}
-
-		> .main {
-			flex: 1;
-			min-width: 0;
-
-			> .body {
-				> .cw {
-					cursor: default;
-					display: block;
-					margin: 0;
-					padding: 0;
-					overflow-wrap: break-word;
-
-					> .text {
-						margin-right: 8px;
-					}
-				}
-
-				> .content {
-					&.collapsed {
-						position: relative;
-						max-height: 9em;
-						overflow: hidden;
-
-						> .fade {
-							display: block;
-							position: absolute;
-							bottom: 0;
-							left: 0;
-							width: 100%;
-							height: 64px;
-							background: linear-gradient(0deg, var(--panel), var(--X15));
-
-							> span {
-								display: inline-block;
-								background: var(--panel);
-								padding: 6px 10px;
-								font-size: 0.8em;
-								border-radius: 999px;
-								box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
-							}
-
-							&:hover {
-								> span {
-									background: var(--panelHighlight);
-								}
-							}
-						}
-					}
-
-					> .text {
-						overflow-wrap: break-word;
-
-						> .reply {
-							color: var(--accent);
-							margin-right: 0.5em;
-						}
-
-						> .rp {
-							margin-left: 4px;
-							font-style: oblique;
-							color: var(--renote);
-						}
-
-						> .translation {
-							border: solid 0.5px var(--divider);
-							border-radius: var(--radius);
-							padding: 12px;
-							margin-top: 8px;
-						}
-					}
-
-					> .url-preview {
-						margin-top: 8px;
-					}
-
-					> .poll {
-						font-size: 80%;
-					}
-
-					> .renote {
-						padding: 8px 0;
-
-						> * {
-							padding: 16px;
-							border: dashed 1px var(--renote);
-							border-radius: 8px;
-						}
-					}
-				}
-
-				> .channel {
-					opacity: 0.7;
-					font-size: 80%;
-				}
-			}
-
-			> .footer {
-				> .button {
-					margin: 0;
-					padding: 8px;
-					opacity: 0.7;
-
-					&:not(:last-child) {
-						margin-right: 28px;
-					}
-
-					&:hover {
-						color: var(--fgHighlighted);
-					}
-
-					> .count {
-						display: inline;
-						margin: 0 0 0 8px;
-						opacity: 0.7;
-					}
-
-					&.reacted {
-						color: var(--accent);
-					}
-				}
-			}
-		}
-	}
-
-	> .reply {
-		border-top: solid 0.5px var(--divider);
-	}
-
-	&.max-width_500px {
-		font-size: 0.9em;
-	}
-
-	&.max-width_450px {
-		> .renote {
-			padding: 8px 16px 0 16px;
-		}
-
-		> .info {
-			padding: 8px 16px 0 16px;
-		}
-
-		> .article {
-			padding: 14px 16px 9px;
-
-			> .avatar {
-				margin: 0 10px 8px 0;
-				width: 50px;
-				height: 50px;
-				top: calc(14px + var(--stickyTop, 0px));
-			}
-		}
-	}
-
-	&.max-width_350px {
-		> .article {
-			> .main {
-				> .footer {
-					> .button {
-						&:not(:last-child) {
-							margin-right: 18px;
-						}
-					}
-				}
-			}
-		}
-	}
-
-	&.max-width_300px {
-		font-size: 0.825em;
-
-		> .article {
-			> .avatar {
-				width: 44px;
-				height: 44px;
-			}
-
-			> .main {
-				> .footer {
-					> .button {
-						&:not(:last-child) {
-							margin-right: 12px;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-
-.muted {
-	padding: 8px;
-	text-align: center;
-	opacity: 0.7;
-}
-</style>
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
deleted file mode 100644
index 919cb29952..0000000000
--- a/src/client/components/notes.vue
+++ /dev/null
@@ -1,130 +0,0 @@
-<template>
-<transition name="fade" mode="out-in">
-	<MkLoading v-if="fetching"/>
-
-	<MkError v-else-if="error" @retry="init()"/>
-
-	<div class="_fullinfo" v-else-if="empty">
-		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
-		<div>{{ $ts.noNotes }}</div>
-	</div>
-
-	<div v-else class="giivymft" :class="{ noGap }">
-		<div v-show="more && reversed" style="margin-bottom: var(--margin);">
-			<MkButton style="margin: 0 auto;" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-				<template v-if="moreFetching"><MkLoading inline/></template>
-			</MkButton>
-		</div>
-
-		<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true" class="notes">
-			<XNote class="qtqtichx" :note="note" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
-		</XList>
-
-		<div v-show="more && !reversed" style="margin-top: var(--margin);">
-			<MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-				<template v-if="moreFetching"><MkLoading inline/></template>
-			</MkButton>
-		</div>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@client/scripts/paging';
-import XNote from './note.vue';
-import XList from './date-separated-list.vue';
-import MkButton from '@client/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		XNote, XList, MkButton,
-	},
-
-	mixins: [
-		paging({
-			before: (self) => {
-				self.$emit('before');
-			},
-
-			after: (self, e) => {
-				self.$emit('after', e);
-			}
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-		prop: {
-			type: String,
-			required: false
-		},
-		noGap: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['before', 'after'],
-
-	computed: {
-		notes(): any[] {
-			return this.prop ? this.items.map(item => item[this.prop]) : this.items;
-		},
-
-		reversed(): boolean {
-			return this.pagination.reversed;
-		}
-	},
-
-	methods: {
-		updated(oldValue, newValue) {
-			const i = this.notes.findIndex(n => n === oldValue);
-			if (this.prop) {
-				this.items[i][this.prop] = newValue;
-			} else {
-				this.items[i] = newValue;
-			}
-		},
-
-		focus() {
-			this.$refs.notes.focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.giivymft {
-	&.noGap {
-		> .notes {
-			background: var(--panel);
-		}
-	}
-
-	&:not(.noGap) {
-		> .notes {
-			background: var(--bg);
-
-			.qtqtichx {
-				background: var(--panel);
-				border-radius: var(--radius);
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/notification-setting-window.vue b/src/client/components/notification-setting-window.vue
deleted file mode 100644
index 14e0b76cc6..0000000000
--- a/src/client/components/notification-setting-window.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="400"
-	:height="450"
-	:with-ok-button="true"
-	:ok-button-disabled="false"
-	@ok="ok()"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
->
-	<template #header>{{ $ts.notificationSetting }}</template>
-	<div class="_monolithic_">
-		<div v-if="showGlobalToggle" class="_section">
-			<MkSwitch v-model="useGlobalSetting">
-				{{ $ts.useGlobalSetting }}
-				<template #caption>{{ $ts.useGlobalSettingDesc }}</template>
-			</MkSwitch>
-		</div>
-		<div v-if="!useGlobalSetting" class="_section">
-			<MkInfo>{{ $ts.notificationSettingDesc }}</MkInfo>
-			<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
-			<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
-			<MkSwitch v-for="type in notificationTypes" :key="type" v-model="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch>
-		</div>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkSwitch from './form/switch.vue';
-import MkInfo from './ui/info.vue';
-import MkButton from './ui/button.vue';
-import { notificationTypes } from '@/types';
-
-export default defineComponent({
-	components: {
-		XModalWindow,
-		MkSwitch,
-		MkInfo,
-		MkButton
-	},
-
-	props: {
-		includingTypes: {
-			// TODO: これで型に合わないものを弾いてくれるのかどうか要調査
-			type: Array as PropType<typeof notificationTypes[number][]>,
-			required: false,
-			default: null,
-		},
-		showGlobalToggle: {
-			type: Boolean,
-			required: false,
-			default: true,
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			typesMap: {} as Record<typeof notificationTypes[number], boolean>,
-			useGlobalSetting: false,
-			notificationTypes,
-		};
-	},
-
-	created() {
-		this.useGlobalSetting = this.includingTypes === null && this.showGlobalToggle;
-
-		for (const type of this.notificationTypes) {
-			this.typesMap[type] = this.includingTypes === null || this.includingTypes.includes(type);
-		}
-	},
-
-	methods: {
-		ok() {
-			const includingTypes = this.useGlobalSetting ? null : (Object.keys(this.typesMap) as typeof notificationTypes[number][])
-				.filter(type => this.typesMap[type]);
-
-			this.$emit('done', { includingTypes });
-			this.$refs.dialog.close();
-		},
-
-		disableAll() {
-			for (const type in this.typesMap) {
-				this.typesMap[type as typeof notificationTypes[number]] = false;
-			}
-		},
-
-		enableAll() {
-			for (const type in this.typesMap) {
-				this.typesMap[type as typeof notificationTypes[number]] = true;
-			}
-		}
-	}
-});
-</script>
diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue
deleted file mode 100644
index ce1fa5b160..0000000000
--- a/src/client/components/notification.vue
+++ /dev/null
@@ -1,362 +0,0 @@
-<template>
-<div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }" ref="elRef">
-	<div class="head">
-		<MkAvatar v-if="notification.user" class="icon" :user="notification.user"/>
-		<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
-		<div class="sub-icon" :class="notification.type">
-			<i v-if="notification.type === 'follow'" class="fas fa-plus"></i>
-			<i v-else-if="notification.type === 'receiveFollowRequest'" class="fas fa-clock"></i>
-			<i v-else-if="notification.type === 'followRequestAccepted'" class="fas fa-check"></i>
-			<i v-else-if="notification.type === 'groupInvited'" class="fas fa-id-card-alt"></i>
-			<i v-else-if="notification.type === 'renote'" class="fas fa-retweet"></i>
-			<i v-else-if="notification.type === 'reply'" class="fas fa-reply"></i>
-			<i v-else-if="notification.type === 'mention'" class="fas fa-at"></i>
-			<i v-else-if="notification.type === 'quote'" class="fas fa-quote-left"></i>
-			<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
-			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
-			<XReactionIcon v-else-if="notification.type === 'reaction'"
-				:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
-				:custom-emojis="notification.note.emojis"
-				:no-style="true"
-				@touchstart.passive="onReactionMouseover"
-				@mouseover="onReactionMouseover"
-				@mouseleave="onReactionMouseleave"
-				@touchend="onReactionMouseleave"
-				ref="reactionRef"
-			/>
-		</div>
-	</div>
-	<div class="tail">
-		<header>
-			<MkA v-if="notification.user" class="name" :to="userPage(notification.user)" v-user-preview="notification.user.id"><MkUserName :user="notification.user"/></MkA>
-			<span v-else>{{ notification.header }}</span>
-			<MkTime :time="notification.createdAt" v-if="withTime" class="time"/>
-		</header>
-		<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
-			<i class="fas fa-quote-left"></i>
-			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
-			<i class="fas fa-quote-right"></i>
-		</MkA>
-		<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
-			<i class="fas fa-quote-left"></i>
-			<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/>
-			<i class="fas fa-quote-right"></i>
-		</MkA>
-		<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
-			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
-		</MkA>
-		<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
-			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
-		</MkA>
-		<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
-			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
-		</MkA>
-		<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
-			<i class="fas fa-quote-left"></i>
-			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
-			<i class="fas fa-quote-right"></i>
-		</MkA>
-		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
-		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $ts.followRequestAccepted }}</span>
-		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $ts.reject }}</button></div></span>
-		<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $ts.reject }}</button></div></span>
-		<span v-if="notification.type === 'app'" class="text">
-			<Mfm :text="notification.body" :nowrap="!full"/>
-		</span>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
-import { getNoteSummary } from '@/misc/get-note-summary';
-import XReactionIcon from './reaction-icon.vue';
-import MkFollowButton from './follow-button.vue';
-import XReactionTooltip from './reaction-tooltip.vue';
-import notePage from '@client/filters/note';
-import { userPage } from '@client/filters/user';
-import { i18n } from '@client/i18n';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XReactionIcon, MkFollowButton
-	},
-
-	props: {
-		notification: {
-			type: Object,
-			required: true,
-		},
-		withTime: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		full: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-
-	setup(props) {
-		const elRef = ref<HTMLElement>(null);
-		const reactionRef = ref(null);
-
-		onMounted(() => {
-			let readObserver: IntersectionObserver = null;
-			let connection = null;
-
-			if (!props.notification.isRead) {
-				readObserver = new IntersectionObserver((entries, observer) => {
-					if (!entries.some(entry => entry.isIntersecting)) return;
-					os.stream.send('readNotification', {
-						id: props.notification.id
-					});
-					entries.map(({ target }) => observer.unobserve(target));
-				});
-
-				readObserver.observe(elRef.value);
-
-				connection = os.stream.useChannel('main');
-				connection.on('readAllNotifications', () => readObserver.unobserve(elRef.value));
-			}
-
-			onUnmounted(() => {
-				if (readObserver) readObserver.unobserve(elRef.value);
-				if (connection) connection.dispose();
-			});
-		});
-
-		const followRequestDone = ref(false);
-		const groupInviteDone = ref(false);
-
-		const acceptFollowRequest = () => {
-			followRequestDone.value = true;
-			os.api('following/requests/accept', { userId: props.notification.user.id });
-		};
-
-		const rejectFollowRequest = () => {
-			followRequestDone.value = true;
-			os.api('following/requests/reject', { userId: props.notification.user.id });
-		};
-
-		const acceptGroupInvitation = () => {
-			groupInviteDone.value = true;
-			os.apiWithDialog('users/groups/invitations/accept', { invitationId: props.notification.invitation.id });
-		};
-
-		const rejectGroupInvitation = () => {
-			groupInviteDone.value = true;
-			os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
-		};
-
-		let isReactionHovering = false;
-		let reactionTooltipTimeoutId;
-
-		const onReactionMouseover = () => {
-			if (isReactionHovering) return;
-			isReactionHovering = true;
-			reactionTooltipTimeoutId = setTimeout(openReactionTooltip, 300);
-		};
-
-		const onReactionMouseleave = () => {
-			if (!isReactionHovering) return;
-			isReactionHovering = false;
-			clearTimeout(reactionTooltipTimeoutId);
-			closeReactionTooltip();
-		};
-
-		let changeReactionTooltipShowingState: () => void;
-
-		const openReactionTooltip = () => {
-			closeReactionTooltip();
-			if (!isReactionHovering) return;
-
-			const showing = ref(true);
-			os.popup(XReactionTooltip, {
-				showing,
-				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
-				emojis: props.notification.note.emojis,
-				source: reactionRef.value.$el,
-			}, {}, 'closed');
-
-			changeReactionTooltipShowingState = () => {
-				showing.value = false;
-			};
-		};
-
-		const closeReactionTooltip = () => {
-			if (changeReactionTooltipShowingState != null) {
-				changeReactionTooltipShowingState();
-				changeReactionTooltipShowingState = null;
-			}
-		};
-
-		return {
-			getNoteSummary: (text: string) => getNoteSummary(text, i18n.locale),
-			followRequestDone,
-			groupInviteDone,
-			notePage,
-			userPage,
-			acceptFollowRequest,
-			rejectFollowRequest,
-			acceptGroupInvitation,
-			rejectGroupInvitation,
-			onReactionMouseover,
-			onReactionMouseleave,
-			elRef,
-			reactionRef,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.qglefbjs {
-	position: relative;
-	box-sizing: border-box;
-	padding: 24px 32px;
-	font-size: 0.9em;
-	overflow-wrap: break-word;
-	display: flex;
-	contain: content;
-
-	&.max-width_600px {
-		padding: 16px;
-		font-size: 0.9em;
-	}
-
-	&.max-width_500px {
-		padding: 12px;
-		font-size: 0.8em;
-	}
-
-	&:after {
-		content: "";
-		display: block;
-		clear: both;
-	}
-
-	> .head {
-		position: sticky;
-		top: 0;
-		flex-shrink: 0;
-		width: 42px;
-		height: 42px;
-		margin-right: 8px;
-
-		> .icon {
-			display: block;
-			width: 100%;
-			height: 100%;
-			border-radius: 6px;
-		}
-
-		> .sub-icon {
-			position: absolute;
-			z-index: 1;
-			bottom: -2px;
-			right: -2px;
-			width: 20px;
-			height: 20px;
-			box-sizing: border-box;
-			border-radius: 100%;
-			background: var(--panel);
-			box-shadow: 0 0 0 3px var(--panel);
-			font-size: 12px;
-			text-align: center;
-
-			&:empty {
-				display: none;
-			}
-
-			> * {
-				color: #fff;
-				width: 100%;
-				height: 100%;
-			}
-
-			&.follow, &.followRequestAccepted, &.receiveFollowRequest, &.groupInvited {
-				padding: 3px;
-				background: #36aed2;
-				pointer-events: none;
-			}
-
-			&.renote {
-				padding: 3px;
-				background: #36d298;
-				pointer-events: none;
-			}
-
-			&.quote {
-				padding: 3px;
-				background: #36d298;
-				pointer-events: none;
-			}
-
-			&.reply {
-				padding: 3px;
-				background: #007aff;
-				pointer-events: none;
-			}
-
-			&.mention {
-				padding: 3px;
-				background: #88a6b7;
-				pointer-events: none;
-			}
-
-			&.pollVote {
-				padding: 3px;
-				background: #88a6b7;
-				pointer-events: none;
-			}
-		}
-	}
-
-	> .tail {
-		flex: 1;
-		min-width: 0;
-
-		> header {
-			display: flex;
-			align-items: baseline;
-			white-space: nowrap;
-
-			> .name {
-				text-overflow: ellipsis;
-				white-space: nowrap;
-				min-width: 0;
-				overflow: hidden;
-			}
-
-			> .time {
-				margin-left: auto;
-				font-size: 0.9em;
-			}
-		}
-
-		> .text {
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-
-			> i {
-				vertical-align: super;
-				font-size: 50%;
-				opacity: 0.5;
-			}
-
-			> i:first-child {
-				margin-right: 4px;
-			}
-
-			> i:last-child {
-				margin-left: 4px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
deleted file mode 100644
index 78c1cce0c7..0000000000
--- a/src/client/components/notifications.vue
+++ /dev/null
@@ -1,159 +0,0 @@
-<template>
-<transition name="fade" mode="out-in">
-	<MkLoading v-if="fetching"/>
-
-	<MkError v-else-if="error" @retry="init()"/>
-
-	<p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p>
-
-	<div v-else>
-		<XList class="elsfgstc" :items="items" v-slot="{ item: notification }" :no-gap="true">
-			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/>
-			<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
-		</XList>
-
-		<MkButton primary style="margin: var(--margin) auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-			<template v-if="moreFetching"><MkLoading inline/></template>
-		</MkButton>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType, markRaw } from 'vue';
-import paging from '@client/scripts/paging';
-import XNotification from './notification.vue';
-import XList from './date-separated-list.vue';
-import XNote from './note.vue';
-import { notificationTypes } from '@/types';
-import * as os from '@client/os';
-import MkButton from '@client/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		XNotification,
-		XList,
-		XNote,
-		MkButton,
-	},
-
-	mixins: [
-		paging({}),
-	],
-
-	props: {
-		includeTypes: {
-			type: Array as PropType<typeof notificationTypes[number][]>,
-			required: false,
-			default: null,
-		},
-		unreadOnly: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-
-	data() {
-		return {
-			connection: null,
-			pagination: {
-				endpoint: 'i/notifications',
-				limit: 10,
-				params: () => ({
-					includeTypes: this.allIncludeTypes || undefined,
-					unreadOnly: this.unreadOnly,
-				})
-			},
-		};
-	},
-
-	computed: {
-		allIncludeTypes() {
-			return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
-		}
-	},
-
-	watch: {
-		includeTypes: {
-			handler() {
-				this.reload();
-			},
-			deep: true
-		},
-		unreadOnly: {
-			handler() {
-				this.reload();
-			},
-		},
-		// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、
-		// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
-		'$i.mutingNotificationTypes': {
-			handler() {
-				if (this.includeTypes === null) {
-					this.reload();
-				}
-			},
-			deep: true
-		}
-	},
-
-	mounted() {
-		this.connection = markRaw(os.stream.useChannel('main'));
-		this.connection.on('notification', this.onNotification);
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		onNotification(notification) {
-			const isMuted = !this.allIncludeTypes.includes(notification.type);
-			if (isMuted || document.visibilityState === 'visible') {
-				os.stream.send('readNotification', {
-					id: notification.id
-				});
-			}
-
-			if (!isMuted) {
-				this.prepend({
-					...notification,
-					isRead: document.visibilityState === 'visible'
-				});
-			}
-		},
-
-		noteUpdated(oldValue, newValue) {
-			const i = this.items.findIndex(n => n.note === oldValue);
-			this.items[i] = {
-				...this.items[i],
-				note: newValue
-			};
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.mfcuwfyp {
-	margin: 0;
-	padding: 16px;
-	text-align: center;
-	color: var(--fg);
-}
-
-.elsfgstc {
-	background: var(--panel);
-}
-</style>
diff --git a/src/client/components/number-diff.vue b/src/client/components/number-diff.vue
deleted file mode 100644
index 690f89dd59..0000000000
--- a/src/client/components/number-diff.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
-	<slot name="before"></slot>{{ isPlus ? '+' : '' }}{{ number(value) }}<slot name="after"></slot>
-</span>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import number from '@client/filters/number';
-
-export default defineComponent({
-	props: {
-		value: {
-			type: Number,
-			required: true
-		},
-	},
-
-	setup(props) {
-		const isPlus = computed(() => props.value > 0);
-		const isMinus = computed(() => props.value < 0);
-		const isZero = computed(() => props.value === 0);
-		return {
-			isPlus,
-			isMinus,
-			isZero,
-			number,
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ceaaebcd {
-	&.isPlus {
-		color: var(--success);
-	}
-
-	&.isMinus {
-		color: var(--error);
-	}
-
-	&.isZero {
-		opacity: 0.5;
-	}
-}
-</style>
diff --git a/src/client/components/page-preview.vue b/src/client/components/page-preview.vue
deleted file mode 100644
index 090c4a6a6c..0000000000
--- a/src/client/components/page-preview.vue
+++ /dev/null
@@ -1,162 +0,0 @@
-<template>
-<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block _isolated" tabindex="-1">
-	<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
-	<article>
-		<header>
-			<h1 :title="page.title">{{ page.title }}</h1>
-		</header>
-		<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
-		<footer>
-			<img class="icon" :src="page.user.avatarUrl"/>
-			<p>{{ userName(page.user) }}</p>
-		</footer>
-	</article>
-</MkA>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { userName } from '@client/filters/user';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		page: {
-			type: Object,
-			required: true
-		},
-	},
-	methods: {
-		userName
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.vhpxefrj {
-	display: block;
-
-	&:hover {
-		text-decoration: none;
-		color: var(--accent);
-	}
-
-	> .thumbnail {
-		width: 100%;
-		height: 200px;
-		background-position: center;
-		background-size: cover;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-
-		> button {
-			font-size: 3.5em;
-			opacity: 0.7;
-
-			&:hover {
-				font-size: 4em;
-				opacity: 0.9;
-			}
-		}
-
-		& + article {
-			left: 100px;
-			width: calc(100% - 100px);
-		}
-	}
-
-	> article {
-		padding: 16px;
-
-		> header {
-			margin-bottom: 8px;
-
-			> h1 {
-				margin: 0;
-				font-size: 1em;
-				color: var(--urlPreviewTitle);
-			}
-		}
-
-		> p {
-			margin: 0;
-			color: var(--urlPreviewText);
-			font-size: 0.8em;
-		}
-
-		> footer {
-			margin-top: 8px;
-			height: 16px;
-
-			> img {
-				display: inline-block;
-				width: 16px;
-				height: 16px;
-				margin-right: 4px;
-				vertical-align: top;
-			}
-
-			> p {
-				display: inline-block;
-				margin: 0;
-				color: var(--urlPreviewInfo);
-				font-size: 0.8em;
-				line-height: 16px;
-				vertical-align: top;
-			}
-		}
-	}
-
-	@media (max-width: 700px) {
-		> .thumbnail {
-			position: relative;
-			width: 100%;
-			height: 100px;
-
-			& + article {
-				left: 0;
-				width: 100%;
-			}
-		}
-	}
-
-	@media (max-width: 550px) {
-		font-size: 12px;
-
-		> .thumbnail {
-			height: 80px;
-		}
-
-		> article {
-			padding: 12px;
-		}
-	}
-
-	@media (max-width: 500px) {
-		font-size: 10px;
-
-		> .thumbnail {
-			height: 70px;
-		}
-
-		> article {
-			padding: 8px;
-
-			> header {
-				margin-bottom: 4px;
-			}
-
-			> footer {
-				margin-top: 4px;
-
-				> img {
-					width: 12px;
-					height: 12px;
-				}
-			}
-		}
-	}
-}
-
-</style>
diff --git a/src/client/components/page-window.vue b/src/client/components/page-window.vue
deleted file mode 100644
index bc7c5b7a19..0000000000
--- a/src/client/components/page-window.vue
+++ /dev/null
@@ -1,167 +0,0 @@
-<template>
-<XWindow ref="window"
-	:initial-width="500"
-	:initial-height="500"
-	:can-resize="true"
-	:close-button="true"
-	:contextmenu="contextmenu"
-	@closed="$emit('closed')"
->
-	<template #header>
-		<template v-if="pageInfo">
-			<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i>
-			<span>{{ pageInfo.title }}</span>
-		</template>
-	</template>
-	<template #headerLeft>
-		<button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
-	</template>
-	<div class="yrolvcoq">
-		<MkStickyContainer>
-			<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
-			<component :is="component" v-bind="props" :ref="changePage"/>
-		</MkStickyContainer>
-	</div>
-</XWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XWindow from '@client/components/ui/window.vue';
-import { popout } from '@client/scripts/popout';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { resolve } from '@client/router';
-import { url } from '@client/config';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XWindow,
-	},
-
-	inject: {
-		sideViewHook: {
-			default: null
-		}
-	},
-
-	provide() {
-		return {
-			navHook: (path) => {
-				this.navigate(path);
-			},
-			shouldHeaderThin: true,
-		};
-	},
-
-	props: {
-		initialPath: {
-			type: String,
-			required: true,
-		},
-		initialComponent: {
-			type: Object,
-			required: true,
-		},
-		initialProps: {
-			type: Object,
-			required: false,
-			default: () => {},
-		},
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			pageInfo: null,
-			path: this.initialPath,
-			component: this.initialComponent,
-			props: this.initialProps,
-			history: [],
-		};
-	},
-
-	computed: {
-		url(): string {
-			return url + this.path;
-		},
-
-		contextmenu() {
-			return [{
-				type: 'label',
-				text: this.path,
-			}, {
-				icon: 'fas fa-expand-alt',
-				text: this.$ts.showInPage,
-				action: this.expand
-			}, this.sideViewHook ? {
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.sideViewHook(this.path);
-					this.$refs.window.close();
-				}
-			} : undefined, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.popout,
-				action: this.popout
-			}, null, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.openInNewTab,
-				action: () => {
-					window.open(this.url, '_blank');
-					this.$refs.window.close();
-				}
-			}, {
-				icon: 'fas fa-link',
-				text: this.$ts.copyLink,
-				action: () => {
-					copyToClipboard(this.url);
-				}
-			}];
-		},
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		navigate(path, record = true) {
-			if (record) this.history.push(this.path);
-			this.path = path;
-			const { component, props } = resolve(path);
-			this.component = component;
-			this.props = props;
-		},
-
-		back() {
-			this.navigate(this.history.pop(), false);
-		},
-
-		close() {
-			this.$refs.window.close();
-		},
-
-		expand() {
-			this.$router.push(this.path);
-			this.$refs.window.close();
-		},
-
-		popout() {
-			popout(this.path, this.$el);
-			this.$refs.window.close();
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.yrolvcoq {
-	min-height: 100%;
-}
-</style>
diff --git a/src/client/components/page/page.block.vue b/src/client/components/page/page.block.vue
deleted file mode 100644
index ffd9ce89f9..0000000000
--- a/src/client/components/page/page.block.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<component :is="'x-' + block.type" :block="block" :hpml="hpml" :key="block.id" :h="h"/>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-import XText from './page.text.vue';
-import XSection from './page.section.vue';
-import XImage from './page.image.vue';
-import XButton from './page.button.vue';
-import XNumberInput from './page.number-input.vue';
-import XTextInput from './page.text-input.vue';
-import XTextareaInput from './page.textarea-input.vue';
-import XSwitch from './page.switch.vue';
-import XIf from './page.if.vue';
-import XTextarea from './page.textarea.vue';
-import XPost from './page.post.vue';
-import XCounter from './page.counter.vue';
-import XRadioButton from './page.radio-button.vue';
-import XCanvas from './page.canvas.vue';
-import XNote from './page.note.vue';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { Block } from '@client/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote
-	},
-	props: {
-		block: {
-			type: Object as PropType<Block>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		},
-		h: {
-			type: Number,
-			required: true
-		}
-	},
-});
-</script>
diff --git a/src/client/components/page/page.button.vue b/src/client/components/page/page.button.vue
deleted file mode 100644
index c6ae675212..0000000000
--- a/src/client/components/page/page.button.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<template>
-<div>
-	<MkButton class="kudkigyw" @click="click()" :primary="block.primary">{{ hpml.interpolate(block.text) }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType, unref } from 'vue';
-import MkButton from '../ui/button.vue';
-import * as os from '@client/os';
-import { ButtonBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-	props: {
-		block: {
-			type: Object as PropType<ButtonBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	methods: {
-		click() {
-			if (this.block.action === 'dialog') {
-				this.hpml.eval();
-				os.dialog({
-					text: this.hpml.interpolate(this.block.content)
-				});
-			} else if (this.block.action === 'resetRandom') {
-				this.hpml.updateRandomSeed(Math.random());
-				this.hpml.eval();
-			} else if (this.block.action === 'pushEvent') {
-				os.api('page-push', {
-					pageId: this.hpml.page.id,
-					event: this.block.event,
-					...(this.block.var ? {
-						var: unref(this.hpml.vars)[this.block.var]
-					} : {})
-				});
-
-				os.dialog({
-					type: 'success',
-					text: this.hpml.interpolate(this.block.message)
-				});
-			} else if (this.block.action === 'callAiScript') {
-				this.hpml.callAiScript(this.block.fn);
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
-	display: inline-block;
-	min-width: 200px;
-	max-width: 450px;
-	margin: 8px 0;
-}
-</style>
diff --git a/src/client/components/page/page.canvas.vue b/src/client/components/page/page.canvas.vue
deleted file mode 100644
index e26db597f2..0000000000
--- a/src/client/components/page/page.canvas.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-<div class="ysrxegms">
-	<canvas ref="canvas" :width="block.width" :height="block.height"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
-import * as os from '@client/os';
-import { CanvasBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-
-export default defineComponent({
-	props: {
-		block: {
-			type: Object as PropType<CanvasBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const canvas: Ref<any> = ref(null);
-
-		onMounted(() => {
-			props.hpml.registerCanvas(props.block.name, canvas.value);
-		});
-
-		return {
-			canvas
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ysrxegms {
-	display: inline-block;
-	vertical-align: bottom;
-	overflow: auto;
-	max-width: 100%;
-
-	> canvas {
-		display: block;
-	}
-}
-</style>
diff --git a/src/client/components/page/page.counter.vue b/src/client/components/page/page.counter.vue
deleted file mode 100644
index dad7ac3da0..0000000000
--- a/src/client/components/page/page.counter.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
-<div>
-	<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkButton from '../ui/button.vue';
-import * as os from '@client/os';
-import { CounterVarBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-	props: {
-		block: {
-			type: Object as PropType<CounterVarBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function click() {
-			props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1));
-			props.hpml.eval();
-		}
-
-		return {
-			click
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.llumlmnx {
-	display: inline-block;
-	min-width: 300px;
-	max-width: 450px;
-	margin: 8px 0;
-}
-</style>
diff --git a/src/client/components/page/page.if.vue b/src/client/components/page/page.if.vue
deleted file mode 100644
index a70525e07c..0000000000
--- a/src/client/components/page/page.if.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<div v-show="hpml.vars.value[block.var]">
-	<XBlock v-for="child in block.children" :block="child" :hpml="hpml" :key="child.id" :h="h"/>
-</div>
-</template>
-
-<script lang="ts">
-import { IfBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { defineComponent, defineAsyncComponent, PropType } from 'vue';
-
-export default defineComponent({
-	components: {
-		XBlock: defineAsyncComponent(() => import('./page.block.vue'))
-	},
-	props: {
-		block: {
-			type: Object as PropType<IfBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		},
-		h: {
-			type: Number,
-			required: true
-		}
-	},
-});
-</script>
diff --git a/src/client/components/page/page.image.vue b/src/client/components/page/page.image.vue
deleted file mode 100644
index 14dedc98a0..0000000000
--- a/src/client/components/page/page.image.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<template>
-<div class="lzyxtsnt">
-	<img v-if="image" :src="image.url"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-import * as os from '@client/os';
-import { ImageBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-
-export default defineComponent({
-	props: {
-		block: {
-			type: Object as PropType<ImageBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
-
-		return {
-			image
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.lzyxtsnt {
-	> img {
-		max-width: 100%;
-	}
-}
-</style>
diff --git a/src/client/components/page/page.note.vue b/src/client/components/page/page.note.vue
deleted file mode 100644
index 7a3f88bb1f..0000000000
--- a/src/client/components/page/page.note.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<div class="voxdxuby">
-	<XNote v-if="note && !block.detailed" v-model:note="note" :key="note.id + ':normal'"/>
-	<XNoteDetailed v-if="note && block.detailed" v-model:note="note" :key="note.id + ':detail'"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
-import XNote from '@client/components/note.vue';
-import XNoteDetailed from '@client/components/note-detailed.vue';
-import * as os from '@client/os';
-import { NoteBlock } from '@client/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		XNote,
-		XNoteDetailed,
-	},
-	props: {
-		block: {
-			type: Object as PropType<NoteBlock>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const note: Ref<Record<string, any> | null> = ref(null);
-
-		onMounted(() => {
-			os.api('notes/show', { noteId: props.block.note })
-			.then(result => {
-				note.value = result;
-			});
-		});
-
-		return {
-			note
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.voxdxuby {
-	margin: 1em 0;
-}
-</style>
diff --git a/src/client/components/page/page.number-input.vue b/src/client/components/page/page.number-input.vue
deleted file mode 100644
index 5d9168f130..0000000000
--- a/src/client/components/page/page.number-input.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<template>
-<div>
-	<MkInput class="kudkigyw" :model-value="value" @update:modelValue="updateValue($event)" type="number">
-		<template #label>{{ hpml.interpolate(block.text) }}</template>
-	</MkInput>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkInput from '../form/input.vue';
-import * as os from '@client/os';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { NumberInputVarBlock } from '@client/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkInput
-	},
-	props: {
-		block: {
-			type: Object as PropType<NumberInputVarBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
-	display: inline-block;
-	min-width: 300px;
-	max-width: 450px;
-	margin: 8px 0;
-}
-</style>
diff --git a/src/client/components/page/page.post.vue b/src/client/components/page/page.post.vue
deleted file mode 100644
index c20d7cade1..0000000000
--- a/src/client/components/page/page.post.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-<template>
-<div class="ngbfujlo">
-	<MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea>
-	<MkButton class="button" primary @click="post()" :disabled="posting || posted">
-		<i v-if="posted" class="fas fa-check"></i>
-		<i v-else class="fas fa-paper-plane"></i>
-	</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-import MkTextarea from '../form/textarea.vue';
-import MkButton from '../ui/button.vue';
-import { apiUrl } from '@client/config';
-import * as os from '@client/os';
-import { PostBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-
-export default defineComponent({
-	components: {
-		MkTextarea,
-		MkButton,
-	},
-	props: {
-		block: {
-			type: Object as PropType<PostBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	data() {
-		return {
-			text: this.hpml.interpolate(this.block.text),
-			posted: false,
-			posting: false,
-		};
-	},
-	watch: {
-		'hpml.vars': {
-			handler() {
-				this.text = this.hpml.interpolate(this.block.text);
-			},
-			deep: true
-		}
-	},
-	methods: {
-		upload() {
-			const promise = new Promise((ok) => {
-				const canvas = this.hpml.canvases[this.block.canvasId];
-				canvas.toBlob(blob => {
-					const data = new FormData();
-					data.append('file', blob);
-					data.append('i', this.$i.token);
-					if (this.$store.state.uploadFolder) {
-						data.append('folderId', this.$store.state.uploadFolder);
-					}
-
-					fetch(apiUrl + '/drive/files/create', {
-						method: 'POST',
-						body: data
-					})
-					.then(response => response.json())
-					.then(f => {
-						ok(f);
-					})
-				});
-			});
-			os.promiseDialog(promise);
-			return promise;
-		},
-		async post() {
-			this.posting = true;
-			const file = this.block.attachCanvasImage ? await this.upload() : null;
-			os.apiWithDialog('notes/create', {
-				text: this.text === '' ? null : this.text,
-				fileIds: file ? [file.id] : undefined,
-			}).then(() => {
-				this.posted = true;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ngbfujlo {
-	position: relative;
-	padding: 32px;
-	border-radius: 6px;
-	box-shadow: 0 2px 8px var(--shadow);
-	z-index: 1;
-
-	> .button {
-		margin-top: 32px;
-	}
-
-	@media (max-width: 600px) {
-		padding: 16px;
-
-		> .button {
-			margin-top: 16px;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/page/page.radio-button.vue b/src/client/components/page/page.radio-button.vue
deleted file mode 100644
index 590e59d706..0000000000
--- a/src/client/components/page/page.radio-button.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-<div>
-	<div>{{ hpml.interpolate(block.title) }}</div>
-	<MkRadio v-for="item in block.values" :modelValue="value" @update:modelValue="updateValue($event)" :value="item" :key="item">{{ item }}</MkRadio>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkRadio from '../form/radio.vue';
-import * as os from '@client/os';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { RadioButtonVarBlock } from '@client/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkRadio
-	},
-	props: {
-		block: {
-			type: Object as PropType<RadioButtonVarBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue: string) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue
-		};
-	}
-});
-</script>
diff --git a/src/client/components/page/page.section.vue b/src/client/components/page/page.section.vue
deleted file mode 100644
index 81cab12501..0000000000
--- a/src/client/components/page/page.section.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<section class="sdgxphyu">
-	<component :is="'h' + h">{{ block.title }}</component>
-
-	<div class="children">
-		<XBlock v-for="child in block.children" :block="child" :hpml="hpml" :key="child.id" :h="h + 1"/>
-	</div>
-</section>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, PropType } from 'vue';
-import * as os from '@client/os';
-import { SectionBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-
-export default defineComponent({
-	components: {
-		XBlock: defineAsyncComponent(() => import('./page.block.vue'))
-	},
-	props: {
-		block: {
-			type: Object as PropType<SectionBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		},
-		h: {
-			required: true
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.sdgxphyu {
-	margin: 1.5em 0;
-
-	> h2 {
-		font-size: 1.35em;
-		margin: 0 0 0.5em 0;
-	}
-
-	> h3 {
-		font-size: 1em;
-		margin: 0 0 0.5em 0;
-	}
-
-	> h4 {
-		font-size: 1em;
-		margin: 0 0 0.5em 0;
-	}
-
-	> .children {
-		//padding 16px
-	}
-}
-</style>
diff --git a/src/client/components/page/page.switch.vue b/src/client/components/page/page.switch.vue
deleted file mode 100644
index 4d74e5df39..0000000000
--- a/src/client/components/page/page.switch.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<template>
-<div class="hkcxmtwj">
-	<MkSwitch :model-value="value" @update:modelValue="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkSwitch from '../form/switch.vue';
-import * as os from '@client/os';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { SwitchVarBlock } from '@client/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkSwitch
-	},
-	props: {
-		block: {
-			type: Object as PropType<SwitchVarBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue: boolean) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hkcxmtwj {
-	display: inline-block;
-	margin: 16px auto;
-
-	& + .hkcxmtwj {
-		margin-left: 16px;
-	}
-}
-</style>
diff --git a/src/client/components/page/page.text-input.vue b/src/client/components/page/page.text-input.vue
deleted file mode 100644
index 6e9ac0b543..0000000000
--- a/src/client/components/page/page.text-input.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<template>
-<div>
-	<MkInput class="kudkigyw" :model-value="value" @update:modelValue="updateValue($event)" type="text">
-		<template #label>{{ hpml.interpolate(block.text) }}</template>
-	</MkInput>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkInput from '../form/input.vue';
-import * as os from '@client/os';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { TextInputVarBlock } from '@client/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkInput
-	},
-	props: {
-		block: {
-			type: Object as PropType<TextInputVarBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.kudkigyw {
-	display: inline-block;
-	min-width: 300px;
-	max-width: 450px;
-	margin: 8px 0;
-}
-</style>
diff --git a/src/client/components/page/page.text.vue b/src/client/components/page/page.text.vue
deleted file mode 100644
index 580c5a93bf..0000000000
--- a/src/client/components/page/page.text.vue
+++ /dev/null
@@ -1,68 +0,0 @@
-<template>
-<div class="mrdgzndn">
-	<Mfm :text="text" :is-note="false" :i="$i" :key="text"/>
-	<MkUrlPreview v-for="url in urls" :url="url" :key="url" class="url"/>
-</div>
-</template>
-
-<script lang="ts">
-import { TextBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { defineAsyncComponent, defineComponent, PropType } from 'vue';
-import * as mfm from 'mfm-js';
-import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
-
-export default defineComponent({
-	components: {
-		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
-	},
-	props: {
-		block: {
-			type: Object as PropType<TextBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	data() {
-		return {
-			text: this.hpml.interpolate(this.block.text),
-		};
-	},
-	computed: {
-		urls(): string[] {
-			if (this.text) {
-				return extractUrlFromMfm(mfm.parse(this.text));
-			} else {
-				return [];
-			}
-		}
-	},
-	watch: {
-		'hpml.vars': {
-			handler() {
-				this.text = this.hpml.interpolate(this.block.text);
-			},
-			deep: true
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.mrdgzndn {
-	&:not(:first-child) {
-		margin-top: 0.5em;
-	}
-
-	&:not(:last-child) {
-		margin-bottom: 0.5em;
-	}
-
-	> .url {
-		margin: 0.5em 0;
-	}
-}
-</style>
diff --git a/src/client/components/page/page.textarea-input.vue b/src/client/components/page/page.textarea-input.vue
deleted file mode 100644
index dfcb398937..0000000000
--- a/src/client/components/page/page.textarea-input.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<div>
-	<MkTextarea :model-value="value" @update:modelValue="updateValue($event)">
-		<template #label>{{ hpml.interpolate(block.text) }}</template>
-	</MkTextarea>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, PropType } from 'vue';
-import MkTextarea from '../form/textarea.vue';
-import * as os from '@client/os';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { HpmlTextInput } from '@client/scripts/hpml';
-import { TextInputVarBlock } from '@client/scripts/hpml/block';
-
-export default defineComponent({
-	components: {
-		MkTextarea
-	},
-	props: {
-		block: {
-			type: Object as PropType<TextInputVarBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	setup(props, ctx) {
-		const value = computed(() => {
-			return props.hpml.vars.value[props.block.name];
-		});
-
-		function updateValue(newValue) {
-			props.hpml.updatePageVar(props.block.name, newValue);
-			props.hpml.eval();
-		}
-
-		return {
-			value,
-			updateValue
-		};
-	}
-});
-</script>
diff --git a/src/client/components/page/page.textarea.vue b/src/client/components/page/page.textarea.vue
deleted file mode 100644
index cf953bf041..0000000000
--- a/src/client/components/page/page.textarea.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-<MkTextarea :model-value="text" readonly></MkTextarea>
-</template>
-
-<script lang="ts">
-import { TextBlock } from '@client/scripts/hpml/block';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { defineComponent, PropType } from 'vue';
-import MkTextarea from '../form/textarea.vue';
-
-export default defineComponent({
-	components: {
-		MkTextarea
-	},
-	props: {
-		block: {
-			type: Object as PropType<TextBlock>,
-			required: true
-		},
-		hpml: {
-			type: Object as PropType<Hpml>,
-			required: true
-		}
-	},
-	data() {
-		return {
-			text: this.hpml.interpolate(this.block.text),
-		};
-	},
-	watch: {
-		'hpml.vars': {
-			handler() {
-				this.text = this.hpml.interpolate(this.block.text);
-			},
-			deep: true
-		}
-	}
-});
-</script>
diff --git a/src/client/components/page/page.vue b/src/client/components/page/page.vue
deleted file mode 100644
index f125365c3d..0000000000
--- a/src/client/components/page/page.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-<template>
-<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }" v-if="hpml">
-	<XBlock v-for="child in page.content" :block="child" :hpml="hpml" :key="child.id" :h="2"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, nextTick, onUnmounted, PropType } from 'vue';
-import { parse } from '@syuilo/aiscript';
-import XBlock from './page.block.vue';
-import { Hpml } from '@client/scripts/hpml/evaluator';
-import { url } from '@client/config';
-import { $i } from '@client/account';
-import { defaultStore } from '@client/store';
-
-export default defineComponent({
-	components: {
-		XBlock
-	},
-	props: {
-		page: {
-			type: Object as PropType<Record<string, any>>,
-			required: true
-		},
-	},
-	setup(props, ctx) {
-
-		const hpml = new Hpml(props.page, {
-			randomSeed: Math.random(),
-			visitor: $i,
-			url: url,
-			enableAiScript: !defaultStore.state.disablePagesScript
-		});
-
-		onMounted(() => {
-			nextTick(() => {
-				if (props.page.script && hpml.aiscript) {
-					let ast;
-					try {
-						ast = parse(props.page.script);
-					} catch (e) {
-						console.error(e);
-						/*os.dialog({
-							type: 'error',
-							text: 'Syntax error :('
-						});*/
-						return;
-					}
-					hpml.aiscript.exec(ast).then(() => {
-						hpml.eval();
-					}).catch(e => {
-						console.error(e);
-						/*os.dialog({
-							type: 'error',
-							text: e
-						});*/
-					});
-				} else {
-					hpml.eval();
-				}
-			});
-			onUnmounted(() => {
-				if (hpml.aiscript) hpml.aiscript.abort();
-			});
-		});
-
-		return {
-			hpml,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.iroscrza {
-	&.serif {
-		> div {
-			font-family: serif;
-		}
-	}
-
-	&.center {
-		text-align: center;
-	}
-}
-</style>
diff --git a/src/client/components/particle.vue b/src/client/components/particle.vue
deleted file mode 100644
index d82705c1e8..0000000000
--- a/src/client/components/particle.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<template>
-<div class="vswabwbm" :style="{ top: `${y - 64}px`, left: `${x - 64}px` }" :class="{ active }">
-	<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
-		<circle fill="none" cx="64" cy="64">
-			<animate attributeName="r"
-				begin="0s" dur="0.5s"
-				values="4; 32"
-				calcMode="spline"
-				keyTimes="0; 1"
-				keySplines="0.165, 0.84, 0.44, 1"
-				repeatCount="1"
-			/>
-			<animate attributeName="stroke-width"
-				begin="0s" dur="0.5s"
-				values="16; 0"
-				calcMode="spline"
-				keyTimes="0; 1"
-				keySplines="0.3, 0.61, 0.355, 1"
-				repeatCount="1"
-			/>
-		</circle>
-		<g fill="none" fill-rule="evenodd">
-			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
-				<animate attributeName="r"
-					begin="0s" dur="0.8s"
-					:values="`${particle.size}; 0`"
-					calcMode="spline"
-					keyTimes="0; 1"
-					keySplines="0.165, 0.84, 0.44, 1"
-					repeatCount="1"
-				/>
-				<animate attributeName="cx"
-					begin="0s" dur="0.8s"
-					:values="`${particle.xA}; ${particle.xB}`"
-					calcMode="spline"
-					keyTimes="0; 1"
-					keySplines="0.3, 0.61, 0.355, 1"
-					repeatCount="1"
-				/>
-				<animate attributeName="cy"
-					begin="0s" dur="0.8s"
-					:values="`${particle.yA}; ${particle.yB}`"
-					calcMode="spline"
-					keyTimes="0; 1"
-					keySplines="0.3, 0.61, 0.355, 1"
-					repeatCount="1"
-				/>
-			</circle>
-		</g>
-	</svg>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		x: {
-			type: Number,
-			required: true
-		},
-		y: {
-			type: Number,
-			required: true
-		}
-	},
-	emits: ['end'],
-	data() {
-		const particles = [];
-		const origin = 64;
-		const colors = ['#FF1493', '#00FFFF', '#FFE202'];
-
-		for (let i = 0; i < 12; i++) {
-			const angle = Math.random() * (Math.PI * 2);
-			const pos = Math.random() * 16;
-			const velocity = 16 + (Math.random() * 48);
-			particles.push({
-				size: 4 + (Math.random() * 8),
-				xA: origin + (Math.sin(angle) * pos),
-				yA: origin + (Math.cos(angle) * pos),
-				xB: origin + (Math.sin(angle) * (pos + velocity)),
-				yB: origin + (Math.cos(angle) * (pos + velocity)),
-				color: colors[Math.floor(Math.random() * colors.length)]
-			});
-		}
-
-		return {
-			particles
-		};
-	},
-	mounted() {
-		setTimeout(() => {
-			this.$emit('end');
-		}, 1100);
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.vswabwbm {
-	pointer-events: none;
-	position: fixed;
-	z-index: 1000000;
-	width: 128px;
-	height: 128px;
-
-	> svg {
-		> circle {
-			stroke: var(--accent);
-		}
-	}
-}
-</style>
diff --git a/src/client/components/poll-editor.vue b/src/client/components/poll-editor.vue
deleted file mode 100644
index b28a1c8baa..0000000000
--- a/src/client/components/poll-editor.vue
+++ /dev/null
@@ -1,251 +0,0 @@
-<template>
-<div class="zmdxowus">
-	<p class="caution" v-if="choices.length < 2">
-		<i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }}
-	</p>
-	<ul ref="choices">
-		<li v-for="(choice, i) in choices" :key="i">
-			<MkInput class="input" :model-value="choice" @update:modelValue="onInput(i, $event)" :placeholder="$t('_poll.choiceN', { n: i + 1 })">
-			</MkInput>
-			<button @click="remove(i)" class="_button">
-				<i class="fas fa-times"></i>
-			</button>
-		</li>
-	</ul>
-	<MkButton class="add" v-if="choices.length < 10" @click="add">{{ $ts.add }}</MkButton>
-	<MkButton class="add" v-else disabled>{{ $ts._poll.noMore }}</MkButton>
-	<section>
-		<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
-		<div>
-			<MkSelect v-model="expiration">
-				<template #label>{{ $ts._poll.expiration }}</template>
-				<option value="infinite">{{ $ts._poll.infinite }}</option>
-				<option value="at">{{ $ts._poll.at }}</option>
-				<option value="after">{{ $ts._poll.after }}</option>
-			</MkSelect>
-			<section v-if="expiration === 'at'">
-				<MkInput v-model="atDate" type="date" class="input">
-					<template #label>{{ $ts._poll.deadlineDate }}</template>
-				</MkInput>
-				<MkInput v-model="atTime" type="time" class="input">
-					<template #label>{{ $ts._poll.deadlineTime }}</template>
-				</MkInput>
-			</section>
-			<section v-if="expiration === 'after'">
-				<MkInput v-model="after" type="number" class="input">
-					<template #label>{{ $ts._poll.duration }}</template>
-				</MkInput>
-				<MkSelect v-model="unit">
-					<option value="second">{{ $ts._time.second }}</option>
-					<option value="minute">{{ $ts._time.minute }}</option>
-					<option value="hour">{{ $ts._time.hour }}</option>
-					<option value="day">{{ $ts._time.day }}</option>
-				</MkSelect>
-			</section>
-		</div>
-	</section>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { addTime } from '../../prelude/time';
-import { formatDateTimeString } from '@/misc/format-time-string';
-import MkInput from './form/input.vue';
-import MkSelect from './form/select.vue';
-import MkSwitch from './form/switch.vue';
-import MkButton from './ui/button.vue';
-
-export default defineComponent({
-	components: {
-		MkInput,
-		MkSelect,
-		MkSwitch,
-		MkButton,
-	},
-
-	props: {
-		poll: {
-			type: Object,
-			required: true
-		}
-	},
-
-	emits: ['updated'],
-
-	data() {
-		return {
-			choices: this.poll.choices,
-			multiple: this.poll.multiple,
-			expiration: 'infinite',
-			atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'),
-			atTime: '00:00',
-			after: 0,
-			unit: 'second',
-		};
-	},
-
-	watch: {
-		choices: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-			deep: true
-		},
-		multiple: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-		expiration: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-		atDate: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-		after: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-		unit: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-	},
-
-	created() {
-		const poll = this.poll;
-		if (poll.expiresAt) {
-			this.expiration = 'at';
-			this.atDate = this.atTime = poll.expiresAt;
-		} else if (typeof poll.expiredAfter === 'number') {
-			this.expiration = 'after';
-			this.after = poll.expiredAfter / 1000;
-		} else {
-			this.expiration = 'infinite';
-		}
-	},
-
-	methods: {
-		onInput(i, e) {
-			this.choices[i] = e;
-		},
-
-		add() {
-			this.choices.push('');
-			this.$nextTick(() => {
-				// TODO
-				//(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
-			});
-		},
-
-		remove(i) {
-			this.choices = this.choices.filter((_, _i) => _i != i);
-		},
-
-		get() {
-			const at = () => {
-				return new Date(`${this.atDate} ${this.atTime}`).getTime();
-			};
-
-			const after = () => {
-				let base = parseInt(this.after);
-				switch (this.unit) {
-					case 'day': base *= 24;
-					case 'hour': base *= 60;
-					case 'minute': base *= 60;
-					case 'second': return base *= 1000;
-					default: return null;
-				}
-			};
-
-			return {
-				choices: this.choices,
-				multiple: this.multiple,
-				...(
-					this.expiration === 'at' ? { expiresAt: at() } :
-					this.expiration === 'after' ? { expiredAfter: after() } : {}
-				)
-			};
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.zmdxowus {
-	padding: 8px;
-
-	> .caution {
-		margin: 0 0 8px 0;
-		font-size: 0.8em;
-		color: #f00;
-
-		> i {
-			margin-right: 4px;
-		}
-	}
-
-	> ul {
-		display: block;
-		margin: 0;
-		padding: 0;
-		list-style: none;
-
-		> li {
-			display: flex;
-			margin: 8px 0;
-			padding: 0;
-			width: 100%;
-
-			> .input {
-				flex: 1;
-				margin-top: 16px;
-				margin-bottom: 0;
-			}
-
-			> button {
-				width: 32px;
-				padding: 4px 0;
-			}
-		}
-	}
-
-	> .add {
-		margin: 8px 0 0 0;
-		z-index: 1;
-	}
-
-	> section {
-		margin: 16px 0 -16px 0;
-
-		> div {
-			margin: 0 8px;
-
-			&:last-child {
-				flex: 1 0 auto;
-
-				> section {
-					align-items: center;
-					display: flex;
-					margin: -32px 0 0;
-
-					> &:first-child {
-						margin-right: 16px;
-					}
-
-					> .input {
-						flex: 1 0 auto;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/poll.vue b/src/client/components/poll.vue
deleted file mode 100644
index b5d430f93b..0000000000
--- a/src/client/components/poll.vue
+++ /dev/null
@@ -1,174 +0,0 @@
-<template>
-<div class="tivcixzd" :class="{ done: closed || isVoted }">
-	<ul>
-		<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }">
-			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
-			<span>
-				<template v-if="choice.isVoted"><i class="fas fa-check"></i></template>
-				<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
-				<span class="votes" v-if="showResult">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span>
-			</span>
-		</li>
-	</ul>
-	<p v-if="!readOnly">
-		<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
-		<span> · </span>
-		<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a>
-		<span v-if="isVoted">{{ $ts._poll.voted }}</span>
-		<span v-else-if="closed">{{ $ts._poll.closed }}</span>
-		<span v-if="remaining > 0"> · {{ timer }}</span>
-	</p>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { sum } from '../../prelude/array';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		readOnly: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-	data() {
-		return {
-			remaining: -1,
-			showResult: false,
-		};
-	},
-	computed: {
-		poll(): any {
-			return this.note.poll;
-		},
-		total(): number {
-			return sum(this.poll.choices.map(x => x.votes));
-		},
-		closed(): boolean {
-			return !this.remaining;
-		},
-		timer(): string {
-			return this.$t(
-				this.remaining >= 86400 ? '_poll.remainingDays' :
-				this.remaining >= 3600 ? '_poll.remainingHours' :
-				this.remaining >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
-					s: Math.floor(this.remaining % 60),
-					m: Math.floor(this.remaining / 60) % 60,
-					h: Math.floor(this.remaining / 3600) % 24,
-					d: Math.floor(this.remaining / 86400)
-				});
-		},
-		isVoted(): boolean {
-			return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
-		}
-	},
-	created() {
-		this.showResult = this.readOnly || this.isVoted;
-
-		if (this.note.poll.expiresAt) {
-			const update = () => {
-				if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
-					requestAnimationFrame(update);
-				else
-					this.showResult = true;
-			};
-
-			update();
-		}
-	},
-	methods: {
-		toggleShowResult() {
-			this.showResult = !this.showResult;
-		},
-		vote(id) {
-			if (this.readOnly || this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
-			os.api('notes/polls/vote', {
-				noteId: this.note.id,
-				choice: id
-			}).then(() => {
-				if (!this.showResult) this.showResult = !this.poll.multiple;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.tivcixzd {
-	> ul {
-		display: block;
-		margin: 0;
-		padding: 0;
-		list-style: none;
-
-		> li {
-			display: block;
-			position: relative;
-			margin: 4px 0;
-			padding: 4px 8px;
-			border: solid 0.5px var(--divider);
-			border-radius: 4px;
-			overflow: hidden;
-			cursor: pointer;
-
-			&:hover {
-				background: rgba(#000, 0.05);
-			}
-
-			&:active {
-				background: rgba(#000, 0.1);
-			}
-
-			> .backdrop {
-				position: absolute;
-				top: 0;
-				left: 0;
-				height: 100%;
-				background: var(--accent);
-				transition: width 1s ease;
-			}
-
-			> span {
-				position: relative;
-
-				> i {
-					margin-right: 4px;
-				}
-
-				> .votes {
-					margin-left: 4px;
-				}
-			}
-		}
-	}
-
-	> p {
-		color: var(--fg);
-
-		a {
-			color: inherit;
-		}
-	}
-
-	&.done {
-		> ul > li {
-			cursor: default;
-
-			&:hover {
-				background: transparent;
-			}
-
-			&:active {
-				background: transparent;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/post-form-attaches.vue b/src/client/components/post-form-attaches.vue
deleted file mode 100644
index 9365365653..0000000000
--- a/src/client/components/post-form-attaches.vue
+++ /dev/null
@@ -1,193 +0,0 @@
-<template>
-<div class="skeikyzd" v-show="files.length != 0">
-	<XDraggable class="files" v-model="_files" item-key="id" animation="150" delay="100" delay-on-touch-only="true">
-		<template #item="{element}">
-			<div @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
-				<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
-				<div class="sensitive" v-if="element.isSensitive">
-					<i class="fas fa-exclamation-triangle icon"></i>
-				</div>
-			</div>
-		</template>
-	</XDraggable>
-	<p class="remain">{{ 4 - files.length }}/4</p>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import MkDriveFileThumbnail from './drive-file-thumbnail.vue'
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
-		MkDriveFileThumbnail
-	},
-
-	props: {
-		files: {
-			type: Array,
-			required: true
-		},
-		detachMediaFn: {
-			type: Function,
-			required: false
-		}
-	},
-
-	emits: ['updated', 'detach', 'changeSensitive', 'changeName'],
-
-	data() {
-		return {
-			menu: null as Promise<null> | null,
-
-		};
-	},
-
-	computed: {
-		_files: {
-			get() {
-				return this.files;
-			},
-			set(value) {
-				this.$emit('updated', value);
-			}
-		}
-	},
-
-	methods: {
-		detachMedia(id) {
-			if (this.detachMediaFn) {
-				this.detachMediaFn(id);
-			} else {
-				this.$emit('detach', id);
-			}
-		},
-		toggleSensitive(file) {
-			os.api('drive/files/update', {
-				fileId: file.id,
-				isSensitive: !file.isSensitive
-			}).then(() => {
-				this.$emit('changeSensitive', file, !file.isSensitive);
-			});
-		},
-		async rename(file) {
-			const { canceled, result } = await os.dialog({
-				title: this.$ts.enterFileName,
-				input: {
-					default: file.name
-				},
-				allowEmpty: false
-			});
-			if (canceled) return;
-			os.api('drive/files/update', {
-				fileId: file.id,
-				name: result
-			}).then(() => {
-				this.$emit('changeName', file, result);
-				file.name = result;
-			});
-		},
-
-		async describe(file) {
-			os.popup(import("@client/components/media-caption.vue"), {
-				title: this.$ts.describeFile,
-				input: {
-					placeholder: this.$ts.inputNewDescription,
-					default: file.comment !== null ? file.comment : "",
-				},
-				image: file
-			}, {
-				done: result => {
-					if (!result || result.canceled) return;
-					let comment = result.result;
-					os.api('drive/files/update', {
-						fileId: file.id,
-						comment: comment.length == 0 ? null : comment
-					});
-				}
-			}, 'closed');
-		},
-
-		showFileMenu(file, ev: MouseEvent) {
-			if (this.menu) return;
-			this.menu = os.popupMenu([{
-				text: this.$ts.renameFile,
-				icon: 'fas fa-i-cursor',
-				action: () => { this.rename(file) }
-			}, {
-				text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
-				icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
-				action: () => { this.toggleSensitive(file) }
-			}, {
-				text: this.$ts.describeFile,
-				icon: 'fas fa-i-cursor',
-				action: () => { this.describe(file) }
-			}, {
-				text: this.$ts.attachCancel,
-				icon: 'fas fa-times-circle',
-				action: () => { this.detachMedia(file.id) }
-			}], ev.currentTarget || ev.target).then(() => this.menu = null);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.skeikyzd {
-	padding: 8px 16px;
-	position: relative;
-
-	> .files {
-		display: flex;
-		flex-wrap: wrap;
-
-		> div {
-			position: relative;
-			width: 64px;
-			height: 64px;
-			margin-right: 4px;
-			border-radius: 4px;
-			overflow: hidden;
-			cursor: move;
-
-			&:hover > .remove {
-				display: block;
-			}
-
-			> .thumbnail {
-				width: 100%;
-				height: 100%;
-				z-index: 1;
-				color: var(--fg);
-			}
-
-			> .sensitive {
-				display: flex;
-				position: absolute;
-				width: 64px;
-				height: 64px;
-				top: 0;
-				left: 0;
-				z-index: 2;
-				background: rgba(17, 17, 17, .7);
-				color: #fff;
-
-				> .icon {
-					margin: auto;
-				}
-			}
-		}
-	}
-
-	> .remain {
-		display: block;
-		position: absolute;
-		top: 8px;
-		right: 8px;
-		margin: 0;
-		padding: 0;
-	}
-}
-</style>
diff --git a/src/client/components/post-form-dialog.vue b/src/client/components/post-form-dialog.vue
deleted file mode 100644
index aa23e3891e..0000000000
--- a/src/client/components/post-form-dialog.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<template>
-<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')" :position="'top'">
-	<MkPostForm @posted="$refs.modal.close()" @cancel="$refs.modal.close()" @esc="$refs.modal.close()" v-bind="$attrs"/>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkModal from '@client/components/ui/modal.vue';
-import MkPostForm from '@client/components/post-form.vue';
-
-export default defineComponent({
-	components: {
-		MkModal,
-		MkPostForm,
-	},
-	emits: ['closed'],
-});
-</script>
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
deleted file mode 100644
index 90df78895c..0000000000
--- a/src/client/components/post-form.vue
+++ /dev/null
@@ -1,980 +0,0 @@
-<template>
-<div class="gafaadew" :class="{ modal, _popup: modal }"
-	v-size="{ max: [310, 500] }"
-	@dragover.stop="onDragover"
-	@dragenter="onDragenter"
-	@dragleave="onDragleave"
-	@drop.stop="onDrop"
->
-	<header>
-		<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
-		<div>
-			<span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
-			<span class="local-only" v-if="localOnly"><i class="fas fa-biohazard"></i></span>
-			<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$ts.visibility" :disabled="channel != null">
-				<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
-				<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
-				<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
-				<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
-			</button>
-			<button class="_button preview" @click="showPreview = !showPreview" :class="{ active: showPreview }" v-tooltip="$ts.previewNoteText"><i class="fas fa-file-code"></i></button>
-			<button class="submit _buttonGradate" :disabled="!canPost" @click="post" data-cy-open-post-form-submit>{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
-		</div>
-	</header>
-	<div class="form" :class="{ fixed }">
-		<XNoteSimple class="preview" v-if="reply" :note="reply"/>
-		<XNoteSimple class="preview" v-if="renote" :note="renote"/>
-		<div class="with-quote" v-if="quoteId"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
-		<div v-if="visibility === 'specified'" class="to-specified">
-			<span style="margin-right: 8px;">{{ $ts.recipient }}</span>
-			<div class="visibleUsers">
-				<span v-for="u in visibleUsers" :key="u.id">
-					<MkAcct :user="u"/>
-					<button class="_button" @click="removeVisibleUser(u)"><i class="fas fa-times"></i></button>
-				</span>
-				<button @click="addVisibleUser" class="_buttonPrimary"><i class="fas fa-plus fa-fw"></i></button>
-			</div>
-		</div>
-		<MkInfo warn v-if="hasNotSpecifiedMentions" class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo>
-		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
-		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd" data-cy-post-form-text/>
-		<input v-show="withHashtags" ref="hashtags" class="hashtags" v-model="hashtags" :placeholder="$ts.hashtags" list="hashtags">
-		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
-		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
-		<XNotePreview class="preview" v-if="showPreview" :text="text"/>
-		<footer>
-			<button class="_button" @click="chooseFileFrom" v-tooltip="$ts.attachFile"><i class="fas fa-photo-video"></i></button>
-			<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$ts.poll"><i class="fas fa-poll-h"></i></button>
-			<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$ts.useCw"><i class="fas fa-eye-slash"></i></button>
-			<button class="_button" @click="insertMention" v-tooltip="$ts.mention"><i class="fas fa-at"></i></button>
-			<button class="_button" @click="withHashtags = !withHashtags" :class="{ active: withHashtags }" v-tooltip="$ts.hashtags"><i class="fas fa-hashtag"></i></button>
-			<button class="_button" @click="insertEmoji" v-tooltip="$ts.emoji"><i class="fas fa-laugh-squint"></i></button>
-			<button class="_button" @click="showActions" v-tooltip="$ts.plugin" v-if="postFormActions.length > 0"><i class="fas fa-plug"></i></button>
-		</footer>
-		<datalist id="hashtags">
-			<option v-for="hashtag in recentHashtags" :value="hashtag" :key="hashtag"/>
-		</datalist>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import insertTextAtCursor from 'insert-text-at-cursor';
-import { length } from 'stringz';
-import { toASCII } from 'punycode/';
-import XNoteSimple from './note-simple.vue';
-import XNotePreview from './note-preview.vue';
-import * as mfm from 'mfm-js';
-import { host, url } from '@client/config';
-import { erase, unique } from '../../prelude/array';
-import { extractMentions } from '@/misc/extract-mentions';
-import { getAcct } from '@/misc/acct';
-import { formatTimeString } from '@/misc/format-time-string';
-import { Autocomplete } from '@client/scripts/autocomplete';
-import { noteVisibilities } from '../../types';
-import * as os from '@client/os';
-import { selectFile } from '@client/scripts/select-file';
-import { defaultStore, notePostInterruptors, postFormActions } from '@client/store';
-import { isMobile } from '@client/scripts/is-mobile';
-import { throttle } from 'throttle-debounce';
-import MkInfo from '@client/components/ui/info.vue';
-import { defaultStore } from '@client/store';
-
-export default defineComponent({
-	components: {
-		XNoteSimple,
-		XNotePreview,
-		XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
-		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')),
-		MkInfo,
-	},
-
-	inject: ['modal'],
-
-	props: {
-		reply: {
-			type: Object,
-			required: false
-		},
-		renote: {
-			type: Object,
-			required: false
-		},
-		channel: {
-			type: Object,
-			required: false
-		},
-		mention: {
-			type: Object,
-			required: false
-		},
-		specified: {
-			type: Object,
-			required: false
-		},
-		initialText: {
-			type: String,
-			required: false
-		},
-		initialVisibility: {
-			type: String,
-			required: false
-		},
-		initialFiles: {
-			type: Array,
-			required: false
-		},
-		initialLocalOnly: {
-			type: Boolean,
-			required: false
-		},
-		visibleUsers: {
-			type: Array,
-			required: false,
-			default: () => []
-		},
-		initialNote: {
-			type: Object,
-			required: false
-		},
-		share: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		fixed: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-	},
-
-	emits: ['posted', 'cancel', 'esc'],
-
-	data() {
-		return {
-			posting: false,
-			text: '',
-			files: [],
-			poll: null,
-			useCw: false,
-			showPreview: false,
-			cw: null,
-			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
-			visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
-			autocomplete: null,
-			draghover: false,
-			quoteId: null,
-			hasNotSpecifiedMentions: false,
-			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
-			imeText: '',
-			typing: throttle(3000, () => {
-				if (this.channel) {
-					os.stream.send('typingOnChannel', { channel: this.channel.id });
-				}
-			}),
-			postFormActions,
-		};
-	},
-
-	computed: {
-		draftKey(): string {
-			let key = this.channel ? `channel:${this.channel.id}` : '';
-
-			if (this.renote) {
-				key += `renote:${this.renote.id}`;
-			} else if (this.reply) {
-				key += `reply:${this.reply.id}`;
-			} else {
-				key += 'note';
-			}
-
-			return key;
-		},
-
-		placeholder(): string {
-			if (this.renote) {
-				return this.$ts._postForm.quotePlaceholder;
-			} else if (this.reply) {
-				return this.$ts._postForm.replyPlaceholder;
-			} else if (this.channel) {
-				return this.$ts._postForm.channelPlaceholder;
-			} else {
-				const xs = [
-					this.$ts._postForm._placeholders.a,
-					this.$ts._postForm._placeholders.b,
-					this.$ts._postForm._placeholders.c,
-					this.$ts._postForm._placeholders.d,
-					this.$ts._postForm._placeholders.e,
-					this.$ts._postForm._placeholders.f
-				];
-				return xs[Math.floor(Math.random() * xs.length)];
-			}
-		},
-
-		submitText(): string {
-			return this.renote
-				? this.$ts.quote
-				: this.reply
-					? this.$ts.reply
-					: this.$ts.note;
-		},
-
-		textLength(): number {
-			return length((this.text + this.imeText).trim());
-		},
-
-		canPost(): boolean {
-			return !this.posting &&
-				(1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
-				(this.textLength <= this.max) &&
-				(!this.poll || this.poll.choices.length >= 2);
-		},
-
-		max(): number {
-			return this.$instance ? this.$instance.maxNoteTextLength : 1000;
-		},
-
-		withHashtags: defaultStore.makeGetterSetter('postFormWithHashtags'),
-		hashtags: defaultStore.makeGetterSetter('postFormHashtags'),
-	},
-
-	watch: {
-		text() {
-			this.checkMissingMention();
-		},
-		visibleUsers: {
-			handler() {
-				this.checkMissingMention();
-			},
-			deep: true
-		}
-	},
-
-	mounted() {
-		if (this.initialText) {
-			this.text = this.initialText;
-		}
-
-		if (this.initialVisibility) {
-			this.visibility = this.initialVisibility;
-		}
-
-		if (this.initialFiles) {
-			this.files = this.initialFiles;
-		}
-
-		if (typeof this.initialLocalOnly === 'boolean') {
-			this.localOnly = this.initialLocalOnly;
-		}
-
-		if (this.mention) {
-			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
-			this.text += ' ';
-		}
-
-		if (this.reply && this.reply.user.host != null) {
-			this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
-		}
-
-		if (this.reply && this.reply.text != null) {
-			const ast = mfm.parse(this.reply.text);
-
-			for (const x of extractMentions(ast)) {
-				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
-
-				// 自分は除外
-				if (this.$i.username == x.username && x.host == null) continue;
-				if (this.$i.username == x.username && x.host == host) continue;
-
-				// 重複は除外
-				if (this.text.indexOf(`${mention} `) != -1) continue;
-
-				this.text += `${mention} `;
-			}
-		}
-
-		if (this.channel) {
-			this.visibility = 'public';
-			this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
-		}
-
-		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
-		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
-			this.visibility = this.reply.visibility;
-			if (this.reply.visibility === 'specified') {
-				os.api('users/show', {
-					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
-				}).then(users => {
-					this.visibleUsers.push(...users);
-				});
-
-				if (this.reply.userId !== this.$i.id) {
-					os.api('users/show', { userId: this.reply.userId }).then(user => {
-						this.visibleUsers.push(user);
-					});
-				}
-			}
-		}
-
-		if (this.specified) {
-			this.visibility = 'specified';
-			this.visibleUsers.push(this.specified);
-		}
-
-		// keep cw when reply
-		if (this.$store.state.keepCw && this.reply && this.reply.cw) {
-			this.useCw = true;
-			this.cw = this.reply.cw;
-		}
-
-		if (this.autofocus) {
-			this.focus();
-
-			this.$nextTick(() => {
-				this.focus();
-			});
-		}
-
-		// TODO: detach when unmount
-		new Autocomplete(this.$refs.text, this, { model: 'text' });
-		new Autocomplete(this.$refs.cw, this, { model: 'cw' });
-		new Autocomplete(this.$refs.hashtags, this, { model: 'hashtags' });
-
-		this.$nextTick(() => {
-			// 書きかけの投稿を復元
-			if (!this.share && !this.mention && !this.specified) {
-				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
-				if (draft) {
-					this.text = draft.data.text;
-					this.useCw = draft.data.useCw;
-					this.cw = draft.data.cw;
-					this.visibility = draft.data.visibility;
-					this.localOnly = draft.data.localOnly;
-					this.files = (draft.data.files || []).filter(e => e);
-					if (draft.data.poll) {
-						this.poll = draft.data.poll;
-					}
-				}
-			}
-
-			// 削除して編集
-			if (this.initialNote) {
-				const init = this.initialNote;
-				this.text = init.text ? init.text : '';
-				this.files = init.files;
-				this.cw = init.cw;
-				this.useCw = init.cw != null;
-				if (init.poll) {
-					this.poll = {
-						choices: init.poll.choices.map(x => x.text),
-						multiple: init.poll.multiple,
-						expiresAt: init.poll.expiresAt,
-						expiredAfter: init.poll.expiredAfter,
-					};
-				}
-				this.visibility = init.visibility;
-				this.localOnly = init.localOnly;
-				this.quoteId = init.renote ? init.renote.id : null;
-			}
-
-			this.$nextTick(() => this.watch());
-		});
-	},
-
-	methods: {
-		watch() {
-			this.$watch('text', () => this.saveDraft());
-			this.$watch('useCw', () => this.saveDraft());
-			this.$watch('cw', () => this.saveDraft());
-			this.$watch('poll', () => this.saveDraft());
-			this.$watch('files', () => this.saveDraft(), { deep: true });
-			this.$watch('visibility', () => this.saveDraft());
-			this.$watch('localOnly', () => this.saveDraft());
-		},
-
-		checkMissingMention() {
-			if (this.visibility === 'specified') {
-				const ast = mfm.parse(this.text);
-
-				for (const x of extractMentions(ast)) {
-					if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
-						this.hasNotSpecifiedMentions = true;
-						return;
-					}
-				}
-				this.hasNotSpecifiedMentions = false;
-			}
-		},
-
-		addMissingMention() {
-			const ast = mfm.parse(this.text);
-
-			for (const x of extractMentions(ast)) {
-				if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
-					os.api('users/show', { username: x.username, host: x.host }).then(user => {
-						this.visibleUsers.push(user);
-					});
-				}
-			}
-		},
-
-		togglePoll() {
-			if (this.poll) {
-				this.poll = null;
-			} else {
-				this.poll = {
-					choices: ['', ''],
-					multiple: false,
-					expiresAt: null,
-					expiredAfter: null,
-				};
-			}
-		},
-
-		addTag(tag: string) {
-			insertTextAtCursor(this.$refs.text, ` #${tag} `);
-		},
-
-		focus() {
-			(this.$refs.text as any).focus();
-		},
-
-		chooseFileFrom(ev) {
-			selectFile(ev.currentTarget || ev.target, this.$ts.attachFile, true).then(files => {
-				for (const file of files) {
-					this.files.push(file);
-				}
-			});
-		},
-
-		detachFile(id) {
-			this.files = this.files.filter(x => x.id != id);
-		},
-
-		updateFiles(files) {
-			this.files = files;
-		},
-
-		updateFileSensitive(file, sensitive) {
-			this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
-		},
-
-		updateFileName(file, name) {
-			this.files[this.files.findIndex(x => x.id === file.id)].name = name;
-		},
-
-		upload(file: File, name?: string) {
-			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
-				this.files.push(res);
-			});
-		},
-
-		onPollUpdate(poll) {
-			this.poll = poll;
-			this.saveDraft();
-		},
-
-		setVisibility() {
-			if (this.channel) {
-				// TODO: information dialog
-				return;
-			}
-
-			os.popup(import('./visibility-picker.vue'), {
-				currentVisibility: this.visibility,
-				currentLocalOnly: this.localOnly,
-				src: this.$refs.visibilityButton
-			}, {
-				changeVisibility: visibility => {
-					this.visibility = visibility;
-					if (this.$store.state.rememberNoteVisibility) {
-						this.$store.set('visibility', visibility);
-					}
-				},
-				changeLocalOnly: localOnly => {
-					this.localOnly = localOnly;
-					if (this.$store.state.rememberNoteVisibility) {
-						this.$store.set('localOnly', localOnly);
-					}
-				}
-			}, 'closed');
-		},
-
-		addVisibleUser() {
-			os.selectUser().then(user => {
-				this.visibleUsers.push(user);
-			});
-		},
-
-		removeVisibleUser(user) {
-			this.visibleUsers = erase(user, this.visibleUsers);
-		},
-
-		clear() {
-			this.text = '';
-			this.files = [];
-			this.poll = null;
-			this.quoteId = null;
-		},
-
-		onKeydown(e: KeyboardEvent) {
-			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
-			if (e.which === 27) this.$emit('esc');
-			this.typing();
-		},
-
-		onCompositionUpdate(e: CompositionEvent) {
-			this.imeText = e.data;
-			this.typing();
-		},
-
-		onCompositionEnd(e: CompositionEvent) {
-			this.imeText = '';
-		},
-
-		async onPaste(e: ClipboardEvent) {
-			for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
-				if (item.kind == 'file') {
-					const file = item.getAsFile();
-					const lio = file.name.lastIndexOf('.');
-					const ext = lio >= 0 ? file.name.slice(lio) : '';
-					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
-					this.upload(file, formatted);
-				}
-			}
-
-			const paste = e.clipboardData.getData('text');
-
-			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
-				e.preventDefault();
-
-				os.dialog({
-					type: 'info',
-					text: this.$ts.quoteQuestion,
-					showCancelButton: true
-				}).then(({ canceled }) => {
-					if (canceled) {
-						insertTextAtCursor(this.$refs.text, paste);
-						return;
-					}
-
-					this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
-				});
-			}
-		},
-
-		onDragover(e) {
-			if (!e.dataTransfer.items[0]) return;
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			if (isFile || isDriveFile) {
-				e.preventDefault();
-				this.draghover = true;
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			}
-		},
-
-		onDragenter(e) {
-			this.draghover = true;
-		},
-
-		onDragleave(e) {
-			this.draghover = false;
-		},
-
-		onDrop(e): void {
-			this.draghover = false;
-
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				e.preventDefault();
-				for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.files.push(file);
-				e.preventDefault();
-			}
-			//#endregion
-		},
-
-		saveDraft() {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			data[this.draftKey] = {
-				updatedAt: new Date(),
-				data: {
-					text: this.text,
-					useCw: this.useCw,
-					cw: this.cw,
-					visibility: this.visibility,
-					localOnly: this.localOnly,
-					files: this.files,
-					poll: this.poll
-				}
-			};
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		},
-
-		deleteDraft() {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			delete data[this.draftKey];
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		},
-
-		async post() {
-			let data = {
-				text: this.text == '' ? undefined : this.text,
-				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
-				replyId: this.reply ? this.reply.id : undefined,
-				renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
-				channelId: this.channel ? this.channel.id : undefined,
-				poll: this.poll,
-				cw: this.useCw ? this.cw || '' : undefined,
-				localOnly: this.localOnly,
-				visibility: this.visibility,
-				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
-				viaMobile: isMobile
-			};
-
-			if (this.withHashtags && this.hashtags && this.hashtags.trim() !== '') {
-				const hashtags = this.hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
-				data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
-			}
-
-			// plugin
-			if (notePostInterruptors.length > 0) {
-				for (const interruptor of notePostInterruptors) {
-					data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
-				}
-			}
-
-			this.posting = true;
-			os.api('notes/create', data).then(() => {
-				this.clear();
-				this.$nextTick(() => {
-					this.deleteDraft();
-					this.$emit('posted');
-					if (data.text && data.text != '') {
-						const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
-						const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
-						localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
-					}
-					this.posting = false;
-				});
-			}).catch(err => {
-				this.posting = false;
-				os.dialog({
-					type: 'error',
-					text: err.message + '\n' + (err as any).id,
-				});
-			});
-		},
-
-		cancel() {
-			this.$emit('cancel');
-		},
-
-		insertMention() {
-			os.selectUser().then(user => {
-				insertTextAtCursor(this.$refs.text, '@' + getAcct(user) + ' ');
-			});
-		},
-
-		async insertEmoji(ev) {
-			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
-		},
-
-		showActions(ev) {
-			os.popupMenu(postFormActions.map(action => ({
-				text: action.title,
-				action: () => {
-					action.handler({
-						text: this.text
-					}, (key, value) => {
-						if (key === 'text') { this.text = value; }
-					});
-				}
-			})), ev.currentTarget || ev.target);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.gafaadew {
-	position: relative;
-
-	&.modal {
-		width: 100%;
-		max-width: 520px;
-	}
-
-	> header {
-		z-index: 1000;
-		height: 66px;
-
-		> .cancel {
-			padding: 0;
-			font-size: 20px;
-			width: 64px;
-			line-height: 66px;
-		}
-
-		> div {
-			position: absolute;
-			top: 0;
-			right: 0;
-
-			> .text-count {
-				opacity: 0.7;
-				line-height: 66px;
-			}
-
-			> .visibility {
-				height: 34px;
-				width: 34px;
-				margin: 0 0 0 8px;
-
-				& + .localOnly {
-					margin-left: 0 !important;
-				}
-			}
-			
-			> .local-only {
-				margin: 0 0 0 12px;
-				opacity: 0.7;
-			}
-
-			> .preview {
-				display: inline-block;
-				padding: 0;
-				margin: 0 8px 0 0;
-				font-size: 16px;
-				width: 34px;
-				height: 34px;
-				border-radius: 6px;
-
-				&:hover {
-					background: var(--X5);
-				}
-
-				&.active {
-					color: var(--accent);
-				}
-			}
-
-			> .submit {
-				margin: 16px 16px 16px 0;
-				padding: 0 12px;
-				line-height: 34px;
-				font-weight: bold;
-				vertical-align: bottom;
-				border-radius: 4px;
-				font-size: 0.9em;
-
-				&:disabled {
-					opacity: 0.7;
-				}
-
-				> i {
-					margin-left: 6px;
-				}
-			}
-		}
-	}
-
-	> .form {
-		> .preview {
-			padding: 16px;
-		}
-
-		> .with-quote {
-			margin: 0 0 8px 0;
-			color: var(--accent);
-
-			> button {
-				padding: 4px 8px;
-				color: var(--accentAlpha04);
-
-				&:hover {
-					color: var(--accentAlpha06);
-				}
-
-				&:active {
-					color: var(--accentDarken30);
-				}
-			}
-		}
-
-		> .to-specified {
-			padding: 6px 24px;
-			margin-bottom: 8px;
-			overflow: auto;
-			white-space: nowrap;
-
-			> .visibleUsers {
-				display: inline;
-				top: -1px;
-				font-size: 14px;
-
-				> button {
-					padding: 4px;
-					border-radius: 8px;
-				}
-
-				> span {
-					margin-right: 14px;
-					padding: 8px 0 8px 8px;
-					border-radius: 8px;
-					background: var(--X4);
-
-					> button {
-						padding: 4px 8px;
-					}
-				}
-			}
-		}
-
-		> .hasNotSpecifiedMentions {
-			margin: 0 20px 16px 20px;
-		}
-
-		> .cw,
-		> .hashtags,
-		> .text {
-			display: block;
-			box-sizing: border-box;
-			padding: 0 24px;
-			margin: 0;
-			width: 100%;
-			font-size: 16px;
-			border: none;
-			border-radius: 0;
-			background: transparent;
-			color: var(--fg);
-			font-family: inherit;
-
-			&:focus {
-				outline: none;
-			}
-
-			&:disabled {
-				opacity: 0.5;
-			}
-		}
-
-		> .cw {
-			z-index: 1;
-			padding-bottom: 8px;
-			border-bottom: solid 0.5px var(--divider);
-		}
-
-		> .hashtags {
-			z-index: 1;
-			padding-top: 8px;
-			padding-bottom: 8px;
-			border-top: solid 0.5px var(--divider);
-		}
-
-		> .text {
-			max-width: 100%;
-			min-width: 100%;
-			min-height: 90px;
-
-			&.withCw {
-				padding-top: 8px;
-			}
-		}
-
-		> footer {
-			padding: 0 16px 16px 16px;
-
-			> button {
-				display: inline-block;
-				padding: 0;
-				margin: 0;
-				font-size: 16px;
-				width: 48px;
-				height: 48px;
-				border-radius: 6px;
-
-				&:hover {
-					background: var(--X5);
-				}
-
-				&.active {
-					color: var(--accent);
-				}
-			}
-		}
-	}
-
-	&.max-width_500px {
-		> header {
-			height: 50px;
-
-			> .cancel {
-				width: 50px;
-				line-height: 50px;
-			}
-
-			> div {
-				> .text-count {
-					line-height: 50px;
-				}
-
-				> .submit {
-					margin: 8px;
-				}
-			}
-		}
-
-		> .form {
-			> .to-specified {
-				padding: 6px 16px;
-			}
-
-			> .cw,
-			> .hashtags,
-			> .text {
-				padding: 0 16px;
-			}
-
-			> .text {
-				min-height: 80px;
-			}
-
-			> footer {
-				padding: 0 8px 8px 8px;
-			}
-		}
-	}
-
-	&.max-width_310px {
-		> .form {
-			> footer {
-				> button {
-					font-size: 14px;
-					width: 44px;
-				height: 44px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/queue-chart.vue b/src/client/components/queue-chart.vue
deleted file mode 100644
index f9c3eccfb5..0000000000
--- a/src/client/components/queue-chart.vue
+++ /dev/null
@@ -1,232 +0,0 @@
-<template>
-<canvas ref="chartEl"></canvas>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
-import {
-	Chart,
-	ArcElement,
-	LineElement,
-	BarElement,
-	PointElement,
-	BarController,
-	LineController,
-	CategoryScale,
-	LinearScale,
-	TimeScale,
-	Legend,
-	Title,
-	Tooltip,
-	SubTitle,
-	Filler,
-} from 'chart.js';
-import number from '@client/filters/number';
-import * as os from '@client/os';
-import { defaultStore } from '@client/store';
-
-Chart.register(
-	ArcElement,
-	LineElement,
-	BarElement,
-	PointElement,
-	BarController,
-	LineController,
-	CategoryScale,
-	LinearScale,
-	TimeScale,
-	Legend,
-	Title,
-	Tooltip,
-	SubTitle,
-	Filler,
-);
-
-const alpha = (hex, a) => {
-	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
-	return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
-
-export default defineComponent({
-	props: {
-		domain: {
-			type: String,
-			required: true,
-		},
-		connection: {
-			required: true,
-		},
-	},
-
-	setup(props) {
-		const chartEl = ref<HTMLCanvasElement>(null);
-
-		const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
-		// フォントカラー
-		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
-		onMounted(() => {
-			const chartInstance = new Chart(chartEl.value, {
-				type: 'line',
-				data: {
-					labels: [],
-					datasets: [{
-						label: 'Process',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderJoinStyle: 'round',
-						borderColor: '#00E396',
-						backgroundColor: alpha('#00E396', 0.1),
-						data: []
-					}, {
-						label: 'Active',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderJoinStyle: 'round',
-						borderColor: '#00BCD4',
-						backgroundColor: alpha('#00BCD4', 0.1),
-						data: []
-					}, {
-						label: 'Waiting',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderJoinStyle: 'round',
-						borderColor: '#FFB300',
-						backgroundColor: alpha('#FFB300', 0.1),
-						yAxisID: 'y2',
-						data: []
-					}, {
-						label: 'Delayed',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderJoinStyle: 'round',
-						borderColor: '#E53935',
-						borderDash: [5, 5],
-						fill: false,
-						yAxisID: 'y2',
-						data: []
-					}],
-				},
-				options: {
-					aspectRatio: 2.5,
-					layout: {
-						padding: {
-							left: 16,
-							right: 16,
-							top: 16,
-							bottom: 8,
-						},
-					},
-					scales: {
-						x: {
-							grid: {
-								display: true,
-								color: gridColor,
-								borderColor: 'rgb(0, 0, 0, 0)',
-							},
-							ticks: {
-								display: false,
-								maxTicksLimit: 10
-							},
-						},
-						y: {
-							min: 0,
-							stack: 'queue',
-							stackWeight: 2,
-							grid: {
-								color: gridColor,
-								borderColor: 'rgb(0, 0, 0, 0)',
-							},
-						},
-						y2: {
-							min: 0,
-							offset: true,
-							stack: 'queue',
-							stackWeight: 1,
-							grid: {
-								color: gridColor,
-								borderColor: 'rgb(0, 0, 0, 0)',
-							},
-						},
-					},
-					interaction: {
-						intersect: false,
-					},
-					plugins: {
-						legend: {
-							position: 'bottom',
-							labels: {
-								boxWidth: 16,
-							},
-						},
-						tooltip: {
-							mode: 'index',
-							animation: {
-								duration: 0,
-							},
-						},
-					},
-				},
-			});
-
-			const onStats = (stats) => {
-				chartInstance.data.labels.push('');
-				chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
-				chartInstance.data.datasets[1].data.push(stats[props.domain].active);
-				chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
-				chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
-				if (chartInstance.data.datasets[0].data.length > 200) {
-					chartInstance.data.labels.shift();
-					chartInstance.data.datasets[0].data.shift();
-					chartInstance.data.datasets[1].data.shift();
-					chartInstance.data.datasets[2].data.shift();
-					chartInstance.data.datasets[3].data.shift();
-				}
-				chartInstance.update();
-			};
-
-			const onStatsLog = (statsLog) => {
-				for (const stats of [...statsLog].reverse()) {
-					chartInstance.data.labels.push('');
-					chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
-					chartInstance.data.datasets[1].data.push(stats[props.domain].active);
-					chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
-					chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
-					if (chartInstance.data.datasets[0].data.length > 200) {
-						chartInstance.data.labels.shift();
-						chartInstance.data.datasets[0].data.shift();
-						chartInstance.data.datasets[1].data.shift();
-						chartInstance.data.datasets[2].data.shift();
-						chartInstance.data.datasets[3].data.shift();
-					}
-				}
-				chartInstance.update();
-			};
-
-			props.connection.on('stats', onStats);
-			props.connection.on('statsLog', onStatsLog);
-
-			onUnmounted(() => {
-				props.connection.off('stats', onStats);
-				props.connection.off('statsLog', onStatsLog);
-			});
-		});
-
-		return {
-			chartEl,
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/components/reaction-icon.vue b/src/client/components/reaction-icon.vue
deleted file mode 100644
index c0ec955e32..0000000000
--- a/src/client/components/reaction-icon.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<template>
-<MkEmoji :emoji="reaction" :custom-emojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		reaction: {
-			type: String,
-			required: true
-		},
-		customEmojis: {
-			required: false,
-			default: () => []
-		},
-		noStyle: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-});
-</script>
diff --git a/src/client/components/reaction-tooltip.vue b/src/client/components/reaction-tooltip.vue
deleted file mode 100644
index 93143cbe81..0000000000
--- a/src/client/components/reaction-tooltip.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-<MkTooltip :source="source" ref="tooltip" @closed="$emit('closed')" :max-width="340">
-	<div class="beeadbfb">
-		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
-		<div class="name">{{ reaction.replace('@.', '') }}</div>
-	</div>
-</MkTooltip>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkTooltip from './ui/tooltip.vue';
-import XReactionIcon from './reaction-icon.vue';
-
-export default defineComponent({
-	components: {
-		MkTooltip,
-		XReactionIcon,
-	},
-	props: {
-		reaction: {
-			type: String,
-			required: true,
-		},
-		emojis: {
-			type: Array,
-			required: true,
-		},
-		source: {
-			required: true,
-		}
-	},
-	emits: ['closed'],
-})
-</script>
-
-<style lang="scss" scoped>
-.beeadbfb {
-	text-align: center;
-
-	> .icon {
-		display: block;
-		width: 60px;
-		margin: 0 auto;
-	}
-
-	> .name {
-		font-size: 0.9em;
-	}
-}
-</style>
diff --git a/src/client/components/reactions-viewer.details.vue b/src/client/components/reactions-viewer.details.vue
deleted file mode 100644
index 7c49bd1d9c..0000000000
--- a/src/client/components/reactions-viewer.details.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<template>
-<MkTooltip :source="source" ref="tooltip" @closed="$emit('closed')" :max-width="340">
-	<div class="bqxuuuey">
-		<div class="reaction">
-			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
-			<div class="name">{{ reaction.replace('@.', '') }}</div>
-		</div>
-		<div class="users">
-			<template v-if="users.length <= 10">
-				<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
-					<MkAvatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
-					<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/>
-				</b>
-			</template>
-			<template v-if="10 < users.length">
-				<b v-for="u in users" :key="u.id" style="margin-right: 12px;">
-					<MkAvatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/>
-					<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/>
-				</b>
-				<span slot="omitted">+{{ count - 10 }}</span>
-			</template>
-		</div>
-	</div>
-</MkTooltip>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkTooltip from './ui/tooltip.vue';
-import XReactionIcon from './reaction-icon.vue';
-
-export default defineComponent({
-	components: {
-		MkTooltip,
-		XReactionIcon
-	},
-	props: {
-		reaction: {
-			type: String,
-			required: true,
-		},
-		users: {
-			type: Array,
-			required: true,
-		},
-		count: {
-			type: Number,
-			required: true,
-		},
-		emojis: {
-			type: Array,
-			required: true,
-		},
-		source: {
-			required: true,
-		}
-	},
-	emits: ['closed'],
-})
-</script>
-
-<style lang="scss" scoped>
-.bqxuuuey {
-	display: flex;
-
-	> .reaction {
-		flex: 1;
-		max-width: 100px;
-		text-align: center;
-
-		> .icon {
-			display: block;
-			width: 60px;
-			margin: 0 auto;
-		}
-
-		> .name {
-			font-size: 0.9em;
-		}
-	}
-
-	> .users {
-		flex: 1;
-		min-width: 0;
-		font-size: 0.9em;
-		border-left: solid 0.5px var(--divider);
-		padding-left: 10px;
-    margin-left: 10px;
-	}
-}
-</style>
diff --git a/src/client/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue
deleted file mode 100644
index f47ba83f61..0000000000
--- a/src/client/components/reactions-viewer.reaction.vue
+++ /dev/null
@@ -1,183 +0,0 @@
-<template>
-<button
-	class="hkzvhatu _button"
-	:class="{ reacted: note.myReaction == reaction, canToggle }"
-	@click="toggleReaction(reaction)"
-	v-if="count > 0"
-	@touchstart.passive="onMouseover"
-	@mouseover="onMouseover"
-	@mouseleave="onMouseleave"
-	@touchend="onMouseleave"
-	ref="reaction"
-	v-particle="canToggle"
->
-	<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
-	<span>{{ count }}</span>
-</button>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref } from 'vue';
-import XDetails from '@client/components/reactions-viewer.details.vue';
-import XReactionIcon from '@client/components/reaction-icon.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XReactionIcon
-	},
-	props: {
-		reaction: {
-			type: String,
-			required: true,
-		},
-		count: {
-			type: Number,
-			required: true,
-		},
-		isInitial: {
-			type: Boolean,
-			required: true,
-		},
-		note: {
-			type: Object,
-			required: true,
-		},
-	},
-	data() {
-		return {
-			close: null,
-			detailsTimeoutId: null,
-			isHovering: false
-		};
-	},
-	computed: {
-		canToggle(): boolean {
-			return !this.reaction.match(/@\w/) && this.$i;
-		},
-	},
-	watch: {
-		count(newCount, oldCount) {
-			if (oldCount < newCount) this.anime();
-			if (this.close != null) this.openDetails();
-		},
-	},
-	mounted() {
-		if (!this.isInitial) this.anime();
-	},
-	methods: {
-		toggleReaction() {
-			if (!this.canToggle) return;
-
-			const oldReaction = this.note.myReaction;
-			if (oldReaction) {
-				os.api('notes/reactions/delete', {
-					noteId: this.note.id
-				}).then(() => {
-					if (oldReaction !== this.reaction) {
-						os.api('notes/reactions/create', {
-							noteId: this.note.id,
-							reaction: this.reaction
-						});
-					}
-				});
-			} else {
-				os.api('notes/reactions/create', {
-					noteId: this.note.id,
-					reaction: this.reaction
-				});
-			}
-		},
-		onMouseover() {
-			if (this.isHovering) return;
-			this.isHovering = true;
-			this.detailsTimeoutId = setTimeout(this.openDetails, 300);
-		},
-		onMouseleave() {
-			if (!this.isHovering) return;
-			this.isHovering = false;
-			clearTimeout(this.detailsTimeoutId);
-			this.closeDetails();
-		},
-		openDetails() {
-			os.api('notes/reactions', {
-				noteId: this.note.id,
-				type: this.reaction,
-				limit: 11
-			}).then((reactions: any[]) => {
-				const users = reactions
-					.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
-					.map(x => x.user);
-
-				this.closeDetails();
-				if (!this.isHovering) return;
-
-				const showing = ref(true);
-				os.popup(XDetails, {
-					showing,
-					reaction: this.reaction,
-					emojis: this.note.emojis,
-					users,
-					count: this.count,
-					source: this.$refs.reaction
-				}, {}, 'closed');
-
-				this.close = () => {
-					showing.value = false;
-				};
-			});
-		},
-		closeDetails() {
-			if (this.close != null) {
-				this.close();
-				this.close = null;
-			}
-		},
-		anime() {
-			if (document.hidden) return;
-
-			// TODO
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hkzvhatu {
-	display: inline-block;
-	height: 32px;
-	margin: 2px;
-	padding: 0 6px;
-	border-radius: 4px;
-
-	&.canToggle {
-		background: rgba(0, 0, 0, 0.05);
-
-		&:hover {
-			background: rgba(0, 0, 0, 0.1);
-		}
-	}
-
-	&:not(.canToggle) {
-		cursor: default;
-	}
-
-	&.reacted {
-		background: var(--accent);
-
-		&:hover {
-			background: var(--accent);
-		}
-
-		> span {
-			color: var(--fgOnAccent);
-		}
-	}
-
-	> span {
-		font-size: 0.9em;
-		line-height: 32px;
-		margin: 0 0 0 4px;
-	}
-}
-</style>
diff --git a/src/client/components/reactions-viewer.vue b/src/client/components/reactions-viewer.vue
deleted file mode 100644
index 94a0318734..0000000000
--- a/src/client/components/reactions-viewer.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<div class="tdflqwzn" :class="{ isMe }">
-	<XReaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XReaction from './reactions-viewer.reaction.vue';
-
-export default defineComponent({
-	components: {
-		XReaction
-	},
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-	data() {
-		return {
-			initialReactions: new Set(Object.keys(this.note.reactions))
-		};
-	},
-	computed: {
-		isMe(): boolean {
-			return this.$i && this.$i.id === this.note.userId;
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.tdflqwzn {
-	margin: 4px -2px 0 -2px;
-
-	&:empty {
-		display: none;
-	}
-
-	&.isMe {
-		> span {
-			cursor: default !important;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/remote-caution.vue b/src/client/components/remote-caution.vue
deleted file mode 100644
index 985ae44694..0000000000
--- a/src/client/components/remote-caution.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<template>
-<div class="jmgmzlwq _block"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ $ts.remoteUserCaution }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		href: {
-			type: String,
-			required: true
-		},
-	},
-	data() {
-		return {
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.jmgmzlwq {
-	font-size: 0.8em;
-	padding: 16px;
-	background: var(--infoWarnBg);
-	color: var(--infoWarnFg);
-
-	> a {
-		margin-left: 4px;
-		color: var(--accent);
-	}
-}
-</style>
diff --git a/src/client/components/sample.vue b/src/client/components/sample.vue
deleted file mode 100644
index c8b46a80e7..0000000000
--- a/src/client/components/sample.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<template>
-<div class="_card">
-	<div class="_content">
-		<MkInput v-model="text">
-			<template #label>Text</template>
-		</MkInput>
-		<MkSwitch v-model="flag">
-			<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
-		</MkSwitch>
-		<div style="margin: 32px 0;">
-			<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
-			<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
-			<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
-		</div>
-		<MkButton inline>This is</MkButton>
-		<MkButton inline primary>the button</MkButton>
-	</div>
-	<div class="_content" style="pointer-events: none;">
-		<Mfm :text="mfm"/>
-	</div>
-	<div class="_content">
-		<MkButton inline primary @click="openMenu">Open menu</MkButton>
-		<MkButton inline primary @click="openDialog">Open dialog</MkButton>
-		<MkButton inline primary @click="openForm">Open form</MkButton>
-		<MkButton inline primary @click="openDrive">Open drive</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkRadio from '@client/components/form/radio.vue';
-import * as os from '@client/os';
-import * as config from '@client/config';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSwitch,
-		MkTextarea,
-		MkRadio,
-	},
-
-	data() {
-		return {
-			text: '',
-			flag: true,
-			radio: 'misskey',
-			mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
-		}
-	},
-
-	methods: {
-		async openDialog() {
-			os.dialog({
-				type: 'warning',
-				title: 'Oh my Aichan',
-				text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
-			});
-		},
-
-		async openForm() {
-			os.form('Example form', {
-				foo: {
-					type: 'boolean',
-					default: true,
-					label: 'This is a boolean property'
-				},
-				bar: {
-					type: 'number',
-					default: 300,
-					label: 'This is a number property'
-				},
-				baz: {
-					type: 'string',
-					default: 'Misskey makes you happy.',
-					label: 'This is a string property'
-				},
-			});
-		},
-
-		async openDrive() {
-			os.selectDriveFile();
-		},
-
-		async selectUser() {
-			os.selectUser();
-		},
-
-		async openMenu(ev) {
-			os.popupMenu([{
-				type: 'label',
-				text: 'Fruits'
-			}, {
-				text: 'Create some apples',
-				action: () => {},
-			}, {
-				text: 'Read some oranges',
-				action: () => {},
-			}, {
-				text: 'Update some melons',
-				action: () => {},
-			}, null, {
-				text: 'Delete some bananas',
-				danger: true,
-				action: () => {},
-			}], ev.currentTarget || ev.target);
-		},
-	}
-});
-</script>
diff --git a/src/client/components/signin-dialog.vue b/src/client/components/signin-dialog.vue
deleted file mode 100644
index 6c38c07d78..0000000000
--- a/src/client/components/signin-dialog.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="370"
-	:height="400"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
->
-	<template #header>{{ $ts.login }}</template>
-
-	<MkSignin :auto-set="autoSet" @login="onLogin"/>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkSignin from './signin.vue';
-
-export default defineComponent({
-	components: {
-		MkSignin,
-		XModalWindow,
-	},
-
-	props: {
-		autoSet: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	methods: {
-		onLogin(res) {
-			this.$emit('done', res);
-			this.$refs.dialog.close();
-		}
-	}
-});
-</script>
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
deleted file mode 100755
index d6e1ee8b68..0000000000
--- a/src/client/components/signin.vue
+++ /dev/null
@@ -1,240 +0,0 @@
-<template>
-<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
-	<div class="auth _section _formRoot">
-		<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
-		<div class="normal-signin" v-if="!totpLogin">
-			<MkInput class="_formBlock" v-model="username" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @update:modelValue="onUsernameChange" data-cy-signin-username>
-				<template #prefix>@</template>
-				<template #suffix>@{{ host }}</template>
-			</MkInput>
-			<MkInput class="_formBlock" v-model="password" :placeholder="$ts.password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required data-cy-signin-password>
-				<template #prefix><i class="fas fa-lock"></i></template>
-				<template #caption><button class="_textButton" @click="resetPassword" type="button">{{ $ts.forgotPassword }}</button></template>
-			</MkInput>
-			<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
-		</div>
-		<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
-			<div v-if="user && user.securityKeys" class="twofa-group tap-group">
-				<p>{{ $ts.tapSecurityKey }}</p>
-				<MkButton @click="queryKey" v-if="!queryingKey">
-					{{ $ts.retry }}
-				</MkButton>
-			</div>
-			<div class="or-hr" v-if="user && user.securityKeys">
-				<p class="or-msg">{{ $ts.or }}</p>
-			</div>
-			<div class="twofa-group totp-group">
-				<p style="margin-bottom:0;">{{ $ts.twoStepAuthentication }}</p>
-				<MkInput v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
-					<template #label>{{ $ts.password }}</template>
-					<template #prefix><i class="fas fa-lock"></i></template>
-				</MkInput>
-				<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
-					<template #label>{{ $ts.token }}</template>
-					<template #prefix><i class="fas fa-gavel"></i></template>
-				</MkInput>
-				<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
-			</div>
-		</div>
-	</div>
-	<div class="social _section">
-		<a class="_borderButton _gap" v-if="meta && meta.enableTwitterIntegration" :href="`${apiUrl}/signin/twitter`"><i class="fab fa-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
-		<a class="_borderButton _gap" v-if="meta && meta.enableGithubIntegration" :href="`${apiUrl}/signin/github`"><i class="fab fa-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
-		<a class="_borderButton _gap" v-if="meta && meta.enableDiscordIntegration" :href="`${apiUrl}/signin/discord`"><i class="fab fa-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
-	</div>
-</form>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { toUnicode } from 'punycode/';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import { apiUrl, host } from '@client/config';
-import { byteify, hexify } from '@client/scripts/2fa';
-import * as os from '@client/os';
-import { login } from '@client/account';
-import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-	},
-
-	props: {
-		withAvatar: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-		autoSet: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['login'],
-
-	data() {
-		return {
-			signing: false,
-			user: null,
-			username: '',
-			password: '',
-			token: '',
-			apiUrl,
-			host: toUnicode(host),
-			totpLogin: false,
-			credential: null,
-			challengeData: null,
-			queryingKey: false,
-		};
-	},
-
-	computed: {
-		meta() {
-			return this.$instance;
-		},
-	},
-
-	methods: {
-		onUsernameChange() {
-			os.api('users/show', {
-				username: this.username
-			}).then(user => {
-				this.user = user;
-			}, () => {
-				this.user = null;
-			});
-		},
-
-		onLogin(res) {
-			if (this.autoSet) {
-				return login(res.i);
-			} else {
-				return;
-			}
-		},
-
-		queryKey() {
-			this.queryingKey = true;
-			return navigator.credentials.get({
-				publicKey: {
-					challenge: byteify(this.challengeData.challenge, 'base64'),
-					allowCredentials: this.challengeData.securityKeys.map(key => ({
-						id: byteify(key.id, 'hex'),
-						type: 'public-key',
-						transports: ['usb', 'nfc', 'ble', 'internal']
-					})),
-					timeout: 60 * 1000
-				}
-			}).catch(() => {
-				this.queryingKey = false;
-				return Promise.reject(null);
-			}).then(credential => {
-				this.queryingKey = false;
-				this.signing = true;
-				return os.api('signin', {
-					username: this.username,
-					password: this.password,
-					signature: hexify(credential.response.signature),
-					authenticatorData: hexify(credential.response.authenticatorData),
-					clientDataJSON: hexify(credential.response.clientDataJSON),
-					credentialId: credential.id,
-					challengeId: this.challengeData.challengeId
-				});
-			}).then(res => {
-				this.$emit('login', res);
-				return this.onLogin(res);
-			}).catch(err => {
-				if (err === null) return;
-				os.dialog({
-					type: 'error',
-					text: this.$ts.signinFailed
-				});
-				this.signing = false;
-			});
-		},
-
-		onSubmit() {
-			this.signing = true;
-			if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
-				if (window.PublicKeyCredential && this.user.securityKeys) {
-					os.api('signin', {
-						username: this.username,
-						password: this.password
-					}).then(res => {
-						this.totpLogin = true;
-						this.signing = false;
-						this.challengeData = res;
-						return this.queryKey();
-					}).catch(this.loginFailed);
-				} else {
-					this.totpLogin = true;
-					this.signing = false;
-				}
-			} else {
-				os.api('signin', {
-					username: this.username,
-					password: this.password,
-					token: this.user && this.user.twoFactorEnabled ? this.token : undefined
-				}).then(res => {
-					this.$emit('login', res);
-					this.onLogin(res);
-				}).catch(this.loginFailed);
-			}
-		},
-
-		loginFailed(err) {
-			switch (err.id) {
-				case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
-					os.dialog({
-						type: 'error',
-						title: this.$ts.loginFailed,
-						text: this.$ts.noSuchUser
-					});
-					break;
-				}
-				case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
-					showSuspendedDialog();
-					break;
-				}
-				default: {
-					os.dialog({
-						type: 'error',
-						title: this.$ts.loginFailed,
-						text: JSON.stringify(err)
-					});
-				}
-			}
-
-			this.challengeData = null;
-			this.totpLogin = false;
-			this.signing = false;
-		},
-
-		resetPassword() {
-			os.popup(import('@client/components/forgot-password.vue'), {}, {
-			}, 'closed');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.eppvobhk {
-	> .auth {
-		> .avatar {
-			margin: 0 auto 0 auto;
-			width: 64px;
-			height: 64px;
-			background: #ddd;
-			background-position: center;
-			background-size: cover;
-			border-radius: 100%;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/signup-dialog.vue b/src/client/components/signup-dialog.vue
deleted file mode 100644
index 9741e8c73b..0000000000
--- a/src/client/components/signup-dialog.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="366"
-	:height="500"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
->
-	<template #header>{{ $ts.signup }}</template>
-
-	<div class="_monolithic_">
-		<div class="_section">
-			<XSignup :auto-set="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
-		</div>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import XSignup from './signup.vue';
-
-export default defineComponent({
-	components: {
-		XSignup,
-		XModalWindow,
-	},
-
-	props: {
-		autoSet: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	methods: {
-		onSignup(res) {
-			this.$emit('done', res);
-			this.$refs.dialog.close();
-		},
-
-		onSignupEmailPending() {
-			this.$refs.dialog.close();
-		}
-	}
-});
-</script>
diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue
deleted file mode 100644
index 8d4340fd36..0000000000
--- a/src/client/components/signup.vue
+++ /dev/null
@@ -1,268 +0,0 @@
-<template>
-<form class="qlvuhzng _formRoot" @submit.prevent="onSubmit" :autocomplete="Math.random()">
-	<template v-if="meta">
-		<MkInput class="_formBlock" v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
-			<template #label>{{ $ts.invitationCode }}</template>
-			<template #prefix><i class="fas fa-key"></i></template>
-		</MkInput>
-		<MkInput class="_formBlock" v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
-			<template #label>{{ $ts.username }} <div class="_button _help" v-tooltip:dialog="$ts.usernameInfo"><i class="far fa-question-circle"></i></div></template>
-			<template #prefix>@</template>
-			<template #suffix>@{{ host }}</template>
-			<template #caption>
-				<span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
-				<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
-				<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
-				<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
-				<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
-				<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
-				<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
-			</template>
-		</MkInput>
-		<MkInput v-if="meta.emailRequiredForSignup" class="_formBlock" v-model="email" :debounce="true" type="email" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeEmail" data-cy-signup-email>
-			<template #label>{{ $ts.emailAddress }} <div class="_button _help" v-tooltip:dialog="$ts._signup.emailAddressInfo"><i class="far fa-question-circle"></i></div></template>
-			<template #prefix><i class="fas fa-envelope"></i></template>
-			<template #caption>
-				<span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
-				<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
-				<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
-				<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
-				<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
-				<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
-				<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
-				<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
-				<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
-			</template>
-		</MkInput>
-		<MkInput class="_formBlock" v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword" data-cy-signup-password>
-			<template #label>{{ $ts.password }}</template>
-			<template #prefix><i class="fas fa-lock"></i></template>
-			<template #caption>
-				<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.weakPassword }}</span>
-				<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="fas fa-check fa-fw"></i> {{ $ts.normalPassword }}</span>
-				<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
-			</template>
-		</MkInput>
-		<MkInput class="_formBlock" v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePasswordRetype" data-cy-signup-password-retype>
-			<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
-			<template #prefix><i class="fas fa-lock"></i></template>
-			<template #caption>
-				<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.passwordMatched }}</span>
-				<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
-			</template>
-		</MkInput>
-		<label v-if="meta.tosUrl" class="_formBlock tou">
-			<input type="checkbox" v-model="ToSAgreement">
-			<I18n :src="$ts.agreeTo">
-				<template #0>
-					<a :href="meta.tosUrl" class="_link" target="_blank">{{ $ts.tos }}</a>
-				</template>
-			</I18n>
-		</label>
-		<captcha v-if="meta.enableHcaptcha" class="_formBlock captcha" provider="hcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
-		<captcha v-if="meta.enableRecaptcha" class="_formBlock captcha" provider="recaptcha" ref="recaptcha" v-model="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
-		<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
-	</template>
-</form>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-const getPasswordStrength = require('syuilo-password-strength');
-import { toUnicode } from 'punycode/';
-import { host, url } from '@client/config';
-import MkButton from './ui/button.vue';
-import MkInput from './form/input.vue';
-import MkSwitch from './form/switch.vue';
-import * as os from '@client/os';
-import { login } from '@client/account';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSwitch,
-		captcha: defineAsyncComponent(() => import('./captcha.vue')),
-	},
-
-	props: {
-		autoSet: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['signup'],
-
-	data() {
-		return {
-			host: toUnicode(host),
-			username: '',
-			password: '',
-			retypedPassword: '',
-			invitationCode: '',
-			email: '',
-			url,
-			usernameState: null,
-			emailState: null,
-			passwordStrength: '',
-			passwordRetypeState: null,
-			submitting: false,
-			ToSAgreement: false,
-			hCaptchaResponse: null,
-			reCaptchaResponse: null,
-		}
-	},
-
-	computed: {
-		meta() {
-			return this.$instance;
-		},
-
-		shouldDisableSubmitting(): boolean {
-			return this.submitting ||
-				this.meta.tosUrl && !this.ToSAgreement ||
-				this.meta.enableHcaptcha && !this.hCaptchaResponse ||
-				this.meta.enableRecaptcha && !this.reCaptchaResponse ||
-				this.passwordRetypeState == 'not-match';
-		},
-
-		shouldShowProfileUrl(): boolean {
-			return (this.username != '' &&
-				this.usernameState != 'invalid-format' &&
-				this.usernameState != 'min-range' &&
-				this.usernameState != 'max-range');
-		}
-	},
-
-	methods: {
-		onChangeUsername() {
-			if (this.username == '') {
-				this.usernameState = null;
-				return;
-			}
-
-			const err =
-				!this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
-				this.username.length < 1 ? 'min-range' :
-				this.username.length > 20 ? 'max-range' :
-				null;
-
-			if (err) {
-				this.usernameState = err;
-				return;
-			}
-
-			this.usernameState = 'wait';
-
-			os.api('username/available', {
-				username: this.username
-			}).then(result => {
-				this.usernameState = result.available ? 'ok' : 'unavailable';
-			}).catch(err => {
-				this.usernameState = 'error';
-			});
-		},
-
-		onChangeEmail() {
-			if (this.email == '') {
-				this.emailState = null;
-				return;
-			}
-
-			this.emailState = 'wait';
-
-			os.api('email-address/available', {
-				emailAddress: this.email
-			}).then(result => {
-				this.emailState = result.available ? 'ok' :
-					result.reason === 'used' ? 'unavailable:used' :
-					result.reason === 'format' ? 'unavailable:format' :
-					result.reason === 'disposable' ? 'unavailable:disposable' :
-					result.reason === 'mx' ? 'unavailable:mx' :
-					result.reason === 'smtp' ? 'unavailable:smtp' :
-					'unavailable';
-			}).catch(err => {
-				this.emailState = 'error';
-			});
-		},
-
-		onChangePassword() {
-			if (this.password == '') {
-				this.passwordStrength = '';
-				return;
-			}
-
-			const strength = getPasswordStrength(this.password);
-			this.passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
-		},
-
-		onChangePasswordRetype() {
-			if (this.retypedPassword == '') {
-				this.passwordRetypeState = null;
-				return;
-			}
-
-			this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
-		},
-
-		onSubmit() {
-			if (this.submitting) return;
-			this.submitting = true;
-
-			os.api('signup', {
-				username: this.username,
-				password: this.password,
-				emailAddress: this.email,
-				invitationCode: this.invitationCode,
-				'hcaptcha-response': this.hCaptchaResponse,
-				'g-recaptcha-response': this.reCaptchaResponse,
-			}).then(() => {
-				if (this.meta.emailRequiredForSignup) {
-					os.dialog({
-						type: 'success',
-						title: this.$ts._signup.almostThere,
-						text: this.$t('_signup.emailSent', { email: this.email }),
-					});
-					this.$emit('signupEmailPending');
-				} else {
-					os.api('signin', {
-						username: this.username,
-						password: this.password
-					}).then(res => {
-						this.$emit('signup', res);
-
-						if (this.autoSet) {
-							login(res.i);
-						}
-					});
-				}
-			}).catch(() => {
-				this.submitting = false;
-				this.$refs.hcaptcha?.reset?.();
-				this.$refs.recaptcha?.reset?.();
-
-				os.dialog({
-					type: 'error',
-					text: this.$ts.somethingHappened
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.qlvuhzng {
-	.captcha {
-		margin: 16px 0;
-	}
-
-	> .tou {
-		display: block;
-		margin: 16px 0;
-		cursor: pointer;
-	}
-}
-</style>
diff --git a/src/client/components/sparkle.vue b/src/client/components/sparkle.vue
deleted file mode 100644
index 942412b445..0000000000
--- a/src/client/components/sparkle.vue
+++ /dev/null
@@ -1,180 +0,0 @@
-<template>
-<span class="mk-sparkle">
-	<span ref="content">
-		<slot></slot>
-	</span>
-	<canvas ref="canvas"></canvas>
-</span>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-const sprite = new Image();
-sprite.src = "/static-assets/client/sparkle-spritesheet.png";
-
-
-export default defineComponent({
-	props: {
-		count: {
-			type: Number,
-			required: true,
-		},
-		speed: {
-			type: Number,
-			required: true,
-		},
-	},
-	data() {
-		return {
-			sprites: [0,6,13,20],
-			particles: [],
-			anim: null,
-			ctx: null,
-		};
-	},
-	methods: {
-		createSparkles(w, h, count) {
-			var holder = [];
-
-			for (var i = 0; i < count; i++) {
-
-				const color = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
-
-				holder[i] = {
-					position: {
-						x: Math.floor(Math.random() * w),
-						y: Math.floor(Math.random() * h)
-					},
-					style: this.sprites[ Math.floor(Math.random() * 4) ],
-					delta: {
-						x: Math.floor(Math.random() * 1000) - 500,
-						y: Math.floor(Math.random() * 1000) - 500
-					},
-					color: color,
-					opacity: Math.random(),
-				};
-
-			}
-
-			return holder;
-		},
-		draw(time) {
-			this.ctx.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
-			this.ctx.beginPath();
-
-			const particleSize = Math.floor(this.fontSize / 2);
-			this.particles.forEach((particle) => {
-				var modulus = Math.floor(Math.random()*7);
-
-				if (Math.floor(time) % modulus === 0) {
-					particle.style = this.sprites[ Math.floor(Math.random()*4) ];
-				}
-
-				this.ctx.save();
-				this.ctx.globalAlpha = particle.opacity;
-				this.ctx.drawImage(sprite, particle.style, 0, 7, 7, particle.position.x, particle.position.y, particleSize, particleSize);
-
-				this.ctx.globalCompositeOperation = "source-atop";
-				this.ctx.globalAlpha = 0.5;
-				this.ctx.fillStyle = particle.color;
-				this.ctx.fillRect(particle.position.x, particle.position.y, particleSize, particleSize);
-
-				this.ctx.restore();
-			});
-			this.ctx.stroke();
-		},
-		tick() {
-			this.anim = window.requestAnimationFrame((time) => {
-				if (!this.$refs.canvas) {
-					return;
-				}
-				this.particles.forEach((particle) => {
-					if (!particle) {
-						return;
-					}
-					var randX = Math.random() > Math.random() * 2;
-					var randY = Math.random() > Math.random() * 3;
-
-					if (randX) {
-						particle.position.x += (particle.delta.x * this.speed) / 1500;
-					}
-
-					if (!randY) {
-						particle.position.y -= (particle.delta.y * this.speed) / 800;
-					}
-
-					if( particle.position.x > this.$refs.canvas.width ) {
-						particle.position.x = -7;
-					} else if (particle.position.x < -7) {
-						particle.position.x = this.$refs.canvas.width;
-					}
-
-					if (particle.position.y > this.$refs.canvas.height) {
-						particle.position.y = -7;
-						particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width);
-					} else if (particle.position.y < -7) {
-						particle.position.y = this.$refs.canvas.height;
-						particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width);
-					}
-
-					particle.opacity -= 0.005;
-
-					if (particle.opacity <= 0) {
-						particle.opacity = 1;
-					}
-				});
-
-				this.draw(time);
-
-				this.tick();
-			});
-		},
-		resize() {
-			if (this.$refs.content) {
-				const contentRect = this.$refs.content.getBoundingClientRect();
-				this.fontSize = parseFloat(getComputedStyle(this.$refs.content).fontSize);
-				const padding = this.fontSize * 0.2;
-
-				this.$refs.canvas.width = parseInt(contentRect.width + padding);
-				this.$refs.canvas.height = parseInt(contentRect.height + padding);
-
-				this.particles = this.createSparkles(this.$refs.canvas.width, this.$refs.canvas.height, this.count);
-			}
-		},
-	},
-	mounted() {
-		this.ctx = this.$refs.canvas.getContext('2d');
-
-		new ResizeObserver(this.resize).observe(this.$refs.content);
-
-		this.resize();
-		this.tick();
-	},
-	updated() {
-		this.resize();
-	},
-	destroyed() {
-		window.cancelAnimationFrame(this.anim);
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-sparkle {
-	position: relative;
-	display: inline-block;
-
-	> span {
-		display: inline-block;
-	}
-
-	> canvas {
-		position: absolute;
-		top: -0.1em;
-		left: -0.1em;
-		pointer-events: none;
-	}
-}
-</style>
diff --git a/src/client/components/sub-note-content.vue b/src/client/components/sub-note-content.vue
deleted file mode 100644
index ff89a9887b..0000000000
--- a/src/client/components/sub-note-content.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="wrmlmaau">
-	<div class="body">
-		<span v-if="note.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
-		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $ts.deleted }})</span>
-		<MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
-		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
-		<MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
-	</div>
-	<details v-if="note.files.length > 0">
-		<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
-		<XMediaList :media-list="note.files"/>
-	</details>
-	<details v-if="note.poll">
-		<summary>{{ $ts.poll }}</summary>
-		<XPoll :note="note"/>
-	</details>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XPoll from './poll.vue';
-import XMediaList from './media-list.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XPoll,
-		XMediaList,
-	},
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-	data() {
-		return {
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.wrmlmaau {
-	overflow-wrap: break-word;
-
-	> .body {
-		> .reply {
-			margin-right: 6px;
-			color: var(--accent);
-		}
-
-		> .rp {
-			margin-left: 4px;
-			font-style: oblique;
-			color: var(--renote);
-		}
-	}
-}
-</style>
diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue
deleted file mode 100644
index c629727358..0000000000
--- a/src/client/components/tab.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<script lang="ts">
-import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
-
-export default defineComponent({
-	props: {
-		modelValue: {
-			required: true,
-		},
-	},
-	render() {
-		const options = this.$slots.default();
-
-		return withDirectives(h('div', {
-			class: 'pxhvhrfw',
-		}, options.map(option => withDirectives(h('button', {
-			class: ['_button', { active: this.modelValue === option.props.value }],
-			key: option.key,
-			disabled: this.modelValue === option.props.value,
-			onClick: () => {
-				this.$emit('update:modelValue', option.props.value);
-			}
-		}, option.children), [
-			[resolveDirective('click-anime')]
-		]))), [
-			[resolveDirective('size'), { max: [500] }]
-		]);
-	}
-});
-</script>
-
-<style lang="scss">
-.pxhvhrfw {
-	display: flex;
-	font-size: 90%;
-
-	> button {
-		flex: 1;
-		padding: 10px 8px;
-		border-radius: var(--radius);
-
-		&:disabled {
-			opacity: 1 !important;
-			cursor: default;
-		}
-
-		&.active {
-			color: var(--accent);
-			background: var(--accentedBg);
-		}
-
-		&:not(.active):hover {
-			color: var(--fgHighlighted);
-			background: var(--panelHighlight);
-		}
-
-		&:not(:first-child) {
-			margin-left: 8px;
-		}
-
-		> .icon {
-			margin-right: 6px;
-		}
-	}
-
-	&.max-width_500px {
-		font-size: 80%;
-
-		> button {
-			padding: 11px 8px;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/taskmanager.api-window.vue b/src/client/components/taskmanager.api-window.vue
deleted file mode 100644
index 807e4a0075..0000000000
--- a/src/client/components/taskmanager.api-window.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<template>
-<XWindow ref="window"
-	:initial-width="370"
-	:initial-height="450"
-	:can-resize="true"
-	@close="$refs.window.close()"
-	@closed="$emit('closed')"
->
-	<template #header>Req Viewer</template>
-
-	<div class="rlkneywz">
-		<MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
-			<option value="req">Request</option>
-			<option value="res">Response</option>
-		</MkTab>
-
-		<code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code>
-		<code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code>
-	</div>
-</XWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import XWindow from '@client/components/ui/window.vue';
-import MkTab from '@client/components/tab.vue';
-
-export default defineComponent({
-	components: {
-		XWindow,
-		MkTab,
-	},
-
-	props: {
-		req: {
-			required: true,
-		}
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			tab: 'req',
-			reqStr: JSON5.stringify(this.req.req, null, '\t'),
-			resStr: JSON5.stringify(this.req.res, null, '\t'),
-		}
-	},
-
-	methods: {
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rlkneywz {
-	display: flex;
-	flex-direction: column;
-	height: 100%;
-
-	> code {
-		display: block;
-		flex: 1;
-		padding: 8px;
-		overflow: auto;
-		font-size: 0.9em;
-		tab-size: 2;
-		white-space: pre;
-	}
-}
-</style>
diff --git a/src/client/components/taskmanager.vue b/src/client/components/taskmanager.vue
deleted file mode 100644
index 6f3d1b0354..0000000000
--- a/src/client/components/taskmanager.vue
+++ /dev/null
@@ -1,233 +0,0 @@
-<template>
-<XWindow ref="window" :initial-width="650" :initial-height="420" :can-resize="true" @closed="$emit('closed')">
-	<template #header>
-		<i class="fas fa-terminal" style="margin-right: 0.5em;"></i>Task Manager
-	</template>
-	<div class="qljqmnzj _monospace">
-		<MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
-			<option value="windows">Windows</option>
-			<option value="stream">Stream</option>
-			<option value="streamPool">Stream (Pool)</option>
-			<option value="api">API</option>
-		</MkTab>
-
-		<div class="content">
-			<div v-if="tab === 'windows'" class="windows" v-follow>
-				<div class="header">
-					<div>#ID</div>
-					<div>Component</div>
-					<div>Action</div>
-				</div>
-				<div v-for="p in popups">
-					<div>#{{ p.id }}</div>
-					<div>{{ p.component.name ? p.component.name : '<anonymous>' }}</div>
-					<div><button class="_textButton" @click="killPopup(p)">Kill</button></div>
-				</div>
-			</div>
-			<div v-if="tab === 'stream'" class="stream" v-follow>
-				<div class="header">
-					<div>#ID</div>
-					<div>Ch</div>
-					<div>Handle</div>
-					<div>In</div>
-					<div>Out</div>
-				</div>
-				<div v-for="c in connections">
-					<div>#{{ c.id }}</div>
-					<div>{{ c.channel }}</div>
-					<div v-if="c.users !== null">(shared)<span v-if="c.name">{{ ' ' + c.name }}</span></div>
-					<div v-else>{{ c.name ? c.name : '<anonymous>' }}</div>
-					<div>{{ c.in }}</div>
-					<div>{{ c.out }}</div>
-				</div>
-			</div>
-			<div v-if="tab === 'streamPool'" class="streamPool" v-follow>
-				<div class="header">
-					<div>#ID</div>
-					<div>Ch</div>
-					<div>Users</div>
-				</div>
-				<div v-for="p in pools">
-					<div>#{{ p.id }}</div>
-					<div>{{ p.channel }}</div>
-					<div>{{ p.users }}</div>
-				</div>
-			</div>
-			<div v-if="tab === 'api'" class="api" v-follow>
-				<div class="header">
-					<div>#ID</div>
-					<div>Endpoint</div>
-					<div>State</div>
-				</div>
-				<div v-for="req in apiRequests" @click="showReq(req)">
-					<div>#{{ req.id }}</div>
-					<div>{{ req.endpoint }}</div>
-					<div class="state" :class="req.state">{{ req.state }}</div>
-				</div>
-			</div>
-		</div>
-
-		<footer>
-			<div><span class="label">Windows</span>{{ popups.length }}</div>
-			<div><span class="label">Stream</span>{{ connections.length }}</div>
-			<div><span class="label">Stream (Pool)</span>{{ pools.length }}</div>
-		</footer>
-	</div>
-</XWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw, onBeforeUnmount, ref, shallowRef } from 'vue';
-import XWindow from '@client/components/ui/window.vue';
-import MkTab from '@client/components/tab.vue';
-import MkButton from '@client/components/ui/button.vue';
-import follow from '@client/directives/follow-append';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XWindow,
-		MkTab,
-		MkButton,
-	},
-
-	directives: {
-		follow
-	},
-
-	props: {
-	},
-
-	emits: ['closed'],
-
-	setup() {
-		const connections = shallowRef([]);
-		const pools = shallowRef([]);
-		const refreshStreamInfo = () => {
-			console.log(os.stream.sharedConnectionPools, os.stream.sharedConnections, os.stream.nonSharedConnections);
-			const conn = os.stream.sharedConnections.map(c => ({
-				id: c.id, name: c.name, channel: c.channel, users: c.pool.users, in: c.inCount, out: c.outCount,
-			})).concat(os.stream.nonSharedConnections.map(c => ({
-				id: c.id, name: c.name, channel: c.channel, users: null, in: c.inCount, out: c.outCount,
-			})));
-			conn.sort((a, b) => (a.id > b.id) ? 1 : -1);
-			connections.value = conn;
-			pools.value = os.stream.sharedConnectionPools;
-		};
-		const interval = setInterval(refreshStreamInfo, 1000);
-		onBeforeUnmount(() => {
-			clearInterval(interval);
-		});
-
-		const killPopup = p => {
-			os.popups.value = os.popups.value.filter(x => x !== p);
-		};
-
-		const showReq = req => {
-			os.popup(import('./taskmanager.api-window.vue'), {
-				req: req
-			}, {
-			}, 'closed');
-		};
-
-		return {
-			tab: ref('stream'),
-			popups: os.popups,
-			apiRequests: os.apiRequests,
-			connections,
-			pools,
-			killPopup,
-			showReq,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.qljqmnzj {
-	display: flex;
-	flex-direction: column;
-	height: 100%;
-
-	> .content {
-		flex: 1;
-		overflow: auto;
-
-		> div {
-			display: table;
-			width: 100%;
-			padding: 16px;
-			box-sizing: border-box;
-
-			> div {
-				display: table-row;
-
-				&:nth-child(even) {
-					//background: rgba(0, 0, 0, 0.1);
-				}
-
-				&.header {
-					opacity: 0.7;
-				}
-
-				> div {
-					display: table-cell;
-					white-space: nowrap;
-
-					&:not(:last-child) {
-						padding-right: 8px;
-					}
-				}
-			}
-
-			&.api {
-				> div {
-					&:not(.header) {
-						cursor: pointer;
-
-						&:hover {
-							color: var(--accent);
-						}
-					}
-
-					> .state {
-						&.pending {
-							color: var(--warn);
-						}
-
-						&.success {
-							color: var(--success);
-						}
-
-						&.failed {
-							color: var(--error);
-						}
-					}
-				}
-			}
-		}
-	}
-
-	> footer {
-		display: flex;
-		width: 100%;
-		padding: 8px 16px;
-		box-sizing: border-box;
-		border-top: solid 0.5px var(--divider);
-		font-size: 0.9em;
-
-		> div {
-			flex: 1;
-
-			> .label {
-				opacity: 0.7;
-				margin-right: 0.5em;
-
-				&:after {
-					content: ":";
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
deleted file mode 100644
index 9676616f2a..0000000000
--- a/src/client/components/timeline.vue
+++ /dev/null
@@ -1,183 +0,0 @@
-<template>
-<XNotes :no-gap="!$store.state.showGapBetweenNotesInTimeline" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import XNotes from './notes.vue';
-import * as os from '@client/os';
-import * as sound from '@client/scripts/sound';
-
-export default defineComponent({
-	components: {
-		XNotes
-	},
-
-	provide() {
-		return {
-			inChannel: this.src === 'channel'
-		};
-	},
-
-	props: {
-		src: {
-			type: String,
-			required: true
-		},
-		list: {
-			type: String,
-			required: false
-		},
-		antenna: {
-			type: String,
-			required: false
-		},
-		channel: {
-			type: String,
-			required: false
-		},
-		sound: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['note', 'queue', 'before', 'after'],
-
-	data() {
-		return {
-			connection: null,
-			connection2: null,
-			pagination: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.showLocalRenotes
-			},
-			query: {},
-			date: null
-		};
-	},
-
-	created() {
-		const prepend = note => {
-			(this.$refs.tl as any).prepend(note);
-
-			this.$emit('note');
-
-			if (this.sound) {
-				sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
-			}
-		};
-
-		const onUserAdded = () => {
-			(this.$refs.tl as any).reload();
-		};
-
-		const onUserRemoved = () => {
-			(this.$refs.tl as any).reload();
-		};
-
-		const onChangeFollowing = () => {
-			if (!this.$refs.tl.backed) {
-				this.$refs.tl.reload();
-			}
-		};
-
-		let endpoint;
-
-		if (this.src == 'antenna') {
-			endpoint = 'antennas/notes';
-			this.query = {
-				antennaId: this.antenna
-			};
-			this.connection = markRaw(os.stream.useChannel('antenna', {
-				antennaId: this.antenna
-			}));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'home') {
-			endpoint = 'notes/timeline';
-			this.connection = markRaw(os.stream.useChannel('homeTimeline'));
-			this.connection.on('note', prepend);
-
-			this.connection2 = markRaw(os.stream.useChannel('main'));
-			this.connection2.on('follow', onChangeFollowing);
-			this.connection2.on('unfollow', onChangeFollowing);
-		} else if (this.src == 'local') {
-			endpoint = 'notes/local-timeline';
-			this.connection = markRaw(os.stream.useChannel('localTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'social') {
-			endpoint = 'notes/hybrid-timeline';
-			this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'global') {
-			endpoint = 'notes/global-timeline';
-			this.connection = markRaw(os.stream.useChannel('globalTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'mentions') {
-			endpoint = 'notes/mentions';
-			this.connection = markRaw(os.stream.useChannel('main'));
-			this.connection.on('mention', prepend);
-		} else if (this.src == 'directs') {
-			endpoint = 'notes/mentions';
-			this.query = {
-				visibility: 'specified'
-			};
-			const onNote = note => {
-				if (note.visibility == 'specified') {
-					prepend(note);
-				}
-			};
-			this.connection = markRaw(os.stream.useChannel('main'));
-			this.connection.on('mention', onNote);
-		} else if (this.src == 'list') {
-			endpoint = 'notes/user-list-timeline';
-			this.query = {
-				listId: this.list
-			};
-			this.connection = markRaw(os.stream.useChannel('userList', {
-				listId: this.list
-			}));
-			this.connection.on('note', prepend);
-			this.connection.on('userAdded', onUserAdded);
-			this.connection.on('userRemoved', onUserRemoved);
-		} else if (this.src == 'channel') {
-			endpoint = 'channels/timeline';
-			this.query = {
-				channelId: this.channel
-			};
-			this.connection = markRaw(os.stream.useChannel('channel', {
-				channelId: this.channel
-			}));
-			this.connection.on('note', prepend);
-		}
-
-		this.pagination = {
-			endpoint: endpoint,
-			limit: 10,
-			params: init => ({
-				untilDate: this.date?.getTime(),
-				...this.baseQuery, ...this.query
-			})
-		};
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-		if (this.connection2) this.connection2.dispose();
-	},
-
-	methods: {
-		focus() {
-			this.$refs.tl.focus();
-		},
-
-		timetravel(date?: Date) {
-			this.date = date;
-			this.$refs.tl.reload();
-		}
-	}
-});
-</script>
diff --git a/src/client/components/toast.vue b/src/client/components/toast.vue
deleted file mode 100644
index fb0de68092..0000000000
--- a/src/client/components/toast.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-<div class="mk-toast">
-	<transition name="notification-slide" appear @after-leave="$emit('closed')">
-		<XNotification :notification="notification" class="notification _acrylic" v-if="showing"/>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNotification from './notification.vue';
-
-export default defineComponent({
-	components: {
-		XNotification
-	},
-	props: {
-		notification: {
-			type: Object,
-			required: true
-		}
-	},
-	emits: ['closed'],
-	data() {
-		return {
-			showing: true
-		};
-	},
-	mounted() {
-		setTimeout(() => {
-			this.showing = false;
-		}, 6000);
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.notification-slide-enter-active, .notification-slide-leave-active {
-	transition: opacity 0.3s, transform 0.3s !important;
-}
-.notification-slide-enter-from, .notification-slide-leave-to {
-	opacity: 0;
-	transform: translateX(-250px);
-}
-
-.mk-toast {
-	position: fixed;
-	z-index: 10000;
-	left: 0;
-	width: 250px;
-	top: 32px;
-	padding: 0 32px;
-	pointer-events: none;
-
-	@media (max-width: 700px) {
-		top: initial;
-		bottom: 112px;
-		padding: 0 16px;
-	}
-
-	@media (max-width: 500px) {
-		bottom: 92px;
-		padding: 0 8px;
-	}
-
-	> .notification {
-		height: 100%;
-		box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
-		border-radius: 8px;
-		overflow: hidden;
-	}
-}
-</style>
diff --git a/src/client/components/token-generate-window.vue b/src/client/components/token-generate-window.vue
deleted file mode 100644
index 86312564cc..0000000000
--- a/src/client/components/token-generate-window.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="400"
-	:height="450"
-	:with-ok-button="true"
-	:ok-button-disabled="false"
-	:can-close="false"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
-	@ok="ok()"
->
-	<template #header>{{ title || $ts.generateAccessToken }}</template>
-	<div v-if="information" class="_section">
-		<MkInfo warn>{{ information }}</MkInfo>
-	</div>
-	<div class="_section">
-		<MkInput v-model="name">
-			<template #label>{{ $ts.name }}</template>
-		</MkInput>
-	</div>
-	<div class="_section">
-		<div style="margin-bottom: 16px;"><b>{{ $ts.permission }}</b></div>
-		<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
-		<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
-		<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { kinds } from '@/misc/api-permissions';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkInput from './form/input.vue';
-import MkTextarea from './form/textarea.vue';
-import MkSwitch from './form/switch.vue';
-import MkButton from './ui/button.vue';
-import MkInfo from './ui/info.vue';
-
-export default defineComponent({
-	components: {
-		XModalWindow,
-		MkInput,
-		MkTextarea,
-		MkSwitch,
-		MkButton,
-		MkInfo,
-	},
-
-	props: {
-		title: {
-			type: String,
-			required: false,
-			default: null
-		},
-		information: {
-			type: String,
-			required: false,
-			default: null
-		},
-		initialName: {
-			type: String,
-			required: false,
-			default: null
-		},
-		initialPermissions: {
-			type: Array,
-			required: false,
-			default: null
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			name: this.initialName,
-			permissions: {},
-			kinds
-		};
-	},
-
-	created() {
-		if (this.initialPermissions) {
-			for (const kind of this.initialPermissions) {
-				this.permissions[kind] = true;
-			}
-		} else {
-			for (const kind of this.kinds) {
-				this.permissions[kind] = false;
-			}
-		}
-	},
-
-	methods: {
-		ok() {
-			this.$emit('done', {
-				name: this.name,
-				permissions: Object.keys(this.permissions).filter(p => this.permissions[p])
-			});
-			this.$refs.dialog.close();
-		},
-
-		disableAll() {
-			for (const p in this.permissions) {
-				this.permissions[p] = false;
-			}
-		},
-
-		enableAll() {
-			for (const p in this.permissions) {
-				this.permissions[p] = true;
-			}
-		}
-	}
-});
-</script>
diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue
deleted file mode 100644
index b5f4547c84..0000000000
--- a/src/client/components/ui/button.vue
+++ /dev/null
@@ -1,262 +0,0 @@
-<template>
-<button v-if="!link" class="bghgjjyj _button"
-	:class="{ inline, primary, gradate, danger, rounded, full }"
-	:type="type"
-	@click="$emit('click', $event)"
-	@mousedown="onMousedown"
->
-	<div ref="ripples" class="ripples"></div>
-	<div class="content">
-		<slot></slot>
-	</div>
-</button>
-<MkA v-else class="bghgjjyj _button"
-	:class="{ inline, primary, gradate, danger, rounded, full }"
-	:to="to"
-	@mousedown="onMousedown"
->
-	<div ref="ripples" class="ripples"></div>
-	<div class="content">
-		<slot></slot>
-	</div>
-</MkA>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		type: {
-			type: String,
-			required: false
-		},
-		primary: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		gradate: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		rounded: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		link: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		to: {
-			type: String,
-			required: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		wait: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		danger: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		full: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	emits: ['click'],
-	mounted() {
-		if (this.autofocus) {
-			this.$nextTick(() => {
-				this.$el.focus();
-			});
-		}
-	},
-	methods: {
-		onMousedown(e: MouseEvent) {
-			function distance(p, q) {
-				return Math.hypot(p.x - q.x, p.y - q.y);
-			}
-
-			function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) {
-				const origin = {x: circleCenterX, y: circleCenterY};
-				const dist1 = distance({x: 0, y: 0}, origin);
-				const dist2 = distance({x: boxW, y: 0}, origin);
-				const dist3 = distance({x: 0, y: boxH}, origin);
-				const dist4 = distance({x: boxW, y: boxH }, origin);
-				return Math.max(dist1, dist2, dist3, dist4) * 2;
-			}
-
-			const rect = e.target.getBoundingClientRect();
-
-			const ripple = document.createElement('div');
-			ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px';
-			ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px';
-
-			this.$refs.ripples.appendChild(ripple);
-
-			const circleCenterX = e.clientX - rect.left;
-			const circleCenterY = e.clientY - rect.top;
-
-			const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
-
-			setTimeout(() => {
-				ripple.style.transform = 'scale(' + (scale / 2) + ')';
-			}, 1);
-			setTimeout(() => {
-				ripple.style.transition = 'all 1s ease';
-				ripple.style.opacity = '0';
-			}, 1000);
-			setTimeout(() => {
-				if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
-			}, 2000);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.bghgjjyj {
-	position: relative;
-	z-index: 1; // 他コンポーネントのbox-shadowに隠されないようにするため
-	display: block;
-	min-width: 100px;
-	width: max-content;
-	padding: 8px 14px;
-	text-align: center;
-	font-weight: normal;
-	font-size: 0.8em;
-	line-height: 22px;
-	box-shadow: none;
-	text-decoration: none;
-	background: var(--buttonBg);
-	border-radius: 4px;
-	overflow: clip;
-	box-sizing: border-box;
-	transition: background 0.1s ease;
-
-	&:not(:disabled):hover {
-		background: var(--buttonHoverBg);
-	}
-
-	&:not(:disabled):active {
-		background: var(--buttonHoverBg);
-	}
-
-	&.full {
-		width: 100%;
-	}
-
-	&.rounded {
-		border-radius: 999px;
-	}
-
-	&.primary {
-		font-weight: bold;
-		color: var(--fgOnAccent) !important;
-		background: var(--accent);
-
-		&:not(:disabled):hover {
-			background: var(--X8);
-		}
-
-		&:not(:disabled):active {
-			background: var(--X8);
-		}
-	}
-
-	&.gradate {
-		font-weight: bold;
-		color: var(--fgOnAccent) !important;
-		background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-
-		&:not(:disabled):hover {
-			background: linear-gradient(90deg, var(--X8), var(--X8));
-		}
-
-		&:not(:disabled):active {
-			background: linear-gradient(90deg, var(--X8), var(--X8));
-		}
-	}
-
-	&.danger {
-		color: #ff2a2a;
-
-		&.primary {
-			color: #fff;
-			background: #ff2a2a;
-
-			&:not(:disabled):hover {
-				background: #ff4242;
-			}
-
-			&:not(:disabled):active {
-				background: #d42e2e;
-			}
-		}
-	}
-
-	&:disabled {
-		opacity: 0.7;
-	}
-
-	&:focus-visible {
-		outline: solid 2px var(--focus);
-		outline-offset: 2px;
-	}
-
-	&.inline {
-		display: inline-block;
-		width: auto;
-		min-width: 100px;
-	}
-
-	> .ripples {
-		position: absolute;
-		z-index: 0;
-		top: 0;
-		left: 0;
-		width: 100%;
-		height: 100%;
-		border-radius: 6px;
-		overflow: hidden;
-
-		::v-deep(div) {
-			position: absolute;
-			width: 2px;
-			height: 2px;
-			border-radius: 100%;
-			background: rgba(0, 0, 0, 0.1);
-			opacity: 1;
-			transform: scale(1);
-			transition: all 0.5s cubic-bezier(0,.5,0,1);
-		}
-	}
-
-	&.primary > .ripples ::v-deep(div) {
-		background: rgba(0, 0, 0, 0.15);
-	}
-
-	> .content {
-		position: relative;
-		z-index: 1;
-	}
-}
-</style>
diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue
deleted file mode 100644
index 14673dfcd7..0000000000
--- a/src/client/components/ui/container.vue
+++ /dev/null
@@ -1,262 +0,0 @@
-<template>
-<div class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }">
-	<header v-if="showHeader" ref="header">
-		<div class="title"><slot name="header"></slot></div>
-		<div class="sub">
-			<slot name="func"></slot>
-			<button class="_button" v-if="foldable" @click="() => showBody = !showBody">
-				<template v-if="showBody"><i class="fas fa-angle-up"></i></template>
-				<template v-else><i class="fas fa-angle-down"></i></template>
-			</button>
-		</div>
-	</header>
-	<transition name="container-toggle"
-		@enter="enter"
-		@after-enter="afterEnter"
-		@leave="leave"
-		@after-leave="afterLeave"
-	>
-		<div v-show="showBody" class="content" :class="{ omitted }" ref="content">
-			<slot></slot>
-			<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }">
-				<span>{{ $ts.showMore }}</span>
-			</button>
-		</div>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		showHeader: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-		thin: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		naked: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		foldable: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		expanded: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-		scrollable: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		maxHeight: {
-			type: Number,
-			required: false,
-			default: null
-		},
-	},
-	data() {
-		return {
-			showBody: this.expanded,
-			omitted: null,
-			ignoreOmit: false,
-		};
-	},
-	mounted() {
-		this.$watch('showBody', showBody => {
-			const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0;
-			this.$el.style.minHeight = `${headerHeight}px`;
-			if (showBody) {
-				this.$el.style.flexBasis = `auto`;
-			} else {
-				this.$el.style.flexBasis = `${headerHeight}px`;
-			}
-		}, {
-			immediate: true
-		});
-
-		this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px');
-
-		const calcOmit = () => {
-			if (this.omitted || this.ignoreOmit || this.maxHeight == null) return;
-			const height = this.$refs.content.offsetHeight;
-			this.omitted = height > this.maxHeight;
-		};
-
-		calcOmit();
-		new ResizeObserver((entries, observer) => {
-			calcOmit();
-		}).observe(this.$refs.content);
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			if (!this.foldable) return;
-			this.showBody = show;
-		},
-
-		enter(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = 0;
-			el.offsetHeight; // reflow
-			el.style.height = elementHeight + 'px';
-		},
-		afterEnter(el) {
-			el.style.height = null;
-		},
-		leave(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = elementHeight + 'px';
-			el.offsetHeight; // reflow
-			el.style.height = 0;
-		},
-		afterLeave(el) {
-			el.style.height = null;
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.container-toggle-enter-active, .container-toggle-leave-active {
-	overflow-y: hidden;
-	transition: opacity 0.5s, height 0.5s !important;
-}
-.container-toggle-enter-from {
-	opacity: 0;
-}
-.container-toggle-leave-to {
-	opacity: 0;
-}
-
-.ukygtjoj {
-	position: relative;
-	overflow: clip;
-
-	&.naked {
-		background: transparent !important;
-		box-shadow: none !important;
-	}
-
-	&.scrollable {
-		display: flex;
-		flex-direction: column;
-
-		> .content {
-			overflow: auto;
-		}
-	}
-
-	> header {
-		position: sticky;
-		top: var(--stickyTop, 0px);
-		left: 0;
-		color: var(--panelHeaderFg);
-		background: var(--panelHeaderBg);
-		border-bottom: solid 0.5px var(--panelHeaderDivider);
-		z-index: 2;
-		line-height: 1.4em;
-
-		> .title {
-			margin: 0;
-			padding: 12px 16px;
-
-			> ::v-deep(i) {
-				margin-right: 6px;
-			}
-
-			&:empty {
-				display: none;
-			}
-		}
-
-		> .sub {
-			position: absolute;
-			z-index: 2;
-			top: 0;
-			right: 0;
-			height: 100%;
-
-			> ::v-deep(button) {
-				width: 42px;
-				height: 100%;
-			}
-		}
-	}
-
-	> .content {
-		--stickyTop: 0px;
-
-		&.omitted {
-			position: relative;
-			max-height: var(--maxHeight);
-			overflow: hidden;
-
-			> .fade {
-				display: block;
-				position: absolute;
-				z-index: 10;
-				bottom: 0;
-				left: 0;
-				width: 100%;
-				height: 64px;
-				background: linear-gradient(0deg, var(--panel), var(--X15));
-
-				> span {
-					display: inline-block;
-					background: var(--panel);
-					padding: 6px 10px;
-					font-size: 0.8em;
-					border-radius: 999px;
-					box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
-				}
-
-				&:hover {
-					> span {
-						background: var(--panelHighlight);
-					}
-				}
-			}
-		}
-	}
-
-	&.max-width_380px, &.thin {
-		> header {
-			> .title {
-				padding: 8px 10px;
-				font-size: 0.9em;
-			}
-		}
-
-		> .content {
-		}
-	}
-}
-
-._forceContainerFull_ .ukygtjoj {
-	> header {
-		> .title {
-			padding: 12px 16px !important;
-		}
-	}
-}
-
-._forceContainerFull_.ukygtjoj {
-	> header {
-		> .title {
-			padding: 12px 16px !important;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/ui/context-menu.vue b/src/client/components/ui/context-menu.vue
deleted file mode 100644
index 61f5d3bf08..0000000000
--- a/src/client/components/ui/context-menu.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-<template>
-<transition :name="$store.state.animation ? 'fade' : ''" appear>
-	<div class="nvlagfpb" @contextmenu.prevent.stop="() => {}">
-		<MkMenu :items="items" @close="$emit('closed')" class="_popup _shadow" :align="'left'"/>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import contains from '@client/scripts/contains';
-import MkMenu from './menu.vue';
-
-export default defineComponent({
-	components: {
-		MkMenu,
-	},
-	props: {
-		items: {
-			type: Array,
-			required: true
-		},
-		ev: {
-			required: true
-		},
-		viaKeyboard: {
-			type: Boolean,
-			required: false
-		},
-	},
-	emits: ['closed'],
-	computed: {
-		keymap(): any {
-			return {
-				'esc': () => this.$emit('closed'),
-			};
-		},
-	},
-	mounted() {
-		let left = this.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
-		let top = this.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
-
-		const width = this.$el.offsetWidth;
-		const height = this.$el.offsetHeight;
-
-		if (left + width - window.pageXOffset > window.innerWidth) {
-			left = window.innerWidth - width + window.pageXOffset;
-		}
-
-		if (top + height - window.pageYOffset > window.innerHeight) {
-			top = window.innerHeight - height + window.pageYOffset;
-		}
-
-		if (top < 0) {
-			top = 0;
-		}
-
-		if (left < 0) {
-			left = 0;
-		}
-
-		this.$el.style.top = top + 'px';
-		this.$el.style.left = left + 'px';
-
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
-			el.addEventListener('mousedown', this.onMousedown);
-		}
-	},
-	beforeUnmount() {
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
-			el.removeEventListener('mousedown', this.onMousedown);
-		}
-	},
-	methods: {
-		onMousedown(e) {
-			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed');
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.nvlagfpb {
-	position: absolute;
-	z-index: 65535;
-}
-
-.fade-enter-active, .fade-leave-active {
-	transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
-	transform-origin: left top;
-}
-
-.fade-enter-from, .fade-leave-to {
-	opacity: 0;
-	transform: scale(0.9);
-}
-</style>
diff --git a/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue
deleted file mode 100644
index 3997421d08..0000000000
--- a/src/client/components/ui/folder.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<template>
-<div class="ssazuxis" v-size="{ max: [500] }">
-	<header @click="showBody = !showBody" class="_button" :style="{ background: bg }">
-		<div class="title"><slot name="header"></slot></div>
-		<div class="divider"></div>
-		<button class="_button">
-			<template v-if="showBody"><i class="fas fa-angle-up"></i></template>
-			<template v-else><i class="fas fa-angle-down"></i></template>
-		</button>
-	</header>
-	<transition name="folder-toggle"
-		@enter="enter"
-		@after-enter="afterEnter"
-		@leave="leave"
-		@after-leave="afterLeave"
-	>
-		<div v-show="showBody">
-			<slot></slot>
-		</div>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as tinycolor from 'tinycolor2';
-
-const localStoragePrefix = 'ui:folder:';
-
-export default defineComponent({
-	props: {
-		expanded: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-		persistKey: {
-			type: String,
-			required: false,
-			default: null
-		},
-	},
-	data() {
-		return {
-			bg: null,
-			showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
-		};
-	},
-	watch: {
-		showBody() {
-			if (this.persistKey) {
-				localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
-			}
-		}
-	},
-	mounted() {
-		function getParentBg(el: Element | null): string {
-			if (el == null || el.tagName === 'BODY') return 'var(--bg)';
-			const bg = el.style.background || el.style.backgroundColor;
-			if (bg) {
-				return bg;
-			} else {
-				return getParentBg(el.parentElement);
-			}
-		}
-		const rawBg = getParentBg(this.$el);
-		const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
-		bg.setAlpha(0.85);
-		this.bg = bg.toRgbString();
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			this.showBody = show;
-		},
-
-		enter(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = 0;
-			el.offsetHeight; // reflow
-			el.style.height = elementHeight + 'px';
-		},
-		afterEnter(el) {
-			el.style.height = null;
-		},
-		leave(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = elementHeight + 'px';
-			el.offsetHeight; // reflow
-			el.style.height = 0;
-		},
-		afterLeave(el) {
-			el.style.height = null;
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.folder-toggle-enter-active, .folder-toggle-leave-active {
-	overflow-y: hidden;
-	transition: opacity 0.5s, height 0.5s !important;
-}
-.folder-toggle-enter-from {
-	opacity: 0;
-}
-.folder-toggle-leave-to {
-	opacity: 0;
-}
-
-.ssazuxis {
-	position: relative;
-
-	> header {
-		display: flex;
-		position: relative;
-		z-index: 10;
-		position: sticky;
-		top: var(--stickyTop, 0px);
-		padding: var(--x-padding);
-		-webkit-backdrop-filter: var(--blur, blur(8px));
-		backdrop-filter: var(--blur, blur(20px));
-
-		> .title {
-			margin: 0;
-			padding: 12px 16px 12px 0;
-
-			> i {
-				margin-right: 6px;
-			}
-
-			&:empty {
-				display: none;
-			}
-		}
-
-		> .divider {
-			flex: 1;
-			margin: auto;
-			height: 1px;
-			background: var(--divider);
-		}
-
-		> button {
-			padding: 12px 0 12px 16px;
-		}
-	}
-
-	&.max-width_500px {
-		> header {
-			> .title {
-				padding: 8px 10px 8px 0;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/ui/hr.vue b/src/client/components/ui/hr.vue
deleted file mode 100644
index fb12b4985f..0000000000
--- a/src/client/components/ui/hr.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-<template>
-<div class="evrzpitu"></div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';import * as os from '@client/os';
-
-export default defineComponent({});
-</script>
-
-<style lang="scss" scoped>
-.evrzpitu
-	margin 16px 0
-	border-bottom solid var(--lineWidth) var(--faceDivider)
-
-</style>
diff --git a/src/client/components/ui/info.vue b/src/client/components/ui/info.vue
deleted file mode 100644
index f6b2edf267..0000000000
--- a/src/client/components/ui/info.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-<div class="fpezltsf" :class="{ warn }">
-	<i v-if="warn" class="fas fa-exclamation-triangle"></i>
-	<i v-else class="fas fa-info-circle"></i>
-	<slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		warn: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	data() {
-		return {
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fpezltsf {
-	padding: 16px;
-	font-size: 90%;
-	background: var(--infoBg);
-	color: var(--infoFg);
-	border-radius: var(--radius);
-
-	&.warn {
-		background: var(--infoWarnBg);
-		color: var(--infoWarnFg);
-	}
-
-	> i {
-		margin-right: 4px;
-	}
-}
-</style>
diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue
deleted file mode 100644
index 5b3a0ae7c2..0000000000
--- a/src/client/components/ui/menu.vue
+++ /dev/null
@@ -1,278 +0,0 @@
-<template>
-<div class="rrevdjwt" :class="{ center: align === 'center' }"
-	:style="{ width: width ? width + 'px' : null }"
-	ref="items"
-	@contextmenu.self="e => e.preventDefault()"
-	v-hotkey="keymap"
->
-	<template v-for="(item, i) in _items">
-		<div v-if="item === null" class="divider"></div>
-		<span v-else-if="item.type === 'label'" class="label item">
-			<span>{{ item.text }}</span>
-		</span>
-		<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
-			<span><MkEllipsis/></span>
-		</span>
-		<MkA v-else-if="item.type === 'link'" :to="item.to" @click.passive="close()" :tabindex="i" class="_button item">
-			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
-			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
-			<span>{{ item.text }}</span>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</MkA>
-		<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item">
-			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
-			<span>{{ item.text }}</span>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</a>
-		<button v-else-if="item.type === 'user'" @click="clicked(item.action, $event)" :tabindex="i" class="_button item">
-			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</button>
-		<button v-else @click="clicked(item.action, $event)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active">
-			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
-			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
-			<span>{{ item.text }}</span>
-			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
-		</button>
-	</template>
-	<span v-if="_items.length === 0" class="none item">
-		<span>{{ $ts.none }}</span>
-	</span>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref, unref } from 'vue';
-import { focusPrev, focusNext } from '@client/scripts/focus';
-import contains from '@client/scripts/contains';
-
-export default defineComponent({
-	props: {
-		items: {
-			type: Array,
-			required: true
-		},
-		viaKeyboard: {
-			type: Boolean,
-			required: false
-		},
-		align: {
-			type: String,
-			requried: false
-		},
-		width: {
-			type: Number,
-			required: false
-		},
-	},
-	emits: ['close'],
-	data() {
-		return {
-			_items: [],
-		};
-	},
-	computed: {
-		keymap(): any {
-			return {
-				'up|k|shift+tab': this.focusUp,
-				'down|j|tab': this.focusDown,
-				'esc': this.close,
-			};
-		},
-	},
-	watch: {
-		items: {
-			handler() {
-				const items = ref(unref(this.items).filter(item => item !== undefined));
-
-				for (let i = 0; i < items.value.length; i++) {
-					const item = items.value[i];
-					
-					if (item && item.then) { // if item is Promise
-						items.value[i] = { type: 'pending' };
-						item.then(actualItem => {
-							items.value[i] = actualItem;
-						});
-					}
-				}
-
-				this._items = items;
-			},
-			immediate: true
-		}
-	},
-	mounted() {
-		if (this.viaKeyboard) {
-			this.$nextTick(() => {
-				focusNext(this.$refs.items.children[0], true, false);
-			});
-		}
-
-		if (this.contextmenuEvent) {
-			this.$el.style.top = this.contextmenuEvent.pageY + 'px';
-			this.$el.style.left = this.contextmenuEvent.pageX + 'px';
-
-			for (const el of Array.from(document.querySelectorAll('body *'))) {
-				el.addEventListener('mousedown', this.onMousedown);
-			}
-		}
-	},
-	beforeUnmount() {
-		for (const el of Array.from(document.querySelectorAll('body *'))) {
-			el.removeEventListener('mousedown', this.onMousedown);
-		}
-	},
-	methods: {
-		clicked(fn, ev) {
-			fn(ev);
-			this.close();
-		},
-		close() {
-			this.$emit('close');
-		},
-		focusUp() {
-			focusPrev(document.activeElement);
-		},
-		focusDown() {
-			focusNext(document.activeElement);
-		},
-		onMousedown(e) {
-			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rrevdjwt {
-	padding: 8px 0;
-	min-width: 200px;
-	max-height: 90vh;
-	overflow: auto;
-
-	&.center {
-		> .item {
-			text-align: center;
-		}
-	}
-
-	> .item {
-		display: block;
-		position: relative;
-		padding: 8px 18px;
-		width: 100%;
-		box-sizing: border-box;
-		white-space: nowrap;
-		font-size: 0.9em;
-		line-height: 20px;
-		text-align: left;
-		overflow: hidden;
-		text-overflow: ellipsis;
-
-		&:before {
-			content: "";
-			display: block;
-			position: absolute;
-			top: 0;
-			left: 0;
-			right: 0;
-			margin: auto;
-			width: calc(100% - 16px);
-			height: 100%;
-			border-radius: 6px;
-		}
-
-		> * {
-			position: relative;
-		}
-
-		&.danger {
-			color: #ff2a2a;
-
-			&:hover {
-				color: #fff;
-
-				&:before {
-					background: #ff4242;
-				}
-			}
-
-			&:active {
-				color: #fff;
-
-				&:before {
-					background: #d42e2e;
-				}
-			}
-		}
-
-		&.active {
-			color: var(--fgOnAccent);
-			opacity: 1;
-
-			&:before {
-				background: var(--accent);
-			}
-		}
-
-		&:not(:disabled):hover {
-			color: var(--accent);
-			text-decoration: none;
-
-			&:before {
-				background: var(--accentedBg);
-			}
-		}
-
-		&:not(:active):focus-visible {
-			box-shadow: 0 0 0 2px var(--focus) inset;
-		}
-
-		&.label {
-			pointer-events: none;
-			font-size: 0.7em;
-			padding-bottom: 4px;
-
-			> span {
-				opacity: 0.7;
-			}
-		}
-
-		&.pending {
-			pointer-events: none;
-			opacity: 0.7;
-		}
-
-		&.none {
-			pointer-events: none;
-			opacity: 0.7;
-		}
-
-		> i {
-			margin-right: 5px;
-			width: 20px;
-		}
-
-		> .avatar {
-			margin-right: 5px;
-			width: 20px;
-			height: 20px;
-		}
-
-		> .indicator {
-			position: absolute;
-			top: 5px;
-			left: 13px;
-			color: var(--indicator);
-			font-size: 12px;
-			animation: blink 1s infinite;
-		}
-	}
-
-	> .divider {
-		margin: 8px 0;
-		height: 1px;
-		background: var(--divider);
-	}
-}
-</style>
diff --git a/src/client/components/ui/modal-window.vue b/src/client/components/ui/modal-window.vue
deleted file mode 100644
index da98192b87..0000000000
--- a/src/client/components/ui/modal-window.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<template>
-<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
-	<div class="ebkgoccj _window _narrow_" @keydown="onKeydown" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) :  (height ? `min(${height}px, 100%)` : '100%') }">
-		<div class="header">
-			<button class="_button" v-if="withOkButton" @click="$emit('close')"><i class="fas fa-times"></i></button>
-			<span class="title">
-				<slot name="header"></slot>
-			</span>
-			<button class="_button" v-if="!withOkButton" @click="$emit('close')"><i class="fas fa-times"></i></button>
-			<button class="_button" v-if="withOkButton" @click="$emit('ok')" :disabled="okButtonDisabled"><i class="fas fa-check"></i></button>
-		</div>
-		<div class="body" v-if="padding">
-			<div class="_section">
-				<slot></slot>
-			</div>
-		</div>
-		<div class="body" v-else>
-			<slot></slot>
-		</div>
-	</div>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkModal from './modal.vue';
-
-export default defineComponent({
-	components: {
-		MkModal
-	},
-	props: {
-		withOkButton: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		okButtonDisabled: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		padding: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		width: {
-			type: Number,
-			required: false,
-			default: 400
-		},
-		height: {
-			type: Number,
-			required: false,
-			default: null
-		},
-		canClose: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		scroll: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-	},
-
-	emits: ['click', 'close', 'closed', 'ok'],
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-		close() {
-			this.$refs.modal.close();
-		},
-
-		onKeydown(e) {
-			if (e.which === 27) { // Esc
-				e.preventDefault();
-				e.stopPropagation();
-				this.close();
-			}
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ebkgoccj {
-	overflow: hidden;
-	display: flex;
-	flex-direction: column;
-	contain: content;
-
-	--root-margin: 24px;
-
-	@media (max-width: 500px) {
-		--root-margin: 16px;
-	}
-
-	> .header {
-		$height: 58px;
-		$height-narrow: 42px;
-		display: flex;
-		flex-shrink: 0;
-		box-shadow: 0px 1px var(--divider);
-
-		> button {
-			height: $height;
-			width: $height;
-
-			@media (max-width: 500px) {
-				height: $height-narrow;
-				width: $height-narrow;
-			}
-		}
-
-		> .title {
-			flex: 1;
-			line-height: $height;
-			padding-left: 32px;
-			font-weight: bold;
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-			pointer-events: none;
-
-			@media (max-width: 500px) {
-				line-height: $height-narrow;
-				padding-left: 16px;
-			}
-		}
-
-		> button + .title {
-			padding-left: 0;
-		}
-	}
-
-	> .body {
-		overflow: auto;
-	}
-}
-</style>
diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue
deleted file mode 100644
index 33fcdb687f..0000000000
--- a/src/client/components/ui/modal.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-<template>
-<transition :name="$store.state.animation ? popup ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? popup ? 500 : 300 : 0" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered">
-	<div v-show="manualShowing != null ? manualShowing : showing" class="qzhlnise" :class="{ front }" v-hotkey.global="keymap" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
-		<div class="bg _modalBg" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
-		<div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content">
-			<slot></slot>
-		</div>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-function getFixedContainer(el: Element | null): Element | null {
-	if (el == null || el.tagName === 'BODY') return null;
-	const position = window.getComputedStyle(el).getPropertyValue('position');
-	if (position === 'fixed') {
-		return el;
-	} else {
-		return getFixedContainer(el.parentElement);
-	}
-}
-
-export default defineComponent({
-	provide: {
-		modal: true
-	},
-	props: {
-		manualShowing: {
-			type: Boolean,
-			required: false,
-			default: null,
-		},
-		srcCenter: {
-			type: Boolean,
-			required: false
-		},
-		src: {
-			required: false,
-		},
-		position: {
-			required: false
-		},
-		front: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-	emits: ['opening', 'click', 'esc', 'close', 'closed'],
-	data() {
-		return {
-			showing: true,
-			fixed: false,
-			transformOrigin: 'center',
-			contentClicking: false,
-		};
-	},
-	computed: {
-		keymap(): any {
-			return {
-				'esc': () => this.$emit('esc'),
-			};
-		},
-		popup(): boolean {
-			return this.src != null;
-		}
-	},
-	mounted() {
-		this.$watch('src', () => {
-			this.fixed = getFixedContainer(this.src) != null;
-			this.$nextTick(() => {
-				this.align();
-			});
-		}, { immediate: true });
-
-		this.$nextTick(() => {
-			const popover = this.$refs.content as any;
-			new ResizeObserver((entries, observer) => {
-				this.align();
-			}).observe(popover);
-		});
-	},
-	methods: {
-		align() {
-			if (!this.popup) return;
-
-			const popover = this.$refs.content as any;
-
-			if (popover == null) return;
-
-			const rect = this.src.getBoundingClientRect();
-			
-			const width = popover.offsetWidth;
-			const height = popover.offsetHeight;
-
-			let left;
-			let top;
-
-			if (this.srcCenter) {
-				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
-				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2);
-				left = (x - (width / 2));
-				top = (y - (height / 2));
-			} else {
-				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
-				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight;
-				left = (x - (width / 2));
-				top = y;
-			}
-
-			if (this.fixed) {
-				if (left + width > window.innerWidth) {
-					left = window.innerWidth - width;
-				}
-
-				if (top + height > window.innerHeight) {
-					top = window.innerHeight - height;
-				}
-			} else {
-				if (left + width - window.pageXOffset > window.innerWidth) {
-					left = window.innerWidth - width + window.pageXOffset - 1;
-				}
-
-				if (top + height - window.pageYOffset > window.innerHeight) {
-					top = window.innerHeight - height + window.pageYOffset - 1;
-				}
-			}
-
-			if (top < 0) {
-				top = 0;
-			}
-
-			if (left < 0) {
-				left = 0;
-			}
-
-			if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) {
-				this.transformOrigin = 'center top';
-			} else {
-				this.transformOrigin = 'center';
-			}
-
-			popover.style.left = left + 'px';
-			popover.style.top = top + 'px';
-		},
-
-		childRendered() {
-			// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
-			const content = this.$refs.content.children[0];
-			content.addEventListener('mousedown', e => {
-				this.contentClicking = true;
-				window.addEventListener('mouseup', e => {
-					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
-					setTimeout(() => {
-						this.contentClicking = false;
-					}, 100);
-				}, { passive: true, once: true });
-			}, { passive: true });
-		},
-
-		close() {
-			this.showing = false;
-			this.$emit('close');
-		},
-
-		onBgClick() {
-			if (this.contentClicking) return;
-			this.$emit('click');
-		},
-
-		onClosed() {
-			this.$emit('closed');
-		}
-	}
-});
-</script>
-
-<style lang="scss">
-.modal-popup-enter-active, .modal-popup-leave-active,
-.modal-enter-from, .modal-leave-to {
-	> .content {
-		transform-origin: var(--transformOrigin);
-	}
-}
-</style>
-
-<style lang="scss" scoped>
-.modal-enter-active, .modal-leave-active {
-	> .bg {
-		transition: opacity 0.3s !important;
-	}
-
-	> .content {
-		transition: opacity 0.3s, transform 0.3s !important;
-	}
-}
-.modal-enter-from, .modal-leave-to {
-	> .bg {
-		opacity: 0;
-	}
-
-	> .content {
-		pointer-events: none;
-		opacity: 0;
-		transform: scale(0.9);
-	}
-}
-
-.modal-popup-enter-active, .modal-popup-leave-active {
-	> .bg {
-		transition: opacity 0.3s !important;
-	}
-
-	> .content {
-		transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1) !important;
-	}
-}
-.modal-popup-enter-from, .modal-popup-leave-to {
-	> .bg {
-		opacity: 0;
-	}
-
-	> .content {
-		pointer-events: none;
-		opacity: 0;
-		transform: scale(0.9);
-	}
-}
-
-.qzhlnise {
-	> .bg {
-		z-index: 10000;
-	}
-
-	> .content:not(.popup) {
-		position: fixed;
-		z-index: 10000;
-		top: 0;
-		bottom: 0;
-		left: 0;
-		right: 0;
-		margin: auto;
-		padding: 32px;
-		// TODO: mask-imageはiOSだとやたら重い。なんとかしたい
-		-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
-		mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
-		overflow: auto;
-		display: flex;
-
-		@media (max-width: 500px) {
-			padding: 16px;
-			-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
-			mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
-		}
-
-		> ::v-deep(*) {
-			margin: auto;
-		}
-
-		&.top {
-			> ::v-deep(*) {
-				margin-top: 0;
-			}
-		}
-	}
-
-	> .content.popup {
-		position: absolute;
-		z-index: 10000;
-
-		&.fixed {
-			position: fixed;
-		}
-	}
-
-	&.front {
-		> .bg {
-			z-index: 20000;
-		}
-
-		> .content:not(.popup) {
-			z-index: 20000;
-		}
-
-		> .content.popup {
-			z-index: 20000;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/ui/pagination.vue b/src/client/components/ui/pagination.vue
deleted file mode 100644
index 1bd77447b7..0000000000
--- a/src/client/components/ui/pagination.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<template>
-<transition name="fade" mode="out-in">
-	<MkLoading v-if="fetching"/>
-
-	<MkError v-else-if="error" @retry="init()"/>
-
-	<div class="empty" v-else-if="empty" key="_empty_">
-		<slot name="empty"></slot>
-	</div>
-
-	<div v-else class="cxiknjgy">
-		<slot :items="items"></slot>
-		<div class="more _gap" v-show="more" key="_more_">
-			<MkButton class="button" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
-				<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-				<template v-if="moreFetching"><MkLoading inline/></template>
-			</MkButton>
-		</div>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from './button.vue';
-import paging from '@client/scripts/paging';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	mixins: [
-		paging({}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-
-		disableAutoLoad: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.cxiknjgy {
-	> .more > .button {
-		margin-left: auto;
-		margin-right: auto;
-		height: 48px;
-		min-width: 150px;
-	}
-}
-</style>
diff --git a/src/client/components/ui/popup-menu.vue b/src/client/components/ui/popup-menu.vue
deleted file mode 100644
index 3ff4c658b1..0000000000
--- a/src/client/components/ui/popup-menu.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
-	<MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/>
-</MkPopup>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPopup from './popup.vue';
-import MkMenu from './menu.vue';
-
-export default defineComponent({
-	components: {
-		MkPopup,
-		MkMenu,
-	},
-
-	props: {
-		items: {
-			type: Array,
-			required: true
-		},
-		align: {
-			type: String,
-			required: false
-		},
-		width: {
-			type: Number,
-			required: false
-		},
-		viaKeyboard: {
-			type: Boolean,
-			required: false
-		},
-		src: {
-			required: false
-		},
-	},
-
-	emits: ['close', 'closed'],
-});
-</script>
diff --git a/src/client/components/ui/popup.vue b/src/client/components/ui/popup.vue
deleted file mode 100644
index 0fb1780cc5..0000000000
--- a/src/client/components/ui/popup.vue
+++ /dev/null
@@ -1,213 +0,0 @@
-<template>
-<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered">
-	<div v-show="manualShowing != null ? manualShowing : showing" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" ref="content" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
-		<slot></slot>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-
-function getFixedContainer(el: Element | null): Element | null {
-	if (el == null || el.tagName === 'BODY') return null;
-	const position = window.getComputedStyle(el).getPropertyValue('position');
-	if (position === 'fixed') {
-		return el;
-	} else {
-		return getFixedContainer(el.parentElement);
-	}
-}
-
-export default defineComponent({
-	props: {
-		manualShowing: {
-			type: Boolean,
-			required: false,
-			default: null,
-		},
-		srcCenter: {
-			type: Boolean,
-			required: false
-		},
-		src: {
-			type: Object as PropType<HTMLElement>,
-			required: false,
-		},
-		position: {
-			required: false
-		},
-		front: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	emits: ['opening', 'click', 'esc', 'close', 'closed'],
-
-	data() {
-		return {
-			showing: true,
-			fixed: false,
-			transformOrigin: 'center',
-			contentClicking: false,
-		};
-	},
-
-	mounted() {
-		this.$watch('src', () => {
-			if (this.src) {
-				this.src.style.pointerEvents = 'none';
-			}
-			this.fixed = getFixedContainer(this.src) != null;
-			this.$nextTick(() => {
-				this.align();
-			});
-		}, { immediate: true });
-
-		this.$nextTick(() => {
-			const popover = this.$refs.content as any;
-			new ResizeObserver((entries, observer) => {
-				this.align();
-			}).observe(popover);
-		});
-
-		document.addEventListener('mousedown', this.onDocumentClick, { passive: true });
-	},
-
-	beforeUnmount() {
-		document.removeEventListener('mousedown', this.onDocumentClick);
-	},
-
-	methods: {
-		align() {
-			if (this.src == null) return;
-
-			const popover = this.$refs.content as any;
-
-			if (popover == null) return;
-
-			const rect = this.src.getBoundingClientRect();
-			
-			const width = popover.offsetWidth;
-			const height = popover.offsetHeight;
-
-			let left;
-			let top;
-
-			if (this.srcCenter) {
-				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
-				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2);
-				left = (x - (width / 2));
-				top = (y - (height / 2));
-			} else {
-				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2);
-				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight;
-				left = (x - (width / 2));
-				top = y;
-			}
-
-			if (this.fixed) {
-				if (left + width > window.innerWidth) {
-					left = window.innerWidth - width;
-				}
-
-				if (top + height > window.innerHeight) {
-					top = window.innerHeight - height;
-				}
-			} else {
-				if (left + width - window.pageXOffset > window.innerWidth) {
-					left = window.innerWidth - width + window.pageXOffset - 1;
-				}
-
-				if (top + height - window.pageYOffset > window.innerHeight) {
-					top = window.innerHeight - height + window.pageYOffset - 1;
-				}
-			}
-
-			if (top < 0) {
-				top = 0;
-			}
-
-			if (left < 0) {
-				left = 0;
-			}
-
-			if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) {
-				this.transformOrigin = 'center top';
-			} else {
-				this.transformOrigin = 'center';
-			}
-
-			popover.style.left = left + 'px';
-			popover.style.top = top + 'px';
-		},
-
-		childRendered() {
-			// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
-			const content = this.$refs.content.children[0];
-			content.addEventListener('mousedown', e => {
-				this.contentClicking = true;
-				window.addEventListener('mouseup', e => {
-					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
-					setTimeout(() => {
-						this.contentClicking = false;
-					}, 100);
-				}, { passive: true, once: true });
-			}, { passive: true });
-		},
-
-		close() {
-			if (this.src) this.src.style.pointerEvents = 'auto';
-			this.showing = false;
-			this.$emit('close');
-		},
-
-		onClosed() {
-			this.$emit('closed');
-		},
-
-		onDocumentClick(ev) {
-			const flyoutElement = this.$refs.content;
-			let targetElement = ev.target;
-			do {
-				if (targetElement === flyoutElement) {
-					return;
-				}
-				targetElement = targetElement.parentNode;
-			} while (targetElement);
-			this.close();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.popup-menu-enter-active {
-	transform-origin: var(--transformOrigin);
-	transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
-}
-.popup-menu-leave-active {
-	transform-origin: var(--transformOrigin);
-	transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1), transform 0.2s cubic-bezier(0.4, 0, 1, 1) !important;
-}
-.popup-menu-enter-from, .popup-menu-leave-to {
-	pointer-events: none;
-	opacity: 0;
-	transform: scale(0.9);
-}
-
-.ccczpooj {
-	position: absolute;
-	z-index: 10000;
-
-	&.fixed {
-		position: fixed;
-	}
-
-	&.front {
-		z-index: 20000;
-	}
-}
-</style>
diff --git a/src/client/components/ui/super-menu.vue b/src/client/components/ui/super-menu.vue
deleted file mode 100644
index 195cc57326..0000000000
--- a/src/client/components/ui/super-menu.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<template>
-<div class="rrevdjwu" :class="{ grid }">
-	<div class="group" v-for="group in def">
-		<div class="title" v-if="group.title">{{ group.title }}</div>
-
-		<div class="items">
-			<template v-for="(item, i) in group.items">
-				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
-					<i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
-					<span class="text">{{ item.text }}</span>
-				</a>
-				<button v-else-if="item.type === 'button'" @click="ev => item.action(ev)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active">
-					<i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
-					<span class="text">{{ item.text }}</span>
-				</button>
-				<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
-					<i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
-					<span class="text">{{ item.text }}</span>
-				</MkA>
-			</template>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref, unref } from 'vue';
-
-export default defineComponent({
-	props: {
-		def: {
-			type: Array,
-			required: true
-		},
-		grid: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.rrevdjwu {
-	> .group {
-		& + .group {
-			margin-top: 16px;
-			padding-top: 16px;
-			border-top: solid 0.5px var(--divider);
-		}
-
-		> .title {
-			font-size: 0.9em;
-			opacity: 0.7;
-			margin: 0 0 8px 12px;
-		}
-	
-		> .items {
-			> .item {
-				display: flex;
-				align-items: center;
-				width: 100%;
-				box-sizing: border-box;
-				padding: 10px 16px 10px 8px;
-				border-radius: 9px;
-				font-size: 0.9em;
-
-				&:hover {
-					text-decoration: none;
-					background: var(--panelHighlight);
-				}
-
-				&.active {
-					color: var(--accent);
-					background: var(--accentedBg);
-				}
-
-				&.danger {
-					color: var(--error);
-				}
-
-				> .icon {
-					width: 32px;
-					margin-right: 2px;
-					flex-shrink: 0;
-					text-align: center;
-					opacity: 0.8;
-				}
-
-				> .text {
-					white-space: nowrap;
-					text-overflow: ellipsis;
-					overflow: hidden;
-					padding-right: 12px;
-				}
-
-			}
-		}
-	}
-
-	&.grid {
-		> .group {
-			& + .group {
-				padding-top: 0;
-				border-top: none;
-			}
-
-			margin-left: 0;
-			margin-right: 0;
-
-			> .title {
-				font-size: 1em;
-				opacity: 0.7;
-				margin: 0 0 8px 16px;
-			}
-
-			> .items {
-				display: grid;
-				grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
-				grid-gap: 8px;
-				padding: 0 16px;
-
-				> .item {
-					flex-direction: column;
-					padding: 18px 16px 16px 16px;
-					background: var(--panel);
-					border-radius: 8px;
-					text-align: center;
-
-					> .icon {
-						display: block;
-						margin-right: 0;
-						margin-bottom: 12px;
-						font-size: 1.5em;
-					}
-
-					> .text {
-						padding-right: 0;
-						width: 100%;
-						font-size: 0.8em;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/ui/tooltip.vue b/src/client/components/ui/tooltip.vue
deleted file mode 100644
index c003895c14..0000000000
--- a/src/client/components/ui/tooltip.vue
+++ /dev/null
@@ -1,92 +0,0 @@
-<template>
-<transition name="tooltip" appear @after-leave="$emit('closed')">
-	<div class="buebdbiu _acrylic _shadow" v-show="showing" ref="content" :style="{ maxWidth: maxWidth + 'px' }">
-		<slot>{{ text }}</slot>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		showing: {
-			type: Boolean,
-			required: true,
-		},
-		source: {
-			required: true,
-		},
-		text: {
-			type: String,
-			required: false
-		},
-		maxWidth: {
-			type: Number,
-			required: false,
-			default: 250,
-		},
-	},
-
-	emits: ['closed'],
-
-	mounted() {
-		this.$nextTick(() => {
-			if (this.source == null) {
-				this.$emit('closed');
-				return;
-			}
-
-			const rect = this.source.getBoundingClientRect();
-
-			const contentWidth = this.$refs.content.offsetWidth;
-			const contentHeight = this.$refs.content.offsetHeight;
-
-			let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2);
-			let top = rect.top + window.pageYOffset - contentHeight;
-
-			left -= (this.$el.offsetWidth / 2);
-
-			if (left + contentWidth - window.pageXOffset > window.innerWidth) {
-				left = window.innerWidth - contentWidth + window.pageXOffset - 1;
-			}
-
-			if (top - window.pageYOffset < 0) {
-				top = rect.top + window.pageYOffset + this.source.offsetHeight;
-				this.$refs.content.style.transformOrigin = 'center top';
-			}
-
-			this.$el.style.left = left + 'px';
-			this.$el.style.top = top + 'px';
-		});
-	},
-})
-</script>
-
-<style lang="scss" scoped>
-.tooltip-enter-active,
-.tooltip-leave-active {
-	opacity: 1;
-	transform: scale(1);
-	transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), opacity 200ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.tooltip-enter-from,
-.tooltip-leave-active {
-	opacity: 0;
-	transform: scale(0.75);
-}
-
-.buebdbiu {
-	position: absolute;
-	z-index: 11000;
-	font-size: 0.8em;
-	padding: 8px 12px;
-	box-sizing: border-box;
-	text-align: center;
-	border-radius: 4px;
-	border: solid 0.5px var(--divider);
-	pointer-events: none;
-	transform-origin: center bottom;
-}
-</style>
diff --git a/src/client/components/ui/window.vue b/src/client/components/ui/window.vue
deleted file mode 100644
index 00284b0467..0000000000
--- a/src/client/components/ui/window.vue
+++ /dev/null
@@ -1,525 +0,0 @@
-<template>
-<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
-	<div class="ebkgocck" :class="{ front }" v-if="showing">
-		<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
-			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
-				<span class="left">
-					<slot name="headerLeft"></slot>
-				</span>
-				<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
-					<slot name="header"></slot>
-				</span>
-				<span class="right">
-					<slot name="headerRight"></slot>
-					<button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button>
-				</span>
-			</div>
-			<div class="body" v-if="padding">
-				<div class="_section">
-					<slot></slot>
-				</div>
-			</div>
-			<div class="body" v-else>
-				<slot></slot>
-			</div>
-		</div>
-		<template v-if="canResize">
-			<div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div>
-			<div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div>
-			<div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div>
-			<div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div>
-			<div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div>
-			<div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div>
-			<div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div>
-			<div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
-		</template>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import contains from '@client/scripts/contains';
-import * as os from '@client/os';
-
-const minHeight = 50;
-const minWidth = 250;
-
-function dragListen(fn) {
-	window.addEventListener('mousemove',  fn);
-	window.addEventListener('touchmove',  fn);
-	window.addEventListener('mouseleave', dragClear.bind(null, fn));
-	window.addEventListener('mouseup',    dragClear.bind(null, fn));
-	window.addEventListener('touchend',   dragClear.bind(null, fn));
-}
-
-function dragClear(fn) {
-	window.removeEventListener('mousemove',  fn);
-	window.removeEventListener('touchmove',  fn);
-	window.removeEventListener('mouseleave', dragClear);
-	window.removeEventListener('mouseup',    dragClear);
-	window.removeEventListener('touchend',   dragClear);
-}
-
-export default defineComponent({
-	provide: {
-		inWindow: true
-	},
-
-	props: {
-		padding: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		initialWidth: {
-			type: Number,
-			required: false,
-			default: 400
-		},
-		initialHeight: {
-			type: Number,
-			required: false,
-			default: null
-		},
-		canResize: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		closeButton: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		mini: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		front: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		contextmenu: {
-			type: Array,
-			required: false,
-		}
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			showing: true,
-			id: Math.random().toString(), // TODO: UUIDとかにする
-		};
-	},
-
-	mounted() {
-		if (this.initialWidth) this.applyTransformWidth(this.initialWidth);
-		if (this.initialHeight) this.applyTransformHeight(this.initialHeight);
-
-		this.applyTransformTop((window.innerHeight / 2) - (this.$el.offsetHeight / 2));
-		this.applyTransformLeft((window.innerWidth / 2) - (this.$el.offsetWidth / 2));
-
-		os.windows.set(this.id, {
-			z: Number(document.defaultView.getComputedStyle(this.$el, null).zIndex)
-		});
-
-		// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
-		this.top();
-
-		window.addEventListener('resize', this.onBrowserResize);
-	},
-
-	unmounted() {
-		os.windows.delete(this.id);
-		window.removeEventListener('resize', this.onBrowserResize);
-	},
-
-	methods: {
-		close() {
-			this.showing = false;
-		},
-
-		onKeydown(e) {
-			if (e.which === 27) { // Esc
-				e.preventDefault();
-				e.stopPropagation();
-				this.close();
-			}
-		},
-
-		onContextmenu(e) {
-			if (this.contextmenu) {
-				os.contextMenu(this.contextmenu, e);
-			}
-		},
-
-		// 最前面へ移動
-		top() {
-			let z = 0;
-			const ws = Array.from(os.windows.entries()).filter(([k, v]) => k !== this.id).map(([k, v]) => v);
-			for (const w of ws) {
-				if (w.z > z) z = w.z;
-			}
-			if (z > 0) {
-				(this.$el as any).style.zIndex = z + 1;
-				os.windows.set(this.id, {
-					z: z + 1
-				});
-			}
-		},
-
-		onBodyMousedown() {
-			this.top();
-		},
-
-		onHeaderMousedown(e) {
-			const main = this.$el as any;
-
-			if (!contains(main, document.activeElement)) main.focus();
-
-			const position = main.getBoundingClientRect();
-
-			const clickX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX;
-			const clickY = e.touches && e.touches.length > 0 ? e.touches[0].clientY : e.clientY;
-			const moveBaseX = clickX - position.left;
-			const moveBaseY = clickY - position.top;
-			const browserWidth = window.innerWidth;
-			const browserHeight = window.innerHeight;
-			const windowWidth = main.offsetWidth;
-			const windowHeight = main.offsetHeight;
-
-			// 動かした時
-			dragListen(me => {
-				const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
-				const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
-
-				let moveLeft = x - moveBaseX;
-				let moveTop = y - moveBaseY;
-
-				// 下はみ出し
-				if (moveTop + windowHeight > browserHeight) moveTop = browserHeight - windowHeight;
-
-				// 左はみ出し
-				if (moveLeft < 0) moveLeft = 0;
-
-				// 上はみ出し
-				if (moveTop < 0) moveTop = 0;
-
-				// 右はみ出し
-				if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
-
-				this.$el.style.left = moveLeft + 'px';
-				this.$el.style.top = moveTop + 'px';
-			});
-		},
-
-		// 上ハンドル掴み時
-		onTopHandleMousedown(e) {
-			const main = this.$el as any;
-
-			const base = e.clientY;
-			const height = parseInt(getComputedStyle(main, '').height, 10);
-			const top = parseInt(getComputedStyle(main, '').top, 10);
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientY - base;
-				if (top + move > 0) {
-					if (height + -move > minHeight) {
-						this.applyTransformHeight(height + -move);
-						this.applyTransformTop(top + move);
-					} else { // 最小の高さより小さくなろうとした時
-						this.applyTransformHeight(minHeight);
-						this.applyTransformTop(top + (height - minHeight));
-					}
-				} else { // 上のはみ出し時
-					this.applyTransformHeight(top + height);
-					this.applyTransformTop(0);
-				}
-			});
-		},
-
-		// 右ハンドル掴み時
-		onRightHandleMousedown(e) {
-			const main = this.$el as any;
-
-			const base = e.clientX;
-			const width = parseInt(getComputedStyle(main, '').width, 10);
-			const left = parseInt(getComputedStyle(main, '').left, 10);
-			const browserWidth = window.innerWidth;
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientX - base;
-				if (left + width + move < browserWidth) {
-					if (width + move > minWidth) {
-						this.applyTransformWidth(width + move);
-					} else { // 最小の幅より小さくなろうとした時
-						this.applyTransformWidth(minWidth);
-					}
-				} else { // 右のはみ出し時
-					this.applyTransformWidth(browserWidth - left);
-				}
-			});
-		},
-
-		// 下ハンドル掴み時
-		onBottomHandleMousedown(e) {
-			const main = this.$el as any;
-
-			const base = e.clientY;
-			const height = parseInt(getComputedStyle(main, '').height, 10);
-			const top = parseInt(getComputedStyle(main, '').top, 10);
-			const browserHeight = window.innerHeight;
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientY - base;
-				if (top + height + move < browserHeight) {
-					if (height + move > minHeight) {
-						this.applyTransformHeight(height + move);
-					} else { // 最小の高さより小さくなろうとした時
-						this.applyTransformHeight(minHeight);
-					}
-				} else { // 下のはみ出し時
-					this.applyTransformHeight(browserHeight - top);
-				}
-			});
-		},
-
-		// 左ハンドル掴み時
-		onLeftHandleMousedown(e) {
-			const main = this.$el as any;
-
-			const base = e.clientX;
-			const width = parseInt(getComputedStyle(main, '').width, 10);
-			const left = parseInt(getComputedStyle(main, '').left, 10);
-
-			// 動かした時
-			dragListen(me => {
-				const move = me.clientX - base;
-				if (left + move > 0) {
-					if (width + -move > minWidth) {
-						this.applyTransformWidth(width + -move);
-						this.applyTransformLeft(left + move);
-					} else { // 最小の幅より小さくなろうとした時
-						this.applyTransformWidth(minWidth);
-						this.applyTransformLeft(left + (width - minWidth));
-					}
-				} else { // 左のはみ出し時
-					this.applyTransformWidth(left + width);
-					this.applyTransformLeft(0);
-				}
-			});
-		},
-
-		// 左上ハンドル掴み時
-		onTopLeftHandleMousedown(e) {
-			this.onTopHandleMousedown(e);
-			this.onLeftHandleMousedown(e);
-		},
-
-		// 右上ハンドル掴み時
-		onTopRightHandleMousedown(e) {
-			this.onTopHandleMousedown(e);
-			this.onRightHandleMousedown(e);
-		},
-
-		// 右下ハンドル掴み時
-		onBottomRightHandleMousedown(e) {
-			this.onBottomHandleMousedown(e);
-			this.onRightHandleMousedown(e);
-		},
-
-		// 左下ハンドル掴み時
-		onBottomLeftHandleMousedown(e) {
-			this.onBottomHandleMousedown(e);
-			this.onLeftHandleMousedown(e);
-		},
-
-		// 高さを適用
-		applyTransformHeight(height) {
-			if (height > window.innerHeight) height = window.innerHeight;
-			(this.$el as any).style.height = height + 'px';
-		},
-
-		// 幅を適用
-		applyTransformWidth(width) {
-			if (width > window.innerWidth) width = window.innerWidth;
-			(this.$el as any).style.width = width + 'px';
-		},
-
-		// Y座標を適用
-		applyTransformTop(top) {
-			(this.$el as any).style.top = top + 'px';
-		},
-
-		// X座標を適用
-		applyTransformLeft(left) {
-			(this.$el as any).style.left = left + 'px';
-		},
-
-		onBrowserResize() {
-			const main = this.$el as any;
-			const position = main.getBoundingClientRect();
-			const browserWidth = window.innerWidth;
-			const browserHeight = window.innerHeight;
-			const windowWidth = main.offsetWidth;
-			const windowHeight = main.offsetHeight;
-			if (position.left < 0) main.style.left = 0;     // 左はみ出し
-			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px';  // 下はみ出し
-			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px';    // 右はみ出し
-			if (position.top < 0) main.style.top = 0;       // 上はみ出し
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.window-enter-active, .window-leave-active {
-	transition: opacity 0.2s, transform 0.2s !important;
-}
-.window-enter-from, .window-leave-to {
-	pointer-events: none;
-	opacity: 0;
-	transform: scale(0.9);
-}
-
-.ebkgocck {
-	position: fixed;
-	top: 0;
-	left: 0;
-	z-index: 10000; // mk-modalのと同じでなければならない
-
-	&.front {
-		z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない
-	}
-
-	> .body {
-		overflow: hidden;
-		display: flex;
-		flex-direction: column;
-		contain: content;
-		width: 100%;
-    height: 100%;
-
-		> .header {
-			--height: 50px;
-
-			&.mini {
-				--height: 38px;
-			}
-
-			display: flex;
-			position: relative;
-			z-index: 1;
-			flex-shrink: 0;
-			user-select: none;
-			height: var(--height);
-			border-bottom: solid 1px var(--divider);
-
-			> .left, > .right {
-				> ::v-deep(button) {
-					height: var(--height);
-					width: var(--height);
-
-					&:hover {
-						color: var(--fgHighlighted);
-					}
-				}
-			}
-
-			> .title {
-				flex: 1;
-				position: relative;
-				line-height: var(--height);
-				white-space: nowrap;
-				overflow: hidden;
-				text-overflow: ellipsis;
-				text-align: center;
-				cursor: move;
-			}
-		}
-
-		> .body {
-			flex: 1;
-			overflow: auto;
-		}
-	}
-
-	> .handle {
-		$size: 8px;
-
-		position: absolute;
-
-		&.top {
-			top: -($size);
-			left: 0;
-			width: 100%;
-			height: $size;
-			cursor: ns-resize;
-		}
-
-		&.right {
-			top: 0;
-			right: -($size);
-			width: $size;
-			height: 100%;
-			cursor: ew-resize;
-		}
-
-		&.bottom {
-			bottom: -($size);
-			left: 0;
-			width: 100%;
-			height: $size;
-			cursor: ns-resize;
-		}
-
-		&.left {
-			top: 0;
-			left: -($size);
-			width: $size;
-			height: 100%;
-			cursor: ew-resize;
-		}
-
-		&.top-left {
-			top: -($size);
-			left: -($size);
-			width: $size * 2;
-			height: $size * 2;
-			cursor: nwse-resize;
-		}
-
-		&.top-right {
-			top: -($size);
-			right: -($size);
-			width: $size * 2;
-			height: $size * 2;
-			cursor: nesw-resize;
-		}
-
-		&.bottom-right {
-			bottom: -($size);
-			right: -($size);
-			width: $size * 2;
-			height: $size * 2;
-			cursor: nwse-resize;
-		}
-
-		&.bottom-left {
-			bottom: -($size);
-			left: -($size);
-			width: $size * 2;
-			height: $size * 2;
-			cursor: nesw-resize;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/updated.vue b/src/client/components/updated.vue
deleted file mode 100644
index 9e5b24acdb..0000000000
--- a/src/client/components/updated.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
-	<div class="ewlycnyt">
-		<div class="title">{{ $ts.misskeyUpdated }}</div>
-		<div class="version">✨{{ version }}🚀</div>
-		<MkButton full @click="whatIsNew">{{ $ts.whatIsNew }}</MkButton>
-		<MkButton class="gotIt" primary full @click="$refs.modal.close()">{{ $ts.gotIt }}</MkButton>
-	</div>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkModal from '@client/components/ui/modal.vue';
-import MkButton from '@client/components/ui/button.vue';
-import { version } from '@client/config';
-
-export default defineComponent({
-	components: {
-		MkModal,
-		MkButton,
-	},
-
-	data() {
-		return {
-			version: version,
-		};
-	},
-
-	methods: {
-		whatIsNew() {
-			this.$refs.modal.close();
-			window.open(`https://misskey-hub.net/docs/releases.html#_${version.replace(/\./g, '-')}`, '_blank');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ewlycnyt {
-	position: relative;
-	padding: 32px;
-	min-width: 320px;
-	max-width: 480px;
-	box-sizing: border-box;
-	text-align: center;
-	background: var(--panel);
-	border-radius: var(--radius);
-
-	> .title {
-		font-weight: bold;
-	}
-
-	> .version {
-		margin: 1em 0;
-	}
-
-	> .gotIt {
-		margin: 8px 0 0 0;
-	}
-}
-</style>
diff --git a/src/client/components/url-preview-popup.vue b/src/client/components/url-preview-popup.vue
deleted file mode 100644
index b5e0fce207..0000000000
--- a/src/client/components/url-preview-popup.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<div class="fgmtyycl" :style="{ top: top + 'px', left: left + 'px' }">
-	<transition name="zoom" @after-leave="$emit('closed')">
-		<MkUrlPreview class="_popup _shadow" :url="url" v-if="showing"/>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkUrlPreview from './url-preview.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkUrlPreview
-	},
-
-	props: {
-		url: {
-			type: String,
-			required: true
-		},
-		source: {
-			required: true
-		},
-		showing: {
-			type: Boolean,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			u: null,
-			top: 0,
-			left: 0,
-		};
-	},
-
-	mounted() {
-		const rect = this.source.getBoundingClientRect();
-		const x = Math.max((rect.left + (this.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset;
-		const y = rect.top + this.source.offsetHeight + window.pageYOffset;
-
-		this.top = y;
-		this.left = x;
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.fgmtyycl {
-	position: absolute;
-	z-index: 11000;
-	width: 500px;
-	max-width: calc(90vw - 12px);
-	pointer-events: none;
-}
-</style>
diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue
deleted file mode 100644
index 1d44b04578..0000000000
--- a/src/client/components/url-preview.vue
+++ /dev/null
@@ -1,334 +0,0 @@
-<template>
-<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
-	<button class="disablePlayer" @click="playerEnabled = false" :title="$ts.disablePlayer"><i class="fas fa-times"></i></button>
-	<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
-</div>
-<div v-else-if="tweetId && tweetExpanded" class="twitter" ref="twitter">
-	<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
-</div>
-<div v-else class="mk-url-preview" v-size="{ max: [400, 350] }">
-	<transition name="zoom" mode="out-in">
-		<component :is="self ? 'MkA' : 'a'" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching">
-			<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
-				<button class="_button" v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$ts.enablePlayer"><i class="fas fa-play-circle"></i></button>
-			</div>
-			<article>
-				<header>
-					<h1 :title="title">{{ title }}</h1>
-				</header>
-				<p v-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
-				<footer>
-					<img class="icon" v-if="icon" :src="icon"/>
-					<p :title="sitename">{{ sitename }}</p>
-				</footer>
-			</article>
-		</component>
-	</transition>
-	<div class="expandTweet" v-if="tweetId">
-		<a @click="tweetExpanded = true">
-			<i class="fab fa-twitter"></i> {{ $ts.expandTweet }}
-		</a>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { url as local, lang } from '@client/config';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		url: {
-			type: String,
-			require: true
-		},
-
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-
-		compact: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	data() {
-		const self = this.url.startsWith(local);
-		return {
-			local,
-			fetching: true,
-			title: null,
-			description: null,
-			thumbnail: null,
-			icon: null,
-			sitename: null,
-			player: {
-				url: null,
-				width: null,
-				height: null
-			},
-			tweetId: null,
-			tweetExpanded: this.detail,
-			embedId: `embed${Math.random().toString().replace(/\D/,'')}`,
-			tweetHeight: 150,
- 			tweetLeft: 0,
-			playerEnabled: false,
-			self: self,
-			attr: self ? 'to' : 'href',
-			target: self ? null : '_blank',
-		};
-	},
-
-	created() {
-		const requestUrl = new URL(this.url);
-
-		if (requestUrl.hostname == 'twitter.com') {
-			const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
-			if (m) this.tweetId = m[1];
-		}
-
-		if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
-			requestUrl.hostname = 'www.youtube.com';
-		}
-
-		const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
-
-		requestUrl.hash = '';
-
-		fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
-			res.json().then(info => {
-				if (info.url == null) return;
-				this.title = info.title;
-				this.description = info.description;
-				this.thumbnail = info.thumbnail;
-				this.icon = info.icon;
-				this.sitename = info.sitename;
-				this.fetching = false;
-				this.player = info.player;
-			})
-		});
-
-		(window as any).addEventListener('message', this.adjustTweetHeight);
-	},
-
-	mounted() {
-		// 300pxないと絶対右にはみ出るので左に移動してしまう
-		const areaWidth = (this.$el as any)?.clientWidth;
-		if (areaWidth && areaWidth < 300) this.tweetLeft = areaWidth - 241;
-	},
-
-	methods: {
-		adjustTweetHeight(message: any) {
-			if (message.origin !== 'https://platform.twitter.com') return;
-			const embed = message.data?.['twttr.embed'];
-			if (embed?.method !== 'twttr.private.resize') return;
-			if (embed?.id !== this.embedId) return;
-			const height = embed?.params[0]?.height;
-			if (height) this.tweetHeight = height;
- 		},
-	},
-
-	beforeUnmount() {
-		(window as any).removeEventListener('message', this.adjustTweetHeight);
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.player {
-	position: relative;
-	width: 100%;
-
-	> button {
-		position: absolute;
-		top: -1.5em;
-		right: 0;
-		font-size: 1em;
-		width: 1.5em;
-		height: 1.5em;
-		padding: 0;
-		margin: 0;
-		color: var(--fg);
-		background: rgba(128, 128, 128, 0.2);
-		opacity: 0.7;
-
-		&:hover {
-			opacity: 0.9;
-		}
-	}
-
-	> iframe {
-		height: 100%;
-		left: 0;
-		position: absolute;
-		top: 0;
-		width: 100%;
-	}
-}
-
-.mk-url-preview {
-	&.max-width_400px {
-		> a {
-			font-size: 12px;
-
-			> .thumbnail {
-				height: 80px;
-			}
-
-			> article {
-				padding: 12px;
-			}
-		}
-	}
-
-	&.max-width_350px {
-		> a {
-			font-size: 10px;
-
-			> .thumbnail {
-				height: 70px;
-			}
-
-			> article {
-				padding: 8px;
-
-				> header {
-					margin-bottom: 4px;
-				}
-
-				> footer {
-					margin-top: 4px;
-
-					> img {
-						width: 12px;
-						height: 12px;
-					}
-				}
-			}
-
-			&.compact {
-				> .thumbnail {
-					position: absolute;
-					width: 56px;
-					height: 100%;
-				}
-
-				> article {
-					left: 56px;
-					width: calc(100% - 56px);
-					padding: 4px;
-
-					> header {
-						margin-bottom: 2px;
-					}
-
-					> footer {
-						margin-top: 2px;
-					}
-				}
-			}
-		}
-	}
-
-	> a {
-		position: relative;
-		display: block;
-		font-size: 14px;
-		box-shadow: 0 0 0 1px var(--divider);
-		border-radius: 8px;
-		overflow: hidden;
-
-		&:hover {
-			text-decoration: none;
-			border-color: rgba(0, 0, 0, 0.2);
-
-			> article > header > h1 {
-				text-decoration: underline;
-			}
-		}
-
-		> .thumbnail {
-			position: absolute;
-			width: 100px;
-			height: 100%;
-			background-position: center;
-			background-size: cover;
-			display: flex;
-			justify-content: center;
-			align-items: center;
-
-			> button {
-				font-size: 3.5em;
-				opacity: 0.7;
-
-				&:hover {
-					font-size: 4em;
-					opacity: 0.9;
-				}
-			}
-
-			& + article {
-				left: 100px;
-				width: calc(100% - 100px);
-			}
-		}
-
-		> article {
-			position: relative;
-			box-sizing: border-box;
-			padding: 16px;
-
-			> header {
-				margin-bottom: 8px;
-
-				> h1 {
-					margin: 0;
-					font-size: 1em;
-				}
-			}
-
-			> p {
-				margin: 0;
-				font-size: 0.8em;
-			}
-
-			> footer {
-				margin-top: 8px;
-				height: 16px;
-
-				> img {
-					display: inline-block;
-					width: 16px;
-					height: 16px;
-					margin-right: 4px;
-					vertical-align: top;
-				}
-
-				> p {
-					display: inline-block;
-					margin: 0;
-					color: var(--urlPreviewInfo);
-					font-size: 0.8em;
-					line-height: 16px;
-					vertical-align: top;
-				}
-			}
-		}
-
-		&.compact {
-			> article {
-				> header h1, p, footer {
-					overflow: hidden;
-					white-space: nowrap;
-					text-overflow: ellipsis;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/components/user-info.vue b/src/client/components/user-info.vue
deleted file mode 100644
index e76f2ecaa6..0000000000
--- a/src/client/components/user-info.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-<template>
-<div class="_panel vjnjpkug">
-	<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
-	<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
-	<div class="title">
-		<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
-		<p class="username"><MkAcct :user="user"/></p>
-	</div>
-	<div class="description">
-		<div class="mfm" v-if="user.description">
-			<Mfm :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/>
-		</div>
-		<span v-else style="opacity: 0.7;">{{ $ts.noAccountDescription }}</span>
-	</div>
-	<div class="status">
-		<div>
-			<p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span>
-		</div>
-		<div>
-			<p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span>
-		</div>
-		<div>
-			<p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span>
-		</div>
-	</div>
-	<MkFollowButton class="koudoku-button" v-if="$i && user.id != $i.id" :user="user" mini/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { parseAcct } from '@/misc/acct';
-import MkFollowButton from './follow-button.vue';
-import { userPage } from '@client/filters/user';
-
-export default defineComponent({
-	components: {
-		MkFollowButton
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-		userPage,
-		parseAcct,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.vjnjpkug {
-	position: relative;
-
-	> .banner {
-		height: 84px;
-		background-color: rgba(0, 0, 0, 0.1);
-		background-size: cover;
-		background-position: center;
-	}
-
-	> .avatar {
-		display: block;
-		position: absolute;
-		top: 62px;
-		left: 13px;
-		z-index: 2;
-		width: 58px;
-		height: 58px;
-		border: solid 4px var(--panel);
-	}
-
-	> .title {
-		display: block;
-		padding: 10px 0 10px 88px;
-
-		> .name {
-			display: inline-block;
-			margin: 0;
-			font-weight: bold;
-			line-height: 16px;
-			word-break: break-all;
-		}
-
-		> .username {
-			display: block;
-			margin: 0;
-			line-height: 16px;
-			font-size: 0.8em;
-			color: var(--fg);
-			opacity: 0.7;
-		}
-	}
-
-	> .description {
-		padding: 16px;
-		font-size: 0.8em;
-		border-top: solid 0.5px var(--divider);
-
-		> .mfm {
-			display: -webkit-box;
-			-webkit-line-clamp: 3;
-			-webkit-box-orient: vertical;  
-			overflow: hidden;
-		}
-	}
-
-	> .status {
-		padding: 10px 16px;
-		border-top: solid 0.5px var(--divider);
-
-		> div {
-			display: inline-block;
-			width: 33%;
-
-			> p {
-				margin: 0;
-				font-size: 0.7em;
-				color: var(--fg);
-			}
-
-			> span {
-				font-size: 1em;
-				color: var(--accent);
-			}
-		}
-	}
-
-	> .koudoku-button {
-		position: absolute;
-		top: 8px;
-		right: 8px;
-	}
-}
-</style>
diff --git a/src/client/components/user-list.vue b/src/client/components/user-list.vue
deleted file mode 100644
index 9c91183971..0000000000
--- a/src/client/components/user-list.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<template>
-<MkError v-if="error" @retry="init()"/>
-
-<div v-else class="efvhhmdq _isolated">
-	<div class="no-users" v-if="empty">
-		<p>{{ $ts.noUsers }}</p>
-	</div>
-	<div class="users">
-		<MkUserInfo class="user" v-for="user in users" :user="user" :key="user.id"/>
-	</div>
-	<button class="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :class="{ fetching: moreFetching }" v-show="more" :disabled="moreFetching">
-		<template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ moreFetching ? $ts.loading : $ts.loadMore }}
-	</button>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@client/scripts/paging';
-import MkUserInfo from './user-info.vue';
-import { userPage } from '@client/filters/user';
-
-export default defineComponent({
-	components: {
-		MkUserInfo,
-	},
-
-	mixins: [
-		paging({}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-		extract: {
-			required: false
-		},
-		expanded: {
-			type: Boolean,
-			default: true
-		},
-	},
-
-	computed: {
-		users() {
-			return this.extract ? this.extract(this.items) : this.items;
-		}
-	},
-
-	methods: {
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.efvhhmdq {
-	> .no-users {
-		text-align: center;
-	}
-
-	> .users {
-		display: grid;
-		grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-		grid-gap: var(--margin);
-	}
-
-	> .more {
-		display: block;
-		width: 100%;
-		padding: 16px;
-
-		&:hover {
-			background: rgba(#000, 0.025);
-		}
-
-		&:active {
-			background: rgba(#000, 0.05);
-		}
-
-		&.fetching {
-			cursor: wait;
-		}
-
-		> i {
-			margin-right: 4px;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/user-online-indicator.vue b/src/client/components/user-online-indicator.vue
deleted file mode 100644
index afaf0e8736..0000000000
--- a/src/client/components/user-online-indicator.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<div class="fzgwjkgc" :class="user.onlineStatus" v-tooltip="text"></div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	computed: {
-		text(): string {
-			switch (this.user.onlineStatus) {
-				case 'online': return this.$ts.online;
-				case 'active': return this.$ts.active;
-				case 'offline': return this.$ts.offline;
-				case 'unknown': return this.$ts.unknown;
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fzgwjkgc {
-	box-shadow: 0 0 0 3px var(--panel);
-	border-radius: 120%; // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる
-
-	&.online {
-		background: #58d4c9;
-	}
-
-	&.active {
-		background: #e4bc48;
-	}
-
-	&.offline {
-		background: #ea5353;
-	}
-
-	&.unknown {
-		background: #888;
-	}
-}
-</style>
diff --git a/src/client/components/user-preview.vue b/src/client/components/user-preview.vue
deleted file mode 100644
index 1249f205aa..0000000000
--- a/src/client/components/user-preview.vue
+++ /dev/null
@@ -1,192 +0,0 @@
-<template>
-<transition name="popup" appear @after-leave="$emit('closed')">
-	<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }">
-		<div v-if="fetched" class="info">
-			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
-			<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
-			<div class="title">
-				<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
-				<p class="username"><MkAcct :user="user"/></p>
-			</div>
-			<div class="description">
-				<Mfm v-if="user.description" :text="user.description" :author="user" :i="$i" :custom-emojis="user.emojis"/>
-			</div>
-			<div class="status">
-				<div>
-					<p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span>
-				</div>
-				<div>
-					<p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span>
-				</div>
-				<div>
-					<p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span>
-				</div>
-			</div>
-			<MkFollowButton class="koudoku-button" v-if="$i && user.id != $i.id" :user="user" mini/>
-		</div>
-		<div v-else>
-			<MkLoading/>
-		</div>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { parseAcct } from '@/misc/acct';
-import MkFollowButton from './follow-button.vue';
-import { userPage } from '@client/filters/user';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkFollowButton
-	},
-
-	props: {
-		showing: {
-			type: Boolean,
-			required: true
-		},
-		q: {
-			type: String,
-			required: true
-		},
-		source: {
-			required: true
-		}
-	},
-
-	emits: ['closed', 'mouseover', 'mouseleave'],
-
-	data() {
-		return {
-			user: null,
-			fetched: false,
-			top: 0,
-			left: 0,
-		};
-	},
-
-	mounted() {
-		if (typeof this.q == 'object') {
-			this.user = this.q;
-			this.fetched = true;
-		} else {
-			const query = this.q.startsWith('@') ?
-				parseAcct(this.q.substr(1)) :
-				{ userId: this.q };
-
-			os.api('users/show', query).then(user => {
-				if (!this.showing) return;
-				this.user = user;
-				this.fetched = true;
-			});
-		}
-
-		const rect = this.source.getBoundingClientRect();
-		const x = ((rect.left + (this.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
-		const y = rect.top + this.source.offsetHeight + window.pageYOffset;
-
-		this.top = y;
-		this.left = x;
-	},
-
-	methods: {
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.popup-enter-active, .popup-leave-active {
-	transition: opacity 0.3s, transform 0.3s !important;
-}
-.popup-enter-from, .popup-leave-to {
-	opacity: 0;
-	transform: scale(0.9);
-}
-
-.fxxzrfni {
-	position: absolute;
-	z-index: 11000;
-	width: 300px;
-	overflow: hidden;
-	transform-origin: center top;
-
-	> .info {
-		> .banner {
-			height: 84px;
-			background-color: rgba(0, 0, 0, 0.1);
-			background-size: cover;
-			background-position: center;
-		}
-
-		> .avatar {
-			display: block;
-			position: absolute;
-			top: 62px;
-			left: 13px;
-			z-index: 2;
-			width: 58px;
-			height: 58px;
-			border: solid 3px var(--face);
-			border-radius: 8px;
-		}
-
-		> .title {
-			display: block;
-			padding: 8px 0 8px 82px;
-
-			> .name {
-				display: inline-block;
-				margin: 0;
-				font-weight: bold;
-				line-height: 16px;
-				word-break: break-all;
-			}
-
-			> .username {
-				display: block;
-				margin: 0;
-				line-height: 16px;
-				font-size: 0.8em;
-				color: var(--fg);
-				opacity: 0.7;
-			}
-		}
-
-		> .description {
-			padding: 0 16px;
-			font-size: 0.8em;
-			color: var(--fg);
-		}
-
-		> .status {
-			padding: 8px 16px;
-
-			> div {
-				display: inline-block;
-				width: 33%;
-
-				> p {
-					margin: 0;
-					font-size: 0.7em;
-					color: var(--fg);
-				}
-
-				> span {
-					font-size: 1em;
-					color: var(--accent);
-				}
-			}
-		}
-
-		> .koudoku-button {
-			position: absolute;
-			top: 8px;
-			right: 8px;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/user-select-dialog.vue b/src/client/components/user-select-dialog.vue
deleted file mode 100644
index 0f3ee2a126..0000000000
--- a/src/client/components/user-select-dialog.vue
+++ /dev/null
@@ -1,199 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:with-ok-button="true"
-	:ok-button-disabled="selected == null"
-	@click="cancel()"
-	@close="cancel()"
-	@ok="ok()"
-	@closed="$emit('closed')"
->
-	<template #header>{{ $ts.selectUser }}</template>
-	<div class="tbhwbxda _monolithic_">
-		<div class="_section">
-			<div class="_inputSplit">
-				<MkInput v-model="username" class="input" @update:modelValue="search" ref="username">
-					<template #label>{{ $ts.username }}</template>
-					<template #prefix>@</template>
-				</MkInput>
-				<MkInput v-model="host" class="input" @update:modelValue="search">
-					<template #label>{{ $ts.host }}</template>
-					<template #prefix>@</template>
-				</MkInput>
-			</div>
-		</div>
-		<div class="_section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }">
-			<div class="users" v-if="users.length > 0">
-				<div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
-					<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
-					<div class="body">
-						<MkUserName :user="user" class="name"/>
-						<MkAcct :user="user" class="acct"/>
-					</div>
-				</div>
-			</div>
-			<div v-else class="empty">
-				<span>{{ $ts.noUsers }}</span>
-			</div>
-		</div>
-		<div class="_section recent" v-if="username == '' && host == ''">
-			<div class="users">
-				<div class="user" v-for="user in recentUsers" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
-					<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
-					<div class="body">
-						<MkUserName :user="user" class="name"/>
-						<MkAcct :user="user" class="acct"/>
-					</div>
-				</div>
-			</div>
-		</div>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkInput from './form/input.vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkInput,
-		XModalWindow,
-	},
-
-	props: {
-	},
-
-	emits: ['ok', 'cancel', 'closed'],
-
-	data() {
-		return {
-			username: '',
-			host: '',
-			recentUsers: [],
-			users: [],
-			selected: null,
-		};
-	},
-
-	async mounted() {
-		this.focus();
-
-		this.$nextTick(() => {
-			this.focus();
-		});
-
-		this.recentUsers = await os.api('users/show', {
-			userIds: this.$store.state.recentlyUsedUsers
-		});
-	},
-
-	methods: {
-		search() {
-			if (this.username == '' && this.host == '') {
-				this.users = [];
-				return;
-			}
-			os.api('users/search-by-username-and-host', {
-				username: this.username,
-				host: this.host,
-				limit: 10,
-				detail: false
-			}).then(users => {
-				this.users = users;
-			});
-		},
-
-		focus() {
-			this.$refs.username.focus();
-		},
-
-		ok() {
-			this.$emit('ok', this.selected);
-			this.$refs.dialog.close();
-
-			// 最近使ったユーザー更新
-			let recents = this.$store.state.recentlyUsedUsers;
-			recents = recents.filter(x => x !== this.selected.id);
-			recents.unshift(this.selected.id);
-			this.$store.set('recentlyUsedUsers', recents.splice(0, 16));
-		},
-
-		cancel() {
-			this.$emit('cancel');
-			this.$refs.dialog.close();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.tbhwbxda {
-	> ._section {
-		display: flex;
-		flex-direction: column;
-		overflow: auto;
-		height: 100%;
-
-		&.result.hit {
-			padding: 0;
-		}
-
-		&.recent {
-			padding: 0;
-		}
-
-		> .users {
-			flex: 1;
-			overflow: auto;
-			padding: 8px 0;
-
-			> .user {
-				display: flex;
-				align-items: center;
-				padding: 8px var(--root-margin);
-				font-size: 14px;
-
-				&:hover {
-					background: var(--X7);
-				}
-
-				&.selected {
-					background: var(--accent);
-					color: #fff;
-				}
-
-				> * {
-					pointer-events: none;
-					user-select: none;
-				}
-
-				> .avatar {
-					width: 45px;
-					height: 45px;
-				}
-
-				> .body {
-					padding: 0 8px;
-					min-width: 0;
-
-					> .name {
-						display: block;
-						font-weight: bold;
-					}
-
-					> .acct {
-						opacity: 0.5;
-					}
-				}
-			}
-		}
-
-		> .empty {
-			opacity: 0.7;
-			text-align: center;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue
deleted file mode 100644
index 5199f34c14..0000000000
--- a/src/client/components/users-dialog.vue
+++ /dev/null
@@ -1,147 +0,0 @@
-<template>
-<div class="mk-users-dialog">
-	<div class="header">
-		<span>{{ title }}</span>
-		<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
-	</div>
-
-	<div class="users">
-		<MkA v-for="item in items" class="user" :key="item.id" :to="userPage(extract ? extract(item) : item)">
-			<MkAvatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true" :show-indicator="true"/>
-			<div class="body">
-				<MkUserName :user="extract ? extract(item) : item" class="name"/>
-				<MkAcct :user="extract ? extract(item) : item" class="acct"/>
-			</div>
-		</MkA>
-	</div>
-	<button class="more _button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching">
-		<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-		<template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>
-	</button>
-
-	<p class="empty" v-if="empty">{{ $ts.noUsers }}</p>
-
-	<MkError v-if="error" @retry="init()"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@client/scripts/paging';
-import { userPage } from '@client/filters/user';
-
-export default defineComponent({
-	mixins: [
-		paging({}),
-	],
-
-	props: {
-		title: {
-			required: true
-		},
-		pagination: {
-			required: true
-		},
-		extract: {
-			required: false
-		}
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-users-dialog {
-	width: 350px;
-	height: 350px;
-	background: var(--panel);
-	border-radius: var(--radius);
-	overflow: hidden;
-	display: flex;
-	flex-direction: column;
-
-	> .header {
-		display: flex;
-		flex-shrink: 0;
-
-		> button {
-			height: 58px;
-			width: 58px;
-
-			@media (max-width: 500px) {
-				height: 42px;
-				width: 42px;
-			}
-		}
-
-		> span {
-			flex: 1;
-			line-height: 58px;
-			padding-left: 32px;
-			font-weight: bold;
-
-			@media (max-width: 500px) {
-				line-height: 42px;
-				padding-left: 16px;
-			}
-		}
-	}
-
-	> .users {
-		flex: 1;
-		overflow: auto;
-
-		&:empty {
-			display: none;
-		}
-
-		> .user {
-			display: flex;
-			align-items: center;
-			font-size: 14px;
-			padding: 8px 32px;
-
-			@media (max-width: 500px) {
-				padding: 8px 16px;
-			}
-
-			> * {
-				pointer-events: none;
-				user-select: none;
-			}
-
-			> .avatar {
-				width: 45px;
-				height: 45px;
-			}
-
-			> .body {
-				padding: 0 8px;
-				overflow: hidden;
-
-				> .name {
-					display: block;
-					font-weight: bold;
-				}
-
-				> .acct {
-					opacity: 0.5;
-				}
-			}
-		}
-	}
-
-	> .empty {
-		text-align: center;
-		opacity: 0.5;
-	}
-}
-</style>
diff --git a/src/client/components/visibility-picker.vue b/src/client/components/visibility-picker.vue
deleted file mode 100644
index 492ec092e3..0000000000
--- a/src/client/components/visibility-picker.vue
+++ /dev/null
@@ -1,167 +0,0 @@
-<template>
-<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
-	<div class="gqyayizv _popup">
-		<button class="_button" @click="choose('public')" :class="{ active: v == 'public' }" data-index="1" key="public">
-			<div><i class="fas fa-globe"></i></div>
-			<div>
-				<span>{{ $ts._visibility.public }}</span>
-				<span>{{ $ts._visibility.publicDescription }}</span>
-			</div>
-		</button>
-		<button class="_button" @click="choose('home')" :class="{ active: v == 'home' }" data-index="2" key="home">
-			<div><i class="fas fa-home"></i></div>
-			<div>
-				<span>{{ $ts._visibility.home }}</span>
-				<span>{{ $ts._visibility.homeDescription }}</span>
-			</div>
-		</button>
-		<button class="_button" @click="choose('followers')" :class="{ active: v == 'followers' }" data-index="3" key="followers">
-			<div><i class="fas fa-unlock"></i></div>
-			<div>
-				<span>{{ $ts._visibility.followers }}</span>
-				<span>{{ $ts._visibility.followersDescription }}</span>
-			</div>
-		</button>
-		<button :disabled="localOnly" class="_button" @click="choose('specified')" :class="{ active: v == 'specified' }" data-index="4" key="specified">
-			<div><i class="fas fa-envelope"></i></div>
-			<div>
-				<span>{{ $ts._visibility.specified }}</span>
-				<span>{{ $ts._visibility.specifiedDescription }}</span>
-			</div>
-		</button>
-		<div class="divider"></div>
-		<button class="_button localOnly" @click="localOnly = !localOnly" :class="{ active: localOnly }" data-index="5" key="localOnly">
-			<div><i class="fas fa-biohazard"></i></div>
-			<div>
-				<span>{{ $ts._visibility.localOnly }}</span>
-				<span>{{ $ts._visibility.localOnlyDescription }}</span>
-			</div>
-			<div><i :class="localOnly ? 'fas fa-toggle-on' : 'fas fa-toggle-off'"></i></div>
-		</button>
-	</div>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkModal from '@client/components/ui/modal.vue';
-
-export default defineComponent({
-	components: {
-		MkModal,
-	},
-	props: {
-		currentVisibility: {
-			type: String,
-			required: true
-		},
-		currentLocalOnly: {
-			type: Boolean,
-			required: true
-		},
-		src: {
-			required: false
-		},
-	},
-	emits: ['change-visibility', 'change-local-only', 'closed'],
-	data() {
-		return {
-			v: this.currentVisibility,
-			localOnly: this.currentLocalOnly,
-		}
-	},
-	watch: {
-		localOnly() {
-			this.$emit('change-local-only', this.localOnly);
-		}
-	},
-	methods: {
-		choose(visibility) {
-			this.v = visibility;
-			this.$emit('change-visibility', visibility);
-			this.$nextTick(() => {
-				this.$refs.modal.close();
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.gqyayizv {
-	width: 240px;
-	padding: 8px 0;
-
-	> .divider {
-		margin: 8px 0;
-		border-top: solid 0.5px var(--divider);
-	}
-
-	> button {
-		display: flex;
-		padding: 8px 14px;
-		font-size: 12px;
-		text-align: left;
-		width: 100%;
-		box-sizing: border-box;
-
-		&:hover {
-			background: rgba(0, 0, 0, 0.05);
-		}
-
-		&:active {
-			background: rgba(0, 0, 0, 0.1);
-		}
-
-		&.active {
-			color: #fff;
-			background: var(--accent);
-		}
-
-		&.localOnly.active {
-			color: var(--accent);
-			background: inherit;
-		}
-
-		> *:nth-child(1) {
-			display: flex;
-			justify-content: center;
-			align-items: center;
-			margin-right: 10px;
-			width: 16px;
-			top: 0;
-			bottom: 0;
-			margin-top: auto;
-			margin-bottom: auto;
-		}
-
-		> *:nth-child(2) {
-			flex: 1 1 auto;
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-
-			> span:first-child {
-				display: block;
-				font-weight: bold;
-			}
-
-			> span:last-child:not(:first-child) {
-				opacity: 0.6;
-			}
-		}
-
-		> *:nth-child(3) {
-			display: flex;
-			justify-content: center;
-			align-items: center;
-			margin-left: 10px;
-			width: 16px;
-			top: 0;
-			bottom: 0;
-			margin-top: auto;
-			margin-bottom: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/components/waiting-dialog.vue b/src/client/components/waiting-dialog.vue
deleted file mode 100644
index ea9f6756b2..0000000000
--- a/src/client/components/waiting-dialog.vue
+++ /dev/null
@@ -1,92 +0,0 @@
-<template>
-<MkModal ref="modal" @click="success ? done() : () => {}" @closed="$emit('closed')">
-	<div class="iuyakobc" :class="{ iconOnly: (text == null) || success }">
-		<i v-if="success" class="fas fa-check icon success"></i>
-		<i v-else class="fas fa-spinner fa-pulse icon waiting"></i>
-		<div class="text" v-if="text && !success">{{ text }}<MkEllipsis/></div>
-	</div>
-</MkModal>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkModal from '@client/components/ui/modal.vue';
-
-export default defineComponent({
-	components: {
-		MkModal,
-	},
-
-	props: {
-		success: {
-			type: Boolean,
-			required: true,
-		},
-		showing: {
-			type: Boolean,
-			required: true,
-		},
-		text: {
-			type: String,
-			required: false,
-		},
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-		};
-	},
-
-	watch: {
-		showing() {
-			if (!this.showing) this.done();
-		}
-	},
-
-	methods: {
-		done() {
-			this.$emit('done');
-			this.$refs.modal.close();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.iuyakobc {
-	position: relative;
-	padding: 32px;
-	box-sizing: border-box;
-	text-align: center;
-	background: var(--panel);
-	border-radius: var(--radius);
-	width: 250px;
-
-	&.iconOnly {
-		padding: 0;
-		width: 96px;
-		height: 96px;
-		display: flex;
-		align-items: center;
-		justify-content: center;
-	}
-
-	> .icon {
-		font-size: 32px;
-
-		&.success {
-			color: var(--accent);
-		}
-
-		&.waiting {
-			opacity: 0.7;
-		}
-	}
-
-	> .text {
-		margin-top: 16px;
-	}
-}
-</style>
diff --git a/src/client/components/widgets.vue b/src/client/components/widgets.vue
deleted file mode 100644
index aef5de453c..0000000000
--- a/src/client/components/widgets.vue
+++ /dev/null
@@ -1,152 +0,0 @@
-<template>
-<div class="vjoppmmu">
-	<template v-if="edit">
-		<header>
-			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
-				<template #label>{{ $ts.selectWidget }}</template>
-				<option v-for="widget in widgetDefs" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
-			</MkSelect>
-			<MkButton inline @click="addWidget" primary><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
-			<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
-		</header>
-		<XDraggable
-			v-model="_widgets"
-			item-key="id"
-			animation="150"
-		>
-			<template #item="{element}">
-				<div class="customize-container">
-					<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
-					<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
-					<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" @updateProps="updateWidget(element.id, $event)"/>
-				</div>
-			</template>
-		</XDraggable>
-	</template>
-	<component v-else class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" @updateProps="updateWidget(widget.id, $event)"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-import MkSelect from '@client/components/form/select.vue';
-import MkButton from '@client/components/ui/button.vue';
-import { widgets as widgetDefs } from '@client/widgets';
-
-export default defineComponent({
-	components: {
-		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
-		MkSelect,
-		MkButton,
-	},
-
-	props: {
-		widgets: {
-			type: Array,
-			required: true,
-		},
-		edit: {
-			type: Boolean,
-			required: true,
-		},
-	},
-
-	emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
-
-	data() {
-		return {
-			widgetAdderSelected: null,
-			widgetDefs,
-			settings: {},
-		};
-	},
-
-	computed: {
-		_widgets: {
-			get() {
-				return this.widgets;
-			},
-			set(value) {
-				this.$emit('updateWidgets', value);
-			}
-		}
-	},
-
-	methods: {
-		configWidget(id) {
-			this.settings[id]();
-		},
-
-		addWidget() {
-			if (this.widgetAdderSelected == null) return;
-
-			this.$emit('addWidget', {
-				name: this.widgetAdderSelected,
-				id: uuid(),
-				data: {}
-			});
-
-			this.widgetAdderSelected = null;
-		},
-
-		removeWidget(widget) {
-			this.$emit('removeWidget', widget);
-		},
-
-		updateWidget(id, data) {
-			this.$emit('updateWidget', { id, data });
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.vjoppmmu {
-	> header {
-		margin: 16px 0;
-
-		> * {
-			width: 100%;
-			padding: 4px;
-		}
-	}
-
-	> .widget, .customize-container {
-		margin: var(--margin) 0;
-
-		&:first-of-type {
-			margin-top: 0;
-		}
-	}
-
-	.customize-container {
-		position: relative;
-		cursor: move;
-
-		> *:not(.remove):not(.config) {
-			pointer-events: none;
-		}
-
-		> .config,
-		> .remove {
-			position: absolute;
-			z-index: 10000;
-			top: 8px;
-			width: 32px;
-			height: 32px;
-			color: #fff;
-			background: rgba(#000, 0.7);
-			border-radius: 4px;
-		}
-
-		> .config {
-			right: 8px + 8px + 32px;
-		}
-
-		> .remove {
-			right: 8px;
-		}
-	}
-}
-</style>
diff --git a/src/client/config.ts b/src/client/config.ts
deleted file mode 100644
index f2022b0f02..0000000000
--- a/src/client/config.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-const address = new URL(location.href);
-const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content;
-
-export const host = address.host;
-export const hostname = address.hostname;
-export const url = address.origin;
-export const apiUrl = url + '/api';
-export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
-export const lang = localStorage.getItem('lang');
-export const langs = _LANGS_;
-export const locale = JSON.parse(localStorage.getItem('locale'));
-export const version = _VERSION_;
-export const instanceName = siteName === 'Misskey' ? host : siteName;
-export const ui = localStorage.getItem('ui');
-export const debug = localStorage.getItem('debug') === 'true';
diff --git a/src/client/directives/anim.ts b/src/client/directives/anim.ts
deleted file mode 100644
index 1ceef984d8..0000000000
--- a/src/client/directives/anim.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Directive } from 'vue';
-
-export default {
-	beforeMount(src, binding, vn) {
-		src.style.opacity = '0';
-		src.style.transform = 'scale(0.9)';
-		// ページネーションと相性が悪いので
-		//if (typeof binding.value === 'number') src.style.transitionDelay = `${binding.value * 30}ms`;
-		src.classList.add('_zoom');
-	},
-
-	mounted(src, binding, vn) {
-		setTimeout(() => {
-			src.style.opacity = '1';
-			src.style.transform = 'none';
-		}, 1);
-	},
-} as Directive;
diff --git a/src/client/directives/appear.ts b/src/client/directives/appear.ts
deleted file mode 100644
index a504d11ef9..0000000000
--- a/src/client/directives/appear.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Directive } from 'vue';
-
-export default {
-	mounted(src, binding, vn) {
-		const fn = binding.value;
-		if (fn == null) return;
-
-		const observer = new IntersectionObserver(entries => {
-			if (entries.some(entry => entry.isIntersecting)) {
-				fn();
-			}
-		});
-
-		observer.observe(src);
-
-		src._observer_ = observer;
-	},
-
-	unmounted(src, binding, vn) {
-		if (src._observer_) src._observer_.disconnect();
-	}
-} as Directive;
diff --git a/src/client/directives/click-anime.ts b/src/client/directives/click-anime.ts
deleted file mode 100644
index 0d5a6da94e..0000000000
--- a/src/client/directives/click-anime.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Directive } from 'vue';
-import { defaultStore } from '@client/store';
-
-export default {
-	mounted(el, binding, vn) {
-		if (!defaultStore.state.animation) return;
-
-		el.classList.add('_anime_bounce_standBy');
-
-		el.addEventListener('mousedown', () => {
-			el.classList.add('_anime_bounce_standBy');
-			el.classList.add('_anime_bounce_ready');
-
-			el.addEventListener('mouseleave', () => {
-				el.classList.remove('_anime_bounce_ready');
-			});
-		});
-
-		el.addEventListener('click', () => {
-			el.classList.add('_anime_bounce');
-		});
-
-		el.addEventListener('animationend', () => {
-			el.classList.remove('_anime_bounce_ready');
-			el.classList.remove('_anime_bounce');
-			el.classList.add('_anime_bounce_standBy');
-		});
-	}
-} as Directive;
diff --git a/src/client/directives/follow-append.ts b/src/client/directives/follow-append.ts
deleted file mode 100644
index ef3f9af43f..0000000000
--- a/src/client/directives/follow-append.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Directive } from 'vue';
-import { getScrollContainer, getScrollPosition } from '@client/scripts/scroll';
-
-export default {
-	mounted(src, binding, vn) {
-		if (binding.value === false) return;
-
-		let isBottom = true;
-
-		const container = getScrollContainer(src)!;
-		container.addEventListener('scroll', () => {
-			const pos = getScrollPosition(container);
-			const viewHeight = container.clientHeight;
-			const height = container.scrollHeight;
-			isBottom = (pos + viewHeight > height - 32);
-		}, { passive: true });
-		container.scrollTop = container.scrollHeight;
-
-		const ro = new ResizeObserver((entries, observer) => {
-			if (isBottom) {
-				const height = container.scrollHeight;
-				container.scrollTop = height;
-			}
-		});
-
-		ro.observe(src);
-
-		// TODO: 新たにプロパティを作るのをやめMapを使う
-		src._ro_ = ro;
-	},
-
-	unmounted(src, binding, vn) {
-		if (src._ro_) src._ro_.unobserve(src);
-	}
-} as Directive;
diff --git a/src/client/directives/get-size.ts b/src/client/directives/get-size.ts
deleted file mode 100644
index e3b5dea0f3..0000000000
--- a/src/client/directives/get-size.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Directive } from 'vue';
-
-export default {
-	mounted(src, binding, vn) {
-		const calc = () => {
-			const height = src.clientHeight;
-			const width = src.clientWidth;
-
-			// 要素が(一時的に)DOMに存在しないときは計算スキップ
-			if (height === 0) return;
-
-			binding.value(width, height);
-		};
-
-		calc();
-
-		// Vue3では使えなくなった
-		// 無くても大丈夫か...？
-		// TODO: ↑大丈夫じゃなかったので解決策を探す
-		//vn.context.$on('hook:activated', calc);
-
-		const ro = new ResizeObserver((entries, observer) => {
-			calc();
-		});
-		ro.observe(src);
-
-		src._get_size_ro_ = ro;
-	},
-
-	unmounted(src, binding, vn) {
-		binding.value(0, 0);
-		src._get_size_ro_.unobserve(src);
-	}
-} as Directive;
diff --git a/src/client/directives/hotkey.ts b/src/client/directives/hotkey.ts
deleted file mode 100644
index d813a95074..0000000000
--- a/src/client/directives/hotkey.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Directive } from 'vue';
-import { makeHotkey } from '../scripts/hotkey';
-
-export default {
-	mounted(el, binding) {
-		el._hotkey_global = binding.modifiers.global === true;
-
-		el._keyHandler = makeHotkey(binding.value);
-
-		if (el._hotkey_global) {
-			document.addEventListener('keydown', el._keyHandler);
-		} else {
-			el.addEventListener('keydown', el._keyHandler);
-		}
-	},
-
-	unmounted(el) {
-		if (el._hotkey_global) {
-			document.removeEventListener('keydown', el._keyHandler);
-		} else {
-			el.removeEventListener('keydown', el._keyHandler);
-		}
-	}
-} as Directive;
diff --git a/src/client/directives/index.ts b/src/client/directives/index.ts
deleted file mode 100644
index cd71bc26d3..0000000000
--- a/src/client/directives/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { App } from 'vue';
-
-import userPreview from './user-preview';
-import size from './size';
-import getSize from './get-size';
-import particle from './particle';
-import tooltip from './tooltip';
-import hotkey from './hotkey';
-import appear from './appear';
-import anim from './anim';
-import stickyContainer from './sticky-container';
-import clickAnime from './click-anime';
-
-export default function(app: App) {
-	app.directive('userPreview', userPreview);
-	app.directive('user-preview', userPreview);
-	app.directive('size', size);
-	app.directive('get-size', getSize);
-	app.directive('particle', particle);
-	app.directive('tooltip', tooltip);
-	app.directive('hotkey', hotkey);
-	app.directive('appear', appear);
-	app.directive('anim', anim);
-	app.directive('click-anime', clickAnime);
-	app.directive('sticky-container', stickyContainer);
-}
diff --git a/src/client/directives/particle.ts b/src/client/directives/particle.ts
deleted file mode 100644
index 1676e1182e..0000000000
--- a/src/client/directives/particle.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import Particle from '@client/components/particle.vue';
-import { popup } from '@client/os';
-
-export default {
-	mounted(el, binding, vn) {
-		// 明示的に false であればバインドしない
-		if (binding.value === false) return;
-
-		el.addEventListener('click', () => {
-			const rect = el.getBoundingClientRect();
-
-			const x = rect.left + (el.clientWidth / 2);
-			const y = rect.top + (el.clientHeight / 2);
-
-			popup(Particle, { x, y }, {}, 'end');
-		});
-	}
-};
diff --git a/src/client/directives/size.ts b/src/client/directives/size.ts
deleted file mode 100644
index a72a97abcc..0000000000
--- a/src/client/directives/size.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { Directive } from 'vue';
-
-//const observers = new Map<Element, ResizeObserver>();
-
-export default {
-	mounted(src, binding, vn) {
-		const query = binding.value;
-
-		const addClass = (el: Element, cls: string) => {
-			el.classList.add(cls);
-		};
-
-		const removeClass = (el: Element, cls: string) => {
-			el.classList.remove(cls);
-		};
-
-		const calc = () => {
-			const width = src.clientWidth;
-
-			// 要素が(一時的に)DOMに存在しないときは計算スキップ
-			if (width === 0) return;
-
-			if (query.max) {
-				for (const v of query.max) {
-					if (width <= v) {
-						addClass(src, 'max-width_' + v + 'px');
-					} else {
-						removeClass(src, 'max-width_' + v + 'px');
-					}
-				}
-			}
-			if (query.min) {
-				for (const v of query.min) {
-					if (width >= v) {
-						addClass(src, 'min-width_' + v + 'px');
-					} else {
-						removeClass(src, 'min-width_' + v + 'px');
-					}
-				}
-			}
-		};
-
-		calc();
-
-		window.addEventListener('resize', calc);
-
-		// Vue3では使えなくなった
-		// 無くても大丈夫か...？
-		// TODO: ↑大丈夫じゃなかったので解決策を探す
-		//vn.context.$on('hook:activated', calc);
-
-		//const ro = new ResizeObserver((entries, observer) => {
-		//	calc();
-		//});
-
-		//ro.observe(el);
-
-		// TODO: 新たにプロパティを作るのをやめMapを使う
-		// ただメモリ的には↓の方が省メモリかもしれないので検討中
-		//el._ro_ = ro;
-		src._calc_ = calc;
-	},
-
-	unmounted(src, binding, vn) {
-		//el._ro_.unobserve(el);
-		window.removeEventListener('resize', src._calc_);
-	}
-} as Directive;
diff --git a/src/client/directives/sticky-container.ts b/src/client/directives/sticky-container.ts
deleted file mode 100644
index 9610eba4da..0000000000
--- a/src/client/directives/sticky-container.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Directive } from 'vue';
-
-export default {
-	mounted(src, binding, vn) {
-		//const query = binding.value;
-
-		const header = src.children[0];
-		const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px';
-		src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
-		header.style.setProperty('--stickyTop', currentStickyTop);
-		header.style.position = 'sticky';
-		header.style.top = 'var(--stickyTop)';
-		header.style.zIndex = '1';
-	},
-} as Directive;
diff --git a/src/client/directives/tooltip.ts b/src/client/directives/tooltip.ts
deleted file mode 100644
index 32d137b2e2..0000000000
--- a/src/client/directives/tooltip.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Directive, ref } from 'vue';
-import { isDeviceTouch } from '@client/scripts/is-device-touch';
-import { popup, dialog } from '@client/os';
-
-const start = isDeviceTouch ? 'touchstart' : 'mouseover';
-const end = isDeviceTouch ? 'touchend' : 'mouseleave';
-const delay = 100;
-
-export default {
-	mounted(el: HTMLElement, binding, vn) {
-		const self = (el as any)._tooltipDirective_ = {} as any;
-
-		self.text = binding.value as string;
-		self._close = null;
-		self.showTimer = null;
-		self.hideTimer = null;
-		self.checkTimer = null;
-
-		self.close = () => {
-			if (self._close) {
-				clearInterval(self.checkTimer);
-				self._close();
-				self._close = null;
-			}
-		};
-
-		if (binding.arg === 'dialog') {
-			el.addEventListener('click', (ev) => {
-				ev.preventDefault();
-				ev.stopPropagation();
-				dialog({
-					type: 'info',
-					text: binding.value,
-				});
-				return false;
-			});
-		}
-
-		self.show = () => {
-			if (!document.body.contains(el)) return;
-			if (self._close) return;
-			if (self.text == null) return;
-
-			const showing = ref(true);
-			popup(import('@client/components/ui/tooltip.vue'), {
-				showing,
-				text: self.text,
-				source: el
-			}, {}, 'closed');
-
-			self._close = () => {
-				showing.value = false;
-			};
-		};
-
-		el.addEventListener('selectstart', e => {
-			e.preventDefault();
-		});
-
-		el.addEventListener(start, () => {
-			clearTimeout(self.showTimer);
-			clearTimeout(self.hideTimer);
-			self.showTimer = setTimeout(self.show, delay);
-		}, { passive: true });
-
-		el.addEventListener(end, () => {
-			clearTimeout(self.showTimer);
-			clearTimeout(self.hideTimer);
-			self.hideTimer = setTimeout(self.close, delay);
-		}, { passive: true });
-
-		el.addEventListener('click', () => {
-			clearTimeout(self.showTimer);
-			self.close();
-		});
-	},
-
-	updated(el, binding) {
-		const self = el._tooltipDirective_;
-		self.text = binding.value as string;
-	},
-
-	unmounted(el, binding, vn) {
-		const self = el._tooltipDirective_;
-		clearInterval(self.checkTimer);
-	},
-} as Directive;
diff --git a/src/client/directives/user-preview.ts b/src/client/directives/user-preview.ts
deleted file mode 100644
index 26d6c5efb3..0000000000
--- a/src/client/directives/user-preview.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { Directive, ref } from 'vue';
-import autobind from 'autobind-decorator';
-import { popup } from '@client/os';
-
-export class UserPreview {
-	private el;
-	private user;
-	private showTimer;
-	private hideTimer;
-	private checkTimer;
-	private promise;
-
-	constructor(el, user) {
-		this.el = el;
-		this.user = user;
-
-		this.attach();
-	}
-
-	@autobind
-	private show() {
-		if (!document.body.contains(this.el)) return;
-		if (this.promise) return;
-
-		const showing = ref(true);
-
-		popup(import('@client/components/user-preview.vue'), {
-			showing,
-			q: this.user,
-			source: this.el
-		}, {
-			mouseover: () => {
-				clearTimeout(this.hideTimer);
-			},
-			mouseleave: () => {
-				clearTimeout(this.showTimer);
-				this.hideTimer = setTimeout(this.close, 500);
-			},
-		}, 'closed');
-
-		this.promise = {
-			cancel: () => {
-				showing.value = false;
-			}
-		};
-
-		this.checkTimer = setInterval(() => {
-			if (!document.body.contains(this.el)) {
-				clearTimeout(this.showTimer);
-				clearTimeout(this.hideTimer);
-				this.close();
-			}
-		}, 1000);
-	}
-
-	@autobind
-	private close() {
-		if (this.promise) {
-			clearInterval(this.checkTimer);
-			this.promise.cancel();
-			this.promise = null;
-		}
-	}
-
-	@autobind
-	private onMouseover() {
-		clearTimeout(this.showTimer);
-		clearTimeout(this.hideTimer);
-		this.showTimer = setTimeout(this.show, 500);
-	}
-
-	@autobind
-	private onMouseleave() {
-		clearTimeout(this.showTimer);
-		clearTimeout(this.hideTimer);
-		this.hideTimer = setTimeout(this.close, 500);
-	}
-
-	@autobind
-	private onClick() {
-		clearTimeout(this.showTimer);
-		this.close();
-	}
-
-	@autobind
-	public attach() {
-		this.el.addEventListener('mouseover', this.onMouseover);
-		this.el.addEventListener('mouseleave', this.onMouseleave);
-		this.el.addEventListener('click', this.onClick);
-	}
-
-	@autobind
-	public detach() {
-		this.el.removeEventListener('mouseover', this.onMouseover);
-		this.el.removeEventListener('mouseleave', this.onMouseleave);
-		this.el.removeEventListener('click', this.onClick);
-		clearInterval(this.checkTimer);
-	}
-}
-
-export default {
-	mounted(el: HTMLElement, binding, vn) {
-		if (binding.value == null) return;
-
-		// TODO: 新たにプロパティを作るのをやめMapを使う
-		// ただメモリ的には↓の方が省メモリかもしれないので検討中
-		const self = (el as any)._userPreviewDirective_ = {} as any;
-
-		self.preview = new UserPreview(el, binding.value);
-	},
-
-	unmounted(el, binding, vn) {
-		if (binding.value == null) return;
-
-		const self = el._userPreviewDirective_;
-		self.preview.detach();
-	}
-} as Directive;
diff --git a/src/client/events.ts b/src/client/events.ts
deleted file mode 100644
index dbbd908b8f..0000000000
--- a/src/client/events.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { EventEmitter } from 'eventemitter3';
-
-// TODO: 型付け
-export const globalEvents = new EventEmitter();
diff --git a/src/client/filters/bytes.ts b/src/client/filters/bytes.ts
deleted file mode 100644
index 50e63534b6..0000000000
--- a/src/client/filters/bytes.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export default (v, digits = 0) => {
-	if (v == null) return '?';
-	const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
-	if (v == 0) return '0';
-	const isMinus = v < 0;
-	if (isMinus) v = -v;
-	const i = Math.floor(Math.log(v) / Math.log(1024));
-	return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
-};
diff --git a/src/client/filters/note.ts b/src/client/filters/note.ts
deleted file mode 100644
index 5c000cf83b..0000000000
--- a/src/client/filters/note.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default note => {
-	return `/notes/${note.id}`;
-};
diff --git a/src/client/filters/number.ts b/src/client/filters/number.ts
deleted file mode 100644
index 880a848ca4..0000000000
--- a/src/client/filters/number.ts
+++ /dev/null
@@ -1 +0,0 @@
-export default n => n == null ? 'N/A' : n.toLocaleString();
diff --git a/src/client/filters/user.ts b/src/client/filters/user.ts
deleted file mode 100644
index f432de739d..0000000000
--- a/src/client/filters/user.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { getAcct } from '@/misc/acct';
-import getUserName from '@/misc/get-user-name';
-import { url } from '@client/config';
-
-export const acct = user => {
-	return getAcct(user);
-};
-
-export const userName = user => {
-	return getUserName(user);
-};
-
-export const userPage = (user, path?, absolute = false) => {
-	return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
-};
diff --git a/src/client/i18n.ts b/src/client/i18n.ts
deleted file mode 100644
index dc23676474..0000000000
--- a/src/client/i18n.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { markRaw } from 'vue';
-import { locale } from '@client/config';
-import { I18n } from '@/misc/i18n';
-
-export const i18n = markRaw(new I18n(locale));
-
-// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
-declare module '@vue/runtime-core' {
-	interface ComponentCustomProperties {
-		$t: typeof i18n['t'];
-		$ts: typeof i18n['locale'];
-	}
-}
diff --git a/src/client/init.ts b/src/client/init.ts
deleted file mode 100644
index 81d17f0d27..0000000000
--- a/src/client/init.ts
+++ /dev/null
@@ -1,420 +0,0 @@
-/**
- * Client entry point
- */
-
-import '@client/style.scss';
-
-//#region account indexedDB migration
-import { set } from '@client/scripts/idb-proxy';
-
-if (localStorage.getItem('accounts') != null) {
-	set('accounts', JSON.parse(localStorage.getItem('accounts')));
-	localStorage.removeItem('accounts');
-}
-//#endregion
-
-import * as Sentry from '@sentry/browser';
-import { Integrations } from '@sentry/tracing';
-import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue';
-import compareVersions from 'compare-versions';
-
-import widgets from '@client/widgets';
-import directives from '@client/directives';
-import components from '@client/components';
-import { version, ui, lang, host } from '@client/config';
-import { router } from '@client/router';
-import { applyTheme } from '@client/scripts/theme';
-import { isDeviceDarkmode } from '@client/scripts/is-device-darkmode';
-import { i18n } from '@client/i18n';
-import { stream, dialog, post, popup } from '@client/os';
-import * as sound from '@client/scripts/sound';
-import { $i, refreshAccount, login, updateAccount, signout } from '@client/account';
-import { defaultStore, ColdDeviceStorage } from '@client/store';
-import { fetchInstance, instance } from '@client/instance';
-import { makeHotkey } from '@client/scripts/hotkey';
-import { search } from '@client/scripts/search';
-import { isMobile } from '@client/scripts/is-mobile';
-import { initializeSw } from '@client/scripts/initialize-sw';
-import { reloadChannel } from '@client/scripts/unison-reload';
-import { reactionPicker } from '@client/scripts/reaction-picker';
-import { getUrlWithoutLoginId } from '@client/scripts/login-id';
-import { getAccountFromId } from '@client/scripts/get-account-from-id';
-
-console.info(`Misskey v${version}`);
-
-// boot.jsのやつを解除
-window.onerror = null;
-window.onunhandledrejection = null;
-
-if (_DEV_) {
-	console.warn('Development mode!!!');
-
-	console.info(`vue ${vueVersion}`);
-
-	(window as any).$i = $i;
-	(window as any).$store = defaultStore;
-
-	window.addEventListener('error', event => {
-		console.error(event);
-		/*
-		dialog({
-			type: 'error',
-			title: 'DEV: Unhandled error',
-			text: event.message
-		});
-		*/
-	});
-
-	window.addEventListener('unhandledrejection', event => {
-		console.error(event);
-		/*
-		dialog({
-			type: 'error',
-			title: 'DEV: Unhandled promise rejection',
-			text: event.reason
-		});
-		*/
-	});
-}
-
-if (defaultStore.state.reportError && !_DEV_) {
-	Sentry.init({
-		dsn: 'https://fd273254a07a4b61857607a9ea05d629@o501808.ingest.sentry.io/5583438',
-		tracesSampleRate: 1.0,
-	});
-
-	Sentry.setTag('misskey_version', version);
-	Sentry.setTag('ui', ui);
-	Sentry.setTag('lang', lang);
-	Sentry.setTag('host', host);
-}
-
-// タッチデバイスでCSSの:hoverを機能させる
-document.addEventListener('touchend', () => {}, { passive: true });
-
-// 一斉リロード
-reloadChannel.addEventListener('message', path => {
-	if (path !== null) location.href = path;
-	else location.reload();
-});
-
-//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-// TODO: いつの日にか消したい
-const vh = window.innerHeight * 0.01;
-document.documentElement.style.setProperty('--vh', `${vh}px`);
-window.addEventListener('resize', () => {
-	const vh = window.innerHeight * 0.01;
-	document.documentElement.style.setProperty('--vh', `${vh}px`);
-});
-//#endregion
-
-// If mobile, insert the viewport meta tag
-if (isMobile || window.innerWidth <= 1024) {
-	const viewport = document.getElementsByName('viewport').item(0);
-	viewport.setAttribute('content',
-		`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
-	document.head.appendChild(viewport);
-}
-
-//#region Set lang attr
-const html = document.documentElement;
-html.setAttribute('lang', lang);
-//#endregion
-
-//#region loginId
-const params = new URLSearchParams(location.search);
-const loginId = params.get('loginId');
-
-if (loginId) {
-	const target = getUrlWithoutLoginId(location.href);
-
-	if (!$i || $i.id !== loginId) {
-		const account = await getAccountFromId(loginId);
-		if (account) {
-			await login(account.token, target);
-		}
-	}
-
-	history.replaceState({ misskey: 'loginId' }, '', target);
-}
-
-//#endregion
-
-//#region Fetch user
-if ($i && $i.token) {
-	if (_DEV_) {
-		console.log('account cache found. refreshing...');
-	}
-
-	refreshAccount();
-} else {
-	if (_DEV_) {
-		console.log('no account cache found.');
-	}
-
-	// 連携ログインの場合用にCookieを参照する
-	const i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1];
-
-	if (i != null && i !== 'null') {
-		if (_DEV_) {
-			console.log('signing...');
-		}
-
-		try {
-			document.body.innerHTML = '<div>Please wait...</div>';
-			await login(i);
-			location.reload();
-		} catch (e) {
-			// Render the error screen
-			// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
-			document.body.innerHTML = '<div id="err">Oops!</div>';
-		}
-	} else {
-		if (_DEV_) {
-			console.log('not signed in');
-		}
-	}
-}
-//#endregion
-
-fetchInstance().then(() => {
-	localStorage.setItem('v', instance.version);
-
-	// Init service worker
-	initializeSw();
-});
-
-const app = createApp(await (
-	window.location.search === '?zen' ? import('@client/ui/zen.vue') :
-	!$i                               ? import('@client/ui/visitor.vue') :
-	ui === 'deck'                     ? import('@client/ui/deck.vue') :
-	ui === 'desktop'                  ? import('@client/ui/desktop.vue') :
-	ui === 'chat'                     ? import('@client/ui/chat/index.vue') :
-	ui === 'classic'                  ? import('@client/ui/classic.vue') :
-	import('@client/ui/universal.vue')
-).then(x => x.default));
-
-if (_DEV_) {
-	app.config.performance = true;
-}
-
-app.config.globalProperties = {
-	$i,
-	$store: defaultStore,
-	$instance: instance,
-	$t: i18n.t,
-	$ts: i18n.locale,
-};
-
-app.use(router);
-
-widgets(app);
-directives(app);
-components(app);
-
-await router.isReady();
-
-const splash = document.getElementById('splash');
-// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
-if (splash) splash.addEventListener('transitionend', () => {
-	splash.remove();
-});
-
-const rootEl = document.createElement('div');
-document.body.appendChild(rootEl);
-app.mount(rootEl);
-
-reactionPicker.init();
-
-if (splash) {
-	splash.style.opacity = '0';
-	splash.style.pointerEvents = 'none';
-}
-
-// クライアントが更新されたか？
-const lastVersion = localStorage.getItem('lastVersion');
-if (lastVersion !== version) {
-	localStorage.setItem('lastVersion', version);
-
-	// テーマリビルドするため
-	localStorage.removeItem('theme');
-
-	try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
-		if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
-			// ログインしてる場合だけ
-			if ($i) {
-				popup(import('@client/components/updated.vue'), {}, {}, 'closed');
-			}
-		}
-	} catch (e) {
-	}
-}
-
-// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
-watch(defaultStore.reactiveState.darkMode, (darkMode) => {
-	applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
-}, { immediate: localStorage.theme == null });
-
-const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
-const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
-
-watch(darkTheme, (theme) => {
-	if (defaultStore.state.darkMode) {
-		applyTheme(theme);
-	}
-});
-
-watch(lightTheme, (theme) => {
-	if (!defaultStore.state.darkMode) {
-		applyTheme(theme);
-	}
-});
-
-//#region Sync dark mode
-if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-	defaultStore.set('darkMode', isDeviceDarkmode());
-}
-
-window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
-	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-		defaultStore.set('darkMode', mql.matches);
-	}
-});
-//#endregion
-
-// shortcut
-document.addEventListener('keydown', makeHotkey({
-	'd': () => {
-		defaultStore.set('darkMode', !defaultStore.state.darkMode);
-	},
-	'p|n': post,
-	's': search,
-	//TODO: 'h|/': help
-}));
-
-watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
-	document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
-}, { immediate: true });
-
-watch(defaultStore.reactiveState.useBlurEffect, v => {
-	if (v) {
-		document.documentElement.style.removeProperty('--blur');
-	} else {
-		document.documentElement.style.setProperty('--blur', 'none');
-	}
-}, { immediate: true });
-
-let reloadDialogShowing = false;
-stream.on('_disconnected_', async () => {
-	if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
-		location.reload();
-	} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
-		if (reloadDialogShowing) return;
-		reloadDialogShowing = true;
-		const { canceled } = await dialog({
-			type: 'warning',
-			title: i18n.locale.disconnectedFromServer,
-			text: i18n.locale.reloadConfirm,
-			showCancelButton: true
-		});
-		reloadDialogShowing = false;
-		if (!canceled) {
-			location.reload();
-		}
-	}
-});
-
-stream.on('emojiAdded', data => {
-	// TODO
-	//store.commit('instance/set', );
-});
-
-for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
-	import('./plugin').then(({ install }) => {
-		install(plugin);
-	});
-}
-
-if ($i) {
-	if ($i.isDeleted) {
-		dialog({
-			type: 'warning',
-			text: i18n.locale.accountDeletionInProgress,
-		});
-	}
-
-	if ('Notification' in window) {
-		// 許可を得ていなかったらリクエスト
-		if (Notification.permission === 'default') {
-			Notification.requestPermission();
-		}
-	}
-
-	const main = markRaw(stream.useChannel('main', null, 'System'));
-
-	// 自分の情報が更新されたとき
-	main.on('meUpdated', i => {
-		updateAccount(i);
-	});
-
-	main.on('readAllNotifications', () => {
-		updateAccount({ hasUnreadNotification: false });
-	});
-
-	main.on('unreadNotification', () => {
-		updateAccount({ hasUnreadNotification: true });
-	});
-
-	main.on('unreadMention', () => {
-		updateAccount({ hasUnreadMentions: true });
-	});
-
-	main.on('readAllUnreadMentions', () => {
-		updateAccount({ hasUnreadMentions: false });
-	});
-
-	main.on('unreadSpecifiedNote', () => {
-		updateAccount({ hasUnreadSpecifiedNotes: true });
-	});
-
-	main.on('readAllUnreadSpecifiedNotes', () => {
-		updateAccount({ hasUnreadSpecifiedNotes: false });
-	});
-
-	main.on('readAllMessagingMessages', () => {
-		updateAccount({ hasUnreadMessagingMessage: false });
-	});
-
-	main.on('unreadMessagingMessage', () => {
-		updateAccount({ hasUnreadMessagingMessage: true });
-		sound.play('chatBg');
-	});
-
-	main.on('readAllAntennas', () => {
-		updateAccount({ hasUnreadAntenna: false });
-	});
-
-	main.on('unreadAntenna', () => {
-		updateAccount({ hasUnreadAntenna: true });
-		sound.play('antenna');
-	});
-
-	main.on('readAllAnnouncements', () => {
-		updateAccount({ hasUnreadAnnouncement: false });
-	});
-
-	main.on('readAllChannels', () => {
-		updateAccount({ hasUnreadChannel: false });
-	});
-
-	main.on('unreadChannel', () => {
-		updateAccount({ hasUnreadChannel: true });
-		sound.play('channel');
-	});
-
-	// トークンが再生成されたとき
-	// このままではMisskeyが利用できないので強制的にサインアウトさせる
-	main.on('myTokenRegenerated', () => {
-		signout();
-	});
-}
diff --git a/src/client/instance.ts b/src/client/instance.ts
deleted file mode 100644
index 6e912aa2e5..0000000000
--- a/src/client/instance.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { computed, reactive } from 'vue';
-import * as Misskey from 'misskey-js';
-import { api } from './os';
-
-// TODO: 他のタブと永続化されたstateを同期
-
-const data = localStorage.getItem('instance');
-
-// TODO: instanceをリアクティブにするかは再考の余地あり
-
-export const instance: Misskey.entities.InstanceMetadata = reactive(data ? JSON.parse(data) : {
-	// TODO: set default values
-});
-
-export async function fetchInstance() {
-	const meta = await api('meta', {
-		detail: false
-	});
-
-	for (const [k, v] of Object.entries(meta)) {
-		instance[k] = v;
-	}
-
-	localStorage.setItem('instance', JSON.stringify(instance));
-}
-
-export const emojiCategories = computed(() => {
-	if (instance.emojis == null) return [];
-	const categories = new Set();
-	for (const emoji of instance.emojis) {
-		categories.add(emoji.category);
-	}
-	return Array.from(categories);
-});
-
-export const emojiTags = computed(() => {
-	if (instance.emojis == null) return [];
-	const tags = new Set();
-	for (const emoji of instance.emojis) {
-		for (const tag of emoji.aliases) {
-			tags.add(tag);
-		}
-	}
-	return Array.from(tags);
-});
-
-// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
-declare module '@vue/runtime-core' {
-	interface ComponentCustomProperties {
-		$instance: typeof instance;
-	}
-}
diff --git a/src/client/menu.ts b/src/client/menu.ts
deleted file mode 100644
index c884772a47..0000000000
--- a/src/client/menu.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import { computed, ref } from 'vue';
-import { search } from '@client/scripts/search';
-import * as os from '@client/os';
-import { i18n } from '@client/i18n';
-import { ui } from '@client/config';
-import { $i } from './account';
-import { unisonReload } from '@client/scripts/unison-reload';
-import { router } from './router';
-
-export const menuDef = {
-	notifications: {
-		title: 'notifications',
-		icon: 'fas fa-bell',
-		show: computed(() => $i != null),
-		indicated: computed(() => $i != null && $i.hasUnreadNotification),
-		to: '/my/notifications',
-	},
-	messaging: {
-		title: 'messaging',
-		icon: 'fas fa-comments',
-		show: computed(() => $i != null),
-		indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage),
-		to: '/my/messaging',
-	},
-	drive: {
-		title: 'drive',
-		icon: 'fas fa-cloud',
-		show: computed(() => $i != null),
-		to: '/my/drive',
-	},
-	followRequests: {
-		title: 'followRequests',
-		icon: 'fas fa-user-clock',
-		show: computed(() => $i != null && $i.isLocked),
-		indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
-		to: '/my/follow-requests',
-	},
-	featured: {
-		title: 'featured',
-		icon: 'fas fa-fire-alt',
-		to: '/featured',
-	},
-	explore: {
-		title: 'explore',
-		icon: 'fas fa-hashtag',
-		to: '/explore',
-	},
-	announcements: {
-		title: 'announcements',
-		icon: 'fas fa-broadcast-tower',
-		indicated: computed(() => $i != null && $i.hasUnreadAnnouncement),
-		to: '/announcements',
-	},
-	search: {
-		title: 'search',
-		icon: 'fas fa-search',
-		action: () => search(),
-	},
-	lists: {
-		title: 'lists',
-		icon: 'fas fa-list-ul',
-		show: computed(() => $i != null),
-		active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')),
-		action: (ev) => {
-			const items = ref([{
-				type: 'pending'
-			}]);
-			os.api('users/lists/list').then(lists => {
-				const _items = [...lists.map(list => ({
-					type: 'link',
-					text: list.name,
-					to: `/timeline/list/${list.id}`
-				})), null, {
-					type: 'link',
-					to: '/my/lists',
-					text: i18n.locale.manageLists,
-					icon: 'fas fa-cog',
-				}];
-				items.value = _items;
-			});
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		},
-	},
-	groups: {
-		title: 'groups',
-		icon: 'fas fa-users',
-		show: computed(() => $i != null),
-		to: '/my/groups',
-	},
-	antennas: {
-		title: 'antennas',
-		icon: 'fas fa-satellite',
-		show: computed(() => $i != null),
-		active: computed(() => router.currentRoute.value.path.startsWith('/timeline/antenna/') || router.currentRoute.value.path === '/my/antennas' || router.currentRoute.value.path.startsWith('/my/antennas/')),
-		action: (ev) => {
-			const items = ref([{
-				type: 'pending'
-			}]);
-			os.api('antennas/list').then(antennas => {
-				const _items = [...antennas.map(antenna => ({
-					type: 'link',
-					text: antenna.name,
-					to: `/timeline/antenna/${antenna.id}`
-				})), null, {
-					type: 'link',
-					to: '/my/antennas',
-					text: i18n.locale.manageAntennas,
-					icon: 'fas fa-cog',
-				}];
-				items.value = _items;
-			});
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		},
-	},
-	mentions: {
-		title: 'mentions',
-		icon: 'fas fa-at',
-		show: computed(() => $i != null),
-		indicated: computed(() => $i != null && $i.hasUnreadMentions),
-		to: '/my/mentions',
-	},
-	messages: {
-		title: 'directNotes',
-		icon: 'fas fa-envelope',
-		show: computed(() => $i != null),
-		indicated: computed(() => $i != null && $i.hasUnreadSpecifiedNotes),
-		to: '/my/messages',
-	},
-	favorites: {
-		title: 'favorites',
-		icon: 'fas fa-star',
-		show: computed(() => $i != null),
-		to: '/my/favorites',
-	},
-	pages: {
-		title: 'pages',
-		icon: 'fas fa-file-alt',
-		to: '/pages',
-	},
-	gallery: {
-		title: 'gallery',
-		icon: 'fas fa-icons',
-		to: '/gallery',
-	},
-	clips: {
-		title: 'clip',
-		icon: 'fas fa-paperclip',
-		show: computed(() => $i != null),
-		to: '/my/clips',
-	},
-	channels: {
-		title: 'channel',
-		icon: 'fas fa-satellite-dish',
-		to: '/channels',
-	},
-	federation: {
-		title: 'federation',
-		icon: 'fas fa-globe',
-		to: '/federation',
-	},
-	emojis: {
-		title: 'emojis',
-		icon: 'fas fa-laugh',
-		to: '/emojis',
-	},
-	games: {
-		title: 'games',
-		icon: 'fas fa-gamepad',
-		to: '/games/reversi',
-	},
-	scratchpad: {
-		title: 'scratchpad',
-		icon: 'fas fa-terminal',
-		to: '/scratchpad',
-	},
-	rooms: {
-		title: 'rooms',
-		icon: 'fas fa-door-closed',
-		show: computed(() => $i != null),
-		to: computed(() => `/@${$i.username}/room`),
-	},
-	ui: {
-		title: 'switchUi',
-		icon: 'fas fa-columns',
-		action: (ev) => {
-			os.popupMenu([{
-				text: i18n.locale.default,
-				active: ui === 'default' || ui === null,
-				action: () => {
-					localStorage.setItem('ui', 'default');
-					unisonReload();
-				}
-			}, {
-				text: i18n.locale.deck,
-				active: ui === 'deck',
-				action: () => {
-					localStorage.setItem('ui', 'deck');
-					unisonReload();
-				}
-			}, {
-				text: i18n.locale.classic,
-				active: ui === 'classic',
-				action: () => {
-					localStorage.setItem('ui', 'classic');
-					unisonReload();
-				}
-			}, {
-				text: 'Chat (β)',
-				active: ui === 'chat',
-				action: () => {
-					localStorage.setItem('ui', 'chat');
-					unisonReload();
-				}
-			}, /*{
-				text: i18n.locale.desktop + ' (β)',
-				active: ui === 'desktop',
-				action: () => {
-					localStorage.setItem('ui', 'desktop');
-					unisonReload();
-				}
-			}*/], ev.currentTarget || ev.target);
-		},
-	},
-};
diff --git a/src/client/os.ts b/src/client/os.ts
deleted file mode 100644
index 743d2d131f..0000000000
--- a/src/client/os.ts
+++ /dev/null
@@ -1,501 +0,0 @@
-// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
-
-import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
-import { EventEmitter } from 'eventemitter3';
-import insertTextAtCursor from 'insert-text-at-cursor';
-import * as Misskey from 'misskey-js';
-import * as Sentry from '@sentry/browser';
-import { apiUrl, debug, url } from '@client/config';
-import MkPostFormDialog from '@client/components/post-form-dialog.vue';
-import MkWaitingDialog from '@client/components/waiting-dialog.vue';
-import { resolve } from '@client/router';
-import { $i } from '@client/account';
-import { defaultStore } from '@client/store';
-
-export const stream = markRaw(new Misskey.Stream(url, $i));
-
-export const pendingApiRequestsCount = ref(0);
-let apiRequestsCount = 0; // for debug
-export const apiRequests = ref([]); // for debug
-
-export const windows = new Map();
-
-const apiClient = new Misskey.api.APIClient({
-	origin: url,
-});
-
-export const api = ((endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) => {
-	pendingApiRequestsCount.value++;
-
-	const onFinally = () => {
-		pendingApiRequestsCount.value--;
-	};
-
-	const log = debug ? reactive({
-		id: ++apiRequestsCount,
-		endpoint,
-		req: markRaw(data),
-		res: null,
-		state: 'pending',
-	}) : null;
-	if (debug) {
-		apiRequests.value.push(log);
-		if (apiRequests.value.length > 128) apiRequests.value.shift();
-	}
-
-	const promise = new Promise((resolve, reject) => {
-		// Append a credential
-		if ($i) (data as any).i = $i.token;
-		if (token !== undefined) (data as any).i = token;
-
-		// Send request
-		fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
-			method: 'POST',
-			body: JSON.stringify(data),
-			credentials: 'omit',
-			cache: 'no-cache'
-		}).then(async (res) => {
-			const body = res.status === 204 ? null : await res.json();
-
-			if (res.status === 200) {
-				resolve(body);
-				if (debug) {
-					log!.res = markRaw(JSON.parse(JSON.stringify(body)));
-					log!.state = 'success';
-				}
-			} else if (res.status === 204) {
-				resolve();
-				if (debug) {
-					log!.state = 'success';
-				}
-			} else {
-				reject(body.error);
-				if (debug) {
-					log!.res = markRaw(body.error);
-					log!.state = 'failed';
-				}
-
-				if (defaultStore.state.reportError && !_DEV_) {
-					Sentry.withScope((scope) => {
-						scope.setTag('api_endpoint', endpoint);
-						scope.setContext('api params', data);
-						scope.setContext('api error info', body.info);
-						scope.setTag('api_error_id', body.id);
-						scope.setTag('api_error_code', body.code);
-						scope.setTag('api_error_kind', body.kind);
-						scope.setLevel(Sentry.Severity.Error);
-						Sentry.captureMessage('API error');
-					});
-				}
-			}
-		}).catch(reject);
-	});
-
-	promise.then(onFinally, onFinally);
-
-	return promise;
-}) as typeof apiClient.request;
-
-export const apiWithDialog = ((
-	endpoint: string,
-	data: Record<string, any> = {},
-	token?: string | null | undefined,
-) => {
-	const promise = api(endpoint, data, token);
-	promiseDialog(promise, null, (e) => {
-		dialog({
-			type: 'error',
-			text: e.message + '\n' + (e as any).id,
-		});
-	});
-
-	return promise;
-}) as typeof api;
-
-export function promiseDialog<T extends Promise<any>>(
-	promise: T,
-	onSuccess?: ((res: any) => void) | null,
-	onFailure?: ((e: Error) => void) | null,
-	text?: string,
-): T {
-	const showing = ref(true);
-	const success = ref(false);
-
-	promise.then(res => {
-		if (onSuccess) {
-			showing.value = false;
-			onSuccess(res);
-		} else {
-			success.value = true;
-			setTimeout(() => {
-				showing.value = false;
-			}, 1000);
-		}
-	}).catch(e => {
-		showing.value = false;
-		if (onFailure) {
-			onFailure(e);
-		} else {
-			dialog({
-				type: 'error',
-				text: e
-			});
-		}
-	});
-
-	// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
-	popup(MkWaitingDialog, {
-		success: success,
-		showing: showing,
-		text: text,
-	}, {}, 'closed');
-
-	return promise;
-}
-
-function isModule(x: any): x is typeof import('*.vue') {
-	return x.default != null;
-}
-
-let popupIdCount = 0;
-export const popups = ref([]) as Ref<{
-	id: any;
-	component: any;
-	props: Record<string, any>;
-}[]>;
-
-export async function popup(component: Component | typeof import('*.vue') | Promise<Component | typeof import('*.vue')>, props: Record<string, any>, events = {}, disposeEvent?: string) {
-	if (component.then) component = await component;
-
-	if (isModule(component)) component = component.default;
-	markRaw(component);
-
-	const id = ++popupIdCount;
-	const dispose = () => {
-		if (_DEV_) console.log('os:popup close', id, component, props, events);
-		// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ？
-		setTimeout(() => {
-			popups.value = popups.value.filter(popup => popup.id !== id);
-		}, 0);
-	};
-	const state = {
-		component,
-		props,
-		events: disposeEvent ? {
-			...events,
-			[disposeEvent]: dispose
-		} : events,
-		id,
-	};
-
-	if (_DEV_) console.log('os:popup open', id, component, props, events);
-	popups.value.push(state);
-
-	return {
-		dispose,
-	};
-}
-
-export function pageWindow(path: string) {
-	const { component, props } = resolve(path);
-	popup(import('@client/components/page-window.vue'), {
-		initialPath: path,
-		initialComponent: markRaw(component),
-		initialProps: props,
-	}, {}, 'closed');
-}
-
-export function modalPageWindow(path: string) {
-	const { component, props } = resolve(path);
-	popup(import('@client/components/modal-page-window.vue'), {
-		initialPath: path,
-		initialComponent: markRaw(component),
-		initialProps: props,
-	}, {}, 'closed');
-}
-
-export function dialog(props: {
-	type: 'error' | 'info' | 'success' | 'warning' | 'waiting';
-	title?: string | null;
-	text?: string | null;
-}) {
-	return new Promise((resolve, reject) => {
-		popup(import('@client/components/dialog.vue'), props, {
-			done: result => {
-				resolve(result ? result : { canceled: true });
-			},
-		}, 'closed');
-	});
-}
-
-export function success() {
-	return new Promise((resolve, reject) => {
-		const showing = ref(true);
-		setTimeout(() => {
-			showing.value = false;
-		}, 1000);
-		popup(import('@client/components/waiting-dialog.vue'), {
-			success: true,
-			showing: showing
-		}, {
-			done: () => resolve(),
-		}, 'closed');
-	});
-}
-
-export function waiting() {
-	return new Promise((resolve, reject) => {
-		const showing = ref(true);
-		popup(import('@client/components/waiting-dialog.vue'), {
-			success: false,
-			showing: showing
-		}, {
-			done: () => resolve(),
-		}, 'closed');
-	});
-}
-
-export function form(title, form) {
-	return new Promise((resolve, reject) => {
-		popup(import('@client/components/form-dialog.vue'), { title, form }, {
-			done: result => {
-				resolve(result);
-			},
-		}, 'closed');
-	});
-}
-
-export async function selectUser() {
-	return new Promise((resolve, reject) => {
-		popup(import('@client/components/user-select-dialog.vue'), {}, {
-			ok: user => {
-				resolve(user);
-			},
-		}, 'closed');
-	});
-}
-
-export async function selectDriveFile(multiple: boolean) {
-	return new Promise((resolve, reject) => {
-		popup(import('@client/components/drive-select-dialog.vue'), {
-			type: 'file',
-			multiple
-		}, {
-			done: files => {
-				if (files) {
-					resolve(multiple ? files : files[0]);
-				}
-			},
-		}, 'closed');
-	});
-}
-
-export async function selectDriveFolder(multiple: boolean) {
-	return new Promise((resolve, reject) => {
-		popup(import('@client/components/drive-select-dialog.vue'), {
-			type: 'folder',
-			multiple
-		}, {
-			done: folders => {
-				if (folders) {
-					resolve(multiple ? folders : folders[0]);
-				}
-			},
-		}, 'closed');
-	});
-}
-
-export async function pickEmoji(src?: HTMLElement, opts) {
-	return new Promise((resolve, reject) => {
-		popup(import('@client/components/emoji-picker-dialog.vue'), {
-			src,
-			...opts
-		}, {
-			done: emoji => {
-				resolve(emoji);
-			},
-		}, 'closed');
-	});
-}
-
-type AwaitType<T> =
-	T extends Promise<infer U> ? U :
-	T extends (...args: any[]) => Promise<infer V> ? V :
-	T;
-let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
-let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
-export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
-	if (openingEmojiPicker) return;
-
-	activeTextarea = initialTextarea;
-
-	const textareas = document.querySelectorAll('textarea, input');
-	for (const textarea of Array.from(textareas)) {
-		textarea.addEventListener('focus', () => {
-			activeTextarea = textarea;
-		});
-	}
-
-	const observer = new MutationObserver(records => {
-		for (const record of records) {
-			for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) {
-				const textareas = node.querySelectorAll('textarea, input') as NodeListOf<NonNullable<typeof activeTextarea>>;
-				for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) {
-					if (document.activeElement === textarea) activeTextarea = textarea;
-					textarea.addEventListener('focus', () => {
-						activeTextarea = textarea;
-					});
-				}
-			}
-		}
-	});
-
-	observer.observe(document.body, {
-		childList: true,
-		subtree: true,
-		attributes: false,
-		characterData: false,
-	});
-
-	openingEmojiPicker = await popup(import('@client/components/emoji-picker-window.vue'), {
-		src,
-		...opts
-	}, {
-		chosen: emoji => {
-			insertTextAtCursor(activeTextarea, emoji);
-		},
-		closed: () => {
-			openingEmojiPicker!.dispose();
-			openingEmojiPicker = null;
-			observer.disconnect();
-		}
-	});
-}
-
-export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: {
-	align?: string;
-	width?: number;
-	viaKeyboard?: boolean;
-}) {
-	return new Promise((resolve, reject) => {
-		let dispose;
-		popup(import('@client/components/ui/popup-menu.vue'), {
-			items,
-			src,
-			width: options?.width,
-			align: options?.align,
-			viaKeyboard: options?.viaKeyboard
-		}, {
-			closed: () => {
-				resolve();
-				dispose();
-			},
-		}).then(res => {
-			dispose = res.dispose;
-		});
-	});
-}
-
-export function contextMenu(items: any[], ev: MouseEvent) {
-	ev.preventDefault();
-	return new Promise((resolve, reject) => {
-		let dispose;
-		popup(import('@client/components/ui/context-menu.vue'), {
-			items,
-			ev,
-		}, {
-			closed: () => {
-				resolve();
-				dispose();
-			},
-		}).then(res => {
-			dispose = res.dispose;
-		});
-	});
-}
-
-export function post(props: Record<string, any>) {
-	return new Promise((resolve, reject) => {
-		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
-		// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
-		//       Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
-		//       複数のpost formを開いたときに場合によってはエラーになる
-		//       もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
-		let dispose;
-		popup(MkPostFormDialog, props, {
-			closed: () => {
-				resolve();
-				dispose();
-			},
-		}).then(res => {
-			dispose = res.dispose;
-		});
-	});
-}
-
-export const deckGlobalEvents = new EventEmitter();
-
-export const uploads = ref([]);
-
-export function upload(file: File, folder?: any, name?: string) {
-	if (folder && typeof folder == 'object') folder = folder.id;
-
-	return new Promise((resolve, reject) => {
-		const id = Math.random();
-
-		const reader = new FileReader();
-		reader.onload = (e) => {
-			const ctx = reactive({
-				id: id,
-				name: name || file.name || 'untitled',
-				progressMax: undefined,
-				progressValue: undefined,
-				img: window.URL.createObjectURL(file)
-			});
-
-			uploads.value.push(ctx);
-
-			const data = new FormData();
-			data.append('i', $i.token);
-			data.append('force', 'true');
-			data.append('file', file);
-
-			if (folder) data.append('folderId', folder);
-			if (name) data.append('name', name);
-
-			const xhr = new XMLHttpRequest();
-			xhr.open('POST', apiUrl + '/drive/files/create', true);
-			xhr.onload = (e: any) => {
-				const driveFile = JSON.parse(e.target.response);
-
-				resolve(driveFile);
-
-				uploads.value = uploads.value.filter(x => x.id != id);
-			};
-
-			xhr.upload.onprogress = e => {
-				if (e.lengthComputable) {
-					ctx.progressMax = e.total;
-					ctx.progressValue = e.loaded;
-				}
-			};
-
-			xhr.send(data);
-		};
-		reader.readAsArrayBuffer(file);
-	});
-}
-
-/*
-export function checkExistence(fileData: ArrayBuffer): Promise<any> {
-	return new Promise((resolve, reject) => {
-		const data = new FormData();
-		data.append('md5', getMD5(fileData));
-
-		os.api('drive/files/find-by-hash', {
-			md5: getMD5(fileData)
-		}).then(resp => {
-			resolve(resp.length > 0 ? resp[0] : null);
-		});
-	});
-}*/
diff --git a/src/client/pages/_error_.vue b/src/client/pages/_error_.vue
deleted file mode 100644
index d1cefad8dd..0000000000
--- a/src/client/pages/_error_.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<template>
-<MkLoading v-if="!loaded" />
-<transition :name="$store.state.animation ? 'zoom' : ''" appear>
-	<div class="mjndxjch" v-show="loaded">
-		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
-		<p><b><i class="fas fa-exclamation-triangle"></i> {{ $ts.pageLoadError }}</b></p>
-		<p v-if="version === meta.version">{{ $ts.pageLoadErrorDescription }}</p>
-		<p v-else-if="serverIsDead">{{ $ts.serverIsDead }}</p>
-		<template v-else>
-			<p>{{ $ts.newVersionOfClientAvailable }}</p>
-			<p>{{ $ts.youShouldUpgradeClient }}</p>
-			<MkButton @click="reload" class="button primary">{{ $ts.reload }}</MkButton>
-		</template>
-		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p>
-		<p v-if="error" class="error">ERROR: {{ error }}</p>
-	</div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as symbols from '@client/symbols';
-import { version } from '@client/config';
-import * as os from '@client/os';
-import { unisonReload } from '@client/scripts/unison-reload';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-	props: {
-		error: {
-			required: false,
-		}
-	},
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.error,
-				icon: 'fas fa-exclamation-triangle'
-			},
-			loaded: false,
-			serverIsDead: false,
-			meta: {} as any,
-			version,
-		};
-	},
-	created() {
-		os.api('meta', {
-			detail: false
-		}).then(meta => {
-			this.loaded = true;
-			this.serverIsDead = false;
-			this.meta = meta;
-			localStorage.setItem('v', meta.version);
-		}, () => {
-			this.loaded = true;
-			this.serverIsDead = true;
-		});
-	},
-	methods: {
-		reload() {
-			unisonReload();
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.mjndxjch {
-	padding: 32px;
-	text-align: center;
-
-	> p {
-		margin: 0 0 12px 0;
-	}
-
-	> .button {
-		margin: 8px auto;
-	}
-
-	> img {
-		vertical-align: bottom;
-		height: 128px;
-		margin-bottom: 24px;
-		border-radius: 16px;
-	}
-
-	> .error {
-		opacity: 0.7;
-	}
-}
-</style>
diff --git a/src/client/pages/_loading_.vue b/src/client/pages/_loading_.vue
deleted file mode 100644
index 34ecaf9b33..0000000000
--- a/src/client/pages/_loading_.vue
+++ /dev/null
@@ -1,10 +0,0 @@
-<template>
-<MkLoading/>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({});
-</script>
diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue
deleted file mode 100644
index decee3a0f0..0000000000
--- a/src/client/pages/about-misskey.vue
+++ /dev/null
@@ -1,238 +0,0 @@
-<template>
-<div style="overflow: clip;">
-	<FormBase class="znqjceqz">
-		<div id="debug"></div>
-		<section class="_debobigegoItem about">
-			<div class="_debobigegoPanel panel" :class="{ playing: easterEggEngine != null }" ref="about">
-				<img src="/static-assets/client/about-icon.png" alt="" class="icon" @load="iconLoaded" draggable="false" @click="gravity"/>
-				<div class="misskey">Misskey</div>
-				<div class="version">v{{ version }}</div>
-				<span class="emoji" v-for="emoji in easterEggEmojis" :key="emoji.id" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
-			</div>
-		</section>
-		<section class="_debobigegoItem" style="text-align: center; padding: 0 16px;">
-			{{ $ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ $ts.learnMore }}</a>
-		</section>
-		<FormGroup>
-			<FormLink to="https://github.com/misskey-dev/misskey" external>
-				<template #icon><i class="fas fa-code"></i></template>
-				{{ $ts._aboutMisskey.source }}
-				<template #suffix>GitHub</template>
-			</FormLink>
-			<FormLink to="https://crowdin.com/project/misskey" external>
-				<template #icon><i class="fas fa-language"></i></template>
-				{{ $ts._aboutMisskey.translation }}
-				<template #suffix>Crowdin</template>
-			</FormLink>
-			<FormLink to="https://www.patreon.com/syuilo" external>
-				<template #icon><i class="fas fa-hand-holding-medical"></i></template>
-				{{ $ts._aboutMisskey.donate }}
-				<template #suffix>Patreon</template>
-			</FormLink>
-		</FormGroup>
-		<FormGroup>
-			<template #label>{{ $ts._aboutMisskey.contributors }}</template>
-			<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
-			<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
-			<FormLink to="https://github.com/mei23" external>@mei23</FormLink>
-			<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink>
-			<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink>
-			<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
-			<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
-			<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
-			<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
-			<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template>
-		</FormGroup>
-		<FormGroup>
-			<template #label><Mfm text="[jelly ❤]"/> {{ $ts._aboutMisskey.patrons }}</template>
-			<FormKeyValueView v-for="patron in patrons" :key="patron"><template #key>{{ patron }}</template></FormKeyValueView>
-			<template #caption>{{ $ts._aboutMisskey.morePatrons }}</template>
-		</FormGroup>
-	</FormBase>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { version } from '@client/config';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import MkLink from '@client/components/link.vue';
-import { physics } from '@client/scripts/physics';
-import * as symbols from '@client/symbols';
-
-const patrons = [
-	'Satsuki Yanagi',
-	'noellabo',
-	'mametsuko',
-	'AureoleArk',
-	'Gargron',
-	'Nokotaro Takeda',
-	'Suji Yan',
-	'Hekovic',
-	'Gitmo Life Services',
-	'nenohi',
-	'naga_rus',
-	'Melilot',
-	'Efertone',
-	'oi_yekssim',
-	'nanami kan',
-	'motcha',
-	'dansup',
-	'Quinton Macejkovic',
-	'YUKIMOCHI',
-	'mewl hayabusa',
-	'makokunsan',
-	'Peter G.',
-	'Nesakko',
-	'regtan',
-	'見当かなみ',
-	'natalie',
-	'Jerry',
-	'takimura',
-	'sikyosyounin',
-	'YuzuRyo61',
-	'sheeta.s',
-	'osapon',
-	'mkatze',
-	'CG',
-	'nafuchoco',
-	'Takumi Sugita',
-	'chidori ninokura',
-	'mydarkstar',
-	'kiritan',
-	'kabo2468y',
-	'weepjp',
-	'Liaizon Wakest',
-	'Steffen K9',
-	'Roujo',
-	'uroco @99',
-	'totokoro',
-	'public_yusuke',
-	'wara',
-	'S Y',
-	'Denshi',
-	'Osushimaru',
-	'吴浥',
-	'DignifiedSilence',
-	't_w',
-];
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormGroup,
-		FormLink,
-		FormKeyValueView,
-		MkLink,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.aboutMisskey,
-				icon: null
-			},
-			version,
-			patrons,
-			easterEggReady: false,
-			easterEggEmojis: [],
-			easterEggEngine: null,
-		}
-	},
-
-	beforeUnmount() {
-		if (this.easterEggEngine) {
-			this.easterEggEngine.stop();
-		}
-	},
-
-	methods: {
-		iconLoaded() {
-			const emojis = this.$store.state.reactions;
-			const containerWidth = this.$refs.about.offsetWidth;
-			for (let i = 0; i < 32; i++) {
-				this.easterEggEmojis.push({
-					id: i.toString(),
-					top: -(128 + (Math.random() * 256)),
-					left: (Math.random() * containerWidth),
-					emoji: emojis[Math.floor(Math.random() * emojis.length)],
-				});
-			}
-
-			this.$nextTick(() => {
-				this.easterEggReady = true;
-			});
-		},
-
-		gravity() {
-			if (!this.easterEggReady) return;
-			this.easterEggReady = false;
-			this.easterEggEngine = physics(this.$refs.about);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.znqjceqz {
-	max-width: 800px;
-	box-sizing: border-box;
-	margin: 0 auto;
-
-	> .about {
-		> .panel {
-			position: relative;
-			text-align: center;
-			padding: 16px;
-
-			&.playing {
-				&, * {
-					user-select: none;
-				}
-
-				* {
-					will-change: transform;
-				}
-
-				> .emoji {
-					visibility: visible;
-				}
-			}
-
-			> .icon {
-				display: block;
-				width: 100px;
-				margin: 0 auto;
-				border-radius: 16px;
-			}
-
-			> .misskey {
-				margin: 0.75em auto 0 auto;
-				width: max-content;
-			}
-
-			> .version {
-				margin: 0 auto;
-				width: max-content;
-				opacity: 0.5;
-			}
-
-			> .emoji {
-				position: absolute;
-				top: 0;
-				left: 0;
-				visibility: hidden;
-
-				> .emoji {
-					pointer-events: none;
-					font-size: 24px;
-					width: 24px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue
deleted file mode 100644
index 2c580c293a..0000000000
--- a/src/client/pages/about.vue
+++ /dev/null
@@ -1,123 +0,0 @@
-<template>
-<FormBase>
-	<div class="_debobigegoItem">
-		<div class="_debobigegoPanel fwhjspax">
-			<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
-			<span class="name">{{ $instance.name || host }}</span>
-		</div>
-	</div>
-
-	<FormTextarea readonly :value="$instance.description">
-	</FormTextarea>
-
-	<FormGroup>
-		<FormKeyValueView>
-			<template #key>Misskey</template>
-			<template #value>v{{ version }}</template>
-		</FormKeyValueView>
-		<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink>
-	</FormGroup>
-
-	<FormGroup>
-		<FormKeyValueView>
-			<template #key>{{ $ts.administrator }}</template>
-			<template #value>{{ $instance.maintainerName }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.contact }}</template>
-			<template #value>{{ $instance.maintainerEmail }}</template>
-		</FormKeyValueView>
-	</FormGroup>
-
-	<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" external>{{ $ts.tos }}</FormLink>
-
-	<FormSuspense :p="initStats">
-		<FormGroup>
-			<template #label>{{ $ts.statistics }}</template>
-			<FormKeyValueView>
-				<template #key>{{ $ts.users }}</template>
-				<template #value>{{ number(stats.originalUsersCount) }}</template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>{{ $ts.notes }}</template>
-				<template #value>{{ number(stats.originalNotesCount) }}</template>
-			</FormKeyValueView>
-		</FormGroup>
-	</FormSuspense>
-
-	<FormGroup>
-		<template #label>Well-known resources</template>
-		<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
-		<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
-		<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
-		<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
-		<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { version, instanceName } from '@client/config';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-import * as symbols from '@client/symbols';
-import { host } from '@client/config';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormGroup,
-		FormLink,
-		FormKeyValueView,
-		FormTextarea,
-		FormSuspense,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.instanceInfo,
-				icon: 'fas fa-info-circle'
-			},
-			host,
-			version,
-			instanceName,
-			stats: null,
-			initStats: () => os.api('stats', {
-			}).then((stats) => {
-				this.stats = stats;
-			})
-		}
-	},
-
-	methods: {
-		number
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fwhjspax {
-	padding: 16px;
-	text-align: center;
-
-	> .icon {
-		display: block;
-		margin: auto;
-		height: 64px;
-		border-radius: 8px;
-	}
-
-	> .name {
-		display: block;
-		margin-top: 12px;
-	}
-}
-</style>
diff --git a/src/client/pages/admin/abuses.vue b/src/client/pages/admin/abuses.vue
deleted file mode 100644
index fb99347f34..0000000000
--- a/src/client/pages/admin/abuses.vue
+++ /dev/null
@@ -1,170 +0,0 @@
-<template>
-<div class="lcixvhis">
-	<div class="_section reports">
-		<div class="_content">
-			<div class="inputs" style="display: flex;">
-				<MkSelect v-model="state" style="margin: 0; flex: 1;">
-					<template #label>{{ $ts.state }}</template>
-					<option value="all">{{ $ts.all }}</option>
-					<option value="unresolved">{{ $ts.unresolved }}</option>
-					<option value="resolved">{{ $ts.resolved }}</option>
-				</MkSelect>
-				<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
-					<template #label>{{ $ts.reporteeOrigin }}</template>
-					<option value="combined">{{ $ts.all }}</option>
-					<option value="local">{{ $ts.local }}</option>
-					<option value="remote">{{ $ts.remote }}</option>
-				</MkSelect>
-				<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
-					<template #label>{{ $ts.reporterOrigin }}</template>
-					<option value="combined">{{ $ts.all }}</option>
-					<option value="local">{{ $ts.local }}</option>
-					<option value="remote">{{ $ts.remote }}</option>
-				</MkSelect>
-			</div>
-			<!-- TODO
-			<div class="inputs" style="display: flex; padding-top: 1.2em;">
-				<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()">
-					<span>{{ $ts.username }}</span>
-				</MkInput>
-				<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
-					<span>{{ $ts.host }}</span>
-				</MkInput>
-			</div>
-			-->
-
-			<MkPagination :pagination="pagination" #default="{items}" ref="reports" style="margin-top: var(--margin);">
-				<div class="bcekxzvu _card _gap" v-for="report in items" :key="report.id">
-					<div class="_content target">
-						<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
-						<div class="info">
-							<MkUserName class="name" :user="report.targetUser"/>
-							<div class="acct">@{{ acct(report.targetUser) }}</div>
-						</div>
-					</div>
-					<div class="_content">
-						<div>
-							<Mfm :text="report.comment"/>
-						</div>
-						<hr>
-						<div>Reporter: <MkAcct :user="report.reporter"/></div>
-						<div><MkTime :time="report.createdAt"/></div>
-					</div>
-					<div class="_footer">
-						<div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div>
-						<MkButton @click="resolve(report)" primary v-if="!report.resolved">{{ $ts.abuseMarkAsResolved }}</MkButton>
-					</div>
-				</div>
-			</MkPagination>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { parseAcct } from '@/misc/acct';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import { acct } from '@client/filters/user';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkPagination,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.abuseReports,
-				icon: 'fas fa-exclamation-circle',
-				bg: 'var(--bg)',
-			},
-			searchUsername: '',
-			searchHost: '',
-			state: 'unresolved',
-			reporterOrigin: 'combined',
-			targetUserOrigin: 'combined',
-			pagination: {
-				endpoint: 'admin/abuse-user-reports',
-				limit: 10,
-				params: () => ({
-					state: this.state,
-					reporterOrigin: this.reporterOrigin,
-					targetUserOrigin: this.targetUserOrigin,
-				}),
-			},
-		}
-	},
-
-	watch: {
-		state() {
-			this.$refs.reports.reload();
-		},
-
-		reporterOrigin() {
-			this.$refs.reports.reload();
-		},
-
-		targetUserOrigin() {
-			this.$refs.reports.reload();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		acct,
-
-		resolve(report) {
-			os.apiWithDialog('admin/resolve-abuse-user-report', {
-				reportId: report.id,
-			}).then(() => {
-				this.$refs.reports.removeItem(item => item.id === report.id);
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.lcixvhis {
-	margin: var(--margin);
-}
-
-.bcekxzvu {
-	> .target {
-		display: flex;
-		width: 100%;
-		box-sizing: border-box;
-		text-align: left;
-		align-items: center;
-					
-		> .avatar {
-			width: 42px;
-			height: 42px;
-		}
-
-		> .info {
-			margin-left: 0.3em;
-			padding: 0 8px;
-			flex: 1;
-
-			> .name {
-				font-weight: bold;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/admin/ads.vue b/src/client/pages/admin/ads.vue
deleted file mode 100644
index 4d39bb4e40..0000000000
--- a/src/client/pages/admin/ads.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<template>
-<div class="uqshojas">
-	<section class="_card _gap ads" v-for="ad in ads">
-		<div class="_content ad">
-			<MkAd v-if="ad.url" :specify="ad"/>
-			<MkInput v-model="ad.url" type="url">
-				<template #label>URL</template>
-			</MkInput>
-			<MkInput v-model="ad.imageUrl">
-				<template #label>{{ $ts.imageUrl }}</template>
-			</MkInput>
-			<div style="margin: 32px 0;">
-				<MkRadio v-model="ad.place" value="square">square</MkRadio>
-				<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
-				<MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
-			</div>
-			<!--
-			<div style="margin: 32px 0;">
-				{{ $ts.priority }}
-				<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
-				<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
-				<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
-			</div>
-			-->
-			<MkInput v-model="ad.ratio" type="number">
-				<template #label>{{ $ts.ratio }}</template>
-			</MkInput>
-			<MkInput v-model="ad.expiresAt" type="date">
-				<template #label>{{ $ts.expiration }}</template>
-			</MkInput>
-			<MkTextarea v-model="ad.memo">
-				<template #label>{{ $ts.memo }}</template>
-			</MkTextarea>
-			<div class="buttons">
-				<MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-				<MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
-			</div>
-		</div>
-	</section>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkRadio from '@client/components/form/radio.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkTextarea,
-		MkRadio,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.ads,
-				icon: 'fas fa-audio-description',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.add,
-					handler: this.add,
-				}],
-			},
-			ads: [],
-		}
-	},
-
-	created() {
-		os.api('admin/ad/list').then(ads => {
-			this.ads = ads;
-		});
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		add() {
-			this.ads.unshift({
-				id: null,
-				memo: '',
-				place: 'square',
-				priority: 'middle',
-				ratio: 1,
-				url: '',
-				imageUrl: null,
-				expiresAt: null,
-			});
-		},
-
-		remove(ad) {
-			os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: ad.url }),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.ads = this.ads.filter(x => x != ad);
-				os.apiWithDialog('admin/ad/delete', {
-					id: ad.id
-				});
-			});
-		},
-
-		save(ad) {
-			if (ad.id == null) {
-				os.apiWithDialog('admin/ad/create', {
-					...ad,
-					expiresAt: new Date(ad.expiresAt).getTime()
-				});
-			} else {
-				os.apiWithDialog('admin/ad/update', {
-					...ad,
-					expiresAt: new Date(ad.expiresAt).getTime()
-				});
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.uqshojas {
-	margin: var(--margin);
-}
-</style>
diff --git a/src/client/pages/admin/announcements.vue b/src/client/pages/admin/announcements.vue
deleted file mode 100644
index 4ace515b0b..0000000000
--- a/src/client/pages/admin/announcements.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<template>
-<div class="ztgjmzrw">
-	<section class="_card _gap announcements" v-for="announcement in announcements">
-		<div class="_content announcement">
-			<MkInput v-model="announcement.title">
-				<template #label>{{ $ts.title }}</template>
-			</MkInput>
-			<MkTextarea v-model="announcement.text">
-				<template #label>{{ $ts.text }}</template>
-			</MkTextarea>
-			<MkInput v-model="announcement.imageUrl">
-				<template #label>{{ $ts.imageUrl }}</template>
-			</MkInput>
-			<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
-			<div class="buttons">
-				<MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-				<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
-			</div>
-		</div>
-	</section>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkTextarea,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.announcements,
-				icon: 'fas fa-broadcast-tower',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.add,
-					handler: this.add,
-				}],
-			},
-			announcements: [],
-		}
-	},
-
-	created() {
-		os.api('admin/announcements/list').then(announcements => {
-			this.announcements = announcements;
-		});
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		add() {
-			this.announcements.unshift({
-				id: null,
-				title: '',
-				text: '',
-				imageUrl: null
-			});
-		},
-
-		remove(announcement) {
-			os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: announcement.title }),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				this.announcements = this.announcements.filter(x => x != announcement);
-				os.api('admin/announcements/delete', announcement);
-			});
-		},
-
-		save(announcement) {
-			if (announcement.id == null) {
-				os.api('admin/announcements/create', announcement).then(() => {
-					os.dialog({
-						type: 'success',
-						text: this.$ts.saved
-					});
-				}).catch(e => {
-					os.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			} else {
-				os.api('admin/announcements/update', announcement).then(() => {
-					os.dialog({
-						type: 'success',
-						text: this.$ts.saved
-					});
-				}).catch(e => {
-					os.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ztgjmzrw {
-	margin: var(--margin);
-}
-</style>
diff --git a/src/client/pages/admin/bot-protection.vue b/src/client/pages/admin/bot-protection.vue
deleted file mode 100644
index 731f114cc2..0000000000
--- a/src/client/pages/admin/bot-protection.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormRadios v-model="provider">
-			<template #desc><i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}</template>
-			<option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
-			<option value="hcaptcha">hCaptcha</option>
-			<option value="recaptcha">reCAPTCHA</option>
-		</FormRadios>
-
-		<template v-if="provider === 'hcaptcha'">
-			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">hCaptcha</div>
-				<div class="main">
-					<FormInput v-model="hcaptchaSiteKey">
-						<template #prefix><i class="fas fa-key"></i></template>
-						<span>{{ $ts.hcaptchaSiteKey }}</span>
-					</FormInput>
-					<FormInput v-model="hcaptchaSecretKey">
-						<template #prefix><i class="fas fa-key"></i></template>
-						<span>{{ $ts.hcaptchaSecretKey }}</span>
-					</FormInput>
-				</div>
-			</div>
-			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">{{ $ts.preview }}</div>
-				<div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
-					<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
-				</div>
-			</div>
-		</template>
-		<template v-else-if="provider === 'recaptcha'">
-			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">reCAPTCHA</div>
-				<div class="main">
-					<FormInput v-model="recaptchaSiteKey">
-						<template #prefix><i class="fas fa-key"></i></template>
-						<span>{{ $ts.recaptchaSiteKey }}</span>
-					</FormInput>
-					<FormInput v-model="recaptchaSecretKey">
-						<template #prefix><i class="fas fa-key"></i></template>
-						<span>{{ $ts.recaptchaSecretKey }}</span>
-					</FormInput>
-				</div>
-			</div>
-			<div v-if="recaptchaSiteKey" class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">{{ $ts.preview }}</div>
-				<div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
-					<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
-				</div>
-			</div>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormRadios from '@client/components/debobigego/radios.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormRadios,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormInfo,
-		FormSuspense,
-		MkCaptcha: defineAsyncComponent(() => import('@client/components/captcha.vue')),
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.botProtection,
-				icon: 'fas fa-shield-alt'
-			},
-			provider: null,
-			enableHcaptcha: false,
-			hcaptchaSiteKey: null,
-			hcaptchaSecretKey: null,
-			enableRecaptcha: false,
-			recaptchaSiteKey: null,
-			recaptchaSecretKey: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableHcaptcha = meta.enableHcaptcha;
-			this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
-			this.hcaptchaSecretKey = meta.hcaptchaSecretKey;
-			this.enableRecaptcha = meta.enableRecaptcha;
-			this.recaptchaSiteKey = meta.recaptchaSiteKey;
-			this.recaptchaSecretKey = meta.recaptchaSecretKey;
-
-			this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null;
-
-			this.$watch(() => this.provider, () => {
-				this.enableHcaptcha = this.provider === 'hcaptcha';
-				this.enableRecaptcha = this.provider === 'recaptcha';
-			});
-		},
-	
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableHcaptcha: this.enableHcaptcha,
-				hcaptchaSiteKey: this.hcaptchaSiteKey,
-				hcaptchaSecretKey: this.hcaptchaSecretKey,
-				enableRecaptcha: this.enableRecaptcha,
-				recaptchaSiteKey: this.recaptchaSiteKey,
-				recaptchaSecretKey: this.recaptchaSecretKey,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/database.vue b/src/client/pages/admin/database.vue
deleted file mode 100644
index ffbeed8b30..0000000000
--- a/src/client/pages/admin/database.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="databasePromiseFactory" v-slot="{ result: database }">
-		<FormGroup v-for="table in database" :key="table[0]">
-			<template #label>{{ table[0] }}</template>
-			<FormKeyValueView>
-				<template #key>Size</template>
-				<template #value>{{ bytes(table[1].size) }}</template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>Records</template>
-				<template #value>{{ number(table[1].count) }}</template>
-			</FormKeyValueView>
-		</FormGroup>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-
-export default defineComponent({
-	components: {
-		FormSuspense,
-		FormKeyValueView,
-		FormBase,
-		FormGroup,
-		FormLink,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.database,
-				icon: 'fas fa-database',
-				bg: 'var(--bg)',
-			},
-			databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		bytes, number,
-	}
-});
-</script>
diff --git a/src/client/pages/admin/email-settings.vue b/src/client/pages/admin/email-settings.vue
deleted file mode 100644
index ebf724fcdd..0000000000
--- a/src/client/pages/admin/email-settings.vue
+++ /dev/null
@@ -1,128 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
-
-		<template v-if="enableEmail">
-			<FormInput v-model="email" type="email">
-				<span>{{ $ts.emailAddress }}</span>
-			</FormInput>
-
-			<div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
-				<div class="_debobigegoLabel">{{ $ts.smtpConfig }}</div>
-				<div class="main">
-					<FormInput v-model="smtpHost">
-						<span>{{ $ts.smtpHost }}</span>
-					</FormInput>
-					<FormInput v-model="smtpPort" type="number">
-						<span>{{ $ts.smtpPort }}</span>
-					</FormInput>
-					<FormInput v-model="smtpUser">
-						<span>{{ $ts.smtpUser }}</span>
-					</FormInput>
-					<FormInput v-model="smtpPass" type="password">
-						<span>{{ $ts.smtpPass }}</span>
-					</FormInput>
-					<FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
-					<FormSwitch v-model="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
-				</div>
-			</div>
-
-			<FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.emailServer,
-				icon: 'fas fa-envelope',
-				bg: 'var(--bg)',
-			},
-			enableEmail: false,
-			email: null,
-			smtpSecure: false,
-			smtpHost: '',
-			smtpPort: 0,
-			smtpUser: '',
-			smtpPass: '',
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableEmail = meta.enableEmail;
-			this.email = meta.email;
-			this.smtpSecure = meta.smtpSecure;
-			this.smtpHost = meta.smtpHost;
-			this.smtpPort = meta.smtpPort;
-			this.smtpUser = meta.smtpUser;
-			this.smtpPass = meta.smtpPass;
-		},
-
-		async testEmail() {
-			const { canceled, result: destination } = await os.dialog({
-				title: this.$ts.destination,
-				input: {
-					placeholder: this.$instance.maintainerEmail
-				}
-			});
-			if (canceled) return;
-			os.apiWithDialog('admin/send-email', {
-				to: destination,
-				subject: 'Test email',
-				text: 'Yo'
-			});
-		},
-
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableEmail: this.enableEmail,
-				email: this.email,
-				smtpSecure: this.smtpSecure,
-				smtpHost: this.smtpHost,
-				smtpPort: this.smtpPort,
-				smtpUser: this.smtpUser,
-				smtpPass: this.smtpPass,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/emoji-edit-dialog.vue b/src/client/pages/admin/emoji-edit-dialog.vue
deleted file mode 100644
index 4854c69884..0000000000
--- a/src/client/pages/admin/emoji-edit-dialog.vue
+++ /dev/null
@@ -1,120 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="370"
-	:with-ok-button="true"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
-	@ok="ok()"
->
-	<template #header>:{{ emoji.name }}:</template>
-
-	<div class="_monolithic_">
-		<div class="yigymqpb _section">
-			<img :src="emoji.url" class="img"/>
-			<MkInput class="_formBlock" v-model="name">
-				<template #label>{{ $ts.name }}</template>
-			</MkInput>
-			<MkInput class="_formBlock" v-model="category" :datalist="categories">
-				<template #label>{{ $ts.category }}</template>
-			</MkInput>
-			<MkInput class="_formBlock" v-model="aliases">
-				<template #label>{{ $ts.tags }}</template>
-				<template #caption>{{ $ts.setMultipleBySeparatingWithSpace }}</template>
-			</MkInput>
-			<MkButton danger @click="del()"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
-		</div>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-import { unique } from '../../../prelude/array';
-
-export default defineComponent({
-	components: {
-		XModalWindow,
-		MkButton,
-		MkInput,
-	},
-
-	props: {
-		emoji: {
-			required: true,
-		}
-	},
-
-	emits: ['done', 'closed'],
-
-	data() {
-		return {
-			name: this.emoji.name,
-			category: this.emoji.category,
-			aliases: this.emoji.aliases?.join(' '),
-			categories: [],
-		}
-	},
-
-	created() {
-		os.api('meta', { detail: false }).then(({ emojis }) => {
-			this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
-		});
-	},
-
-	methods: {
-		ok() {
-			this.update();
-		},
-
-		async update() {
-			await os.apiWithDialog('admin/emoji/update', {
-				id: this.emoji.id,
-				name: this.name,
-				category: this.category,
-				aliases: this.aliases.split(' '),
-			});
-
-			this.$emit('done', {
-				updated: {
-					name: this.name,
-					category: this.category,
-					aliases: this.aliases.split(' '),
-				}
-			});
-			this.$refs.dialog.close();
-		},
-
-		async del() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: this.emoji.name }),
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			os.api('admin/emoji/remove', {
-				id: this.emoji.id
-			}).then(() => {
-				this.$emit('done', {
-					deleted: true
-				});
-				this.$refs.dialog.close();
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.yigymqpb {
-	> .img {
-		display: block;
-		height: 64px;
-		margin: 0 auto;
-	}
-}
-</style>
diff --git a/src/client/pages/admin/emojis.vue b/src/client/pages/admin/emojis.vue
deleted file mode 100644
index 5876db349e..0000000000
--- a/src/client/pages/admin/emojis.vue
+++ /dev/null
@@ -1,263 +0,0 @@
-<template>
-<div class="ogwlenmc">
-	<div class="local" v-if="tab === 'local'">
-		<MkInput v-model="query" :debounce="true" type="search" style="margin: var(--margin);">
-			<template #prefix><i class="fas fa-search"></i></template>
-			<template #label>{{ $ts.search }}</template>
-		</MkInput>
-		<MkPagination :pagination="pagination" ref="emojis">
-			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
-			<template #default="{items}">
-				<div class="ldhfsamy">
-					<button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)">
-						<img :src="emoji.url" class="img" :alt="emoji.name"/>
-						<div class="body">
-							<div class="name _monospace">{{ emoji.name }}</div>
-							<div class="info">{{ emoji.category }}</div>
-						</div>
-					</button>
-				</div>
-			</template>
-		</MkPagination>
-	</div>
-
-	<div class="remote" v-else-if="tab === 'remote'">
-		<MkInput v-model="queryRemote" :debounce="true" type="search" style="margin: var(--margin);">
-			<template #prefix><i class="fas fa-search"></i></template>
-			<template #label>{{ $ts.search }}</template>
-		</MkInput>
-		<MkInput v-model="host" :debounce="true" style="margin: var(--margin);">
-			<template #label>{{ $ts.host }}</template>
-		</MkInput>
-		<MkPagination :pagination="remotePagination" ref="remoteEmojis">
-			<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
-			<template #default="{items}">
-				<div class="ldhfsamy">
-					<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
-						<img :src="emoji.url" class="img" :alt="emoji.name"/>
-						<div class="body">
-							<div class="name _monospace">{{ emoji.name }}</div>
-							<div class="info">{{ emoji.host }}</div>
-						</div>
-					</div>
-				</div>
-			</template>
-		</MkPagination>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, toRef } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkTab from '@client/components/tab.vue';
-import { selectFile } from '@client/scripts/select-file';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkTab,
-		MkButton,
-		MkInput,
-		MkPagination,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.customEmojis,
-				icon: 'fas fa-laugh',
-				bg: 'var(--bg)',
-				actions: [{
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.addEmoji,
-					handler: this.add,
-				}],
-				tabs: [{
-					active: this.tab === 'local',
-					title: this.$ts.local,
-					onClick: () => { this.tab = 'local'; },
-				}, {
-					active: this.tab === 'remote',
-					title: this.$ts.remote,
-					onClick: () => { this.tab = 'remote'; },
-				},]
-			})),
-			tab: 'local',
-			query: null,
-			queryRemote: null,
-			host: '',
-			pagination: {
-				endpoint: 'admin/emoji/list',
-				limit: 30,
-				params: computed(() => ({
-					query: (this.query && this.query !== '') ? this.query : null
-				}))
-			},
-			remotePagination: {
-				endpoint: 'admin/emoji/list-remote',
-				limit: 30,
-				params: computed(() => ({
-					query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
-					host: (this.host && this.host !== '') ? this.host : null
-				}))
-			},
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', toRef(this, symbols.PAGE_INFO));
-	},
-
-	methods: {
-		async add(e) {
-			const files = await selectFile(e.currentTarget || e.target, null, true);
-
-			const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
-				fileId: file.id,
-			})));
-			promise.then(() => {
-				this.$refs.emojis.reload();
-			});
-			os.promiseDialog(promise);
-		},
-
-		edit(emoji) {
-			os.popup(import('./emoji-edit-dialog.vue'), {
-				emoji: emoji
-			}, {
-				done: result => {
-					if (result.updated) {
-						this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
-							...emoji,
-							...result.updated
-						});
-					} else if (result.deleted) {
-						this.$refs.emojis.removeItem(item => item.id === emoji.id);
-					}
-				},
-			}, 'closed');
-		},
-
-		im(emoji) {
-			os.apiWithDialog('admin/emoji/copy', {
-				emojiId: emoji.id,
-			});
-		},
-
-		remoteMenu(emoji, ev) {
-			os.popupMenu([{
-				type: 'label',
-				text: ':' + emoji.name + ':',
-			}, {
-				text: this.$ts.import,
-				icon: 'fas fa-plus',
-				action: () => { this.im(emoji) }
-			}], ev.currentTarget || ev.target);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ogwlenmc {
-	> .local {
-	  .empty {
-    	margin: var(--margin);
-		}
-		
-		.ldhfsamy {
-			display: grid;
-			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
-			grid-gap: 12px;
-			margin: var(--margin);
-	
-			> .emoji {
-				display: flex;
-				align-items: center;
-				padding: 12px;
-				text-align: left;
-
-				&:hover {
-					color: var(--accent);
-				}
-
-				> .img {
-					width: 42px;
-					height: 42px;
-				}
-
-				> .body {
-					padding: 0 0 0 8px;
-					white-space: nowrap;
-					overflow: hidden;
-
-					> .name {
-						text-overflow: ellipsis;
-						overflow: hidden;
-					}
-
-					> .info {
-						opacity: 0.5;
-						text-overflow: ellipsis;
-						overflow: hidden;
-					}
-				}
-			}
-		}
-	}
-
-	> .remote {
-	  .empty {
-      margin: var(--margin);
-    }
-								
-		.ldhfsamy {
-			display: grid;
-			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
-			grid-gap: 12px;
-			margin: var(--margin);
-
-			> .emoji {
-				display: flex;
-				align-items: center;
-				padding: 12px;
-				text-align: left;
-
-				&:hover {
-					color: var(--accent);
-				}
-
-				> .img {
-					width: 32px;
-					height: 32px;
-				}
-
-				> .body {
-					padding: 0 0 0 8px;
-					white-space: nowrap;
-					overflow: hidden;
-
-					> .name {
-						text-overflow: ellipsis;
-						overflow: hidden;
-					}
-
-					> .info {
-						opacity: 0.5;
-						font-size: 90%;
-						text-overflow: ellipsis;
-						overflow: hidden;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/admin/file-dialog.vue b/src/client/pages/admin/file-dialog.vue
deleted file mode 100644
index 02d83e5022..0000000000
--- a/src/client/pages/admin/file-dialog.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="370"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
->
-	<template #header v-if="file">{{ file.name }}</template>
-	<div class="cxqhhsmd" v-if="file">
-		<div class="_section">
-			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
-			<div class="info">
-				<span style="margin-right: 1em;">{{ file.type }}</span>
-				<span>{{ bytes(file.size) }}</span>
-				<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
-			</div>
-		</div>
-		<div class="_section">
-			<div class="_content">
-				<MkSwitch @update:modelValue="toggleIsSensitive" v-model="isSensitive">NSFW</MkSwitch>
-			</div>
-		</div>
-		<div class="_section">
-			<div class="_content">
-				<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton>
-				<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
-			</div>
-		</div>
-		<div class="_section" v-if="info">
-			<details class="_content rawdata">
-				<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
-			</details>
-		</div>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
-import Progress from '@client/scripts/loading';
-import bytes from '@client/filters/bytes';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkSwitch,
-		XModalWindow,
-		MkDriveFileThumbnail,
-	},
-
-	props: {
-		fileId: {
-			required: true,
-		}
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			file: null,
-			info: null,
-			isSensitive: false,
-		};
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		async fetch() {
-			Progress.start();
-			this.file = await os.api('drive/files/show', { fileId: this.fileId });
-			this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
-			this.isSensitive = this.file.isSensitive;
-			Progress.done();
-		},
-
-		showUser() {
-			os.pageWindow(`/user-info/${this.file.userId}`);
-		},
-
-		async del() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: this.file.name }),
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			os.apiWithDialog('drive/files/delete', {
-				fileId: this.file.id
-			});
-		},
-
-		async toggleIsSensitive(v) {
-			await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
-			this.isSensitive = v;
-		},
-
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.cxqhhsmd {
-	> ._section {
-		> .thumbnail {
-			height: 150px;
-			max-width: 100%;
-		}
-
-		> .info {
-			text-align: center;
-			margin-top: 8px;
-		}
-		
-		> .rawdata {
-			overflow: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/admin/files-settings.vue b/src/client/pages/admin/files-settings.vue
deleted file mode 100644
index 8aefa9e90d..0000000000
--- a/src/client/pages/admin/files-settings.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="cacheRemoteFiles">
-			{{ $ts.cacheRemoteFiles }}
-			<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
-		</FormSwitch>
-
-		<FormSwitch v-model="proxyRemoteFiles">
-			{{ $ts.proxyRemoteFiles }}
-			<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
-		</FormSwitch>
-
-		<FormInput v-model="localDriveCapacityMb" type="number">
-			<span>{{ $ts.driveCapacityPerLocalAccount }}</span>
-			<template #suffix>MB</template>
-			<template #desc>{{ $ts.inMb }}</template>
-		</FormInput>
-
-		<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
-			<span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
-			<template #suffix>MB</template>
-			<template #desc>{{ $ts.inMb }}</template>
-		</FormInput>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.files,
-				icon: 'fas fa-cloud',
-				bg: 'var(--bg)',
-			},
-			cacheRemoteFiles: false,
-			proxyRemoteFiles: false,
-			localDriveCapacityMb: 0,
-			remoteDriveCapacityMb: 0,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.cacheRemoteFiles = meta.cacheRemoteFiles;
-			this.proxyRemoteFiles = meta.proxyRemoteFiles;
-			this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
-			this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				cacheRemoteFiles: this.cacheRemoteFiles,
-				proxyRemoteFiles: this.proxyRemoteFiles,
-				localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
-				remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/files.vue b/src/client/pages/admin/files.vue
deleted file mode 100644
index 55189cfd84..0000000000
--- a/src/client/pages/admin/files.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<template>
-<div class="xrmjdkdw">
-	<MkContainer :foldable="true" class="lookup">
-		<template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template>
-		<div class="xrmjdkdw-lookup">
-			<MkInput class="item" v-model="q" type="text" @enter="find()">
-				<template #label>{{ $ts.fileIdOrUrl }}</template>
-			</MkInput>
-			<MkButton @click="find()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
-		</div>
-	</MkContainer>
-
-	<div class="_section">
-		<div class="_content">
-			<div class="inputs" style="display: flex;">
-				<MkSelect v-model="origin" style="margin: 0; flex: 1;">
-					<template #label>{{ $ts.instance }}</template>
-					<option value="combined">{{ $ts.all }}</option>
-					<option value="local">{{ $ts.local }}</option>
-					<option value="remote">{{ $ts.remote }}</option>
-				</MkSelect>
-				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
-					<template #label>{{ $ts.host }}</template>
-				</MkInput>
-			</div>
-			<div class="inputs" style="display: flex; padding-top: 1.2em;">
-				<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
-					<template #label>MIME type</template>
-				</MkInput>
-			</div>
-			<MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files">
-				<button class="file _panel _button _gap" v-for="file in items" :key="file.id" @click="show(file, $event)">
-					<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
-					<div class="body">
-						<div>
-							<small style="opacity: 0.7;">{{ file.name }}</small>
-						</div>
-						<div>
-							<MkAcct v-if="file.user" :user="file.user"/>
-							<div v-else>{{ $ts.system }}</div>
-						</div>
-						<div>
-							<span style="margin-right: 1em;">{{ file.type }}</span>
-							<span>{{ bytes(file.size) }}</span>
-						</div>
-						<div>
-							<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
-						</div>
-					</div>
-				</button>
-			</MkPagination>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
-import bytes from '@client/filters/bytes';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkPagination,
-		MkContainer,
-		MkDriveFileThumbnail,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.files,
-				icon: 'fas fa-cloud',
-				bg: 'var(--bg)',
-				actions: [{
-					text: this.$ts.clearCachedFiles,
-					icon: 'fas fa-trash-alt',
-					handler: this.clear
-				}]
-			},
-			q: null,
-			origin: 'local',
-			type: null,
-			searchHost: '',
-			pagination: {
-				endpoint: 'admin/drive/files',
-				limit: 10,
-				params: () => ({
-					type: (this.type && this.type !== '') ? this.type : null,
-					origin: this.origin,
-					hostname: (this.hostname && this.hostname !== '') ? this.hostname : null,
-				}),
-			},
-		}
-	},
-
-	watch: {
-		type() {
-			this.$refs.files.reload();
-		},
-		origin() {
-			this.$refs.files.reload();
-		},
-		searchHost() {
-			this.$refs.files.reload();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		clear() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.clearCachedFilesConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.apiWithDialog('admin/drive/clean-remote-files', {});
-			});
-		},
-
-		show(file, ev) {
-			os.popup(import('./file-dialog.vue'), {
-				fileId: file.id
-			}, {}, 'closed');
-		},
-
-		find() {
-			os.api('admin/drive/show-file', this.q.startsWith('http://') || this.q.startsWith('https://') ? { url: this.q.trim() } : { fileId: this.q.trim() }).then(file => {
-				this.show(file);
-			}).catch(e => {
-				if (e.code === 'NO_SUCH_FILE') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.notFound
-					});
-				}
-			});
-		},
-
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xrmjdkdw {
-	margin: var(--margin);
-
-	> .lookup {
-		margin-bottom: 16px;
-	}
-
-	.urempief {
-		margin-top: var(--margin);
-
-		> .file {
-			display: flex;
-			width: 100%;
-			box-sizing: border-box;
-			text-align: left;
-			align-items: center;
-
-			&:hover {
-				color: var(--accent);
-			}
-
-			> .thumbnail {
-				width: 128px;
-				height: 128px;
-			}
-
-			> .body {
-				margin-left: 0.3em;
-				padding: 8px;
-				flex: 1;
-
-				@media (max-width: 500px) {
-					font-size: 14px;
-				}
-			}
-		}
-	}
-}
-
-.xrmjdkdw-lookup {
-	padding: 16px;
-
-	> .item {
-		margin-bottom: 16px;
-	}
-}
-</style>
diff --git a/src/client/pages/admin/index.vue b/src/client/pages/admin/index.vue
deleted file mode 100644
index 28157ff05a..0000000000
--- a/src/client/pages/admin/index.vue
+++ /dev/null
@@ -1,388 +0,0 @@
-<template>
-<div class="hiyeyicy" :class="{ wide: !narrow }" ref="el">
-	<div class="nav" v-if="!narrow || page == null">
-		<MkHeader :info="header"></MkHeader>
-	
-		<MkSpacer :content-max="700">
-			<div class="lxpfedzu">
-				<div class="banner">
-					<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
-				</div>
-
-				<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
-				<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
-
-				<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
-			</div>
-		</MkSpacer>
-	</div>
-	<div class="main">
-		<MkStickyContainer>
-			<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
-			<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
-		</MkStickyContainer>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue';
-import { i18n } from '@client/i18n';
-import MkSuperMenu from '@client/components/ui/super-menu.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import MkInfo from '@client/components/ui/info.vue';
-import { scroll } from '@client/scripts/scroll';
-import { instance } from '@client/instance';
-import * as symbols from '@client/symbols';
-import * as os from '@client/os';
-import { lookupUser } from '@client/scripts/lookup-user';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		MkSuperMenu,
-		FormGroup,
-		FormButton,
-		MkInfo,
-	},
-
-	provide: {
-		shouldOmitHeaderTitle: false,
-	},
-
-	props: {
-		initialPage: {
-			type: String,
-			required: false
-		}
-	},
-
-	setup(props, context) {
-		const indexInfo = {
-			title: i18n.locale.controlPanel,
-			icon: 'fas fa-cog',
-			bg: 'var(--bg)',
-			hideHeader: true,
-		};
-		const INFO = ref(indexInfo);
-		const childInfo = ref(null);
-		const page = ref(props.initialPage);
-		const narrow = ref(false);
-		const view = ref(null);
-		const el = ref(null);
-		const onInfo = (viewInfo) => {
-			if (isRef(viewInfo)) {
-				watch(viewInfo, () => {
-					childInfo.value = viewInfo.value;
-				}, { immediate: true });
-			} else {
-				childInfo.value = viewInfo;
-			}
-		};
-		const pageProps = ref({});
-
-		const isEmpty = (x: any) => x == null || x == '';
-
-		const noMaintainerInformation = ref(false);
-		const noBotProtection = ref(false);
-
-		os.api('meta', { detail: true }).then(meta => {
-			// TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する
-			noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
-			noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
-		});
-
-		const menuDef = computed(() => [{
-			title: i18n.locale.quickAction,
-			items: [{
-				type: 'button',
-				icon: 'fas fa-search',
-				text: i18n.locale.lookup,
-				action: lookup,
-			}, ...(instance.disableRegistration ? [{
-				type: 'button',
-				icon: 'fas fa-user',
-				text: i18n.locale.invite,
-				action: invite,
-			}] : [])],
-		}, {
-			title: i18n.locale.administration,
-			items: [{
-				icon: 'fas fa-tachometer-alt',
-				text: i18n.locale.dashboard,
-				to: '/admin/overview',
-				active: page.value === 'overview',
-			}, {
-				icon: 'fas fa-users',
-				text: i18n.locale.users,
-				to: '/admin/users',
-				active: page.value === 'users',
-			}, {
-				icon: 'fas fa-laugh',
-				text: i18n.locale.customEmojis,
-				to: '/admin/emojis',
-				active: page.value === 'emojis',
-			}, {
-				icon: 'fas fa-globe',
-				text: i18n.locale.federation,
-				to: '/admin/federation',
-				active: page.value === 'federation',
-			}, {
-				icon: 'fas fa-clipboard-list',
-				text: i18n.locale.jobQueue,
-				to: '/admin/queue',
-				active: page.value === 'queue',
-			}, {
-				icon: 'fas fa-cloud',
-				text: i18n.locale.files,
-				to: '/admin/files',
-				active: page.value === 'files',
-			}, {
-				icon: 'fas fa-broadcast-tower',
-				text: i18n.locale.announcements,
-				to: '/admin/announcements',
-				active: page.value === 'announcements',
-			}, {
-				icon: 'fas fa-audio-description',
-				text: i18n.locale.ads,
-				to: '/admin/ads',
-				active: page.value === 'ads',
-			}, {
-				icon: 'fas fa-exclamation-circle',
-				text: i18n.locale.abuseReports,
-				to: '/admin/abuses',
-				active: page.value === 'abuses',
-			}],
-		}, {
-			title: i18n.locale.settings,
-			items: [{
-				icon: 'fas fa-cog',
-				text: i18n.locale.general,
-				to: '/admin/settings',
-				active: page.value === 'settings',
-			}, {
-				icon: 'fas fa-cloud',
-				text: i18n.locale.files,
-				to: '/admin/files-settings',
-				active: page.value === 'files-settings',
-			}, {
-				icon: 'fas fa-envelope',
-				text: i18n.locale.emailServer,
-				to: '/admin/email-settings',
-				active: page.value === 'email-settings',
-			}, {
-				icon: 'fas fa-cloud',
-				text: i18n.locale.objectStorage,
-				to: '/admin/object-storage',
-				active: page.value === 'object-storage',
-			}, {
-				icon: 'fas fa-lock',
-				text: i18n.locale.security,
-				to: '/admin/security',
-				active: page.value === 'security',
-			}, {
-				icon: 'fas fa-bolt',
-				text: 'ServiceWorker',
-				to: '/admin/service-worker',
-				active: page.value === 'service-worker',
-			}, {
-				icon: 'fas fa-globe',
-				text: i18n.locale.relays,
-				to: '/admin/relays',
-				active: page.value === 'relays',
-			}, {
-				icon: 'fas fa-share-alt',
-				text: i18n.locale.integration,
-				to: '/admin/integrations',
-				active: page.value === 'integrations',
-			}, {
-				icon: 'fas fa-ban',
-				text: i18n.locale.instanceBlocking,
-				to: '/admin/instance-block',
-				active: page.value === 'instance-block',
-			}, {
-				icon: 'fas fa-ghost',
-				text: i18n.locale.proxyAccount,
-				to: '/admin/proxy-account',
-				active: page.value === 'proxy-account',
-			}, {
-				icon: 'fas fa-cogs',
-				text: i18n.locale.other,
-				to: '/admin/other-settings',
-				active: page.value === 'other-settings',
-			}],
-		}, {
-			title: i18n.locale.info,
-			items: [{
-				icon: 'fas fa-database',
-				text: i18n.locale.database,
-				to: '/admin/database',
-				active: page.value === 'database',
-			}],
-		}]);
-		const component = computed(() => {
-			if (page.value == null) return null;
-			switch (page.value) {
-				case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
-				case 'users': return defineAsyncComponent(() => import('./users.vue'));
-				case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
-				case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
-				case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
-				case 'files': return defineAsyncComponent(() => import('./files.vue'));
-				case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
-				case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
-				case 'database': return defineAsyncComponent(() => import('./database.vue'));
-				case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
-				case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
-				case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
-				case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
-				case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
-				case 'security': return defineAsyncComponent(() => import('./security.vue'));
-				case 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.vue'));
-				case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue'));
-				case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
-				case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
-				case 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue'));
-				case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue'));
-				case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue'));
-				case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
-				case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
-				case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
-			}
-		});
-
-		watch(component, () => {
-			pageProps.value = {};
-
-			nextTick(() => {
-				scroll(el.value, { top: 0 });
-			});
-		}, { immediate: true });
-
-		watch(() => props.initialPage, () => {
-			if (props.initialPage == null && !narrow.value) {
-				page.value = 'overview';
-			} else {
-				page.value = props.initialPage;
-				if (props.initialPage == null) {
-					INFO.value = indexInfo;
-				}
-			}
-		});
-
-		onMounted(() => {
-			narrow.value = el.value.offsetWidth < 800;
-			if (!narrow.value) {
-				page.value = 'overview';
-			}
-		});
-
-		const invite = () => {
-			os.api('admin/invite').then(x => {
-				os.dialog({
-					type: 'info',
-					text: x.code
-				});
-			}).catch(e => {
-				os.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		};
-
-		const lookup = (ev) => {
-			os.popupMenu([{
-				text: i18n.locale.user,
-				icon: 'fas fa-user',
-				action: () => {
-					lookupUser();
-				}
-			}, {
-				text: i18n.locale.note,
-				icon: 'fas fa-pencil-alt',
-				action: () => {
-					alert('TODO');
-				}
-			}, {
-				text: i18n.locale.file,
-				icon: 'fas fa-cloud',
-				action: () => {
-					alert('TODO');
-				}
-			}, {
-				text: i18n.locale.instance,
-				icon: 'fas fa-globe',
-				action: () => {
-					alert('TODO');
-				}
-			}], ev.currentTarget || ev.target);
-		};
-
-		return {
-			[symbols.PAGE_INFO]: INFO,
-			menuDef,
-			header: {
-				title: i18n.locale.controlPanel,
-			},
-			noMaintainerInformation,
-			noBotProtection,
-			page,
-			narrow,
-			view,
-			el,
-			onInfo,
-			childInfo,
-			pageProps,
-			component,
-			invite,
-			lookup,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.hiyeyicy {
-	&.wide {
-		display: flex;
-		margin: 0 auto;
-		height: 100%;
-
-		> .nav {
-			width: 32%;
-			max-width: 280px;
-			box-sizing: border-box;
-			border-right: solid 0.5px var(--divider);
-			overflow: auto;
-			height: 100%;
-		}
-
-		> .main {
-			flex: 1;
-			min-width: 0;
-		}
-	}
-
-	> .nav {
-		.lxpfedzu {
-			> .info {
-				margin: 16px 0;
-			}
-
-			> .banner {
-				margin: 16px;
-
-				> .icon {
-					display: block;
-					margin: auto;
-					height: 42px;
-					border-radius: 8px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/admin/instance-block.vue b/src/client/pages/admin/instance-block.vue
deleted file mode 100644
index 105cdb4941..0000000000
--- a/src/client/pages/admin/instance-block.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormTextarea v-model="blockedHosts">
-			<span>{{ $ts.blockedInstances }}</span>
-			<template #desc>{{ $ts.blockedInstancesDescription }}</template>
-		</FormTextarea>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormTextarea,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.instanceBlocking,
-				icon: 'fas fa-ban',
-				bg: 'var(--bg)',
-			},
-			blockedHosts: '',
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.blockedHosts = meta.blockedHosts.join('\n');
-		},
-
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				blockedHosts: this.blockedHosts.split('\n') || [],
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/instance.vue b/src/client/pages/admin/instance.vue
deleted file mode 100644
index 5572fbbf75..0000000000
--- a/src/client/pages/admin/instance.vue
+++ /dev/null
@@ -1,321 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
-	:width="520"
-	:height="500"
-	@close="$refs.dialog.close()"
-	@closed="$emit('closed')"
->
-	<template #header>{{ instance.host }}</template>
-	<div class="mk-instance-info">
-		<div class="_table section">
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.software }}</div>
-					<div class="_data">{{ instance.softwareName || '?' }}</div>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.version }}</div>
-					<div class="_data">{{ instance.softwareVersion || '?' }}</div>
-				</div>
-			</div>
-		</div>
-		<div class="_table data section">
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.registeredAt }}</div>
-					<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.following }}</div>
-					<button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.followers }}</div>
-					<button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.users }}</div>
-					<button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.notes }}</div>
-					<div class="_data">{{ number(instance.notesCount) }}</div>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.files }}</div>
-					<div class="_data">{{ number(instance.driveFiles) }}</div>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.storageUsage }}</div>
-					<div class="_data">{{ bytes(instance.driveUsage) }}</div>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.latestRequestSentAt }}</div>
-					<div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
-				</div>
-				<div class="_cell">
-					<div class="_label">{{ $ts.latestStatus }}</div>
-					<div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div>
-				</div>
-			</div>
-			<div class="_row">
-				<div class="_cell">
-					<div class="_label">{{ $ts.latestRequestReceivedAt }}</div>
-					<div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
-				</div>
-			</div>
-		</div>
-		<div class="chart">
-			<div class="header">
-				<span class="label">{{ $ts.charts }}</span>
-				<div class="selects">
-					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
-						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
-						<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
-						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
-						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
-						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
-						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
-						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
-						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
-						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
-						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
-						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
-					</MkSelect>
-					<MkSelect v-model="chartSpan" style="margin: 0;">
-						<option value="hour">{{ $ts.perHour }}</option>
-						<option value="day">{{ $ts.perDay }}</option>
-					</MkSelect>
-				</div>
-			</div>
-			<div class="chart">
-				<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
-			</div>
-		</div>
-		<div class="operations section">
-			<span class="label">{{ $ts.operations }}</span>
-			<MkSwitch v-model="isSuspended" class="switch">{{ $ts.stopActivityDelivery }}</MkSwitch>
-			<MkSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</MkSwitch>
-			<details>
-				<summary>{{ $ts.deleteAllFiles }}</summary>
-				<MkButton @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton>
-			</details>
-			<details>
-				<summary>{{ $ts.removeAllFollowing }}</summary>
-				<MkButton @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><i class="fas fa-minus-circle"></i> {{ $ts.removeAllFollowing }}</MkButton>
-				<MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo>
-			</details>
-		</div>
-		<details class="metadata section">
-			<summary class="label">{{ $ts.metadata }}</summary>
-			<pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre>
-		</details>
-	</div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkUsersDialog from '@client/components/users-dialog.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import MkInfo from '@client/components/ui/info.vue';
-import MkChart from '@client/components/chart.vue';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XModalWindow,
-		MkSelect,
-		MkButton,
-		MkSwitch,
-		MkInfo,
-		MkChart,
-	},
-
-	props: {
-		instance: {
-			type: Object,
-			required: true
-		}
-	},
-
-	emits: ['closed'],
-
-	data() {
-		return {
-			isSuspended: this.instance.isSuspended,
-			chartSrc: 'requests',
-			chartSpan: 'hour',
-		};
-	},
-
-	computed: {
-		meta() {
-			return this.$instance;
-		},
-
-		isBlocked() {
-			return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host);
-		}
-	},
-
-	watch: {
-		isSuspended() {
-			os.api('admin/federation/update-instance', {
-				host: this.instance.host,
-				isSuspended: this.isSuspended
-			});
-		},
-	},
-
-	methods: {
-		changeBlock(e) {
-			os.api('admin/update-meta', {
-				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
-			});
-		},
-
-		removeAllFollowing() {
-			os.apiWithDialog('admin/federation/remove-all-following', {
-				host: this.instance.host
-			});
-		},
-
-		deleteAllFiles() {
-			os.apiWithDialog('admin/federation/delete-all-files', {
-				host: this.instance.host
-			});
-		},
-
-		showFollowing() {
-			os.modal(MkUsersDialog, {
-				title: this.$ts.instanceFollowing,
-				pagination: {
-					endpoint: 'federation/following',
-					limit: 10,
-					params: {
-						host: this.instance.host
-					}
-				},
-				extract: item => item.follower
-			});
-		},
-
-		showFollowers() {
-			os.modal(MkUsersDialog, {
-				title: this.$ts.instanceFollowers,
-				pagination: {
-					endpoint: 'federation/followers',
-					limit: 10,
-					params: {
-						host: this.instance.host
-					}
-				},
-				extract: item => item.followee
-			});
-		},
-
-		showUsers() {
-			os.modal(MkUsersDialog, {
-				title: this.$ts.instanceUsers,
-				pagination: {
-					endpoint: 'federation/users',
-					limit: 10,
-					params: {
-						host: this.instance.host
-					}
-				}
-			});
-		},
-
-		bytes,
-
-		number
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-instance-info {
-	overflow: auto;
-
-	> .section {
-		padding: 16px 32px;
-
-		@media (max-width: 500px) {
-			padding: 8px 16px;
-		}
-
-		&:not(:first-child) {
-			border-top: solid 0.5px var(--divider);
-		}
-	}
-
-	> .chart {
-		border-top: solid 0.5px var(--divider);
-		padding: 16px 0 12px 0;
-
-		> .header {
-			padding: 0 32px;
-
-			@media (max-width: 500px) {
-				padding: 0 16px;
-			}
-
-			> .label {
-				font-size: 80%;
-				opacity: 0.7;
-			}
-
-			> .selects {
-				display: flex;
-			}
-		}
-
-		> .chart {
-			padding: 0 16px;
-
-			@media (max-width: 500px) {
-				padding: 0;
-			}
-		}
-	}
-
-	> .operations {
-		> .label {
-			font-size: 80%;
-			opacity: 0.7;
-		}
-
-		> .switch {
-			margin: 16px 0;
-		}
-	}
-
-	> .metadata {
-		> .label {
-			font-size: 80%;
-			opacity: 0.7;
-		}
-
-		> pre > code {
-			display: block;
-			max-height: 200px;
-			overflow: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/admin/integrations-discord.vue b/src/client/pages/admin/integrations-discord.vue
deleted file mode 100644
index c33b24f17f..0000000000
--- a/src/client/pages/admin/integrations-discord.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableDiscordIntegration">
-			{{ $ts.enable }}
-		</FormSwitch>
-
-		<template v-if="enableDiscordIntegration">
-			<FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
-		
-			<FormInput v-model="discordClientId">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Client ID
-			</FormInput>
-
-			<FormInput v-model="discordClientSecret">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Client Secret
-			</FormInput>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormInfo,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'Discord',
-				icon: 'fab fa-discord'
-			},
-			enableDiscordIntegration: false,
-			discordClientId: null,
-			discordClientSecret: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableDiscordIntegration = meta.enableDiscordIntegration;
-			this.discordClientId = meta.discordClientId;
-			this.discordClientSecret = meta.discordClientSecret;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableDiscordIntegration: this.enableDiscordIntegration,
-				discordClientId: this.discordClientId,
-				discordClientSecret: this.discordClientSecret,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/integrations-github.vue b/src/client/pages/admin/integrations-github.vue
deleted file mode 100644
index cdf85868ff..0000000000
--- a/src/client/pages/admin/integrations-github.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableGithubIntegration">
-			{{ $ts.enable }}
-		</FormSwitch>
-
-		<template v-if="enableGithubIntegration">
-			<FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
-		
-			<FormInput v-model="githubClientId">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Client ID
-			</FormInput>
-
-			<FormInput v-model="githubClientSecret">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Client Secret
-			</FormInput>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormInfo,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'GitHub',
-				icon: 'fab fa-github'
-			},
-			enableGithubIntegration: false,
-			githubClientId: null,
-			githubClientSecret: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableGithubIntegration = meta.enableGithubIntegration;
-			this.githubClientId = meta.githubClientId;
-			this.githubClientSecret = meta.githubClientSecret;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableGithubIntegration: this.enableGithubIntegration,
-				githubClientId: this.githubClientId,
-				githubClientSecret: this.githubClientSecret,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/integrations-twitter.vue b/src/client/pages/admin/integrations-twitter.vue
deleted file mode 100644
index ed7d097d0a..0000000000
--- a/src/client/pages/admin/integrations-twitter.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableTwitterIntegration">
-			{{ $ts.enable }}
-		</FormSwitch>
-
-		<template v-if="enableTwitterIntegration">
-			<FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
-		
-			<FormInput v-model="twitterConsumerKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Consumer Key
-			</FormInput>
-
-			<FormInput v-model="twitterConsumerSecret">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Consumer Secret
-			</FormInput>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormInfo,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'Twitter',
-				icon: 'fab fa-twitter'
-			},
-			enableTwitterIntegration: false,
-			twitterConsumerKey: null,
-			twitterConsumerSecret: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableTwitterIntegration = meta.enableTwitterIntegration;
-			this.twitterConsumerKey = meta.twitterConsumerKey;
-			this.twitterConsumerSecret = meta.twitterConsumerSecret;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableTwitterIntegration: this.enableTwitterIntegration,
-				twitterConsumerKey: this.twitterConsumerKey,
-				twitterConsumerSecret: this.twitterConsumerSecret,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/integrations.vue b/src/client/pages/admin/integrations.vue
deleted file mode 100644
index bdc2cec4d0..0000000000
--- a/src/client/pages/admin/integrations.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormLink to="/admin/integrations/twitter">
-			<i class="fab fa-twitter"></i> Twitter
-			<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
-		</FormLink>
-		<FormLink to="/admin/integrations/github">
-			<i class="fab fa-github"></i> GitHub
-			<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
-		</FormLink>
-		<FormLink to="/admin/integrations/discord">
-			<i class="fab fa-discord"></i> Discord
-			<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
-		</FormLink>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormLink,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormTextarea,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.integration,
-				icon: 'fas fa-share-alt',
-				bg: 'var(--bg)',
-			},
-			enableTwitterIntegration: false,
-			enableGithubIntegration: false,
-			enableDiscordIntegration: false,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableTwitterIntegration = meta.enableTwitterIntegration;
-			this.enableGithubIntegration = meta.enableGithubIntegration;
-			this.enableDiscordIntegration = meta.enableDiscordIntegration;
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/admin/metrics.vue b/src/client/pages/admin/metrics.vue
deleted file mode 100644
index da36f6c688..0000000000
--- a/src/client/pages/admin/metrics.vue
+++ /dev/null
@@ -1,472 +0,0 @@
-<template>
-<div class="_debobigegoItem">
-	<div class="_debobigegoLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
-	<div class="_debobigegoPanel xhexznfu">
-		<div>
-			<canvas :ref="cpumem"></canvas>
-		</div>
-		<div v-if="serverInfo">
-			<div class="_table">
-				<div class="_row">
-					<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
-					<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
-					<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
-				</div>
-			</div>
-		</div>
-	</div>
-</div>
-<div class="_debobigegoItem">
-	<div class="_debobigegoLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
-	<div class="_debobigegoPanel xhexznfu">
-		<div>
-			<canvas :ref="disk"></canvas>
-		</div>
-		<div v-if="serverInfo">
-			<div class="_table">
-				<div class="_row">
-					<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
-					<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
-					<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
-				</div>
-			</div>
-		</div>
-	</div>
-</div>
-<div class="_debobigegoItem">
-	<div class="_debobigegoLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
-	<div class="_debobigegoPanel xhexznfu">
-		<div>
-			<canvas :ref="net"></canvas>
-		</div>
-		<div v-if="serverInfo">
-			<div class="_table">
-				<div class="_row">
-					<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
-				</div>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import {
-  Chart,
-  ArcElement,
-  LineElement,
-  BarElement,
-  PointElement,
-  BarController,
-  LineController,
-  CategoryScale,
-  LinearScale,
-  Legend,
-  Title,
-  Tooltip,
-  SubTitle
-} from 'chart.js';
-import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import MkwFederation from '../../widgets/federation.vue';
-import { version, url } from '@client/config';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-import MkInstanceInfo from './instance.vue';
-
-Chart.register(
-  ArcElement,
-  LineElement,
-  BarElement,
-  PointElement,
-  BarController,
-  LineController,
-  CategoryScale,
-  LinearScale,
-  Legend,
-  Title,
-  Tooltip,
-  SubTitle
-);
-
-const alpha = (hex, a) => {
-	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
-	return `rgba(${r}, ${g}, ${b}, ${a})`;
-};
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkSelect,
-		MkInput,
-		MkContainer,
-		MkFolder,
-		MkwFederation,
-	},
-
-	data() {
-		return {
-			version,
-			url,
-			stats: null,
-			serverInfo: null,
-			connection: null,
-			queueConnection: markRaw(os.stream.useChannel('queueStats')),
-			memUsage: 0,
-			chartCpuMem: null,
-			chartNet: null,
-			jobs: [],
-			logs: [],
-			logLevel: 'all',
-			logDomain: '',
-			modLogs: [],
-			dbInfo: null,
-			overviewHeight: '1fr',
-			queueHeight: '1fr',
-			paused: false,
-		}
-	},
-
-	computed: {
-		gridColor() {
-			// TODO: var(--panel)の色が暗いか明るいかで判定する
-			return this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-		},
-	},
-
-	mounted() {
-		this.fetchJobs();
-
-		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
-		os.api('admin/server-info', {}).then(res => {
-			this.serverInfo = res;
-
-			this.connection = markRaw(os.stream.useChannel('serverStats'));
-			this.connection.on('stats', this.onStats);
-			this.connection.on('statsLog', this.onStatsLog);
-			this.connection.send('requestLog', {
-				id: Math.random().toString().substr(2, 8),
-				length: 150
-			});
-
-			this.$nextTick(() => {
-				this.queueConnection.send('requestLog', {
-					id: Math.random().toString().substr(2, 8),
-					length: 200
-				});
-			});
-		});
-	},
-
-	beforeUnmount() {
-		if (this.connection) {
-			this.connection.off('stats', this.onStats);
-			this.connection.off('statsLog', this.onStatsLog);
-			this.connection.dispose();
-		}
-		this.queueConnection.dispose();
-	},
-
-	methods: {
-		cpumem(el) {
-			if (this.chartCpuMem != null) return;
-			this.chartCpuMem = markRaw(new Chart(el, {
-				type: 'line',
-				data: {
-					labels: [],
-					datasets: [{
-						label: 'CPU',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#86b300',
-						backgroundColor: alpha('#86b300', 0.1),
-						data: []
-					}, {
-						label: 'MEM (active)',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#935dbf',
-						backgroundColor: alpha('#935dbf', 0.02),
-						data: []
-					}, {
-						label: 'MEM (used)',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#935dbf',
-						borderDash: [5, 5],
-						fill: false,
-						data: []
-					}]
-				},
-				options: {
-					aspectRatio: 3,
-					layout: {
-						padding: {
-							left: 16,
-							right: 16,
-							top: 16,
-							bottom: 0
-						}
-					},
-					legend: {
-						position: 'bottom',
-						labels: {
-							boxWidth: 16,
-						}
-					},
-					scales: {
-						x: {
-							gridLines: {
-								display: false,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false,
-							}
-						},
-						y: {
-							position: 'right',
-							gridLines: {
-								display: true,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false,
-								max: 100
-							}
-						}
-					},
-					tooltips: {
-						intersect: false,
-						mode: 'index',
-					}
-				}
-			}));
-		},
-
-		net(el) {
-			if (this.chartNet != null) return;
-			this.chartNet = markRaw(new Chart(el, {
-				type: 'line',
-				data: {
-					labels: [],
-					datasets: [{
-						label: 'In',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#94a029',
-						backgroundColor: alpha('#94a029', 0.1),
-						data: []
-					}, {
-						label: 'Out',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#ff9156',
-						backgroundColor: alpha('#ff9156', 0.1),
-						data: []
-					}]
-				},
-				options: {
-					aspectRatio: 3,
-					layout: {
-						padding: {
-							left: 16,
-							right: 16,
-							top: 16,
-							bottom: 0
-						}
-					},
-					legend: {
-						position: 'bottom',
-						labels: {
-							boxWidth: 16,
-						}
-					},
-					scales: {
-						x: {
-							gridLines: {
-								display: false,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false
-							}
-						},
-						y: {
-							position: 'right',
-							gridLines: {
-								display: true,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false,
-							}
-						}
-					},
-					tooltips: {
-						intersect: false,
-						mode: 'index',
-					}
-				}
-			}));
-		},
-
-		disk(el) {
-			if (this.chartDisk != null) return;
-			this.chartDisk = markRaw(new Chart(el, {
-				type: 'line',
-				data: {
-					labels: [],
-					datasets: [{
-						label: 'Read',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#94a029',
-						backgroundColor: alpha('#94a029', 0.1),
-						data: []
-					}, {
-						label: 'Write',
-						pointRadius: 0,
-						tension: 0,
-						borderWidth: 2,
-						borderColor: '#ff9156',
-						backgroundColor: alpha('#ff9156', 0.1),
-						data: []
-					}]
-				},
-				options: {
-					aspectRatio: 3,
-					layout: {
-						padding: {
-							left: 16,
-							right: 16,
-							top: 16,
-							bottom: 0
-						}
-					},
-					legend: {
-						position: 'bottom',
-						labels: {
-							boxWidth: 16,
-						}
-					},
-					scales: {
-						x: {
-							gridLines: {
-								display: false,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false
-							}
-						},
-						y: {
-							position: 'right',
-							gridLines: {
-								display: true,
-								color: this.gridColor,
-								zeroLineColor: this.gridColor,
-							},
-							ticks: {
-								display: false,
-							}
-						}
-					},
-					tooltips: {
-						intersect: false,
-						mode: 'index',
-					}
-				}
-			}));
-		},
-
-		fetchJobs() {
-			os.api('admin/queue/deliver-delayed', {}).then(jobs => {
-				this.jobs = jobs;
-			});
-		},
-
-		onStats(stats) {
-			if (this.paused) return;
-
-			const cpu = (stats.cpu * 100).toFixed(0);
-			const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
-			const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
-			this.memUsage = stats.mem.active;
-
-			this.chartCpuMem.data.labels.push('');
-			this.chartCpuMem.data.datasets[0].data.push(cpu);
-			this.chartCpuMem.data.datasets[1].data.push(memActive);
-			this.chartCpuMem.data.datasets[2].data.push(memUsed);
-			this.chartNet.data.labels.push('');
-			this.chartNet.data.datasets[0].data.push(stats.net.rx);
-			this.chartNet.data.datasets[1].data.push(stats.net.tx);
-			this.chartDisk.data.labels.push('');
-			this.chartDisk.data.datasets[0].data.push(stats.fs.r);
-			this.chartDisk.data.datasets[1].data.push(stats.fs.w);
-			if (this.chartCpuMem.data.datasets[0].data.length > 150) {
-				this.chartCpuMem.data.labels.shift();
-				this.chartCpuMem.data.datasets[0].data.shift();
-				this.chartCpuMem.data.datasets[1].data.shift();
-				this.chartCpuMem.data.datasets[2].data.shift();
-				this.chartNet.data.labels.shift();
-				this.chartNet.data.datasets[0].data.shift();
-				this.chartNet.data.datasets[1].data.shift();
-				this.chartDisk.data.labels.shift();
-				this.chartDisk.data.datasets[0].data.shift();
-				this.chartDisk.data.datasets[1].data.shift();
-			}
-			this.chartCpuMem.update();
-			this.chartNet.update();
-			this.chartDisk.update();
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of [...statsLog].reverse()) {
-				this.onStats(stats);
-			}
-		},
-
-		bytes,
-
-		number,
-
-		pause() {
-			this.paused = true;
-		},
-
-		resume() {
-			this.paused = false;
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xhexznfu {
-	> div:nth-child(2) {
-		padding: 16px;
-		border-top: solid 0.5px var(--divider);
-	}
-}
-</style>
diff --git a/src/client/pages/admin/object-storage.vue b/src/client/pages/admin/object-storage.vue
deleted file mode 100644
index 2d765270e6..0000000000
--- a/src/client/pages/admin/object-storage.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
-
-		<template v-if="useObjectStorage">
-			<FormInput v-model="objectStorageBaseUrl">
-				<span>{{ $ts.objectStorageBaseUrl }}</span>
-				<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStorageBucket">
-				<span>{{ $ts.objectStorageBucket }}</span>
-				<template #desc>{{ $ts.objectStorageBucketDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStoragePrefix">
-				<span>{{ $ts.objectStoragePrefix }}</span>
-				<template #desc>{{ $ts.objectStoragePrefixDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStorageEndpoint">
-				<span>{{ $ts.objectStorageEndpoint }}</span>
-				<template #desc>{{ $ts.objectStorageEndpointDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStorageRegion">
-				<span>{{ $ts.objectStorageRegion }}</span>
-				<template #desc>{{ $ts.objectStorageRegionDesc }}</template>
-			</FormInput>
-
-			<FormInput v-model="objectStorageAccessKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				<span>Access key</span>
-			</FormInput>
-
-			<FormInput v-model="objectStorageSecretKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				<span>Secret key</span>
-			</FormInput>
-
-			<FormSwitch v-model="objectStorageUseSSL">
-				{{ $ts.objectStorageUseSSL }}
-				<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template>
-			</FormSwitch>
-
-			<FormSwitch v-model="objectStorageUseProxy">
-				{{ $ts.objectStorageUseProxy }}
-				<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template>
-			</FormSwitch>
-
-			<FormSwitch v-model="objectStorageSetPublicRead">
-				{{ $ts.objectStorageSetPublicRead }}
-			</FormSwitch>
-
-			<FormSwitch v-model="objectStorageS3ForcePathStyle">
-				s3ForcePathStyle
-			</FormSwitch>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.objectStorage,
-				icon: 'fas fa-cloud',
-				bg: 'var(--bg)',
-			},
-			useObjectStorage: false,
-			objectStorageBaseUrl: null,
-			objectStorageBucket: null,
-			objectStoragePrefix: null,
-			objectStorageEndpoint: null,
-			objectStorageRegion: null,
-			objectStoragePort: null,
-			objectStorageAccessKey: null,
-			objectStorageSecretKey: null,
-			objectStorageUseSSL: false,
-			objectStorageUseProxy: false,
-			objectStorageSetPublicRead: false,
-			objectStorageS3ForcePathStyle: true,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.useObjectStorage = meta.useObjectStorage;
-			this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
-			this.objectStorageBucket = meta.objectStorageBucket;
-			this.objectStoragePrefix = meta.objectStoragePrefix;
-			this.objectStorageEndpoint = meta.objectStorageEndpoint;
-			this.objectStorageRegion = meta.objectStorageRegion;
-			this.objectStoragePort = meta.objectStoragePort;
-			this.objectStorageAccessKey = meta.objectStorageAccessKey;
-			this.objectStorageSecretKey = meta.objectStorageSecretKey;
-			this.objectStorageUseSSL = meta.objectStorageUseSSL;
-			this.objectStorageUseProxy = meta.objectStorageUseProxy;
-			this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
-			this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				useObjectStorage: this.useObjectStorage,
-				objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
-				objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
-				objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
-				objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
-				objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
-				objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
-				objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
-				objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
-				objectStorageUseSSL: this.objectStorageUseSSL,
-				objectStorageUseProxy: this.objectStorageUseProxy,
-				objectStorageSetPublicRead: this.objectStorageSetPublicRead,
-				objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/other-settings.vue b/src/client/pages/admin/other-settings.vue
deleted file mode 100644
index 4e55df41fb..0000000000
--- a/src/client/pages/admin/other-settings.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormGroup>
-			<FormInput v-model="summalyProxy">
-				<template #prefix><i class="fas fa-link"></i></template>
-				Summaly Proxy URL
-			</FormInput>
-		</FormGroup>
-		<FormGroup>
-			<FormInput v-model="deeplAuthKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				DeepL Auth Key
-			</FormInput>
-			<FormSwitch v-model="deeplIsPro">
-				Pro account
-			</FormSwitch>
-		</FormGroup>
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.other,
-				icon: 'fas fa-cogs',
-				bg: 'var(--bg)',
-			},
-			summalyProxy: '',
-			deeplAuthKey: '',
-			deeplIsPro: false,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.summalyProxy = meta.summalyProxy;
-			this.deeplAuthKey = meta.deeplAuthKey;
-			this.deeplIsPro = meta.deeplIsPro;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				summalyProxy: this.summalyProxy,
-				deeplAuthKey: this.deeplAuthKey,
-				deeplIsPro: this.deeplIsPro,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/overview.vue b/src/client/pages/admin/overview.vue
deleted file mode 100644
index ced200351e..0000000000
--- a/src/client/pages/admin/overview.vue
+++ /dev/null
@@ -1,236 +0,0 @@
-<template>
-<div class="edbbcaef" v-size="{ max: [740] }">
-	<div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
-		<div class="number _panel">
-			<div class="label">Users</div>
-			<div class="value _monospace">
-				{{ number(stats.originalUsersCount) }}
-				<MkNumberDiff v-if="usersComparedToThePrevDay != null" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
-			</div>
-		</div>
-		<div class="number _panel">
-			<div class="label">Notes</div>
-			<div class="value _monospace">
-				{{ number(stats.originalNotesCount) }}
-				<MkNumberDiff v-if="notesComparedToThePrevDay != null" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
-			</div>
-		</div>
-	</div>
-
-	<MkContainer :foldable="true" class="charts">
-		<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
-		<div style="padding-top: 12px;">
-			<MkInstanceStats :chart-limit="500" :detailed="true"/>
-		</div>
-	</MkContainer>
-
-	<div class="queue">
-		<MkContainer :foldable="true" :thin="true" class="deliver">
-			<template #header>Queue: deliver</template>
-			<MkQueueChart :connection="queueStatsConnection" domain="deliver"/>
-		</MkContainer>
-		<MkContainer :foldable="true" :thin="true" class="inbox">
-			<template #header>Queue: inbox</template>
-			<MkQueueChart :connection="queueStatsConnection" domain="inbox"/>
-		</MkContainer>
-	</div>
-
-		<!--<XMetrics/>-->
-
-	<MkFolder style="margin: var(--margin)">
-		<template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
-		<div class="cfcdecdf">
-			<div class="number _panel">
-				<div class="label">Misskey</div>
-				<div class="value _monospace">{{ version }}</div>
-			</div>
-			<div class="number _panel" v-if="serverInfo">
-				<div class="label">Node.js</div>
-				<div class="value _monospace">{{ serverInfo.node }}</div>
-			</div>
-			<div class="number _panel" v-if="serverInfo">
-				<div class="label">PostgreSQL</div>
-				<div class="value _monospace">{{ serverInfo.psql }}</div>
-			</div>
-			<div class="number _panel" v-if="serverInfo">
-				<div class="label">Redis</div>
-				<div class="value _monospace">{{ serverInfo.redis }}</div>
-			</div>
-			<div class="number _panel">
-				<div class="label">Vue</div>
-				<div class="value _monospace">{{ vueVersion }}</div>
-			</div>
-		</div>
-	</MkFolder>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw, version as vueVersion } from 'vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import MkInstanceStats from '@client/components/instance-stats.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkNumberDiff from '@client/components/number-diff.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import MkQueueChart from '@client/components/queue-chart.vue';
-import { version, url } from '@client/config';
-import bytes from '@client/filters/bytes';
-import number from '@client/filters/number';
-import MkInstanceInfo from './instance.vue';
-import XMetrics from './metrics.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkNumberDiff,
-		FormKeyValueView,
-		MkInstanceStats,
-		MkContainer,
-		MkFolder,
-		MkQueueChart,
-		XMetrics,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.dashboard,
-				icon: 'fas fa-tachometer-alt',
-				bg: 'var(--bg)',
-			},
-			version,
-			vueVersion,
-			url,
-			stats: null,
-			meta: null,
-			serverInfo: null,
-			usersComparedToThePrevDay: null,
-			notesComparedToThePrevDay: null,
-			fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
-			fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
-			queueStatsConnection: markRaw(os.stream.useChannel('queueStats')),
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-		
-		os.api('stats', {}).then(stats => {
-			this.stats = stats;
-
-			os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
-				this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1];
-			});
-
-			os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
-				this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1];
-			});
-		});
-
-		os.api('admin/server-info', {}).then(serverInfo => {
-			this.serverInfo = serverInfo;
-		});
-
-		this.$nextTick(() => {
-			this.queueStatsConnection.send('requestLog', {
-				id: Math.random().toString().substr(2, 8),
-				length: 200
-			});
-		});
-	},
-
-	beforeUnmount() {
-		this.queueStatsConnection.dispose();
-	},
-
-	methods: {
-		async showInstanceInfo(q) {
-			let instance = q;
-			if (typeof q === 'string') {
-				instance = await os.api('federation/show-instance', {
-					host: q
-				});
-			}
-			os.popup(MkInstanceInfo, {
-				instance: instance
-			}, {}, 'closed');
-		},
-
-		bytes,
-
-		number,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.edbbcaef {
-	.cfcdecdf {
-		display: grid;
-		grid-gap: 8px;
-		grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
-
-		> .number {
-			padding: 12px 16px;
-
-			> .label {
-				opacity: 0.7;
-				font-size: 0.8em;
-			}
-
-			> .value {
-				font-weight: bold;
-				font-size: 1.2em;
-
-				> .diff {
-					font-size: 0.8em;
-				}
-			}
-		}
-	}
-
-	> .charts {
-		margin: var(--margin);
-	}
-
-	> .queue {
-		margin: var(--margin);
-		display: flex;
-
-		> .deliver,
-		> .inbox {
-			flex: 1;
-			width: 50%;
-
-			&:not(:first-child) {
-				margin-left: var(--margin);
-			}
-		}
-	}
-
-	&.max-width_740px {
-		> .queue {
-			display: block;
-
-			> .deliver,
-			> .inbox {
-				width: 100%;
-
-				&:not(:first-child) {
-					margin-top: var(--margin);
-					margin-left: 0;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/admin/proxy-account.vue b/src/client/pages/admin/proxy-account.vue
deleted file mode 100644
index b1ece19710..0000000000
--- a/src/client/pages/admin/proxy-account.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>{{ $ts.proxyAccount }}</template>
-				<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
-			</FormKeyValueView>
-			<template #caption>{{ $ts.proxyAccountDescription }}</template>
-		</FormGroup>
-
-		<FormButton @click="chooseProxyAccount" primary>{{ $ts.selectAccount }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormKeyValueView,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormTextarea,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.proxyAccount,
-				icon: 'fas fa-ghost',
-				bg: 'var(--bg)',
-			},
-			proxyAccount: null,
-			proxyAccountId: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.proxyAccountId = meta.proxyAccountId;
-			if (this.proxyAccountId) {
-				this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
-			}
-		},
-
-		chooseProxyAccount() {
-			os.selectUser().then(user => {
-				this.proxyAccount = user;
-				this.proxyAccountId = user.id;
-				this.save();
-			});
-		},
-
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				proxyAccountId: this.proxyAccountId,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/queue.chart.vue b/src/client/pages/admin/queue.chart.vue
deleted file mode 100644
index 38cd0b10ef..0000000000
--- a/src/client/pages/admin/queue.chart.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-<div class="_debobigegoItem">
-	<div class="_debobigegoLabel"><slot name="title"></slot></div>
-	<div class="_debobigegoPanel pumxzjhg">
-		<div class="_table status">
-			<div class="_row">
-				<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
-				<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
-				<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
-				<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
-			</div>
-		</div>
-		<div class="">
-			<MkQueueChart :domain="domain" :connection="connection"/>
-		</div>
-		<div class="jobs">
-			<div v-if="jobs.length > 0">
-				<div v-for="job in jobs" :key="job[0]">
-					<span>{{ job[0] }}</span>
-					<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
-				</div>
-			</div>
-			<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
-import number from '@client/filters/number';
-import MkQueueChart from '@client/components/queue-chart.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkQueueChart
-	},
-
-	props: {
-		domain: {
-			type: String,
-			required: true,
-		},
-		connection: {
-			required: true,
-		},
-	},
-
-	setup(props) {
-		const activeSincePrevTick = ref(0);
-		const active = ref(0);
-		const waiting = ref(0);
-		const delayed = ref(0);
-		const jobs = ref([]);
-
-		onMounted(() => {
-			os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
-				jobs.value = result;
-			});
-
-			const onStats = (stats) => {
-				activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
-				active.value = stats[props.domain].active;
-				waiting.value = stats[props.domain].waiting;
-				delayed.value = stats[props.domain].delayed;
-			};
-
-			props.connection.on('stats', onStats);
-
-			onUnmounted(() => {
-				props.connection.off('stats', onStats);
-			});
-		});
-
-		return {
-			jobs,
-			activeSincePrevTick,
-			active,
-			waiting,
-			delayed,
-			number,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.pumxzjhg {
-	> .status {
-		padding: 16px;
-		border-bottom: solid 0.5px var(--divider);
-	}
-
-	> .jobs {
-		padding: 16px;
-		border-top: solid 0.5px var(--divider);
-		max-height: 180px;
-		overflow: auto;
-	}
-}
-</style>
diff --git a/src/client/pages/admin/queue.vue b/src/client/pages/admin/queue.vue
deleted file mode 100644
index f88825eb19..0000000000
--- a/src/client/pages/admin/queue.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-<FormBase>
-	<XQueue :connection="connection" domain="inbox">
-		<template #title>In</template>
-	</XQueue>
-	<XQueue :connection="connection" domain="deliver">
-		<template #title>Out</template>
-	</XQueue>
-	<FormButton @click="clear()" danger><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import XQueue from './queue.chart.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		MkButton,
-		XQueue,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.jobQueue,
-				icon: 'fas fa-clipboard-list',
-				bg: 'var(--bg)',
-			},
-			connection: markRaw(os.stream.useChannel('queueStats')),
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
-		this.$nextTick(() => {
-			this.connection.send('requestLog', {
-				id: Math.random().toString().substr(2, 8),
-				length: 200
-			});
-		});
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		clear() {
-			os.dialog({
-				type: 'warning',
-				title: this.$ts.clearQueueConfirmTitle,
-				text: this.$ts.clearQueueConfirmText,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.apiWithDialog('admin/queue/clear', {});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/relays.vue b/src/client/pages/admin/relays.vue
deleted file mode 100644
index 7d7888eaa8..0000000000
--- a/src/client/pages/admin/relays.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<template>
-<FormBase class="relaycxt">
-	<FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton>
-
-	<div class="_debobigegoItem" v-for="relay in relays" :key="relay.inbox">
-		<div class="_debobigegoPanel" style="padding: 16px;">
-			<div>{{ relay.inbox }}</div>
-			<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
-			<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
-		</div>
-	</div>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		MkButton,
-		MkInput,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.relays,
-				icon: 'fas fa-globe',
-				bg: 'var(--bg)',
-			},
-			relays: [],
-			inbox: '',
-		}
-	},
-
-	created() {
-		this.refresh();
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async addRelay() {
-			const { canceled, result: inbox } = await os.dialog({
-				title: this.$ts.addRelay,
-				input: {
-					placeholder: this.$ts.inboxUrl
-				}
-			});
-			if (canceled) return;
-			os.api('admin/relays/add', {
-				inbox
-			}).then((relay: any) => {
-				this.refresh();
-			}).catch((e: any) => {
-				os.dialog({
-					type: 'error',
-					text: e.message || e
-				});
-			});
-		},
-
-		remove(inbox: string) {
-			os.api('admin/relays/remove', {
-				inbox
-			}).then(() => {
-				this.refresh();
-			}).catch((e: any) => {
-				os.dialog({
-					type: 'error',
-					text: e.message || e
-				});
-			});
-		},
-
-		refresh() {
-			os.api('admin/relays/list').then((relays: any) => {
-				this.relays = relays;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/admin/security.vue b/src/client/pages/admin/security.vue
deleted file mode 100644
index 4365b6800c..0000000000
--- a/src/client/pages/admin/security.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormLink to="/admin/bot-protection">
-			<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
-			<template #suffix v-if="enableHcaptcha">hCaptcha</template>
-			<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
-			<template #suffix v-else>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
-		</FormLink>
-
-		<FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
-
-		<FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormLink,
-		FormSwitch,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.security,
-				icon: 'fas fa-lock',
-				bg: 'var(--bg)',
-			},
-			enableHcaptcha: false,
-			enableRecaptcha: false,
-			enableRegistration: false,
-			emailRequiredForSignup: false,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableHcaptcha = meta.enableHcaptcha;
-			this.enableRecaptcha = meta.enableRecaptcha;
-			this.enableRegistration = !meta.disableRegistration;
-			this.emailRequiredForSignup = meta.emailRequiredForSignup;
-		},
-	
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				disableRegistration: !this.enableRegistration,
-				emailRequiredForSignup: this.emailRequiredForSignup,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/service-worker.vue b/src/client/pages/admin/service-worker.vue
deleted file mode 100644
index 430e02ad2e..0000000000
--- a/src/client/pages/admin/service-worker.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormSwitch v-model="enableServiceWorker">
-			{{ $ts.enableServiceworker }}
-			<template #desc>{{ $ts.serviceworkerInfo }}</template>
-		</FormSwitch>
-
-		<template v-if="enableServiceWorker">
-			<FormInput v-model="swPublicKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Public key
-			</FormInput>
-
-			<FormInput v-model="swPrivateKey">
-				<template #prefix><i class="fas fa-key"></i></template>
-				Private key
-			</FormInput>
-		</template>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'ServiceWorker',
-				icon: 'fas fa-bolt',
-				bg: 'var(--bg)',
-			},
-			enableServiceWorker: false,
-			swPublicKey: null,
-			swPrivateKey: null,
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.enableServiceWorker = meta.enableServiceWorker;
-			this.swPublicKey = meta.swPublickey;
-			this.swPrivateKey = meta.swPrivateKey;
-		},
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				enableServiceWorker: this.enableServiceWorker,
-				swPublicKey: this.swPublicKey,
-				swPrivateKey: this.swPrivateKey,
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/settings.vue b/src/client/pages/admin/settings.vue
deleted file mode 100644
index 7bd363e5f3..0000000000
--- a/src/client/pages/admin/settings.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormInput v-model="name">
-			<span>{{ $ts.instanceName }}</span>
-		</FormInput>
-
-		<FormTextarea v-model="description">
-			<span>{{ $ts.instanceDescription }}</span>
-		</FormTextarea>
-
-		<FormInput v-model="iconUrl">
-			<template #prefix><i class="fas fa-link"></i></template>
-			<span>{{ $ts.iconUrl }}</span>
-		</FormInput>
-
-		<FormInput v-model="bannerUrl">
-			<template #prefix><i class="fas fa-link"></i></template>
-			<span>{{ $ts.bannerUrl }}</span>
-		</FormInput>
-
-		<FormInput v-model="backgroundImageUrl">
-			<template #prefix><i class="fas fa-link"></i></template>
-			<span>{{ $ts.backgroundImageUrl }}</span>
-		</FormInput>
-
-		<FormInput v-model="tosUrl">
-			<template #prefix><i class="fas fa-link"></i></template>
-			<span>{{ $ts.tosUrl }}</span>
-		</FormInput>
-
-		<FormInput v-model="maintainerName">
-			<span>{{ $ts.maintainerName }}</span>
-		</FormInput>
-
-		<FormInput v-model="maintainerEmail" type="email">
-			<template #prefix><i class="fas fa-envelope"></i></template>
-			<span>{{ $ts.maintainerEmail }}</span>
-		</FormInput>
-
-		<FormTextarea v-model="pinnedUsers">
-			<span>{{ $ts.pinnedUsers }}</span>
-			<template #desc>{{ $ts.pinnedUsersDescription }}</template>
-		</FormTextarea>
-
-		<FormInput v-model="maxNoteTextLength" type="number">
-			<template #prefix><i class="fas fa-pencil-alt"></i></template>
-			<span>{{ $ts.maxNoteTextLength }}</span>
-		</FormInput>
-
-		<FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
-		<FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
-		<FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo>
-
-		<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { fetchInstance } from '@client/instance';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormInput,
-		FormBase,
-		FormGroup,
-		FormButton,
-		FormTextarea,
-		FormInfo,
-		FormSuspense,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.general,
-				icon: 'fas fa-cog',
-				bg: 'var(--bg)',
-			},
-			name: null,
-			description: null,
-			tosUrl: null as string | null,
-			maintainerName: null,
-			maintainerEmail: null,
-			iconUrl: null,
-			bannerUrl: null,
-			backgroundImageUrl: null,
-			maxNoteTextLength: 0,
-			enableLocalTimeline: false,
-			enableGlobalTimeline: false,
-			pinnedUsers: '',
-		}
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async init() {
-			const meta = await os.api('meta', { detail: true });
-			this.name = meta.name;
-			this.description = meta.description;
-			this.tosUrl = meta.tosUrl;
-			this.iconUrl = meta.iconUrl;
-			this.bannerUrl = meta.bannerUrl;
-			this.backgroundImageUrl = meta.backgroundImageUrl;
-			this.maintainerName = meta.maintainerName;
-			this.maintainerEmail = meta.maintainerEmail;
-			this.maxNoteTextLength = meta.maxNoteTextLength;
-			this.enableLocalTimeline = !meta.disableLocalTimeline;
-			this.enableGlobalTimeline = !meta.disableGlobalTimeline;
-			this.pinnedUsers = meta.pinnedUsers.join('\n');
-		},
-
-		save() {
-			os.apiWithDialog('admin/update-meta', {
-				name: this.name,
-				description: this.description,
-				tosUrl: this.tosUrl,
-				iconUrl: this.iconUrl,
-				bannerUrl: this.bannerUrl,
-				backgroundImageUrl: this.backgroundImageUrl,
-				maintainerName: this.maintainerName,
-				maintainerEmail: this.maintainerEmail,
-				maxNoteTextLength: this.maxNoteTextLength,
-				disableLocalTimeline: !this.enableLocalTimeline,
-				disableGlobalTimeline: !this.enableGlobalTimeline,
-				pinnedUsers: this.pinnedUsers.split('\n'),
-			}).then(() => {
-				fetchInstance();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/admin/users.vue b/src/client/pages/admin/users.vue
deleted file mode 100644
index 37a54d2de3..0000000000
--- a/src/client/pages/admin/users.vue
+++ /dev/null
@@ -1,254 +0,0 @@
-<template>
-<div class="lknzcolw">
-	<div class="users">
-		<div class="inputs">
-			<MkSelect v-model="sort" style="flex: 1;">
-				<template #label>{{ $ts.sort }}</template>
-				<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
-				<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
-			</MkSelect>
-			<MkSelect v-model="state" style="flex: 1;">
-				<template #label>{{ $ts.state }}</template>
-				<option value="all">{{ $ts.all }}</option>
-				<option value="available">{{ $ts.normal }}</option>
-				<option value="admin">{{ $ts.administrator }}</option>
-				<option value="moderator">{{ $ts.moderator }}</option>
-				<option value="silenced">{{ $ts.silence }}</option>
-				<option value="suspended">{{ $ts.suspend }}</option>
-			</MkSelect>
-			<MkSelect v-model="origin" style="flex: 1;">
-				<template #label>{{ $ts.instance }}</template>
-				<option value="combined">{{ $ts.all }}</option>
-				<option value="local">{{ $ts.local }}</option>
-				<option value="remote">{{ $ts.remote }}</option>
-			</MkSelect>
-		</div>
-		<div class="inputs">
-			<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
-				<template #prefix>@</template>
-				<template #label>{{ $ts.username }}</template>
-			</MkInput>
-			<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
-				<template #prefix>@</template>
-				<template #label>{{ $ts.host }}</template>
-			</MkInput>
-		</div>
-
-		<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users">
-			<button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)">
-				<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
-				<div class="body">
-					<header>
-						<MkUserName class="name" :user="user"/>
-						<span class="acct">@{{ acct(user) }}</span>
-						<span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span>
-						<span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span>
-						<span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span>
-						<span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span>
-					</header>
-					<div>
-						<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
-					</div>
-					<div>
-						<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
-					</div>
-				</div>
-			</button>
-		</MkPagination>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import { acct } from '@client/filters/user';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { lookupUser } from '@client/scripts/lookup-user';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkPagination,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.users,
-				icon: 'fas fa-users',
-				bg: 'var(--bg)',
-				actions: [{
-					icon: 'fas fa-search',
-					text: this.$ts.search,
-					handler: this.searchUser
-				}, {
-					asFullButton: true,
-					icon: 'fas fa-plus',
-					text: this.$ts.addUser,
-					handler: this.addUser
-				}, {
-					asFullButton: true,
-					icon: 'fas fa-search',
-					text: this.$ts.lookup,
-					handler: this.lookupUser
-				}],
-			},
-			sort: '+createdAt',
-			state: 'all',
-			origin: 'local',
-			searchUsername: '',
-			searchHost: '',
-			pagination: {
-				endpoint: 'admin/show-users',
-				limit: 10,
-				params: () => ({
-					sort: this.sort,
-					state: this.state,
-					origin: this.origin,
-					username: this.searchUsername,
-					hostname: this.searchHost,
-				}),
-				offsetMode: true
-			},
-		}
-	},
-
-	watch: {
-		sort() {
-			this.$refs.users.reload();
-		},
-		state() {
-			this.$refs.users.reload();
-		},
-		origin() {
-			this.$refs.users.reload();
-		},
-	},
-
-	async mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		lookupUser,
-
-		searchUser() {
-			os.selectUser().then(user => {
-				this.show(user);
-			});
-		},
-
-		async addUser() {
-			const { canceled: canceled1, result: username } = await os.dialog({
-				title: this.$ts.username,
-				input: true
-			});
-			if (canceled1) return;
-
-			const { canceled: canceled2, result: password } = await os.dialog({
-				title: this.$ts.password,
-				input: { type: 'password' }
-			});
-			if (canceled2) return;
-
-			os.apiWithDialog('admin/accounts/create', {
-				username: username,
-				password: password,
-			}).then(res => {
-				this.$refs.users.reload();
-			});
-		},
-
-		show(user) {
-			os.pageWindow(`/user-info/${user.id}`);
-		},
-
-		acct
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.lknzcolw {
-	> .users {
-		margin: var(--margin);
-
-		> .inputs {
-			display: flex;
-			margin-bottom: 16px;
-
-			> * {
-				margin-right: 16px;
-
-				&:last-child {
-					margin-right: 0;
-				}
-			}
-		}
-	
-		> .users {
-			margin-top: var(--margin);
-
-			> .user {
-				display: flex;
-				width: 100%;
-				box-sizing: border-box;
-				text-align: left;
-				align-items: center;
-				padding: 16px;
-
-				&:hover {
-					color: var(--accent);
-				}
-
-				> .avatar {
-					width: 60px;
-					height: 60px;
-				}
-
-				> .body {
-					margin-left: 0.3em;
-					padding: 0 8px;
-					flex: 1;
-
-					@media (max-width: 500px) {
-						font-size: 14px;
-					}
-
-					> header {
-						> .name {
-							font-weight: bold;
-						}
-
-						> .acct {
-							margin-left: 8px;
-							opacity: 0.7;
-						}
-
-						> .staff {
-							margin-left: 0.5em;
-							color: var(--badge);
-						}
-
-						> .punished {
-							margin-left: 0.5em;
-							color: #4dabf7;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/advanced-theme-editor.vue b/src/client/pages/advanced-theme-editor.vue
deleted file mode 100644
index 8a63d74887..0000000000
--- a/src/client/pages/advanced-theme-editor.vue
+++ /dev/null
@@ -1,352 +0,0 @@
-<template>
-<div class="t9makv94">
-	<section class="_section">
-		<div class="_content">
-			<details>
-				<summary>{{ $ts.import }}</summary>
-				<MkTextarea v-model="themeToImport">
-					{{ $ts._theme.importInfo }}
-				</MkTextarea>
-				<MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
-			</details>
-		</div>
-	</section>
-	<section class="_section">
-		<div class="_content _card _gap">
-			<div class="_content">
-				<MkInput v-model="name" required><span>{{ $ts.name }}</span></MkInput>
-				<MkInput v-model="author" required><span>{{ $ts.author }}</span></MkInput>
-				<MkTextarea v-model="description"><span>{{ $ts.description }}</span></MkTextarea>
-				<div class="_inputs">
-					<div v-text="$ts._theme.base" />
-					<MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio>
-					<MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio>
-				</div>
-			</div>
-		</div>
-		<div class="_content _card _gap">
-			<div class="list-view _content">
-				<div class="item" v-for="([ k, v ], i) in theme" :key="k">
-					<div class="_inputs">
-						<div>
-							{{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }}
-							<button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" />
-						</div>
-						<div>
-							<div class="type" @click="chooseType($event, i)">
-								{{ getTypeOf(v) }} <i class="fas fa-chevron-down"></i>
-							</div>
-							<!-- default -->
-							<div v-if="v === null" v-text="baseProps[k]" class="default-value" />
-							<!-- color -->
-							<div v-else-if="typeof v === 'string'" class="color">
-								<input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
-								<MkInput class="select" :value="v" @update:modelValue="colorChanged($event, i)"/>
-							</div>
-							<!-- ref const -->
-							<MkInput v-else-if="v.type === 'refConst'" v-model="v.key">
-								<template #prefix>$</template>
-								<span>{{ $ts.name }}</span>
-							</MkInput>
-							<!-- ref props -->
-							<MkSelect class="select" v-else-if="v.type === 'refProp'" v-model="v.key">
-								<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
-							</MkSelect>
-							<!-- func -->
-							<template v-else-if="v.type === 'func'">
-								<MkSelect class="select" v-model="v.name">
-									<template #label>{{ $ts._theme.funcKind }}</template>
-									<option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option>
-								</MkSelect>
-								<MkInput type="number" v-model="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput>
-								<MkSelect class="select" v-model="v.value">
-									<template #label>{{ $ts._theme.basedProp }}</template>
-									<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
-								</MkSelect>
-							</template>
-							<!-- CSS -->
-							<MkInput v-else-if="v.type === 'css'" v-model="v.value">
-								<span>CSS</span>
-							</MkInput>
-						</div>
-					</div>
-				</div>
-				<MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton>
-			</div>
-		</div>
-	</section>
-	<section class="_section">
-		<details class="_content">
-			<summary>{{ $ts.sample }}</summary>
-			<MkSample/>
-		</details>
-	</section>
-	<section class="_section">
-		<div class="_content">
-			<MkButton inline @click="preview">{{ $ts.preview }}</MkButton>
-			<MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton>
-		</div>
-	</section>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import { toUnicode } from 'punycode/';
-
-import MkRadio from '@client/components/form/radio.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkSample from '@client/components/sample.vue';
-
-import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@client/scripts/theme-editor';
-import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@client/scripts/theme';
-import { host } from '@client/config';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import { addTheme } from '@client/theme-store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkRadio,
-		MkButton,
-		MkInput,
-		MkTextarea,
-		MkSelect,
-		MkSample,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.themeEditor,
-				icon: 'fas fa-palette',
-			},
-			theme: [] as ThemeViewModel,
-			name: '',
-			description: '',
-			baseTheme: 'light' as 'dark' | 'light',
-			author: `@${this.$i.username}@${toUnicode(host)}`,
-			themeToImport: '',
-			changed: false,
-			lightTheme, darkTheme, themeProps,
-		}
-	},
-
-	computed: {
-		baseProps() {
-			return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props;
-		},
-	},
-
-	beforeUnmount() {
-		window.removeEventListener('beforeunload', this.beforeunload);
-	},
-
-	async beforeRouteLeave(to, from, next) {
-		if (this.changed && !(await this.confirm())) {
-			next(false);
-		} else {
-			next();
-		}
-	},
-
-	mounted() {
-		this.init();
-		window.addEventListener('beforeunload', this.beforeunload);
-		const changed = () => this.changed = true;
-		this.$watch('name', changed);
-		this.$watch('description', changed);
-		this.$watch('baseTheme', changed);
-		this.$watch('author', changed);
-		this.$watch('theme', changed);
-	},
-
-	methods: {
-		beforeunload(e: BeforeUnloadEvent) {
-			if (this.changed) {
-				e.preventDefault();
-				e.returnValue = '';
-			}
-		},
-
-		async confirm(): Promise<boolean> {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$ts.leaveConfirm,
-				showCancelButton: true
-			});
-			return !canceled;
-		},
-
-		init() {
-			const t: ThemeViewModel = [];
-			for (const key of themeProps) {
-				t.push([ key, null ]);
-			}
-			this.theme = t;
-		},
-	
-		async del(i: number) {
-			const { canceled } = await os.dialog({ 
-				type: 'warning',
-				showCancelButton: true,
-				text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
-			});
-			if (canceled) return;
-			Vue.delete(this.theme, i);
-		},
-	
-		async addConst() {
-			const { canceled, result } = await os.dialog({
-				title: this.$ts._theme.inputConstantName,
-				input: true
-			});
-			if (canceled) return;
-			this.theme.push([ '$' + result, '#000000']);
-		},
-	
-		save() {
-			const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
-			addTheme(theme);
-			os.dialog({
-				type: 'success',
-				text: this.$t('_theme.installed', { name: theme.name })
-			});
-			this.changed = false;
-		},
-	
-		preview() {
-			const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
-			try {
-				applyTheme(theme, false);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: e.message
-				});
-			}
-		},
-	
-		async importTheme() {
-			if (this.changed && (!await this.confirm())) return;
-
-			try {
-				const theme = JSON5.parse(this.themeToImport) as Theme;
-				if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid);
-
-				this.name = theme.name;
-				this.description = theme.desc || '';
-				this.author = theme.author;
-				this.baseTheme = theme.base || 'light';
-				this.theme = convertToViewModel(theme);
-				this.themeToImport = '';
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: e.message
-				});
-			}
-		},
-	
-		colorChanged(color: string, i: number) {
-			this.theme[i] = [this.theme[i][0], color];
-		},
-
-		getTypeOf(v: ThemeValue) {
-			return v === null
-				? this.$ts._theme.defaultValue
-				: typeof v === 'string'
-					? this.$ts._theme.color
-					: this.$t('_theme.' + v.type);
-		},
-	
-		async chooseType(e: MouseEvent, i: number) {
-			const newValue = await this.showTypeMenu(e);
-			this.theme[i] = [ this.theme[i][0], newValue ];
-		},
-	
-		showTypeMenu(e: MouseEvent) {
-			return new Promise<ThemeValue>((resolve) => {
-				os.popupMenu([{
-					text: this.$ts._theme.defaultValue,
-					action: () => resolve(null),
-				}, {
-					text: this.$ts._theme.color,
-					action: () => resolve('#000000'),
-				}, {
-					text: this.$ts._theme.func,
-					action: () => resolve({
-						type: 'func', name: 'alpha', arg: 1, value: 'accent'
-					}),
-				}, {
-					text: this.$ts._theme.refProp,
-					action: () => resolve({
-						type: 'refProp', key: 'accent',
-					}),
-				}, {
-					text: this.$ts._theme.refConst,
-					action: () => resolve({
-						type: 'refConst', key: '',
-					}),
-				}, {
-					text: 'CSS',
-					action: () => resolve({
-						type: 'css', value: '',
-					}),
-				}], e.currentTarget || e.target);
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.t9makv94 {
-	> ._section {
-		> ._content {
-			> .list-view {
-				> .item {
-					min-height: 48px;
-					word-break: break-all;
-
-					&:not(:last-child) {
-						margin-bottom: 8px;
-					}
-
-					.select {
-						margin: 24px 0;
-					}
-
-					.type {
-						cursor: pointer;
-					}
-
-					.default-value {
-						opacity: 0.6;
-						pointer-events: none;
-						user-select: none;
-					}
-
-					.color {
-						> input {
-							display: inline-block;
-							width: 1.5em;
-							height: 1.5em;
-						}
-
-						> div {
-							margin-left: 8px;
-							display: inline-block;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue
deleted file mode 100644
index 429d183d1e..0000000000
--- a/src/client/pages/announcements.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<template>
-<MkSpacer :content-max="800">
-	<MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content">
-		<section class="_card announcement" v-for="(announcement, i) in items" :key="announcement.id">
-			<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
-			<div class="_content">
-				<Mfm :text="announcement.text"/>
-				<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
-			</div>
-			<div class="_footer" v-if="$i && !announcement.isRead">
-				<MkButton @click="read(items, announcement, i)" primary><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
-			</div>
-		</section>
-	</MkPagination>
-</MkSpacer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkButton
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.announcements,
-				icon: 'fas fa-broadcast-tower',
-				bg: 'var(--bg)',
-			},
-			pagination: {
-				endpoint: 'announcements',
-				limit: 10,
-			},
-		};
-	},
-
-	methods: {
-		// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
-		read(items, announcement, i) {
-			items[i] = {
-				...announcement,
-				isRead: true,
-			};
-			os.api('i/read-announcement', { announcementId: announcement.id });
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ruryvtyk {
-	> .announcement {
-		&:not(:last-child) {
-			margin-bottom: var(--margin);
-		}
-
-		> ._content {
-			> img {
-				display: block;
-				max-height: 300px;
-				max-width: 100%;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/antenna-timeline.vue b/src/client/pages/antenna-timeline.vue
deleted file mode 100644
index c99124dbdc..0000000000
--- a/src/client/pages/antenna-timeline.vue
+++ /dev/null
@@ -1,147 +0,0 @@
-<template>
-<div class="tqmomfks" v-hotkey.global="keymap" v-size="{ min: [800] }">
-	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
-	<div class="tl _block">
-		<XTimeline ref="tl" class="tl"
-			:key="antennaId"
-			src="antenna"
-			:antenna="antennaId"
-			:sound="true"
-			@before="before()"
-			@after="after()"
-			@queue="queueUpdated"
-		/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, computed } from 'vue';
-import Progress from '@client/scripts/loading';
-import XTimeline from '@client/components/timeline.vue';
-import { scroll } from '@client/scripts/scroll';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XTimeline,
-	},
-
-	props: {
-		antennaId: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			antenna: null,
-			queue: 0,
-			[symbols.PAGE_INFO]: computed(() => this.antenna ? {
-				title: this.antenna.name,
-				icon: 'fas fa-satellite',
-				bg: 'var(--bg)',
-				actions: [{
-					icon: 'fas fa-calendar-alt',
-					text: this.$ts.jumpToSpecifiedDate,
-					handler: this.timetravel
-				}, {
-					icon: 'fas fa-cog',
-					text: this.$ts.settings,
-					handler: this.settings
-				}],
-			} : null),
-		};
-	},
-
-	computed: {
-		keymap(): any {
-			return {
-				't': this.focus
-			};
-		},
-	},
-
-	watch: {
-		antennaId: {
-			async handler() {
-				this.antenna = await os.api('antennas/show', {
-					antennaId: this.antennaId
-				});
-			},
-			immediate: true
-		}
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		},
-
-		queueUpdated(q) {
-			this.queue = q;
-		},
-
-		top() {
-			scroll(this.$el, { top: 0 });
-		},
-
-		async timetravel() {
-			const { canceled, result: date } = await os.dialog({
-				title: this.$ts.date,
-				input: {
-					type: 'date'
-				}
-			});
-			if (canceled) return;
-
-			this.$refs.tl.timetravel(new Date(date));
-		},
-
-		settings() {
-			this.$router.push(`/my/antennas/${this.antennaId}`);
-		},
-
-		focus() {
-			(this.$refs.tl as any).focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.tqmomfks {
-	padding: var(--margin);
-
-	> .new {
-		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 16px);
-		z-index: 1000;
-		width: 100%;
-
-		> button {
-			display: block;
-			margin: var(--margin) auto 0 auto;
-			padding: 8px 16px;
-			border-radius: 32px;
-		}
-	}
-
-	> .tl {
-		background: var(--bg);
-		border-radius: var(--radius);
-		overflow: clip;
-	}
-
-	&.min-width_800px {
-		max-width: 800px;
-		margin: 0 auto;
-	}
-}
-</style>
diff --git a/src/client/pages/api-console.vue b/src/client/pages/api-console.vue
deleted file mode 100644
index 9aa7d4ea4d..0000000000
--- a/src/client/pages/api-console.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<template>
-<div class="_root">
-	<div class="_block" style="padding: 24px;">
-		<MkInput v-model="endpoint" :datalist="endpoints" @update:modelValue="onEndpointChange()" class="">
-			<template #label>Endpoint</template>
-		</MkInput>
-		<MkTextarea v-model="body" code>
-			<template #label>Params (JSON or JSON5)</template>
-		</MkTextarea>
-		<MkSwitch v-model="withCredential">
-			With credential
-		</MkSwitch>
-		<MkButton primary full @click="send" :disabled="sending">
-			<template v-if="sending"><MkEllipsis/></template>
-			<template v-else><i class="fas fa-paper-plane"></i> Send</template>
-		</MkButton>
-	</div>
-	<div v-if="res" class="_block" style="padding: 24px;">
-		<MkTextarea v-model="res" code readonly tall>
-			<template #label>Response</template>
-		</MkTextarea>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton, MkInput, MkTextarea, MkSwitch,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'API console',
-				icon: 'fas fa-terminal'
-			},
-
-			endpoint: '',
-			body: '{}',
-			res: null,
-			sending: false,
-			endpoints: [],
-			withCredential: true,
-
-		};
-	},
-
-	created() {
-		os.api('endpoints').then(endpoints => {
-			this.endpoints = endpoints;
-		});
-	},
-
-	methods: {
-		send() {
-			this.sending = true;
-			os.api(this.endpoint, JSON5.parse(this.body)).then(res => {
-				this.sending = false;
-				this.res = JSON5.stringify(res, null, 2);
-			}, err => {
-				this.sending = false;
-				this.res = JSON5.stringify(err, null, 2);
-			});
-		},
-
-		onEndpointChange() {
-			os.api('endpoint', { endpoint: this.endpoint }, this.withCredential ? undefined : null).then(endpoint => {
-				const body = {};
-				for (const p of endpoint.params) {
-					body[p.name] =
-						p.type === 'String' ? '' :
-						p.type === 'Number' ? 0 :
-						p.type === 'Boolean' ? false :
-						p.type === 'Array' ? [] :
-						p.type === 'Object' ? {} :
-						null;
-				}
-				this.body = JSON5.stringify(body, null, 2);
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/auth.form.vue b/src/client/pages/auth.form.vue
deleted file mode 100644
index 10c466c73c..0000000000
--- a/src/client/pages/auth.form.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<section class="_section">
-	<div class="_title">{{ $t('_auth.shareAccess', { name: app.name }) }}</div>
-	<div class="_content">
-		<h2>{{ app.name }}</h2>
-		<p class="id">{{ app.id }}</p>
-		<p class="description">{{ app.description }}</p>
-	</div>
-	<div class="_content">
-		<h2>{{ $ts._auth.permissionAsk }}</h2>
-		<ul>
-			<li v-for="p in app.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
-		</ul>
-	</div>
-	<div class="_footer">
-		<MkButton @click="cancel" inline>{{ $ts.cancel }}</MkButton>
-		<MkButton @click="accept" inline primary>{{ $ts.accept }}</MkButton>
-	</div>
-</section>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-	props: ['session'],
-	computed: {
-		name(): string {
-			const el = document.createElement('div');
-			el.textContent = this.app.name
-			return el.innerHTML;
-		},
-		app(): any {
-			return this.session.app;
-		}
-	},
-	methods: {
-		cancel() {
-			os.api('auth/deny', {
-				token: this.session.token
-			}).then(() => {
-				this.$emit('denied');
-			});
-		},
-
-		accept() {
-			os.api('auth/accept', {
-				token: this.session.token
-			}).then(() => {
-				this.$emit('accepted');
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue
deleted file mode 100755
index 3656d48c42..0000000000
--- a/src/client/pages/auth.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<template>
-<div class="" v-if="$i && fetching">
-	<MkLoading/>
-</div>
-<div v-else-if="$i">
-	<XForm
-		class="form"
-		ref="form"
-		v-if="state == 'waiting'"
-		:session="session"
-		@denied="state = 'denied'"
-		@accepted="accepted"
-	/>
-	<div class="denied" v-if="state == 'denied'">
-		<h1>{{ $ts._auth.denied }}</h1>
-	</div>
-	<div class="accepted" v-if="state == 'accepted'">
-		<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$ts.allowed }}</h1>
-		<p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p>
-		<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p>
-	</div>
-	<div class="error" v-if="state == 'fetch-session-error'">
-		<p>{{ $ts.somethingHappened }}</p>
-	</div>
-</div>
-<div class="signin" v-else>
-	<MkSignin @login="onLogin"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XForm from './auth.form.vue';
-import MkSignin from '@client/components/signin.vue';
-import * as os from '@client/os';
-import { login } from '@client/account';
-
-export default defineComponent({
-	components: {
-		XForm,
-		MkSignin,
-	},
-	data() {
-		return {
-			state: null,
-			session: null,
-			fetching: true
-		};
-	},
-	computed: {
-		token(): string {
-			return this.$route.params.token;
-		}
-	},
-	mounted() {
-		if (!this.$i) return;
-
-		// Fetch session
-		os.api('auth/session/show', {
-			token: this.token
-		}).then(session => {
-			this.session = session;
-			this.fetching = false;
-
-			// 既に連携していた場合
-			if (this.session.app.isAuthorized) {
-				os.api('auth/accept', {
-					token: this.session.token
-				}).then(() => {
-					this.accepted();
-				});
-			} else {
-				this.state = 'waiting';
-			}
-		}).catch(error => {
-			this.state = 'fetch-session-error';
-			this.fetching = false;
-		});
-	},
-	methods: {
-		accepted() {
-			this.state = 'accepted';
-			if (this.session.app.callbackUrl) {
-				location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
-			}
-		}, onLogin(res) {
-			login(res.i);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/channel-editor.vue b/src/client/pages/channel-editor.vue
deleted file mode 100644
index 67e27896ce..0000000000
--- a/src/client/pages/channel-editor.vue
+++ /dev/null
@@ -1,129 +0,0 @@
-<template>
-<div>
-	<div class="_section">
-		<div class="_content">
-			<MkInput v-model="name">
-				<template #label>{{ $ts.name }}</template>
-			</MkInput>
-
-			<MkTextarea v-model="description">
-				<template #label>{{ $ts.description }}</template>
-			</MkTextarea>
-
-			<div class="banner">
-				<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton>
-				<div v-else-if="bannerUrl">
-					<img :src="bannerUrl" style="width: 100%;"/>
-					<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton>
-				</div>
-			</div>
-		</div>
-		<div class="_footer">
-			<MkButton @click="save()" primary><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import { selectFile } from '@client/scripts/select-file';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkTextarea, MkButton, MkInput,
-	},
-
-	props: {
-		channelId: {
-			type: String,
-			required: false
-		},
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.channelId ? {
-				title: this.$ts._channel.edit,
-				icon: 'fas fa-satellite-dish',
-			} : {
-				title: this.$ts._channel.create,
-				icon: 'fas fa-satellite-dish',
-			}),
-			channel: null,
-			name: null,
-			description: null,
-			bannerUrl: null,
-			bannerId: null,
-		};
-	},
-
-	watch: {
-		async bannerId() {
-			if (this.bannerId == null) {
-				this.bannerUrl = null;
-			} else {
-				this.bannerUrl = (await os.api('drive/files/show', {
-					fileId: this.bannerId,
-				})).url;
-			}
-		},
-	},
-
-	async created() {
-		if (this.channelId) {
-			this.channel = await os.api('channels/show', {
-				channelId: this.channelId,
-			});
-
-			this.name = this.channel.name;
-			this.description = this.channel.description;
-			this.bannerId = this.channel.bannerId;
-			this.bannerUrl = this.channel.bannerUrl;
-		}
-	},
-
-	methods: {
-		save() {
-			const params = {
-				name: this.name,
-				description: this.description,
-				bannerId: this.bannerId,
-			};
-
-			if (this.channelId) {
-				params.channelId = this.channelId;
-				os.api('channels/update', params)
-				.then(channel => {
-					os.success();
-				});
-			} else {
-				os.api('channels/create', params)
-				.then(channel => {
-					os.success();
-					this.$router.push(`/channels/${channel.id}`);
-				});
-			}
-		},
-
-		setBannerImage(e) {
-			selectFile(e.currentTarget || e.target, null, false).then(file => {
-				this.bannerId = file.id;
-			});
-		},
-
-		removeBannerImage() {
-			this.bannerId = null;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/channel.vue b/src/client/pages/channel.vue
deleted file mode 100644
index d725db9e49..0000000000
--- a/src/client/pages/channel.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-<template>
-<div v-if="channel" class="_section">
-	<div class="wpgynlbz _content _panel _gap" :class="{ hide: !showBanner }">
-		<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
-		<button class="_button toggle" @click="() => showBanner = !showBanner">
-			<template v-if="showBanner"><i class="fas fa-angle-up"></i></template>
-			<template v-else><i class="fas fa-angle-down"></i></template>
-		</button>
-		<div class="hideOverlay" v-if="!showBanner">
-		</div>
-		<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
-			<div class="status">
-				<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
-				<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
-			</div>
-			<div class="fade"></div>
-		</div>
-		<div class="description" v-if="channel.description">
-			<Mfm :text="channel.description" :is-note="false" :i="$i"/>
-		</div>
-	</div>
-
-	<XPostForm :channel="channel" class="post-form _content _panel _gap" fixed v-if="$i"/>
-
-	<XTimeline class="_content _gap" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import XPostForm from '@client/components/post-form.vue';
-import XTimeline from '@client/components/timeline.vue';
-import XChannelFollowButton from '@client/components/channel-follow-button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkContainer,
-		XPostForm,
-		XTimeline,
-		XChannelFollowButton
-	},
-
-	props: {
-		channelId: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.channel ? {
-				title: this.channel.name,
-				icon: 'fas fa-satellite-dish',
-			} : null),
-			channel: null,
-			showBanner: true,
-			pagination: {
-				endpoint: 'channels/timeline',
-				limit: 10,
-				params: () => ({
-					channelId: this.channelId,
-				})
-			},
-		};
-	},
-
-	watch: {
-		channelId: {
-			async handler() {
-				this.channel = await os.api('channels/show', {
-					channelId: this.channelId,
-				});
-			},
-			immediate: true
-		}
-	},
-
-	created() {
-
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.wpgynlbz {
-	position: relative;
-
-	> .subscribe {
-		position: absolute;
-		z-index: 1;
-		top: 16px;
-		left: 16px;
-	}
-
-	> .toggle {
-		position: absolute;
-		z-index: 2;
-    top: 8px;
-		right: 8px;
-		font-size: 1.2em;
-		width: 48px;
-		height: 48px;
-		color: #fff;
-		background: rgba(0, 0, 0, 0.5);
-		border-radius: 100%;
-		
-		> i {
-			vertical-align: middle;
-		}
-	}
-	
-	> .banner {
-		position: relative;
-		height: 200px;
-		background-position: center;
-		background-size: cover;
-
-		> .fade {
-			position: absolute;
-			bottom: 0;
-			left: 0;
-			width: 100%;
-			height: 64px;
-			background: linear-gradient(0deg, var(--panel), var(--X15));
-		}
-
-		> .status {
-			position: absolute;
-			z-index: 1;
-			bottom: 16px;
-			right: 16px;
-			padding: 8px 12px;
-			font-size: 80%;
-			background: rgba(0, 0, 0, 0.7);
-			border-radius: 6px;
-			color: #fff;
-		}
-	}
-
-	> .description {
-		padding: 16px;
-	}
-
-	> .hideOverlay {
-		position: absolute;
-		z-index: 1;
-		top: 0;
-		left: 0;
-		width: 100%;
-		height: 100%;
-		-webkit-backdrop-filter: var(--blur, blur(16px));
-		backdrop-filter: var(--blur, blur(16px));
-		background: rgba(0, 0, 0, 0.3);
-	}
-
-	&.hide {
-		> .subscribe {
-			display: none;
-		}
-
-		> .toggle {
-			top: 0;
-			right: 0;
-			height: 100%;
-			background: transparent;
-		}
-
-		> .banner {
-			height: 42px;
-			filter: blur(8px);
-
-			> * {
-				display: none;
-			}
-		}
-
-		> .description {
-			display: none;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/channels.vue b/src/client/pages/channels.vue
deleted file mode 100644
index fd1408c253..0000000000
--- a/src/client/pages/channels.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<template>
-<div>
-	<div class="_section" style="padding: 0;" v-if="$i">
-		<MkTab class="_content" v-model="tab">
-			<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._channel.featured }}</option>
-			<option value="following"><i class="fas fa-heart"></i> {{ $ts._channel.following }}</option>
-			<option value="owned"><i class="fas fa-edit"></i> {{ $ts._channel.owned }}</option>
-		</MkTab>
-	</div>
-
-	<div class="_section">
-		<div class="_content grwlizim featured" v-if="tab === 'featured'">
-			<MkPagination :pagination="featuredPagination" #default="{items}">
-				<MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/>
-			</MkPagination>
-		</div>
-
-		<div class="_content grwlizim following" v-if="tab === 'following'">
-			<MkPagination :pagination="followingPagination" #default="{items}">
-				<MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/>
-			</MkPagination>
-		</div>
-
-		<div class="_content grwlizim owned" v-if="tab === 'owned'">
-			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
-			<MkPagination :pagination="ownedPagination" #default="{items}">
-				<MkChannelPreview v-for="channel in items" class="_gap" :channel="channel" :key="channel.id"/>
-			</MkPagination>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkChannelPreview from '@client/components/channel-preview.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkTab from '@client/components/tab.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkChannelPreview, MkPagination, MkButton, MkTab
-	},
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.channel,
-				icon: 'fas fa-satellite-dish',
-				action: {
-					icon: 'fas fa-plus',
-					handler: this.create
-				}
-			},
-			tab: 'featured',
-			featuredPagination: {
-				endpoint: 'channels/featured',
-				noPaging: true,
-			},
-			followingPagination: {
-				endpoint: 'channels/followed',
-				limit: 5,
-			},
-			ownedPagination: {
-				endpoint: 'channels/owned',
-				limit: 5,
-			},
-		};
-	},
-	methods: {
-		create() {
-			this.$router.push(`/channels/new`);
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/clip.vue b/src/client/pages/clip.vue
deleted file mode 100644
index e4b00d5e28..0000000000
--- a/src/client/pages/clip.vue
+++ /dev/null
@@ -1,154 +0,0 @@
-<template>
-<div v-if="clip" class="_section">
-	<div class="okzinsic _content _panel _gap">
-		<div class="description" v-if="clip.description">
-			<Mfm :text="clip.description" :is-note="false" :i="$i"/>
-		</div>
-		<div class="user">
-			<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
-		</div>
-	</div>
-
-	<XNotes class="_content _gap" :pagination="pagination" :detail="true"/>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import XPostForm from '@client/components/post-form.vue';
-import XNotes from '@client/components/notes.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkContainer,
-		XPostForm,
-		XNotes,
-	},
-
-	props: {
-		clipId: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.clip ? {
-				title: this.clip.name,
-				icon: 'fas fa-paperclip',
-				action: {
-					icon: 'fas fa-ellipsis-h',
-					handler: this.menu
-				}
-			} : null),
-			clip: null,
-			pagination: {
-				endpoint: 'clips/notes',
-				limit: 10,
-				params: () => ({
-					clipId: this.clipId,
-				})
-			},
-		};
-	},
-
-	computed: {
-		isOwned(): boolean {
-			return this.$i && this.clip && (this.$i.id === this.clip.userId);
-		}
-	},
-
-	watch: {
-		clipId: {
-			async handler() {
-				this.clip = await os.api('clips/show', {
-					clipId: this.clipId,
-				});
-			},
-			immediate: true
-		}
-	},
-
-	created() {
-
-	},
-
-	methods: {
-		menu(ev) {
-			os.popupMenu([this.isOwned ? {
-				icon: 'fas fa-pencil-alt',
-				text: this.$ts.edit,
-				action: async () => {
-					const { canceled, result } = await os.form(this.clip.name, {
-						name: {
-							type: 'string',
-							label: this.$ts.name,
-							default: this.clip.name
-						},
-						description: {
-							type: 'string',
-							required: false,
-							multiline: true,
-							label: this.$ts.description,
-							default: this.clip.description
-						},
-						isPublic: {
-							type: 'boolean',
-							label: this.$ts.public,
-							default: this.clip.isPublic
-						}
-					});
-					if (canceled) return;
-
-					os.apiWithDialog('clips/update', {
-						clipId: this.clip.id,
-						...result
-					});
-				}
-			} : undefined, this.isOwned ? {
-				icon: 'fas fa-trash-alt',
-				text: this.$ts.delete,
-				danger: true,
-				action: async () => {
-					const { canceled } = await os.dialog({
-						type: 'warning',
-						text: this.$t('deleteAreYouSure', { x: this.clip.name }),
-						showCancelButton: true
-					});
-					if (canceled) return;
-
-					await os.apiWithDialog('clips/delete', {
-						clipId: this.clip.id,
-					});
-				}
-			} : undefined], ev.currentTarget || ev.target);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.okzinsic {
-	position: relative;
-
-	> .description {
-		padding: 16px;
-	}
-
-	> .user {
-		$height: 32px;
-		padding: 16px;
-		border-top: solid 0.5px var(--divider);
-		line-height: $height;
-
-		> .avatar {
-			width: $height;
-			height: $height;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/drive.vue b/src/client/pages/drive.vue
deleted file mode 100644
index 9ee1ea8859..0000000000
--- a/src/client/pages/drive.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<template>
-<div>
-	<XDrive ref="drive" @cd="x => folder = x"/>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import XDrive from '@client/components/drive.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XDrive
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: computed(() => this.folder ? this.folder.name : this.$ts.drive),
-				icon: 'fas fa-cloud',
-			},
-			folder: null,
-		};
-	},
-});
-</script>
diff --git a/src/client/pages/emojis.category.vue b/src/client/pages/emojis.category.vue
deleted file mode 100644
index e725bcb31f..0000000000
--- a/src/client/pages/emojis.category.vue
+++ /dev/null
@@ -1,135 +0,0 @@
-<template>
-<div class="driuhtrh">
-	<div class="query">
-		<MkInput v-model="q" class="" :placeholder="$ts.search">
-			<template #prefix><i class="fas fa-search"></i></template>
-		</MkInput>
-
-		<!-- たくさんあると邪魔
-		<div class="tags">
-			<span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
-		</div>
-		-->
-	</div>
-
-	<MkFolder class="emojis" v-if="searchEmojis">
-		<template #header>{{ $ts.searchResult }}</template>
-		<div class="zuvgdzyt">
-			<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/>
-		</div>
-	</MkFolder>
-	
-	<MkFolder class="emojis" v-for="category in customEmojiCategories" :key="category">
-		<template #header>{{ category || $ts.other }}</template>
-		<div class="zuvgdzyt">
-			<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
-		</div>
-	</MkFolder>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, computed } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import MkTab from '@client/components/tab.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { emojiCategories, emojiTags } from '@client/instance';
-import XEmoji from './emojis.emoji.vue';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkFolder,
-		MkTab,
-		XEmoji,
-	},
-
-	data() {
-		return {
-			q: '',
-			customEmojiCategories: emojiCategories,
-			customEmojis: this.$instance.emojis,
-			tags: emojiTags,
-			selectedTags: new Set(),
-			searchEmojis: null,
-		}
-	},
-
-	watch: {
-		q() { this.search(); },
-		selectedTags: {
-			handler() {
-				this.search();
-			},
-			deep: true
-		},
-	},
-
-	methods: {
-		search() {
-			if ((this.q === '' || this.q == null) && this.selectedTags.size === 0) {
-				this.searchEmojis = null;
-				return;
-			}
-
-			if (this.selectedTags.size === 0) {
-				this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q));
-			} else {
-				this.searchEmojis = this.customEmojis.filter(e => (e.name.includes(this.q) || e.aliases.includes(this.q)) && [...this.selectedTags].every(t => e.aliases.includes(t)));
-			}
-		},
-
-		toggleTag(tag) {
-			if (this.selectedTags.has(tag)) {
-				this.selectedTags.delete(tag);
-			} else {
-				this.selectedTags.add(tag);
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.driuhtrh {
-	background: var(--bg);
-
-	> .query {
-		background: var(--bg);
-		padding: 16px;
-
-		> .tags {
-			> .tag {
-				display: inline-block;
-				margin: 8px 8px 0 0;
-				padding: 4px 8px;
-				font-size: 0.9em;
-				background: var(--accentedBg);
-				border-radius: 5px;
-
-				&.active {
-					background: var(--accent);
-					color: var(--fgOnAccent);
-				}
-			}
-		}
-	}
-
-	> .emojis {
-		--x-padding: 0 16px;
-
-		.zuvgdzyt {
-			display: grid;
-			grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
-			grid-gap: 12px;
-			margin: 0 var(--margin) var(--margin) var(--margin);
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/emojis.emoji.vue b/src/client/pages/emojis.emoji.vue
deleted file mode 100644
index ca0ef2dbb7..0000000000
--- a/src/client/pages/emojis.emoji.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<template>
-<button class="zuvgdzyu _button" @click="menu">
-	<img :src="emoji.url" class="img" :alt="emoji.name"/>
-	<div class="body">
-		<div class="name _monospace">{{ emoji.name }}</div>
-		<div class="info">{{ emoji.aliases.join(' ') }}</div>
-	</div>
-</button>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import VanillaTilt from 'vanilla-tilt';
-
-export default defineComponent({
-	props: {
-		emoji: {
-			type: Object,
-			required: true,
-		}
-	},
-
-	mounted() {
-		if (this.$store.animation) {
-			VanillaTilt.init(this.$el, {
-				reverse: true,
-				gyroscope: false,
-				scale: 1.1,
-				speed: 500,
-			});
-		}
-	},
-
-	methods: {
-		menu(ev) {
-			os.popupMenu([{
-				type: 'label',
-				text: ':' + this.emoji.name + ':',
-			}, {
-				text: this.$ts.copy,
-				icon: 'fas fa-copy',
-				action: () => {
-					copyToClipboard(`:${this.emoji.name}:`);
-					os.success();
-				}
-			}], ev.currentTarget || ev.target);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.zuvgdzyu {
-	display: flex;
-	align-items: center;
-	padding: 12px;
-	text-align: left;
-	background: var(--panel);
-	border-radius: 8px;
-	transform-style: preserve-3d;
-	transform: perspective(1000px);
-
-	&:hover {
-		border-color: var(--accent);
-	}
-
-	> .img {
-		width: 42px;
-		height: 42px;
-		transform: translateZ(20px);
-	}
-
-	> .body {
-		padding: 0 0 0 8px;
-		white-space: nowrap;
-		overflow: hidden;
-		transform: translateZ(10px);
-
-		> .name {
-			text-overflow: ellipsis;
-			overflow: hidden;
-		}
-
-		> .info {
-			opacity: 0.5;
-			font-size: 0.9em;
-			text-overflow: ellipsis;
-			overflow: hidden;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/emojis.vue b/src/client/pages/emojis.vue
deleted file mode 100644
index 8918de2338..0000000000
--- a/src/client/pages/emojis.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-<div :class="$style.root">
-	<XCategory v-if="tab === 'category'"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, computed } from 'vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import XCategory from './emojis.category.vue';
-
-export default defineComponent({
-	components: {
-		XCategory,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.customEmojis,
-				icon: 'fas fa-laugh',
-				bg: 'var(--bg)',
-			})),
-			tab: 'category',
-		}
-	},
-});
-</script>
-
-<style lang="scss" module>
-.root {
-	max-width: 1000px;
-	margin: 0 auto;
-}
-</style>
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
deleted file mode 100644
index 5a23d34d27..0000000000
--- a/src/client/pages/explore.vue
+++ /dev/null
@@ -1,261 +0,0 @@
-<template>
-<div>
-	<MkSpacer :content-max="1200">
-		<div class="lznhrdub">
-			<div v-if="tab === 'local'">
-				<div class="localfedi7 _block _isolated" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
-					<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header>
-					<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div>
-				</div>
-
-				<template v-if="tag == null">
-					<MkFolder class="_gap" persist-key="explore-pinned-users">
-						<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
-						<XUserList :pagination="pinnedUsers"/>
-					</MkFolder>
-					<MkFolder class="_gap" persist-key="explore-popular-users">
-						<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
-						<XUserList :pagination="popularUsers"/>
-					</MkFolder>
-					<MkFolder class="_gap" persist-key="explore-recently-updated-users">
-						<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
-						<XUserList :pagination="recentlyUpdatedUsers"/>
-					</MkFolder>
-					<MkFolder class="_gap" persist-key="explore-recently-registered-users">
-						<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
-						<XUserList :pagination="recentlyRegisteredUsers"/>
-					</MkFolder>
-				</template>
-			</div>
-			<div v-else-if="tab === 'remote'">
-				<div class="localfedi7 _block _isolated" v-if="tag == null" :style="{ backgroundImage: `url(/static-assets/client/fedi.jpg)` }">
-					<header><span>{{ $ts.exploreFediverse }}</span></header>
-				</div>
-
-				<MkFolder :foldable="true" :expanded="false" ref="tags" class="_gap">
-					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
-
-					<div class="vxjfqztj">
-						<MkA v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</MkA>
-						<MkA v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</MkA>
-					</div>
-				</MkFolder>
-
-				<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
-					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
-					<XUserList :pagination="tagUsers"/>
-				</MkFolder>
-
-				<template v-if="tag == null">
-					<MkFolder class="_gap">
-						<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
-						<XUserList :pagination="popularUsersF"/>
-					</MkFolder>
-					<MkFolder class="_gap">
-						<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
-						<XUserList :pagination="recentlyUpdatedUsersF"/>
-					</MkFolder>
-					<MkFolder class="_gap">
-						<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
-						<XUserList :pagination="recentlyRegisteredUsersF"/>
-					</MkFolder>
-				</template>
-			</div>
-			<div v-else-if="tab === 'search'">
-				<div class="_isolated">
-					<MkInput v-model="searchQuery" :debounce="true" type="search">
-						<template #prefix><i class="fas fa-search"></i></template>
-						<template #label>{{ $ts.searchUser }}</template>
-					</MkInput>
-					<MkRadios v-model="searchOrigin">
-						<option value="local">{{ $ts.local }}</option>
-						<option value="remote">{{ $ts.remote }}</option>
-						<option value="both">{{ $ts.all }}</option>
-					</MkRadios>
-				</div>
-
-				<XUserList v-if="searchQuery" class="_gap" :pagination="searchPagination" ref="search"/>
-			</div>
-		</div>
-	</MkSpacer>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import XUserList from '@client/components/user-list.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkRadios from '@client/components/form/radios.vue';
-import number from '@client/filters/number';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XUserList,
-		MkFolder,
-		MkInput,
-		MkRadios,
-	},
-
-	props: {
-		tag: {
-			type: String,
-			required: false
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.explore,
-				icon: 'fas fa-hashtag',
-				bg: 'var(--bg)',
-				tabs: [{
-					active: this.tab === 'local',
-					title: this.$ts.local,
-					onClick: () => { this.tab = 'local'; },
-				}, {
-					active: this.tab === 'remote',
-					title: this.$ts.remote,
-					onClick: () => { this.tab = 'remote'; },
-				}, {
-					active: this.tab === 'search',
-					title: this.$ts.search,
-					onClick: () => { this.tab = 'search'; },
-				},]
-			})),
-			tab: 'local',
-			pinnedUsers: { endpoint: 'pinned-users' },
-			popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
-				state: 'alive',
-				origin: 'local',
-				sort: '+follower',
-			} },
-			recentlyUpdatedUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
-				origin: 'local',
-				sort: '+updatedAt',
-			} },
-			recentlyRegisteredUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
-				origin: 'local',
-				state: 'alive',
-				sort: '+createdAt',
-			} },
-			popularUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
-				state: 'alive',
-				origin: 'remote',
-				sort: '+follower',
-			} },
-			recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
-				origin: 'combined',
-				sort: '+updatedAt',
-			} },
-			recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: {
-				origin: 'combined',
-				sort: '+createdAt',
-			} },
-			searchPagination: {
-				endpoint: 'users/search',
-				limit: 10,
-				params: computed(() => (this.searchQuery && this.searchQuery !== '') ? {
-					query: this.searchQuery,
-					origin: this.searchOrigin,
-				} : null)
-			},
-			tagsLocal: [],
-			tagsRemote: [],
-			stats: null,
-			searchQuery: null,
-			searchOrigin: 'combined',
-			num: number,
-		};
-	},
-
-	computed: {
-		meta() {
-			return this.$instance;
-		},
-		tagUsers(): any {
-			return {
-				endpoint: 'hashtags/users',
-				limit: 30,
-				params: {
-					tag: this.tag,
-					origin: 'combined',
-					sort: '+follower',
-				}
-			};
-		},
-	},
-
-	watch: {
-		tag() {
-			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
-		},
-	},
-
-	created() {
-		os.api('hashtags/list', {
-			sort: '+attachedLocalUsers',
-			attachedToLocalUserOnly: true,
-			limit: 30
-		}).then(tags => {
-			this.tagsLocal = tags;
-		});
-		os.api('hashtags/list', {
-			sort: '+attachedRemoteUsers',
-			attachedToRemoteUserOnly: true,
-			limit: 30
-		}).then(tags => {
-			this.tagsRemote = tags;
-		});
-		os.api('stats').then(stats => {
-			this.stats = stats;
-		});
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.localfedi7 {
-	color: #fff;
-	padding: 16px;
-	height: 80px;
-	background-position: 50%;
-	background-size: cover;
-	margin-bottom: var(--margin);
-
-	> * {
-		&:not(:last-child) {
-			margin-bottom: 8px;
-		}
-
-		> span {
-			display: inline-block;
-			padding: 6px 8px;
-			background: rgba(0, 0, 0, 0.7);
-		}
-	}
-
-	> header {
-		font-size: 20px;
-		font-weight: bold;
-	}
-
-	> div {
-		font-size: 14px;
-		opacity: 0.8;
-	}
-}
-
-.vxjfqztj {
-	> * {
-		margin-right: 16px;
-
-		&.local {
-			font-weight: bold;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue
deleted file mode 100644
index f13723c2d1..0000000000
--- a/src/client/pages/favorites.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<div class="jmelgwjh">
-	<div class="body">
-		<XNotes class="notes" :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XNotes from '@client/components/notes.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.favorites,
-				icon: 'fas fa-star',
-				bg: 'var(--bg)',
-			},
-			pagination: {
-				endpoint: 'i/favorites',
-				limit: 10,
-				params: () => ({
-				})
-			},
-		};
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.jmelgwjh {
-	background: var(--bg);
-
-	> .body {
-		box-sizing: border-box;
-		max-width: 800px;
-		margin: 0 auto;
-		padding: 16px;
-	}
-}
-</style>
diff --git a/src/client/pages/featured.vue b/src/client/pages/featured.vue
deleted file mode 100644
index 50df26bfb1..0000000000
--- a/src/client/pages/featured.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-<MkSpacer :content-max="800">
-	<XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/>
-</MkSpacer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XNotes from '@client/components/notes.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.featured,
-				icon: 'fas fa-fire-alt',
-				bg: 'var(--bg)',
-			},
-			pagination: {
-				endpoint: 'notes/featured',
-				limit: 10,
-				offsetMode: true,
-			},
-		};
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/federation.vue b/src/client/pages/federation.vue
deleted file mode 100644
index eae6a05367..0000000000
--- a/src/client/pages/federation.vue
+++ /dev/null
@@ -1,265 +0,0 @@
-<template>
-<div class="taeiyria">
-	<div class="query">
-		<MkInput v-model="host" :debounce="true" class="">
-			<template #prefix><i class="fas fa-search"></i></template>
-			<template #label>{{ $ts.host }}</template>
-		</MkInput>
-		<div class="_inputSplit">
-			<MkSelect v-model="state">
-				<template #label>{{ $ts.state }}</template>
-				<option value="all">{{ $ts.all }}</option>
-				<option value="federating">{{ $ts.federating }}</option>
-				<option value="subscribing">{{ $ts.subscribing }}</option>
-				<option value="publishing">{{ $ts.publishing }}</option>
-				<option value="suspended">{{ $ts.suspended }}</option>
-				<option value="blocked">{{ $ts.blocked }}</option>
-				<option value="notResponding">{{ $ts.notResponding }}</option>
-			</MkSelect>
-			<MkSelect v-model="sort">
-				<template #label>{{ $ts.sort }}</template>
-				<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
-				<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
-				<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
-				<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
-				<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
-				<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
-				<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
-				<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option>
-				<option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option>
-				<option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option>
-				<option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option>
-			</MkSelect>
-		</div>
-	</div>
-
-	<MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
-		<div class="dqokceoi">
-			<MkA class="instance" v-for="instance in items" :key="instance.id" :to="`/instance-info/${instance.host}`">
-				<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
-				<div class="table">
-					<div class="cell">
-						<div class="key">{{ $ts.registeredAt }}</div>
-						<div class="value"><MkTime :time="instance.caughtAt"/></div>
-					</div>
-					<div class="cell">
-						<div class="key">{{ $ts.software }}</div>
-						<div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div>
-					</div>
-					<div class="cell">
-						<div class="key">{{ $ts.version }}</div>
-						<div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div>
-					</div>
-					<div class="cell">
-						<div class="key">{{ $ts.users }}</div>
-						<div class="value">{{ instance.usersCount }}</div>
-					</div>
-					<div class="cell">
-						<div class="key">{{ $ts.notes }}</div>
-						<div class="value">{{ instance.notesCount }}</div>
-					</div>
-					<div class="cell">
-						<div class="key">{{ $ts.sent }}</div>
-						<div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
-					</div>
-					<div class="cell">
-						<div class="key">{{ $ts.received }}</div>
-						<div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
-					</div>
-				</div>
-				<div class="footer">
-					<span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span>
-					<span class="pubSub">
-						<span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span>
-						<span class="sub" v-else><i class="fas fa-caret-down icon"></i>-</span>
-						<span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span>
-						<span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span>
-					</span>
-					<span class="right">
-						<span class="latestStatus">{{ instance.latestStatus || '-' }}</span>
-						<span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span>
-					</span>
-				</div>
-			</MkA>
-		</div>
-	</MkPagination>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSelect,
-		MkPagination,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.federation,
-				icon: 'fas fa-globe',
-				bg: 'var(--bg)',
-			},
-			host: '',
-			state: 'federating',
-			sort: '+pubSub',
-			pagination: {
-				endpoint: 'federation/instances',
-				limit: 10,
-				offsetMode: true,
-				params: () => ({
-					sort: this.sort,
-					host: this.host != '' ? this.host : null,
-					...(
-						this.state === 'federating' ? { federating: true } :
-						this.state === 'subscribing' ? { subscribing: true } :
-						this.state === 'publishing' ? { publishing: true } :
-						this.state === 'suspended' ? { suspended: true } :
-						this.state === 'blocked' ? { blocked: true } :
-						this.state === 'notResponding' ? { notResponding: true } :
-						{})
-				})
-			},
-		}
-	},
-
-	watch: {
-		host() {
-			this.$refs.instances.reload();
-		},
-		state() {
-			this.$refs.instances.reload();
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		getStatus(instance) {
-			if (instance.isSuspended) return 'suspended';
-			if (instance.isNotResponding) return 'error';
-			return 'alive';
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.taeiyria {
-	> .query {
-		background: var(--bg);
-		padding: 16px;
-	}
-}
-
-.dqokceoi {
-	display: grid;
-	grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
-	grid-gap: 12px;
-	padding: 16px;
-
-	> .instance {
-		padding: 16px;
-		border: solid 1px var(--divider);
-		border-radius: 6px;
-
-		&:hover {
-			border: solid 1px var(--accent);
-			text-decoration: none;
-		}
-
-		> .host {
-			font-weight: bold;
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-
-			> img {
-				width: 18px;
-				height: 18px;
-				margin-right: 6px;
-				vertical-align: middle;
-			}
-		}
-
-		> .table {
-			display: grid;
-			grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
-			grid-gap: 6px;
-			margin: 6px 0;
-			font-size: 70%;
-
-			> .cell {
-				> .key, > .value {
-					white-space: nowrap;
-					overflow: hidden;
-					text-overflow: ellipsis;
-				}
-
-				> .key {
-					opacity: 0.7;
-				}
-
-				> .value {
-				}
-			}
-		}
-
-		> .footer {
-			display: flex;
-			align-items: center;
-			font-size: 0.9em;
-
-			> .status {
-				&.suspended {
-					opacity: 0.5;
-				}
-
-				&.error {
-					color: var(--error);
-				}
-
-				&.alive {
-					color: var(--success);
-				}
-			}
-
-			> .pubSub {
-				margin-left: 8px;
-			}
-
-			> .right {
-				margin-left: auto;
-
-				> .latestStatus {
-					border: solid 1px var(--divider);
-					border-radius: 4px;
-					margin: 0 8px;
-					padding: 0 4px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue
deleted file mode 100644
index 6115dda454..0000000000
--- a/src/client/pages/follow-requests.vue
+++ /dev/null
@@ -1,153 +0,0 @@
-<template>
-<div>
-	<MkPagination :pagination="pagination" class="mk-follow-requests" ref="list">
-		<template #empty>
-			<div class="_fullinfo">
-				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
-				<div>{{ $ts.noFollowRequests }}</div>
-			</div>
-		</template>
-		<template #default="{items}">
-			<div class="user _panel" v-for="req in items" :key="req.id">
-				<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
-				<div class="body">
-					<div class="name">
-						<MkA class="name" :to="userPage(req.follower)" v-user-preview="req.follower.id"><MkUserName :user="req.follower"/></MkA>
-						<p class="acct">@{{ acct(req.follower) }}</p>
-					</div>
-					<div class="description" v-if="req.follower.description" :title="req.follower.description">
-						<Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
-					</div>
-					<div class="actions">
-						<button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
-						<button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
-					</div>
-				</div>
-			</div>
-		</template>
-	</MkPagination>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkPagination
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.followRequests,
-				icon: 'fas fa-user-clock',
-			},
-			pagination: {
-				endpoint: 'following/requests/list',
-				limit: 10,
-			},
-		};
-	},
-
-	methods: {
-		accept(user) {
-			os.api('following/requests/accept', { userId: user.id }).then(() => {
-				this.$refs.list.reload();
-			});
-		},
-		reject(user) {
-			os.api('following/requests/reject', { userId: user.id }).then(() => {
-				this.$refs.list.reload();
-			});
-		},
-		userPage,
-		acct
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-follow-requests {
-	> .user {
-		display: flex;
-		padding: 16px;
-
-		> .avatar {
-			display: block;
-			flex-shrink: 0;
-			margin: 0 12px 0 0;
-			width: 42px;
-			height: 42px;
-			border-radius: 8px;
-		}
-
-		> .body {
-			display: flex;
-			width: calc(100% - 54px);
-			position: relative;
-
-			> .name {
-				width: 45%;
-
-				@media (max-width: 500px) {
-					width: 100%;
-				}
-
-				> .name,
-				> .acct {
-					display: block;
-					white-space: nowrap;
-					text-overflow: ellipsis;
-					overflow: hidden;
-					margin: 0;
-				}
-
-				> .name {
-					font-size: 16px;
-					line-height: 24px;
-				}
-
-				> .acct {
-					font-size: 15px;
-					line-height: 16px;
-					opacity: 0.7;
-				}
-			}
-
-			> .description {
-				width: 55%;
-				line-height: 42px;
-				white-space: nowrap;
-				overflow: hidden;
-				text-overflow: ellipsis;
-				opacity: 0.7;
-				font-size: 14px;
-				padding-right: 40px;
-				padding-left: 8px;
-				box-sizing: border-box;
-
-				@media (max-width: 500px) {
-					display: none;
-				}
-			}
-
-			> .actions {
-				position: absolute;
-				top: 0;
-				bottom: 0;
-				right: 0;
-				margin: auto 0;
-
-				> button {
-					padding: 12px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/follow.vue b/src/client/pages/follow.vue
deleted file mode 100644
index d5247aff1e..0000000000
--- a/src/client/pages/follow.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div class="mk-follow-page">
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import { parseAcct } from '@/misc/acct';
-
-export default defineComponent({
-	created() {
-		const acct = new URL(location.href).searchParams.get('acct');
-		if (acct == null) return;
-
-		let promise;
-
-		if (acct.startsWith('https://')) {
-			promise = os.api('ap/show', {
-				uri: acct
-			});
-			promise.then(res => {
-				if (res.type == 'User') {
-					this.follow(res.object);
-				} else if (res.type === 'Note') {
-					this.$router.push(`/notes/${res.object.id}`);
-				} else {
-					os.dialog({
-						type: 'error',
-						text: 'Not a user'
-					}).then(() => {
-						window.close();
-					});
-				}
-			});
-		} else {
-			promise = os.api('users/show', parseAcct(acct));
-			promise.then(user => {
-				this.follow(user);
-			});
-		}
-
-		os.promiseDialog(promise, null, null, this.$ts.fetchingAsApObject);
-	},
-
-	methods: {
-		async follow(user) {
-			const { canceled } = await os.dialog({
-				type: 'question',
-				text: this.$t('followConfirm', { name: user.name || user.username }),
-				showCancelButton: true
-			});
-
-			if (canceled) {
-				window.close();
-				return;
-			}
-			
-			os.apiWithDialog('following/create', {
-				userId: user.id
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/gallery/edit.vue b/src/client/pages/gallery/edit.vue
deleted file mode 100644
index 8e74b068ef..0000000000
--- a/src/client/pages/gallery/edit.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormInput v-model="title">
-			<span>{{ $ts.title }}</span>
-		</FormInput>
-
-		<FormTextarea v-model="description" :max="500">
-			<span>{{ $ts.description }}</span>
-		</FormTextarea>
-
-		<FormGroup>
-			<div v-for="file in files" :key="file.id" class="_debobigegoItem _debobigegoPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
-				<div class="name">{{ file.name }}</div>
-				<button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button>
-			</div>
-			<FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
-		</FormGroup>
-
-		<FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
-
-		<FormButton v-if="postId" @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-		<FormButton v-else @click="save" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
-
-		<FormButton v-if="postId" @click="del" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormTuple from '@client/components/debobigego/tuple.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import { selectFile } from '@client/scripts/select-file';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormButton,
-		FormInput,
-		FormTextarea,
-		FormSwitch,
-		FormBase,
-		FormGroup,
-		FormSuspense,
-	},
-
-	props: {
-		postId: {
-			type: String,
-			required: false,
-			default: null,
-		}
-	},
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.postId ? {
-				title: this.$ts.edit,
-				icon: 'fas fa-pencil-alt'
-			} : {
-				title: this.$ts.postToGallery,
-				icon: 'fas fa-pencil-alt'
-			}),
-			init: null,
-			files: [],
-			description: null,
-			title: null,
-			isSensitive: false,
-		}
-	},
-
-	watch: {
-		postId: {
-			handler() {
-				this.init = () => this.postId ? os.api('gallery/posts/show', {
-					postId: this.postId
-				}).then(post => {
-					this.files = post.files;
-					this.title = post.title;
-					this.description = post.description;
-					this.isSensitive = post.isSensitive;
-				}) : Promise.resolve(null);
-			},
-			immediate: true,
-		}
-	},
-
-	methods: {
-		selectFile(e) {
-			selectFile(e.currentTarget || e.target, null, true).then(files => {
-				this.files = this.files.concat(files);
-			});
-		},
-
-		remove(file) {
-			this.files = this.files.filter(f => f.id !== file.id);
-		},
-
-		async save() {
-			if (this.postId) {
-				await os.apiWithDialog('gallery/posts/update', {
-					postId: this.postId,
-					title: this.title,
-					description: this.description,
-					fileIds: this.files.map(file => file.id),
-					isSensitive: this.isSensitive,
-				});
-				this.$router.push(`/gallery/${this.postId}`);
-			} else {
-				const post = await os.apiWithDialog('gallery/posts/create', {
-					title: this.title,
-					description: this.description,
-					fileIds: this.files.map(file => file.id),
-					isSensitive: this.isSensitive,
-				});
-				this.$router.push(`/gallery/${post.id}`);
-			}
-		},
-
-		async del() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$ts.deleteConfirm,
-				showCancelButton: true
-			});
-			if (canceled) return;
-			await os.apiWithDialog('gallery/posts/delete', {
-				postId: this.postId,
-			});
-			this.$router.push(`/gallery`);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.wqugxsfx {
-	height: 200px;
-	background-size: contain;
-	background-position: center;
-	background-repeat: no-repeat;
-	position: relative;
-
-	> .name {
-		position: absolute;
-		top: 8px;
-		left: 9px;
-		padding: 8px;
-		background: var(--panel);
-	}
-
-	> .remove {
-		position: absolute;
-		top: 8px;
-		right: 9px;
-		padding: 8px;
-		background: var(--panel);
-	}
-}
-</style>
diff --git a/src/client/pages/gallery/index.vue b/src/client/pages/gallery/index.vue
deleted file mode 100644
index ffc599513e..0000000000
--- a/src/client/pages/gallery/index.vue
+++ /dev/null
@@ -1,152 +0,0 @@
-<template>
-<div class="xprsixdl _root">
-	<MkTab v-model="tab" v-if="$i">
-		<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
-		<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
-		<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
-	</MkTab>
-
-	<div v-if="tab === 'explore'">
-		<MkFolder class="_gap">
-			<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
-			<MkPagination :pagination="recentPostsPagination" #default="{items}" :disable-auto-load="true">
-				<div class="vfpdbgtk">
-					<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
-				</div>
-			</MkPagination>
-		</MkFolder>
-		<MkFolder class="_gap">
-			<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
-			<MkPagination :pagination="popularPostsPagination" #default="{items}" :disable-auto-load="true">
-				<div class="vfpdbgtk">
-					<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
-				</div>
-			</MkPagination>
-		</MkFolder>
-	</div>
-	<div v-else-if="tab === 'liked'">
-		<MkPagination :pagination="likedPostsPagination" #default="{items}">
-			<div class="vfpdbgtk">
-				<MkGalleryPostPreview v-for="like in items" :post="like.post" :key="like.id" class="post"/>
-			</div>
-		</MkPagination>
-	</div>
-	<div v-else-if="tab === 'my'">
-		<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
-		<MkPagination :pagination="myPostsPagination" #default="{items}">
-			<div class="vfpdbgtk">
-				<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
-			</div>
-		</MkPagination>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import XUserList from '@client/components/user-list.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkTab from '@client/components/tab.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue';
-import number from '@client/filters/number';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XUserList,
-		MkFolder,
-		MkInput,
-		MkButton,
-		MkTab,
-		MkPagination,
-		MkGalleryPostPreview,
-	},
-
-	props: {
-		tag: {
-			type: String,
-			required: false
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.gallery,
-				icon: 'fas fa-icons'
-			},
-			tab: 'explore',
-			recentPostsPagination: {
-				endpoint: 'gallery/posts',
-				limit: 6,
-			},
-			popularPostsPagination: {
-				endpoint: 'gallery/featured',
-				limit: 5,
-			},
-			myPostsPagination: {
-				endpoint: 'i/gallery/posts',
-				limit: 5,
-			},
-			likedPostsPagination: {
-				endpoint: 'i/gallery/likes',
-				limit: 5,
-			},
-			tags: [],
-		};
-	},
-
-	computed: {
-		meta() {
-			return this.$instance;
-		},
-		tagUsers(): any {
-			return {
-				endpoint: 'hashtags/users',
-				limit: 30,
-				params: {
-					tag: this.tag,
-					origin: 'combined',
-					sort: '+follower',
-				}
-			};
-		},
-	},
-
-	watch: {
-		tag() {
-			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null);
-		},
-	},
-
-	created() {
-
-	},
-
-	methods: {
-
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.xprsixdl {
-	max-width: 1400px;
-	margin: 0 auto;
-}
-
-.vfpdbgtk {
-	display: grid;
-	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: 12px;
-	margin: 0 var(--margin);
-
-	> .post {
-
-	}
-}
-</style>
diff --git a/src/client/pages/gallery/post.vue b/src/client/pages/gallery/post.vue
deleted file mode 100644
index dbac003e38..0000000000
--- a/src/client/pages/gallery/post.vue
+++ /dev/null
@@ -1,282 +0,0 @@
-<template>
-<div class="_root">
-	<transition name="fade" mode="out-in">
-		<div v-if="post" class="rkxwuolj">
-			<div class="files">
-				<div class="file" v-for="file in post.files" :key="file.id">
-					<img :src="file.url"/>
-				</div>
-			</div>
-			<div class="body _block">
-				<div class="title">{{ post.title }}</div>
-				<div class="description"><Mfm :text="post.description"/></div>
-				<div class="info">
-					<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
-				</div>
-				<div class="actions">
-					<div class="like">
-						<MkButton class="button" @click="unlike()" v-if="post.isLiked" v-tooltip="$ts._gallery.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton>
-						<MkButton class="button" @click="like()" v-else v-tooltip="$ts._gallery.like"><i class="far fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton>
-					</div>
-					<div class="other">
-						<button v-if="$i && $i.id === post.user.id" class="_button" @click="edit" v-tooltip="$ts.edit" v-click-anime><i class="fas fa-pencil-alt fa-fw"></i></button>
-						<button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
-						<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
-					</div>
-				</div>
-				<div class="user">
-					<MkAvatar :user="post.user" class="avatar"/>
-					<div class="name">
-						<MkUserName :user="post.user" style="display: block;"/>
-						<MkAcct :user="post.user"/>
-					</div>
-					<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
-				</div>
-			</div>
-			<MkAd :prefer="['horizontal', 'horizontal-big']"/>
-			<MkContainer :max-height="300" :foldable="true" class="other">
-				<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
-				<MkPagination :pagination="otherPostsPagination" #default="{items}">
-					<div class="sdrarzaf">
-						<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
-					</div>
-				</MkPagination>
-			</MkContainer>
-		</div>
-		<MkError v-else-if="error" @retry="fetch()"/>
-		<MkLoading v-else/>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import MkContainer from '@client/components/ui/container.vue';
-import ImgWithBlurhash from '@client/components/img-with-blurhash.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue';
-import MkFollowButton from '@client/components/follow-button.vue';
-import { url } from '@client/config';
-
-export default defineComponent({
-	components: {
-		MkContainer,
-		ImgWithBlurhash,
-		MkPagination,
-		MkGalleryPostPreview,
-		MkButton,
-		MkFollowButton,
-	},
-	props: {
-		postId: {
-			type: String,
-			required: true
-		}
-	},
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.post ? {
-				title: this.post.title,
-				avatar: this.post.user,
-				path: `/gallery/${this.post.id}`,
-				share: {
-					title: this.post.title,
-					text: this.post.description,
-				},
-				actions: [{
-					icon: 'fas fa-pencil-alt',
-					text: this.$ts.edit,
-					handler: this.edit
-				}]
-			} : null),
-			otherPostsPagination: {
-				endpoint: 'users/gallery/posts',
-				limit: 6,
-				params: () => ({
-					userId: this.post.user.id
-				})
-			},
-			post: null,
-			error: null,
-		};
-	},
-
-	watch: {
-		postId: 'fetch'
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			this.post = null;
-			os.api('gallery/posts/show', {
-				postId: this.postId
-			}).then(post => {
-				this.post = post;
-			}).catch(e => {
-				this.error = e;
-			});
-		},
-
-		share() {
-			navigator.share({
-				title: this.post.title,
-				text: this.post.description,
-				url: `${url}/gallery/${this.post.id}`
-			});
-		},
-
-		shareWithNote() {
-			os.post({
-				initialText: `${this.post.title} ${url}/gallery/${this.post.id}`
-			});
-		},
-
-		like() {
-			os.apiWithDialog('gallery/posts/like', {
-				postId: this.postId,
-			}).then(() => {
-				this.post.isLiked = true;
-				this.post.likedCount++;
-			});
-		},
-
-		async unlike() {
-			const confirm = await os.dialog({
-				type: 'warning',
-				showCancelButton: true,
-				text: this.$ts.unlikeConfirm,
-			});
-			if (confirm.canceled) return;
-			os.apiWithDialog('gallery/posts/unlike', {
-				postId: this.postId,
-			}).then(() => {
-				this.post.isLiked = false;
-				this.post.likedCount--;
-			});
-		},
-
-		edit() {
-			this.$router.push(`/gallery/${this.post.id}/edit`);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.rkxwuolj {
-	> .files {
-		> .file {
-			> img {
-				display: block;
-				max-width: 100%;
-				max-height: 500px;
-				margin: 0 auto;
-			}
-
-			& + .file {
-				margin-top: 16px;
-			}
-		}
-	}
-
-	> .body {
-		padding: 32px;
-
-		> .title {
-			font-weight: bold;
-			font-size: 1.2em;
-			margin-bottom: 16px;
-		}
-
-		> .info {
-			margin-top: 16px;
-			font-size: 90%;
-			opacity: 0.7;
-		}
-
-		> .actions {
-			display: flex;
-			align-items: center;
-			margin-top: 16px;
-			padding: 16px 0 0 0;
-			border-top: solid 0.5px var(--divider);
-
-			> .like {
-				> .button {
-					--accent: rgb(241 97 132);
-					--X8: rgb(241 92 128);
-					--buttonBg: rgb(216 71 106 / 5%);
-					--buttonHoverBg: rgb(216 71 106 / 10%);
-					color: #ff002f;
-
-					::v-deep(.count) {
-						margin-left: 0.5em;
-					}
-				}
-			}
-
-			> .other {
-				margin-left: auto;
-
-				> button {
-					padding: 8px;
-					margin: 0 8px;
-
-					&:hover {
-						color: var(--fgHighlighted);
-					}
-				}
-			}
-		}
-
-		> .user {
-			margin-top: 16px;
-			padding: 16px 0 0 0;
-			border-top: solid 0.5px var(--divider);
-			display: flex;
-			align-items: center;
-
-			> .avatar {
-				width: 52px;
-				height: 52px;
-			}
-
-			> .name {
-				margin: 0 0 0 12px;
-				font-size: 90%;
-			}
-
-			> .koudoku {
-				margin-left: auto;
-			}
-		}
-	}
-}
-
-.sdrarzaf {
-	display: grid;
-	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: 12px;
-	margin: var(--margin);
-
-	> .post {
-
-	}
-}
-</style>
diff --git a/src/client/pages/instance-info.vue b/src/client/pages/instance-info.vue
deleted file mode 100644
index 291ceb5dfd..0000000000
--- a/src/client/pages/instance-info.vue
+++ /dev/null
@@ -1,238 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup v-if="instance">
-		<template #label>{{ instance.host }}</template>
-		<FormGroup>
-			<div class="_debobigegoItem">
-				<div class="_debobigegoPanel fnfelxur">
-					<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
-				</div>
-			</div>
-			<FormKeyValueView>
-				<template #key>Name</template>
-				<template #value><span class="_monospace">{{ instance.name || `(${$ts.unknown})` }}</span></template>
-			</FormKeyValueView>
-		</FormGroup>
-
-		<FormButton v-if="$i.isAdmin || $i.isModerator" @click="info" primary>{{ $ts.settings }}</FormButton>
-
-		<FormTextarea readonly :value="instance.description">
-			<span>{{ $ts.description }}</span>
-		</FormTextarea>
-
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>{{ $ts.software }}</template>
-				<template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }}</span></template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>{{ $ts.version }}</template>
-				<template #value><span class="_monospace">{{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template>
-			</FormKeyValueView>
-		</FormGroup>
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>{{ $ts.administrator }}</template>
-				<template #value><span class="_monospace">{{ instance.maintainerName || `(${$ts.unknown})` }}</span></template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>{{ $ts.contact }}</template>
-				<template #value><span class="_monospace">{{ instance.maintainerEmail || `(${$ts.unknown})` }}</span></template>
-			</FormKeyValueView>
-		</FormGroup>
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>{{ $ts.latestRequestSentAt }}</template>
-				<template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>{{ $ts.latestStatus }}</template>
-				<template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>{{ $ts.latestRequestReceivedAt }}</template>
-				<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
-			</FormKeyValueView>
-		</FormGroup>
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>Open Registrations</template>
-				<template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template>
-			</FormKeyValueView>
-		</FormGroup>
-		<div class="_debobigegoItem">
-			<div class="_debobigegoLabel">{{ $ts.statistics }}</div>
-			<div class="_debobigegoPanel cmhjzshl">
-				<div class="selects">
-					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
-						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
-						<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
-						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
-						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
-						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
-						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
-						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
-						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
-						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
-						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
-						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
-					</MkSelect>
-					<MkSelect v-model="chartSpan" style="margin: 0;">
-						<option value="hour">{{ $ts.perHour }}</option>
-						<option value="day">{{ $ts.perDay }}</option>
-					</MkSelect>
-				</div>
-				<div class="chart">
-					<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
-				</div>
-			</div>
-		</div>
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>{{ $ts.registeredAt }}</template>
-				<template #value><MkTime mode="detail" :time="instance.caughtAt"/></template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>{{ $ts.updatedAt }}</template>
-				<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
-			</FormKeyValueView>
-		</FormGroup>
-		<FormObjectView tall :value="instance">
-			<span>Raw</span>
-		</FormObjectView>
-		<FormGroup>
-			<template #label>Well-known resources</template>
-			<FormLink :to="`https://${host}/.well-known/host-meta`" external>host-meta</FormLink>
-			<FormLink :to="`https://${host}/.well-known/host-meta.json`" external>host-meta.json</FormLink>
-			<FormLink :to="`https://${host}/.well-known/nodeinfo`" external>nodeinfo</FormLink>
-			<FormLink :to="`https://${host}/robots.txt`" external>robots.txt</FormLink>
-			<FormLink :to="`https://${host}/manifest.json`" external>manifest.json</FormLink>
-		</FormGroup>
-		<FormSuspense :p="dnsPromiseFactory" v-slot="{ result: dns }">
-			<FormGroup>
-				<template #label>DNS</template>
-				<FormKeyValueView v-for="record in dns.a" :key="record">
-					<template #key>A</template>
-					<template #value><span class="_monospace">{{ record }}</span></template>
-				</FormKeyValueView>
-				<FormKeyValueView v-for="record in dns.aaaa" :key="record">
-					<template #key>AAAA</template>
-					<template #value><span class="_monospace">{{ record }}</span></template>
-				</FormKeyValueView>
-				<FormKeyValueView v-for="record in dns.cname" :key="record">
-					<template #key>CNAME</template>
-					<template #value><span class="_monospace">{{ record }}</span></template>
-				</FormKeyValueView>
-				<FormKeyValueView v-for="record in dns.txt">
-					<template #key>TXT</template>
-					<template #value><span class="_monospace">{{ record[0] }}</span></template>
-				</FormKeyValueView>
-			</FormGroup>
-		</FormSuspense>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import MkChart from '@client/components/chart.vue';
-import FormObjectView from '@client/components/debobigego/object-view.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import MkSelect from '@client/components/form/select.vue';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-import bytes from '@client/filters/bytes';
-import * as symbols from '@client/symbols';
-import MkInstanceInfo from '@client/pages/admin/instance.vue';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormTextarea,
-		FormObjectView,
-		FormButton,
-		FormLink,
-		FormGroup,
-		FormKeyValueView,
-		FormSuspense,
-		MkSelect,
-		MkChart,
-	},
-
-	props: {
-		host: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.instanceInfo,
-				icon: 'fas fa-info-circle',
-				actions: [{
-					text: `https://${this.host}`,
-					icon: 'fas fa-external-link-alt',
-					handler: () => {
-						window.open(`https://${this.host}`, '_blank');
-					}
-				}],
-			},
-			instance: null,
-			dnsPromiseFactory: () => os.api('federation/dns', {
-				host: this.host
-			}),
-			chartSrc: 'instance-requests',
-			chartSpan: 'hour',
-		}
-	},
-
-	mounted() {
-		this.fetch();
-	},
-
-	methods: {
-		number,
-		bytes,
-
-		async fetch() {
-			this.instance = await os.api('federation/show-instance', {
-				host: this.host
-			});
-		},
-
-		info() {
-			os.popup(MkInstanceInfo, {
-				instance: this.instance
-			}, {}, 'closed');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fnfelxur {
-	padding: 16px;
-
-	> .icon {
-		display: block;
-		margin: auto;
-		height: 64px;
-		border-radius: 8px;
-	}
-}
-
-.cmhjzshl {
-	> .selects {
-		display: flex;
-		padding: 16px;
-	}
-}
-</style>
diff --git a/src/client/pages/mentions.vue b/src/client/pages/mentions.vue
deleted file mode 100644
index 04682a856a..0000000000
--- a/src/client/pages/mentions.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<MkSpacer :content-max="800">
-	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
-</MkSpacer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XNotes from '@client/components/notes.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.mentions,
-				icon: 'fas fa-at',
-				bg: 'var(--bg)',
-			},
-			pagination: {
-				endpoint: 'notes/mentions',
-				limit: 10,
-			},
-		};
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/messages.vue b/src/client/pages/messages.vue
deleted file mode 100644
index e3d668cf45..0000000000
--- a/src/client/pages/messages.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-<MkSpacer :content-max="800">
-	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
-</MkSpacer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XNotes from '@client/components/notes.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.directNotes,
-				icon: 'fas fa-envelope',
-				bg: 'var(--bg)',
-			},
-			pagination: {
-				endpoint: 'notes/mentions',
-				limit: 10,
-				params: () => ({
-					visibility: 'specified'
-				})
-			},
-		};
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue
deleted file mode 100644
index 5b4fd51e55..0000000000
--- a/src/client/pages/messaging/index.vue
+++ /dev/null
@@ -1,307 +0,0 @@
-<template>
-<MkSpacer :content-max="800">
-	<div class="yweeujhr" v-size="{ max: [400] }">
-		<MkButton @click="start" primary class="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton>
-
-		<div class="history" v-if="messages.length > 0">
-			<MkA v-for="(message, i) in messages"
-				class="message _block"
-				:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
-				:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
-				:data-index="i"
-				:key="message.id"
-				v-anim="i"
-			>
-				<div>
-					<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/>
-					<header v-if="message.groupId">
-						<span class="name">{{ message.group.name }}</span>
-						<MkTime :time="message.createdAt" class="time"/>
-					</header>
-					<header v-else>
-						<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
-						<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
-						<MkTime :time="message.createdAt" class="time"/>
-					</header>
-					<div class="body">
-						<p class="text"><span class="me" v-if="isMe(message)">{{ $ts.you }}:</span>{{ message.text }}</p>
-					</div>
-				</div>
-			</MkA>
-		</div>
-		<div class="_fullinfo" v-if="!fetching && messages.length == 0">
-			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
-			<div>{{ $ts.noHistory }}</div>
-		</div>
-		<MkLoading v-if="fetching"/>
-	</div>
-</MkSpacer>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
-import { getAcct } from '@/misc/acct';
-import MkButton from '@client/components/ui/button.vue';
-import { acct } from '@client/filters/user';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.messaging,
-				icon: 'fas fa-comments',
-				bg: 'var(--bg)',
-			},
-			fetching: true,
-			moreFetching: false,
-			messages: [],
-			connection: null,
-		};
-	},
-
-	mounted() {
-		this.connection = markRaw(os.stream.useChannel('messagingIndex'));
-
-		this.connection.on('message', this.onMessage);
-		this.connection.on('read', this.onRead);
-
-		os.api('messaging/history', { group: false }).then(userMessages => {
-			os.api('messaging/history', { group: true }).then(groupMessages => {
-				const messages = userMessages.concat(groupMessages);
-				messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
-				this.messages = messages;
-				this.fetching = false;
-			});
-		});
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		getAcct,
-
-		isMe(message) {
-			return message.userId == this.$i.id;
-		},
-
-		onMessage(message) {
-			if (message.recipientId) {
-				this.messages = this.messages.filter(m => !(
-					(m.recipientId == message.recipientId && m.userId == message.userId) ||
-					(m.recipientId == message.userId && m.userId == message.recipientId)));
-
-				this.messages.unshift(message);
-			} else if (message.groupId) {
-				this.messages = this.messages.filter(m => m.groupId !== message.groupId);
-				this.messages.unshift(message);
-			}
-		},
-
-		onRead(ids) {
-			for (const id of ids) {
-				const found = this.messages.find(m => m.id == id);
-				if (found) {
-					if (found.recipientId) {
-						found.isRead = true;
-					} else if (found.groupId) {
-						found.reads.push(this.$i.id);
-					}
-				}
-			}
-		},
-
-		start(ev) {
-			os.popupMenu([{
-				text: this.$ts.messagingWithUser,
-				icon: 'fas fa-user',
-				action: () => { this.startUser() }
-			}, {
-				text: this.$ts.messagingWithGroup,
-				icon: 'fas fa-users',
-				action: () => { this.startGroup() }
-			}], ev.currentTarget || ev.target);
-		},
-
-		async startUser() {
-			os.selectUser().then(user => {
-				this.$router.push(`/my/messaging/${getAcct(user)}`);
-			});
-		},
-
-		async startGroup() {
-			const groups1 = await os.api('users/groups/owned');
-			const groups2 = await os.api('users/groups/joined');
-			if (groups1.length === 0 && groups2.length === 0) {
-				os.dialog({
-					type: 'warning',
-					title: this.$ts.youHaveNoGroups,
-					text: this.$ts.joinOrCreateGroup,
-				});
-				return;
-			}
-			const { canceled, result: group } = await os.dialog({
-				type: null,
-				title: this.$ts.group,
-				select: {
-					items: groups1.concat(groups2).map(group => ({
-						value: group, text: group.name
-					}))
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			this.$router.push(`/my/messaging/group/${group.id}`);
-		},
-
-		acct
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.yweeujhr {
-
-	> .start {
-		margin: 0 auto var(--margin) auto;
-	}
-
-	> .history {
-		> .message {
-			display: block;
-			text-decoration: none;
-			margin-bottom: var(--margin);
-
-			* {
-				pointer-events: none;
-				user-select: none;
-			}
-
-			&:hover {
-				.avatar {
-					filter: saturate(200%);
-				}
-			}
-
-			&:active {
-			}
-
-			&.isRead,
-			&.isMe {
-				opacity: 0.8;
-			}
-
-			&:not(.isMe):not(.isRead) {
-				> div {
-					background-image: url("/static-assets/client/unread.svg");
-					background-repeat: no-repeat;
-					background-position: 0 center;
-				}
-			}
-
-			&:after {
-				content: "";
-				display: block;
-				clear: both;
-			}
-
-			> div {
-				padding: 20px 30px;
-
-				&:after {
-					content: "";
-					display: block;
-					clear: both;
-				}
-
-				> header {
-					display: flex;
-					align-items: center;
-					margin-bottom: 2px;
-					white-space: nowrap;
-					overflow: hidden;
-
-					> .name {
-						margin: 0;
-						padding: 0;
-						overflow: hidden;
-						text-overflow: ellipsis;
-						font-size: 1em;
-						font-weight: bold;
-						transition: all 0.1s ease;
-					}
-
-					> .username {
-						margin: 0 8px;
-					}
-
-					> .time {
-						margin: 0 0 0 auto;
-					}
-				}
-
-				> .avatar {
-					float: left;
-					width: 54px;
-					height: 54px;
-					margin: 0 16px 0 0;
-					border-radius: 8px;
-					transition: all 0.1s ease;
-				}
-
-				> .body {
-
-					> .text {
-						display: block;
-						margin: 0 0 0 0;
-						padding: 0;
-						overflow: hidden;
-						overflow-wrap: break-word;
-						font-size: 1.1em;
-						color: var(--faceText);
-
-						.me {
-							opacity: 0.7;
-						}
-					}
-
-					> .image {
-						display: block;
-						max-width: 100%;
-						max-height: 512px;
-					}
-				}
-			}
-		}
-	}
-
-	&.max-width_400px {
-		> .history {
-			> .message {
-				&:not(.isMe):not(.isRead) {
-					> div {
-						background-image: none;
-						border-left: solid 4px #3aa2dc;
-					}
-				}
-
-				> div {
-					padding: 16px;
-					font-size: 0.9em;
-
-					> .avatar {
-						margin: 0 12px 0 0;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue
deleted file mode 100644
index 31c42e4ab3..0000000000
--- a/src/client/pages/messaging/messaging-room.form.vue
+++ /dev/null
@@ -1,348 +0,0 @@
-<template>
-<div class="pemppnzi _block"
-	@dragover.stop="onDragover"
-	@drop.stop="onDrop"
->
-	<textarea
-		v-model="text"
-		ref="text"
-		@keypress="onKeypress"
-		@compositionupdate="onCompositionUpdate"
-		@paste="onPaste"
-		:placeholder="$ts.inputMessageHere"
-	></textarea>
-	<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
-	<button class="send _button" @click="send" :disabled="!canSend || sending" :title="$ts.send">
-		<template v-if="!sending"><i class="fas fa-paper-plane"></i></template><template v-if="sending"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>
-	</button>
-	<button class="_button" @click="chooseFile"><i class="fas fa-photo-video"></i></button>
-	<button class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
-	<input ref="file" type="file" @change="onChangeFile"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import insertTextAtCursor from 'insert-text-at-cursor';
-import * as autosize from 'autosize';
-import { formatTimeString } from '@/misc/format-time-string';
-import { selectFile } from '@client/scripts/select-file';
-import * as os from '@client/os';
-import { Autocomplete } from '@client/scripts/autocomplete';
-import { throttle } from 'throttle-debounce';
-
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			requird: false,
-		},
-		group: {
-			type: Object,
-			requird: false,
-		},
-	},
-	data() {
-		return {
-			text: null,
-			file: null,
-			sending: false,
-			typing: throttle(3000, () => {
-				os.stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id });
-			}),
-		};
-	},
-	computed: {
-		draftKey(): string {
-			return this.user ? 'user:' + this.user.id : 'group:' + this.group.id;
-		},
-		canSend(): boolean {
-			return (this.text != null && this.text != '') || this.file != null;
-		},
-		room(): any {
-			return this.$parent;
-		}
-	},
-	watch: {
-		text() {
-			this.saveDraft();
-		},
-		file() {
-			this.saveDraft();
-		}
-	},
-	mounted() {
-		autosize(this.$refs.text);
-
-		// TODO: detach when unmount
-		new Autocomplete(this.$refs.text, this, { model: 'text' });
-
-		// 書きかけの投稿を復元
-		const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
-		if (draft) {
-			this.text = draft.data.text;
-			this.file = draft.data.file;
-		}
-	},
-	methods: {
-		async onPaste(e: ClipboardEvent) {
-			const data = e.clipboardData;
-			const items = data.items;
-
-			if (items.length == 1) {
-				if (items[0].kind == 'file') {
-					const file = items[0].getAsFile();
-					const lio = file.name.lastIndexOf('.');
-					const ext = lio >= 0 ? file.name.slice(lio) : '';
-					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
-					const name = this.$store.state.pasteDialog
-						? await os.dialog({
-							title: this.$ts.enterFileName,
-							input: {
-								default: formatted
-							},
-							allowEmpty: false
-						}).then(({ canceled, result }) => canceled ? false : result)
-						: formatted;
-					if (name) this.upload(file, name);
-				}
-			} else {
-				if (items[0].kind == 'file') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.onlyOneFileCanBeAttached
-					});
-				}
-			}
-		},
-
-		onDragover(e) {
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			if (isFile || isDriveFile) {
-				e.preventDefault();
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			}
-		},
-
-		onDrop(e): void {
-			// ファイルだったら
-			if (e.dataTransfer.files.length == 1) {
-				e.preventDefault();
-				this.upload(e.dataTransfer.files[0]);
-				return;
-			} else if (e.dataTransfer.files.length > 1) {
-				e.preventDefault();
-				os.dialog({
-					type: 'error',
-					text: this.$ts.onlyOneFileCanBeAttached
-				});
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				this.file = JSON.parse(driveFile);
-				e.preventDefault();
-			}
-			//#endregion
-		},
-
-		onKeypress(e) {
-			this.typing();
-			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
-				this.send();
-			}
-		},
-
-		onCompositionUpdate() {
-			this.typing();
-		},
-
-		chooseFile(e) {
-			selectFile(e.currentTarget || e.target, this.$ts.selectFile, false).then(file => {
-				this.file = file;
-			});
-		},
-
-		onChangeFile() {
-			this.upload((this.$refs.file as any).files[0]);
-		},
-
-		upload(file: File, name?: string) {
-			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
-				this.file = res;
-			});
-		},
-
-		send() {
-			this.sending = true;
-			os.api('messaging/messages/create', {
-				userId: this.user ? this.user.id : undefined,
-				groupId: this.group ? this.group.id : undefined,
-				text: this.text ? this.text : undefined,
-				fileId: this.file ? this.file.id : undefined
-			}).then(message => {
-				this.clear();
-			}).catch(err => {
-				console.error(err);
-			}).then(() => {
-				this.sending = false;
-			});
-		},
-
-		clear() {
-			this.text = '';
-			this.file = null;
-			this.deleteDraft();
-		},
-
-		saveDraft() {
-			const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
-
-			data[this.draftKey] = {
-				updatedAt: new Date(),
-				data: {
-					text: this.text,
-					file: this.file
-				}
-			}
-
-			localStorage.setItem('message_drafts', JSON.stringify(data));
-		},
-
-		deleteDraft() {
-			const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
-
-			delete data[this.draftKey];
-
-			localStorage.setItem('message_drafts', JSON.stringify(data));
-		},
-
-		async insertEmoji(ev) {
-			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.pemppnzi {
-	position: relative;
-
-	> textarea {
-		cursor: auto;
-		display: block;
-		width: 100%;
-		min-width: 100%;
-		max-width: 100%;
-		height: 80px;
-		margin: 0;
-		padding: 16px 16px 0 16px;
-		resize: none;
-		font-size: 1em;
-		font-family: inherit;
-		outline: none;
-		border: none;
-		border-radius: 0;
-		box-shadow: none;
-		background: transparent;
-		box-sizing: border-box;
-		color: var(--fg);
-	}
-
-	> .file {
-		padding: 8px;
-		color: #444;
-		background: #eee;
-		cursor: pointer;
-	}
-
-	> .send {
-		position: absolute;
-		bottom: 0;
-		right: 0;
-		margin: 0;
-		padding: 16px;
-		font-size: 1em;
-		transition: color 0.1s ease;
-		color: var(--accent);
-
-		&:active {
-			color: var(--accentDarken);
-			transition: color 0s ease;
-		}
-	}
-
-	.files {
-		display: block;
-		margin: 0;
-		padding: 0 8px;
-		list-style: none;
-
-		&:after {
-			content: '';
-			display: block;
-			clear: both;
-		}
-
-		> li {
-			display: block;
-			float: left;
-			margin: 4px;
-			padding: 0;
-			width: 64px;
-			height: 64px;
-			background-color: #eee;
-			background-repeat: no-repeat;
-			background-position: center center;
-			background-size: cover;
-			cursor: move;
-
-			&:hover {
-				> .remove {
-					display: block;
-				}
-			}
-
-			> .remove {
-				display: none;
-				position: absolute;
-				right: -6px;
-				top: -6px;
-				margin: 0;
-				padding: 0;
-				background: transparent;
-				outline: none;
-				border: none;
-				border-radius: 0;
-				box-shadow: none;
-				cursor: pointer;
-			}
-		}
-	}
-
-	._button {
-		margin: 0;
-		padding: 16px;
-		font-size: 1em;
-		font-weight: normal;
-		text-decoration: none;
-		transition: color 0.1s ease;
-
-		&:hover {
-			color: var(--accent);
-		}
-
-		&:active {
-			color: var(--accentDarken);
-			transition: color 0s ease;
-		}
-	}
-
-	input[type=file] {
-		display: none;
-	}
-}
-</style>
diff --git a/src/client/pages/messaging/messaging-room.message.vue b/src/client/pages/messaging/messaging-room.message.vue
deleted file mode 100644
index a2740c0bdc..0000000000
--- a/src/client/pages/messaging/messaging-room.message.vue
+++ /dev/null
@@ -1,350 +0,0 @@
-<template>
-<div class="thvuemwp" :class="{ isMe }" v-size="{ max: [400, 500] }">
-	<MkAvatar class="avatar" :user="message.user" :show-indicator="true"/>
-	<div class="content">
-		<div class="balloon" :class="{ noText: message.text == null }" >
-			<button class="delete-button" v-if="isMe" :title="$ts.delete" @click="del">
-				<img src="/static-assets/client/remove.png" alt="Delete"/>
-			</button>
-			<div class="content" v-if="!message.isDeleted">
-				<Mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$i"/>
-				<div class="file" v-if="message.file">
-					<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
-						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
-						<p v-else>{{ message.file.name }}</p>
-					</a>
-				</div>
-			</div>
-			<div class="content" v-else>
-				<p class="is-deleted">{{ $ts.deleted }}</p>
-			</div>
-		</div>
-		<div></div>
-		<MkUrlPreview v-for="url in urls" :url="url" :key="url" style="margin: 8px 0;"/>
-		<footer>
-			<template v-if="isGroup">
-				<span class="read" v-if="message.reads.length > 0">{{ $ts.messageRead }} {{ message.reads.length }}</span>
-			</template>
-			<template v-else>
-				<span class="read" v-if="isMe && message.isRead">{{ $ts.messageRead }}</span>
-			</template>
-			<MkTime :time="message.createdAt"/>
-			<template v-if="message.is_edited"><i class="fas fa-pencil-alt"></i></template>
-		</footer>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as mfm from 'mfm-js';
-import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
-import MkUrlPreview from '@client/components/url-preview.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkUrlPreview
-	},
-	props: {
-		message: {
-			required: true
-		},
-		isGroup: {
-			required: false
-		}
-	},
-	computed: {
-		isMe(): boolean {
-			return this.message.userId === this.$i.id;
-		},
-		urls(): string[] {
-			if (this.message.text) {
-				return extractUrlFromMfm(mfm.parse(this.message.text));
-			} else {
-				return [];
-			}
-		}
-	},
-	methods: {
-		del() {
-			os.api('messaging/messages/delete', {
-				messageId: this.message.id
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.thvuemwp {
-	$me-balloon-color: var(--accent);
-
-	position: relative;
-	background-color: transparent;
-	display: flex;
-
-	> .avatar {
-		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 16px);
-		display: block;
-		width: 54px;
-		height: 54px;
-		transition: all 0.1s ease;
-	}
-
-	> .content {
-		min-width: 0;
-
-		> .balloon {
-			position: relative;
-			display: inline-flex;
-			align-items: center;
-			padding: 0;
-			min-height: 38px;
-			border-radius: 16px;
-			max-width: 100%;
-
-			&:before {
-				content: "";
-				pointer-events: none;
-				display: block;
-				position: absolute;
-				top: 12px;
-			}
-
-			& + * {
-				clear: both;
-			}
-
-			&:hover {
-				> .delete-button {
-					display: block;
-				}
-			}
-
-			> .delete-button {
-				display: none;
-				position: absolute;
-				z-index: 1;
-				top: -4px;
-				right: -4px;
-				margin: 0;
-				padding: 0;
-				cursor: pointer;
-				outline: none;
-				border: none;
-				border-radius: 0;
-				box-shadow: none;
-				background: transparent;
-
-				> img {
-					vertical-align: bottom;
-					width: 16px;
-					height: 16px;
-					cursor: pointer;
-				}
-			}
-
-			> .content {
-				max-width: 100%;
-
-				> .is-deleted {
-					display: block;
-					margin: 0;
-					padding: 0;
-					overflow: hidden;
-					overflow-wrap: break-word;
-					font-size: 1em;
-					color: rgba(#000, 0.5);
-				}
-
-				> .text {
-					display: block;
-					margin: 0;
-					padding: 12px 18px;
-					overflow: hidden;
-					overflow-wrap: break-word;
-					word-break: break-word;
-					font-size: 1em;
-					color: rgba(#000, 0.8);
-
-					& + .file {
-						> a {
-							border-radius: 0 0 16px 16px;
-						}
-					}
-				}
-
-				> .file {
-					> a {
-						display: block;
-						max-width: 100%;
-						border-radius: 16px;
-						overflow: hidden;
-						text-decoration: none;
-
-						&:hover {
-							text-decoration: none;
-
-							> p {
-								background: #ccc;
-							}
-						}
-
-						> * {
-							display: block;
-							margin: 0;
-							width: 100%;
-							max-height: 512px;
-							object-fit: contain;
-							box-sizing: border-box;
-						}
-
-						> p {
-							padding: 30px;
-							text-align: center;
-							color: #555;
-							background: #ddd;
-						}
-					}
-				}
-			}
-		}
-
-		> footer {
-			display: block;
-			margin: 2px 0 0 0;
-			font-size: 0.65em;
-
-			> .read {
-				margin: 0 8px;
-			}
-
-			> i {
-				margin-left: 4px;
-			}
-		}
-	}
-
-	&:not(.isMe) {
-		padding-left: var(--margin);
-
-		> .content {
-			padding-left: 16px;
-			padding-right: 32px;
-
-			> .balloon {
-				$color: var(--messageBg);
-				background: $color;
-
-				&.noText {
-					background: transparent;
-				}
-
-				&:not(.noText):before {
-					left: -14px;
-					border-top: solid 8px transparent;
-					border-right: solid 8px $color;
-					border-bottom: solid 8px transparent;
-					border-left: solid 8px transparent;
-				}
-
-				> .content {
-					> .text {
-						color: var(--fg);
-					}
-				}
-			}
-
-			> footer {
-				text-align: left;
-			}
-		}
-	}
-
-	&.isMe {
-		flex-direction: row-reverse;
-		padding-right: var(--margin);
-
-		> .content {
-			padding-right: 16px;
-			padding-left: 32px;
-			text-align: right;
-
-			> .balloon {
-				background: $me-balloon-color;
-				text-align: left;
-
-				::selection {
-					color: var(--accent);
-					background-color: #fff;
-				} 
-
-				&.noText {
-					background: transparent;
-				}
-
-				&:not(.noText):before {
-					right: -14px;
-					left: auto;
-					border-top: solid 8px transparent;
-					border-right: solid 8px transparent;
-					border-bottom: solid 8px transparent;
-					border-left: solid 8px $me-balloon-color;
-				}
-
-				> .content {
-
-					> p.is-deleted {
-						color: rgba(#fff, 0.5);
-					}
-
-					> .text {
-						&, ::v-deep(*) {
-							color: var(--fgOnAccent) !important;
-						}
-					}
-				}
-			}
-
-			> footer {
-				text-align: right;
-
-				> .read {
-					user-select: none;
-				}
-			}
-		}
-	}
-
-	&.max-width_400px {
-		> .avatar {
-			width: 48px;
-			height: 48px;
-		}
-
-		> .content {
-			> .balloon {
-				> .content {
-					> .text {
-						font-size: 0.9em;
-					}
-				}
-			}
-		}
-	}
-
-	&.max-width_500px {
-		> .content {
-			> .balloon {
-				> .content {
-					> .text {
-						padding: 8px 16px;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue
deleted file mode 100644
index 76e58d5bc9..0000000000
--- a/src/client/pages/messaging/messaging-room.vue
+++ /dev/null
@@ -1,470 +0,0 @@
-<template>
-<div class="_section"
-	@dragover.prevent.stop="onDragover"
-	@drop.prevent.stop="onDrop"
->
-	<div class="_content mk-messaging-room">
-		<div class="body">
-			<MkLoading v-if="fetching"/>
-			<p class="empty" v-if="!fetching && messages.length == 0"><i class="fas fa-info-circle"></i>{{ $ts.noMessagesYet }}</p>
-			<p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><i class="fas fa-flag"></i>{{ $ts.noMoreHistory }}</p>
-			<button class="more _button" ref="loadMore" :class="{ fetching: fetchingMoreMessages }" v-show="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
-				<template v-if="fetchingMoreMessages"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ fetchingMoreMessages ? $ts.loading : $ts.loadMore }}
-			</button>
-			<XList class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed>
-				<XMessage :message="message" :is-group="group != null" :key="message.id"/>
-			</XList>
-		</div>
-		<footer>
-			<div class="typers" v-if="typers.length > 0">
-				<I18n :src="$ts.typingUsers" text-tag="span" class="users">
-					<template #users>
-						<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
-					</template>
-				</I18n>
-				<MkEllipsis/>
-			</div>
-			<transition name="fade">
-				<div class="new-message" v-show="showIndicator">
-					<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ $ts.newMessageExists }}</button>
-				</div>
-			</transition>
-			<XForm v-if="!fetching" :user="user" :group="group" ref="form" class="form"/>
-		</footer>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import XList from '@client/components/date-separated-list.vue';
-import XMessage from './messaging-room.message.vue';
-import XForm from './messaging-room.form.vue';
-import { parseAcct } from '@/misc/acct';
-import { isBottom, onScrollBottom, scroll } from '@client/scripts/scroll';
-import * as os from '@client/os';
-import { popout } from '@client/scripts/popout';
-import * as sound from '@client/scripts/sound';
-import * as symbols from '@client/symbols';
-
-const Component = defineComponent({
-	components: {
-		XMessage,
-		XForm,
-		XList,
-	},
-
-	inject: ['inWindow'],
-
-	props: {
-		userAcct: {
-			type: String,
-			required: false,
-		},
-		groupId: {
-			type: String,
-			required: false,
-		},
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => !this.fetching ? this.user ? {
-				userName: this.user,
-				avatar: this.user,
-				action: {
-					icon: 'fas fa-ellipsis-h',
-					handler: this.menu,
-				},
-			} : {
-				title: this.group.name,
-				icon: 'fas fa-users',
-				action: {
-					icon: 'fas fa-ellipsis-h',
-					handler: this.menu,
-				},
-			} : null),
-			fetching: true,
-			user: null,
-			group: null,
-			fetchingMoreMessages: false,
-			messages: [],
-			existMoreMessages: false,
-			connection: null,
-			showIndicator: false,
-			timer: null,
-			typers: [],
-			ilObserver: new IntersectionObserver(
-				(entries) => entries.some((entry) => entry.isIntersecting)
-					&& !this.fetching
-					&& !this.fetchingMoreMessages
-					&& this.existMoreMessages
-					&& this.fetchMoreMessages()
-			),
-		};
-	},
-
-	computed: {
-		form(): any {
-			return this.$refs.form;
-		}
-	},
-
-	watch: {
-		userAcct: 'fetch',
-		groupId: 'fetch',
-	},
-
-	mounted() {
-		this.fetch();
-		if (this.$store.state.enableInfiniteScroll) {
-			this.$nextTick(() => this.ilObserver.observe(this.$refs.loadMore as Element));
-		}
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-
-		document.removeEventListener('visibilitychange', this.onVisibilitychange);
-
-		this.ilObserver.disconnect();
-	},
-
-	methods: {
-		async fetch() {
-			this.fetching = true;
-			if (this.userAcct) {
-				const user = await os.api('users/show', parseAcct(this.userAcct));
-				this.user = user;
-			} else {
-				const group = await os.api('users/groups/show', { groupId: this.groupId });
-				this.group = group;
-			}
-
-			this.connection = markRaw(os.stream.useChannel('messaging', {
-				otherparty: this.user ? this.user.id : undefined,
-				group: this.group ? this.group.id : undefined,
-			}));
-
-			this.connection.on('message', this.onMessage);
-			this.connection.on('read', this.onRead);
-			this.connection.on('deleted', this.onDeleted);
-			this.connection.on('typers', typers => {
-				this.typers = typers.filter(u => u.id !== this.$i.id);
-			});
-
-			document.addEventListener('visibilitychange', this.onVisibilitychange);
-
-			this.fetchMessages().then(() => {
-				this.scrollToBottom();
-
-				// もっと見るの交差検知を発火させないためにfetchは
-				// スクロールが終わるまでfalseにしておく
-				// scrollendのようなイベントはないのでsetTimeoutで
-				setTimeout(() => this.fetching = false, 300);
-			});
-		},
-
-		onDragover(e) {
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-
-			if (isFile || isDriveFile) {
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			} else {
-				e.dataTransfer.dropEffect = 'none';
-			}
-		},
-
-		onDrop(e): void {
-			// ファイルだったら
-			if (e.dataTransfer.files.length == 1) {
-				this.form.upload(e.dataTransfer.files[0]);
-				return;
-			} else if (e.dataTransfer.files.length > 1) {
-				os.dialog({
-					type: 'error',
-					text: this.$ts.onlyOneFileCanBeAttached
-				});
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.form.file = file;
-			}
-			//#endregion
-		},
-
-		fetchMessages() {
-			return new Promise((resolve, reject) => {
-				const max = this.existMoreMessages ? 20 : 10;
-
-				os.api('messaging/messages', {
-					userId: this.user ? this.user.id : undefined,
-					groupId: this.group ? this.group.id : undefined,
-					limit: max + 1,
-					untilId: this.existMoreMessages ? this.messages[0].id : undefined
-				}).then(messages => {
-					if (messages.length == max + 1) {
-						this.existMoreMessages = true;
-						messages.pop();
-					} else {
-						this.existMoreMessages = false;
-					}
-
-					this.messages.unshift.apply(this.messages, messages.reverse());
-					resolve();
-				});
-			});
-		},
-
-		fetchMoreMessages() {
-			this.fetchingMoreMessages = true;
-			this.fetchMessages().then(() => {
-				this.fetchingMoreMessages = false;
-			});
-		},
-
-		onMessage(message) {
-			sound.play('chat');
-
-			const _isBottom = isBottom(this.$el, 64);
-
-			this.messages.push(message);
-			if (message.userId != this.$i.id && !document.hidden) {
-				this.connection.send('read', {
-					id: message.id
-				});
-			}
-
-			if (_isBottom) {
-				// Scroll to bottom
-				this.$nextTick(() => {
-					this.scrollToBottom();
-				});
-			} else if (message.userId != this.$i.id) {
-				// Notify
-				this.notifyNewMessage();
-			}
-		},
-
-		onRead(x) {
-			if (this.user) {
-				if (!Array.isArray(x)) x = [x];
-				for (const id of x) {
-					if (this.messages.some(x => x.id == id)) {
-						const exist = this.messages.map(x => x.id).indexOf(id);
-						this.messages[exist] = {
-							...this.messages[exist],
-							isRead: true,
-						};
-					}
-				}
-			} else if (this.group) {
-				for (const id of x.ids) {
-					if (this.messages.some(x => x.id == id)) {
-						const exist = this.messages.map(x => x.id).indexOf(id);
-						this.messages[exist] = {
-							...this.messages[exist],
-							reads: [...this.messages[exist].reads, x.userId]
-						};
-					}
-				}
-			}
-		},
-
-		onDeleted(id) {
-			const msg = this.messages.find(m => m.id === id);
-			if (msg) {
-				this.messages = this.messages.filter(m => m.id !== msg.id);
-			}
-		},
-
-		scrollToBottom() {
-			scroll(this.$el, { top: this.$el.offsetHeight });
-		},
-
-		onIndicatorClick() {
-			this.showIndicator = false;
-			this.scrollToBottom();
-		},
-
-		notifyNewMessage() {
-			this.showIndicator = true;
-
-			onScrollBottom(this.$el, () => {
-				this.showIndicator = false;
-			});
-
-			if (this.timer) clearTimeout(this.timer);
-
-			this.timer = setTimeout(() => {
-				this.showIndicator = false;
-			}, 4000);
-		},
-
-		onVisibilitychange() {
-			if (document.hidden) return;
-			for (const message of this.messages) {
-				if (message.userId !== this.$i.id && !message.isRead) {
-					this.connection.send('read', {
-						id: message.id
-					});
-				}
-			}
-		},
-
-		menu(ev) {
-			const path = this.groupId ? `/my/messaging/group/${this.groupId}` : `/my/messaging/${this.userAcct}`;
-
-			os.popupMenu([this.inWindow ? undefined : {
-				text: this.$ts.openInWindow,
-				icon: 'fas fa-window-maximize',
-				action: () => {
-					os.pageWindow(path);
-					this.$router.back();
-				},
-			}, this.inWindow ? undefined : {
-				text: this.$ts.popout,
-				icon: 'fas fa-external-link-alt',
-				action: () => {
-					popout(path);
-					this.$router.back();
-				},
-			}], ev.currentTarget || ev.target);
-		}
-	}
-});
-
-export default Component;
-</script>
-
-<style lang="scss" scoped>
-.mk-messaging-room {
-	> .body {
-		> .empty {
-			width: 100%;
-			margin: 0;
-			padding: 16px 8px 8px 8px;
-			text-align: center;
-			font-size: 0.8em;
-			opacity: 0.5;
-
-			i {
-				margin-right: 4px;
-			}
-		}
-
-		> .no-history {
-			display: block;
-			margin: 0;
-			padding: 16px;
-			text-align: center;
-			font-size: 0.8em;
-			color: var(--messagingRoomInfo);
-			opacity: 0.5;
-
-			i {
-				margin-right: 4px;
-			}
-		}
-
-		> .more {
-			display: block;
-			margin: 16px auto;
-			padding: 0 12px;
-			line-height: 24px;
-			color: #fff;
-			background: rgba(#000, 0.3);
-			border-radius: 12px;
-
-			&:hover {
-				background: rgba(#000, 0.4);
-			}
-
-			&:active {
-				background: rgba(#000, 0.5);
-			}
-
-			&.fetching {
-				cursor: wait;
-			}
-
-			> i {
-				margin-right: 4px;
-			}
-		}
-
-		> .messages {
-			> ::v-deep(*) {
-				margin-bottom: 16px;
-			}
-		}
-	}
-
-	> footer {
-		width: 100%;
-		position: relative;
-
-		> .new-message {
-			position: absolute;
-			top: -48px;
-			width: 100%;
-			padding: 8px 0;
-			text-align: center;
-
-			> button {
-				display: inline-block;
-				margin: 0;
-				padding: 0 12px 0 30px;
-				line-height: 32px;
-				font-size: 12px;
-				border-radius: 16px;
-
-				> i {
-					position: absolute;
-					top: 0;
-					left: 10px;
-					line-height: 32px;
-					font-size: 16px;
-				}
-			}
-		}
-
-		> .typers {
-			position: absolute;
-			bottom: 100%;
-			padding: 0 8px 0 8px;
-			font-size: 0.9em;
-			color: var(--fgTransparentWeak);
-
-			> .users {
-				> .user + .user:before {
-					content: ", ";
-					font-weight: normal;
-				}
-
-				> .user:last-of-type:after {
-					content: " ";
-				}
-			}
-		}
-
-		> .form {
-			border-top: solid 0.5px var(--divider);
-		}
-	}
-}
-
-.fade-enter-active, .fade-leave-active {
-	transition: opacity 0.1s;
-}
-
-.fade-enter-from, .fade-leave-to {
-	transition: opacity 0.5s;
-	opacity: 0;
-}
-</style>
diff --git a/src/client/pages/mfm-cheat-sheet.vue b/src/client/pages/mfm-cheat-sheet.vue
deleted file mode 100644
index 5ff4317627..0000000000
--- a/src/client/pages/mfm-cheat-sheet.vue
+++ /dev/null
@@ -1,365 +0,0 @@
-<template>
-<div class="mwysmxbg">
-	<div class="_isolated">{{ $ts._mfm.intro }}</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.mention }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.mentionDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_mention"/>
-				<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.hashtag }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.hashtagDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_hashtag"/>
-				<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.url }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.urlDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_url"/>
-				<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.link }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.linkDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_link"/>
-				<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.emoji }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.emojiDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_emoji"/>
-				<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.bold }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.boldDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_bold"/>
-				<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.small }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.smallDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_small"/>
-				<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.quote }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.quoteDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_quote"/>
-				<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.center }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.centerDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_center"/>
-				<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.inlineCode }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.inlineCodeDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_inlineCode"/>
-				<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.blockCode }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.blockCodeDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_blockCode"/>
-				<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.inlineMath }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.inlineMathDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_inlineMath"/>
-				<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.search }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.searchDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_search"/>
-				<MkTextarea v-model="preview_search"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.flip }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.flipDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_flip"/>
-				<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.font }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.fontDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_font"/>
-				<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.x2 }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.x2Description }}</p>
-			<div class="preview">
-				<Mfm :text="preview_x2"/>
-				<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.x3 }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.x3Description }}</p>
-			<div class="preview">
-				<Mfm :text="preview_x3"/>
-				<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.x4 }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.x4Description }}</p>
-			<div class="preview">
-				<Mfm :text="preview_x4"/>
-				<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.blur }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.blurDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_blur"/>
-				<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.jelly }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.jellyDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_jelly"/>
-				<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.tada }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.tadaDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_tada"/>
-				<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.jump }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.jumpDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_jump"/>
-				<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.bounce }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.bounceDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_bounce"/>
-				<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.spin }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.spinDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_spin"/>
-				<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.shake }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.shakeDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_shake"/>
-				<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.twitch }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.twitchDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_twitch"/>
-				<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.rainbow }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.rainbowDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_rainbow"/>
-				<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea>
-			</div>
-		</div>
-	</div>
-	<div class="section _block">
-		<div class="title">{{ $ts._mfm.sparkle }}</div>
-		<div class="content">
-			<p>{{ $ts._mfm.sparkleDescription }}</p>
-			<div class="preview">
-				<Mfm :text="preview_sparkle"/>
-				<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkTextarea
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._mfm.cheatSheet,
-				icon: 'fas fa-question-circle',
-			},
-			preview_mention: '@example',
-			preview_hashtag: '#test',
-			preview_url: `https://example.com`,
-			preview_link: `[${this.$ts._mfm.dummy}](https://example.com)`,
-			preview_emoji: this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:`,
-			preview_bold: `**${this.$ts._mfm.dummy}**`,
-			preview_small: `<small>${this.$ts._mfm.dummy}</small>`,
-			preview_center: `<center>${this.$ts._mfm.dummy}</center>`,
-			preview_inlineCode: '`<: "Hello, world!"`',
-			preview_blockCode: '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
-			preview_inlineMath: '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)',
-			preview_quote: `> ${this.$ts._mfm.dummy}`,
-			preview_search: `${this.$ts._mfm.dummy} 検索`,
-			preview_jelly: `$[jelly 🍮]`,
-			preview_tada: `$[tada 🍮]`,
-			preview_jump: `$[jump 🍮]`,
-			preview_bounce: `$[bounce 🍮]`,
-			preview_shake: `$[shake 🍮]`,
-			preview_twitch: `$[twitch 🍮]`,
-			preview_spin: `$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]`,
-			preview_flip: `$[flip ${this.$ts._mfm.dummy}]\n$[flip.v ${this.$ts._mfm.dummy}]\n$[flip.h,v ${this.$ts._mfm.dummy}]`,
-			preview_font: `$[font.serif ${this.$ts._mfm.dummy}]\n$[font.monospace ${this.$ts._mfm.dummy}]\n$[font.cursive ${this.$ts._mfm.dummy}]\n$[font.fantasy ${this.$ts._mfm.dummy}]`,
-			preview_x2: `$[x2 🍮]`,
-			preview_x3: `$[x3 🍮]`,
-			preview_x4: `$[x4 🍮]`,
-			preview_blur: `$[blur ${this.$ts._mfm.dummy}]`,
-			preview_rainbow: `$[rainbow 🍮]`,
-			preview_sparkle: `$[sparkle 🍮]`,
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.mwysmxbg {
-	background: var(--bg);
-
-	> .section {
-		> .title {
-			position: sticky;
-			z-index: 1;
-			top: var(--stickyTop, 0px);
-			padding: 16px;
-			font-weight: bold;
-			-webkit-backdrop-filter: var(--blur, blur(10px));
-			backdrop-filter: var(--blur, blur(10px));
-			background-color: var(--X16);
-		}
-
-		> .content {
-			> p {
-				margin: 0;
-				padding: 16px;
-			}
-
-			> .preview {
-				border-top: solid 0.5px var(--divider);
-				padding: 16px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/miauth.vue b/src/client/pages/miauth.vue
deleted file mode 100644
index 39cd832838..0000000000
--- a/src/client/pages/miauth.vue
+++ /dev/null
@@ -1,100 +0,0 @@
-<template>
-<div v-if="$i">
-	<div class="waiting _section" v-if="state == 'waiting'">
-		<div class="_content">
-			<MkLoading/>
-		</div>
-	</div>
-	<div class="denied _section" v-if="state == 'denied'">
-		<div class="_content">
-			<p>{{ $ts._auth.denied }}</p>
-		</div>
-	</div>
-	<div class="accepted _section" v-else-if="state == 'accepted'">
-		<div class="_content">
-			<p v-if="callback">{{ $ts._auth.callback }}<MkEllipsis/></p>
-			<p v-else>{{ $ts._auth.pleaseGoBack }}</p>
-		</div>
-	</div>
-	<div class="_section" v-else>
-		<div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div>
-		<div class="_title" v-else>{{ $ts._auth.shareAccessAsk }}</div>
-		<div class="_content">
-			<p>{{ $ts._auth.permissionAsk }}</p>
-			<ul>
-				<li v-for="p in permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
-			</ul>
-		</div>
-		<div class="_footer">
-			<MkButton @click="deny" inline>{{ $ts.cancel }}</MkButton>
-			<MkButton @click="accept" inline primary>{{ $ts.accept }}</MkButton>
-		</div>
-	</div>
-</div>
-<div class="signin" v-else>
-	<MkSignin @login="onLogin"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkSignin from '@client/components/signin.vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import { login } from '@client/account';
-
-export default defineComponent({
-	components: {
-		MkSignin,
-		MkButton,
-	},
-	data() {
-		return {
-			state: null
-		};
-	},
-	computed: {
-		session(): string {
-			return this.$route.params.session;
-		},
-		callback(): string {
-			return this.$route.query.callback;
-		},
-		name(): string {
-			return this.$route.query.name;
-		},
-		icon(): string {
-			return this.$route.query.icon;
-		},
-		permission(): string[] {
-			return this.$route.query.permission ? this.$route.query.permission.split(',') : [];
-		},
-	},
-	methods: {
-		async accept() {
-			this.state = 'waiting';
-			await os.api('miauth/gen-token', {
-				session: this.session,
-				name: this.name,
-				iconUrl: this.icon,
-				permission: this.permission,
-			});
-
-			this.state = 'accepted';
-			if (this.callback) {
-				location.href = `${this.callback}?session=${this.session}`;
-			}
-		},
-		deny() {
-			this.state = 'denied';
-		},
-		onLogin(res) {
-			login(res.i);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/my-antennas/create.vue b/src/client/pages/my-antennas/create.vue
deleted file mode 100644
index d4762411e7..0000000000
--- a/src/client/pages/my-antennas/create.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-<div class="geegznzt">
-	<XAntenna :antenna="draft" @created="onAntennaCreated"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import XAntenna from './editor.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		XAntenna,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.manageAntennas,
-				icon: 'fas fa-satellite',
-			},
-			draft: {
-				name: '',
-				src: 'all',
-				userListId: null,
-				userGroupId: null,
-				users: [],
-				keywords: [],
-				excludeKeywords: [],
-				withReplies: false,
-				caseSensitive: false,
-				withFile: false,
-				notify: false
-			},
-		};
-	},
-
-	methods: {
-		onAntennaCreated() {
-			this.$router.push('/my/antennas');
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/my-antennas/edit.vue b/src/client/pages/my-antennas/edit.vue
deleted file mode 100644
index 9deafb4235..0000000000
--- a/src/client/pages/my-antennas/edit.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-<div class="">
-	<XAntenna v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import XAntenna from './editor.vue';
-import * as symbols from '@client/symbols';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		XAntenna,
-	},
-
-	props: {
-		antennaId: {
-			type: String,
-			required: true,
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.manageAntennas,
-				icon: 'fas fa-satellite',
-			},
-			antenna: null,
-		};
-	},
-
-	watch: {
-		antennaId: {
-			async handler() {
-				this.antenna = await os.api('antennas/show', { antennaId: this.antennaId });
-			},
-			immediate: true,
-		}
-	},
-
-	methods: {
-		onAntennaUpdated() {
-			this.$router.push('/my/antennas');
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/my-antennas/editor.vue b/src/client/pages/my-antennas/editor.vue
deleted file mode 100644
index 93ab640030..0000000000
--- a/src/client/pages/my-antennas/editor.vue
+++ /dev/null
@@ -1,190 +0,0 @@
-<template>
-<div class="shaynizk">
-	<div class="form">
-		<MkInput v-model="name" class="_formBlock">
-			<template #label>{{ $ts.name }}</template>
-		</MkInput>
-		<MkSelect v-model="src" class="_formBlock">
-			<template #label>{{ $ts.antennaSource }}</template>
-			<option value="all">{{ $ts._antennaSources.all }}</option>
-			<option value="home">{{ $ts._antennaSources.homeTimeline }}</option>
-			<option value="users">{{ $ts._antennaSources.users }}</option>
-			<option value="list">{{ $ts._antennaSources.userList }}</option>
-			<option value="group">{{ $ts._antennaSources.userGroup }}</option>
-		</MkSelect>
-		<MkSelect v-model="userListId" v-if="src === 'list'" class="_formBlock">
-			<template #label>{{ $ts.userList }}</template>
-			<option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option>
-		</MkSelect>
-		<MkSelect v-model="userGroupId" v-else-if="src === 'group'" class="_formBlock">
-			<template #label>{{ $ts.userGroup }}</template>
-			<option v-for="group in userGroups" :value="group.id" :key="group.id">{{ group.name }}</option>
-		</MkSelect>
-		<MkTextarea v-model="users" v-else-if="src === 'users'" class="_formBlock">
-			<template #label>{{ $ts.users }}</template>
-			<template #caption>{{ $ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ $ts.addUser }}</button></template>
-		</MkTextarea>
-		<MkSwitch v-model="withReplies" class="_formBlock">{{ $ts.withReplies }}</MkSwitch>
-		<MkTextarea v-model="keywords" class="_formBlock">
-			<template #label>{{ $ts.antennaKeywords }}</template>
-			<template #caption>{{ $ts.antennaKeywordsDescription }}</template>
-		</MkTextarea>
-		<MkTextarea v-model="excludeKeywords" class="_formBlock">
-			<template #label>{{ $ts.antennaExcludeKeywords }}</template>
-			<template #caption>{{ $ts.antennaKeywordsDescription }}</template>
-		</MkTextarea>
-		<MkSwitch v-model="caseSensitive" class="_formBlock">{{ $ts.caseSensitive }}</MkSwitch>
-		<MkSwitch v-model="withFile" class="_formBlock">{{ $ts.withFileAntenna }}</MkSwitch>
-		<MkSwitch v-model="notify" class="_formBlock">{{ $ts.notifyAntenna }}</MkSwitch>
-	</div>
-	<div class="actions">
-		<MkButton inline @click="saveAntenna()" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-		<MkButton inline @click="deleteAntenna()" v-if="antenna.id != null" danger><i class="fas fa-trash"></i> {{ $ts.delete }}</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import { getAcct } from '@/misc/acct';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		MkButton, MkInput, MkTextarea, MkSelect, MkSwitch
-	},
-
-	props: {
-		antenna: {
-			type: Object,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			name: '',
-			src: '',
-			userListId: null,
-			userGroupId: null,
-			users: '',
-			keywords: '',
-			excludeKeywords: '',
-			caseSensitive: false,
-			withReplies: false,
-			withFile: false,
-			notify: false,
-			userLists: null,
-			userGroups: null,
-		};
-	},
-
-	watch: {
-		async src() {
-			if (this.src === 'list' && this.userLists === null) {
-				this.userLists = await os.api('users/lists/list');
-			}
-
-			if (this.src === 'group' && this.userGroups === null) {
-				const groups1 = await os.api('users/groups/owned');
-				const groups2 = await os.api('users/groups/joined');
-
-				this.userGroups = [...groups1, ...groups2];
-			}
-		}
-	},
-
-	created() {
-		this.name = this.antenna.name;
-		this.src = this.antenna.src;
-		this.userListId = this.antenna.userListId;
-		this.userGroupId = this.antenna.userGroupId;
-		this.users = this.antenna.users.join('\n');
-		this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n');
-		this.excludeKeywords = this.antenna.excludeKeywords.map(x => x.join(' ')).join('\n');
-		this.caseSensitive = this.antenna.caseSensitive;
-		this.withReplies = this.antenna.withReplies;
-		this.withFile = this.antenna.withFile;
-		this.notify = this.antenna.notify;
-	},
-
-	methods: {
-		async saveAntenna() {
-			if (this.antenna.id == null) {
-				await os.apiWithDialog('antennas/create', {
-					name: this.name,
-					src: this.src,
-					userListId: this.userListId,
-					userGroupId: this.userGroupId,
-					withReplies: this.withReplies,
-					withFile: this.withFile,
-					notify: this.notify,
-					caseSensitive: this.caseSensitive,
-					users: this.users.trim().split('\n').map(x => x.trim()),
-					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
-					excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
-				});
-				this.$emit('created');
-			} else {
-				await os.apiWithDialog('antennas/update', {
-					antennaId: this.antenna.id,
-					name: this.name,
-					src: this.src,
-					userListId: this.userListId,
-					userGroupId: this.userGroupId,
-					withReplies: this.withReplies,
-					withFile: this.withFile,
-					notify: this.notify,
-					caseSensitive: this.caseSensitive,
-					users: this.users.trim().split('\n').map(x => x.trim()),
-					keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
-					excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
-				});
-				this.$emit('updated');
-			}
-		},
-
-		async deleteAntenna() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: this.antenna.name }),
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			await os.api('antennas/delete', {
-				antennaId: this.antenna.id,
-			});
-
-			os.success();
-			this.$emit('deleted');
-		},
-
-		addUser() {
-			os.selectUser().then(user => {
-				this.users = this.users.trim();
-				this.users += '\n@' + getAcct(user);
-				this.users = this.users.trim();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.shaynizk {
-	> .form {
-		padding: 32px;
-	}
-
-	> .actions {
-		padding: 24px 32px;
-		border-top: solid 0.5px var(--divider);
-	}
-}
-</style>
diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue
deleted file mode 100644
index c27bb2c15e..0000000000
--- a/src/client/pages/my-antennas/index.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<template>
-<div class="ieepwinx _section">
-	<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
-
-	<div class="_content">
-		<MkPagination :pagination="pagination" #default="{items}" ref="list">
-			<MkA class="ljoevbzj" v-for="antenna in items" :key="antenna.id" :to="`/my/antennas/${antenna.id}`">
-				<div class="name">{{ antenna.name }}</div>
-			</MkA>
-		</MkPagination>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkButton,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.manageAntennas,
-				icon: 'fas fa-satellite',
-				action: {
-					icon: 'fas fa-plus',
-					handler: this.create
-				}
-			},
-			pagination: {
-				endpoint: 'antennas/list',
-				limit: 10,
-			},
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ieepwinx {
-	padding: 16px;
-
-	> .add {
-		margin: 0 auto 16px auto;
-	}
-
-	.ljoevbzj {
-		display: block;
-		padding: 16px;
-		margin-bottom: 8px;
-		border: solid 1px var(--divider);
-		border-radius: 6px;
-
-		&:hover {
-			border: solid 1px var(--accent);
-			text-decoration: none;
-		}
-
-		> .name {
-			font-weight: bold;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/my-clips/index.vue b/src/client/pages/my-clips/index.vue
deleted file mode 100644
index c4ca474748..0000000000
--- a/src/client/pages/my-clips/index.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<template>
-<div class="_section qtcaoidl">
-	<MkButton @click="create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
-
-	<div class="_content">
-		<MkPagination :pagination="pagination" #default="{items}" ref="list" class="list">
-			<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
-				<b>{{ item.name }}</b>
-				<div v-if="item.description" class="description">{{ item.description }}</div>
-			</MkA>
-		</MkPagination>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkButton,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.clip,
-				icon: 'fas fa-paperclip',
-				action: {
-					icon: 'fas fa-plus',
-					handler: this.create
-				}
-			},
-			pagination: {
-				endpoint: 'clips/list',
-				limit: 10,
-			},
-			draft: null,
-		};
-	},
-
-	methods: {
-		async create() {
-			const { canceled, result } = await os.form(this.$ts.createNewClip, {
-				name: {
-					type: 'string',
-					label: this.$ts.name
-				},
-				description: {
-					type: 'string',
-					required: false,
-					multiline: true,
-					label: this.$ts.description
-				},
-				isPublic: {
-					type: 'boolean',
-					label: this.$ts.public,
-					default: false
-				}
-			});
-			if (canceled) return;
-
-			os.apiWithDialog('clips/create', result);
-		},
-
-		onClipCreated() {
-			this.$refs.list.reload();
-			this.draft = null;
-		},
-
-		onClipDeleted() {
-			this.$refs.list.reload();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.qtcaoidl {
-	> .add {
-		margin: 0 auto 16px auto;
-	}
-
-	> ._content {
-		> .list {
-			> .item {
-				display: block;
-				padding: 16px;
-
-				> .description {
-					margin-top: 8px;
-					padding-top: 8px;
-					border-top: solid 0.5px var(--divider);
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/my-groups/group.vue b/src/client/pages/my-groups/group.vue
deleted file mode 100644
index bd5537cbfa..0000000000
--- a/src/client/pages/my-groups/group.vue
+++ /dev/null
@@ -1,184 +0,0 @@
-<template>
-<div class="mk-group-page">
-	<transition name="zoom" mode="out-in">
-		<div v-if="group" class="_section">
-			<div class="_content">
-				<MkButton inline @click="invite()">{{ $ts.invite }}</MkButton>
-				<MkButton inline @click="renameGroup()">{{ $ts.rename }}</MkButton>
-				<MkButton inline @click="transfer()">{{ $ts.transfer }}</MkButton>
-				<MkButton inline @click="deleteGroup()">{{ $ts.delete }}</MkButton>
-			</div>
-		</div>
-	</transition>
-
-	<transition name="zoom" mode="out-in">
-		<div v-if="group" class="_section members _gap">
-			<div class="_title">{{ $ts.members }}</div>
-			<div class="_content">
-				<div class="users">
-					<div class="user _panel" v-for="user in users" :key="user.id">
-						<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
-						<div class="body">
-							<MkUserName :user="user" class="name"/>
-							<MkAcct :user="user" class="acct"/>
-						</div>
-						<div class="action">
-							<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	props: {
-		groupId: {
-			type: String,
-			required: true,
-		},
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.group ? {
-				title: this.group.name,
-				icon: 'fas fa-users',
-			} : null),
-			group: null,
-			users: [],
-		};
-	},
-
-	watch: {
-		groupId: 'fetch',
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			Progress.start();
-			os.api('users/groups/show', {
-				groupId: this.groupId
-			}).then(group => {
-				this.group = group;
-				os.api('users/show', {
-					userIds: this.group.userIds
-				}).then(users => {
-					this.users = users;
-					Progress.done();
-				});
-			});
-		},
-
-		invite() {
-			os.selectUser().then(user => {
-				os.apiWithDialog('users/groups/invite', {
-					groupId: this.group.id,
-					userId: user.id
-				});
-			});
-		},
-
-		removeUser(user) {
-			os.api('users/groups/pull', {
-				groupId: this.group.id,
-				userId: user.id
-			}).then(() => {
-				this.users = this.users.filter(x => x.id !== user.id);
-			});
-		},
-
-		async renameGroup() {
-			const { canceled, result: name } = await os.dialog({
-				title: this.$ts.groupName,
-				input: {
-					default: this.group.name
-				}
-			});
-			if (canceled) return;
-
-			await os.api('users/groups/update', {
-				groupId: this.group.id,
-				name: name
-			});
-
-			this.group.name = name;
-		},
-
-		transfer() {
-			os.selectUser().then(user => {
-				os.apiWithDialog('users/groups/transfer', {
-					groupId: this.group.id,
-					userId: user.id
-				});
-			});
-		},
-
-		async deleteGroup() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: this.group.name }),
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			await os.apiWithDialog('users/groups/delete', {
-				groupId: this.group.id
-			});
-			this.$router.push('/my/groups');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-group-page {
-	> .members {
-		> ._content {
-			> .users {
-				> .user {
-					display: flex;
-					align-items: center;
-					padding: 16px;
-
-					> .avatar {
-						width: 50px;
-						height: 50px;
-					}
-
-					> .body {
-						flex: 1;
-						padding: 8px;
-
-						> .name {
-							display: block;
-							font-weight: bold;
-						}
-
-						> .acct {
-							opacity: 0.5;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/my-groups/index.vue b/src/client/pages/my-groups/index.vue
deleted file mode 100644
index 34f82f8a71..0000000000
--- a/src/client/pages/my-groups/index.vue
+++ /dev/null
@@ -1,121 +0,0 @@
-<template>
-<div class="">
-	<div class="_section" style="padding: 0;">
-		<MkTab v-model="tab">
-			<option value="owned">{{ $ts.ownedGroups }}</option>
-			<option value="joined">{{ $ts.joinedGroups }}</option>
-			<option value="invites"><i class="fas fa-envelope-open-text"></i> {{ $ts.invites }}</option>
-		</MkTab>
-	</div>
-
-	<div class="_section">
-		<div class="_content" v-if="tab === 'owned'">
-			<MkButton @click="create" primary style="margin: 0 auto var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.createGroup }}</MkButton>
-
-			<MkPagination :pagination="ownedPagination" #default="{items}" ref="owned">
-				<div class="_card" v-for="group in items" :key="group.id">
-					<div class="_title"><MkA :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</MkA></div>
-					<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
-				</div>
-			</MkPagination>
-		</div>
-
-		<div class="_content" v-else-if="tab === 'joined'">
-			<MkPagination :pagination="joinedPagination" #default="{items}" ref="joined">
-				<div class="_card" v-for="group in items" :key="group.id">
-					<div class="_title">{{ group.name }}</div>
-					<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
-				</div>
-			</MkPagination>
-		</div>
-	
-		<div class="_content" v-else-if="tab === 'invites'">
-			<MkPagination :pagination="invitationPagination" #default="{items}" ref="invitations">
-				<div class="_card" v-for="invitation in items" :key="invitation.id">
-					<div class="_title">{{ invitation.group.name }}</div>
-					<div class="_content"><MkAvatars :user-ids="invitation.group.userIds"/></div>
-					<div class="_footer">
-						<MkButton @click="acceptInvite(invitation)" primary inline><i class="fas fa-check"></i> {{ $ts.accept }}</MkButton>
-						<MkButton @click="rejectInvite(invitation)" primary inline><i class="fas fa-ban"></i> {{ $ts.reject }}</MkButton>
-					</div>
-				</div>
-			</MkPagination>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkAvatars from '@client/components/avatars.vue';
-import MkTab from '@client/components/tab.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkButton,
-		MkContainer,
-		MkTab,
-		MkAvatars,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.groups,
-				icon: 'fas fa-users'
-			},
-			tab: 'owned',
-			ownedPagination: {
-				endpoint: 'users/groups/owned',
-				limit: 10,
-			},
-			joinedPagination: {
-				endpoint: 'users/groups/joined',
-				limit: 10,
-			},
-			invitationPagination: {
-				endpoint: 'i/user-group-invites',
-				limit: 10,
-			},
-		};
-	},
-
-	methods: {
-		async create() {
-			const { canceled, result: name } = await os.dialog({
-				title: this.$ts.groupName,
-				input: true
-			});
-			if (canceled) return;
-			await os.api('users/groups/create', { name: name });
-			this.$refs.owned.reload();
-			os.success();
-		},
-		acceptInvite(invitation) {
-			os.api('users/groups/invitations/accept', {
-				invitationId: invitation.id
-			}).then(() => {
-				os.success();
-				this.$refs.invitations.reload();
-				this.$refs.joined.reload();
-			});
-		},
-		rejectInvite(invitation) {
-			os.api('users/groups/invitations/reject', {
-				invitationId: invitation.id
-			}).then(() => {
-				this.$refs.invitations.reload();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/src/client/pages/my-lists/index.vue b/src/client/pages/my-lists/index.vue
deleted file mode 100644
index 687e9e630e..0000000000
--- a/src/client/pages/my-lists/index.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<template>
-<div class="qkcjvfiv">
-	<MkButton @click="create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
-
-	<MkPagination :pagination="pagination" #default="{items}" class="lists _content" ref="list">
-		<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
-			<div class="name">{{ list.name }}</div>
-			<MkAvatars :user-ids="list.userIds"/>
-		</MkA>
-	</MkPagination>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkAvatars from '@client/components/avatars.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkButton,
-		MkAvatars,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.manageLists,
-				icon: 'fas fa-list-ul',
-				bg: 'var(--bg)',
-				action: {
-					icon: 'fas fa-plus',
-					handler: this.create
-				},
-			},
-			pagination: {
-				endpoint: 'users/lists/list',
-				limit: 10,
-			},
-		};
-	},
-
-	methods: {
-		async create() {
-			const { canceled, result: name } = await os.dialog({
-				title: this.$ts.enterListName,
-				input: true
-			});
-			if (canceled) return;
-			await os.api('users/lists/create', { name: name });
-			this.$refs.list.reload();
-			os.success();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.qkcjvfiv {
-	padding: 16px;
-
-	> .add {
-		margin: 0 auto var(--margin) auto;
-	}
-
-	> .lists {
-		> .list {
-			display: block;
-			padding: 16px;
-			border: solid 1px var(--divider);
-			border-radius: 6px;
-
-			&:hover {
-				border: solid 1px var(--accent);
-				text-decoration: none;
-			}
-
-			> .name {
-				margin-bottom: 4px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue
deleted file mode 100644
index 049d370b4e..0000000000
--- a/src/client/pages/my-lists/list.vue
+++ /dev/null
@@ -1,170 +0,0 @@
-<template>
-<div class="mk-list-page">
-	<transition name="zoom" mode="out-in">
-		<div v-if="list" class="_section">
-			<div class="_content">
-				<MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
-				<MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton>
-				<MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton>
-			</div>
-		</div>
-	</transition>
-
-	<transition name="zoom" mode="out-in">
-		<div v-if="list" class="_section members _gap">
-			<div class="_title">{{ $ts.members }}</div>
-			<div class="_content">
-				<div class="users">
-					<div class="user _panel" v-for="user in users" :key="user.id">
-						<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
-						<div class="body">
-							<MkUserName :user="user" class="name"/>
-							<MkAcct :user="user" class="acct"/>
-						</div>
-						<div class="action">
-							<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.list ? {
-				title: this.list.name,
-				icon: 'fas fa-list-ul',
-			} : null),
-			list: null,
-			users: [],
-		};
-	},
-
-	watch: {
-		$route: 'fetch'
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			Progress.start();
-			os.api('users/lists/show', {
-				listId: this.$route.params.list
-			}).then(list => {
-				this.list = list;
-				os.api('users/show', {
-					userIds: this.list.userIds
-				}).then(users => {
-					this.users = users;
-					Progress.done();
-				});
-			});
-		},
-
-		addUser() {
-			os.selectUser().then(user => {
-				os.apiWithDialog('users/lists/push', {
-					listId: this.list.id,
-					userId: user.id
-				}).then(() => {
-					this.users.push(user);
-				});
-			});
-		},
-
-		removeUser(user) {
-			os.api('users/lists/pull', {
-				listId: this.list.id,
-				userId: user.id
-			}).then(() => {
-				this.users = this.users.filter(x => x.id !== user.id);
-			});
-		},
-
-		async renameList() {
-			const { canceled, result: name } = await os.dialog({
-				title: this.$ts.enterListName,
-				input: {
-					default: this.list.name
-				}
-			});
-			if (canceled) return;
-
-			await os.api('users/lists/update', {
-				listId: this.list.id,
-				name: name
-			});
-
-			this.list.name = name;
-		},
-
-		async deleteList() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: this.list.name }),
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			await os.api('users/lists/delete', {
-				listId: this.list.id
-			});
-			os.success();
-			this.$router.push('/my/lists');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-list-page {
-	> .members {
-		> ._content {
-			> .users {
-				> .user {
-					display: flex;
-					align-items: center;
-					padding: 16px;
-
-					> .avatar {
-						width: 50px;
-						height: 50px;
-					}
-
-					> .body {
-						flex: 1;
-						padding: 8px;
-
-						> .name {
-							display: block;
-							font-weight: bold;
-						}
-
-						> .acct {
-							opacity: 0.5;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/not-found.vue b/src/client/pages/not-found.vue
deleted file mode 100644
index 5e7fe17f75..0000000000
--- a/src/client/pages/not-found.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<template>
-<div class="ipledcug">
-	<div class="_fullinfo">
-		<img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/>
-		<div>{{ $ts.notFoundDescription }}</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.notFound,
-				icon: 'fas fa-exclamation-triangle'
-			},
-		}
-	},
-});
-</script>
diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue
deleted file mode 100644
index 8e95430d67..0000000000
--- a/src/client/pages/note.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<template>
-<MkSpacer :content-max="800">
-	<div class="fcuexfpr">
-		<transition name="fade" mode="out-in">
-			<div v-if="note" class="note">
-				<div class="_gap" v-if="showNext">
-					<XNotes class="_content" :pagination="next" :no-gap="true"/>
-				</div>
-
-				<div class="main _gap">
-					<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton>
-					<div class="note _gap">
-						<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_isolated"/>
-						<XNoteDetailed v-model:note="note" :key="note.id" class="_isolated note"/>
-					</div>
-					<div class="_content clips _gap" v-if="clips && clips.length > 0">
-						<div class="title">{{ $ts.clip }}</div>
-						<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
-							<b>{{ item.name }}</b>
-							<div v-if="item.description" class="description">{{ item.description }}</div>
-							<div class="user">
-								<MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/>
-							</div>
-						</MkA>
-					</div>
-					<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton>
-				</div>
-
-				<div class="_gap" v-if="showPrev">
-					<XNotes class="_content" :pagination="prev" :no-gap="true"/>
-				</div>
-			</div>
-			<MkError v-else-if="error" @retry="fetch()"/>
-			<MkLoading v-else/>
-		</transition>
-	</div>
-</MkSpacer>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import XNote from '@client/components/note.vue';
-import XNoteDetailed from '@client/components/note-detailed.vue';
-import XNotes from '@client/components/notes.vue';
-import MkRemoteCaution from '@client/components/remote-caution.vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNote,
-		XNoteDetailed,
-		XNotes,
-		MkRemoteCaution,
-		MkButton,
-	},
-	props: {
-		noteId: {
-			type: String,
-			required: true
-		}
-	},
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.note ? {
-				title: this.$ts.note,
-				subtitle: new Date(this.note.createdAt).toLocaleString(),
-				avatar: this.note.user,
-				path: `/notes/${this.note.id}`,
-				share: {
-					title: this.$t('noteOf', { user: this.note.user.name }),
-					text: this.note.text,
-				},
-				bg: 'var(--bg)',
-			} : null),
-			note: null,
-			clips: null,
-			hasPrev: false,
-			hasNext: false,
-			showPrev: false,
-			showNext: false,
-			error: null,
-			prev: {
-				endpoint: 'users/notes',
-				limit: 10,
-				params: init => ({
-					userId: this.note.userId,
-					untilId: this.note.id,
-				})
-			},
-			next: {
-				reversed: true,
-				endpoint: 'users/notes',
-				limit: 10,
-				params: init => ({
-					userId: this.note.userId,
-					sinceId: this.note.id,
-				})
-			},
-		};
-	},
-	watch: {
-		noteId: 'fetch'
-	},
-	created() {
-		this.fetch();
-	},
-	methods: {
-		fetch() {
-			this.note = null;
-			os.api('notes/show', {
-				noteId: this.noteId
-			}).then(note => {
-				this.note = note;
-				Promise.all([
-					os.api('notes/clips', {
-						noteId: note.id,
-					}),
-					os.api('users/notes', {
-						userId: note.userId,
-						untilId: note.id,
-						limit: 1,
-					}),
-					os.api('users/notes', {
-						userId: note.userId,
-						sinceId: note.id,
-						limit: 1,
-					}),
-				]).then(([clips, prev, next]) => {
-					this.clips = clips;
-					this.hasPrev = prev.length !== 0;
-					this.hasNext = next.length !== 0;
-				});
-			}).catch(e => {
-				this.error = e;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.fcuexfpr {
-	background: var(--bg);
-
-	> .note {
-		> .main {
-			> .load {
-				min-width: 0;
-				margin: 0 auto;
-				border-radius: 999px;
-
-				&.next {
-					margin-bottom: var(--margin);
-				}
-
-				&.prev {
-					margin-top: var(--margin);
-				}
-			}
-
-			> .note {
-				> .note {
-					border-radius: var(--radius);
-					background: var(--panel);
-				}
-			}
-
-			> .clips {
-				> .title {
-					font-weight: bold;
-					padding: 12px;
-				}
-
-				> .item {
-					display: block;
-					padding: 16px;
-
-					> .description {
-						padding: 8px 0;
-					}
-
-					> .user {
-						$height: 32px;
-						padding-top: 16px;
-						border-top: solid 0.5px var(--divider);
-						line-height: $height;
-
-						> .avatar {
-							width: $height;
-							height: $height;
-						}
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue
deleted file mode 100644
index 8d6adec48d..0000000000
--- a/src/client/pages/notifications.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<template>
-<MkSpacer :content-max="800">
-	<div class="clupoqwt">
-		<XNotifications class="notifications" @before="before" @after="after" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
-	</div>
-</MkSpacer>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XNotifications from '@client/components/notifications.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { notificationTypes } from '@/types';
-
-export default defineComponent({
-	components: {
-		XNotifications
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.notifications,
-				icon: 'fas fa-bell',
-				bg: 'var(--bg)',
-				actions: [{
-					text: this.$ts.filter,
-					icon: 'fas fa-filter',
-					highlighted: this.includeTypes != null,
-					handler: this.setFilter,
-				}, {
-					text: this.$ts.markAllAsRead,
-					icon: 'fas fa-check',
-					handler: () => {
-						os.apiWithDialog('notifications/mark-all-as-read');
-					},
-				}],
-				tabs: [{
-					active: this.tab === 'all',
-					title: this.$ts.all,
-					onClick: () => { this.tab = 'all'; },
-				}, {
-					active: this.tab === 'unread',
-					title: this.$ts.unread,
-					onClick: () => { this.tab = 'unread'; },
-				},]
-			})),
-			tab: 'all',
-			includeTypes: null,
-		};
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		},
-
-		setFilter(ev) {
-			const typeItems = notificationTypes.map(t => ({
-				text: this.$t(`_notification._types.${t}`),
-				active: this.includeTypes && this.includeTypes.includes(t),
-				action: () => {
-					this.includeTypes = [t];
-				}
-			}));
-			const items = this.includeTypes != null ? [{
-				icon: 'fas fa-times',
-				text: this.$ts.clear,
-				action: () => {
-					this.includeTypes = null;
-				}
-			}, null, ...typeItems] : typeItems;
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.clupoqwt {
-}
-</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.button.vue b/src/client/pages/page-editor/els/page-editor.el.button.vue
deleted file mode 100644
index 85e9d7e711..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.button.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.button }}</template>
-
-	<section class="xfhsjczc">
-		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._button.text }}</template></MkInput>
-		<MkSwitch v-model="value.primary"><span>{{ $ts._pages.blocks._button.colored }}</span></MkSwitch>
-		<MkSelect v-model="value.action">
-			<template #label>{{ $ts._pages.blocks._button.action }}</template>
-			<option value="dialog">{{ $ts._pages.blocks._button._action.dialog }}</option>
-			<option value="resetRandom">{{ $ts._pages.blocks._button._action.resetRandom }}</option>
-			<option value="pushEvent">{{ $ts._pages.blocks._button._action.pushEvent }}</option>
-			<option value="callAiScript">{{ $ts._pages.blocks._button._action.callAiScript }}</option>
-		</MkSelect>
-		<template v-if="value.action === 'dialog'">
-			<MkInput v-model="value.content"><template #label>{{ $ts._pages.blocks._button._action._dialog.content }}</template></MkInput>
-		</template>
-		<template v-else-if="value.action === 'pushEvent'">
-			<MkInput v-model="value.event"><template #label>{{ $ts._pages.blocks._button._action._pushEvent.event }}</template></MkInput>
-			<MkInput v-model="value.message"><template #label>{{ $ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput>
-			<MkSelect v-model="value.var">
-				<template #label>{{ $ts._pages.blocks._button._action._pushEvent.variable }}</template>
-				<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
-				<option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option>
-				<optgroup :label="$ts._pages.script.pageVariables">
-					<option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option>
-				</optgroup>
-				<optgroup :label="$ts._pages.script.enviromentVariables">
-					<option v-for="v in hpml.getEnvVarsByType()" :value="v">{{ v }}</option>
-				</optgroup>
-			</MkSelect>
-		</template>
-		<template v-else-if="value.action === 'callAiScript'">
-			<MkInput v-model="value.fn"><template #label>{{ $ts._pages.blocks._button._action._callAiScript.functionName }}</template></MkInput>
-		</template>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkSelect, MkInput, MkSwitch
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-		hpml: {
-			required: true,
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.text == null) this.value.text = '';
-		if (this.value.action == null) this.value.action = 'dialog';
-		if (this.value.content == null) this.value.content = null;
-		if (this.value.event == null) this.value.event = null;
-		if (this.value.message == null) this.value.message = null;
-		if (this.value.primary == null) this.value.primary = false;
-		if (this.value.var == null) this.value.var = null;
-		if (this.value.fn == null) this.value.fn = null;
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.xfhsjczc {
-	padding: 0 16px 0 16px;
-}
-</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.canvas.vue b/src/client/pages/page-editor/els/page-editor.el.canvas.vue
deleted file mode 100644
index c40d69a7c1..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.canvas.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-paint-brush"></i> {{ $ts._pages.blocks.canvas }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="value.name">
-			<template #prefix><i class="fas fa-magic"></i></template>
-			<template #label>{{ $ts._pages.blocks._canvas.id }}</template>
-		</MkInput>
-		<MkInput v-model="value.width" type="number">
-			<template #label>{{ $ts._pages.blocks._canvas.width }}</template>
-			<template #suffix>px</template>
-		</MkInput>
-		<MkInput v-model="value.height" type="number">
-			<template #label>{{ $ts._pages.blocks._canvas.height }}</template>
-			<template #suffix>px</template>
-		</MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkInput
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.name == null) this.value.name = '';
-		if (this.value.width == null) this.value.width = 300;
-		if (this.value.height == null) this.value.height = 200;
-	},
-});
-</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.counter.vue b/src/client/pages/page-editor/els/page-editor.el.counter.vue
deleted file mode 100644
index de7994e3ba..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.counter.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.counter }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="value.name">
-			<template #prefix><i class="fas fa-magic"></i></template>
-			<template #label>{{ $ts._pages.blocks._counter.name }}</template>
-		</MkInput>
-		<MkInput v-model="value.text">
-			<template #label>{{ $ts._pages.blocks._counter.text }}</template>
-		</MkInput>
-		<MkInput v-model="value.inc" type="number">
-			<template #label>{{ $ts._pages.blocks._counter.inc }}</template>
-		</MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkInput
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.name == null) this.value.name = '';
-	},
-});
-</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue
deleted file mode 100644
index 52f4dac22e..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.if.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-question"></i> {{ $ts._pages.blocks.if }}</template>
-	<template #func>
-		<button @click="add()" class="_button">
-			<i class="fas fa-plus"></i>
-		</button>
-	</template>
-
-	<section class="romcojzs">
-		<MkSelect v-model="value.var">
-			<template #label>{{ $ts._pages.blocks._if.variable }}</template>
-			<option v-for="v in hpml.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option>
-			<optgroup :label="$ts._pages.script.pageVariables">
-				<option v-for="v in hpml.getPageVarsByType('boolean')" :value="v">{{ v }}</option>
-			</optgroup>
-			<optgroup :label="$ts._pages.script.enviromentVariables">
-				<option v-for="v in hpml.getEnvVarsByType('boolean')" :value="v">{{ v }}</option>
-			</optgroup>
-		</MkSelect>
-
-		<XBlocks class="children" v-model="value.children" :hpml="hpml"/>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-import XContainer from '../page-editor.container.vue';
-import MkSelect from '@client/components/form/select.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkSelect,
-		XBlocks: defineAsyncComponent(() => import('../page-editor.blocks.vue')),
-	},
-
-	inject: ['getPageBlockList'],
-
-	props: {
-		value: {
-			required: true
-		},
-		hpml: {
-			required: true,
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.children == null) this.value.children = [];
-		if (this.value.var === undefined) this.value.var = null;
-	},
-
-	methods: {
-		async add() {
-			const { canceled, result: type } = await os.dialog({
-				type: null,
-				title: this.$ts._pages.chooseBlock,
-				select: {
-					groupedItems: this.getPageBlockList()
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			const id = uuid();
-			this.value.children.push({ id, type });
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.romcojzs {
-	padding: 0 16px 16px 16px;
-}
-</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.image.vue b/src/client/pages/page-editor/els/page-editor.el.image.vue
deleted file mode 100644
index d96879f50d..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.image.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-image"></i> {{ $ts._pages.blocks.image }}</template>
-	<template #func>
-		<button @click="choose()">
-			<i class="fas fa-folder-open"></i>
-		</button>
-	</template>
-
-	<section class="oyyftmcf">
-		<MkDriveFileThumbnail class="preview" v-if="file" :file="file" fit="contain" @click="choose()"/>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkDriveFileThumbnail
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			file: null,
-		};
-	},
-
-	created() {
-		if (this.value.fileId === undefined) this.value.fileId = null;
-	},
-
-	mounted() {
-		if (this.value.fileId == null) {
-			this.choose();
-		} else {
-			os.api('drive/files/show', {
-				fileId: this.value.fileId
-			}).then(file => {
-				this.file = file;
-			});
-		}
-	},
-
-	methods: {
-		async choose() {
-			os.selectDriveFile(false).then(file => {
-				this.file = file;
-				this.value.fileId = file.id;
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.oyyftmcf {
-	> .preview {
-		height: 150px;
-	}
-}
-</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.note.vue b/src/client/pages/page-editor/els/page-editor.el.note.vue
deleted file mode 100644
index 9feec395b7..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.note.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-sticky-note"></i> {{ $ts._pages.blocks.note }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="id">
-			<template #label>{{ $ts._pages.blocks._note.id }}</template>
-			<template #caption>{{ $ts._pages.blocks._note.idDescription }}</template>
-		</MkInput>
-		<MkSwitch v-model="value.detailed"><span>{{ $ts._pages.blocks._note.detailed }}</span></MkSwitch>
-
-		<XNote v-if="note && !value.detailed" v-model:note="note" :key="note.id + ':normal'" style="margin-bottom: 16px;"/>
-		<XNoteDetailed v-if="note && value.detailed" v-model:note="note" :key="note.id + ':detail'" style="margin-bottom: 16px;"/>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import XNote from '@client/components/note.vue';
-import XNoteDetailed from '@client/components/note-detailed.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkInput, MkSwitch, XNote, XNoteDetailed,
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			id: this.value.note,
-			note: null,
-		};
-	},
-
-	watch: {
-		id: {
-			async handler() {
-				if (this.id && (this.id.startsWith('http://') || this.id.startsWith('https://'))) {
-					this.value.note = this.id.endsWith('/') ? this.id.substr(0, this.id.length - 1).split('/').pop() : this.id.split('/').pop();
-				} else {
-					this.value.note = this.id;
-				}
-
-				this.note = await os.api('notes/show', { noteId: this.value.note });
-			},
-			immediate: true
-		},
-	},
-
-	created() {
-		if (this.value.note == null) this.value.note = null;
-		if (this.value.detailed == null) this.value.detailed = false;
-	},
-});
-</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
deleted file mode 100644
index 57b1397824..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.number-input.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.numberInput }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="value.name">
-			<template #prefix><i class="fas fa-magic"></i></template>
-			<template #label>{{ $ts._pages.blocks._numberInput.name }}</template>
-		</MkInput>
-		<MkInput v-model="value.text">
-			<template #label>{{ $ts._pages.blocks._numberInput.text }}</template>
-		</MkInput>
-		<MkInput v-model="value.default" type="number">
-			<template #label>{{ $ts._pages.blocks._numberInput.default }}</template>
-		</MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkInput
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.name == null) this.value.name = '';
-	},
-});
-</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.post.vue b/src/client/pages/page-editor/els/page-editor.el.post.vue
deleted file mode 100644
index e21ccfd345..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.post.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-paper-plane"></i> {{ $ts._pages.blocks.post }}</template>
-
-	<section style="padding: 16px;">
-		<MkTextarea v-model="value.text"><template #label>{{ $ts._pages.blocks._post.text }}</template></MkTextarea>
-		<MkSwitch v-model="value.attachCanvasImage"><span>{{ $ts._pages.blocks._post.attachCanvasImage }}</span></MkSwitch>
-		<MkInput v-if="value.attachCanvasImage" v-model="value.canvasId"><template #label>{{ $ts._pages.blocks._post.canvasId }}</template></MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkTextarea, MkInput, MkSwitch
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.text == null) this.value.text = '';
-		if (this.value.attachCanvasImage == null) this.value.attachCanvasImage = false;
-		if (this.value.canvasId == null) this.value.canvasId = '';
-	},
-});
-</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
deleted file mode 100644
index 62fb231f79..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.radioButton }}</template>
-
-	<section style="padding: 0 16px 16px 16px;">
-		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._radioButton.name }}</template></MkInput>
-		<MkInput v-model="value.title"><template #label>{{ $ts._pages.blocks._radioButton.title }}</template></MkInput>
-		<MkTextarea v-model="values"><template #label>{{ $ts._pages.blocks._radioButton.values }}</template></MkTextarea>
-		<MkInput v-model="value.default"><template #label>{{ $ts._pages.blocks._radioButton.default }}</template></MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkTextarea, MkInput
-	},
-	props: {
-		value: {
-			required: true
-		},
-	},
-	data() {
-		return {
-			values: '',
-		};
-	},
-	watch: {
-		values: {
-			handler() {
-				this.value.values = this.values.split('\n');
-			},
-			deep: true
-		}
-	},
-	created() {
-		if (this.value.name == null) this.value.name = '';
-		if (this.value.title == null) this.value.title = '';
-		if (this.value.values == null) this.value.values = [];
-		this.values = this.value.values.join('\n');
-	},
-});
-</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue
deleted file mode 100644
index 75bdf120c0..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.section.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-sticky-note"></i> {{ value.title }}</template>
-	<template #func>
-		<button @click="rename()" class="_button">
-			<i class="fas fa-pencil-alt"></i>
-		</button>
-		<button @click="add()" class="_button">
-			<i class="fas fa-plus"></i>
-		</button>
-	</template>
-
-	<section class="ilrvjyvi">
-		<XBlocks class="children" v-model="value.children" :hpml="hpml"/>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-import XContainer from '../page-editor.container.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer,
-		XBlocks: defineAsyncComponent(() => import('../page-editor.blocks.vue')),
-	},
-
-	inject: ['getPageBlockList'],
-
-	props: {
-		value: {
-			required: true
-		},
-		hpml: {
-			required: true,
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.title == null) this.value.title = null;
-		if (this.value.children == null) this.value.children = [];
-	},
-
-	mounted() {
-		if (this.value.title == null) {
-			this.rename();
-		}
-	},
-
-	methods: {
-		async rename() {
-			const { canceled, result: title } = await os.dialog({
-				title: 'Enter title',
-				input: {
-					type: 'text',
-					default: this.value.title
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			this.value.title = title;
-		},
-
-		async add() {
-			const { canceled, result: type } = await os.dialog({
-				type: null,
-				title: this.$ts._pages.chooseBlock,
-				select: {
-					groupedItems: this.getPageBlockList()
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			const id = uuid();
-			this.value.children.push({ id, type });
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ilrvjyvi {
-	> .children {
-		padding: 16px;
-	}
-}
-</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.switch.vue b/src/client/pages/page-editor/els/page-editor.el.switch.vue
deleted file mode 100644
index cf15f58c82..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.switch.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.switch }}</template>
-
-	<section class="kjuadyyj">
-		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._switch.name }}</template></MkInput>
-		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._switch.text }}</template></MkInput>
-		<MkSwitch v-model="value.default"><span>{{ $ts._pages.blocks._switch.default }}</span></MkSwitch>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkSwitch, MkInput
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.name == null) this.value.name = '';
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.kjuadyyj {
-	padding: 0 16px 16px 16px;
-}
-</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
deleted file mode 100644
index 210199befd..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.text-input.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.textInput }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._textInput.name }}</template></MkInput>
-		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._textInput.text }}</template></MkInput>
-		<MkInput v-model="value.default" type="text"><template #label>{{ $ts._pages.blocks._textInput.default }}</template></MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkInput
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.name == null) this.value.name = '';
-	},
-});
-</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.text.vue b/src/client/pages/page-editor/els/page-editor.el.text.vue
deleted file mode 100644
index 668dd5f52d..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.text.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-align-left"></i> {{ $ts._pages.blocks.text }}</template>
-
-	<section class="vckmsadr">
-		<textarea v-model="value.text"></textarea>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.text == null) this.value.text = '';
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.vckmsadr {
-	> textarea {
-		display: block;
-		-webkit-appearance: none;
-		-moz-appearance: none;
-		appearance: none;
-		width: 100%;
-		min-width: 100%;
-		min-height: 150px;
-		border: none;
-		box-shadow: none;
-		padding: 16px;
-		background: transparent;
-		color: var(--fg);
-		font-size: 14px;
-		box-sizing: border-box;
-	}
-}
-</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
deleted file mode 100644
index 14f36db2a1..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-bolt"></i> {{ $ts._pages.blocks.textareaInput }}</template>
-
-	<section style="padding: 0 16px 16px 16px;">
-		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._textareaInput.name }}</template></MkInput>
-		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._textareaInput.text }}</template></MkInput>
-		<MkTextarea v-model="value.default"><template #label>{{ $ts._pages.blocks._textareaInput.default }}</template></MkTextarea>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkInput from '@client/components/form/input.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer, MkTextarea, MkInput
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.name == null) this.value.name = '';
-	},
-});
-</script>
diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/pages/page-editor/els/page-editor.el.textarea.vue
deleted file mode 100644
index a29d5bd3f2..0000000000
--- a/src/client/pages/page-editor/els/page-editor.el.textarea.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template>
-<XContainer @remove="() => $emit('remove')" :draggable="true">
-	<template #header><i class="fas fa-align-left"></i> {{ $ts._pages.blocks.textarea }}</template>
-
-	<section class="ihymsbbe">
-		<textarea v-model="value.text"></textarea>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XContainer
-	},
-
-	props: {
-		value: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	created() {
-		if (this.value.text == null) this.value.text = '';
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ihymsbbe {
-	> textarea {
-		display: block;
-		-webkit-appearance: none;
-		-moz-appearance: none;
-		appearance: none;
-		width: 100%;
-		min-width: 100%;
-		min-height: 150px;
-		border: none;
-		box-shadow: none;
-		padding: 16px;
-		background: transparent;
-		color: var(--fg);
-		font-size: 14px;
-		box-sizing: border-box;
-	}
-}
-</style>
diff --git a/src/client/pages/page-editor/page-editor.blocks.vue b/src/client/pages/page-editor/page-editor.blocks.vue
deleted file mode 100644
index c27162a26e..0000000000
--- a/src/client/pages/page-editor/page-editor.blocks.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-<template>
-<XDraggable tag="div" v-model="blocks" item-key="id" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5">
-	<template #item="{element}">
-		<component :is="'x-' + element.type" :value="element" @update:value="updateItem" @remove="() => removeItem(element)" :hpml="hpml"/>
-	</template>
-</XDraggable>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import XSection from './els/page-editor.el.section.vue';
-import XText from './els/page-editor.el.text.vue';
-import XTextarea from './els/page-editor.el.textarea.vue';
-import XImage from './els/page-editor.el.image.vue';
-import XButton from './els/page-editor.el.button.vue';
-import XTextInput from './els/page-editor.el.text-input.vue';
-import XTextareaInput from './els/page-editor.el.textarea-input.vue';
-import XNumberInput from './els/page-editor.el.number-input.vue';
-import XSwitch from './els/page-editor.el.switch.vue';
-import XIf from './els/page-editor.el.if.vue';
-import XPost from './els/page-editor.el.post.vue';
-import XCounter from './els/page-editor.el.counter.vue';
-import XRadioButton from './els/page-editor.el.radio-button.vue';
-import XCanvas from './els/page-editor.el.canvas.vue';
-import XNote from './els/page-editor.el.note.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
-		XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas, XNote
-	},
-
-	props: {
-		modelValue: {
-			type: Array,
-			required: true
-		},
-		hpml: {
-			required: true,
-		},
-	},
-
-	emits: ['update:modelValue'],
-
-	computed: {
-		blocks: {
-			get() {
-				return this.modelValue;
-			},
-			set(value) {
-				this.$emit('update:modelValue', value);
-			}
-		}
-	},
-
-	methods: {
-		updateItem(v) {
-			const i = this.blocks.findIndex(x => x.id === v.id);
-			const newValue = [
-				...this.blocks.slice(0, i),
-				v,
-				...this.blocks.slice(i + 1)
-			];
-			this.$emit('update:modelValue', newValue);
-		},
-
-		removeItem(el) {
-			const i = this.blocks.findIndex(x => x.id === el.id);
-			const newValue = [
-				...this.blocks.slice(0, i),
-				...this.blocks.slice(i + 1)
-			];
-			this.$emit('update:modelValue', newValue);
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/page-editor/page-editor.container.vue b/src/client/pages/page-editor/page-editor.container.vue
deleted file mode 100644
index afd261fac7..0000000000
--- a/src/client/pages/page-editor/page-editor.container.vue
+++ /dev/null
@@ -1,159 +0,0 @@
-<template>
-<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
-	<header>
-		<div class="title"><slot name="header"></slot></div>
-		<div class="buttons">
-			<slot name="func"></slot>
-			<button v-if="removable" @click="remove()" class="_button">
-				<i class="fas fa-trash-alt"></i>
-			</button>
-			<button v-if="draggable" class="drag-handle _button">
-				<i class="fas fa-bars"></i>
-			</button>
-			<button @click="toggleContent(!showBody)" class="_button">
-				<template v-if="showBody"><i class="fas fa-angle-up"></i></template>
-				<template v-else><i class="fas fa-angle-down"></i></template>
-			</button>
-		</div>
-	</header>
-	<p v-show="showBody" class="error" v-if="error != null">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
-	<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
-	<div v-show="showBody" class="body">
-		<slot></slot>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		expanded: {
-			type: Boolean,
-			default: true
-		},
-		removable: {
-			type: Boolean,
-			default: true
-		},
-		draggable: {
-			type: Boolean,
-			default: false
-		},
-		error: {
-			required: false,
-			default: null
-		},
-		warn: {
-			required: false,
-			default: null
-		}
-	},
-	emits: ['toggle', 'remove'],
-	data() {
-		return {
-			showBody: this.expanded,
-		};
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			this.showBody = show;
-			this.$emit('toggle', show);
-		},
-		remove() {
-			this.$emit('remove');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.cpjygsrt {
-	position: relative;
-	overflow: hidden;
-	background: var(--panel);
-	border: solid 2px var(--X12);
-	border-radius: 6px;
-
-	&:hover {
-		border: solid 2px var(--X13);
-	}
-
-	&.warn {
-		border: solid 2px #dec44c;
-	}
-
-	&.error {
-		border: solid 2px #f00;
-	}
-
-	& + .cpjygsrt {
-		margin-top: 16px;
-	}
-
-	> header {
-		> .title {
-			z-index: 1;
-			margin: 0;
-			padding: 0 16px;
-			line-height: 42px;
-			font-size: 0.9em;
-			font-weight: bold;
-			box-shadow: 0 1px rgba(#000, 0.07);
-
-			> i {
-				margin-right: 6px;
-			}
-
-			&:empty {
-				display: none;
-			}
-		}
-
-		> .buttons {
-			position: absolute;
-			z-index: 2;
-			top: 0;
-			right: 0;
-
-			> button {
-				padding: 0;
-				width: 42px;
-				font-size: 0.9em;
-				line-height: 42px;
-			}
-
-			.drag-handle {
-				cursor: move;
-			}
-		}
-	}
-
-	> .warn {
-		color: #b19e49;
-		margin: 0;
-		padding: 16px 16px 0 16px;
-		font-size: 14px;
-	}
-
-	> .error {
-		color: #f00;
-		margin: 0;
-		padding: 16px 16px 0 16px;
-		font-size: 14px;
-	}
-
-	> .body {
-		::v-deep(.juejbjww), ::v-deep(.eiipwacr) {
-			&:not(.inline):first-child {
-				margin-top: 28px;
-			}
-
-			&:not(.inline):last-child {
-				margin-bottom: 20px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue
deleted file mode 100644
index 3313fc1ba9..0000000000
--- a/src/client/pages/page-editor/page-editor.script-block.vue
+++ /dev/null
@@ -1,281 +0,0 @@
-<template>
-<XContainer :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable">
-	<template #header><i v-if="icon" :class="icon"></i> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template>
-	<template #func>
-		<button @click="changeType()" class="_button">
-			<i class="fas fa-pencil-alt"></i>
-		</button>
-	</template>
-
-	<section v-if="modelValue.type === null" class="pbglfege" @click="changeType()">
-		{{ $ts._pages.script.emptySlot }}
-	</section>
-	<section v-else-if="modelValue.type === 'text'" class="tbwccoaw">
-		<input v-model="modelValue.value"/>
-	</section>
-	<section v-else-if="modelValue.type === 'multiLineText'" class="tbwccoaw">
-		<textarea v-model="modelValue.value"></textarea>
-	</section>
-	<section v-else-if="modelValue.type === 'textList'" class="tbwccoaw">
-		<textarea v-model="modelValue.value" :placeholder="$ts._pages.script.blocks._textList.info"></textarea>
-	</section>
-	<section v-else-if="modelValue.type === 'number'" class="tbwccoaw">
-		<input v-model="modelValue.value" type="number"/>
-	</section>
-	<section v-else-if="modelValue.type === 'ref'" class="hpdwcrvs">
-		<select v-model="modelValue.value">
-			<option v-for="v in hpml.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
-			<optgroup :label="$ts._pages.script.argVariables">
-				<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
-			</optgroup>
-			<optgroup :label="$ts._pages.script.pageVariables">
-				<option v-for="v in hpml.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
-			</optgroup>
-			<optgroup :label="$ts._pages.script.enviromentVariables">
-				<option v-for="v in hpml.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
-			</optgroup>
-		</select>
-	</section>
-	<section v-else-if="modelValue.type === 'aiScriptVar'" class="tbwccoaw">
-		<input v-model="modelValue.value"/>
-	</section>
-	<section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
-		<MkTextarea v-model="slots">
-			<template #label>{{ $ts._pages.script.blocks._fn.slots }}</template>
-			<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
-		</MkTextarea>
-		<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/>
-	</section>
-	<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
-		<XV v-for="(x, i) in modelValue.args" v-model="value.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name" :key="i"/>
-	</section>
-	<section v-else class="" style="padding:16px;">
-		<XV v-for="(x, i) in modelValue.args" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots" :key="i"/>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-import XContainer from './page-editor.container.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import { blockDefs } from '@client/scripts/hpml/index';
-import * as os from '@client/os';
-import { isLiteralValue } from '@client/scripts/hpml/expr';
-import { funcDefs } from '@client/scripts/hpml/lib';
-
-export default defineComponent({
-	components: {
-		XContainer, MkTextarea,
-		XV: defineAsyncComponent(() => import('./page-editor.script-block.vue')),
-	},
-
-	inject: ['getScriptBlockList'],
-
-	props: {
-		getExpectedType: {
-			required: false,
-			default: null
-		},
-		modelValue: {
-			required: true
-		},
-		title: {
-			required: false
-		},
-		removable: {
-			required: false,
-			default: false
-		},
-		hpml: {
-			required: true,
-		},
-		name: {
-			required: true,
-		},
-		fnSlots: {
-			required: false,
-		},
-		draggable: {
-			required: false,
-			default: false
-		}
-	},
-
-	data() {
-		return {
-			error: null,
-			warn: null,
-			slots: '',
-		};
-	},
-
-	computed: {
-		icon(): any {
-			if (this.modelValue.type === null) return null;
-			if (this.modelValue.type.startsWith('fn:')) return 'fas fa-plug';
-			return blockDefs.find(x => x.type === this.modelValue.type).icon;
-		},
-		typeText(): any {
-			if (this.modelValue.type === null) return null;
-			if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1];
-			return this.$t(`_pages.script.blocks.${this.modelValue.type}`);
-		},
-	},
-
-	watch: {
-		slots: {
-			handler() {
-				this.modelValue.value.slots = this.slots.split('\n').map(x => ({
-					name: x,
-					type: null
-				}));
-			},
-			deep: true
-		}
-	},
-
-	created() {
-		if (this.modelValue.value == null) this.modelValue.value = null;
-
-		if (this.modelValue.value && this.modelValue.value.slots) this.slots = this.modelValue.value.slots.map(x => x.name).join('\n');
-
-		this.$watch(() => this.modelValue.type, (t) => {
-			this.warn = null;
-
-			if (this.modelValue.type === 'fn') {
-				const id = uuid();
-				this.modelValue.value = {
-					slots: [],
-					expression: { id, type: null }
-				};
-				return;
-			}
-
-			if (this.modelValue.type && this.modelValue.type.startsWith('fn:')) {
-				const fnName = this.modelValue.type.split(':')[1];
-				const fn = this.hpml.getVarByName(fnName);
-
-				const empties = [];
-				for (let i = 0; i < fn.value.slots.length; i++) {
-					const id = uuid();
-					empties.push({ id, type: null });
-				}
-				this.modelValue.args = empties;
-				return;
-			}
-
-			if (isLiteralValue(this.modelValue)) return;
-
-			const empties = [];
-			for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
-				const id = uuid();
-				empties.push({ id, type: null });
-			}
-			this.modelValue.args = empties;
-
-			for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
-				const inType = funcDefs[this.modelValue.type].in[i];
-				if (typeof inType !== 'number') {
-					if (inType === 'number') this.modelValue.args[i].type = 'number';
-					if (inType === 'string') this.modelValue.args[i].type = 'text';
-				}
-			}
-		});
-
-		this.$watch(() => this.modelValue.args, (args) => {
-			if (args == null) {
-				this.warn = null;
-				return;
-			}
-			const emptySlotIndex = args.findIndex(x => x.type === null);
-			if (emptySlotIndex !== -1 && emptySlotIndex < args.length) {
-				this.warn = {
-					slot: emptySlotIndex
-				};
-			} else {
-				this.warn = null;
-			}
-		}, {
-			deep: true
-		});
-
-		this.$watch(() => this.hpml.variables, () => {
-			if (this.type != null && this.modelValue) {
-				this.error = this.hpml.typeCheck(this.modelValue);
-			}
-		}, {
-			deep: true
-		});
-	},
-
-	methods: {
-		async changeType() {
-			const { canceled, result: type } = await os.dialog({
-				type: null,
-				title: this.$ts._pages.selectType,
-				select: {
-					groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null)
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			this.modelValue.type = type;
-		},
-
-		_getExpectedType(slot: number) {
-			return this.hpml.getExpectedType(this.modelValue, slot);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.turmquns {
-	opacity: 0.7;
-}
-
-.pbglfege {
-	opacity: 0.5;
-	padding: 16px;
-	text-align: center;
-	cursor: pointer;
-	color: var(--fg);
-}
-
-.tbwccoaw {
-	> input,
-	> textarea {
-		display: block;
-		-webkit-appearance: none;
-		-moz-appearance: none;
-		appearance: none;
-		width: 100%;
-		max-width: 100%;
-		min-width: 100%;
-		border: none;
-		box-shadow: none;
-		padding: 16px;
-		font-size: 16px;
-		background: transparent;
-		color: var(--fg);
-		box-sizing: border-box;
-	}
-
-	> textarea {
-		min-height: 100px;
-	}
-}
-
-.hpdwcrvs {
-	padding: 16px;
-
-	> select {
-		display: block;
-		padding: 4px;
-		font-size: 16px;
-		width: 100%;
-	}
-}
-</style>
diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue
deleted file mode 100644
index aefcc14564..0000000000
--- a/src/client/pages/page-editor/page-editor.vue
+++ /dev/null
@@ -1,561 +0,0 @@
-<template>
-<div>
-	<div class="jqqmcavi" style="margin: 16px;">
-		<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton>
-		<MkButton inline @click="save" primary class="button" v-if="!readonly"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-		<MkButton inline @click="duplicate" class="button" v-if="pageId"><i class="fas fa-copy"></i> {{ $ts.duplicate }}</MkButton>
-		<MkButton inline @click="del" class="button" v-if="pageId && !readonly" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
-	</div>
-
-	<div v-if="tab === 'settings'">
-		<div style="padding: 16px;" class="_formRoot">
-			<MkInput v-model="title" class="_formBlock">
-				<template #label>{{ $ts._pages.title }}</template>
-			</MkInput>
-
-			<MkInput v-model="summary" class="_formBlock">
-				<template #label>{{ $ts._pages.summary }}</template>
-			</MkInput>
-
-			<MkInput v-model="name" class="_formBlock">
-				<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
-				<template #label>{{ $ts._pages.url }}</template>
-			</MkInput>
-
-			<MkSwitch v-model="alignCenter" class="_formBlock">{{ $ts._pages.alignCenter }}</MkSwitch>
-
-			<MkSelect v-model="font" class="_formBlock">
-				<template #label>{{ $ts._pages.font }}</template>
-				<option value="serif">{{ $ts._pages.fontSerif }}</option>
-				<option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option>
-			</MkSelect>
-
-			<MkSwitch v-model="hideTitleWhenPinned" class="_formBlock">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch>
-
-			<div class="eyeCatch">
-				<MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="fas fa-plus"></i> {{ $ts._pages.eyeCatchingImageSet }}</MkButton>
-				<div v-else-if="eyeCatchingImage">
-					<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/>
-					<MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><i class="fas fa-trash-alt"></i> {{ $ts._pages.eyeCatchingImageRemove }}</MkButton>
-				</div>
-			</div>
-		</div>
-	</div>
-
-	<div v-else-if="tab === 'contents'">
-		<div style="padding: 16px;">
-			<XBlocks class="content" v-model="content" :hpml="hpml"/>
-
-			<MkButton @click="add()" v-if="!readonly"><i class="fas fa-plus"></i></MkButton>
-		</div>
-	</div>
-
-	<div v-else-if="tab === 'variables'">
-		<div class="qmuvgica">
-			<XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
-				<template #item="{element}">
-					<XVariable
-						:modelValue="element"
-						:removable="true"
-						@remove="() => removeVariable(element)"
-						:hpml="hpml"
-						:name="element.name"
-						:title="element.name"
-						:draggable="true"
-					/>
-				</template>
-			</XDraggable>
-
-			<MkButton @click="addVariable()" class="add" v-if="!readonly"><i class="fas fa-plus"></i></MkButton>
-		</div>
-	</div>
-
-	<div v-else-if="tab === 'script'">
-		<div>
-			<MkTextarea class="_code" v-model="script"/>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, computed } from 'vue';
-import 'prismjs';
-import { highlight, languages } from 'prismjs/components/prism-core';
-import 'prismjs/components/prism-clike';
-import 'prismjs/components/prism-javascript';
-import 'prismjs/themes/prism-okaidia.css';
-import 'vue-prism-editor/dist/prismeditor.min.css';
-import { v4 as uuid } from 'uuid';
-import XVariable from './page-editor.script-block.vue';
-import XBlocks from './page-editor.blocks.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/form/select.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import MkInput from '@client/components/form/input.vue';
-import { blockDefs } from '@client/scripts/hpml/index';
-import { HpmlTypeChecker } from '@client/scripts/hpml/type-checker';
-import { url } from '@client/config';
-import { collectPageVars } from '@client/scripts/collect-page-vars';
-import * as os from '@client/os';
-import { selectFile } from '@client/scripts/select-file';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
-		XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput,
-	},
-
-	props: {
-		initPageId: {
-			type: String,
-			required: false
-		},
-		initPageName: {
-			type: String,
-			required: false
-		},
-		initUser: {
-			type: String,
-			required: false
-		},
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => {
-				let title = this.$ts._pages.newPage;
-				if (this.initPageId) {
-					title = this.$ts._pages.editPage;
-				}
-				else if (this.initPageName && this.initUser) {
-					title = this.$ts._pages.readPage;
-				}
-				return {
-					title: title,
-					icon: 'fas fa-pencil-alt',
-					bg: 'var(--bg)',
-					tabs: [{
-						active: this.tab === 'settings',
-						title: this.$ts._pages.pageSetting,
-						icon: 'fas fa-cog',
-						onClick: () => { this.tab = 'settings'; },
-					}, {
-						active: this.tab === 'contents',
-						title: this.$ts._pages.contents,
-						icon: 'fas fa-sticky-note',
-						onClick: () => { this.tab = 'contents'; },
-					}, {
-						active: this.tab === 'variables',
-						title: this.$ts._pages.variables,
-						icon: 'fas fa-magic',
-						onClick: () => { this.tab = 'variables'; },
-					}, {
-						active: this.tab === 'script',
-						title: this.$ts.script,
-						icon: 'fas fa-code',
-						onClick: () => { this.tab = 'script'; },
-					}],
-				};
-			}),
-			tab: 'settings',
-			author: this.$i,
-			readonly: false,
-			page: null,
-			pageId: null,
-			currentName: null,
-			title: '',
-			summary: null,
-			name: Date.now().toString(),
-			eyeCatchingImage: null,
-			eyeCatchingImageId: null,
-			font: 'sans-serif',
-			content: [],
-			alignCenter: false,
-			hideTitleWhenPinned: false,
-			variables: [],
-			hpml: null,
-			script: '',
-			url,
-		};
-	},
-
-	watch: {
-		async eyeCatchingImageId() {
-			if (this.eyeCatchingImageId == null) {
-				this.eyeCatchingImage = null;
-			} else {
-				this.eyeCatchingImage = await os.api('drive/files/show', {
-					fileId: this.eyeCatchingImageId,
-				});
-			}
-		},
-	},
-
-	async created() {
-		this.hpml = new HpmlTypeChecker();
-
-		this.$watch('variables', () => {
-			this.hpml.variables = this.variables;
-		}, { deep: true });
-
-		this.$watch('content', () => {
-			this.hpml.pageVars = collectPageVars(this.content);
-		}, { deep: true });
-
-		if (this.initPageId) {
-			this.page = await os.api('pages/show', {
-				pageId: this.initPageId,
-			});
-		} else if (this.initPageName && this.initUser) {
-			this.page = await os.api('pages/show', {
-				name: this.initPageName,
-				username: this.initUser,
-			});
-			this.readonly = true;
-		}
-
-		if (this.page) {
-			this.author = this.page.user;
-			this.pageId = this.page.id;
-			this.title = this.page.title;
-			this.name = this.page.name;
-			this.currentName = this.page.name;
-			this.summary = this.page.summary;
-			this.font = this.page.font;
-			this.script = this.page.script;
-			this.hideTitleWhenPinned = this.page.hideTitleWhenPinned;
-			this.alignCenter = this.page.alignCenter;
-			this.content = this.page.content;
-			this.variables = this.page.variables;
-			this.eyeCatchingImageId = this.page.eyeCatchingImageId;
-		} else {
-			const id = uuid();
-			this.content = [{
-				id,
-				type: 'text',
-				text: 'Hello World!'
-			}];
-		}
-	},
-
-	provide() {
-		return {
-			readonly: this.readonly,
-			getScriptBlockList: this.getScriptBlockList,
-			getPageBlockList: this.getPageBlockList
-		}
-	},
-
-	methods: {
-		getSaveOptions() {
-			return {
-				title: this.title.trim(),
-				name: this.name.trim(),
-				summary: this.summary,
-				font: this.font,
-				script: this.script,
-				hideTitleWhenPinned: this.hideTitleWhenPinned,
-				alignCenter: this.alignCenter,
-				content: this.content,
-				variables: this.variables,
-				eyeCatchingImageId: this.eyeCatchingImageId,
-			};
-		},
-
-		save() {
-			const options = this.getSaveOptions();
-
-			const onError = err => {
-				if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') {
-					if (err.info.param == 'name') {
-						os.dialog({
-							type: 'error',
-							title: this.$ts._pages.invalidNameTitle,
-							text: this.$ts._pages.invalidNameText
-						});
-					}
-				} else if (err.code == 'NAME_ALREADY_EXISTS') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts._pages.nameAlreadyExists
-					});
-				}
-			};
-
-			if (this.pageId) {
-				options.pageId = this.pageId;
-				os.api('pages/update', options)
-				.then(page => {
-					this.currentName = this.name.trim();
-					os.dialog({
-						type: 'success',
-						text: this.$ts._pages.updated
-					});
-				}).catch(onError);
-			} else {
-				os.api('pages/create', options)
-				.then(page => {
-					this.pageId = page.id;
-					this.currentName = this.name.trim();
-					os.dialog({
-						type: 'success',
-						text: this.$ts._pages.created
-					});
-					this.$router.push(`/pages/edit/${this.pageId}`);
-				}).catch(onError);
-			}
-		},
-
-		del() {
-			os.dialog({
-				type: 'warning',
-				text: this.$t('removeAreYouSure', { x: this.title.trim() }),
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				os.api('pages/delete', {
-					pageId: this.pageId,
-				}).then(() => {
-					os.dialog({
-						type: 'success',
-						text: this.$ts._pages.deleted
-					});
-					this.$router.push(`/pages`);
-				});
-			});
-		},
-
-		duplicate() {
-			this.title = this.title + ' - copy';
-			this.name = this.name + '-copy';
-			os.api('pages/create', this.getSaveOptions()).then(page => {
-				this.pageId = page.id;
-				this.currentName = this.name.trim();
-				os.dialog({
-					type: 'success',
-					text: this.$ts._pages.created
-				});
-				this.$router.push(`/pages/edit/${this.pageId}`);
-			});
-		},
-
-		async add() {
-			const { canceled, result: type } = await os.dialog({
-				type: null,
-				title: this.$ts._pages.chooseBlock,
-				select: {
-					groupedItems: this.getPageBlockList()
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			const id = uuid();
-			this.content.push({ id, type });
-		},
-
-		async addVariable() {
-			let { canceled, result: name } = await os.dialog({
-				title: this.$ts._pages.enterVariableName,
-				input: {
-					type: 'text',
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			name = name.trim();
-
-			if (this.hpml.isUsedName(name)) {
-				os.dialog({
-					type: 'error',
-					text: this.$ts._pages.variableNameIsAlreadyUsed
-				});
-				return;
-			}
-
-			const id = uuid();
-			this.variables.push({ id, name, type: null });
-		},
-
-		removeVariable(v) {
-			this.variables = this.variables.filter(x => x.name !== v.name);
-		},
-
-		getPageBlockList() {
-			return [{
-				label: this.$ts._pages.contentBlocks,
-				items: [
-					{ value: 'section', text: this.$ts._pages.blocks.section },
-					{ value: 'text', text: this.$ts._pages.blocks.text },
-					{ value: 'image', text: this.$ts._pages.blocks.image },
-					{ value: 'textarea', text: this.$ts._pages.blocks.textarea },
-					{ value: 'note', text: this.$ts._pages.blocks.note },
-					{ value: 'canvas', text: this.$ts._pages.blocks.canvas },
-				]
-			}, {
-				label: this.$ts._pages.inputBlocks,
-				items: [
-					{ value: 'button', text: this.$ts._pages.blocks.button },
-					{ value: 'radioButton', text: this.$ts._pages.blocks.radioButton },
-					{ value: 'textInput', text: this.$ts._pages.blocks.textInput },
-					{ value: 'textareaInput', text: this.$ts._pages.blocks.textareaInput },
-					{ value: 'numberInput', text: this.$ts._pages.blocks.numberInput },
-					{ value: 'switch', text: this.$ts._pages.blocks.switch },
-					{ value: 'counter', text: this.$ts._pages.blocks.counter }
-				]
-			}, {
-				label: this.$ts._pages.specialBlocks,
-				items: [
-					{ value: 'if', text: this.$ts._pages.blocks.if },
-					{ value: 'post', text: this.$ts._pages.blocks.post }
-				]
-			}];
-		},
-
-		getScriptBlockList(type: string = null) {
-			const list = [];
-
-			const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number');
-
-			for (const block of blocks) {
-				const category = list.find(x => x.category === block.category);
-				if (category) {
-					category.items.push({
-						value: block.type,
-						text: this.$t(`_pages.script.blocks.${block.type}`)
-					});
-				} else {
-					list.push({
-						category: block.category,
-						label: this.$t(`_pages.script.categories.${block.category}`),
-						items: [{
-							value: block.type,
-							text: this.$t(`_pages.script.blocks.${block.type}`)
-						}]
-					});
-				}
-			}
-
-			const userFns = this.variables.filter(x => x.type === 'fn');
-			if (userFns.length > 0) {
-				list.unshift({
-					label: this.$t(`_pages.script.categories.fn`),
-					items: userFns.map(v => ({
-						value: 'fn:' + v.name,
-						text: v.name
-					}))
-				});
-			}
-
-			return list;
-		},
-
-		setEyeCatchingImage(e) {
-			selectFile(e.currentTarget || e.target, null, false).then(file => {
-				this.eyeCatchingImageId = file.id;
-			});
-		},
-
-		removeEyeCatchingImage() {
-			this.eyeCatchingImageId = null;
-		},
-
-		highlighter(code) {
-			return highlight(code, languages.js, 'javascript');
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.jqqmcavi {
-	> .button {
-		& + .button {
-			margin-left: 8px;
-		}
-	}
-}
-
-.gwbmwxkm {
-	position: relative;
-
-	> header {
-		> .title {
-			z-index: 1;
-			margin: 0;
-			padding: 0 16px;
-			line-height: 42px;
-			font-size: 0.9em;
-			font-weight: bold;
-			box-shadow: 0 1px rgba(#000, 0.07);
-
-			> i {
-				margin-right: 6px;
-			}
-
-			&:empty {
-				display: none;
-			}
-		}
-
-		> .buttons {
-			position: absolute;
-			z-index: 2;
-			top: 0;
-			right: 0;
-
-			> button {
-				padding: 0;
-				width: 42px;
-				font-size: 0.9em;
-				line-height: 42px;
-			}
-		}
-	}
-
-	> section {
-		padding: 0 32px 32px 32px;
-
-		@media (max-width: 500px) {
-			padding: 0 16px 16px 16px;
-		}
-
-		> .view {
-			display: inline-block;
-			margin: 16px 0 0 0;
-			font-size: 14px;
-		}
-
-		> .content {
-			margin-bottom: 16px;
-		}
-
-		> .eyeCatch {
-			margin-bottom: 16px;
-
-			> div {
-				> img {
-					max-width: 100%;
-				}
-			}
-		}
-	}
-}
-
-.qmuvgica {
-	padding: 16px;
-
-	> .variables {
-		margin-bottom: 16px;
-	}
-
-	> .add {
-		margin-bottom: 16px;
-	}
-}
-</style>
diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue
deleted file mode 100644
index 3ea687a35d..0000000000
--- a/src/client/pages/page.vue
+++ /dev/null
@@ -1,311 +0,0 @@
-<template>
-<div>
-	<transition name="fade" mode="out-in">
-		<div v-if="page" class="xcukqgmh" :key="page.id" v-size="{ max: [450] }">
-			<div class="_block main">
-				<!--
-				<div class="header">
-					<h1>{{ page.title }}</h1>
-				</div>
-				-->
-				<div class="banner">
-					<img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/>
-				</div>
-				<div class="content">
-					<XPage :page="page"/>
-				</div>
-				<div class="actions">
-					<div class="like">
-						<MkButton class="button" @click="unlike()" v-if="page.isLiked" v-tooltip="$ts._pages.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
-						<MkButton class="button" @click="like()" v-else v-tooltip="$ts._pages.like"><i class="far fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
-					</div>
-					<div class="other">
-						<button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
-						<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
-					</div>
-				</div>
-				<div class="user">
-					<MkAvatar :user="page.user" class="avatar"/>
-					<div class="name">
-						<MkUserName :user="page.user" style="display: block;"/>
-						<MkAcct :user="page.user"/>
-					</div>
-					<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
-				</div>
-				<div class="links">
-					<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
-					<template v-if="$i && $i.id === page.userId">
-						<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
-						<button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button>
-						<button v-else @click="pin(true)" class="link _textButton">{{ $ts.pin }}</button>
-					</template>
-				</div>
-			</div>
-			<div class="footer">
-				<div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
-				<div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
-			</div>
-			<MkAd :prefer="['horizontal', 'horizontal-big']"/>
-			<MkContainer :max-height="300" :foldable="true" class="other">
-				<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
-				<MkPagination :pagination="otherPostsPagination" #default="{items}">
-					<MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/>
-				</MkPagination>
-			</MkContainer>
-		</div>
-		<MkError v-else-if="error" @retry="fetch()"/>
-		<MkLoading v-else/>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import XPage from '@client/components/page/page.vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { url } from '@client/config';
-import MkFollowButton from '@client/components/follow-button.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkPagePreview from '@client/components/page-preview.vue';
-
-export default defineComponent({
-	components: {
-		XPage,
-		MkButton,
-		MkFollowButton,
-		MkContainer,
-		MkPagination,
-		MkPagePreview,
-	},
-
-	props: {
-		pageName: {
-			type: String,
-			required: true
-		},
-		username: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.page ? {
-				title: computed(() => this.page.title || this.page.name),
-				avatar: this.page.user,
-				path: `/@${this.page.user.username}/pages/${this.page.name}`,
-				share: {
-					title: this.page.title || this.page.name,
-					text: this.page.summary,
-				},
-			} : null),
-			page: null,
-			error: null,
-			otherPostsPagination: {
-				endpoint: 'users/pages',
-				limit: 6,
-				params: () => ({
-					userId: this.page.user.id
-				})
-			},
-		};
-	},
-
-	computed: {
-		path(): string {
-			return this.username + '/' + this.pageName;
-		}
-	},
-
-	watch: {
-		path() {
-			this.fetch();
-		}
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			this.page = null;
-			os.api('pages/show', {
-				name: this.pageName,
-				username: this.username,
-			}).then(page => {
-				this.page = page;
-			}).catch(e => {
-				this.error = e;
-			});
-		},
-
-		share() {
-			navigator.share({
-				title: this.page.title || this.page.name,
-				text: this.page.summary,
-				url: `${url}/@${this.page.user.username}/pages/${this.page.name}`
-			});
-		},
-
-		shareWithNote() {
-			os.post({
-				initialText: `${this.page.title || this.page.name} ${url}/@${this.page.user.username}/pages/${this.page.name}`
-			});
-		},
-
-		like() {
-			os.apiWithDialog('pages/like', {
-				pageId: this.page.id,
-			}).then(() => {
-				this.page.isLiked = true;
-				this.page.likedCount++;
-			});
-		},
-
-		async unlike() {
-			const confirm = await os.dialog({
-				type: 'warning',
-				showCancelButton: true,
-				text: this.$ts.unlikeConfirm,
-			});
-			if (confirm.canceled) return;
-			os.apiWithDialog('pages/unlike', {
-				pageId: this.page.id,
-			}).then(() => {
-				this.page.isLiked = false;
-				this.page.likedCount--;
-			});
-		},
-
-		pin(pin) {
-			os.apiWithDialog('i/update', {
-				pinnedPageId: pin ? this.page.id : null,
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.xcukqgmh {
-	--padding: 32px;
-
-	&.max-width_450px {
-		--padding: 16px;
-	}
-
-	> .main {
-		padding: var(--padding);
-
-		> .header {
-			padding: 16px;
-
-			> h1 {
-				margin: 0;
-			}
-		}
-
-		> .banner {
-			> img {
-				// TODO: 良い感じのアスペクト比で表示
-				display: block;
-				width: 100%;
-				height: 150px;
-				object-fit: cover;
-			}
-		}
-
-		> .content {
-			margin-top: 16px;
-			padding: 16px 0 0 0;
-		}
-
-		> .actions {
-			display: flex;
-			align-items: center;
-			margin-top: 16px;
-			padding: 16px 0 0 0;
-			border-top: solid 0.5px var(--divider);
-
-			> .like {
-				> .button {
-					--accent: rgb(241 97 132);
-					--X8: rgb(241 92 128);
-					--buttonBg: rgb(216 71 106 / 5%);
-					--buttonHoverBg: rgb(216 71 106 / 10%);
-					color: #ff002f;
-
-					::v-deep(.count) {
-						margin-left: 0.5em;
-					}
-				}
-			}
-
-			> .other {
-				margin-left: auto;
-
-				> button {
-					padding: 8px;
-					margin: 0 8px;
-
-					&:hover {
-						color: var(--fgHighlighted);
-					}
-				}
-			}
-		}
-
-		> .user {
-			margin-top: 16px;
-			padding: 16px 0 0 0;
-			border-top: solid 0.5px var(--divider);
-			display: flex;
-			align-items: center;
-
-			> .avatar {
-				width: 52px;
-				height: 52px;
-			}
-
-			> .name {
-				margin: 0 0 0 12px;
-				font-size: 90%;
-			}
-
-			> .koudoku {
-				margin-left: auto;
-			}
-		}
-
-		> .links {
-			margin-top: 16px;
-			padding: 24px 0 0 0;
-			border-top: solid 0.5px var(--divider);
-
-			> .link {
-				margin-right: 0.75em;
-			}
-		}
-	}
-
-	> .footer {
-		margin: var(--padding);
-		font-size: 85%;
-		opacity: 0.75;
-	}
-}
-</style>
diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue
deleted file mode 100644
index 6963682592..0000000000
--- a/src/client/pages/pages.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-<template>
-<MkSpacer>
-	<!-- TODO: MkHeaderに統合 -->
-	<MkTab v-model="tab" v-if="$i">
-		<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._pages.featured }}</option>
-		<option value="my"><i class="fas fa-edit"></i> {{ $ts._pages.my }}</option>
-		<option value="liked"><i class="fas fa-heart"></i> {{ $ts._pages.liked }}</option>
-	</MkTab>
-
-	<div class="_section">
-		<div class="rknalgpo _content" v-if="tab === 'featured'">
-			<MkPagination :pagination="featuredPagesPagination" #default="{items}">
-				<MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
-			</MkPagination>
-		</div>
-
-		<div class="rknalgpo _content my" v-if="tab === 'my'">
-			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
-			<MkPagination :pagination="myPagesPagination" #default="{items}">
-				<MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
-			</MkPagination>
-		</div>
-
-		<div class="rknalgpo _content" v-if="tab === 'liked'">
-			<MkPagination :pagination="likedPagesPagination" #default="{items}">
-				<MkPagePreview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
-			</MkPagination>
-		</div>
-	</div>
-</MkSpacer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagePreview from '@client/components/page-preview.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkTab from '@client/components/tab.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkPagePreview, MkPagination, MkButton, MkTab
-	},
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.pages,
-				icon: 'fas fa-sticky-note',
-				bg: 'var(--bg)',
-				actions: [{
-					icon: 'fas fa-plus',
-					text: this.$ts.create,
-					handler: this.create,
-				}],
-			},
-			tab: 'featured',
-			featuredPagesPagination: {
-				endpoint: 'pages/featured',
-				noPaging: true,
-			},
-			myPagesPagination: {
-				endpoint: 'i/pages',
-				limit: 5,
-			},
-			likedPagesPagination: {
-				endpoint: 'i/page-likes',
-				limit: 5,
-			},
-		};
-	},
-	methods: {
-		create() {
-			this.$router.push(`/pages/new`);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rknalgpo {
-	&.my .ckltabjg:first-child {
-		margin-top: 16px;
-	}
-
-	.ckltabjg:not(:last-child) {
-		margin-bottom: 8px;
-	}
-
-	@media (min-width: 500px) {
-		.ckltabjg:not(:last-child) {
-			margin-bottom: 16px;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/preview.vue b/src/client/pages/preview.vue
deleted file mode 100644
index 3df446e676..0000000000
--- a/src/client/pages/preview.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<template>
-<div class="graojtoi">
-	<MkSample/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkSample from '@client/components/sample.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkSample,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.preview,
-				icon: 'fas fa-eye',
-			},
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.graojtoi {
-	padding: var(--margin);
-}
-</style>
diff --git a/src/client/pages/reset-password.vue b/src/client/pages/reset-password.vue
deleted file mode 100644
index 6dd9f24259..0000000000
--- a/src/client/pages/reset-password.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<template>
-<FormBase v-if="token">
-	<FormInput v-model="password" type="password">
-		<template #prefix><i class="fas fa-lock"></i></template>
-		<span>{{ $ts.newPassword }}</span>
-	</FormInput>
-	
-	<FormButton primary @click="save">{{ $ts.save }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormGroup,
-		FormLink,
-		FormInput,
-		FormButton,
-	},
-
-	props: {
-		token: {
-			type: String,
-			required: false
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.resetPassword,
-				icon: 'fas fa-lock'
-			},
-			password: '',
-		}
-	},
-
-	mounted() {
-		if (this.token == null) {
-			os.popup(import('@client/components/forgot-password.vue'), {}, {}, 'closed');
-			this.$router.push('/');
-		}
-	},
-
-	methods: {
-		async save() {
-			await os.apiWithDialog('reset-password', {
-				token: this.token,
-				password: this.password,
-			});
-			this.$router.push('/');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/reversi/game.board.vue b/src/client/pages/reversi/game.board.vue
deleted file mode 100644
index 0dd36faced..0000000000
--- a/src/client/pages/reversi/game.board.vue
+++ /dev/null
@@ -1,528 +0,0 @@
-<template>
-<div class="xqnhankfuuilcwvhgsopeqncafzsquya">
-	<header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ $ts._reversi.black }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ $ts._reversi.white }})</header>
-
-	<div style="overflow: hidden; line-height: 28px;">
-		<p class="turn" v-if="!iAmPlayer && !game.isEnded">
-			<Mfm :key="'turn:' + turnUser().name" :text="$t('_reversi.turnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
-			<MkEllipsis/>
-		</p>
-		<p class="turn" v-if="logPos != logs.length">
-			<Mfm :key="'past-turn-of:' + turnUser().name" :text="$t('_reversi.pastTurnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
-		</p>
-		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn()">{{ $ts._reversi.opponentTurn }}<MkEllipsis/></p>
-		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn()" style="animation: tada 1s linear infinite both;">{{ $ts._reversi.myTurn }}</p>
-		<p class="result" v-if="game.isEnded && logPos == logs.length">
-			<template v-if="game.winner">
-				<Mfm :key="'won'" :text="$t('_reversi.won', { name: game.winner.name })" :plain="true" :custom-emojis="game.winner.emojis"/>
-				<span v-if="game.surrendered != null"> ({{ $ts._reversi.surrendered }})</span>
-			</template>
-			<template v-else>{{ $ts._reversi.drawn }}</template>
-		</p>
-	</div>
-
-	<div class="board">
-		<div class="labels-x" v-if="$store.state.gamesReversiShowBoardLabels">
-			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
-		</div>
-		<div class="flex">
-			<div class="labels-y" v-if="$store.state.gamesReversiShowBoardLabels">
-				<div v-for="i in game.map.length">{{ i }}</div>
-			</div>
-			<div class="cells" :style="cellsStyle">
-				<div v-for="(stone, i) in o.board"
-					:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn(), can: turnUser() ? o.canPut(turnUser().id == blackUser.id, i) : null, prev: o.prevPos == i }"
-					@click="set(i)"
-					:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"
-				>
-					<template v-if="$store.state.gamesReversiUseAvatarStones || true">
-						<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black">
-						<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white">
-					</template>
-					<template v-else>
-						<i v-if="stone === true" class="fas fa-circle"></i>
-						<i v-if="stone === false" class="far fa-circle"></i>
-					</template>
-				</div>
-			</div>
-			<div class="labels-y" v-if="$store.state.gamesReversiShowBoardLabels">
-				<div v-for="i in game.map.length">{{ i }}</div>
-			</div>
-		</div>
-		<div class="labels-x" v-if="$store.state.gamesReversiShowBoardLabels">
-			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
-		</div>
-	</div>
-
-	<p class="status"><b>{{ $t('_reversi.turnCount', { count: logPos }) }}</b> {{ $ts._reversi.black }}:{{ o.blackCount }} {{ $ts._reversi.white }}:{{ o.whiteCount }} {{ $ts._reversi.total }}:{{ o.blackCount + o.whiteCount }}</p>
-
-	<div class="actions" v-if="!game.isEnded && iAmPlayer">
-		<MkButton @click="surrender" inline>{{ $ts._reversi.surrender }}</MkButton>
-	</div>
-
-	<div class="player" v-if="game.isEnded">
-		<span>{{ logPos }} / {{ logs.length }}</span>
-		<div class="buttons" v-if="!autoplaying">
-			<MkButton inline @click="logPos = 0" :disabled="logPos == 0"><i class="fas fa-angle-double-left"></i></MkButton>
-			<MkButton inline @click="logPos--" :disabled="logPos == 0"><i class="fas fa-angle-left"></i></MkButton>
-			<MkButton inline @click="logPos++" :disabled="logPos == logs.length"><i class="fas fa-angle-right"></i></MkButton>
-			<MkButton inline @click="logPos = logs.length" :disabled="logPos == logs.length"><i class="fas fa-angle-double-right"></i></MkButton>
-		</div>
-		<MkButton @click="autoplay()" :disabled="autoplaying" style="margin: var(--margin) auto 0 auto;"><i class="fas fa-play"></i></MkButton>
-	</div>
-
-	<div class="info">
-		<p v-if="game.isLlotheo">{{ $ts._reversi.isLlotheo }}</p>
-		<p v-if="game.loopedBoard">{{ $ts._reversi.loopedMap }}</p>
-		<p v-if="game.canPutEverywhere">{{ $ts._reversi.canPutEverywhere }}</p>
-	</div>
-
-	<div class="watchers">
-		<MkAvatar v-for="user in watchers" :key="user.id" :user="user" class="avatar"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as CRC32 from 'crc-32';
-import Reversi, { Color } from '../../../games/reversi/core';
-import { url } from '@client/config';
-import MkButton from '@client/components/ui/button.vue';
-import { userPage } from '@client/filters/user';
-import * as os from '@client/os';
-import * as sound from '@client/scripts/sound';
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-
-	props: {
-		initGame: {
-			type: Object,
-			require: true
-		},
-		connection: {
-			type: Object,
-			require: true
-		},
-	},
-
-	data() {
-		return {
-			game: JSON.parse(JSON.stringify(this.initGame)),
-			o: null as Reversi,
-			logs: [],
-			logPos: 0,
-			watchers: [],
-			pollingClock: null,
-		};
-	},
-
-	computed: {
-		iAmPlayer(): boolean {
-			if (!this.$i) return false;
-			return this.game.user1Id == this.$i.id || this.game.user2Id == this.$i.id;
-		},
-
-		myColor(): Color {
-			if (!this.iAmPlayer) return null;
-			if (this.game.user1Id == this.$i.id && this.game.black == 1) return true;
-			if (this.game.user2Id == this.$i.id && this.game.black == 2) return true;
-			return false;
-		},
-
-		opColor(): Color {
-			if (!this.iAmPlayer) return null;
-			return this.myColor === true ? false : true;
-		},
-
-		blackUser(): any {
-			return this.game.black == 1 ? this.game.user1 : this.game.user2;
-		},
-
-		whiteUser(): any {
-			return this.game.black == 1 ? this.game.user2 : this.game.user1;
-		},
-
-		cellsStyle(): any {
-			return {
-				'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`,
-				'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)`
-			};
-		}
-	},
-
-	watch: {
-		logPos(v) {
-			if (!this.game.isEnded) return;
-			const o = new Reversi(this.game.map, {
-				isLlotheo: this.game.isLlotheo,
-				canPutEverywhere: this.game.canPutEverywhere,
-				loopedBoard: this.game.loopedBoard
-			});
-			for (const log of this.logs.slice(0, v)) {
-				o.put(log.color, log.pos);
-			}
-			this.o = o;
-			//this.$forceUpdate();
-		}
-	},
-
-	created() {
-		this.o = new Reversi(this.game.map, {
-			isLlotheo: this.game.isLlotheo,
-			canPutEverywhere: this.game.canPutEverywhere,
-			loopedBoard: this.game.loopedBoard
-		});
-
-		for (const log of this.game.logs) {
-			this.o.put(log.color, log.pos);
-		}
-
-		this.logs = this.game.logs;
-		this.logPos = this.logs.length;
-
-		// 通信を取りこぼしてもいいように定期的にポーリングさせる
-		if (this.game.isStarted && !this.game.isEnded) {
-			this.pollingClock = setInterval(() => {
-				if (this.game.isEnded) return;
-				const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
-				this.connection.send('check', {
-					crc32: crc32
-				});
-			}, 3000);
-		}
-	},
-
-	mounted() {
-		this.connection.on('set', this.onSet);
-		this.connection.on('rescue', this.onRescue);
-		this.connection.on('ended', this.onEnded);
-		this.connection.on('watchers', this.onWatchers);
-	},
-
-	beforeUnmount() {
-		this.connection.off('set', this.onSet);
-		this.connection.off('rescue', this.onRescue);
-		this.connection.off('ended', this.onEnded);
-		this.connection.off('watchers', this.onWatchers);
-
-		clearInterval(this.pollingClock);
-	},
-
-	methods: {
-		userPage,
-
-		// this.o がリアクティブになった折にはcomputedにできる
-		turnUser(): any {
-			if (this.o.turn === true) {
-				return this.game.black == 1 ? this.game.user1 : this.game.user2;
-			} else if (this.o.turn === false) {
-				return this.game.black == 1 ? this.game.user2 : this.game.user1;
-			} else {
-				return null;
-			}
-		},
-
-		// this.o がリアクティブになった折にはcomputedにできる
-		isMyTurn(): boolean {
-			if (!this.iAmPlayer) return false;
-			if (this.turnUser() == null) return false;
-			return this.turnUser().id == this.$i.id;
-		},
-
-		set(pos) {
-			if (this.game.isEnded) return;
-			if (!this.iAmPlayer) return;
-			if (!this.isMyTurn()) return;
-			if (!this.o.canPut(this.myColor, pos)) return;
-
-			this.o.put(this.myColor, pos);
-
-			// サウンドを再生する
-			sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite');
-
-			this.connection.send('set', {
-				pos: pos
-			});
-
-			this.checkEnd();
-
-			this.$forceUpdate();
-		},
-
-		onSet(x) {
-			this.logs.push(x);
-			this.logPos++;
-			this.o.put(x.color, x.pos);
-			this.checkEnd();
-			this.$forceUpdate();
-
-			// サウンドを再生する
-			if (x.color !== this.myColor) {
-				sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
-			}
-		},
-
-		onEnded(x) {
-			this.game = JSON.parse(JSON.stringify(x.game));
-		},
-
-		checkEnd() {
-			this.game.isEnded = this.o.isEnded;
-			if (this.game.isEnded) {
-				if (this.o.winner === true) {
-					this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id;
-					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
-				} else if (this.o.winner === false) {
-					this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id;
-					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
-				} else {
-					this.game.winnerId = null;
-					this.game.winner = null;
-				}
-			}
-		},
-
-		// 正しいゲーム情報が送られてきたとき
-		onRescue(game) {
-			this.game = JSON.parse(JSON.stringify(game));
-
-			this.o = new Reversi(this.game.map, {
-				isLlotheo: this.game.isLlotheo,
-				canPutEverywhere: this.game.canPutEverywhere,
-				loopedBoard: this.game.loopedBoard
-			});
-
-			for (const log of this.game.logs) {
-				this.o.put(log.color, log.pos, true);
-			}
-
-			this.logs = this.game.logs;
-			this.logPos = this.logs.length;
-
-			this.checkEnd();
-			this.$forceUpdate();
-		},
-
-		onWatchers(users) {
-			this.watchers = users;
-		},
-
-		surrender() {
-			os.api('games/reversi/games/surrender', {
-				gameId: this.game.id
-			});
-		},
-
-		autoplay() {
-			this.autoplaying = true;
-			this.logPos = 0;
-
-			setTimeout(() => {
-				this.logPos = 1;
-
-				let i = 1;
-				let previousLog = this.game.logs[0];
-				const tick = () => {
-					const log = this.game.logs[i];
-					const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime()
-					setTimeout(() => {
-						i++;
-						this.logPos++;
-						previousLog = log;
-
-						if (i < this.game.logs.length) {
-							tick();
-						} else {
-							this.autoplaying = false;
-						}
-					}, time);
-				};
-
-				tick();
-			}, 1000);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-@use "sass:math";
-
-.xqnhankfuuilcwvhgsopeqncafzsquya {
-	text-align: center;
-
-	> .go-index {
-		position: absolute;
-		top: 0;
-		left: 0;
-		z-index: 1;
-		width: 42px;
-		height :42px;
-	}
-
-	> header {
-		padding: 8px;
-		border-bottom: dashed 1px var(--divider);
-	}
-
-	> .board {
-		width: calc(100% - 16px);
-		max-width: 500px;
-		margin: 0 auto;
-
-		$label-size: 16px;
-		$gap: 4px;
-
-		> .labels-x {
-			height: $label-size;
-			padding: 0 $label-size;
-			display: flex;
-
-			> * {
-				flex: 1;
-				display: flex;
-				align-items: center;
-				justify-content: center;
-				font-size: 0.8em;
-
-				&:first-child {
-					margin-left: -(math.div($gap, 2));
-				}
-
-				&:last-child {
-					margin-right: -(math.div($gap, 2));
-				}
-			}
-		}
-
-		> .flex {
-			display: flex;
-
-			> .labels-y {
-				width: $label-size;
-				display: flex;
-				flex-direction: column;
-
-				> * {
-					flex: 1;
-					display: flex;
-					align-items: center;
-					justify-content: center;
-					font-size: 12px;
-
-					&:first-child {
-						margin-top: -(math.div($gap, 2));
-					}
-
-					&:last-child {
-						margin-bottom: -(math.div($gap, 2));
-					}
-				}
-			}
-
-			> .cells {
-				flex: 1;
-				display: grid;
-				grid-gap: $gap;
-
-				> div {
-					background: transparent;
-					border-radius: 6px;
-					overflow: hidden;
-
-					* {
-						pointer-events: none;
-						user-select: none;
-					}
-
-					&.empty {
-						border: solid 2px var(--divider);
-					}
-
-					&.empty.can {
-						border-color: var(--accent);
-					}
-
-					&.empty.myTurn {
-						border-color: var(--divider);
-
-						&.can {
-							border-color: var(--accent);
-							cursor: pointer;
-
-							&:hover {
-								background: var(--accent);
-							}
-						}
-					}
-
-					&.prev {
-						box-shadow: 0 0 0 4px var(--accent);
-					}
-
-					&.isEnded {
-						border-color: var(--divider);
-					}
-
-					&.none {
-						border-color: transparent !important;
-					}
-
-					> svg, > img {
-						display: block;
-						width: 100%;
-						height: 100%;
-					}
-				}
-			}
-		}
-	}
-
-	> .status {
-		margin: 0;
-		padding: 16px 0;
-	}
-
-	> .actions {
-		padding-bottom: 16px;
-	}
-
-	> .player {
-		padding: 0 16px 32px 16px;
-		margin: 0 auto;
-		max-width: 500px;
-
-		> span {
-			display: inline-block;
-			margin: 0 8px;
-			min-width: 70px;
-		}
-
-		> .buttons {
-			display: flex;
-
-			> * {
-				flex: 1;
-			}
-		}
-	}
-
-	> .watchers {
-		padding: 0 0 16px 0;
-
-		&:empty {
-			display: none;
-		}
-
-		> .avatar {
-			width: 32px;
-			height: 32px;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/reversi/game.setting.vue b/src/client/pages/reversi/game.setting.vue
deleted file mode 100644
index eb6f24e4ab..0000000000
--- a/src/client/pages/reversi/game.setting.vue
+++ /dev/null
@@ -1,390 +0,0 @@
-<template>
-<div class="urbixznjwwuukfsckrwzwsqzsxornqij">
-	<header><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></header>
-
-	<div>
-		<p>{{ $ts._reversi.gameSettings }}</p>
-
-		<div class="card map _panel">
-			<header>
-				<select v-model="mapName" :placeholder="$ts._reversi.chooseBoard" @change="onMapChange">
-					<option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/>
-					<option :label="$ts.random" :value="null"/>
-					<optgroup v-for="c in mapCategories" :key="c" :label="c">
-						<option v-for="m in Object.values(maps).filter(m => m.category == c)" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
-					</optgroup>
-				</select>
-			</header>
-
-			<div>
-				<div class="random" v-if="game.map == null"><i class="fas fa-dice"></i></div>
-				<div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
-					<div v-for="(x, i) in game.map.join('')" :class="{ none: x == ' ' }" @click="onPixelClick(i, x)">
-						<i v-if="x === 'b'" class="fas fa-circle"></i>
-						<i v-if="x === 'w'" class="far fa-circle"></i>
-					</div>
-				</div>
-			</div>
-		</div>
-
-		<div class="card _panel">
-			<header>
-				<span>{{ $ts._reversi.blackOrWhite }}</span>
-			</header>
-
-			<div>
-				<MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $ts.random }}</MkRadio>
-				<MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')">
-					<I18n :src="$ts._reversi.blackIs" tag="span">
-						<template #name>
-							<b><MkUserName :user="game.user1"/></b>
-						</template>
-					</I18n>
-				</MkRadio>
-				<MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')">
-					<I18n :src="$ts._reversi.blackIs" tag="span">
-						<template #name>
-							<b><MkUserName :user="game.user2"/></b>
-						</template>
-					</I18n>
-				</MkRadio>
-			</div>
-		</div>
-
-		<div class="card _panel">
-			<header>
-				<span>{{ $ts._reversi.rules }}</span>
-			</header>
-
-			<div>
-				<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ $ts._reversi.isLlotheo }}</MkSwitch>
-				<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ $ts._reversi.loopedMap }}</MkSwitch>
-				<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ $ts._reversi.canPutEverywhere }}</MkSwitch>
-			</div>
-		</div>
-
-		<div class="card form _panel" v-if="form">
-			<header>
-				<span>{{ $ts._reversi.botSettings }}</span>
-			</header>
-
-			<div>
-				<template v-for="item in form">
-					<MkSwitch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</MkSwitch>
-
-					<div class="card" v-if="item.type == 'radio'" :key="item.id">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<MkRadio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @update:modelValue="onChangeForm(item)">{{ r.label }}</MkRadio>
-						</div>
-					</div>
-
-					<div class="card" v-if="item.type == 'slider'" :key="item.id">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<input type="range" :min="item.min" :max="item.max" :step="item.step || 1" v-model="item.value" @change="onChangeForm(item)"/>
-						</div>
-					</div>
-
-					<div class="card" v-if="item.type == 'textbox'" :key="item.id">
-						<header>
-							<span>{{ item.label }}</span>
-						</header>
-
-						<div>
-							<input v-model="item.value" @change="onChangeForm(item)"/>
-						</div>
-					</div>
-				</template>
-			</div>
-		</div>
-	</div>
-
-	<footer class="_acrylic">
-		<p class="status">
-			<template v-if="isAccepted && isOpAccepted">{{ $ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
-			<template v-if="isAccepted && !isOpAccepted">{{ $ts._reversi.waitingForOther }}<MkEllipsis/></template>
-			<template v-if="!isAccepted && isOpAccepted">{{ $ts._reversi.waitingForMe }}</template>
-			<template v-if="!isAccepted && !isOpAccepted">{{ $ts._reversi.waitingBoth }}<MkEllipsis/></template>
-		</p>
-
-		<div class="actions">
-			<MkButton inline @click="exit">{{ $ts.cancel }}</MkButton>
-			<MkButton inline primary @click="accept" v-if="!isAccepted">{{ $ts._reversi.ready }}</MkButton>
-			<MkButton inline primary @click="cancel" v-if="isAccepted">{{ $ts._reversi.cancelReady }}</MkButton>
-		</div>
-	</footer>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as maps from '../../../games/reversi/maps';
-import MkButton from '@client/components/ui/button.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import MkRadio from '@client/components/form/radio.vue';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkSwitch,
-		MkRadio,
-	},
-
-	props: {
-		initGame: {
-			type: Object,
-			require: true
-		},
-		connection: {
-			type: Object,
-			require: true
-		},
-	},
-
-	data() {
-		return {
-			game: this.initGame,
-			o: null,
-			isLlotheo: false,
-			mapName: maps.eighteight.name,
-			maps: maps,
-			form: null,
-			messages: [],
-		};
-	},
-
-	computed: {
-		mapCategories(): string[] {
-			const categories = Object.values(maps).map(x => x.category);
-			return categories.filter((item, pos) => categories.indexOf(item) == pos);
-		},
-		isAccepted(): boolean {
-			if (this.game.user1Id == this.$i.id && this.game.user1Accepted) return true;
-			if (this.game.user2Id == this.$i.id && this.game.user2Accepted) return true;
-			return false;
-		},
-		isOpAccepted(): boolean {
-			if (this.game.user1Id != this.$i.id && this.game.user1Accepted) return true;
-			if (this.game.user2Id != this.$i.id && this.game.user2Accepted) return true;
-			return false;
-		}
-	},
-
-	created() {
-		this.connection.on('changeAccepts', this.onChangeAccepts);
-		this.connection.on('updateSettings', this.onUpdateSettings);
-		this.connection.on('initForm', this.onInitForm);
-		this.connection.on('message', this.onMessage);
-
-		if (this.game.user1Id != this.$i.id && this.game.form1) this.form = this.game.form1;
-		if (this.game.user2Id != this.$i.id && this.game.form2) this.form = this.game.form2;
-	},
-
-	beforeUnmount() {
-		this.connection.off('changeAccepts', this.onChangeAccepts);
-		this.connection.off('updateSettings', this.onUpdateSettings);
-		this.connection.off('initForm', this.onInitForm);
-		this.connection.off('message', this.onMessage);
-	},
-
-	methods: {
-		exit() {
-
-		},
-
-		accept() {
-			this.connection.send('accept', {});
-		},
-
-		cancel() {
-			this.connection.send('cancelAccept', {});
-		},
-
-		onChangeAccepts(accepts) {
-			this.game.user1Accepted = accepts.user1;
-			this.game.user2Accepted = accepts.user2;
-		},
-
-		updateSettings(key: string) {
-			this.connection.send('updateSettings', {
-				key: key,
-				value: this.game[key]
-			});
-		},
-
-		onUpdateSettings({ key, value }) {
-			this.game[key] = value;
-			if (this.game.map == null) {
-				this.mapName = null;
-			} else {
-				const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join(''));
-				this.mapName = found ? found.name : '-Custom-';
-			}
-		},
-
-		onInitForm(x) {
-			if (x.userId == this.$i.id) return;
-			this.form = x.form;
-		},
-
-		onMessage(x) {
-			if (x.userId == this.$i.id) return;
-			this.messages.unshift(x.message);
-		},
-
-		onChangeForm(item) {
-			this.connection.send('updateForm', {
-				id: item.id,
-				value: item.value
-			});
-		},
-
-		onMapChange() {
-			if (this.mapName == null) {
-				this.game.map = null;
-			} else {
-				this.game.map = Object.values(maps).find(x => x.name == this.mapName).data;
-			}
-			this.updateSettings('map');
-		},
-
-		onPixelClick(pos, pixel) {
-			const x = pos % this.game.map[0].length;
-			const y = Math.floor(pos / this.game.map[0].length);
-			const newPixel =
-				pixel == ' ' ? '-' :
-				pixel == '-' ? 'b' :
-				pixel == 'b' ? 'w' :
-				' ';
-			const line = this.game.map[y].split('');
-			line[x] = newPixel;
-			this.game.map[y] = line.join('');
-			this.updateSettings('map');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.urbixznjwwuukfsckrwzwsqzsxornqij {
-	text-align: center;
-	background: var(--bg);
-
-	> header {
-		padding: 8px;
-		border-bottom: dashed 1px #c4cdd4;
-	}
-
-	> div {
-		padding: 0 16px;
-
-		> .card {
-			margin: 0 auto 16px auto;
-
-			&.map {
-				> header {
-					> select {
-						width: 100%;
-						padding: 12px 14px;
-						background: var(--face);
-						border: 1px solid var(--inputBorder);
-						border-radius: 4px;
-						color: var(--fg);
-						cursor: pointer;
-						transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
-						-webkit-appearance: none;
-						-moz-appearance: none;
-						appearance: none;
-
-						&:focus-visible,
-						&:active {
-							border-color: var(--accent);
-						}
-					}
-				}
-
-				> div {
-					> .random {
-						padding: 32px 0;
-						font-size: 64px;
-						color: var(--fg);
-						opacity: 0.7;
-					}
-
-					> .board {
-						display: grid;
-						grid-gap: 4px;
-						width: 300px;
-						height: 300px;
-						margin: 0 auto;
-						color: var(--fg);
-
-						> div {
-							background: transparent;
-							border: solid 2px var(--divider);
-							border-radius: 6px;
-							overflow: hidden;
-							cursor: pointer;
-
-							* {
-								pointer-events: none;
-								user-select: none;
-								width: 100%;
-								height: 100%;
-							}
-
-							&.none {
-								border-color: transparent;
-							}
-						}
-					}
-				}
-			}
-
-			&.form {
-				> div {
-					> .card + .card {
-						margin-top: 16px;
-					}
-
-					input[type='range'] {
-						width: 100%;
-					}
-				}
-			}
-		}
-
-		.card {
-			max-width: 400px;
-
-			> header {
-				padding: 18px 20px;
-				border-bottom: 1px solid var(--divider);
-			}
-
-			> div {
-				padding: 20px;
-				color: var(--fg);
-			}
-		}
-	}
-
-	> footer {
-		position: sticky;
-		bottom: 0;
-		padding: 16px;
-		border-top: solid 1px var(--divider);
-
-		> .status {
-			margin: 0 0 16px 0;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/reversi/game.vue b/src/client/pages/reversi/game.vue
deleted file mode 100644
index ae10b45b5b..0000000000
--- a/src/client/pages/reversi/game.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<div v-if="game == null"><MkLoading/></div>
-<GameSetting v-else-if="!game.isStarted" :init-game="game" :connection="connection"/>
-<GameBoard v-else :init-game="game" :connection="connection"/>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import GameSetting from './game.setting.vue';
-import GameBoard from './game.board.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		GameSetting,
-		GameBoard,
-	},
-
-	props: {
-		gameId: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._reversi.reversi,
-				icon: 'fas fa-gamepad'
-			},
-			game: null,
-			connection: null,
-		};
-	},
-
-	watch: {
-		gameId() {
-			this.fetch();
-		}
-	},
-
-	mounted() {
-		this.fetch();
-	},
-
-	beforeUnmount() {
-		if (this.connection) {
-			this.connection.dispose();
-		}
-	},
-
-	methods: {
-		fetch() {
-			os.api('games/reversi/games/show', {
-				gameId: this.gameId
-			}).then(game => {
-				this.game = game;
-
-				if (this.connection) {
-					this.connection.dispose();
-				}
-				this.connection = markRaw(os.stream.useChannel('gamesReversiGame', {
-					gameId: this.game.id
-				}));
-				this.connection.on('started', this.onStarted);
-			});
-		},
-
-		onStarted(game) {
-			Object.assign(this.game, game);
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/reversi/index.vue b/src/client/pages/reversi/index.vue
deleted file mode 100644
index cedfd12089..0000000000
--- a/src/client/pages/reversi/index.vue
+++ /dev/null
@@ -1,279 +0,0 @@
-<template>
-<div class="bgvwxkhb" v-if="!matching">
-	<h1>Misskey {{ $ts._reversi.reversi }}</h1>
-
-	<div class="play">
-		<MkButton primary round @click="match" style="margin: var(--margin) auto 0 auto;">{{ $ts.invite }}</MkButton>
-	</div>
-
-	<div class="_section">
-		<MkFolder v-if="invitations.length > 0">
-			<template #header>{{ $ts.invitations }}</template>
-			<div class="nfcacttm">
-				<button class="invitation _panel _button" v-for="invitation in invitations" tabindex="-1" @click="accept(invitation)">
-					<MkAvatar class="avatar" :user="invitation.parent" :show-indicator="true"/>
-					<span class="name"><b><MkUserName :user="invitation.parent"/></b></span>
-					<span class="username">@{{ invitation.parent.username }}</span>
-					<MkTime :time="invitation.createdAt" class="time"/>
-				</button>
-			</div>
-		</MkFolder>
-
-		<MkFolder v-if="myGames.length > 0">
-			<template #header>{{ $ts._reversi.myGames }}</template>
-			<div class="knextgwz">
-				<MkA class="game _panel" v-for="g in myGames" tabindex="-1" :to="`/games/reversi/${g.id}`" :key="g.id">
-					<div class="players">
-						<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
-					</div>
-					<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
-				</MkA>
-			</div>
-		</MkFolder>
-
-		<MkFolder v-if="games.length > 0">
-			<template #header>{{ $ts._reversi.allGames }}</template>
-			<div class="knextgwz">
-				<MkA class="game _panel" v-for="g in games" tabindex="-1" :to="`/games/reversi/${g.id}`" :key="g.id">
-					<div class="players">
-						<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
-					</div>
-					<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
-				</MkA>
-			</div>
-		</MkFolder>
-	</div>
-</div>
-<div class="sazhgisb" v-else>
-	<h1>
-		<I18n :src="$ts.waitingFor" tag="span">
-			<template #x>
-				<b><MkUserName :user="matching"/></b>
-			</template>
-		</I18n>
-		<MkEllipsis/>
-	</h1>
-	<div class="cancel">
-		<MkButton inline round @click="cancel">{{ $ts.cancel }}</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import * as os from '@client/os';
-import MkButton from '@client/components/ui/button.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton, MkFolder,
-	},
-
-	inject: ['navHook'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._reversi.reversi,
-				icon: 'fas fa-gamepad'
-			},
-			games: [],
-			gamesFetching: true,
-			gamesMoreFetching: false,
-			myGames: [],
-			matching: null,
-			invitations: [],
-			connection: null,
-			pingClock: null,
-		};
-	},
-
-	mounted() {
-		if (this.$i) {
-			this.connection = markRaw(os.stream.useChannel('gamesReversi'));
-
-			this.connection.on('invited', this.onInvited);
-
-			this.connection.on('matched', this.onMatched);
-
-			this.pingClock = setInterval(() => {
-				if (this.matching) {
-					this.connection.send('ping', {
-						id: this.matching.id
-					});
-				}
-			}, 3000);
-
-			os.api('games/reversi/games', {
-				my: true
-			}).then(games => {
-				this.myGames = games;
-			});
-
-			os.api('games/reversi/invitations').then(invitations => {
-				this.invitations = this.invitations.concat(invitations);
-			});
-		}
-
-		os.api('games/reversi/games').then(games => {
-			this.games = games;
-			this.gamesFetching = false;
-		});
-	},
-
-	beforeUnmount() {
-		if (this.connection) {
-			this.connection.dispose();
-			clearInterval(this.pingClock);
-		}
-	},
-
-	methods: {
-		go(game) {
-			const url = '/games/reversi/' + game.id;
-			if (this.navHook) {
-				this.navHook(url);
-			} else {
-				this.$router.push(url);
-			}
-		},
-
-		async match() {
-			const user = await os.selectUser({ local: true });
-			if (user == null) return;
-			os.api('games/reversi/match', {
-				userId: user.id
-			}).then(res => {
-				if (res == null) {
-					this.matching = user;
-				} else {
-					this.go(res);
-				}
-			});
-		},
-
-		cancel() {
-			this.matching = null;
-			os.api('games/reversi/match/cancel');
-		},
-
-		accept(invitation) {
-			os.api('games/reversi/match', {
-				userId: invitation.parent.id
-			}).then(game => {
-				if (game) {
-					this.go(game);
-				}
-			});
-		},
-
-		onMatched(game) {
-			this.go(game);
-		},
-
-		onInvited(invite) {
-			this.invitations.unshift(invite);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.bgvwxkhb {
-	> h1 {
-		margin: 0;
-		padding: 24px;
-		text-align: center;
-		font-size: 1.5em;
-		background: linear-gradient(0deg, #43c583, #438881);
-		color: #fff;
-	}
-
-	> .play {
-		text-align: center;
-	}
-}
-
-.sazhgisb {
-	text-align: center;
-}
-
-.nfcacttm {
-	> .invitation {
-		display: flex;
-		box-sizing: border-box;
-		width: 100%;
-		padding: 16px;
-		line-height: 32px;
-		text-align: left;
-
-		> .avatar {
-			width: 32px;
-			height: 32px;
-			margin-right: 8px;
-		}
-
-		> .name {
-			margin-right: 8px;
-		}
-
-		> .username {
-			margin-right: 8px;
-			opacity: 0.7;
-		}
-
-		> .time {
-			margin-left: auto;
-			opacity: 0.7;
-		}
-	}
-}
-
-.knextgwz {
-	display: grid;
-	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: var(--margin);
-
-	> .game {
-		> .players {
-			text-align: center;
-			padding: 16px;
-			line-height: 32px;
-
-			> .avatar {
-				width: 32px;
-				height: 32px;
-
-				&:first-child {
-					margin-right: 8px;
-				}
-
-				&:last-child {
-					margin-left: 8px;
-				}
-			}
-		}
-
-		> footer {
-			display: flex;
-			align-items: baseline;
-			border-top: solid 0.5px var(--divider);
-			padding: 6px 8px;
-			font-size: 0.9em;
-
-			> .state {
-				&.playing {
-					color: var(--accent);
-				}
-			}
-
-			> .time {
-				margin-left: auto;
-				opacity: 0.7;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/room/preview.vue b/src/client/pages/room/preview.vue
deleted file mode 100644
index 0cb6bcf04c..0000000000
--- a/src/client/pages/room/preview.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<template>
-<canvas width="224" height="128"></canvas>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as THREE from 'three';
-import * as os from '@client/os';
-
-export default defineComponent({
-	data() {
-		return {
-			selected: null,
-			objectHeight: 0,
-			orbitRadius: 5
-		};
-	},
-
-	mounted() {
-		const canvas = this.$el;
-
-		const width = canvas.width;
-		const height = canvas.height;
-
-		const scene = new THREE.Scene();
-
-		const renderer = new THREE.WebGLRenderer({
-			canvas: canvas,
-			antialias: true,
-			alpha: false
-		});
-		renderer.setPixelRatio(window.devicePixelRatio);
-		renderer.setSize(width, height);
-		renderer.setClearColor(0x000000);
-		renderer.autoClear = false;
-		renderer.shadowMap.enabled = true;
-		renderer.shadowMap.cullFace = THREE.CullFaceBack;
-
-		const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
-		camera.zoom = 10;
-		camera.position.x = 0;
-		camera.position.y = 2;
-		camera.position.z = 0;
-		camera.updateProjectionMatrix();
-		scene.add(camera);
-
-		const ambientLight = new THREE.AmbientLight(0xffffff, 1);
-		ambientLight.castShadow = false;
-		scene.add(ambientLight);
-
-		const light = new THREE.PointLight(0xffffff, 1, 100);
-		light.position.set(3, 3, 3);
-		scene.add(light);
-
-		const grid = new THREE.GridHelper(5, 16, 0x444444, 0x222222);
-		scene.add(grid);
-
-		const render = () => {
-			const timer = Date.now() * 0.0004;
-			requestAnimationFrame(render);
-			
-			camera.position.y = Math.sin(Math.PI / 6) * this.orbitRadius;	// Math.PI / 6 => 30deg
-			camera.position.z = Math.cos(timer) * this.orbitRadius;
-			camera.position.x = Math.sin(timer) * this.orbitRadius;
-			camera.lookAt(new THREE.Vector3(0, this.objectHeight / 2, 0));
-			renderer.render(scene, camera);
-		};
-
-		this.selected = selected => {
-			const obj = selected.clone();
-
-			// Remove current object
-			const current = scene.getObjectByName('obj');
-			if (current != null) {
-				scene.remove(current);
-			}
-
-			// Add new object
-			obj.name = 'obj';
-			obj.position.x = 0;
-			obj.position.y = 0;
-			obj.position.z = 0;
-			obj.rotation.x = 0;
-			obj.rotation.y = 0;
-			obj.rotation.z = 0;
-			obj.traverse(child => {
-				if (child instanceof THREE.Mesh) {
-					child.material = child.material.clone();
-					return child.material.emissive.setHex(0x000000);
-				}
-			});
-			const objectBoundingBox = new THREE.Box3().setFromObject(obj);
-			this.objectHeight = objectBoundingBox.max.y - objectBoundingBox.min.y;
-
-			const objectWidth = objectBoundingBox.max.x - objectBoundingBox.min.x;
-			const objectDepth = objectBoundingBox.max.z - objectBoundingBox.min.z;
-
-			const horizontal = Math.hypot(objectWidth, objectDepth) / camera.aspect;
-			this.orbitRadius = Math.max(horizontal, this.objectHeight) * camera.zoom * 0.625 / Math.tan(camera.fov * 0.5 * (Math.PI / 180));
-		
-			scene.add(obj);
-		};
-
-		render();
-	},
-});
-</script>
diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue
deleted file mode 100644
index 671dca3577..0000000000
--- a/src/client/pages/room/room.vue
+++ /dev/null
@@ -1,285 +0,0 @@
-<template>
-<div class="hveuntkp">
-	<div class="controller _section" v-if="objectSelected">
-		<div class="_content">
-			<p class="name">{{ selectedFurnitureName }}</p>
-			<XPreview ref="preview"/>
-			<template v-if="selectedFurnitureInfo.props">
-				<div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k">
-					<p>{{ k }}</p>
-					<template v-if="selectedFurnitureInfo.props[k] === 'image'">
-						<MkButton @click="chooseImage(k, $event)">{{ $ts._rooms.chooseImage }}</MkButton>
-					</template>
-					<template v-else-if="selectedFurnitureInfo.props[k] === 'color'">
-						<input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/>
-					</template>
-				</div>
-			</template>
-		</div>
-		<div class="_content">
-			<MkButton inline @click="translate()" :primary="isTranslateMode"><i class="fas fa-arrows-alt"></i> {{ $ts._rooms.translate }}</MkButton>
-			<MkButton inline @click="rotate()" :primary="isRotateMode"><i class="fas fa-undo"></i> {{ $ts._rooms.rotate }}</MkButton>
-			<MkButton inline v-if="isTranslateMode || isRotateMode" @click="exit()"><i class="fas fa-ban"></i> {{ $ts._rooms.exit }}</MkButton>
-		</div>
-		<div class="_content">
-			<MkButton @click="remove()"><i class="fas fa-trash-alt"></i> {{ $ts._rooms.remove }}</MkButton>
-		</div>
-	</div>
-
-	<div class="menu _section" v-if="isMyRoom">
-		<div class="_content">
-			<MkButton @click="add()"><i class="fas fa-box-open"></i> {{ $ts._rooms.addFurniture }}</MkButton>
-		</div>
-		<div class="_content">
-			<MkSelect :model-value="roomType" @update:modelValue="updateRoomType($event)">
-				<template #label>{{ $ts._rooms.roomType }}</template>
-				<option value="default">{{ $ts._rooms._roomType.default }}</option>
-				<option value="washitsu">{{ $ts._rooms._roomType.washitsu }}</option>
-			</MkSelect>
-			<label v-if="roomType === 'default'">
-				<span>{{ $ts._rooms.carpetColor }}</span>
-				<input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/>
-			</label>
-		</div>
-		<div class="_content">
-			<MkButton inline :disabled="!changed" primary @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-			<MkButton inline @click="clear()"><i class="fas fa-broom"></i> {{ $ts._rooms.clear }}</MkButton>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import { Room } from '@client/scripts/room/room';
-import { parseAcct } from '@/misc/acct';
-import XPreview from './preview.vue';
-const storeItems = require('@client/scripts/room/furnitures.json5');
-import { query as urlQuery } from '../../../prelude/url';
-import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/form/select.vue';
-import { selectFile } from '@client/scripts/select-file';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import * as symbols from '@client/symbols';
-
-let room: Room;
-
-export default defineComponent({
-	components: {
-		XPreview,
-		MkButton,
-		MkSelect,
-	},
-
-	props: {
-		acct: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.user ? {
-				title: this.$ts.room,
-				avatar: this.user,
-			} : null),
-			user: null,
-			objectSelected: false,
-			selectedFurnitureName: null,
-			selectedFurnitureInfo: null,
-			selectedFurnitureProps: null,
-			roomType: null,
-			carpetColor: null,
-			isTranslateMode: false,
-			isRotateMode: false,
-			isMyRoom: false,
-			changed: false,
-		};
-	},
-
-	async mounted() {
-		window.addEventListener('beforeunload', this.beforeunload);
-
-		this.user = await os.api('users/show', {
-			...parseAcct(this.acct)
-		});
-
-		this.isMyRoom = this.$i && (this.$i.id === this.user.id);
-
-		const roomInfo = await os.api('room/show', {
-			userId: this.user.id
-		});
-
-		this.roomType = roomInfo.roomType;
-		this.carpetColor = roomInfo.carpetColor;
-
-		room = new Room(this.user, this.isMyRoom, roomInfo, this.$el, {
-			graphicsQuality: ColdDeviceStorage.get('roomGraphicsQuality'),
-			onChangeSelect: obj => {
-				this.objectSelected = obj != null;
-				if (obj) {
-					const f = room.findFurnitureById(obj.name);
-					this.selectedFurnitureName = this.$t('_rooms._furnitures.' + f.type);
-					this.selectedFurnitureInfo = storeItems.find(x => x.id === f.type);
-					this.selectedFurnitureProps = f.props
-						? JSON.parse(JSON.stringify(f.props)) // Disable reactivity
-						: null;
-					this.$nextTick(() => {
-						this.$refs.preview.selected(obj);
-					});
-				}
-			},
-			useOrthographicCamera: ColdDeviceStorage.get('roomUseOrthographicCamera'),
-		});
-	},
-
-	beforeRouteLeave(to, from, next) {
-		if (this.changed) {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.leaveConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) {
-					next(false);
-				} else {
-					next();
-				}
-			});
-		} else {
-			next();
-		}
-	},
-
-	beforeUnmount() {
-		room.destroy();
-		window.removeEventListener('beforeunload', this.beforeunload);
-	},
-
-	methods: {
-		beforeunload(e: BeforeUnloadEvent) {
-			if (this.changed) {
-				e.preventDefault();
-				e.returnValue = '';
-			}
-		},
-
-		async add() {
-			const { canceled, result: id } = await os.dialog({
-				type: null,
-				title: this.$ts._rooms.addFurniture,
-				select: {
-					items: storeItems.map(item => ({
-						value: item.id, text: this.$t('_rooms._furnitures.' + item.id)
-					}))
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			room.addFurniture(id);
-			this.changed = true;
-		},
-
-		remove() {
-			this.isTranslateMode = false;
-			this.isRotateMode = false;
-			room.removeFurniture();
-			this.changed = true;
-		},
-
-		save() {
-			os.api('room/update', {
-				room: room.getRoomInfo()
-			}).then(() => {
-				this.changed = false;
-				os.success();
-			}).catch((e: any) => {
-				os.dialog({
-					type: 'error',
-					text: e.message
-				});
-			});
-		},
-
-		clear() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts._rooms.clearConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				room.removeAllFurnitures();
-				this.changed = true;
-			});
-		},
-
-		chooseImage(key, e) {
-			selectFile(e.currentTarget || e.target, null, false).then(file => {
-				room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`);
-				this.$refs.preview.selected(room.getSelectedObject());
-				this.changed = true;
-			});
-		},
-
-		updateColor(key, ev) {
-			room.updateProp(key, ev.target.value);
-			this.$refs.preview.selected(room.getSelectedObject());
-			this.changed = true;
-		},
-
-		updateCarpetColor(ev) {
-			room.updateCarpetColor(ev.target.value);
-			this.carpetColor = ev.target.value;
-			this.changed = true;
-		},
-
-		updateRoomType(type) {
-			room.changeRoomType(type);
-			this.roomType = type;
-			this.changed = true;
-		},
-
-		translate() {
-			if (this.isTranslateMode) {
-				this.exit();
-			} else {
-				this.isRotateMode = false;
-				this.isTranslateMode = true;
-				room.enterTransformMode('translate');
-			}
-			this.changed = true;
-		},
-
-		rotate() {
-			if (this.isRotateMode) {
-				this.exit();
-			} else {
-				this.isTranslateMode = false;
-				this.isRotateMode = true;
-				room.enterTransformMode('rotate');
-			}
-			this.changed = true;
-		},
-
-		exit() {
-			this.isTranslateMode = false;
-			this.isRotateMode = false;
-			room.exitTransformMode();
-			this.changed = true;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hveuntkp {
-	position: relative;
-	min-height: 500px;
-
-	> ::v-deep(canvas) {
-		display: block;
-	}
-}
-</style>
diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue
deleted file mode 100644
index 99164ec51f..0000000000
--- a/src/client/pages/scratchpad.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-<template>
-<div class="iltifgqe">
-	<div class="editor _panel _gap">
-		<PrismEditor class="_code code" v-model="code" :highlight="highlighter" :line-numbers="false"/>
-		<MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><i class="fas fa-play"></i></MkButton>
-	</div>
-
-	<MkContainer :foldable="true" class="_gap">
-		<template #header>{{ $ts.output }}</template>
-		<div class="bepmlvbi">
-			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div>
-		</div>
-	</MkContainer>
-
-	<div class="_gap">
-		{{ $ts.scratchpadDescription }}
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import 'prismjs';
-import { highlight, languages } from 'prismjs/components/prism-core';
-import 'prismjs/components/prism-clike';
-import 'prismjs/components/prism-javascript';
-import 'prismjs/themes/prism-okaidia.css';
-import { PrismEditor } from 'vue-prism-editor';
-import 'vue-prism-editor/dist/prismeditor.min.css';
-import { AiScript, parse, utils, values } from '@syuilo/aiscript';
-import MkContainer from '@client/components/ui/container.vue';
-import MkButton from '@client/components/ui/button.vue';
-import { createAiScriptEnv } from '@client/scripts/aiscript/api';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkContainer,
-		MkButton,
-		PrismEditor,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.scratchpad,
-				icon: 'fas fa-terminal',
-			},
-			code: '',
-			logs: [],
-		}
-	},
-
-	watch: {
-		code() {
-			localStorage.setItem('scratchpad', this.code);
-		}
-	},
-
-	created() {
-		const saved = localStorage.getItem('scratchpad');
-		if (saved) {
-			this.code = saved;
-		}
-	},
-
-	methods: {
-		async run() {
-			this.logs = [];
-			const aiscript = new AiScript(createAiScriptEnv({
-				storageKey: 'scratchpad',
-				token: this.$i?.token,
-			}), {
-				in: (q) => {
-					return new Promise(ok => {
-						os.dialog({
-							title: q,
-							input: {}
-						}).then(({ canceled, result: a }) => {
-							ok(a);
-						});
-					});
-				},
-				out: (value) => {
-					this.logs.push({
-						id: Math.random(),
-						text: value.type === 'str' ? value.value : utils.valToString(value),
-						print: true
-					});
-				},
-				log: (type, params) => {
-					switch (type) {
-						case 'end': this.logs.push({
-							id: Math.random(),
-							text: utils.valToString(params.val, true),
-							print: false
-						}); break;
-						default: break;
-					}
-				}
-			});
-
-			let ast;
-			try {
-				ast = parse(this.code);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: 'Syntax error :('
-				});
-				return;
-			}
-			try {
-				await aiscript.exec(ast);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: e
-				});
-			}
-		},
-
-		highlighter(code) {
-			return highlight(code, languages.js, 'javascript');
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.iltifgqe {
-	padding: 16px;
-
-	> .editor {
-		position: relative;
-	}
-}
-
-.bepmlvbi {
-	padding: 16px;
-
-	> .log {
-		&:not(.print) {
-			opacity: 0.7;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/search.vue b/src/client/pages/search.vue
deleted file mode 100644
index 8cf4d32a8f..0000000000
--- a/src/client/pages/search.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<template>
-<div class="_section">
-	<div class="_content">
-		<XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XNotes from '@client/components/notes.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: computed(() => this.$t('searchWith', { q: this.$route.query.q })),
-				icon: 'fas fa-search',
-			},
-			pagination: {
-				endpoint: 'notes/search',
-				limit: 10,
-				params: () => ({
-					query: this.$route.query.q,
-					channelId: this.$route.query.channel,
-				})
-			},
-		};
-	},
-
-	watch: {
-		$route() {
-			(this.$refs.notes as any).reload();
-		}
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/2fa.vue b/src/client/pages/settings/2fa.vue
deleted file mode 100644
index 386e7c635a..0000000000
--- a/src/client/pages/settings/2fa.vue
+++ /dev/null
@@ -1,247 +0,0 @@
-<template>
-<section class="_card">
-	<div class="_title"><i class="fas fa-lock"></i> {{ $ts.twoStepAuthentication }}</div>
-	<div class="_content">
-		<MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $ts._2fa.registerDevice }}</MkButton>
-		<template v-if="$i.twoFactorEnabled">
-			<p>{{ $ts._2fa.alreadyRegistered }}</p>
-			<MkButton @click="unregister">{{ $ts.unregister }}</MkButton>
-
-			<template v-if="supportsCredentials">
-				<hr class="totp-method-sep">
-
-				<h2 class="heading">{{ $ts.securityKey }}</h2>
-				<p>{{ $ts._2fa.securityKeyInfo }}</p>
-				<div class="key-list">
-					<div class="key" v-for="key in $i.securityKeysList">
-						<h3>{{ key.name }}</h3>
-						<div class="last-used">{{ $ts.lastUsed }}<MkTime :time="key.lastUsed"/></div>
-						<MkButton @click="unregisterKey(key)">{{ $ts.unregister }}</MkButton>
-					</div>
-				</div>
-
-				<MkSwitch v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin" v-if="$i.securityKeysList.length > 0">{{ $ts.passwordLessLogin }}</MkSwitch>
-
-				<MkInfo warn v-if="registration && registration.error">{{ $ts.error }} {{ registration.error }}</MkInfo>
-				<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $ts._2fa.registerKey }}</MkButton>
-
-				<ol v-if="registration && !registration.error">
-					<li v-if="registration.stage >= 0">
-						{{ $ts.tapSecurityKey }}
-						<i v-if="registration.saving && registration.stage == 0" class="fas fa-spinner fa-pulse fa-fw"></i>
-					</li>
-					<li v-if="registration.stage >= 1">
-						<MkForm :disabled="registration.stage != 1 || registration.saving">
-							<MkInput v-model="keyName" :max="30">
-								<template #label>{{ $ts.securityKeyName }}</template>
-							</MkInput>
-							<MkButton @click="registerKey" :disabled="keyName.length == 0">{{ $ts.registerSecurityKey }}</MkButton>
-							<i v-if="registration.saving && registration.stage == 1" class="fas fa-spinner fa-pulse fa-fw"></i>
-						</MkForm>
-					</li>
-				</ol>
-			</template>
-		</template>
-		<div v-if="data && !$i.twoFactorEnabled">
-			<ol style="margin: 0; padding: 0 0 0 1em;">
-				<li>
-					<I18n :src="$ts._2fa.step1" tag="span">
-						<template #a>
-							<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
-						</template>
-						<template #b>
-							<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
-						</template>
-					</I18n>
-				</li>
-				<li>{{ $ts._2fa.step2 }}<br><img :src="data.qr"></li>
-				<li>{{ $ts._2fa.step3 }}<br>
-					<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ $ts.token }}</template></MkInput>
-					<MkButton primary @click="submit">{{ $ts.done }}</MkButton>
-				</li>
-			</ol>
-			<MkInfo>{{ $ts._2fa.step4 }}</MkInfo>
-		</div>
-	</div>
-</section>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { hostname } from '@client/config';
-import { byteify, hexify, stringify } from '@client/scripts/2fa';
-import MkButton from '@client/components/ui/button.vue';
-import MkInfo from '@client/components/ui/info.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		MkButton, MkInfo, MkInput, MkSwitch
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.twoStepAuthentication,
-				icon: 'fas fa-lock'
-			},
-			data: null,
-			supportsCredentials: !!navigator.credentials,
-			usePasswordLessLogin: this.$i.usePasswordLessLogin,
-			registration: null,
-			keyName: '',
-			token: null,
-		};
-	},
-
-	methods: {
-		register() {
-			os.dialog({
-				title: this.$ts.password,
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				os.api('i/2fa/register', {
-					password: password
-				}).then(data => {
-					this.data = data;
-				});
-			});
-		},
-
-		unregister() {
-			os.dialog({
-				title: this.$ts.password,
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				os.api('i/2fa/unregister', {
-					password: password
-				}).then(() => {
-					this.usePasswordLessLogin = false;
-					this.updatePasswordLessLogin();
-				}).then(() => {
-					os.success();
-					this.$i.twoFactorEnabled = false;
-				});
-			});
-		},
-
-		submit() {
-			os.api('i/2fa/done', {
-				token: this.token
-			}).then(() => {
-				os.success();
-				this.$i.twoFactorEnabled = true;
-			}).catch(e => {
-				os.dialog({
-					type: 'error',
-					text: e
-				});
-			});
-		},
-
-		registerKey() {
-			this.registration.saving = true;
-			os.api('i/2fa/key-done', {
-				password: this.registration.password,
-				name: this.keyName,
-				challengeId: this.registration.challengeId,
-				// we convert each 16 bits to a string to serialise
-				clientDataJSON: stringify(this.registration.credential.response.clientDataJSON),
-				attestationObject: hexify(this.registration.credential.response.attestationObject)
-			}).then(key => {
-				this.registration = null;
-				key.lastUsed = new Date();
-				os.success();
-			})
-		},
-
-		unregisterKey(key) {
-			os.dialog({
-				title: this.$ts.password,
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				return os.api('i/2fa/remove-key', {
-					password,
-					credentialId: key.id
-				}).then(() => {
-					this.usePasswordLessLogin = false;
-					this.updatePasswordLessLogin();
-				}).then(() => {
-					os.success();
-				});
-			});
-		},
-
-		addSecurityKey() {
-			os.dialog({
-				title: this.$ts.password,
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				os.api('i/2fa/register-key', {
-					password
-				}).then(registration => {
-					this.registration = {
-						password,
-						challengeId: registration.challengeId,
-						stage: 0,
-						publicKeyOptions: {
-							challenge: byteify(registration.challenge, 'base64'),
-							rp: {
-								id: hostname,
-								name: 'Misskey'
-							},
-							user: {
-								id: byteify(this.$i.id, 'ascii'),
-								name: this.$i.username,
-								displayName: this.$i.name,
-							},
-							pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
-							timeout: 60000,
-							attestation: 'direct'
-						},
-						saving: true
-					};
-					return navigator.credentials.create({
-						publicKey: this.registration.publicKeyOptions
-					});
-				}).then(credential => {
-					this.registration.credential = credential;
-					this.registration.saving = false;
-					this.registration.stage = 1;
-				}).catch(err => {
-					console.warn('Error while registering?', err);
-					this.registration.error = err.message;
-					this.registration.stage = -1;
-				});
-			});
-		},
-
-		updatePasswordLessLogin() {
-			os.api('i/2fa/password-less', {
-				value: !!this.usePasswordLessLogin
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/account-info.vue b/src/client/pages/settings/account-info.vue
deleted file mode 100644
index 16ce91b12f..0000000000
--- a/src/client/pages/settings/account-info.vue
+++ /dev/null
@@ -1,185 +0,0 @@
-<template>
-<FormBase>
-	<FormKeyValueView>
-		<template #key>ID</template>
-		<template #value><span class="_monospace">{{ $i.id }}</span></template>
-	</FormKeyValueView>
-
-	<FormGroup>
-		<FormKeyValueView>
-			<template #key>{{ $ts.registeredDate }}</template>
-			<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
-		</FormKeyValueView>
-	</FormGroup>
-
-	<FormGroup v-if="stats">
-		<template #label>{{ $ts.statistics }}</template>
-		<FormKeyValueView>
-			<template #key>{{ $ts.notesCount }}</template>
-			<template #value>{{ number(stats.notesCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.repliesCount }}</template>
-			<template #value>{{ number(stats.repliesCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.renotesCount }}</template>
-			<template #value>{{ number(stats.renotesCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.repliedCount }}</template>
-			<template #value>{{ number(stats.repliedCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.renotedCount }}</template>
-			<template #value>{{ number(stats.renotedCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.pollVotesCount }}</template>
-			<template #value>{{ number(stats.pollVotesCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.pollVotedCount }}</template>
-			<template #value>{{ number(stats.pollVotedCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.sentReactionsCount }}</template>
-			<template #value>{{ number(stats.sentReactionsCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.receivedReactionsCount }}</template>
-			<template #value>{{ number(stats.receivedReactionsCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.noteFavoritesCount }}</template>
-			<template #value>{{ number(stats.noteFavoritesCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.followingCount }}</template>
-			<template #value>{{ number(stats.followingCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template>
-			<template #value>{{ number(stats.localFollowingCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template>
-			<template #value>{{ number(stats.remoteFollowingCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.followersCount }}</template>
-			<template #value>{{ number(stats.followersCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template>
-			<template #value>{{ number(stats.localFollowersCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template>
-			<template #value>{{ number(stats.remoteFollowersCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.pageLikesCount }}</template>
-			<template #value>{{ number(stats.pageLikesCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.pageLikedCount }}</template>
-			<template #value>{{ number(stats.pageLikedCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.driveFilesCount }}</template>
-			<template #value>{{ number(stats.driveFilesCount) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.driveUsage }}</template>
-			<template #value>{{ bytes(stats.driveUsage) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.reversiCount }}</template>
-			<template #value>{{ number(stats.reversiCount) }}</template>
-		</FormKeyValueView>
-	</FormGroup>
-
-	<FormGroup>
-		<template #label>{{ $ts.other }}</template>
-		<FormKeyValueView>
-			<template #key>emailVerified</template>
-			<template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>twoFactorEnabled</template>
-			<template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>securityKeys</template>
-			<template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>usePasswordLessLogin</template>
-			<template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>isModerator</template>
-			<template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>isAdmin</template>
-			<template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template>
-		</FormKeyValueView>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-import bytes from '@client/filters/bytes';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSelect,
-		FormSwitch,
-		FormButton,
-		FormLink,
-		FormGroup,
-		FormKeyValueView,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.accountInfo,
-				icon: 'fas fa-info-circle'
-			},
-			stats: null
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
-		os.api('users/stats', {
-			userId: this.$i.id
-		}).then(stats => {
-			this.stats = stats;
-		});
-	},
-
-	methods: {
-		number,
-		bytes,
-	}
-});
-</script>
diff --git a/src/client/pages/settings/accounts.vue b/src/client/pages/settings/accounts.vue
deleted file mode 100644
index d2966cc216..0000000000
--- a/src/client/pages/settings/accounts.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<FormButton @click="addAccount" primary><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton>
-
-		<div class="_debobigegoItem _button" v-for="account in accounts" :key="account.id" @click="menu(account, $event)">
-			<div class="_debobigegoPanel lcjjdxlm">
-				<div class="avatar">
-					<MkAvatar :user="account" class="avatar"/>
-				</div>
-				<div class="body">
-					<div class="name">
-						<MkUserName :user="account"/>
-					</div>
-					<div class="acct">
-						<MkAcct :user="account"/>
-					</div>
-				</div>
-			</div>
-		</div>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { getAccounts, addAccount, login } from '@client/account';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSuspense,
-		FormButton,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.accounts,
-				icon: 'fas fa-users',
-				bg: 'var(--bg)',
-			},
-			storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)),
-			accounts: null,
-			init: async () => os.api('users/show', {
-				userIds: (await this.storedAccounts).map(x => x.id)
-			}).then(accounts => {
-				this.accounts = accounts;
-			}),
-		};
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		menu(account, ev) {
-			os.popupMenu([{
-				text: this.$ts.switch,
-				icon: 'fas fa-exchange-alt',
-				action: () => this.switchAccount(account),
-			}, {
-				text: this.$ts.remove,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: () => this.removeAccount(account),
-			}], ev.currentTarget || ev.target);
-		},
-
-		addAccount(ev) {
-			os.popupMenu([{
-				text: this.$ts.existingAccount,
-				action: () => { this.addExistingAccount(); },
-			}, {
-				text: this.$ts.createAccount,
-				action: () => { this.createAccount(); },
-			}], ev.currentTarget || ev.target);
-		},
-
-		addExistingAccount() {
-			os.popup(import('@client/components/signin-dialog.vue'), {}, {
-				done: res => {
-					addAccount(res.id, res.i);
-					os.success();
-				},
-			}, 'closed');
-		},
-
-		createAccount() {
-			os.popup(import('@client/components/signup-dialog.vue'), {}, {
-				done: res => {
-					addAccount(res.id, res.i);
-					this.switchAccountWithToken(res.i);
-				},
-			}, 'closed');
-		},
-
-		async switchAccount(account: any) {
-			const storedAccounts = await getAccounts();
-			const token = storedAccounts.find(x => x.id === account.id).token;
-			this.switchAccountWithToken(token);
-		},
-
-		switchAccountWithToken(token: string) {
-			login(token);
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.lcjjdxlm {
-	display: flex;
-	padding: 16px;
-
-	> .avatar {
-		display: block;
-		flex-shrink: 0;
-		margin: 0 12px 0 0;
-
-		> .avatar {
-			width: 50px;
-			height: 50px;
-		}
-	}
-
-	> .body {
-		display: flex;
-		flex-direction: column;
-		justify-content: center;
-		width: calc(100% - 62px);
-		position: relative;
-
-		> .name {
-			font-weight: bold;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/settings/api.vue b/src/client/pages/settings/api.vue
deleted file mode 100644
index 5c7496e2f9..0000000000
--- a/src/client/pages/settings/api.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<FormBase>
-	<FormButton @click="generateToken" primary>{{ $ts.generateAccessToken }}</FormButton>
-	<FormLink to="/settings/apps">{{ $ts.manageAccessTokens }}</FormLink>
-	<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		FormLink,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'API',
-				icon: 'fas fa-key',
-				bg: 'var(--bg)',
-			},
-			isDesktop: window.innerWidth >= 1100,
-		};
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		generateToken() {
-			os.popup(import('@client/components/token-generate-window.vue'), {}, {
-				done: async result => {
-					const { name, permissions } = result;
-					const { token } = await os.api('miauth/gen-token', {
-						session: null,
-						name: name,
-						permission: permissions,
-					});
-
-					os.dialog({
-						type: 'success',
-						title: this.$ts.token,
-						text: token
-					});
-				},
-			}, 'closed');
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/apps.vue b/src/client/pages/settings/apps.vue
deleted file mode 100644
index da4f672adf..0000000000
--- a/src/client/pages/settings/apps.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<template>
-<FormBase>
-	<FormPagination :pagination="pagination" ref="list">
-		<template #empty>
-			<div class="_fullinfo">
-				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
-				<div>{{ $ts.nothing }}</div>
-			</div>
-		</template>
-		<template #default="{items}">
-			<div class="_debobigegoPanel bfomjevm" v-for="token in items" :key="token.id">
-				<img class="icon" :src="token.iconUrl" alt="" v-if="token.iconUrl"/>
-				<div class="body">
-					<div class="name">{{ token.name }}</div>
-					<div class="description">{{ token.description }}</div>
-					<div class="_keyValue">
-						<div>{{ $ts.installedDate }}:</div>
-						<div><MkTime :time="token.createdAt"/></div>
-					</div>
-					<div class="_keyValue">
-						<div>{{ $ts.lastUsedDate }}:</div>
-						<div><MkTime :time="token.lastUsedAt"/></div>
-					</div>
-					<div class="actions">
-						<button class="_button" @click="revoke(token)"><i class="fas fa-trash-alt"></i></button>
-					</div>
-					<details>
-						<summary>{{ $ts.details }}</summary>
-						<ul>
-							<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
-						</ul>
-					</details>
-				</div>
-			</div>
-		</template>
-	</FormPagination>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormPagination from '@client/components/debobigego/pagination.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormPagination,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.installedApps,
-				icon: 'fas fa-plug',
-				bg: 'var(--bg)',
-			},
-			pagination: {
-				endpoint: 'i/apps',
-				limit: 100,
-				params: {
-					sort: '+lastUsedAt'
-				}
-			},
-		};
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		revoke(token) {
-			os.api('i/revoke-token', { tokenId: token.id }).then(() => {
-				this.$refs.list.reload();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.bfomjevm {
-	display: flex;
-	padding: 16px;
-
-	> .icon {
-		display: block;
-		flex-shrink: 0;
-		margin: 0 12px 0 0;
-		width: 50px;
-		height: 50px;
-		border-radius: 8px;
-	}
-
-	> .body {
-		width: calc(100% - 62px);
-		position: relative;
-
-		> .name {
-			font-weight: bold;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/settings/custom-css.vue b/src/client/pages/settings/custom-css.vue
deleted file mode 100644
index fd473a11fa..0000000000
--- a/src/client/pages/settings/custom-css.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<template>
-<FormBase>
-	<FormInfo warn>{{ $ts.customCssWarn }}</FormInfo>
-
-	<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;">
-		<span>{{ $ts.local }}</span>
-	</FormTextarea>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import { unisonReload } from '@client/scripts/unison-reload';
-import * as symbols from '@client/symbols';
-import { defaultStore } from '@client/store';
-
-export default defineComponent({
-	components: {
-		FormTextarea,
-		FormSelect,
-		FormRadios,
-		FormBase,
-		FormGroup,
-		FormLink,
-		FormButton,
-		FormInfo,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.customCss,
-				icon: 'fas fa-code',
-				bg: 'var(--bg)',
-			},
-			localCustomCss: localStorage.getItem('customCss')
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
-		this.$watch('localCustomCss', this.apply);
-	},
-
-	methods: {
-		async apply() {
-			localStorage.setItem('customCss', this.localCustomCss);
-
-			const { canceled } = await os.dialog({
-				type: 'info',
-				text: this.$ts.reloadToApplySetting,
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			unisonReload();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/deck.vue b/src/client/pages/settings/deck.vue
deleted file mode 100644
index e4b5c697c4..0000000000
--- a/src/client/pages/settings/deck.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
-		<FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch>
-	</FormGroup>
-
-	<FormSwitch v-model="alwaysShowMainColumn">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
-
-	<FormRadios v-model="columnAlign">
-		<template #desc>{{ $ts._deck.columnAlign }}</template>
-		<option value="left">{{ $ts.left }}</option>
-		<option value="center">{{ $ts.center }}</option>
-	</FormRadios>
-
-	<FormRadios v-model="columnHeaderHeight">
-		<template #desc>{{ $ts._deck.columnHeaderHeight }}</template>
-		<option :value="42">{{ $ts.narrow }}</option>
-		<option :value="45">{{ $ts.medium }}</option>
-		<option :value="48">{{ $ts.wide }}</option>
-	</FormRadios>
-
-	<FormInput v-model="columnMargin" type="number">
-		<span>{{ $ts._deck.columnMargin }}</span>
-		<template #suffix>px</template>
-	</FormInput>
-
-	<FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormRadios from '@client/components/debobigego/radios.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import { deckStore } from '@client/ui/deck/deck-store';
-import * as os from '@client/os';
-import { unisonReload } from '@client/scripts/unison-reload';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormLink,
-		FormInput,
-		FormRadios,
-		FormBase,
-		FormGroup,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.deck,
-				icon: 'fas fa-columns',
-				bg: 'var(--bg)',
-			},
-		}
-	},
-
-	computed: {
-		navWindow: deckStore.makeGetterSetter('navWindow'),
-		alwaysShowMainColumn: deckStore.makeGetterSetter('alwaysShowMainColumn'),
-		columnAlign: deckStore.makeGetterSetter('columnAlign'),
-		columnMargin: deckStore.makeGetterSetter('columnMargin'),
-		columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'),
-		profile: deckStore.makeGetterSetter('profile'),
-	},
-
-	watch: {
-		async navWindow() {
-			const { canceled } = await os.dialog({
-				type: 'info',
-				text: this.$ts.reloadToApplySetting,
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			unisonReload();
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async setProfile() {
-			const { canceled, result: name } = await os.dialog({
-				title: this.$ts._deck.profile,
-				input: {
-					allowEmpty: false
-				}
-			});
-			if (canceled) return;
-			this.profile = name;
-			unisonReload();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/delete-account.vue b/src/client/pages/settings/delete-account.vue
deleted file mode 100644
index 6bac214e04..0000000000
--- a/src/client/pages/settings/delete-account.vue
+++ /dev/null
@@ -1,68 +0,0 @@
-<template>
-<FormBase>
-	<FormInfo warn>{{ $ts._accountDelete.mayTakeTime }}</FormInfo>
-	<FormInfo>{{ $ts._accountDelete.sendEmail }}</FormInfo>
-	<FormButton @click="deleteAccount" danger v-if="!$i.isDeleted">{{ $ts._accountDelete.requestAccountDelete }}</FormButton>
-	<FormButton disabled v-else>{{ $ts._accountDelete.inProgress }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import { debug } from '@client/config';
-import { signout } from '@client/account';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		FormGroup,
-		FormInfo,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._accountDelete.accountDelete,
-				icon: 'fas fa-exclamation-triangle',
-				bg: 'var(--bg)',
-			},
-			debug,
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async deleteAccount() {
-			const { canceled, result: password } = await os.dialog({
-				title: this.$ts.password,
-				input: {
-					type: 'password'
-				}
-			});
-			if (canceled) return;
-
-			await os.apiWithDialog('i/delete-account', {
-				password: password
-			});
-
-			await os.dialog({
-				title: this.$ts._accountDelete.started,
-			});
-
-			signout();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/drive.vue b/src/client/pages/settings/drive.vue
deleted file mode 100644
index 2d73eb4df7..0000000000
--- a/src/client/pages/settings/drive.vue
+++ /dev/null
@@ -1,147 +0,0 @@
-<template>
-<FormBase class="">
-	<FormGroup v-if="!fetching">
-		<template #label>{{ $ts.usageAmount }}</template>
-		<div class="_debobigegoItem uawsfosz">
-			<div class="_debobigegoPanel">
-				<div class="meter"><div :style="meterStyle"></div></div>
-			</div>
-		</div>
-		<FormKeyValueView>
-			<template #key>{{ $ts.capacity }}</template>
-			<template #value>{{ bytes(capacity, 1) }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.inUse }}</template>
-			<template #value>{{ bytes(usage, 1) }}</template>
-		</FormKeyValueView>
-	</FormGroup>
-
-	<div class="_debobigegoItem">
-		<div class="_debobigegoLabel">{{ $ts.statistics }}</div>
-		<div class="_debobigegoPanel">
-			<div ref="chart"></div>
-		</div>
-	</div>
-
-	<FormButton :center="false" @click="chooseUploadFolder()" primary>
-		{{ $ts.uploadFolder }}
-		<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
-		<template #suffixIcon><i class="fas fa-folder-open"></i></template>
-	</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as tinycolor from 'tinycolor2';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import * as os from '@client/os';
-import bytes from '@client/filters/bytes';
-import * as symbols from '@client/symbols';
-
-// TODO: render chart
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		FormGroup,
-		FormKeyValueView,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.drive,
-				icon: 'fas fa-cloud',
-				bg: 'var(--bg)',
-			},
-			fetching: true,
-			usage: null,
-			capacity: null,
-			uploadFolder: null,
-		}
-	},
-
-	computed: {
-		meterStyle(): any {
-			return {
-				width: `${this.usage / this.capacity * 100}%`,
-				background: tinycolor({
-					h: 180 - (this.usage / this.capacity * 180),
-					s: 0.7,
-					l: 0.5
-				})
-			};
-		}
-	},
-
-	async created() {
-		os.api('drive').then(info => {
-			this.capacity = info.capacity;
-			this.usage = info.usage;
-			this.fetching = false;
-			this.$nextTick(() => {
-				this.renderChart();
-			});
-		});
-
-		if (this.$store.state.uploadFolder) {
-			this.uploadFolder = await os.api('drive/folders/show', {
-				folderId: this.$store.state.uploadFolder
-			});
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		chooseUploadFolder() {
-			os.selectDriveFolder(false).then(async folder => {
-				this.$store.set('uploadFolder', folder ? folder.id : null);
-				os.success();
-				if (this.$store.state.uploadFolder) {
-					this.uploadFolder = await os.api('drive/folders/show', {
-						folderId: this.$store.state.uploadFolder
-					});
-				} else {
-					this.uploadFolder = null;
-				}
-			});
-		},
-
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-@use "sass:math";
-
-.uawsfosz {
-	> div {
-		padding: 24px;
-
-		> .meter {
-			$size: 12px;
-			background: rgba(0, 0, 0, 0.1);
-			border-radius: math.div($size, 2);
-			overflow: hidden;
-
-			> div {
-				height: $size;
-				border-radius: math.div($size, 2);
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/settings/email-address.vue b/src/client/pages/settings/email-address.vue
deleted file mode 100644
index f98b22ada7..0000000000
--- a/src/client/pages/settings/email-address.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<FormInput v-model="emailAddress" type="email">
-			{{ $ts.emailAddress }}
-			<template #desc v-if="$i.email && !$i.emailVerified">{{ $ts.verificationEmailSent }}</template>
-			<template #desc v-else-if="emailAddress === $i.email && $i.emailVerified">{{ $ts.emailVerified }}</template>
-		</FormInput>
-	</FormGroup>
-	<FormButton @click="save" primary>{{ $ts.save }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormInput,
-		FormButton,
-		FormGroup,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.emailAddress,
-				icon: 'fas fa-envelope',
-				bg: 'var(--bg)',
-			},
-			emailAddress: null,
-			code: null,
-		}
-	},
-
-	created() {
-		this.emailAddress = this.$i.email;
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		save() {
-			os.dialog({
-				title: this.$ts.password,
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				os.apiWithDialog('i/update-email', {
-					password: password,
-					email: this.emailAddress,
-				});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/email-notification.vue b/src/client/pages/settings/email-notification.vue
deleted file mode 100644
index 1b78621c3f..0000000000
--- a/src/client/pages/settings/email-notification.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<FormSwitch v-model="mention">
-			{{ $ts._notification._types.mention }}
-		</FormSwitch>
-		<FormSwitch v-model="reply">
-			{{ $ts._notification._types.reply }}
-		</FormSwitch>
-		<FormSwitch v-model="quote">
-			{{ $ts._notification._types.quote }}
-		</FormSwitch>
-		<FormSwitch v-model="follow">
-			{{ $ts._notification._types.follow }}
-		</FormSwitch>
-		<FormSwitch v-model="receiveFollowRequest">
-			{{ $ts._notification._types.receiveFollowRequest }}
-		</FormSwitch>
-		<FormSwitch v-model="groupInvited">
-			{{ $ts._notification._types.groupInvited }}
-		</FormSwitch>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSwitch,
-		FormButton,
-		FormGroup,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.emailNotification,
-				icon: 'fas fa-envelope',
-				bg: 'var(--bg)',
-			},
-
-			mention: this.$i.emailNotificationTypes.includes('mention'),
-			reply: this.$i.emailNotificationTypes.includes('reply'),
-			quote: this.$i.emailNotificationTypes.includes('quote'),
-			follow: this.$i.emailNotificationTypes.includes('follow'),
-			receiveFollowRequest: this.$i.emailNotificationTypes.includes('receiveFollowRequest'),
-			groupInvited: this.$i.emailNotificationTypes.includes('groupInvited'),
-		}
-	},
-
-	created() {
-		this.$watch('mention', this.save);
-		this.$watch('reply', this.save);
-		this.$watch('quote', this.save);
-		this.$watch('follow', this.save);
-		this.$watch('receiveFollowRequest', this.save);
-		this.$watch('groupInvited', this.save);
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		save() {
-			os.api('i/update', {
-				emailNotificationTypes: [
-					...[this.mention ? 'mention' : null],
-					...[this.reply ? 'reply' : null],
-					...[this.quote ? 'quote' : null],
-					...[this.follow ? 'follow' : null],
-					...[this.receiveFollowRequest ? 'receiveFollowRequest' : null],
-					...[this.groupInvited ? 'groupInvited' : null],
-				].filter(x => x != null)
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/email.vue b/src/client/pages/settings/email.vue
deleted file mode 100644
index adc62133ac..0000000000
--- a/src/client/pages/settings/email.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<template #label>{{ $ts.emailAddress }}</template>
-		<FormLink to="/settings/email/address">
-			<template v-if="$i.email && !$i.emailVerified" #icon><i class="fas fa-exclamation-triangle" style="color: var(--warn);"></i></template>
-			<template v-else-if="$i.email && $i.emailVerified" #icon><i class="fas fa-check" style="color: var(--success);"></i></template>
-			{{ $i.email || $ts.notSet }}
-		</FormLink>
-	</FormGroup>
-
-	<FormLink to="/settings/email/notification">
-		<template #icon><i class="fas fa-bell"></i></template>
-		{{ $ts.emailNotification }}
-	</FormLink>
-
-	<FormSwitch :value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
-		{{ $ts.receiveAnnouncementFromInstance }}
-	</FormSwitch>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormLink,
-		FormButton,
-		FormSwitch,
-		FormGroup,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.email,
-				icon: 'fas fa-envelope',
-				bg: 'var(--bg)',
-			},
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		onChangeReceiveAnnouncementEmail(v) {
-			os.api('i/update', {
-				receiveAnnouncementEmail: v
-			});
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/experimental-features.vue b/src/client/pages/settings/experimental-features.vue
deleted file mode 100644
index 971c45a628..0000000000
--- a/src/client/pages/settings/experimental-features.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
-<FormBase>
-	<FormButton @click="error()">error test</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSelect,
-		FormSwitch,
-		FormButton,
-		FormLink,
-		FormGroup,
-		FormKeyValueView,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.experimentalFeatures,
-				icon: 'fas fa-flask'
-			},
-			stats: null
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		error() {
-			throw new Error('Test error');
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue
deleted file mode 100644
index 59dd251948..0000000000
--- a/src/client/pages/settings/general.vue
+++ /dev/null
@@ -1,223 +0,0 @@
-<template>
-<FormBase>
-	<FormSwitch v-model="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch>
-
-	<FormSelect v-model="lang">
-		<template #label>{{ $ts.uiLanguage }}</template>
-		<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
-		<template #caption>
-			<I18n :src="$ts.i18nInfo" tag="span">
-				<template #link>
-					<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
-				</template>
-			</I18n>
-		</template>
-	</FormSelect>
-
-	<FormGroup>
-		<template #label>{{ $ts.behavior }}</template>
-		<FormSwitch v-model="imageNewTab">{{ $ts.openImageInNewTab }}</FormSwitch>
-		<FormSwitch v-model="enableInfiniteScroll">{{ $ts.enableInfiniteScroll }}</FormSwitch>
-		<FormSwitch v-model="useReactionPickerForContextMenu">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch>
-		<FormSwitch v-model="disablePagesScript">{{ $ts.disablePagesScript }}</FormSwitch>
-	</FormGroup>
-
-	<FormSelect v-model="serverDisconnectedBehavior">
-		<template #label>{{ $ts.whenServerDisconnected }}</template>
-		<option value="reload">{{ $ts._serverDisconnectedBehavior.reload }}</option>
-		<option value="dialog">{{ $ts._serverDisconnectedBehavior.dialog }}</option>
-		<option value="quiet">{{ $ts._serverDisconnectedBehavior.quiet }}</option>
-	</FormSelect>
-
-	<FormGroup>
-		<template #label>{{ $ts.appearance }}</template>
-		<FormSwitch v-model="disableAnimatedMfm">{{ $ts.disableAnimatedMfm }}</FormSwitch>
-		<FormSwitch v-model="reduceAnimation">{{ $ts.reduceUiAnimation }}</FormSwitch>
-		<FormSwitch v-model="useBlurEffect">{{ $ts.useBlurEffect }}</FormSwitch>
-		<FormSwitch v-model="useBlurEffectForModal">{{ $ts.useBlurEffectForModal }}</FormSwitch>
-		<FormSwitch v-model="showGapBetweenNotesInTimeline">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch>
-		<FormSwitch v-model="loadRawImages">{{ $ts.loadRawImages }}</FormSwitch>
-		<FormSwitch v-model="disableShowingAnimatedImages">{{ $ts.disableShowingAnimatedImages }}</FormSwitch>
-		<FormSwitch v-model="squareAvatars">{{ $ts.squareAvatars }}</FormSwitch>
-		<FormSwitch v-model="useSystemFont">{{ $ts.useSystemFont }}</FormSwitch>
-		<FormSwitch v-model="useOsNativeEmojis">{{ $ts.useOsNativeEmojis }}
-			<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪" :key="useOsNativeEmojis"/></div>
-		</FormSwitch>
-	</FormGroup>
-
-	<FormGroup>
-		<FormSwitch v-model="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch>
-	</FormGroup>
-
-	<FormRadios v-model="fontSize">
-		<template #desc>{{ $ts.fontSize }}</template>
-		<option value="small"><span style="font-size: 14px;">Aa</span></option>
-		<option :value="null"><span style="font-size: 16px;">Aa</span></option>
-		<option value="large"><span style="font-size: 18px;">Aa</span></option>
-		<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
-	</FormRadios>
-
-	<FormSelect v-model="instanceTicker">
-		<template #label>{{ $ts.instanceTicker }}</template>
-		<option value="none">{{ $ts._instanceTicker.none }}</option>
-		<option value="remote">{{ $ts._instanceTicker.remote }}</option>
-		<option value="always">{{ $ts._instanceTicker.always }}</option>
-	</FormSelect>
-
-	<FormSelect v-model="nsfw">
-		<template #label>{{ $ts.nsfw }}</template>
-		<option value="respect">{{ $ts._nsfw.respect }}</option>
-		<option value="ignore">{{ $ts._nsfw.ignore }}</option>
-		<option value="force">{{ $ts._nsfw.force }}</option>
-	</FormSelect>
-
-	<FormGroup>
-		<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
-		<FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch>
-	</FormGroup>
-
-	<FormSelect v-model="chatOpenBehavior">
-		<template #label>{{ $ts.chatOpenBehavior }}</template>
-		<option value="page">{{ $ts.showInPage }}</option>
-		<option value="window">{{ $ts.openInWindow }}</option>
-		<option value="popout">{{ $ts.popout }}</option>
-	</FormSelect>
-
-	<FormLink to="/settings/deck">{{ $ts.deck }}</FormLink>
-
-	<FormLink to="/settings/custom-css"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormSelect from '@client/components/debobigego/select.vue';
-import FormRadios from '@client/components/debobigego/radios.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import MkLink from '@client/components/link.vue';
-import { langs } from '@client/config';
-import { defaultStore } from '@client/store';
-import { ColdDeviceStorage } from '@client/store';
-import * as os from '@client/os';
-import { unisonReload } from '@client/scripts/unison-reload';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkLink,
-		FormSwitch,
-		FormSelect,
-		FormRadios,
-		FormBase,
-		FormGroup,
-		FormLink,
-		FormButton,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.general,
-				icon: 'fas fa-cogs',
-				bg: 'var(--bg)'
-			},
-			langs,
-			lang: localStorage.getItem('lang'),
-			fontSize: localStorage.getItem('fontSize'),
-			useSystemFont: localStorage.getItem('useSystemFont') != null,
-		}
-	},
-
-	computed: {
-		serverDisconnectedBehavior: defaultStore.makeGetterSetter('serverDisconnectedBehavior'),
-		reduceAnimation: defaultStore.makeGetterSetter('animation', v => !v, v => !v),
-		useBlurEffectForModal: defaultStore.makeGetterSetter('useBlurEffectForModal'),
-		useBlurEffect: defaultStore.makeGetterSetter('useBlurEffect'),
-		showGapBetweenNotesInTimeline: defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'),
-		disableAnimatedMfm: defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v),
-		useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'),
-		disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'),
-		loadRawImages: defaultStore.makeGetterSetter('loadRawImages'),
-		imageNewTab: defaultStore.makeGetterSetter('imageNewTab'),
-		nsfw: defaultStore.makeGetterSetter('nsfw'),
-		disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'),
-		showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'),
-		defaultSideView: defaultStore.makeGetterSetter('defaultSideView'),
-		chatOpenBehavior: ColdDeviceStorage.makeGetterSetter('chatOpenBehavior'),
-		instanceTicker: defaultStore.makeGetterSetter('instanceTicker'),
-		enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
-		useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'),
-		squareAvatars: defaultStore.makeGetterSetter('squareAvatars'),
-		aiChanMode: defaultStore.makeGetterSetter('aiChanMode'),
-	},
-
-	watch: {
-		lang() {
-			localStorage.setItem('lang', this.lang);
-			localStorage.removeItem('locale');
-			this.reloadAsk();
-		},
-
-		fontSize() {
-			if (this.fontSize == null) {
-				localStorage.removeItem('fontSize');
-			} else {
-				localStorage.setItem('fontSize', this.fontSize);
-			}
-			this.reloadAsk();
-		},
-
-		useSystemFont() {
-			if (this.useSystemFont) {
-				localStorage.setItem('useSystemFont', 't');
-			} else {
-				localStorage.removeItem('useSystemFont');
-			}
-			this.reloadAsk();
-		},
-
-		enableInfiniteScroll() {
-			this.reloadAsk();
-		},
-
-		squareAvatars() {
-			this.reloadAsk();
-		},
-
-		aiChanMode() {
-			this.reloadAsk();
-		},
-
-		showGapBetweenNotesInTimeline() {
-			this.reloadAsk();
-		},
-
-		instanceTicker() {
-			this.reloadAsk();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async reloadAsk() {
-			const { canceled } = await os.dialog({
-				type: 'info',
-				text: this.$ts.reloadToApplySetting,
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			unisonReload();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue
deleted file mode 100644
index eeaa1f1602..0000000000
--- a/src/client/pages/settings/import-export.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<template>
-<div style="margin: 16px;">
-	<FormSection>
-		<template #label>{{ $ts._exportOrImport.allNotes }}</template>
-		<MkButton :class="$style.button" inline @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
-	</FormSection>
-	<FormSection>
-		<template #label>{{ $ts._exportOrImport.followingList }}</template>
-		<MkButton :class="$style.button" inline @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
-		<MkButton :class="$style.button" inline @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
-	</FormSection>
-	<FormSection>
-		<template #label>{{ $ts._exportOrImport.userLists }}</template>
-		<MkButton :class="$style.button" inline @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
-		<MkButton :class="$style.button" inline @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
-	</FormSection>
-	<FormSection>
-		<template #label>{{ $ts._exportOrImport.muteList }}</template>
-		<MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
-		<MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
-	</FormSection>
-	<FormSection>
-		<template #label>{{ $ts._exportOrImport.blockingList }}</template>
-		<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
-		<MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
-	</FormSection>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import FormSection from '@client/components/form/section.vue';
-import * as os from '@client/os';
-import { selectFile } from '@client/scripts/select-file';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormSection,
-		MkButton,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.importAndExport,
-				icon: 'fas fa-boxes',
-				bg: 'var(--bg)',
-			},
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		doExport(target) {
-			os.api(
-				target === 'notes' ? 'i/export-notes' :
-				target === 'following' ? 'i/export-following' :
-				target === 'blocking' ? 'i/export-blocking' :
-				target === 'user-lists' ? 'i/export-user-lists' :
-				target === 'muting' ? 'i/export-mute' :
-				null, {})
-			.then(() => {
-				os.dialog({
-					type: 'info',
-					text: this.$ts.exportRequested
-				});
-			}).catch((e: any) => {
-				os.dialog({
-					type: 'error',
-					text: e.message
-				});
-			});
-		},
-
-		async doImport(target, e) {
-			const file = await selectFile(e.currentTarget || e.target);
-			
-			os.api(
-				target === 'following' ? 'i/import-following' :
-				target === 'user-lists' ? 'i/import-user-lists' :
-				target === 'muting' ? 'i/import-muting' :
-				target === 'blocking' ? 'i/import-blocking' :
-				null, {
-					fileId: file.id
-			}).then(() => {
-				os.dialog({
-					type: 'info',
-					text: this.$ts.importRequested
-				});
-			}).catch((e: any) => {
-				os.dialog({
-					type: 'error',
-					text: e.message
-				});
-			});
-		},
-	}
-});
-</script>
-
-<style module>
-.button {
-	margin-right: 16px;
-}
-</style>
diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue
deleted file mode 100644
index cf053dbe63..0000000000
--- a/src/client/pages/settings/index.vue
+++ /dev/null
@@ -1,326 +0,0 @@
-<template>
-<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
-	<div class="nav" v-if="!narrow || page == null">
-		<MkSpacer :content-max="700">
-			<div class="baaadecd">
-				<div class="title">{{ $ts.settings }}</div>
-				<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
-				<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
-			</div>
-		</MkSpacer>
-	</div>
-	<div class="main">
-		<component :is="component" :key="page" v-bind="pageProps"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
-import { i18n } from '@client/i18n';
-import MkInfo from '@client/components/ui/info.vue';
-import MkSuperMenu from '@client/components/ui/super-menu.vue';
-import { scroll } from '@client/scripts/scroll';
-import { signout } from '@client/account';
-import { unisonReload } from '@client/scripts/unison-reload';
-import * as symbols from '@client/symbols';
-import { instance } from '@client/instance';
-import { $i } from '@client/account';
-
-export default defineComponent({
-	components: {
-		MkInfo,
-		MkSuperMenu,
-	},
-
-	props: {
-		initialPage: {
-			type: String,
-			required: false
-		}
-	},
-
-	setup(props, context) {
-		const indexInfo = {
-			title: i18n.locale.settings,
-			icon: 'fas fa-cog',
-			bg: 'var(--bg)',
-			hideHeader: true,
-		};
-		const INFO = ref(indexInfo);
-		const page = ref(props.initialPage);
-		const narrow = ref(false);
-		const view = ref(null);
-		const el = ref(null);
-		const menuDef = computed(() => [{
-			title: i18n.locale.basicSettings,
-			items: [{
-				icon: 'fas fa-user',
-				text: i18n.locale.profile,
-				to: '/settings/profile',
-				active: page.value === 'profile',
-			}, {
-				icon: 'fas fa-lock-open',
-				text: i18n.locale.privacy,
-				to: '/settings/privacy',
-				active: page.value === 'privacy',
-			}, {
-				icon: 'fas fa-laugh',
-				text: i18n.locale.reaction,
-				to: '/settings/reaction',
-				active: page.value === 'reaction',
-			}, {
-				icon: 'fas fa-cloud',
-				text: i18n.locale.drive,
-				to: '/settings/drive',
-				active: page.value === 'drive',
-			}, {
-				icon: 'fas fa-bell',
-				text: i18n.locale.notifications,
-				to: '/settings/notifications',
-				active: page.value === 'notifications',
-			}, {
-				icon: 'fas fa-envelope',
-				text: i18n.locale.email,
-				to: '/settings/email',
-				active: page.value === 'email',
-			}, {
-				icon: 'fas fa-share-alt',
-				text: i18n.locale.integration,
-				to: '/settings/integration',
-				active: page.value === 'integration',
-			}, {
-				icon: 'fas fa-lock',
-				text: i18n.locale.security,
-				to: '/settings/security',
-				active: page.value === 'security',
-			}],
-		}, {
-			title: i18n.locale.clientSettings,
-			items: [{
-				icon: 'fas fa-cogs',
-				text: i18n.locale.general,
-				to: '/settings/general',
-				active: page.value === 'general',
-			}, {
-				icon: 'fas fa-palette',
-				text: i18n.locale.theme,
-				to: '/settings/theme',
-				active: page.value === 'theme',
-			}, {
-				icon: 'fas fa-list-ul',
-				text: i18n.locale.menu,
-				to: '/settings/menu',
-				active: page.value === 'menu',
-			}, {
-				icon: 'fas fa-music',
-				text: i18n.locale.sounds,
-				to: '/settings/sounds',
-				active: page.value === 'sounds',
-			}, {
-				icon: 'fas fa-plug',
-				text: i18n.locale.plugins,
-				to: '/settings/plugin',
-				active: page.value === 'plugin',
-			}],
-		}, {
-			title: i18n.locale.otherSettings,
-			items: [{
-				icon: 'fas fa-boxes',
-				text: i18n.locale.importAndExport,
-				to: '/settings/import-export',
-				active: page.value === 'import-export',
-			}, {
-				icon: 'fas fa-ban',
-				text: i18n.locale.muteAndBlock,
-				to: '/settings/mute-block',
-				active: page.value === 'mute-block',
-			}, {
-				icon: 'fas fa-comment-slash',
-				text: i18n.locale.wordMute,
-				to: '/settings/word-mute',
-				active: page.value === 'word-mute',
-			}, {
-				icon: 'fas fa-key',
-				text: 'API',
-				to: '/settings/api',
-				active: page.value === 'api',
-			}, {
-				icon: 'fas fa-ellipsis-h',
-				text: i18n.locale.other,
-				to: '/settings/other',
-				active: page.value === 'other',
-			}],
-		}, {
-			items: [{
-				type: 'button',
-				icon: 'fas fa-trash',
-				text: i18n.locale.clearCache,
-				action: () => {
-					localStorage.removeItem('locale');
-					localStorage.removeItem('theme');
-					unisonReload();
-				},
-			}, {
-				type: 'button',
-				icon: 'fas fa-sign-in-alt fa-flip-horizontal',
-				text: i18n.locale.logout,
-				action: () => {
-					signout();
-				},
-				danger: true,
-			},],
-		}]);
-
-		const pageProps = ref({});
-		const component = computed(() => {
-			if (page.value == null) return null;
-			switch (page.value) {
-				case 'accounts': return defineAsyncComponent(() => import('./accounts.vue'));
-				case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
-				case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
-				case 'reaction': return defineAsyncComponent(() => import('./reaction.vue'));
-				case 'drive': return defineAsyncComponent(() => import('./drive.vue'));
-				case 'notifications': return defineAsyncComponent(() => import('./notifications.vue'));
-				case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue'));
-				case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
-				case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
-				case 'security': return defineAsyncComponent(() => import('./security.vue'));
-				case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
-				case 'api': return defineAsyncComponent(() => import('./api.vue'));
-				case 'apps': return defineAsyncComponent(() => import('./apps.vue'));
-				case 'other': return defineAsyncComponent(() => import('./other.vue'));
-				case 'general': return defineAsyncComponent(() => import('./general.vue'));
-				case 'email': return defineAsyncComponent(() => import('./email.vue'));
-				case 'email/address': return defineAsyncComponent(() => import('./email-address.vue'));
-				case 'email/notification': return defineAsyncComponent(() => import('./email-notification.vue'));
-				case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
-				case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
-				case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
-				case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
-				case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
-				case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
-				case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
-				case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
-				case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
-				case 'plugin/manage': return defineAsyncComponent(() => import('./plugin.manage.vue'));
-				case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
-				case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
-				case 'update': return defineAsyncComponent(() => import('./update.vue'));
-				case 'registry': return defineAsyncComponent(() => import('./registry.vue'));
-				case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue'));
-				case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
-			}
-			if (page.value.startsWith('registry/keys/system/')) {
-				return defineAsyncComponent(() => import('./registry.keys.vue'));
-			}
-			if (page.value.startsWith('registry/value/system/')) {
-				return defineAsyncComponent(() => import('./registry.value.vue'));
-			}
-		});
-
-		watch(component, () => {
-			pageProps.value = {};
-
-			if (page.value) {
-				if (page.value.startsWith('registry/keys/system/')) {
-					pageProps.value.scope = page.value.replace('registry/keys/system/', '').split('/');
-				}
-				if (page.value.startsWith('registry/value/system/')) {
-					const path = page.value.replace('registry/value/system/', '').split('/');
-					pageProps.value.xKey = path.pop();
-					pageProps.value.scope = path;
-				}
-			}
-
-			nextTick(() => {
-				scroll(el.value, { top: 0 });
-			});
-		}, { immediate: true });
-
-		watch(() => props.initialPage, () => {
-			if (props.initialPage == null && !narrow.value) {
-				page.value = 'profile';
-			} else {
-				page.value = props.initialPage;
-				if (props.initialPage == null) {
-					INFO.value = indexInfo;
-				}
-			}
-		});
-
-		onMounted(() => {
-			narrow.value = el.value.offsetWidth < 800;
-			if (!narrow.value) {
-				page.value = 'profile';
-			}
-		});
-
-		const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
-
-		return {
-			[symbols.PAGE_INFO]: INFO,
-			page,
-			menuDef,
-			narrow,
-			view,
-			el,
-			pageProps,
-			component,
-			emailNotConfigured,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.vvcocwet {
-	> .nav {
-		.baaadecd {
-			> .title {
-				margin: 16px;
-				font-size: 1.5em;
-				font-weight: bold;
-			}
-
-			> .info {
-				margin: 0 16px;
-			}
-
-			> .accounts {
-				> .avatar {
-					display: block;
-					width: 50px;
-					height: 50px;
-					margin: 8px auto 16px auto;
-				}
-			}
-		}
-	}
-
-	&.wide {
-		display: flex;
-		max-width: 1000px;
-		margin: 0 auto;
-		height: 100%;
-
-		> .nav {
-			width: 32%;
-			box-sizing: border-box;
-			overflow: auto;
-
-			.baaadecd {
-				> .title {
-					margin: 24px 0;
-				}
-			}
-		}
-
-		> .main {
-			flex: 1;
-			min-width: 0;
-			overflow: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue
deleted file mode 100644
index 7f398dde9d..0000000000
--- a/src/client/pages/settings/integration.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<template>
-<FormBase>
-	<div class="_debobigegoItem" v-if="enableTwitterIntegration">
-		<div class="_debobigegoLabel"><i class="fab fa-twitter"></i> Twitter</div>
-		<div class="_debobigegoPanel" style="padding: 16px;">
-			<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
-			<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectService }}</MkButton>
-			<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectService }}</MkButton>
-		</div>
-	</div>
-
-	<div class="_debobigegoItem" v-if="enableDiscordIntegration">
-		<div class="_debobigegoLabel"><i class="fab fa-discord"></i> Discord</div>
-		<div class="_debobigegoPanel" style="padding: 16px;">
-			<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
-			<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectService }}</MkButton>
-			<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectService }}</MkButton>
-		</div>
-	</div>
-
-	<div class="_debobigegoItem" v-if="enableGithubIntegration">
-		<div class="_debobigegoLabel"><i class="fab fa-github"></i> GitHub</div>
-		<div class="_debobigegoPanel" style="padding: 16px;">
-			<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
-			<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectService }}</MkButton>
-			<MkButton v-else @click="connectGithub" primary>{{ $ts.connectService }}</MkButton>
-		</div>
-	</div>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { apiUrl } from '@client/config';
-import FormBase from '@client/components/debobigego/base.vue';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		MkButton
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.integration,
-				icon: 'fas fa-share-alt',
-				bg: 'var(--bg)',
-			},
-			apiUrl,
-			twitterForm: null,
-			discordForm: null,
-			githubForm: null,
-			enableTwitterIntegration: false,
-			enableDiscordIntegration: false,
-			enableGithubIntegration: false,
-		};
-	},
-
-	computed: {
-		integrations() {
-			return this.$i.integrations;
-		},
-		
-		meta() {
-			return this.$instance;
-		},
-	},
-
-	created() {
-		this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
-		this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
-		this.enableGithubIntegration = this.meta.enableGithubIntegration;
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
-		document.cookie = `igi=${this.$i.token}; path=/;` +
-			` max-age=31536000;` +
-			(document.location.protocol.startsWith('https') ? ' secure' : '');
-
-		this.$watch('integrations', () => {
-			if (this.integrations.twitter) {
-				if (this.twitterForm) this.twitterForm.close();
-			}
-			if (this.integrations.discord) {
-				if (this.discordForm) this.discordForm.close();
-			}
-			if (this.integrations.github) {
-				if (this.githubForm) this.githubForm.close();
-			}
-		}, {
-			deep: true
-		});
-	},
-
-	methods: {
-		connectTwitter() {
-			this.twitterForm = window.open(apiUrl + '/connect/twitter',
-				'twitter_connect_window',
-				'height=570, width=520');
-		},
-
-		disconnectTwitter() {
-			window.open(apiUrl + '/disconnect/twitter',
-				'twitter_disconnect_window',
-				'height=570, width=520');
-		},
-
-		connectDiscord() {
-			this.discordForm = window.open(apiUrl + '/connect/discord',
-				'discord_connect_window',
-				'height=570, width=520');
-		},
-
-		disconnectDiscord() {
-			window.open(apiUrl + '/disconnect/discord',
-				'discord_disconnect_window',
-				'height=570, width=520');
-		},
-
-		connectGithub() {
-			this.githubForm = window.open(apiUrl + '/connect/github',
-				'github_connect_window',
-				'height=570, width=520');
-		},
-
-		disconnectGithub() {
-			window.open(apiUrl + '/disconnect/github',
-				'github_disconnect_window',
-				'height=570, width=520');
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/menu.vue b/src/client/pages/settings/menu.vue
deleted file mode 100644
index 31472eb0c1..0000000000
--- a/src/client/pages/settings/menu.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<template>
-<FormBase>
-	<FormTextarea v-model="items" tall manual-save>
-		<span>{{ $ts.menu }}</span>
-		<template #desc><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template>
-	</FormTextarea>
-
-	<FormRadios v-model="menuDisplay">
-		<template #desc>{{ $ts.display }}</template>
-		<option value="sideFull">{{ $ts._menuDisplay.sideFull }}</option>
-		<option value="sideIcon">{{ $ts._menuDisplay.sideIcon }}</option>
-		<option value="top">{{ $ts._menuDisplay.top }}</option>
-		<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ $ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
-	</FormRadios>
-
-	<FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormRadios from '@client/components/debobigego/radios.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import { menuDef } from '@client/menu';
-import { defaultStore } from '@client/store';
-import * as symbols from '@client/symbols';
-import { unisonReload } from '@client/scripts/unison-reload';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		FormTextarea,
-		FormRadios,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.menu,
-				icon: 'fas fa-list-ul',
-				bg: 'var(--bg)',
-			},
-			menuDef: menuDef,
-			items: defaultStore.state.menu.join('\n'),
-		}
-	},
-
-	computed: {
-		splited(): string[] {
-			return this.items.trim().split('\n').filter(x => x.trim() !== '');
-		},
-
-		menuDisplay: defaultStore.makeGetterSetter('menuDisplay')
-	},
-
-	watch: {
-		menuDisplay() {
-			this.reloadAsk();
-		},
-
-		items() {
-			this.save();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async addItem() {
-			const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k));
-			const { canceled, result: item } = await os.dialog({
-				type: null,
-				title: this.$ts.addItem,
-				select: {
-					items: [...menu.map(k => ({
-						value: k, text: this.$ts[this.menuDef[k].title]
-					})), ...[{
-						value: '-', text: this.$ts.divider
-					}]]
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			this.items = [...this.splited, item].join('\n');
-		},
-
-		save() {
-			this.$store.set('menu', this.splited);
-			this.reloadAsk();
-		},
-
-		reset() {
-			this.$store.reset('menu');
-			this.items = this.$store.state.menu.join('\n');
-		},
-
-		async reloadAsk() {
-			const { canceled } = await os.dialog({
-				type: 'info',
-				text: this.$ts.reloadToApplySetting,
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			unisonReload();
-		}
-	},
-});
-</script>
diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue
deleted file mode 100644
index 18b2fc0af4..0000000000
--- a/src/client/pages/settings/mute-block.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
-	<MkTab v-model="tab" style="margin-bottom: var(--margin);">
-		<option value="mute">{{ $ts.mutedUsers }}</option>
-		<option value="block">{{ $ts.blockedUsers }}</option>
-	</MkTab>
-	<div v-if="tab === 'mute'">
-		<MkPagination :pagination="mutingPagination" class="muting">
-			<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
-			<template #default="{items}">
-				<FormGroup>
-					<FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
-						<MkAcct :user="mute.mutee"/>
-					</FormLink>
-				</FormGroup>
-			</template>
-		</MkPagination>
-	</div>
-	<div v-if="tab === 'block'">
-		<MkPagination :pagination="blockingPagination" class="blocking">
-			<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
-			<template #default="{items}">
-				<FormGroup>
-					<FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
-						<MkAcct :user="block.blockee"/>
-					</FormLink>
-				</FormGroup>
-			</template>
-		</MkPagination>
-	</div>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkTab from '@client/components/tab.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import { userPage } from '@client/filters/user';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkTab,
-		FormInfo,
-		FormBase,
-		FormGroup,
-		FormLink,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.muteAndBlock,
-				icon: 'fas fa-ban',
-				bg: 'var(--bg)',
-			},
-			tab: 'mute',
-			mutingPagination: {
-				endpoint: 'mute/list',
-				limit: 10,
-			},
-			blockingPagination: {
-				endpoint: 'blocking/list',
-				limit: 10,
-			},
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		userPage
-	}
-});
-</script>
diff --git a/src/client/pages/settings/notifications.vue b/src/client/pages/settings/notifications.vue
deleted file mode 100644
index 5f84349474..0000000000
--- a/src/client/pages/settings/notifications.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<template>
-<FormBase>
-	<FormLink @click="configure">{{ $ts.notificationSetting }}</FormLink>
-	<FormGroup>
-		<FormButton @click="readAllNotifications">{{ $ts.markAsReadAllNotifications }}</FormButton>
-		<FormButton @click="readAllUnreadNotes">{{ $ts.markAsReadAllUnreadNotes }}</FormButton>
-		<FormButton @click="readAllMessagingMessages">{{ $ts.markAsReadAllTalkMessages }}</FormButton>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import { notificationTypes } from '@/types';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormLink,
-		FormButton,
-		FormGroup,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.notifications,
-				icon: 'fas fa-bell',
-				bg: 'var(--bg)',
-			},
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		readAllUnreadNotes() {
-			os.api('i/read-all-unread-notes');
-		},
-
-		readAllMessagingMessages() {
-			os.api('i/read-all-messaging-messages');
-		},
-
-		readAllNotifications() {
-			os.api('notifications/mark-all-as-read');
-		},
-
-		configure() {
-			const includingTypes = notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
-			os.popup(import('@client/components/notification-setting-window.vue'), {
-				includingTypes,
-				showGlobalToggle: false,
-			}, {
-				done: async (res) => {
-					const { includingTypes: value } = res;
-					await os.apiWithDialog('i/update', {
-						mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
-					}).then(i => {
-						this.$i.mutingNotificationTypes = i.mutingNotificationTypes;
-					});
-				}
-			}, 'closed');
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue
deleted file mode 100644
index 2eb922453f..0000000000
--- a/src/client/pages/settings/other.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-<template>
-<FormBase>
-	<FormLink to="/settings/update">Misskey Update</FormLink>
-
-	<FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote">
-		{{ $ts.showFeaturedNotesInTimeline }}
-	</FormSwitch>
-
-	<FormSwitch v-model="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
-
-	<FormLink to="/settings/account-info">{{ $ts.accountInfo }}</FormLink>
-	<FormLink to="/settings/experimental-features">{{ $ts.experimentalFeatures }}</FormLink>
-
-	<FormGroup>
-		<template #label>{{ $ts.developer }}</template>
-		<FormSwitch v-model="debug" @update:modelValue="changeDebug">
-			DEBUG MODE
-		</FormSwitch>
-		<template v-if="debug">
-			<FormButton @click="taskmanager">Task Manager</FormButton>
-		</template>
-	</FormGroup>
-
-	<FormLink to="/settings/registry"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink>
-
-	<FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
-	<FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
-
-	<FormLink to="/settings/delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import { debug } from '@client/config';
-import { defaultStore } from '@client/store';
-import { unisonReload } from '@client/scripts/unison-reload';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSelect,
-		FormSwitch,
-		FormButton,
-		FormLink,
-		FormGroup,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.other,
-				icon: 'fas fa-ellipsis-h',
-				bg: 'var(--bg)',
-			},
-			debug,
-		}
-	},
-
-	computed: {
-		reportError: defaultStore.makeGetterSetter('reportError'),
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		changeDebug(v) {
-			console.log(v);
-			localStorage.setItem('debug', v.toString());
-			unisonReload();
-		},
-
-		onChangeInjectFeaturedNote(v) {
-			os.api('i/update', {
-				injectFeaturedNote: v
-			});
-		},
-
-		taskmanager() {
-			os.popup(import('@client/components/taskmanager.vue'), {
-			}, {}, 'closed');
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/plugin.install.vue b/src/client/pages/settings/plugin.install.vue
deleted file mode 100644
index 709ef11abb..0000000000
--- a/src/client/pages/settings/plugin.install.vue
+++ /dev/null
@@ -1,147 +0,0 @@
-<template>
-<FormBase>
-	<FormInfo warn>{{ $ts._plugin.installWarn }}</FormInfo>
-
-	<FormGroup>
-		<FormTextarea v-model="code" tall>
-			<span>{{ $ts.code }}</span>
-		</FormTextarea>
-	</FormGroup>
-
-	<FormButton @click="install" :disabled="code == null" primary inline><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { AiScript, parse } from '@syuilo/aiscript';
-import { serialize } from '@syuilo/aiscript/built/serializer';
-import { v4 as uuid } from 'uuid';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import { unisonReload } from '@client/scripts/unison-reload';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormTextarea,
-		FormSelect,
-		FormRadios,
-		FormBase,
-		FormGroup,
-		FormLink,
-		FormButton,
-		FormInfo,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._plugin.install,
-				icon: 'fas fa-download',
-				bg: 'var(--bg)',
-			},
-			code: null,
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		installPlugin({ id, meta, ast, token }) {
-			ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
-				...meta,
-				id,
-				active: true,
-				configData: {},
-				token: token,
-				ast: ast
-			}));
-		},
-
-		async install() {
-			let ast;
-			try {
-				ast = parse(this.code);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: 'Syntax error :('
-				});
-				return;
-			}
-			const meta = AiScript.collectMetadata(ast);
-			if (meta == null) {
-				os.dialog({
-					type: 'error',
-					text: 'No metadata found :('
-				});
-				return;
-			}
-			const data = meta.get(null);
-			if (data == null) {
-				os.dialog({
-					type: 'error',
-					text: 'No metadata found :('
-				});
-				return;
-			}
-			const { name, version, author, description, permissions, config } = data;
-			if (name == null || version == null || author == null) {
-				os.dialog({
-					type: 'error',
-					text: 'Required property not found :('
-				});
-				return;
-			}
-
-			const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
-				os.popup(import('@client/components/token-generate-window.vue'), {
-					title: this.$ts.tokenRequested,
-					information: this.$ts.pluginTokenRequestedDescription,
-					initialName: name,
-					initialPermissions: permissions
-				}, {
-					done: async result => {
-						const { name, permissions } = result;
-						const { token } = await os.api('miauth/gen-token', {
-							session: null,
-							name: name,
-							permission: permissions,
-						});
-
-						res(token);
-					}
-				}, 'closed');
-			});
-
-			this.installPlugin({
-				id: uuid(),
-				meta: {
-					name, version, author, description, permissions, config
-				},
-				token,
-				ast: serialize(ast)
-			});
-
-			os.success();
-
-			this.$nextTick(() => {
-				unisonReload();
-			});
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/plugin.manage.vue b/src/client/pages/settings/plugin.manage.vue
deleted file mode 100644
index f1c27f1e3c..0000000000
--- a/src/client/pages/settings/plugin.manage.vue
+++ /dev/null
@@ -1,115 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup v-for="plugin in plugins" :key="plugin.id">
-		<template #label><span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span></template>
-
-		<FormSwitch :value="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
-		<div class="_debobigegoItem">
-			<div class="_debobigegoPanel" style="padding: 16px;">
-				<div class="_keyValue">
-					<div>{{ $ts.author }}:</div>
-					<div>{{ plugin.author }}</div>
-				</div>
-				<div class="_keyValue">
-					<div>{{ $ts.description }}:</div>
-					<div>{{ plugin.description }}</div>
-				</div>
-				<div class="_keyValue">
-					<div>{{ $ts.permission }}:</div>
-					<div>{{ plugin.permissions }}</div>
-				</div>
-			</div>
-		</div>
-		<div class="_debobigegoItem">
-			<div class="_debobigegoPanel" style="padding: 16px;">
-				<MkButton @click="config(plugin)" inline v-if="plugin.config"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton>
-				<MkButton @click="uninstall(plugin)" inline danger><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton>
-			</div>
-		</div>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkSelect from '@client/components/form/select.vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkTextarea,
-		MkSelect,
-		FormSwitch,
-		FormBase,
-		FormGroup,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._plugin.manage,
-				icon: 'fas fa-plug',
-				bg: 'var(--bg)',
-			},
-			plugins: ColdDeviceStorage.get('plugins'),
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		uninstall(plugin) {
-			ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
-			os.success();
-			this.$nextTick(() => {
-				unisonReload();
-			});
-		},
-
-		// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
-		async config(plugin) {
-			const config = plugin.config;
-			for (const key in plugin.configData) {
-				config[key].default = plugin.configData[key];
-			}
-
-			const { canceled, result } = await os.form(plugin.name, config);
-			if (canceled) return;
-
-			const plugins = ColdDeviceStorage.get('plugins');
-			plugins.find(p => p.id === plugin.id).configData = result;
-			ColdDeviceStorage.set('plugins', plugins);
-
-			this.$nextTick(() => {
-				location.reload();
-			});
-		},
-
-		changeActive(plugin, active) {
-			const plugins = ColdDeviceStorage.get('plugins');
-			plugins.find(p => p.id === plugin.id).active = active;
-			ColdDeviceStorage.set('plugins', plugins);
-
-			this.$nextTick(() => {
-				location.reload();
-			});
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/settings/plugin.vue b/src/client/pages/settings/plugin.vue
deleted file mode 100644
index 23f263bbbd..0000000000
--- a/src/client/pages/settings/plugin.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<FormBase>
-	<FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._plugin.install }}</FormLink>
-	<FormLink to="/settings/plugin/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._plugin.manage }}<template #suffix>{{ plugins }}</template></FormLink>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormLink,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.plugins,
-				icon: 'fas fa-plug',
-				bg: 'var(--bg)',
-			},
-			plugins: ColdDeviceStorage.get('plugins').length,
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue
deleted file mode 100644
index 5e0c259ca3..0000000000
--- a/src/client/pages/settings/privacy.vue
+++ /dev/null
@@ -1,120 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<FormSwitch v-model="isLocked" @update:modelValue="save()">{{ $ts.makeFollowManuallyApprove }}</FormSwitch>
-		<FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
-		<template #caption>{{ $ts.lockedAccountInfo }}</template>
-	</FormGroup>
-	<FormSwitch v-model="publicReactions" @update:modelValue="save()">
-		{{ $ts.makeReactionsPublic }}
-		<template #desc>{{ $ts.makeReactionsPublicDescription }}</template>
-	</FormSwitch>
-	<FormGroup>
-		<template #label>{{ $ts.ffVisibility }}</template>
-		<FormSelect v-model="ffVisibility">
-			<option value="public">{{ $ts._ffVisibility.public }}</option>
-			<option value="followers">{{ $ts._ffVisibility.followers }}</option>
-			<option value="private">{{ $ts._ffVisibility.private }}</option>
-		</FormSelect>
-		<template #caption>{{ $ts.ffVisibilityDescription }}</template>
-	</FormGroup>
-	<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
-		{{ $ts.hideOnlineStatus }}
-		<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
-	</FormSwitch>
-	<FormSwitch v-model="noCrawle" @update:modelValue="save()">
-		{{ $ts.noCrawle }}
-		<template #desc>{{ $ts.noCrawleDescription }}</template>
-	</FormSwitch>
-	<FormSwitch v-model="isExplorable" @update:modelValue="save()">
-		{{ $ts.makeExplorable }}
-		<template #desc>{{ $ts.makeExplorableDescription }}</template>
-	</FormSwitch>
-	<FormSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ $ts.rememberNoteVisibility }}</FormSwitch>
-	<FormGroup v-if="!rememberNoteVisibility">
-		<template #label>{{ $ts.defaultNoteVisibility }}</template>
-		<FormSelect v-model="defaultNoteVisibility">
-			<option value="public">{{ $ts._visibility.public }}</option>
-			<option value="home">{{ $ts._visibility.home }}</option>
-			<option value="followers">{{ $ts._visibility.followers }}</option>
-			<option value="specified">{{ $ts._visibility.specified }}</option>
-		</FormSelect>
-		<FormSwitch v-model="defaultNoteLocalOnly">{{ $ts._visibility.localOnly }}</FormSwitch>
-	</FormGroup>
-	<FormSwitch v-model="keepCw" @update:modelValue="save()">{{ $ts.keepCw }}</FormSwitch>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormSelect from '@client/components/debobigego/select.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import * as os from '@client/os';
-import { defaultStore } from '@client/store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSelect,
-		FormGroup,
-		FormSwitch,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.privacy,
-				icon: 'fas fa-lock-open',
-				bg: 'var(--bg)',
-			},
-			isLocked: false,
-			autoAcceptFollowed: false,
-			noCrawle: false,
-			isExplorable: false,
-			hideOnlineStatus: false,
-			publicReactions: false,
-			ffVisibility: 'public',
-		}
-	},
-
-	computed: {
-		defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'),
-		defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'),
-		rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'),
-		keepCw: defaultStore.makeGetterSetter('keepCw'),
-	},
-
-	created() {
-		this.isLocked = this.$i.isLocked;
-		this.autoAcceptFollowed = this.$i.autoAcceptFollowed;
-		this.noCrawle = this.$i.noCrawle;
-		this.isExplorable = this.$i.isExplorable;
-		this.hideOnlineStatus = this.$i.hideOnlineStatus;
-		this.publicReactions = this.$i.publicReactions;
-		this.ffVisibility = this.$i.ffVisibility;
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		save() {
-			os.api('i/update', {
-				isLocked: !!this.isLocked,
-				autoAcceptFollowed: !!this.autoAcceptFollowed,
-				noCrawle: !!this.noCrawle,
-				isExplorable: !!this.isExplorable,
-				hideOnlineStatus: !!this.hideOnlineStatus,
-				publicReactions: !!this.publicReactions,
-				ffVisibility: this.ffVisibility,
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue
deleted file mode 100644
index b993b5fc72..0000000000
--- a/src/client/pages/settings/profile.vue
+++ /dev/null
@@ -1,281 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<div class="_debobigegoItem _debobigegoPanel llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
-			<MkAvatar class="avatar" :user="$i"/>
-		</div>
-		<FormButton @click="changeAvatar" primary>{{ $ts._profile.changeAvatar }}</FormButton>
-		<FormButton @click="changeBanner" primary>{{ $ts._profile.changeBanner }}</FormButton>
-	</FormGroup>
-
-	<FormInput v-model="name" :max="30" manual-save>
-		<span>{{ $ts._profile.name }}</span>
-	</FormInput>
-
-	<FormTextarea v-model="description" :max="500" tall manual-save>
-		<span>{{ $ts._profile.description }}</span>
-		<template #desc>{{ $ts._profile.youCanIncludeHashtags }}</template>
-	</FormTextarea>
-
-	<FormInput v-model="location" manual-save>
-		<span>{{ $ts.location }}</span>
-		<template #prefix><i class="fas fa-map-marker-alt"></i></template>
-	</FormInput>
-
-	<FormInput v-model="birthday" type="date" manual-save>
-		<span>{{ $ts.birthday }}</span>
-		<template #prefix><i class="fas fa-birthday-cake"></i></template>
-	</FormInput>
-
-	<FormSelect v-model="lang">
-		<template #label>{{ $ts.language }}</template>
-		<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
-	</FormSelect>
-
-	<FormGroup>
-		<FormButton @click="editMetadata" primary>{{ $ts._profile.metadataEdit }}</FormButton>
-		<template #caption>{{ $ts._profile.metadataDescription }}</template>
-	</FormGroup>
-
-	<FormSwitch v-model="isCat">{{ $ts.flagAsCat }}<template #desc>{{ $ts.flagAsCatDescription }}</template></FormSwitch>
-
-	<FormSwitch v-model="isBot">{{ $ts.flagAsBot }}<template #desc>{{ $ts.flagAsBotDescription }}</template></FormSwitch>
-
-	<FormSwitch v-model="alwaysMarkNsfw">{{ $ts.alwaysMarkSensitive }}</FormSwitch>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormSelect from '@client/components/debobigego/select.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import { host, langs } from '@client/config';
-import { selectFile } from '@client/scripts/select-file';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormButton,
-		FormInput,
-		FormTextarea,
-		FormSwitch,
-		FormSelect,
-		FormBase,
-		FormGroup,
-	},
-	
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.profile,
-				icon: 'fas fa-user',
-				bg: 'var(--bg)',
-			},
-			host,
-			langs,
-			name: null,
-			description: null,
-			birthday: null,
-			lang: null,
-			location: null,
-			fieldName0: null,
-			fieldValue0: null,
-			fieldName1: null,
-			fieldValue1: null,
-			fieldName2: null,
-			fieldValue2: null,
-			fieldName3: null,
-			fieldValue3: null,
-			avatarId: null,
-			bannerId: null,
-			isBot: false,
-			isCat: false,
-			alwaysMarkNsfw: false,
-			saving: false,
-		}
-	},
-
-	created() {
-		this.name = this.$i.name;
-		this.description = this.$i.description;
-		this.location = this.$i.location;
-		this.birthday = this.$i.birthday;
-		this.lang = this.$i.lang;
-		this.avatarId = this.$i.avatarId;
-		this.bannerId = this.$i.bannerId;
-		this.isBot = this.$i.isBot;
-		this.isCat = this.$i.isCat;
-		this.alwaysMarkNsfw = this.$i.alwaysMarkNsfw;
-
-		this.fieldName0 = this.$i.fields[0] ? this.$i.fields[0].name : null;
-		this.fieldValue0 = this.$i.fields[0] ? this.$i.fields[0].value : null;
-		this.fieldName1 = this.$i.fields[1] ? this.$i.fields[1].name : null;
-		this.fieldValue1 = this.$i.fields[1] ? this.$i.fields[1].value : null;
-		this.fieldName2 = this.$i.fields[2] ? this.$i.fields[2].name : null;
-		this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null;
-		this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null;
-		this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null;
-
-		this.$watch('name', this.save);
-		this.$watch('description', this.save);
-		this.$watch('location', this.save);
-		this.$watch('birthday', this.save);
-		this.$watch('lang', this.save);
-		this.$watch('isBot', this.save);
-		this.$watch('isCat', this.save);
-		this.$watch('alwaysMarkNsfw', this.save);
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		changeAvatar(e) {
-			selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => {
-				os.api('i/update', {
-					avatarId: file.id,
-				});
-			});
-		},
-
-		changeBanner(e) {
-			selectFile(e.currentTarget || e.target, this.$ts.banner).then(file => {
-				os.api('i/update', {
-					bannerId: file.id,
-				});
-			});
-		},
-
-		async editMetadata() {
-			const { canceled, result } = await os.form(this.$ts._profile.metadata, {
-				fieldName0: {
-					type: 'string',
-					label: this.$ts._profile.metadataLabel + ' 1',
-					default: this.fieldName0,
-				},
-				fieldValue0: {
-					type: 'string',
-					label: this.$ts._profile.metadataContent + ' 1',
-					default: this.fieldValue0,
-				},
-				fieldName1: {
-					type: 'string',
-					label: this.$ts._profile.metadataLabel + ' 2',
-					default: this.fieldName1,
-				},
-				fieldValue1: {
-					type: 'string',
-					label: this.$ts._profile.metadataContent + ' 2',
-					default: this.fieldValue1,
-				},
-				fieldName2: {
-					type: 'string',
-					label: this.$ts._profile.metadataLabel + ' 3',
-					default: this.fieldName2,
-				},
-				fieldValue2: {
-					type: 'string',
-					label: this.$ts._profile.metadataContent + ' 3',
-					default: this.fieldValue2,
-				},
-				fieldName3: {
-					type: 'string',
-					label: this.$ts._profile.metadataLabel + ' 4',
-					default: this.fieldName3,
-				},
-				fieldValue3: {
-					type: 'string',
-					label: this.$ts._profile.metadataContent + ' 4',
-					default: this.fieldValue3,
-				},
-			});
-			if (canceled) return;
-
-			this.fieldName0 = result.fieldName0;
-			this.fieldValue0 = result.fieldValue0;
-			this.fieldName1 = result.fieldName1;
-			this.fieldValue1 = result.fieldValue1;
-			this.fieldName2 = result.fieldName2;
-			this.fieldValue2 = result.fieldValue2;
-			this.fieldName3 = result.fieldName3;
-			this.fieldValue3 = result.fieldValue3;
-
-			const fields = [
-				{ name: this.fieldName0, value: this.fieldValue0 },
-				{ name: this.fieldName1, value: this.fieldValue1 },
-				{ name: this.fieldName2, value: this.fieldValue2 },
-				{ name: this.fieldName3, value: this.fieldValue3 },
-			];
-
-			os.api('i/update', {
-				fields,
-			}).then(i => {
-				os.success();
-			}).catch(err => {
-				os.dialog({
-					type: 'error',
-					text: err.id
-				});
-			});
-		},
-
-		save() {
-			this.saving = true;
-
-			os.apiWithDialog('i/update', {
-				name: this.name || null,
-				description: this.description || null,
-				location: this.location || null,
-				birthday: this.birthday || null,
-				lang: this.lang || null,
-				isBot: !!this.isBot,
-				isCat: !!this.isCat,
-				alwaysMarkNsfw: !!this.alwaysMarkNsfw,
-			}).then(i => {
-				this.saving = false;
-				this.$i.avatarId = i.avatarId;
-				this.$i.avatarUrl = i.avatarUrl;
-				this.$i.bannerId = i.bannerId;
-				this.$i.bannerUrl = i.bannerUrl;
-			}).catch(err => {
-				this.saving = false;
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.llvierxe {
-	position: relative;
-	height: 150px;
-	background-size: cover;
-	background-position: center;
-
-	> * {
-		pointer-events: none;
-	}
-
-	> .avatar {
-		position: absolute;
-		top: 0;
-		bottom: 0;
-		left: 0;
-		right: 0;
-		display: block;
-		width: 72px;
-		height: 72px;
-		margin: auto;
-		box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
-	}
-}
-</style>
diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue
deleted file mode 100644
index a5ff46097d..0000000000
--- a/src/client/pages/settings/reaction.vue
+++ /dev/null
@@ -1,152 +0,0 @@
-<template>
-<FormBase>
-	<div class="_debobigegoItem">
-		<div class="_debobigegoLabel">{{ $ts.reactionSettingDescription }}</div>
-		<div class="_debobigegoPanel">
-			<XDraggable class="zoaiodol" v-model="reactions" :item-key="item => item" animation="150" delay="100" delay-on-touch-only="true">
-				<template #item="{element}">
-					<button class="_button item" @click="remove(element, $event)">
-						<MkEmoji :emoji="element" :normal="true"/>
-					</button>
-				</template>
-				<template #footer>
-					<button class="_button add" @click="chooseEmoji"><i class="fas fa-plus"></i></button>
-				</template>
-			</XDraggable>
-		</div>
-		<div class="_debobigegoCaption">{{ $ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ $ts.preview }}</button></div>
-	</div>
-
-	<FormRadios v-model="reactionPickerWidth">
-		<template #desc>{{ $ts.width }}</template>
-		<option :value="1">{{ $ts.small }}</option>
-		<option :value="2">{{ $ts.medium }}</option>
-		<option :value="3">{{ $ts.large }}</option>
-	</FormRadios>
-	<FormRadios v-model="reactionPickerHeight">
-		<template #desc>{{ $ts.height }}</template>
-		<option :value="1">{{ $ts.small }}</option>
-		<option :value="2">{{ $ts.medium }}</option>
-		<option :value="3">{{ $ts.large }}</option>
-	</FormRadios>
-	<FormButton @click="preview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
-	<FormButton danger @click="setDefault"><i class="fas fa-undo"></i> {{ $ts.default }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XDraggable from 'vuedraggable';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormRadios from '@client/components/debobigego/radios.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import * as os from '@client/os';
-import { defaultStore } from '@client/store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormInput,
-		FormButton,
-		FormBase,
-		FormRadios,
-		XDraggable,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.reaction,
-				icon: 'fas fa-laugh',
-				action: {
-					icon: 'fas fa-eye',
-					handler: this.preview
-				},
-				bg: 'var(--bg)',
-			},
-			reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)),
-		}
-	},
-
-	computed: {
-		reactionPickerWidth: defaultStore.makeGetterSetter('reactionPickerWidth'),
-		reactionPickerHeight: defaultStore.makeGetterSetter('reactionPickerHeight'),
-	},
-
-	watch: {
-		reactions: {
-			handler() {
-				this.save();
-			},
-			deep: true
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		save() {
-			this.$store.set('reactions', this.reactions);
-		},
-
-		remove(reaction, ev) {
-			os.popupMenu([{
-				text: this.$ts.remove,
-				action: () => {
-					this.reactions = this.reactions.filter(x => x !== reaction)
-				}
-			}], ev.currentTarget || ev.target);
-		},
-
-		preview(ev) {
-			os.popup(import('@client/components/emoji-picker-dialog.vue'), {
-				asReactionPicker: true,
-				src: ev.currentTarget || ev.target,
-			}, {}, 'closed');
-		},
-
-		async setDefault() {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$ts.resetAreYouSure,
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			this.reactions = JSON.parse(JSON.stringify(this.$store.def.reactions.default));
-		},
-
-		chooseEmoji(ev) {
-			os.pickEmoji(ev.currentTarget || ev.target, {
-				showPinned: false
-			}).then(emoji => {
-				if (!this.reactions.includes(emoji)) {
-					this.reactions.push(emoji);
-				}
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.zoaiodol {
-	padding: 16px;
-
-	> .item {
-		display: inline-block;
-		padding: 8px;
-		cursor: move;
-	}
-
-	> .add {
-		display: inline-block;
-		padding: 8px;
-	}
-}
-</style>
diff --git a/src/client/pages/settings/registry.keys.vue b/src/client/pages/settings/registry.keys.vue
deleted file mode 100644
index d99002e50f..0000000000
--- a/src/client/pages/settings/registry.keys.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<FormKeyValueView>
-			<template #key>{{ $ts._registry.domain }}</template>
-			<template #value>{{ $ts.system }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts._registry.scope }}</template>
-			<template #value>{{ scope.join('/') }}</template>
-		</FormKeyValueView>
-	</FormGroup>
-	
-	<FormGroup v-if="keys">
-		<template #label>{{ $ts._registry.keys }}</template>
-		<FormLink v-for="key in keys" :to="`/settings/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
-	</FormGroup>
-
-	<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSelect,
-		FormSwitch,
-		FormButton,
-		FormLink,
-		FormGroup,
-		FormKeyValueView,
-	},
-
-	props: {
-		scope: {
-			required: true
-		}
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.registry,
-				icon: 'fas fa-cogs',
-				bg: 'var(--bg)',
-			},
-			keys: null,
-		}
-	},
-
-	watch: {
-		scope() {
-			this.fetch();
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			os.api('i/registry/keys-with-type', {
-				scope: this.scope
-			}).then(keys => {
-				this.keys = Object.entries(keys).sort((a, b) => a[0].localeCompare(b[0]));
-			});
-		},
-
-		async createKey() {
-			const { canceled, result } = await os.form(this.$ts._registry.createKey, {
-				key: {
-					type: 'string',
-					label: this.$ts._registry.key,
-				},
-				value: {
-					type: 'string',
-					multiline: true,
-					label: this.$ts.value,
-				},
-				scope: {
-					type: 'string',
-					label: this.$ts._registry.scope,
-					default: this.scope.join('/')
-				}
-			});
-			if (canceled) return;
-			os.apiWithDialog('i/registry/set', {
-				scope: result.scope.split('/'),
-				key: result.key,
-				value: JSON5.parse(result.value),
-			}).then(() => {
-				this.fetch();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/registry.value.vue b/src/client/pages/settings/registry.value.vue
deleted file mode 100644
index 06be5737e9..0000000000
--- a/src/client/pages/settings/registry.value.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-<template>
-<FormBase>
-	<FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo>
-
-	<template v-if="value">
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>{{ $ts._registry.domain }}</template>
-				<template #value>{{ $ts.system }}</template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>{{ $ts._registry.scope }}</template>
-				<template #value>{{ scope.join('/') }}</template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>{{ $ts._registry.key }}</template>
-				<template #value>{{ xKey }}</template>
-			</FormKeyValueView>
-		</FormGroup>
-
-		<FormGroup>
-			<FormTextarea tall v-model="valueForEditor" class="_monospace" style="tab-size: 2;">
-				<span>{{ $ts.value }} (JSON)</span>
-			</FormTextarea>
-			<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-		</FormGroup>
-
-		<FormKeyValueView>
-			<template #key>{{ $ts.updatedAt }}</template>
-			<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
-		</FormKeyValueView>
-
-		<FormButton danger @click="del"><i class="fas fa-trash"></i> {{ $ts.delete }}</FormButton>
-	</template>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import FormInfo from '@client/components/debobigego/info.vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormInfo,
-		FormBase,
-		FormSelect,
-		FormSwitch,
-		FormButton,
-		FormTextarea,
-		FormGroup,
-		FormKeyValueView,
-	},
-
-	props: {
-		scope: {
-			required: true
-		},
-		xKey: {
-			required: true
-		},
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.registry,
-				icon: 'fas fa-cogs',
-				bg: 'var(--bg)',
-			},
-			value: null,
-			valueForEditor: null,
-		}
-	},
-
-	watch: {
-		key() {
-			this.fetch();
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-		this.fetch();
-	},
-
-	methods: {
-		fetch() {
-			os.api('i/registry/get-detail', {
-				scope: this.scope,
-				key: this.xKey
-			}).then(value => {
-				this.value = value;
-				this.valueForEditor = JSON5.stringify(this.value.value, null, '\t');
-			});
-		},
-
-		save() {
-			try {
-				JSON5.parse(this.valueForEditor);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: this.$ts.invalidValue
-				});
-				return;
-			}
-
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.saveConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				os.apiWithDialog('i/registry/set', {
-					scope: this.scope,
-					key: this.xKey,
-					value: JSON5.parse(this.valueForEditor)
-				});
-			});
-		},
-
-		del() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.deleteConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-				os.apiWithDialog('i/registry/remove', {
-					scope: this.scope,
-					key: this.xKey
-				});
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/registry.vue b/src/client/pages/settings/registry.vue
deleted file mode 100644
index e4fb230d5c..0000000000
--- a/src/client/pages/settings/registry.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup v-if="scopes">
-		<template #label>{{ $ts.system }}</template>
-		<FormLink v-for="scope in scopes" :to="`/settings/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
-	</FormGroup>
-	<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSelect,
-		FormSwitch,
-		FormButton,
-		FormLink,
-		FormGroup,
-		FormKeyValueView,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.registry,
-				icon: 'fas fa-cogs',
-				bg: 'var(--bg)',
-			},
-			scopes: null,
-		}
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		fetch() {
-			os.api('i/registry/scopes').then(scopes => {
-				this.scopes = scopes.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
-			});
-		},
-
-		async createKey() {
-			const { canceled, result } = await os.form(this.$ts._registry.createKey, {
-				key: {
-					type: 'string',
-					label: this.$ts._registry.key,
-				},
-				value: {
-					type: 'string',
-					multiline: true,
-					label: this.$ts.value,
-				},
-				scope: {
-					type: 'string',
-					label: this.$ts._registry.scope,
-				}
-			});
-			if (canceled) return;
-			os.apiWithDialog('i/registry/set', {
-				scope: result.scope.split('/'),
-				key: result.key,
-				value: JSON5.parse(result.value),
-			}).then(() => {
-				this.fetch();
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue
deleted file mode 100644
index e051685a82..0000000000
--- a/src/client/pages/settings/security.vue
+++ /dev/null
@@ -1,158 +0,0 @@
-<template>
-<FormBase>
-	<X2fa/>
-	<FormLink to="/settings/2fa"><template #icon><i class="fas fa-mobile-alt"></i></template>{{ $ts.twoStepAuthentication }}</FormLink>
-	<FormButton primary @click="change()">{{ $ts.changePassword }}</FormButton>
-	<FormPagination :pagination="pagination">
-		<template #label>{{ $ts.signinHistory }}</template>
-		<template #default="{items}">
-			<div class="_debobigegoPanel timnmucd" v-for="item in items" :key="item.id">
-				<header>
-					<i v-if="item.success" class="fas fa-check icon succ"></i>
-					<i v-else class="fas fa-times-circle icon fail"></i>
-					<code class="ip _monospace">{{ item.ip }}</code>
-					<MkTime :time="item.createdAt" class="time"/>
-				</header>
-			</div>
-		</template>
-	</FormPagination>
-	<FormGroup>
-		<FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ $ts.regenerateLoginToken }}</FormButton>
-		<template #caption>{{ $ts.regenerateLoginTokenDescription }}</template>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormPagination from '@client/components/debobigego/pagination.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormLink,
-		FormButton,
-		FormPagination,
-		FormGroup,
-	},
-	
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.security,
-				icon: 'fas fa-lock',
-				bg: 'var(--bg)',
-			},
-			pagination: {
-				endpoint: 'i/signin-history',
-				limit: 5,
-			},
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async change() {
-			const { canceled: canceled1, result: currentPassword } = await os.dialog({
-				title: this.$ts.currentPassword,
-				input: {
-					type: 'password'
-				}
-			});
-			if (canceled1) return;
-
-			const { canceled: canceled2, result: newPassword } = await os.dialog({
-				title: this.$ts.newPassword,
-				input: {
-					type: 'password'
-				}
-			});
-			if (canceled2) return;
-
-			const { canceled: canceled3, result: newPassword2 } = await os.dialog({
-				title: this.$ts.newPasswordRetype,
-				input: {
-					type: 'password'
-				}
-			});
-			if (canceled3) return;
-
-			if (newPassword !== newPassword2) {
-				os.dialog({
-					type: 'error',
-					text: this.$ts.retypedNotMatch
-				});
-				return;
-			}
-			
-			os.apiWithDialog('i/change-password', {
-				currentPassword,
-				newPassword
-			});
-		},
-
-		regenerateToken() {
-			os.dialog({
-				title: this.$ts.password,
-				input: {
-					type: 'password'
-				}
-			}).then(({ canceled, result: password }) => {
-				if (canceled) return;
-				os.api('i/regenerate_token', {
-					password: password
-				});
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.timnmucd {
-	padding: 16px;
-
-	> header {
-		display: flex;
-		align-items: center;
-
-		> .icon {
-			width: 1em;
-			margin-right: 0.75em;
-
-			&.succ {
-				color: var(--success);
-			}
-
-			&.fail {
-				color: var(--error);
-			}
-		}
-
-		> .ip {
-			flex: 1;
-			min-width: 0;
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-			margin-right: 12px;
-		}
-
-		> .time {
-			margin-left: auto;
-			opacity: 0.7;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/settings/sounds.vue b/src/client/pages/settings/sounds.vue
deleted file mode 100644
index 07310619c8..0000000000
--- a/src/client/pages/settings/sounds.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template>
-<FormBase>
-	<FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05">
-		<template #label><i class="fas fa-volume-icon"></i> {{ $ts.masterVolume }}</template>
-	</FormRange>
-
-	<FormGroup>
-		<template #label>{{ $ts.sounds }}</template>
-		<FormButton v-for="type in Object.keys(sounds)" :key="type" :center="false" @click="edit(type)">
-			{{ $t('_sfx.' + type) }}
-			<template #suffix>{{ sounds[type].type || $ts.none }}</template>
-			<template #suffixIcon><i class="fas fa-chevron-down"></i></template>
-		</FormButton>
-	</FormGroup>
-
-	<FormButton @click="reset()" danger><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormRange from '@client/components/debobigego/range.vue';
-import FormSelect from '@client/components/debobigego/select.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import { playFile } from '@client/scripts/sound';
-import * as symbols from '@client/symbols';
-
-const soundsTypes = [
-	null,
-	'syuilo/up',
-	'syuilo/down',
-	'syuilo/pope1',
-	'syuilo/pope2',
-	'syuilo/waon',
-	'syuilo/popo',
-	'syuilo/triple',
-	'syuilo/poi1',
-	'syuilo/poi2',
-	'syuilo/pirori',
-	'syuilo/pirori-wet',
-	'syuilo/pirori-square-wet',
-	'syuilo/square-pico',
-	'syuilo/reverved',
-	'syuilo/ryukyu',
-	'syuilo/kick',
-	'syuilo/snare',
-	'syuilo/queue-jammed',
-	'aisha/1',
-	'aisha/2',
-	'aisha/3',
-	'noizenecio/kick_gaba',
-	'noizenecio/kick_gaba2',
-];
-
-export default defineComponent({
-	components: {
-		FormSelect,
-		FormButton,
-		FormBase,
-		FormRange,
-		FormGroup,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.sounds,
-				icon: 'fas fa-music',
-				bg: 'var(--bg)',
-			},
-			sounds: {},
-		}
-	},
-
-	computed: {
-		masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す
-			get() { return ColdDeviceStorage.get('sound_masterVolume'); },
-			set(value) { ColdDeviceStorage.set('sound_masterVolume', value); }
-		},
-		volumeIcon() {
-			return this.masterVolume === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up';
-		}
-	},
-
-	created() {
-		this.sounds.note = ColdDeviceStorage.get('sound_note');
-		this.sounds.noteMy = ColdDeviceStorage.get('sound_noteMy');
-		this.sounds.notification = ColdDeviceStorage.get('sound_notification');
-		this.sounds.chat = ColdDeviceStorage.get('sound_chat');
-		this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg');
-		this.sounds.antenna = ColdDeviceStorage.get('sound_antenna');
-		this.sounds.channel = ColdDeviceStorage.get('sound_channel');
-		this.sounds.reversiPutBlack = ColdDeviceStorage.get('sound_reversiPutBlack');
-		this.sounds.reversiPutWhite = ColdDeviceStorage.get('sound_reversiPutWhite');
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async edit(type) {
-			const { canceled, result } = await os.form(this.$t('_sfx.' + type), {
-				type: {
-					type: 'enum',
-					enum: soundsTypes.map(x => ({
-						value: x,
-						label: x == null ? this.$ts.none : x,
-					})),
-					label: this.$ts.sound,
-					default: this.sounds[type].type,
-				},
-				volume: {
-					type: 'range',
-					mim: 0,
-					max: 1,
-					step: 0.05,
-					label: this.$ts.volume,
-					default: this.sounds[type].volume
-				},
-				listen: {
-					type: 'button',
-					content: this.$ts.listen,
-					action: (_, values) => {
-						playFile(values.type, values.volume);
-					}
-				}
-			});
-			if (canceled) return;
-
-			const v = {
-				type: result.type,
-				volume: result.volume,
-			};
-
-			ColdDeviceStorage.set('sound_' + type, v);
-			this.sounds[type] = v;
-		},
-
-		reset() {
-			for (const sound of Object.keys(this.sounds)) {
-				const v = ColdDeviceStorage.default['sound_' + sound];
-				ColdDeviceStorage.set('sound_' + sound, v);
-				this.sounds[sound] = v;
-			}
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/settings/theme.install.vue b/src/client/pages/settings/theme.install.vue
deleted file mode 100644
index 9fbb28929d..0000000000
--- a/src/client/pages/settings/theme.install.vue
+++ /dev/null
@@ -1,105 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<FormTextarea v-model="installThemeCode">
-			<span>{{ $ts._theme.code }}</span>
-		</FormTextarea>
-		<FormButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
-	</FormGroup>
-
-	<FormButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import { applyTheme, validateTheme } from '@client/scripts/theme';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import { addTheme, getThemes } from '@client/theme-store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormTextarea,
-		FormSelect,
-		FormRadios,
-		FormBase,
-		FormGroup,
-		FormLink,
-		FormButton,
-	},
-
-	emits: ['info'],
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._theme.install,
-				icon: 'fas fa-download',
-				bg: 'var(--bg)',
-			},
-			installThemeCode: null,
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		parseThemeCode(code) {
-			let theme;
-
-			try {
-				theme = JSON5.parse(code);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: this.$ts._theme.invalid
-				});
-				return false;
-			}
-			if (!validateTheme(theme)) {
-				os.dialog({
-					type: 'error',
-					text: this.$ts._theme.invalid
-				});
-				return false;
-			}
-			if (getThemes().some(t => t.id === theme.id)) {
-				os.dialog({
-					type: 'info',
-					text: this.$ts._theme.alreadyInstalled
-				});
-				return false;
-			}
-
-			return theme;
-		},
-
-		preview(code) {
-			const theme = this.parseThemeCode(code);
-			if (theme) applyTheme(theme, false);
-		},
-
-		async install(code) {
-			const theme = this.parseThemeCode(code);
-			if (!theme) return;
-			await addTheme(theme);
-			os.dialog({
-				type: 'success',
-				text: this.$t('_theme.installed', { name: theme.name })
-			});
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/theme.manage.vue b/src/client/pages/settings/theme.manage.vue
deleted file mode 100644
index 1a11a664f0..0000000000
--- a/src/client/pages/settings/theme.manage.vue
+++ /dev/null
@@ -1,105 +0,0 @@
-<template>
-<FormBase>
-	<FormSelect v-model="selectedThemeId">
-		<template #label>{{ $ts.theme }}</template>
-		<optgroup :label="$ts._theme.installedThemes">
-			<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-		</optgroup>
-		<optgroup :label="$ts._theme.builtinThemes">
-			<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-		</optgroup>
-	</FormSelect>
-	<template v-if="selectedTheme">
-		<FormInput readonly :modelValue="selectedTheme.author">
-			<span>{{ $ts.author }}</span>
-		</FormInput>
-		<FormTextarea readonly :modelValue="selectedTheme.desc" v-if="selectedTheme.desc">
-			<span>{{ $ts._theme.description }}</span>
-		</FormTextarea>
-		<FormTextarea readonly tall :modelValue="selectedThemeCode">
-			<span>{{ $ts._theme.code }}</span>
-			<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template>
-		</FormTextarea>
-		<FormButton @click="uninstall()" danger v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
-	</template>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormSelect from '@client/components/debobigego/select.vue';
-import FormRadios from '@client/components/debobigego/radios.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormInput from '@client/components/debobigego/input.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import { Theme, builtinThemes } from '@client/scripts/theme';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import { getThemes, removeTheme } from '@client/theme-store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormTextarea,
-		FormSelect,
-		FormRadios,
-		FormBase,
-		FormGroup,
-		FormInput,
-		FormButton,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts._theme.manage,
-				icon: 'fas fa-folder-open',
-				bg: 'var(--bg)',
-			},
-			installedThemes: getThemes(),
-			builtinThemes,
-			selectedThemeId: null,
-		}
-	},
-
-	computed: {
-		themes(): Theme[] {
-			return this.builtinThemes.concat(this.installedThemes);
-		},
-	
-		selectedTheme() {
-			if (this.selectedThemeId == null) return null;
-			return this.themes.find(x => x.id === this.selectedThemeId);
-		},
-
-		selectedThemeCode() {
-			if (this.selectedTheme == null) return null;
-			return JSON5.stringify(this.selectedTheme, null, '\t');
-		},
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		copyThemeCode() {
-			copyToClipboard(this.selectedThemeCode);
-			os.success();
-		},
-
-		uninstall() {
-			removeTheme(this.selectedTheme);
-			this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId);
-			this.selectedThemeId = null;
-			os.success();
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue
deleted file mode 100644
index c6be42251c..0000000000
--- a/src/client/pages/settings/theme.vue
+++ /dev/null
@@ -1,424 +0,0 @@
-<template>
-<FormBase>
-	<FormGroup>
-		<div class="rfqxtzch _debobigegoItem _debobigegoPanel">
-			<div class="darkMode">
-				<div class="toggleWrapper">
-					<input type="checkbox" class="dn" id="dn" v-model="darkMode"/>
-					<label for="dn" class="toggle">
-						<span class="before">{{ $ts.light }}</span>
-						<span class="after">{{ $ts.dark }}</span>
-						<span class="toggle__handler">
-							<span class="crater crater--1"></span>
-							<span class="crater crater--2"></span>
-							<span class="crater crater--3"></span>
-						</span>
-						<span class="star star--1"></span>
-						<span class="star star--2"></span>
-						<span class="star star--3"></span>
-						<span class="star star--4"></span>
-						<span class="star star--5"></span>
-						<span class="star star--6"></span>
-					</label>
-				</div>
-			</div>
-		</div>
-		<FormSwitch v-model="syncDeviceDarkMode">{{ $ts.syncDeviceDarkMode }}</FormSwitch>
-	</FormGroup>
-
-	<template v-if="darkMode">
-		<FormSelect v-model="darkThemeId">
-			<template #label>{{ $ts.themeForDarkMode }}</template>
-			<optgroup :label="$ts.darkThemes">
-				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-			</optgroup>
-			<optgroup :label="$ts.lightThemes">
-				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-			</optgroup>
-		</FormSelect>
-		<FormSelect v-model="lightThemeId">
-			<template #label>{{ $ts.themeForLightMode }}</template>
-			<optgroup :label="$ts.lightThemes">
-				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-			</optgroup>
-			<optgroup :label="$ts.darkThemes">
-				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-			</optgroup>
-		</FormSelect>
-	</template>
-	<template v-else>
-		<FormSelect v-model="lightThemeId">
-			<template #label>{{ $ts.themeForLightMode }}</template>
-			<optgroup :label="$ts.lightThemes">
-				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-			</optgroup>
-			<optgroup :label="$ts.darkThemes">
-				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-			</optgroup>
-		</FormSelect>
-		<FormSelect v-model="darkThemeId">
-			<template #label>{{ $ts.themeForDarkMode }}</template>
-			<optgroup :label="$ts.darkThemes">
-				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-			</optgroup>
-			<optgroup :label="$ts.lightThemes">
-				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
-			</optgroup>
-		</FormSelect>
-	</template>
-
-	<FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $ts.setWallpaper }}</FormButton>
-	<FormButton primary v-else @click="wallpaper = null">{{ $ts.removeWallpaper }}</FormButton>
-
-	<FormGroup>
-		<FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="fas fa-globe"></i></template>{{ $ts._theme.explore }}</FormLink>
-		<FormLink to="/settings/theme/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._theme.install }}</FormLink>
-	</FormGroup>
-
-	<FormGroup>
-		<FormLink to="/theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }}</FormLink>
-		<!--<FormLink to="/advanced-theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>-->
-	</FormGroup>
-
-	<FormLink to="/settings/theme/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormSelect from '@client/components/debobigego/select.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import { builtinThemes } from '@client/scripts/theme';
-import { selectFile } from '@client/scripts/select-file';
-import { isDeviceDarkmode } from '@client/scripts/is-device-darkmode';
-import { ColdDeviceStorage } from '@client/store';
-import { i18n } from '@client/i18n';
-import { defaultStore } from '@client/store';
-import { fetchThemes, getThemes } from '@client/theme-store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormSwitch,
-		FormSelect,
-		FormBase,
-		FormGroup,
-		FormLink,
-		FormButton,
-	},
-
-	emits: ['info'],
-
-	setup(props, { emit }) {
-		const INFO = {
-			title: i18n.locale.theme,
-			icon: 'fas fa-palette',
-				bg: 'var(--bg)',
-		};
-
-		const installedThemes = ref(getThemes());
-		const themes = computed(() => builtinThemes.concat(installedThemes.value));
-		const darkThemes = computed(() => themes.value.filter(t => t.base == 'dark' || t.kind == 'dark'));
-		const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light'));
-		const darkTheme = ColdDeviceStorage.ref('darkTheme');
-		const darkThemeId = computed({
-			get() {
-				return darkTheme.value.id;
-			},
-			set(id) {
-				ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id))
-			}
-		});
-		const lightTheme = ColdDeviceStorage.ref('lightTheme');
-		const lightThemeId = computed({
-			get() {
-				return lightTheme.value.id;
-			},
-			set(id) {
-				ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id))
-			}
-		});
-		const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
-		const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
-		const wallpaper = ref(localStorage.getItem('wallpaper'));
-		const themesCount = installedThemes.value.length;
-
-		watch(syncDeviceDarkMode, () => {
-			if (syncDeviceDarkMode) {
-				defaultStore.set('darkMode', isDeviceDarkmode());
-			}
-		});
-
-		watch(wallpaper, () => {
-			if (wallpaper.value == null) {
-				localStorage.removeItem('wallpaper');
-			} else {
-				localStorage.setItem('wallpaper', wallpaper.value);
-			}
-			location.reload();
-		});
-
-		onMounted(() => {
-			emit('info', INFO);
-		});
-
-		onActivated(() => {
-			fetchThemes().then(() => {
-				installedThemes.value = getThemes();
-			});
-		});
-
-		fetchThemes().then(() => {
-			installedThemes.value = getThemes();
-		});
-
-		return {
-			[symbols.PAGE_INFO]: INFO,
-			darkThemes,
-			lightThemes,
-			darkThemeId,
-			lightThemeId,
-			darkMode,
-			syncDeviceDarkMode,
-			themesCount,
-			wallpaper,
-			setWallpaper(e) {
-				selectFile(e.currentTarget || e.target, null, false).then(file => {
-					wallpaper.value = file.url;
-				});
-			},
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rfqxtzch {
-	padding: 16px;
-
-	> .darkMode {
-		position: relative;
-		padding: 32px 0;
-
-		&.disabled {
-			opacity: 0.7;
-
-			&, * {
-				cursor: not-allowed !important;
-			}
-		}
-
-		.toggleWrapper {
-			position: absolute;
-			top: 50%;
-			left: 50%;
-			overflow: hidden;
-			padding: 0 100px;
-			transform: translate3d(-50%, -50%, 0);
-
-			input {
-				position: absolute;
-				left: -99em;
-			}
-		}
-
-		.toggle {
-			cursor: pointer;
-			display: inline-block;
-			position: relative;
-			width: 90px;
-			height: 50px;
-			background-color: #83D8FF;
-			border-radius: 90px - 6;
-			transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-
-			> .before, > .after {
-				position: absolute;
-				top: 15px;
-				font-size: 18px;
-				transition: color 1s ease;
-			}
-
-			> .before {
-				left: -70px;
-				color: var(--accent);
-			}
-
-			> .after {
-				right: -68px;
-				color: var(--fg);
-			}
-		}
-
-		.toggle__handler {
-			display: inline-block;
-			position: relative;
-			z-index: 1;
-			top: 3px;
-			left: 3px;
-			width: 50px - 6;
-			height: 50px - 6;
-			background-color: #FFCF96;
-			border-radius: 50px;
-			box-shadow: 0 2px 6px rgba(0,0,0,.3);
-			transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
-			transform:  rotate(-45deg);
-
-			.crater {
-				position: absolute;
-				background-color: #E8CDA5;
-				opacity: 0;
-				transition: opacity 200ms ease-in-out !important;
-				border-radius: 100%;
-			}
-
-			.crater--1 {
-				top: 18px;
-				left: 10px;
-				width: 4px;
-				height: 4px;
-			}
-
-			.crater--2 {
-				top: 28px;
-				left: 22px;
-				width: 6px;
-				height: 6px;
-			}
-
-			.crater--3 {
-				top: 10px;
-				left: 25px;
-				width: 8px;
-				height: 8px;
-			}
-		}
-
-		.star {
-			position: absolute;
-			background-color: #ffffff;
-			transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-			border-radius: 50%;
-		}
-
-		.star--1 {
-			top: 10px;
-			left: 35px;
-			z-index: 0;
-			width: 30px;
-			height: 3px;
-		}
-
-		.star--2 {
-			top: 18px;
-			left: 28px;
-			z-index: 1;
-			width: 30px;
-			height: 3px;
-		}
-
-		.star--3 {
-			top: 27px;
-			left: 40px;
-			z-index: 0;
-			width: 30px;
-			height: 3px;
-		}
-
-		.star--4,
-		.star--5,
-		.star--6 {
-			opacity: 0;
-			transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-		}
-
-		.star--4 {
-			top: 16px;
-			left: 11px;
-			z-index: 0;
-			width: 2px;
-			height: 2px;
-			transform: translate3d(3px,0,0);
-		}
-
-		.star--5 {
-			top: 32px;
-			left: 17px;
-			z-index: 0;
-			width: 3px;
-			height: 3px;
-			transform: translate3d(3px,0,0);
-		}
-
-		.star--6 {
-			top: 36px;
-			left: 28px;
-			z-index: 0;
-			width: 2px;
-			height: 2px;
-			transform: translate3d(3px,0,0);
-		}
-
-		input:checked {
-			+ .toggle {
-				background-color: #749DD6;
-
-				> .before {
-					color: var(--fg);
-				}
-
-				> .after {
-					color: var(--accent);
-				}
-
-				.toggle__handler {
-					background-color: #FFE5B5;
-					transform: translate3d(40px, 0, 0) rotate(0);
-
-					.crater { opacity: 1; }
-				}
-
-				.star--1 {
-					width: 2px;
-					height: 2px;
-				}
-
-				.star--2 {
-					width: 4px;
-					height: 4px;
-					transform: translate3d(-5px, 0, 0);
-				}
-
-				.star--3 {
-					width: 2px;
-					height: 2px;
-					transform: translate3d(-7px, 0, 0);
-				}
-
-				.star--4,
-				.star--5,
-				.star--6 {
-					opacity: 1;
-					transform: translate3d(0,0,0);
-				}
-
-				.star--4 {
-					transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-				}
-
-				.star--5 {
-					transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-				}
-
-				.star--6 {
-					transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/settings/update.vue b/src/client/pages/settings/update.vue
deleted file mode 100644
index 8bc459e936..0000000000
--- a/src/client/pages/settings/update.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<template>
-<FormBase>
-	<template v-if="meta">
-		<FormInfo v-if="version === meta.version">{{ $ts.youAreRunningUpToDateClient }}</FormInfo>
-		<FormInfo v-else warn>{{ $ts.newVersionOfClientAvailable }}</FormInfo>
-	</template>
-	<FormGroup>
-		<template #label>{{ instanceName }}</template>
-		<FormKeyValueView>
-			<template #key>{{ $ts.currentVersion }}</template>
-			<template #value>{{ version }}</template>
-		</FormKeyValueView>
-		<FormKeyValueView>
-			<template #key>{{ $ts.latestVersion }}</template>
-			<template #value v-if="meta">{{ meta.version }}</template>
-			<template #value v-else><MkEllipsis/></template>
-		</FormKeyValueView>
-	</FormGroup>
-	<FormGroup>
-		<template #label>Misskey</template>
-		<FormKeyValueView>
-			<template #key>{{ $ts.latestVersion }}</template>
-			<template #value v-if="releases">{{ releases[0].tag_name }}</template>
-			<template #value v-else><MkEllipsis/></template>
-		</FormKeyValueView>
-		<template #caption v-if="releases"><MkTime :time="releases[0].published_at" mode="detail"/></template>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import * as os from '@client/os';
-import { version, instanceName } from '@client/config';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormSelect,
-		FormSwitch,
-		FormButton,
-		FormLink,
-		FormGroup,
-		FormKeyValueView,
-		FormInfo,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'Misskey Update',
-				icon: 'fas fa-sync-alt',
-				bg: 'var(--bg)',
-			},
-			version,
-			instanceName,
-			releases: null,
-			meta: null
-		}
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-
-		os.api('meta', {
-			detail: false
-		}).then(meta => {
-			this.meta = meta;
-			localStorage.setItem('v', meta.version);
-		});
-
-		fetch('https://api.github.com/repos/misskey-dev/misskey/releases', {
-			method: 'GET',
-		})
-		.then(res => res.json())
-		.then(res => {
-			this.releases = res;
-		});
-	},
-
-	methods: {
-	}
-});
-</script>
diff --git a/src/client/pages/settings/word-mute.vue b/src/client/pages/settings/word-mute.vue
deleted file mode 100644
index 53948b1b1e..0000000000
--- a/src/client/pages/settings/word-mute.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<template>
-<div>
-	<MkTab v-model="tab">
-		<option value="soft">{{ $ts._wordMute.soft }}</option>
-		<option value="hard">{{ $ts._wordMute.hard }}</option>
-	</MkTab>
-	<FormBase>
-		<div class="_debobigegoItem">
-			<div v-show="tab === 'soft'">
-				<FormInfo>{{ $ts._wordMute.softDescription }}</FormInfo>
-				<FormTextarea v-model="softMutedWords">
-					<span>{{ $ts._wordMute.muteWords }}</span>
-					<template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
-				</FormTextarea>
-			</div>
-			<div v-show="tab === 'hard'">
-				<FormInfo>{{ $ts._wordMute.hardDescription }}</FormInfo>
-				<FormTextarea v-model="hardMutedWords">
-					<span>{{ $ts._wordMute.muteWords }}</span>
-					<template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
-				</FormTextarea>
-				<FormKeyValueView v-if="hardWordMutedNotesCount != null">
-					<template #key>{{ $ts._wordMute.mutedNotes }}</template>
-					<template #value>{{ number(hardWordMutedNotesCount) }}</template>
-				</FormKeyValueView>
-			</div>
-		</div>
-		<FormButton @click="save()" primary inline :disabled="!changed"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-	</FormBase>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormInfo from '@client/components/debobigego/info.vue';
-import MkTab from '@client/components/tab.vue';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		FormTextarea,
-		FormKeyValueView,
-		MkTab,
-		FormInfo,
-	},
-
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.wordMute,
-				icon: 'fas fa-comment-slash',
-				bg: 'var(--bg)',
-			},
-			tab: 'soft',
-			softMutedWords: '',
-			hardMutedWords: '',
-			hardWordMutedNotesCount: null,
-			changed: false,
-		}
-	},
-
-	watch: {
-		softMutedWords: {
-			handler() {
-				this.changed = true;
-			},
-			deep: true
-		},
-		hardMutedWords: {
-			handler() {
-				this.changed = true;
-			},
-			deep: true
-		},
-	},
-
-	async created() {
-		this.softMutedWords = this.$store.state.mutedWords.map(x => x.join(' ')).join('\n');
-		this.hardMutedWords = this.$i.mutedWords.map(x => x.join(' ')).join('\n');
-
-		this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
-	},
-
-	mounted() {
-		this.$emit('info', this[symbols.PAGE_INFO]);
-	},
-
-	methods: {
-		async save() {
-			this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')));
-			await os.api('i/update', {
-				mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
-			});
-			this.changed = false;
-		},
-
-		number
-	}
-});
-</script>
diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue
deleted file mode 100644
index cb7347fae6..0000000000
--- a/src/client/pages/share.vue
+++ /dev/null
@@ -1,184 +0,0 @@
-<template>
-<div class="">
-	<section class="_section">
-		<div class="_content">
-			<XPostForm
-				v-if="state === 'writing'"
-				fixed
-				:share="true"
-				:initial-text="initialText"
-				:initial-visibility="visibility"
-				:initial-files="files"
-				:initial-local-only="localOnly"
-				:reply="reply"
-				:renote="renote"
-				:visible-users="visibleUsers"
-				@posted="state = 'posted'"
-				class="_panel"
-			/>
-			<MkButton v-else-if="state === 'posted'" primary @click="close()" class="close">{{ $ts.close }}</MkButton>
-		</div>
-	</section>
-</div>
-</template>
-
-<script lang="ts">
-// SPECIFICATION: https://misskey-hub.net/docs/features/share-form.html
-
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import XPostForm from '@client/components/post-form.vue';
-import * as os from '@client/os';
-import { noteVisibilities } from '@/types';
-import { parseAcct } from '@/misc/acct';
-import * as symbols from '@client/symbols';
-import * as Misskey from 'misskey-js';
-
-export default defineComponent({
-	components: {
-		XPostForm,
-		MkButton,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.share,
-				icon: 'fas fa-share-alt'
-			},
-			state: 'fetching' as 'fetching' | 'writing' | 'posted',
-
-			title: null as string | null,
-			initialText: null as string | null,
-			reply: null as Misskey.entities.Note | null,
-			renote: null as Misskey.entities.Note | null,
-			visibility: null as string | null,
-			localOnly: null as boolean | null,
-			files: [] as Misskey.entities.DriveFile[],
-			visibleUsers: [] as Misskey.entities.User[],
-		}
-	},
-
-	async created() {
-		const urlParams = new URLSearchParams(window.location.search);
-
-		this.title = urlParams.get('title');
-		const text = urlParams.get('text');
-		const url = urlParams.get('url');
-
-		let noteText = '';
-		if (this.title) noteText += `[ ${this.title} ]\n`;
-		// Googleニュース対策
-		if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, '');
-		else if (text && this.title !== text) noteText += `${text}\n`;
-		if (url) noteText += `${url}`;
-		this.initialText = noteText.trim();
-
-		const visibility = urlParams.get('visibility');
-		if (noteVisibilities.includes(visibility)) {
-			this.visibility = visibility;
-		}
-
-		if (this.visibility === 'specified') {
-			const visibleUserIds = urlParams.get('visibleUserIds');
-			const visibleAccts = urlParams.get('visibleAccts');
-			await Promise.all(
-				[
-					...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []),
-					...(visibleAccts ? visibleAccts.split(',').map(parseAcct) : [])
-				]
-				// TypeScriptの指示通りに変換する
-				.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
-				.map(q => os.api('users/show', q)
-					.then(user => {
-						this.visibleUsers.push(user);
-					}, () => {
-						console.error(`Invalid user query: ${JSON.stringify(q)}`);
-					})
-				)
-			);
-		}
-
-		const localOnly = urlParams.get('localOnly');
-		if (localOnly === '0') this.localOnly = false;
-		else if (localOnly === '1') this.localOnly = true;
-
-		try {
-			//#region Reply
-			const replyId = urlParams.get('replyId');
-			const replyUri = urlParams.get('replyUri');
-			if (replyId) {
-				this.reply = await os.api('notes/show', {
-					noteId: replyId
-				});
-			} else if (replyUri) {
-				const obj = await os.api('ap/show', {
-					uri: replyUri
-				});
-				if (obj.type === 'Note') {
-					this.reply = obj.object;
-				}
-			}
-			//#endregion
-
-			//#region Renote
-			const renoteId = urlParams.get('renoteId');
-			const renoteUri = urlParams.get('renoteUri');
-			if (renoteId) {
-				this.renote = await os.api('notes/show', {
-					noteId: renoteId
-				});
-			} else if (renoteUri) {
-				const obj = await os.api('ap/show', {
-					uri: renoteUri
-				});
-				if (obj.type === 'Note') {
-					this.renote = obj.object;
-				}
-			}
-			//#endregion
-
-			//#region Drive files
-			const fileIds = urlParams.get('fileIds');
-			if (fileIds) {
-				await Promise.all(
-					fileIds.split(',')
-					.map(fileId => os.api('drive/files/show', { fileId })
-						.then(file => {
-							this.files.push(file);
-						}, () => {
-							console.error(`Failed to fetch a file ${fileId}`);
-						})
-					)
-				);
-			}
-			//#endregion
-		} catch (e) {
-			os.dialog({
-				type: 'error',
-				title: e.message,
-				text: e.name
-			});
-		}
-
-		this.state = 'writing';
-	},
-
-	methods: {
-		close() {
-			window.close();
-
-			// 閉じなければ100ms後タイムラインに
-			setTimeout(() => {
-				this.$router.push('/');
-			}, 100);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.close {
-	margin: 16px auto;
-}
-</style>
diff --git a/src/client/pages/signup-complete.vue b/src/client/pages/signup-complete.vue
deleted file mode 100644
index dada92031a..0000000000
--- a/src/client/pages/signup-complete.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<div>
-	{{ $ts.processing }}
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-import { login } from '@client/account';
-
-export default defineComponent({
-	components: {
-
-	},
-
-	props: {
-		code: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.signup,
-				icon: 'fas fa-user'
-			},
-		}
-	},
-
-	mounted() {
-		os.apiWithDialog('signup-pending', {
-			code: this.code,
-		}).then(res => {
-			login(res.i, '/');
-		});
-	},
-
-	methods: {
-
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/tag.vue b/src/client/pages/tag.vue
deleted file mode 100644
index 3ca9fe5c0c..0000000000
--- a/src/client/pages/tag.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template>
-<div class="_section">
-	<XNotes ref="notes" class="_content" :pagination="pagination" @before="before" @after="after"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XNotes from '@client/components/notes.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes
-	},
-
-	props: {
-		tag: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.tag,
-				icon: 'fas fa-hashtag'
-			},
-			pagination: {
-				endpoint: 'notes/search-by-tag',
-				limit: 10,
-				params: () => ({
-					tag: this.tag,
-				})
-			},
-		};
-	},
-
-	watch: {
-		tag() {
-			(this.$refs.notes as any).reload();
-		}
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/test.vue b/src/client/pages/test.vue
deleted file mode 100644
index fbab0112ed..0000000000
--- a/src/client/pages/test.vue
+++ /dev/null
@@ -1,259 +0,0 @@
-<template>
-<div class="_section">
-	<div class="_content">
-		<div class="_card _gap">
-			<div class="_title">Dialog</div>
-			<div class="_content">
-				<MkInput v-model="dialogTitle">
-					<template #label>Title</template>
-				</MkInput>
-				<MkInput v-model="dialogBody">
-					<template #label>Body</template>
-				</MkInput>
-				<MkRadio v-model="dialogType" value="info">Info</MkRadio>
-				<MkRadio v-model="dialogType" value="success">Success</MkRadio>
-				<MkRadio v-model="dialogType" value="warning">Warn</MkRadio>
-				<MkRadio v-model="dialogType" value="error">Error</MkRadio>
-				<MkSwitch v-model="dialogCancel">
-					<span>With cancel button</span>
-				</MkSwitch>
-				<MkSwitch v-model="dialogCancelByBgClick">
-					<span>Can cancel by modal bg click</span>
-				</MkSwitch>
-				<MkSwitch v-model="dialogInput">
-					<span>With input field</span>
-				</MkSwitch>
-				<MkButton @click="showDialog()">Show</MkButton>
-			</div>
-			<div class="_content">
-				<code>Result: {{ dialogResult }}</code>
-			</div>
-		</div>
-
-		<div class="_card _gap">
-			<div class="_title">Form</div>
-			<div class="_content">
-				<MkInput v-model="formTitle">
-					<template #label>Title</template>
-				</MkInput>
-				<MkTextarea v-model="formForm">
-					<template #label>Form</template>
-				</MkTextarea>
-				<MkButton @click="form()">Show</MkButton>
-			</div>
-			<div class="_content">
-				<code>Result: {{ formResult }}</code>
-			</div>
-		</div>
-
-		<div class="_card _gap">
-			<div class="_title">MFM</div>
-			<div class="_content">
-				<MkTextarea v-model="mfm">
-					<template #label>MFM</template>
-				</MkTextarea>
-			</div>
-			<div class="_content">
-				<Mfm :text="mfm"/>
-			</div>
-		</div>
-
-		<div class="_card _gap">
-			<div class="_title">selectDriveFile</div>
-			<div class="_content">
-				<MkSwitch v-model="selectDriveFileMultiple">
-					<span>Multiple</span>
-				</MkSwitch>
-				<MkButton @click="selectDriveFile()">selectDriveFile</MkButton>
-			</div>
-			<div class="_content">
-				<code>Result: {{ JSON.stringify(selectDriveFileResult) }}</code>
-			</div>
-		</div>
-
-		<div class="_card _gap">
-			<div class="_title">selectDriveFolder</div>
-			<div class="_content">
-				<MkSwitch v-model="selectDriveFolderMultiple">
-					<span>Multiple</span>
-				</MkSwitch>
-				<MkButton @click="selectDriveFolder()">selectDriveFolder</MkButton>
-			</div>
-			<div class="_content">
-				<code>Result: {{ JSON.stringify(selectDriveFolderResult) }}</code>
-			</div>
-		</div>
-
-		<div class="_card _gap">
-			<div class="_title">selectUser</div>
-			<div class="_content">
-				<MkButton @click="selectUser()">selectUser</MkButton>
-			</div>
-			<div class="_content">
-				<code>Result: {{ user }}</code>
-			</div>
-		</div>
-
-		<div class="_card _gap">
-			<div class="_title">Notification</div>
-			<div class="_content">
-				<MkInput v-model="notificationIconUrl">
-					<template #label>Icon URL</template>
-				</MkInput>
-				<MkInput v-model="notificationHeader">
-					<template #label>Header</template>
-				</MkInput>
-				<MkTextarea v-model="notificationBody">
-					<template #label>Body</template>
-				</MkTextarea>
-				<MkButton @click="createNotification()">createNotification</MkButton>
-			</div>
-		</div>
-
-		<div class="_card _gap">
-			<div class="_title">Waiting dialog</div>
-			<div class="_content">
-				<MkButton inline @click="openWaitingDialog()">icon only</MkButton>
-				<MkButton inline @click="openWaitingDialog('Doing')">with text</MkButton>
-			</div>
-		</div>
-
-		<div class="_card _gap">
-			<div class="_title">Messaging window</div>
-			<div class="_content">
-				<MkButton @click="messagingWindowOpen()">open</MkButton>
-			</div>
-		</div>
-
-		<MkButton @click="resetTutorial()">Reset tutorial</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import MkSwitch from '@client/components/form/switch.vue';
-import MkTextarea from '@client/components/form/textarea.vue';
-import MkRadio from '@client/components/form/radio.vue';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-		MkSwitch,
-		MkTextarea,
-		MkRadio,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'TEST',
-				icon: 'fas fa-exclamation-triangle'
-			},
-			dialogTitle: 'Hello',
-			dialogBody: 'World!',
-			dialogType: 'info',
-			dialogCancel: false,
-			dialogCancelByBgClick: true,
-			dialogInput: false,
-			dialogResult: null,
-			formTitle: 'Test form',
-			formForm: JSON.stringify({
-				foo: {
-					type: 'boolean',
-					default: true,
-					label: 'This is a boolean property'
-				},
-				bar: {
-					type: 'number',
-					default: 300,
-					label: 'This is a number property'
-				},
-				baz: {
-					type: 'string',
-					default: 'Misskey makes you happy.',
-					label: 'This is a string property'
-				},
-				qux: {
-					type: 'string',
-					multiline: true,
-					default: 'Misskey makes\nyou happy.',
-					label: 'Multiline string'
-				},
-			}, null, '\t'),
-			formResult: null,
-			mfm: '',
-			selectDriveFileMultiple: false,
-			selectDriveFolderMultiple: false,
-			selectDriveFileResult: null,
-			selectDriveFolderResult: null,
-			user: null,
-			notificationIconUrl: null,
-			notificationHeader: '',
-			notificationBody: '',
-		}
-	},
-
-	methods: {
-		async showDialog() {
-			this.dialogResult = null;
-			this.dialogResult = await os.dialog({
-				type: this.dialogType,
-				title: this.dialogTitle,
-				text: this.dialogBody,
-				showCancelButton: this.dialogCancel,
-				cancelableByBgClick: this.dialogCancelByBgClick,
-				input: this.dialogInput ? {} : null
-			});
-		},
-
-		async form() {
-			this.formResult = null;
-			this.formResult = await os.form(this.formTitle, JSON.parse(this.formForm));
-		},
-
-		async selectDriveFile() {
-			this.selectDriveFileResult = null;
-			this.selectDriveFileResult = await os.selectDriveFile(this.selectDriveFileMultiple);
-		},
-
-		async selectDriveFolder() {
-			this.selectDriveFolderResult = null;
-			this.selectDriveFolderResult = await os.selectDriveFolder(this.selectDriveFolderMultiple);
-		},
-
-		async selectUser() {
-			this.user = null;
-			this.user = await os.selectUser();
-		},
-
-		async createNotification() {
-			os.api('notifications/create', {
-				header: this.notificationHeader,
-				body: this.notificationBody,
-				icon: this.notificationIconUrl,
-			});
-		},
-
-		messagingWindowOpen() {
-			os.pageWindow('/my/messaging');
-		},
-
-		openWaitingDialog(text?) {
-			const promise = new Promise((resolve, reject) => {
-				setTimeout(resolve, 2000);
-			});
-			os.promiseDialog(promise, null, null, text);
-		},
-
-		resetTutorial() {
-			this.$store.set('tutorial', 0);
-		},
-	}
-});
-</script>
diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue
deleted file mode 100644
index 3b10396ab8..0000000000
--- a/src/client/pages/theme-editor.vue
+++ /dev/null
@@ -1,306 +0,0 @@
-<template>
-<FormBase class="cwepdizn">
-	<div class="_debobigegoItem colorPicker">
-		<div class="_debobigegoLabel">{{ $ts.backgroundColor }}</div>
-		<div class="_debobigegoPanel colors">
-			<div class="row">
-				<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
-					<div class="preview" :style="{ background: color.forPreview }"></div>
-				</button>
-			</div>
-			<div class="row">
-				<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
-					<div class="preview" :style="{ background: color.forPreview }"></div>
-				</button>
-			</div>
-		</div>
-	</div>
-	<div class="_debobigegoItem colorPicker">
-		<div class="_debobigegoLabel">{{ $ts.accentColor }}</div>
-		<div class="_debobigegoPanel colors">
-			<div class="row">
-				<button v-for="color in accentColors" :key="color" @click="setAccentColor(color)" class="color rounded _button" :class="{ active: theme.props.accent === color }">
-					<div class="preview" :style="{ background: color }"></div>
-				</button>
-			</div>
-		</div>
-	</div>
-	<div class="_debobigegoItem colorPicker">
-		<div class="_debobigegoLabel">{{ $ts.textColor }}</div>
-		<div class="_debobigegoPanel colors">
-			<div class="row">
-				<button v-for="color in fgColors" :key="color" @click="setFgColor(color)" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }">
-					<div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
-				</button>
-			</div>
-		</div>
-	</div>
-
-	<FormGroup v-if="codeEnabled">
-		<FormTextarea v-model="themeCode" tall>
-			<span>{{ $ts._theme.code }}</span>
-		</FormTextarea>
-		<FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton>
-	</FormGroup>
-	<FormButton v-else @click="codeEnabled = true"><i class="fas fa-code"></i> {{ $ts.editCode }}</FormButton>
-
-	<FormGroup v-if="descriptionEnabled">
-		<FormTextarea v-model="description">
-			<span>{{ $ts._theme.description }}</span>
-		</FormTextarea>
-	</FormGroup>
-	<FormButton v-else @click="descriptionEnabled = true">{{ $ts.addDescription }}</FormButton>
-
-	<FormGroup>
-		<FormButton @click="showPreview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
-		<FormButton @click="saveAs" primary><i class="fas fa-save"></i> {{ $ts.saveAs }}</FormButton>
-	</FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { toUnicode } from 'punycode/';
-import * as tinycolor from 'tinycolor2';
-import { v4 as uuid} from 'uuid';
-import * as JSON5 from 'json5';
-
-import FormBase from '@client/components/debobigego/base.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-
-import { Theme, applyTheme, validateTheme, darkTheme, lightTheme } from '@client/scripts/theme';
-import { host } from '@client/config';
-import * as os from '@client/os';
-import { ColdDeviceStorage } from '@client/store';
-import { addTheme } from '@client/theme-store';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormButton,
-		FormTextarea,
-		FormGroup,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.themeEditor,
-				icon: 'fas fa-palette',
-			},
-			theme: {
-				base: 'light',
-				props: lightTheme.props
-			} as Theme,
-			codeEnabled: false,
-			descriptionEnabled: false,
-			description: null,
-			themeCode: null,
-			bgColors: [
-				{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
-				{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
-				{ color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' },
-				{ color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' },
-				{ color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' },
-				{ color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' },
-				{ color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' },
-				{ color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' },
-				{ color: '#2b2b2b', kind: 'dark', forPreview: '#444444' },
-				{ color: '#362e29', kind: 'dark', forPreview: '#735c4d' },
-				{ color: '#303629', kind: 'dark', forPreview: '#506d2f' },
-				{ color: '#293436', kind: 'dark', forPreview: '#258192' },
-				{ color: '#2e2936', kind: 'dark', forPreview: '#504069' },
-				{ color: '#252722', kind: 'dark', forPreview: '#3c462f' },
-				{ color: '#212525', kind: 'dark', forPreview: '#303e3e' },
-				{ color: '#191919', kind: 'dark', forPreview: '#272727' },
-			],
-			accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'],
-			fgColors: [
-				{ color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
-				{ color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
-				{ color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' },
-				{ color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' },
-				{ color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' },
-				{ color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
-				{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
-			],
-			changed: false,
-		}
-	},
-
-	created() {
-		this.$watch('theme', this.apply, { deep: true });
-		window.addEventListener('beforeunload', this.beforeunload);
-	},
-
-	beforeUnmount() {
-		window.removeEventListener('beforeunload', this.beforeunload);
-	},
-
-	async beforeRouteLeave(to, from) {
-		if (this.changed && !(await this.leaveConfirm())) {
-			return false;
-		}
-	},
-
-	methods: {
-		beforeunload(e: BeforeUnloadEvent) {
-			if (this.changed) {
-				e.preventDefault();
-				e.returnValue = '';
-			}
-		},
-
-		async leaveConfirm(): Promise<boolean> {
-			const { canceled } = await os.dialog({
-				type: 'warning',
-				text: this.$ts.leaveConfirm,
-				showCancelButton: true
-			});
-			return !canceled;
-		},
-
-		showPreview() {
-			os.pageWindow('preview');
-		},
-
-		setBgColor(color) {
-			if (this.theme.base != color.kind) {
-				const base = color.kind === 'dark' ? darkTheme : lightTheme;
-				for (const prop of Object.keys(base.props)) {
-					if (prop === 'accent') continue;
-					if (prop === 'fg') continue;
-					this.theme.props[prop] = base.props[prop];
-				}
-			}
-			this.theme.base = color.kind;
-			this.theme.props.bg = color.color;
-
-			if (this.theme.props.fg) {
-				const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(this.theme.props.fg).toRgbString()));
-				if (matchedFgColor) this.setFgColor(matchedFgColor);
-			}
-		},
-
-		setAccentColor(color) {
-			this.theme.props.accent = color;
-		},
-
-		setFgColor(color) {
-			this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark;
-		},
-
-		apply() {
-			this.themeCode = JSON5.stringify(this.theme, null, '\t');
-			applyTheme(this.theme, false);
-			this.changed = true;
-		},
-
-		applyThemeCode() {
-			let parsed;
-
-			try {
-				parsed = JSON5.parse(this.themeCode);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: this.$ts._theme.invalid
-				});
-				return;
-			}
-
-			this.theme = parsed;
-		},
-
-		async saveAs() {
-			const { canceled, result: name } = await os.dialog({
-				title: this.$ts.name,
-				input: {
-					allowEmpty: false
-				}
-			});
-			if (canceled) return;
-
-			this.theme.id = uuid();
-			this.theme.name = name;
-			this.theme.author = `@${this.$i.username}@${toUnicode(host)}`;
-			if (this.description) this.theme.desc = this.description;
-			addTheme(this.theme);
-			applyTheme(this.theme);
-			if (this.$store.state.darkMode) {
-				ColdDeviceStorage.set('darkTheme', this.theme);
-			} else {
-				ColdDeviceStorage.set('lightTheme', this.theme);
-			}
-			this.changed = false;
-			os.dialog({
-				type: 'success',
-				text: this.$t('_theme.installed', { name: this.theme.name })
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.cwepdizn {
-	max-width: 800px;
-	margin: 0 auto;
-
-	> .colorPicker {
-		> .colors {
-			padding: 32px;
-			text-align: center;
-
-			> .row {
-				> .color {
-					display: inline-block;
-					position: relative;
-					width: 64px;
-					height: 64px;
-					border-radius: 8px;
-
-					> .preview {
-						position: absolute;
-						top: 0;
-						left: 0;
-						right: 0;
-						bottom: 0;
-						margin: auto;
-						width: 42px;
-						height: 42px;
-						border-radius: 4px;
-						box-shadow: 0 2px 4px rgb(0 0 0 / 30%);
-						transition: transform 0.15s ease;
-					}
-
-					&:hover {
-						> .preview {
-							transform: scale(1.1);
-						}
-					}
-
-					&.active {
-						box-shadow: 0 0 0 2px var(--divider) inset;
-					}
-
-					&.rounded {
-						border-radius: 999px;
-
-						> .preview {
-							border-radius: 999px;
-						}
-					}
-
-					&.char {
-						line-height: 42px;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/timeline.tutorial.vue b/src/client/pages/timeline.tutorial.vue
deleted file mode 100644
index 4d10289fbf..0000000000
--- a/src/client/pages/timeline.tutorial.vue
+++ /dev/null
@@ -1,131 +0,0 @@
-<template>
-<div class="_card tbkwesmv">
-	<div class="_title"><i class="fas fa-info-circle"></i> {{ $ts._tutorial.title }}</div>
-	<div class="_content" v-if="tutorial === 0">
-		<div>{{ $ts._tutorial.step1_1 }}</div>
-		<div>{{ $ts._tutorial.step1_2 }}</div>
-		<div>{{ $ts._tutorial.step1_3 }}</div>
-	</div>
-	<div class="_content" v-else-if="tutorial === 1">
-		<div>{{ $ts._tutorial.step2_1 }}</div>
-		<div>{{ $ts._tutorial.step2_2 }}</div>
-		<MkA class="_link" to="/settings/profile">{{ $ts.editProfile }}</MkA>
-	</div>
-	<div class="_content" v-else-if="tutorial === 2">
-		<div>{{ $ts._tutorial.step3_1 }}</div>
-		<div>{{ $ts._tutorial.step3_2 }}</div>
-		<div>{{ $ts._tutorial.step3_3 }}</div>
-		<small>{{ $ts._tutorial.step3_4 }}</small>
-	</div>
-	<div class="_content" v-else-if="tutorial === 3">
-		<div>{{ $ts._tutorial.step4_1 }}</div>
-		<div>{{ $ts._tutorial.step4_2 }}</div>
-	</div>
-	<div class="_content" v-else-if="tutorial === 4">
-		<div>{{ $ts._tutorial.step5_1 }}</div>
-		<I18n :src="$ts._tutorial.step5_2" tag="div">
-			<template #featured>
-				<MkA class="_link" to="/featured">{{ $ts.featured }}</MkA>
-			</template>
-			<template #explore>
-				<MkA class="_link" to="/explore">{{ $ts.explore }}</MkA>
-			</template>
-		</I18n>
-		<div>{{ $ts._tutorial.step5_3 }}</div>
-		<small>{{ $ts._tutorial.step5_4 }}</small>
-	</div>
-	<div class="_content" v-else-if="tutorial === 5">
-		<div>{{ $ts._tutorial.step6_1 }}</div>
-		<div>{{ $ts._tutorial.step6_2 }}</div>
-		<div>{{ $ts._tutorial.step6_3 }}</div>
-	</div>
-	<div class="_content" v-else-if="tutorial === 6">
-		<div>{{ $ts._tutorial.step7_1 }}</div>
-		<I18n :src="$ts._tutorial.step7_2" tag="div">
-			<template #help>
-				<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ $ts.help }}</a>
-			</template>
-		</I18n>
-		<div>{{ $ts._tutorial.step7_3 }}</div>
-	</div>
-
-	<div class="_footer navigation">
-		<div class="step">
-			<button class="arrow _button" @click="tutorial--" :disabled="tutorial === 0">
-				<i class="fas fa-chevron-left"></i>
-			</button>
-			<span>{{ tutorial + 1 }} / 7</span>
-			<button class="arrow _button" @click="tutorial++" :disabled="tutorial === 6">
-				<i class="fas fa-chevron-right"></i>
-			</button>
-		</div>
-		<MkButton class="ok" @click="tutorial = -1" primary v-if="tutorial === 6"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
-		<MkButton class="ok" @click="tutorial++" primary v-else><i class="fas fa-check"></i> {{ $ts.next }}</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-
-	data() {
-		return {
-		}
-	},
-
-	computed: {
-		tutorial: {
-			get() { return this.$store.reactiveState.tutorial.value || 0; },
-			set(value) { this.$store.set('tutorial', value); }
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.tbkwesmv {
-	> ._content {
-		> small {
-			opacity: 0.7;
-		}
-	}
-
-	> .navigation {
-		display: flex;
-		flex-direction: row;
-		align-items: baseline;
-
-		> .step {
-			> .arrow {
-				padding: 4px;
-
-				&:disabled {
-					opacity: 0.5;
-				}
-
-				&:first-child {
-					padding-right: 8px;
-				}
-
-				&:last-child {
-					padding-left: 8px;
-				}
-			}
-
-			> span {
-				margin: 0 4px;
-			}
-		}
-
-		> .ok {
-			margin-left: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue
deleted file mode 100644
index 7b17d585f8..0000000000
--- a/src/client/pages/timeline.vue
+++ /dev/null
@@ -1,225 +0,0 @@
-<template>
-<div class="cmuxhskf" v-size="{ min: [800] }" v-hotkey.global="keymap">
-	<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
-	<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
-
-	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
-	<div class="tl _block">
-		<XTimeline ref="tl" class="tl"
-			:key="src"
-			:src="src"
-			:sound="true"
-			@before="before()"
-			@after="after()"
-			@queue="queueUpdated"
-		/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, computed } from 'vue';
-import Progress from '@client/scripts/loading';
-import XTimeline from '@client/components/timeline.vue';
-import XPostForm from '@client/components/post-form.vue';
-import { scroll } from '@client/scripts/scroll';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	name: 'timeline',
-
-	components: {
-		XTimeline,
-		XTutorial: defineAsyncComponent(() => import('./timeline.tutorial.vue')),
-		XPostForm,
-	},
-
-	data() {
-		return {
-			src: 'home',
-			queue: 0,
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.timeline,
-				icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home',
-				bg: 'var(--bg)',
-				actions: [{
-					icon: 'fas fa-list-ul',
-					text: this.$ts.lists,
-					handler: this.chooseList
-				}, {
-					icon: 'fas fa-satellite',
-					text: this.$ts.antennas,
-					handler: this.chooseAntenna
-				}, {
-					icon: 'fas fa-satellite-dish',
-					text: this.$ts.channel,
-					handler: this.chooseChannel
-				}, {
-					icon: 'fas fa-calendar-alt',
-					text: this.$ts.jumpToSpecifiedDate,
-					handler: this.timetravel
-				}],
-				tabs: [{
-					active: this.src === 'home',
-					title: this.$ts._timelines.home,
-					icon: 'fas fa-home',
-					iconOnly: true,
-					onClick: () => { this.src = 'home'; this.saveSrc(); },
-				}, {
-					active: this.src === 'local',
-					title: this.$ts._timelines.local,
-					icon: 'fas fa-comments',
-					iconOnly: true,
-					onClick: () => { this.src = 'local'; this.saveSrc(); },
-				}, {
-					active: this.src === 'social',
-					title: this.$ts._timelines.social,
-					icon: 'fas fa-share-alt',
-					iconOnly: true,
-					onClick: () => { this.src = 'social'; this.saveSrc(); },
-				}, {
-					active: this.src === 'global',
-					title: this.$ts._timelines.global,
-					icon: 'fas fa-globe',
-					iconOnly: true,
-					onClick: () => { this.src = 'global'; this.saveSrc(); },
-				}],
-			})),
-		};
-	},
-
-	computed: {
-		keymap(): any {
-			return {
-				't': this.focus
-			};
-		},
-
-		isLocalTimelineAvailable(): boolean {
-			return !this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin;
-		},
-
-		isGlobalTimelineAvailable(): boolean {
-			return !this.$instance.disableGlobalTimeline || this.$i.isModerator || this.$i.isAdmin;
-		},
-	},
-
-	watch: {
-		src() {
-			this.showNav = false;
-		},
-	},
-
-	created() {
-		this.src = this.$store.state.tl.src;
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		},
-
-		queueUpdated(q) {
-			this.queue = q;
-		},
-
-		top() {
-			scroll(this.$el, { top: 0 });
-		},
-
-		async chooseList(ev) {
-			const lists = await os.api('users/lists/list');
-			const items = lists.map(list => ({
-				type: 'link',
-				text: list.name,
-				to: `/timeline/list/${list.id}`
-			}));
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		},
-
-		async chooseAntenna(ev) {
-			const antennas = await os.api('antennas/list');
-			const items = antennas.map(antenna => ({
-				type: 'link',
-				text: antenna.name,
-				indicate: antenna.hasUnreadNote,
-				to: `/timeline/antenna/${antenna.id}`
-			}));
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		},
-
-		async chooseChannel(ev) {
-			const channels = await os.api('channels/followed');
-			const items = channels.map(channel => ({
-				type: 'link',
-				text: channel.name,
-				indicate: channel.hasUnreadNote,
-				to: `/channels/${channel.id}`
-			}));
-			os.popupMenu(items, ev.currentTarget || ev.target);
-		},
-
-		saveSrc() {
-			this.$store.set('tl', {
-				src: this.src,
-			});
-		},
-
-		async timetravel() {
-			const { canceled, result: date } = await os.dialog({
-				title: this.$ts.date,
-				input: {
-					type: 'date'
-				}
-			});
-			if (canceled) return;
-
-			this.$refs.tl.timetravel(new Date(date));
-		},
-
-		focus() {
-			(this.$refs.tl as any).focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.cmuxhskf {
-	padding: var(--margin);
-
-	> .new {
-		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 16px);
-		z-index: 1000;
-		width: 100%;
-
-		> button {
-			display: block;
-			margin: var(--margin) auto 0 auto;
-			padding: 8px 16px;
-			border-radius: 32px;
-		}
-	}
-
-	> .post-form {
-		border-radius: var(--radius);
-	}
-
-	> .tl {
-		background: var(--bg);
-		border-radius: var(--radius);
-		overflow: clip;
-	}
-
-	&.min-width_800px {
-		max-width: 800px;
-		margin: 0 auto;
-	}
-}
-</style>
diff --git a/src/client/pages/user-ap-info.vue b/src/client/pages/user-ap-info.vue
deleted file mode 100644
index cbdff874ed..0000000000
--- a/src/client/pages/user-ap-info.vue
+++ /dev/null
@@ -1,124 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="apPromiseFactory" v-slot="{ result: ap }">
-		<FormGroup>
-			<template #label>ActivityPub</template>
-			<FormKeyValueView>
-				<template #key>Type</template>
-				<template #value><span class="_monospace">{{ ap.type }}</span></template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>URI</template>
-				<template #value><span class="_monospace">{{ ap.id }}</span></template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>URL</template>
-				<template #value><span class="_monospace">{{ ap.url }}</span></template>
-			</FormKeyValueView>
-			<FormGroup>
-				<FormKeyValueView>
-					<template #key>Inbox</template>
-					<template #value><span class="_monospace">{{ ap.inbox }}</span></template>
-				</FormKeyValueView>
-				<FormKeyValueView>
-					<template #key>Shared Inbox</template>
-					<template #value><span class="_monospace">{{ ap.sharedInbox || ap.endpoints.sharedInbox }}</span></template>
-				</FormKeyValueView>
-				<FormKeyValueView>
-					<template #key>Outbox</template>
-					<template #value><span class="_monospace">{{ ap.outbox }}</span></template>
-				</FormKeyValueView>
-			</FormGroup>
-			<FormTextarea readonly tall code pre :value="ap.publicKey.publicKeyPem">
-				<span>Public Key</span>
-			</FormTextarea>
-			<FormKeyValueView>
-				<template #key>Discoverable</template>
-				<template #value>{{ ap.discoverable ? $ts.yes : $ts.no }}</template>
-			</FormKeyValueView>
-			<FormKeyValueView>
-				<template #key>ManuallyApprovesFollowers</template>
-				<template #value>{{ ap.manuallyApprovesFollowers ? $ts.yes : $ts.no }}</template>
-			</FormKeyValueView>
-			<FormObjectView tall :value="ap">
-				<span>Raw</span>
-			</FormObjectView>
-			<FormGroup>
-				<FormLink :to="`https://${user.host}/.well-known/webfinger?resource=acct:${user.username}`" external>WebFinger</FormLink>
-			</FormGroup>
-			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
-			<FormKeyValueView v-else>
-				<template #key>{{ $ts.instanceInfo }}</template>
-				<template #value>(Local user)</template>
-			</FormKeyValueView>
-		</FormGroup>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormObjectView from '@client/components/debobigego/object-view.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-import bytes from '@client/filters/bytes';
-import * as symbols from '@client/symbols';
-import { url } from '@client/config';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormTextarea,
-		FormObjectView,
-		FormButton,
-		FormLink,
-		FormGroup,
-		FormKeyValueView,
-		FormSuspense,
-	},
-
-	props: {
-		userId: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.userInfo,
-				icon: 'fas fa-info-circle'
-			},
-			user: null,
-			apPromiseFactory: null,
-		}
-	},
-
-	mounted() {
-		this.fetch();
-	},
-
-	methods: {
-		number,
-		bytes,
-
-		async fetch() {
-			this.user = await os.api('users/show', {
-				userId: this.userId
-			});
-
-			this.apPromiseFactory = () => os.api('ap/get', {
-				uri: this.user.uri || `${url}/users/${this.user.id}`
-			});
-		}
-	}
-});
-</script>
diff --git a/src/client/pages/user-info.vue b/src/client/pages/user-info.vue
deleted file mode 100644
index bf67fc853a..0000000000
--- a/src/client/pages/user-info.vue
+++ /dev/null
@@ -1,245 +0,0 @@
-<template>
-<FormBase>
-	<FormSuspense :p="init">
-		<div class="_debobigegoItem aeakzknw">
-			<MkAvatar class="avatar" :user="user" :show-indicator="true"/>
-		</div>
-
-		<FormLink :to="userPage(user)">Profile</FormLink>
-
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>Acct</template>
-				<template #value><span class="_monospace">{{ acct(user) }}</span></template>
-			</FormKeyValueView>
-
-			<FormKeyValueView>
-				<template #key>ID</template>
-				<template #value><span class="_monospace">{{ user.id }}</span></template>
-			</FormKeyValueView>
-		</FormGroup>
-
-		<FormGroup v-if="iAmModerator">
-			<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:modelValue="toggleModerator" v-model="moderator">{{ $ts.moderator }}</FormSwitch>
-			<FormSwitch @update:modelValue="toggleSilence" v-model="silenced">{{ $ts.silence }}</FormSwitch>
-			<FormSwitch @update:modelValue="toggleSuspend" v-model="suspended">{{ $ts.suspend }}</FormSwitch>
-		</FormGroup>
-
-		<FormGroup>
-			<FormButton v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
-			<FormButton v-if="user.host == null && iAmModerator" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
-		</FormGroup>
-
-		<FormGroup>
-			<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink>
-
-			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
-			<FormKeyValueView v-else>
-				<template #key>{{ $ts.instanceInfo }}</template>
-				<template #value>(Local user)</template>
-			</FormKeyValueView>
-		</FormGroup>
-
-		<FormGroup>
-			<FormKeyValueView>
-				<template #key>{{ $ts.updatedAt }}</template>
-				<template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
-			</FormKeyValueView>
-		</FormGroup>
-
-		<FormObjectView tall :value="user">
-			<span>Raw</span>
-		</FormObjectView>
-	</FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent } from 'vue';
-import FormObjectView from '@client/components/debobigego/object-view.vue';
-import FormTextarea from '@client/components/debobigego/textarea.vue';
-import FormSwitch from '@client/components/debobigego/switch.vue';
-import FormLink from '@client/components/debobigego/link.vue';
-import FormBase from '@client/components/debobigego/base.vue';
-import FormGroup from '@client/components/debobigego/group.vue';
-import FormButton from '@client/components/debobigego/button.vue';
-import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
-import FormSuspense from '@client/components/debobigego/suspense.vue';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-import bytes from '@client/filters/bytes';
-import * as symbols from '@client/symbols';
-import { url } from '@client/config';
-import { userPage, acct } from '@client/filters/user';
-
-export default defineComponent({
-	components: {
-		FormBase,
-		FormTextarea,
-		FormSwitch,
-		FormObjectView,
-		FormButton,
-		FormLink,
-		FormGroup,
-		FormKeyValueView,
-		FormSuspense,
-	},
-
-	props: {
-		userId: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.user ? acct(this.user) : this.$ts.userInfo,
-				icon: 'fas fa-info-circle',
-				actions: this.user ? [this.user.url ? {
-					text: this.user.url,
-					icon: 'fas fa-external-link-alt',
-					handler: () => {
-						window.open(this.user.url, '_blank');
-					}
-				} : undefined].filter(x => x !== undefined) : [],
-			})),
-			init: null,
-			user: null,
-			info: null,
-			moderator: false,
-			silenced: false,
-			suspended: false,
-		}
-	},
-
-	computed: {
-		iAmModerator(): boolean {
-			return this.$i && (this.$i.isAdmin || this.$i.isModerator);
-		}
-	},
-
-	watch: {
-		userId: {
-			handler() {
-				this.init = this.createFetcher();
-			},
-			immediate: true
-		}
-	},
-
-	methods: {
-		number,
-		bytes,
-		userPage,
-		acct,
-
-		createFetcher() {
-			if (this.iAmModerator) {
-				return () => Promise.all([os.api('users/show', {
-					userId: this.userId
-				}), os.api('admin/show-user', {
-					userId: this.userId
-				})]).then(([user, info]) => {
-					this.user = user;
-					this.info = info;
-					this.moderator = this.info.isModerator;
-					this.silenced = this.info.isSilenced;
-					this.suspended = this.info.isSuspended;
-				});
-			} else {
-				return () => os.api('users/show', {
-					userId: this.userId
-				}).then((user) => {
-					this.user = user;
-				});
-			}
-		},
-
-		refreshUser() {
-			this.init = this.createFetcher();
-		},
-
-		async updateRemoteUser() {
-			await os.apiWithDialog('federation/update-remote-user', { userId: this.user.id });
-			this.refreshUser();
-		},
-
-		async resetPassword() {
-			const { password } = await os.api('admin/reset-password', {
-				userId: this.user.id,
-			});
-
-			os.dialog({
-				type: 'success',
-				text: this.$t('newPasswordIs', { password })
-			});
-		},
-
-		async toggleSilence(v) {
-			const confirm = await os.dialog({
-				type: 'warning',
-				showCancelButton: true,
-				text: v ? this.$ts.silenceConfirm : this.$ts.unsilenceConfirm,
-			});
-			if (confirm.canceled) {
-				this.silenced = !v;
-			} else {
-				await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
-				await this.refreshUser();
-			}
-		},
-
-		async toggleSuspend(v) {
-			const confirm = await os.dialog({
-				type: 'warning',
-				showCancelButton: true,
-				text: v ? this.$ts.suspendConfirm : this.$ts.unsuspendConfirm,
-			});
-			if (confirm.canceled) {
-				this.suspended = !v;
-			} else {
-				await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
-				await this.refreshUser();
-			}
-		},
-
-		async toggleModerator(v) {
-			await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id });
-			await this.refreshUser();
-		},
-
-		async deleteAllFiles() {
-			const confirm = await os.dialog({
-				type: 'warning',
-				showCancelButton: true,
-				text: this.$ts.deleteAllFilesConfirm,
-			});
-			if (confirm.canceled) return;
-			const process = async () => {
-				await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
-				os.success();
-			};
-			await process().catch(e => {
-				os.dialog({
-					type: 'error',
-					text: e.toString()
-				});
-			});
-			await this.refreshUser();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.aeakzknw {
-	> .avatar {
-		display: block;
-		margin: 0 auto;
-		width: 64px;
-		height: 64px;
-	}
-}
-</style>
diff --git a/src/client/pages/user-list-timeline.vue b/src/client/pages/user-list-timeline.vue
deleted file mode 100644
index b5e37d4843..0000000000
--- a/src/client/pages/user-list-timeline.vue
+++ /dev/null
@@ -1,147 +0,0 @@
-<template>
-<div class="eqqrhokj" v-hotkey.global="keymap" v-size="{ min: [800] }">
-	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
-	<div class="tl _block">
-		<XTimeline ref="tl" class="tl"
-			:key="listId"
-			src="list"
-			:list="listId"
-			:sound="true"
-			@before="before()"
-			@after="after()"
-			@queue="queueUpdated"
-		/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, computed } from 'vue';
-import Progress from '@client/scripts/loading';
-import XTimeline from '@client/components/timeline.vue';
-import { scroll } from '@client/scripts/scroll';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XTimeline,
-	},
-
-	props: {
-		listId: {
-			type: String,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			list: null,
-			queue: 0,
-			[symbols.PAGE_INFO]: computed(() => this.list ? {
-				title: this.list.name,
-				icon: 'fas fa-list-ul',
-				bg: 'var(--bg)',
-				actions: [{
-					icon: 'fas fa-calendar-alt',
-					text: this.$ts.jumpToSpecifiedDate,
-					handler: this.timetravel
-				}, {
-					icon: 'fas fa-cog',
-					text: this.$ts.settings,
-					handler: this.settings
-				}],
-			} : null),
-		};
-	},
-
-	computed: {
-		keymap(): any {
-			return {
-				't': this.focus
-			};
-		},
-	},
-
-	watch: {
-		listId: {
-			async handler() {
-				this.list = await os.api('users/lists/show', {
-					listId: this.listId
-				});
-			},
-			immediate: true
-		}
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		},
-
-		queueUpdated(q) {
-			this.queue = q;
-		},
-
-		top() {
-			scroll(this.$el, { top: 0 });
-		},
-
-		settings() {
-			this.$router.push(`/my/lists/${this.listId}`);
-		},
-
-		async timetravel() {
-			const { canceled, result: date } = await os.dialog({
-				title: this.$ts.date,
-				input: {
-					type: 'date'
-				}
-			});
-			if (canceled) return;
-
-			this.$refs.tl.timetravel(new Date(date));
-		},
-
-		focus() {
-			(this.$refs.tl as any).focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.eqqrhokj {
-	padding: var(--margin);
-
-	> .new {
-		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 16px);
-		z-index: 1000;
-		width: 100%;
-
-		> button {
-			display: block;
-			margin: var(--margin) auto 0 auto;
-			padding: 8px 16px;
-			border-radius: 32px;
-		}
-	}
-
-	> .tl {
-		background: var(--bg);
-		border-radius: var(--radius);
-		overflow: clip;
-	}
-
-	&.min-width_800px {
-		max-width: 800px;
-		margin: 0 auto;
-	}
-}
-</style>
diff --git a/src/client/pages/user/clips.vue b/src/client/pages/user/clips.vue
deleted file mode 100644
index 53ee554383..0000000000
--- a/src/client/pages/user/clips.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<div>
-	<MkPagination :pagination="pagination" #default="{items}" ref="list">
-		<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
-			<b>{{ item.name }}</b>
-			<div v-if="item.description" class="description">{{ item.description }}</div>
-		</MkA>
-	</MkPagination>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/clips',
-				limit: 20,
-				params: {
-					userId: this.user.id,
-				}
-			},
-		};
-	},
-
-	watch: {
-		user() {
-			this.$refs.list.reload();
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue
deleted file mode 100644
index 1f5ab5993c..0000000000
--- a/src/client/pages/user/follow-list.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div>
-	<MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list">
-		<div class="users _isolated">
-			<MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/>
-		</div>
-	</MkPagination>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkUserInfo from '@client/components/user-info.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkUserInfo,
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		type: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers',
-				limit: 20,
-				params: {
-					userId: this.user.id,
-				}
-			},
-		};
-	},
-
-	watch: {
-		type() {
-			this.$refs.list.reload();
-		},
-
-		user() {
-			this.$refs.list.reload();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-following-or-followers {
-	> .users {
-		display: grid;
-		grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-		grid-gap: var(--margin);
-	}
-}
-</style>
diff --git a/src/client/pages/user/gallery.vue b/src/client/pages/user/gallery.vue
deleted file mode 100644
index c21b3e6428..0000000000
--- a/src/client/pages/user/gallery.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-<div>
-	<MkPagination :pagination="pagination" #default="{items}">
-		<div class="jrnovfpt">
-			<MkGalleryPostPreview v-for="post in items" :post="post" :key="post.id" class="post"/>
-		</div>
-	</MkPagination>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkGalleryPostPreview,
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/gallery/posts',
-				limit: 6,
-				params: () => ({
-					userId: this.user.id
-				})
-			},
-		};
-	},
-
-	watch: {
-		user() {
-			this.$refs.list.reload();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.jrnovfpt {
-	display: grid;
-	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
-	grid-gap: 12px;
-	margin: var(--margin);
-}
-</style>
diff --git a/src/client/pages/user/index.activity.vue b/src/client/pages/user/index.activity.vue
deleted file mode 100644
index be85c252e8..0000000000
--- a/src/client/pages/user/index.activity.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-<MkContainer>
-	<template #header><i class="fas fa-chart-bar" style="margin-right: 0.5em;"></i>{{ $ts.activity }}</template>
-
-	<div style="padding: 8px;">
-		<MkChart src="per-user-notes" :args="{ user, withoutAll: true }" span="day" :limit="limit" :stacked="true" :detailed="false" :aspect-ratio="6"/>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import MkContainer from '@client/components/ui/container.vue';
-import MkChart from '@client/components/chart.vue';
-
-export default defineComponent({
-	components: {
-		MkContainer,
-		MkChart,
-	},
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		limit: {
-			type: Number,
-			required: false,
-			default: 40
-		}
-	},
-});
-</script>
diff --git a/src/client/pages/user/index.photos.vue b/src/client/pages/user/index.photos.vue
deleted file mode 100644
index 5029c3feec..0000000000
--- a/src/client/pages/user/index.photos.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<template>
-<MkContainer :max-height="300" :foldable="true">
-	<template #header><i class="fas fa-image" style="margin-right: 0.5em;"></i>{{ $ts.images }}</template>
-	<div class="ujigsodd">
-		<MkLoading v-if="fetching"/>
-		<div class="stream" v-if="!fetching && images.length > 0">
-			<MkA v-for="image in images"
-				class="img"
-				:to="notePage(image.note)"
-				:key="image.id"
-			>
-				<ImgWithBlurhash :hash="image.blurhash" :src="thumbnail(image.file)" :alt="image.name" :title="image.name"/>
-			</MkA>
-		</div>
-		<p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { getStaticImageUrl } from '@client/scripts/get-static-image-url';
-import notePage from '@client/filters/note';
-import * as os from '@client/os';
-import MkContainer from '@client/components/ui/container.vue';
-import ImgWithBlurhash from '@client/components/img-with-blurhash.vue';
-
-export default defineComponent({
-	components: {
-		MkContainer,
-		ImgWithBlurhash,
-	},
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-	data() {
-		return {
-			fetching: true,
-			images: [],
-		};
-	},
-	mounted() {
-		const image = [
-			'image/jpeg',
-			'image/png',
-			'image/gif',
-			'image/apng',
-			'image/vnd.mozilla.apng',
-		];
-		os.api('users/notes', {
-			userId: this.user.id,
-			fileType: image,
-			excludeNsfw: this.$store.state.nsfw !== 'ignore',
-			limit: 10,
-		}).then(notes => {
-			for (const note of notes) {
-				for (const file of note.files) {
-					this.images.push({
-						note,
-						file
-					});
-				}
-			}
-			this.fetching = false;
-		});
-	},
-	methods: {
-		thumbnail(image: any): string {
-			return this.$store.state.disableShowingAnimatedImages
-				? getStaticImageUrl(image.thumbnailUrl)
-				: image.thumbnailUrl;
-		},
-		notePage
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ujigsodd {
-	padding: 8px;
-
-	> .stream {
-		display: grid;
-		grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
-		grid-gap: 6px;
-
-		> .img {
-			height: 128px;
-			border-radius: 6px;
-			overflow: clip;
-		}
-	}
-
-	> .empty {
-		margin: 0;
-		padding: 16px;
-		text-align: center;
-
-		> i {
-			margin-right: 4px;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue
deleted file mode 100644
index c3444f26f6..0000000000
--- a/src/client/pages/user/index.timeline.vue
+++ /dev/null
@@ -1,68 +0,0 @@
-<template>
-<div class="yrzkoczt" v-sticky-container>
-	<MkTab v-model="with_" class="tab">
-		<option :value="null">{{ $ts.notes }}</option>
-		<option value="replies">{{ $ts.notesAndReplies }}</option>
-		<option value="files">{{ $ts.withFiles }}</option>
-	</MkTab>
-	<XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNotes from '@client/components/notes.vue';
-import MkTab from '@client/components/tab.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XNotes,
-		MkTab,
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true,
-		},
-	},
-
-	watch: {
-		user() {
-			this.$refs.timeline.reload();
-		},
-
-		with_() {
-			this.$refs.timeline.reload();
-		},
-	},
-
-	data() {
-		return {
-			date: null,
-			with_: null,
-			pagination: {
-				endpoint: 'users/notes',
-				limit: 10,
-				params: init => ({
-					userId: this.user.id,
-					includeReplies: this.with_ === 'replies',
-					withFiles: this.with_ === 'files',
-					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
-				})
-			}
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.yrzkoczt {
-	> .tab {
-		margin: calc(var(--margin) / 2) 0;
-		padding: calc(var(--margin) / 2) 0;
-		background: var(--bg);
-	}
-}
-</style>
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
deleted file mode 100644
index 04585f3fd0..0000000000
--- a/src/client/pages/user/index.vue
+++ /dev/null
@@ -1,829 +0,0 @@
-<template>
-<div>
-<transition name="fade" mode="out-in">
-	<div class="ftskorzw wide" v-if="user && narrow === false">
-		<MkRemoteCaution v-if="user.host != null" :href="user.url"/>
-
-		<div class="banner-container" :style="style">
-			<div class="banner" ref="banner" :style="style"></div>
-		</div>
-		<div class="contents">
-			<div class="side _forceContainerFull_">
-				<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
-				<div class="name">
-					<MkUserName :user="user" :nowrap="false" class="name"/>
-					<MkAcct :user="user" :detail="true" class="acct"/>
-				</div>
-				<div class="followed" v-if="$i && $i.id != user.id && user.isFollowed"><span>{{ $ts.followsYou }}</span></div>
-				<div class="status">
-					<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
-						<b>{{ number(user.notesCount) }}</b>
-						<span>{{ $ts.notes }}</span>
-					</MkA>
-					<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
-						<b>{{ number(user.followingCount) }}</b>
-						<span>{{ $ts.following }}</span>
-					</MkA>
-					<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
-						<b>{{ number(user.followersCount) }}</b>
-						<span>{{ $ts.followers }}</span>
-					</MkA>
-				</div>
-				<div class="description">
-					<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
-					<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
-				</div>
-				<div class="fields system">
-					<dl class="field" v-if="user.location">
-						<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
-						<dd class="value">{{ user.location }}</dd>
-					</dl>
-					<dl class="field" v-if="user.birthday">
-						<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
-						<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
-					</dl>
-					<dl class="field">
-						<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
-						<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
-					</dl>
-				</div>
-				<div class="fields" v-if="user.fields.length > 0">
-					<dl class="field" v-for="(field, i) in user.fields" :key="i">
-						<dt class="name">
-							<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
-						</dt>
-						<dd class="value">
-							<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
-						</dd>
-					</dl>
-				</div>
-				<XActivity :user="user" :key="user.id" class="_gap"/>
-				<XPhotos :user="user" :key="user.id" class="_gap"/>
-			</div>
-			<div class="main">
-				<div class="actions">
-					<button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
-					<MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
-				</div>
-				<template v-if="page === 'index'">
-					<div v-if="user.pinnedNotes.length > 0" class="_gap">
-						<XNote v-for="note in user.pinnedNotes" class="note _gap" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
-					</div>
-					<div class="_gap">
-						<XUserTimeline :user="user"/>
-					</div>
-				</template>
-				<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_gap"/>
-				<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_gap"/>
-				<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
-				<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
-			</div>
-		</div>
-	</div>
-	<MkSpacer v-else-if="user && narrow === true" :content-max="800">
-		<div class="ftskorzw narrow" v-size="{ max: [500] }">
-			<!-- TODO -->
-			<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
-			<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
-
-			<div class="profile">
-				<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
-
-				<div class="_block main" :key="user.id">
-					<div class="banner-container" :style="style">
-						<div class="banner" ref="banner" :style="style"></div>
-						<div class="fade"></div>
-						<div class="title">
-							<MkUserName class="name" :user="user" :nowrap="true"/>
-							<div class="bottom">
-								<span class="username"><MkAcct :user="user" :detail="true" /></span>
-								<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
-								<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
-								<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
-								<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
-							</div>
-						</div>
-						<span class="followed" v-if="$i && $i.id != user.id && user.isFollowed">{{ $ts.followsYou }}</span>
-						<div class="actions" v-if="$i">
-							<button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
-							<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
-						</div>
-					</div>
-					<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
-					<div class="title">
-						<MkUserName :user="user" :nowrap="false" class="name"/>
-						<div class="bottom">
-							<span class="username"><MkAcct :user="user" :detail="true" /></span>
-							<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
-							<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
-							<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
-							<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
-						</div>
-					</div>
-					<div class="description">
-						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
-						<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
-					</div>
-					<div class="fields system">
-						<dl class="field" v-if="user.location">
-							<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
-							<dd class="value">{{ user.location }}</dd>
-						</dl>
-						<dl class="field" v-if="user.birthday">
-							<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
-							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
-						</dl>
-						<dl class="field">
-							<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
-							<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
-						</dl>
-					</div>
-					<div class="fields" v-if="user.fields.length > 0">
-						<dl class="field" v-for="(field, i) in user.fields" :key="i">
-							<dt class="name">
-								<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
-							</dt>
-							<dd class="value">
-								<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
-							</dd>
-						</dl>
-					</div>
-					<div class="status">
-						<MkA :to="userPage(user)" :class="{ active: page === 'index' }" v-click-anime>
-							<b>{{ number(user.notesCount) }}</b>
-							<span>{{ $ts.notes }}</span>
-						</MkA>
-						<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }" v-click-anime>
-							<b>{{ number(user.followingCount) }}</b>
-							<span>{{ $ts.following }}</span>
-						</MkA>
-						<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }" v-click-anime>
-							<b>{{ number(user.followersCount) }}</b>
-							<span>{{ $ts.followers }}</span>
-						</MkA>
-					</div>
-				</div>
-			</div>
-
-			<div class="contents">
-				<template v-if="page === 'index'">
-					<div>
-						<div v-if="user.pinnedNotes.length > 0" class="_gap">
-							<XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
-						</div>
-						<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
-						<XPhotos :user="user" :key="user.id"/>
-						<XActivity :user="user" :key="user.id" style="margin-top: var(--margin);"/>
-					</div>
-					<div>
-						<XUserTimeline :user="user"/>
-					</div>
-				</template>
-				<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
-				<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
-				<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
-				<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
-				<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
-				<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
-			</div>
-		</div>
-	</MkSpacer>
-	<MkError v-else-if="error" @retry="fetch()"/>
-	<MkLoading v-else/>
-</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, computed } from 'vue';
-import * as age from 's-age';
-import XUserTimeline from './index.timeline.vue';
-import XNote from '@client/components/note.vue';
-import MkFollowButton from '@client/components/follow-button.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import MkRemoteCaution from '@client/components/remote-caution.vue';
-import MkTab from '@client/components/tab.vue';
-import MkInfo from '@client/components/ui/info.vue';
-import Progress from '@client/scripts/loading';
-import { parseAcct } from '@/misc/acct';
-import { getScrollPosition } from '@client/scripts/scroll';
-import { getUserMenu } from '@client/scripts/get-user-menu';
-import number from '@client/filters/number';
-import { userPage, acct as getAcct } from '@client/filters/user';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XUserTimeline,
-		XNote,
-		MkFollowButton,
-		MkContainer,
-		MkRemoteCaution,
-		MkFolder,
-		MkTab,
-		MkInfo,
-		XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
-		XReactions: defineAsyncComponent(() => import('./reactions.vue')),
-		XClips: defineAsyncComponent(() => import('./clips.vue')),
-		XPages: defineAsyncComponent(() => import('./pages.vue')),
-		XGallery: defineAsyncComponent(() => import('./gallery.vue')),
-		XPhotos: defineAsyncComponent(() => import('./index.photos.vue')),
-		XActivity: defineAsyncComponent(() => import('./index.activity.vue')),
-	},
-
-	props: {
-		acct: {
-			type: String,
-			required: true
-		},
-		page: {
-			type: String,
-			required: false,
-			default: 'index'
-		}
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: computed(() => this.user ? {
-				icon: 'fas fa-user',
-				title: this.user.name ? `${this.user.name} (@${this.user.username})` : `@${this.user.username}`,
-				subtitle: `@${getAcct(this.user)}`,
-				userName: this.user,
-				avatar: this.user,
-				path: `/@${this.user.username}`,
-				share: {
-					title: this.user.name,
-				},
-				bg: 'var(--bg)',
-				tabs: [{
-					active: this.page === 'index',
-					title: this.$ts.overview,
-					icon: 'fas fa-home',
-					onClick: () => { this.$router.push('/@' + getAcct(this.user)); },
-				}, ...(this.$i && (this.$i.id === this.user.id)) || this.user.publicReactions ? [{
-					active: this.page === 'reactions',
-					title: this.$ts.reaction,
-					icon: 'fas fa-laugh',
-					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/reactions'); },
-				}] : [], {
-					active: this.page === 'clips',
-					title: this.$ts.clips,
-					icon: 'fas fa-paperclip',
-					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/clips'); },
-				}, {
-					active: this.page === 'pages',
-					title: this.$ts.pages,
-					icon: 'fas fa-file-alt',
-					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/pages'); },
-				}, {
-					active: this.page === 'gallery',
-					title: this.$ts.gallery,
-					icon: 'fas fa-icons',
-					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/gallery'); },
-				}],
-			} : null),
-			user: null,
-			error: null,
-			parallaxAnimationId: null,
-			narrow: null,
-		};
-	},
-
-	computed: {
-		style(): any {
-			if (this.user.bannerUrl == null) return {};
-			return {
-				backgroundImage: `url(${ this.user.bannerUrl })`
-			};
-		},
-
-		age(): number {
-			return age(this.user.birthday);
-		}
-	},
-
-	watch: {
-		acct: 'fetch'
-	},
-
-	created() {
-		this.fetch();
-	},
-
-	mounted() {
-		window.requestAnimationFrame(this.parallaxLoop);
-		this.narrow = true//this.$el.clientWidth < 1000;
-	},
-
-	beforeUnmount() {
-		window.cancelAnimationFrame(this.parallaxAnimationId);
-	},
-
-	methods: {
-		getAcct,
-
-		fetch() {
-			if (this.acct == null) return;
-			this.user = null;
-			Progress.start();
-			os.api('users/show', parseAcct(this.acct)).then(user => {
-				this.user = user;
-			}).catch(e => {
-				this.error = e;
-			}).finally(() => {
-				Progress.done();
-			});
-		},
-
-		menu(ev) {
-			os.popupMenu(getUserMenu(this.user), ev.currentTarget || ev.target);
-		},
-
-		parallaxLoop() {
-			this.parallaxAnimationId = window.requestAnimationFrame(this.parallaxLoop);
-			this.parallax();
-		},
-
-		parallax() {
-			const banner = this.$refs.banner as any;
-			if (banner == null) return;
-
-			const top = getScrollPosition(this.$el);
-
-			if (top < 0) return;
-
-			const z = 1.75; // 奥行き(小さいほど奥)
-			const pos = -(top / z);
-			banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
-		},
-
-		pinnedNoteUpdated(oldValue, newValue) {
-			const i = this.user.pinnedNotes.findIndex(n => n === oldValue);
-			this.user.pinnedNotes[i] = newValue;
-		},
-
-		number,
-
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
-	transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
-	opacity: 0;
-}
-
-.ftskorzw.wide {
-
-	> .banner-container {
-		position: relative;
-		height: 300px;
-		overflow: hidden;
-		background-size: cover;
-		background-position: center;
-
-		> .banner {
-			height: 100%;
-			background-color: #4c5e6d;
-			background-size: cover;
-			background-position: center;
-			box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
-			will-change: background-position;
-		}
-	}
-
-	> .contents {
-		display: flex;
-		padding: 16px;
-
-		> .side {
-			width: 360px;
-
-			> .avatar {
-				display: block;
-				width: 180px;
-				height: 180px;
-				margin: -130px auto 0 auto;
-			}
-
-			> .name {
-				padding: 16px 0px 20px 0;
-				text-align: center;
-
-				> .name {
-					display: block;
-					font-size: 1.75em;
-					font-weight: bold;
-				}
-			}
-
-			> .followed {
-				text-align: center;
-
-				> span {
-					display: inline-block;
-					font-size: 80%;
-					padding: 8px 12px;
-					margin-bottom: 20px;
-					border: solid 0.5px var(--divider);
-					border-radius: 999px;
-				}
-			}
-
-			> .status {
-				display: flex;
-				padding: 20px 16px;
-				border-top: solid 0.5px var(--divider);
-				font-size: 90%;
-
-				> a {
-					flex: 1;
-					text-align: center;
-
-					&.active {
-						color: var(--accent);
-					}
-
-					&:hover {
-						text-decoration: none;
-					}
-
-					> b {
-						display: block;
-						line-height: 16px;
-					}
-
-					> span {
-						font-size: 75%;
-					}
-				}
-			}
-
-			> .description {
-				padding: 20px 16px;
-				border-top: solid 0.5px var(--divider);
-				font-size: 90%;
-			}
-
-			> .fields {
-				padding: 20px 16px;
-				border-top: solid 0.5px var(--divider);
-				font-size: 90%;
-
-				> .field {
-					display: flex;
-					padding: 0;
-					margin: 0;
-					align-items: center;
-
-					&:not(:last-child) {
-						margin-bottom: 8px;
-					}
-
-					> .name {
-						width: 30%;
-						overflow: hidden;
-						white-space: nowrap;
-						text-overflow: ellipsis;
-						font-weight: bold;
-					}
-
-					> .value {
-						width: 70%;
-						overflow: hidden;
-						white-space: nowrap;
-						text-overflow: ellipsis;
-						margin: 0;
-					}
-				}
-			}
-		}
-
-		> .main {
-			flex: 1;
-			margin-left: var(--margin);
-			min-width: 0;
-
-			> .nav {
-				display: flex;
-				align-items: center;
-				margin-top: var(--margin);
-				//font-size: 120%;
-				font-weight: bold;
-
-				> .link {
-					display: inline-block;
-					padding: 15px 24px 12px 24px;
-					text-align: center;
-					border-bottom: solid 3px transparent;
-
-					&:hover {
-						text-decoration: none;
-					}
-
-					&.active {
-						color: var(--accent);
-						border-bottom-color: var(--accent);
-					}
-
-					&:not(.active):hover {
-						color: var(--fgHighlighted);
-					}
-
-					> .icon {
-						margin-right: 6px;
-					}
-				}
-
-				> .actions {
-					display: flex;
-					align-items: center;
-					margin-left: auto;
-
-					> .menu {
-						padding: 12px 16px;
-					}
-				}
-			}
-		}
-	}
-}
-
-.ftskorzw.narrow {
-	box-sizing: border-box;
-	overflow: clip;
-	background: var(--bg);
-
-	> .punished {
-		font-size: 0.8em;
-		padding: 16px;
-	}
-
-	> .profile {
-
-		> .main {
-			position: relative;
-			overflow: hidden;
-
-			> .banner-container {
-				position: relative;
-				height: 250px;
-				overflow: hidden;
-				background-size: cover;
-				background-position: center;
-
-				> .banner {
-					height: 100%;
-					background-color: #4c5e6d;
-					background-size: cover;
-					background-position: center;
-					box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
-					will-change: background-position;
-				}
-
-				> .fade {
-					position: absolute;
-					bottom: 0;
-					left: 0;
-					width: 100%;
-					height: 78px;
-					background: linear-gradient(transparent, rgba(#000, 0.7));
-				}
-
-				> .followed {
-					position: absolute;
-					top: 12px;
-					left: 12px;
-					padding: 4px 8px;
-					color: #fff;
-					background: rgba(0, 0, 0, 0.7);
-					font-size: 0.7em;
-					border-radius: 6px;
-				}
-
-				> .actions {
-					position: absolute;
-					top: 12px;
-					right: 12px;
-					-webkit-backdrop-filter: var(--blur, blur(8px));
-					backdrop-filter: var(--blur, blur(8px));
-					background: rgba(0, 0, 0, 0.2);
-					padding: 8px;
-					border-radius: 24px;
-
-					> .menu {
-						vertical-align: bottom;
-						height: 31px;
-						width: 31px;
-						color: #fff;
-						text-shadow: 0 0 8px #000;
-						font-size: 16px;
-					}
-
-					> .koudoku {
-						margin-left: 4px;
-						vertical-align: bottom;
-					}
-				}
-
-				> .title {
-					position: absolute;
-					bottom: 0;
-					left: 0;
-					width: 100%;
-					padding: 0 0 8px 154px;
-					box-sizing: border-box;
-					color: #fff;
-
-					> .name {
-						display: block;
-						margin: 0;
-						line-height: 32px;
-						font-weight: bold;
-						font-size: 1.8em;
-						text-shadow: 0 0 8px #000;
-					}
-
-					> .bottom {
-						> * {
-							display: inline-block;
-							margin-right: 16px;
-							line-height: 20px;
-							opacity: 0.8;
-
-							&.username {
-								font-weight: bold;
-							}
-						}
-					}
-				}
-			}
-
-			> .title {
-				display: none;
-				text-align: center;
-				padding: 50px 8px 16px 8px;
-				font-weight: bold;
-				border-bottom: solid 0.5px var(--divider);
-
-				> .bottom {
-					> * {
-						display: inline-block;
-						margin-right: 8px;
-						opacity: 0.8;
-					}
-				}
-			}
-
-			> .avatar {
-				display: block;
-				position: absolute;
-				top: 170px;
-				left: 16px;
-				z-index: 2;
-				width: 120px;
-				height: 120px;
-				box-shadow: 1px 1px 3px rgba(#000, 0.2);
-			}
-
-			> .description {
-				padding: 24px 24px 24px 154px;
-				font-size: 0.95em;
-
-				> .empty {
-					margin: 0;
-					opacity: 0.5;
-				}
-			}
-
-			> .fields {
-				padding: 24px;
-				font-size: 0.9em;
-				border-top: solid 0.5px var(--divider);
-
-				> .field {
-					display: flex;
-					padding: 0;
-					margin: 0;
-					align-items: center;
-
-					&:not(:last-child) {
-						margin-bottom: 8px;
-					}
-
-					> .name {
-						width: 30%;
-						overflow: hidden;
-						white-space: nowrap;
-						text-overflow: ellipsis;
-						font-weight: bold;
-						text-align: center;
-					}
-
-					> .value {
-						width: 70%;
-						overflow: hidden;
-						white-space: nowrap;
-						text-overflow: ellipsis;
-						margin: 0;
-					}
-				}
-
-				&.system > .field > .name {
-				}
-			}
-
-			> .status {
-				display: flex;
-				padding: 24px;
-				border-top: solid 0.5px var(--divider);
-
-				> a {
-					flex: 1;
-					text-align: center;
-
-					&.active {
-						color: var(--accent);
-					}
-
-					&:hover {
-						text-decoration: none;
-					}
-
-					> b {
-						display: block;
-						line-height: 16px;
-					}
-
-					> span {
-						font-size: 70%;
-					}
-				}
-			}
-		}
-	}
-
-	> .contents {
-		> .content {
-			margin-bottom: var(--margin);
-		}
-	}
-
-	&.max-width_500px {
-		> .profile > .main {
-			> .banner-container {
-				height: 140px;
-
-				> .fade {
-					display: none;
-				}
-
-				> .title {
-					display: none;
-				}
-			}
-
-			> .title {
-				display: block;
-			}
-
-			> .avatar {
-				top: 90px;
-				left: 0;
-				right: 0;
-				width: 92px;
-				height: 92px;
-				margin: auto;
-			}
-
-			> .description {
-				padding: 16px;
-				text-align: center;
-			}
-
-			> .fields {
-				padding: 16px;
-			}
-
-			> .status {
-				padding: 16px;
-			}
-		}
-
-		> .contents {
-			> .nav {
-				font-size: 80%;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/user/pages.vue b/src/client/pages/user/pages.vue
deleted file mode 100644
index ece418cf62..0000000000
--- a/src/client/pages/user/pages.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-<div>
-	<MkPagination :pagination="pagination" #default="{items}" ref="list">
-		<MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/>
-	</MkPagination>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagePreview from '@client/components/page-preview.vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkPagePreview,
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/pages',
-				limit: 20,
-				params: {
-					userId: this.user.id,
-				}
-			},
-		};
-	},
-
-	watch: {
-		user() {
-			this.$refs.list.reload();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/src/client/pages/user/reactions.vue b/src/client/pages/user/reactions.vue
deleted file mode 100644
index 5ac7e01027..0000000000
--- a/src/client/pages/user/reactions.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<template>
-<div>
-	<MkPagination :pagination="pagination" #default="{items}" ref="list">
-		<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb">
-			<div class="header">
-				<MkAvatar class="avatar" :user="user"/>
-				<MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/>
-				<MkTime :time="item.createdAt" class="createdAt"/>
-			</div>
-			<MkNote :note="item.note" @update:note="updated(note, $event)" :key="item.id"/>
-		</div>
-	</MkPagination>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkNote from '@client/components/note.vue';
-import MkReactionIcon from '@client/components/reaction-icon.vue';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkNote,
-		MkReactionIcon,
-	},
-
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'users/reactions',
-				limit: 20,
-				params: {
-					userId: this.user.id,
-				}
-			},
-		};
-	},
-
-	watch: {
-		user() {
-			this.$refs.list.reload();
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.afdcfbfb {
-	> .header {
-		display: flex;
-		align-items: center;
-		padding: 8px 16px;
-		margin-bottom: 8px;
-		border-bottom: solid 2px var(--divider);
-
-		> .avatar {
-			width: 24px;
-			height: 24px;
-			margin-right: 8px;
-		}
-
-		> .reaction {
-			width: 32px;
-			height: 32px;
-		}
-
-		> .createdAt {
-			margin-left: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/v.vue b/src/client/pages/v.vue
deleted file mode 100644
index 4440e8070e..0000000000
--- a/src/client/pages/v.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<template>
-<div>
-	<section class="_section">
-		<div class="_content" style="text-align: center;">
-			<img src="/static-assets/icons/512.png" alt="" style="display: block; width: 100px; margin: 0 auto; border-radius: 16px;"/>
-			<div style="margin-top: 0.75em;">Misskey</div>
-			<div style="opacity: 0.5;">v{{ version }}</div>
-		</div>
-	</section>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { version } from '@client/config';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: 'Misskey',
-				icon: null
-			},
-			version,
-		}
-	},
-});
-</script>
diff --git a/src/client/pages/welcome.entrance.a.vue b/src/client/pages/welcome.entrance.a.vue
deleted file mode 100644
index 8e26682cf8..0000000000
--- a/src/client/pages/welcome.entrance.a.vue
+++ /dev/null
@@ -1,320 +0,0 @@
-<template>
-<div class="rsqzvsbo" v-if="meta">
-	<div class="top">
-		<MkFeaturedPhotos class="bg"/>
-		<XTimeline class="tl"/>
-		<div class="shape1"></div>
-		<div class="shape2"></div>
-		<img src="/static-assets/client/misskey.svg" class="misskey"/>
-		<div class="emojis">
-			<MkEmoji :normal="true" :no-style="true" emoji="👍"/>
-			<MkEmoji :normal="true" :no-style="true" emoji="❤"/>
-			<MkEmoji :normal="true" :no-style="true" emoji="😆"/>
-			<MkEmoji :normal="true" :no-style="true" emoji="🎉"/>
-			<MkEmoji :normal="true" :no-style="true" emoji="🍮"/>
-		</div>
-		<div class="main _panel">
-			<div class="bg">
-				<div class="fade"></div>
-			</div>
-			<div class="fg">
-				<h1>
-					<!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に -->
-					<!-- <img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> -->
-					<span class="text">{{ instanceName }}</span>
-				</h1>
-				<div class="about">
-					<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
-				</div>
-				<div class="action">
-					<MkButton @click="signup()" inline gradate data-cy-signup style="margin-right: 12px;">{{ $ts.signup }}</MkButton>
-					<MkButton @click="signin()" inline data-cy-signin>{{ $ts.login }}</MkButton>
-				</div>
-				<div class="status" v-if="onlineUsersCount && stats">
-					<div>
-						<I18n :src="$ts.nUsers" text-tag="span" class="users">
-							<template #n><b>{{ number(stats.originalUsersCount) }}</b></template>
-						</I18n>
-						<I18n :src="$ts.nNotes" text-tag="span" class="notes">
-							<template #n><b>{{ number(stats.originalNotesCount) }}</b></template>
-						</I18n>
-					</div>
-					<I18n :src="$ts.onlineUsersCount" text-tag="span" class="online">
-						<template #n><b>{{ onlineUsersCount }}</b></template>
-					</I18n>
-				</div>
-				<button class="_button _acrylic menu" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { toUnicode } from 'punycode/';
-import XSigninDialog from '@client/components/signin-dialog.vue';
-import XSignupDialog from '@client/components/signup-dialog.vue';
-import MkButton from '@client/components/ui/button.vue';
-import XNote from '@client/components/note.vue';
-import MkFeaturedPhotos from '@client/components/featured-photos.vue';
-import XTimeline from './welcome.timeline.vue';
-import { host, instanceName } from '@client/config';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		XNote,
-		MkFeaturedPhotos,
-		XTimeline,
-	},
-
-	data() {
-		return {
-			host: toUnicode(host),
-			instanceName,
-			meta: null,
-			stats: null,
-			tags: [],
-			onlineUsersCount: null,
-		};
-	},
-
-	created() {
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-
-		os.api('stats').then(stats => {
-			this.stats = stats;
-		});
-
-		os.api('get-online-users-count').then(res => {
-			this.onlineUsersCount = res.count;
-		});
-
-		os.api('hashtags/list', {
-			sort: '+mentionedLocalUsers',
-			limit: 8
-		}).then(tags => {
-			this.tags = tags;
-		});
-	},
-
-	methods: {
-		signin() {
-			os.popup(XSigninDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		signup() {
-			os.popup(XSignupDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		showMenu(ev) {
-			os.popupMenu([{
-				text: this.$t('aboutX', { x: instanceName }),
-				icon: 'fas fa-info-circle',
-				action: () => {
-					os.pageWindow('/about');
-				}
-			}, {
-				text: this.$ts.aboutMisskey,
-				icon: 'fas fa-info-circle',
-				action: () => {
-					os.pageWindow('/about-misskey');
-				}
-			}, null, {
-				text: this.$ts.help,
-				icon: 'fas fa-question-circle',
-				action: () => {
-					window.open(`https://misskey-hub.net/help.md`, '_blank');
-				}
-			}], ev.currentTarget || ev.target);
-		},
-
-		number
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rsqzvsbo {
-	> .top {
-		display: flex;
-		text-align: center;
-		min-height: 100vh;
-		box-sizing: border-box;
-		padding: 16px;
-
-		> .bg {
-			position: absolute;
-			top: 0;
-			right: 0;
-			width: 80%; // 100%からshapeの幅を引いている
-			height: 100%;
-		}
-
-		> .tl {
-			position: absolute;
-			top: 0;
-			bottom: 0;
-			right: 64px;
-			margin: auto;
-			width: 500px;
-			height: calc(100% - 128px);
-			overflow: hidden;
-			-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%);
-			mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%);
-
-			@media (max-width: 1200px) {
-				display: none;
-			}
-		}
-
-		> .shape1 {
-			position: absolute;
-			top: 0;
-			left: 0;
-			width: 100%;
-			height: 100%;
-			background: var(--accent);
-			clip-path: polygon(0% 0%, 45% 0%, 20% 100%, 0% 100%);
-		}
-		> .shape2 {
-			position: absolute;
-			top: 0;
-			left: 0;
-			width: 100%;
-			height: 100%;
-			background: var(--accent);
-			clip-path: polygon(0% 0%, 25% 0%, 35% 100%, 0% 100%);
-			opacity: 0.5;
-		}
-
-		> .misskey {
-			position: absolute;
-			top: 42px;
-			left: 42px;
-			width: 160px;
-
-			@media (max-width: 450px) {
-				width: 130px;
-			}
-		}
-
-		> .emojis {
-			position: absolute;
-			bottom: 32px;
-			left: 35px;
-
-			> * {
-				margin-right: 8px;
-			}
-
-			@media (max-width: 1200px) {
-				display: none;
-			}
-		}
-
-		> .main {
-			position: relative;
-			width: min(480px, 100%);
-			margin: auto auto auto 128px;
-			box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
-
-			@media (max-width: 1200px) {
-				margin: auto;
-			}
-
-			> .bg {
-				position: absolute;
-				top: 0;
-				left: 0;
-				width: 100%;
-				height: 128px;
-				background-position: center;
-				background-size: cover;
-				opacity: 0.75;
-
-				> .fade {
-					position: absolute;
-					bottom: 0;
-					left: 0;
-					width: 100%;
-					height: 128px;
-					background: linear-gradient(0deg, var(--panel), var(--X15));
-				}
-			}
-
-			> .fg {
-				position: relative;
-				z-index: 1;
-
-				> h1 {
-					display: block;
-					margin: 0;
-					padding: 32px 32px 24px 32px;
-					font-size: 1.5em;
-
-					> .logo {
-						vertical-align: bottom;
-						max-height: 120px;
-						max-width: min(100%, 300px);
-					}
-				}
-
-				> .about {
-					padding: 0 32px;
-				}
-
-				> .action {
-					padding: 32px;
-
-					> * {
-						line-height: 28px;
-					}
-				}
-
-				> .status {
-					border-top: solid 0.5px var(--divider);
-					padding: 32px;
-					font-size: 90%;
-
-					> div {
-						> span:not(:last-child) {
-							padding-right: 1em;
-							margin-right: 1em;
-							border-right: solid 0.5px var(--divider);
-						}
-					}
-
-					> .online {
-						::v-deep(b) {
-							color: #41b781;
-						}
-
-						::v-deep(span) {
-							opacity: 0.7;
-						}
-					}
-				}
-
-				> .menu {
-					position: absolute;
-					top: 16px;
-					right: 16px;
-					width: 32px;
-					height: 32px;
-					border-radius: 8px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/welcome.entrance.b.vue b/src/client/pages/welcome.entrance.b.vue
deleted file mode 100644
index 9dc7289b4d..0000000000
--- a/src/client/pages/welcome.entrance.b.vue
+++ /dev/null
@@ -1,236 +0,0 @@
-<template>
-<div class="rsqzvsbo" v-if="meta">
-	<div class="top">
-		<MkFeaturedPhotos class="bg"/>
-		<XTimeline class="tl"/>
-		<div class="shape"></div>
-		<div class="main">
-			<h1>
-				<img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span>
-			</h1>
-			<div class="about">
-				<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
-			</div>
-			<div class="action">
-				<MkButton class="signup" @click="signup()" inline gradate>{{ $ts.signup }}</MkButton>
-				<MkButton class="signin" @click="signin()" inline>{{ $ts.login }}</MkButton>
-			</div>
-			<div class="status" v-if="onlineUsersCount && stats">
-				<div>
-					<I18n :src="$ts.nUsers" text-tag="span" class="users">
-						<template #n><b>{{ number(stats.originalUsersCount) }}</b></template>
-					</I18n>
-					<I18n :src="$ts.nNotes" text-tag="span" class="notes">
-						<template #n><b>{{ number(stats.originalNotesCount) }}</b></template>
-					</I18n>
-				</div>
-				<I18n :src="$ts.onlineUsersCount" text-tag="span" class="online">
-					<template #n><b>{{ onlineUsersCount }}</b></template>
-				</I18n>
-			</div>
-		</div>
-		<img src="/static-assets/client/misskey.svg" class="misskey"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { toUnicode } from 'punycode/';
-import XSigninDialog from '@client/components/signin-dialog.vue';
-import XSignupDialog from '@client/components/signup-dialog.vue';
-import MkButton from '@client/components/ui/button.vue';
-import XNote from '@client/components/note.vue';
-import MkFeaturedPhotos from '@client/components/featured-photos.vue';
-import XTimeline from './welcome.timeline.vue';
-import { host, instanceName } from '@client/config';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		XNote,
-		XTimeline,
-		MkFeaturedPhotos,
-	},
-
-	data() {
-		return {
-			host: toUnicode(host),
-			instanceName,
-			meta: null,
-			stats: null,
-			tags: [],
-			onlineUsersCount: null,
-		};
-	},
-
-	created() {
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-
-		os.api('stats').then(stats => {
-			this.stats = stats;
-		});
-
-		os.api('get-online-users-count').then(res => {
-			this.onlineUsersCount = res.count;
-		});
-
-		os.api('hashtags/list', {
-			sort: '+mentionedLocalUsers',
-			limit: 8
-		}).then(tags => {
-			this.tags = tags;
-		});
-	},
-
-	methods: {
-		signin() {
-			os.popup(XSigninDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		signup() {
-			os.popup(XSignupDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		showMenu(ev) {
-			os.popupMenu([{
-				text: this.$t('aboutX', { x: instanceName }),
-				icon: 'fas fa-info-circle',
-				action: () => {
-					os.pageWindow('/about');
-				}
-			}, {
-				text: this.$ts.aboutMisskey,
-				icon: 'fas fa-info-circle',
-				action: () => {
-					os.pageWindow('/about-misskey');
-				}
-			}, null, {
-				text: this.$ts.help,
-				icon: 'fas fa-question-circle',
-				action: () => {
-					window.open(`https://misskey-hub.net/help.md`, '_blank');
-				}
-			}], ev.currentTarget || ev.target);
-		},
-
-		number
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rsqzvsbo {
-	> .top {
-		min-height: 100vh;
-		box-sizing: border-box;
-
-		> .bg {
-			position: absolute;
-			top: 0;
-			left: 0;
-			width: 100%;
-			height: 100%;
-		}
-
-		> .tl {
-			position: absolute;
-			top: 0;
-			bottom: 0;
-			right: 64px;
-			margin: auto;
-			width: 500px;
-			height: calc(100% - 128px);
-			overflow: hidden;
-			-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%);
-			mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 128px, rgba(0,0,0,1) calc(100% - 128px), rgba(0,0,0,0) 100%);
-		}
-
-		> .shape {
-			position: absolute;
-			top: 0;
-			left: 0;
-			width: 100%;
-			height: 100%;
-			background: var(--accent);
-			clip-path: polygon(0% 0%, 40% 0%, 22% 100%, 0% 100%);
-		}
-
-		> .misskey {
-			position: absolute;
-			bottom: 64px;
-			left: 64px;
-			width: 160px;
-		}
-
-		> .main {
-			position: relative;
-			width: min(450px, 100%);
-			padding: 64px;
-			color: #fff;
-			font-size: 1.1em;
-
-			@media (max-width: 1200px) {
-				margin: auto;
-			}
-
-			> h1 {
-				display: block;
-				margin: 0 0 32px 0;
-				padding: 0;
-
-				> .logo {
-					vertical-align: bottom;
-					max-height: 100px;
-				}
-			}
-
-			> .about {
-				padding: 0;
-			}
-
-			> .action {
-				margin: 32px 0;
-
-				> * {
-					line-height: 32px;
-				}
-
-				> .signup {
-					background: var(--panel);
-					color: var(--fg);
-				}
-
-				> .signin {
-					background: var(--accent);
-					color: inherit;
-				}
-			}
-
-			> .status {
-				margin: 32px 0;
-				border-top: solid 1px rgba(255, 255, 255, 0.5);
-				font-size: 90%;
-
-				> div {
-					padding: 16px 0;
-
-					> span:not(:last-child) {
-						padding-right: 1em;
-						margin-right: 1em;
-						border-right: solid 1px rgba(255, 255, 255, 0.5);
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/welcome.entrance.c.vue b/src/client/pages/welcome.entrance.c.vue
deleted file mode 100644
index a946df86d2..0000000000
--- a/src/client/pages/welcome.entrance.c.vue
+++ /dev/null
@@ -1,305 +0,0 @@
-<template>
-<div class="rsqzvsbo" v-if="meta">
-	<div class="top">
-		<MkFeaturedPhotos class="bg"/>
-		<div class="fade"></div>
-		<div class="emojis">
-			<MkEmoji :normal="true" :no-style="true" emoji="👍"/>
-			<MkEmoji :normal="true" :no-style="true" emoji="❤"/>
-			<MkEmoji :normal="true" :no-style="true" emoji="😆"/>
-			<MkEmoji :normal="true" :no-style="true" emoji="🎉"/>
-			<MkEmoji :normal="true" :no-style="true" emoji="🍮"/>
-		</div>
-		<div class="main">
-			<img src="/static-assets/client/misskey.svg" class="misskey"/>
-			<div class="form _panel">
-				<div class="bg">
-					<div class="fade"></div>
-				</div>
-				<div class="fg">
-					<h1>
-						<img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span>
-					</h1>
-					<div class="about">
-						<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
-					</div>
-					<div class="action">
-						<MkButton @click="signup()" inline gradate>{{ $ts.signup }}</MkButton>
-						<MkButton @click="signin()" inline>{{ $ts.login }}</MkButton>
-					</div>
-					<div class="status" v-if="onlineUsersCount && stats">
-						<div>
-							<I18n :src="$ts.nUsers" text-tag="span" class="users">
-								<template #n><b>{{ number(stats.originalUsersCount) }}</b></template>
-							</I18n>
-							<I18n :src="$ts.nNotes" text-tag="span" class="notes">
-								<template #n><b>{{ number(stats.originalNotesCount) }}</b></template>
-							</I18n>
-						</div>
-						<I18n :src="$ts.onlineUsersCount" text-tag="span" class="online">
-							<template #n><b>{{ onlineUsersCount }}</b></template>
-						</I18n>
-					</div>
-					<button class="_button _acrylic menu" @click="showMenu"><i class="fas fa-ellipsis-h"></i></button>
-				</div>
-			</div>
-			<nav class="nav">
-				<MkA to="/announcements">{{ $ts.announcements }}</MkA>
-				<MkA to="/explore">{{ $ts.explore }}</MkA>
-				<MkA to="/channels">{{ $ts.channel }}</MkA>
-				<MkA to="/featured">{{ $ts.featured }}</MkA>
-			</nav>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { toUnicode } from 'punycode/';
-import XSigninDialog from '@client/components/signin-dialog.vue';
-import XSignupDialog from '@client/components/signup-dialog.vue';
-import MkButton from '@client/components/ui/button.vue';
-import XNote from '@client/components/note.vue';
-import MkFeaturedPhotos from '@client/components/featured-photos.vue';
-import XTimeline from './welcome.timeline.vue';
-import { host, instanceName } from '@client/config';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		XNote,
-		MkFeaturedPhotos,
-		XTimeline,
-	},
-
-	data() {
-		return {
-			host: toUnicode(host),
-			instanceName,
-			meta: null,
-			stats: null,
-			tags: [],
-			onlineUsersCount: null,
-		};
-	},
-
-	created() {
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-
-		os.api('stats').then(stats => {
-			this.stats = stats;
-		});
-
-		os.api('get-online-users-count').then(res => {
-			this.onlineUsersCount = res.count;
-		});
-
-		os.api('hashtags/list', {
-			sort: '+mentionedLocalUsers',
-			limit: 8
-		}).then(tags => {
-			this.tags = tags;
-		});
-	},
-
-	methods: {
-		signin() {
-			os.popup(XSigninDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		signup() {
-			os.popup(XSignupDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		showMenu(ev) {
-			os.popupMenu([{
-				text: this.$t('aboutX', { x: instanceName }),
-				icon: 'fas fa-info-circle',
-				action: () => {
-					os.pageWindow('/about');
-				}
-			}, {
-				text: this.$ts.aboutMisskey,
-				icon: 'fas fa-info-circle',
-				action: () => {
-					os.pageWindow('/about-misskey');
-				}
-			}, null, {
-				text: this.$ts.help,
-				icon: 'fas fa-question-circle',
-				action: () => {
-					window.open(`https://misskey-hub.net/help.md`, '_blank');
-				}
-			}], ev.currentTarget || ev.target);
-		},
-
-		number
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rsqzvsbo {
-	> .top {
-		display: flex;
-		text-align: center;
-		min-height: 100vh;
-		box-sizing: border-box;
-		padding: 16px;
-
-		> .bg {
-			position: absolute;
-			top: 0;
-			left: 0;
-			width: 100%;
-			height: 100%;
-		}
-
-		> .fade {
-			position: absolute;
-			top: 0;
-			left: 0;
-			width: 100%;
-			height: 100%;
-			background: rgba(0, 0, 0, 0.25);
-		}
-
-		> .emojis {
-			position: absolute;
-			bottom: 32px;
-			left: 35px;
-
-			> * {
-				margin-right: 8px;
-			}
-
-			@media (max-width: 1200px) {
-				display: none;
-			}
-		}
-
-		> .main {
-			position: relative;
-			width: min(460px, 100%);
-			margin: auto;
-
-			> .misskey {
-				width: 150px;
-				margin-bottom: 16px;
-
-				@media (max-width: 450px) {
-					width: 130px;
-				}
-			}
-
-			> .form {
-				position: relative;
-				box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
-
-				> .bg {
-					position: absolute;
-					top: 0;
-					left: 0;
-					width: 100%;
-					height: 128px;
-					background-position: center;
-					background-size: cover;
-					opacity: 0.75;
-
-					> .fade {
-						position: absolute;
-						bottom: 0;
-						left: 0;
-						width: 100%;
-						height: 128px;
-						background: linear-gradient(0deg, var(--panel), var(--X15));
-					}
-				}
-
-				> .fg {
-					position: relative;
-					z-index: 1;
-
-					> h1 {
-						display: block;
-						margin: 0;
-						padding: 32px 32px 24px 32px;
-
-						> .logo {
-							vertical-align: bottom;
-							max-height: 120px;
-						}
-					}
-
-					> .about {
-						padding: 0 32px;
-					}
-
-					> .action {
-						padding: 32px;
-
-						> * {
-							line-height: 28px;
-						}
-					}
-
-					> .status {
-						border-top: solid 0.5px var(--divider);
-						padding: 32px;
-						font-size: 90%;
-
-						> div {
-							> span:not(:last-child) {
-								padding-right: 1em;
-								margin-right: 1em;
-								border-right: solid 0.5px var(--divider);
-							}
-						}
-
-						> .online {
-							::v-deep(b) {
-								color: #41b781;
-							}
-
-							::v-deep(span) {
-								opacity: 0.7;
-							}
-						}
-					}
-
-					> .menu {
-						position: absolute;
-						top: 16px;
-						right: 16px;
-						width: 32px;
-						height: 32px;
-						border-radius: 8px;
-					}
-				}
-			}
-
-			> .nav {
-				position: relative;
-				z-index: 2;
-				margin-top: 20px;
-				color: #fff;
-				text-shadow: 0 0 8px black;
-				font-size: 0.9em;
-
-				> *:not(:last-child) {
-					margin-right: 1.5em;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/welcome.setup.vue b/src/client/pages/welcome.setup.vue
deleted file mode 100644
index dfefecc8fa..0000000000
--- a/src/client/pages/welcome.setup.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-<form class="mk-setup" @submit.prevent="submit()">
-	<h1>Welcome to Misskey!</h1>
-	<div>
-		<p>{{ $ts.intro }}</p>
-		<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-admin-username>
-			<template #label>{{ $ts.username }}</template>
-			<template #prefix>@</template>
-			<template #suffix>@{{ host }}</template>
-		</MkInput>
-		<MkInput v-model="password" type="password" data-cy-admin-password>
-			<template #label>{{ $ts.password }}</template>
-			<template #prefix><i class="fas fa-lock"></i></template>
-		</MkInput>
-		<footer>
-			<MkButton primary type="submit" :disabled="submitting" data-cy-admin-ok>
-				{{ submitting ? $ts.processing : $ts.done }}<MkEllipsis v-if="submitting"/>
-			</MkButton>
-		</footer>
-	</div>
-</form>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/form/input.vue';
-import { host } from '@client/config';
-import * as os from '@client/os';
-import { login } from '@client/account';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MkInput,
-	},
-
-	data() {
-		return {
-			username: '',
-			password: '',
-			submitting: false,
-			host,
-		}
-	},
-
-	methods: {
-		submit() {
-			if (this.submitting) return;
-			this.submitting = true;
-
-			os.api('admin/accounts/create', {
-				username: this.username,
-				password: this.password,
-			}).then(res => {
-				return login(res.token);
-			}).catch(() => {
-				this.submitting = false;
-
-				os.dialog({
-					type: 'error',
-					text: this.$ts.somethingHappened
-				});
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-setup {
-	border-radius: var(--radius);
-	box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
-	overflow: hidden;
-	max-width: 500px;
-	margin: 32px auto;
-
-	> h1 {
-		margin: 0;
-		font-size: 1.5em;
-		text-align: center;
-		padding: 32px;
-		background: var(--accent);
-		color: #fff;
-	}
-
-	> div {
-		padding: 32px;
-		background: var(--panel);
-
-		> p {
-			margin-top: 0;
-		}
-
-		> footer {
-			> * {
-				margin: 0 auto;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/welcome.timeline.vue b/src/client/pages/welcome.timeline.vue
deleted file mode 100644
index bd07ac78db..0000000000
--- a/src/client/pages/welcome.timeline.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<template>
-<div class="civpbkhh">
-	<div class="scrollbox" ref="scroll" v-bind:class="{ scroll: isScrolling }">
-		<div v-for="note in notes" class="note">
-			<div class="content _panel">
-				<div class="body">
-					<MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
-					<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
-					<MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
-				</div>
-				<div v-if="note.files.length > 0" class="richcontent">
-					<XMediaList :media-list="note.files"/>
-				</div>
-				<div v-if="note.poll">
-					<XPoll :note="note" :readOnly="true" />
-				</div>
-			</div>
-			<XReactionsViewer :note="note" ref="reactionsViewer"/>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XReactionsViewer from '@client/components/reactions-viewer.vue';
-import XMediaList from '@client/components/media-list.vue';
-import XPoll from '@client/components/poll.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XReactionsViewer,
-		XMediaList,
-		XPoll
-	},
-
-	data() {
-		return {
-			notes: [],
-			isScrolling: false,
-		}
-	},
-
-	created() {
-		os.api('notes/featured').then(notes => {
-			this.notes = notes;
-		});
-	},
-
-	updated() {
-		if (this.$refs.scroll.clientHeight > window.innerHeight) {
-			this.isScrolling = true;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-@keyframes scroll {
-	0% {
-		transform: translate3d(0, 0, 0);
-	}
-	5% {
-		transform: translate3d(0, 0, 0);
-	}
-	75% {
-		transform: translate3d(0, calc(-100% + 90vh), 0);
-	}
-	90% {
-		transform: translate3d(0, calc(-100% + 90vh), 0);
-	}
-}
-
-.civpbkhh {
-	text-align: right;
-
-	> .scrollbox {
-		&.scroll {
-			animation: scroll 45s linear infinite;
-		}
-
-		> .note {
-			margin: 16px 0 16px auto;
-
-			> .content {
-				padding: 16px;
-				margin: 0 0 0 auto;
-				max-width: max-content;
-				border-radius: 16px;
-
-				> .richcontent {
-					min-width: 250px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/pages/welcome.vue b/src/client/pages/welcome.vue
deleted file mode 100644
index b6a715830d..0000000000
--- a/src/client/pages/welcome.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<div v-if="meta">
-	<XSetup v-if="meta.requireSetup"/>
-	<XEntrance v-else/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XSetup from './welcome.setup.vue';
-import XEntrance from './welcome.entrance.a.vue';
-import { instanceName } from '@client/config';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XSetup,
-		XEntrance,
-	},
-
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: instanceName,
-				icon: null
-			},
-			meta: null
-		}
-	},
-
-	created() {
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-	}
-});
-</script>
diff --git a/src/client/pizzax.ts b/src/client/pizzax.ts
deleted file mode 100644
index 396abc2418..0000000000
--- a/src/client/pizzax.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { onUnmounted, Ref, ref, watch } from 'vue';
-import { $i } from './account';
-import { api } from './os';
-
-type StateDef = Record<string, {
-	where: 'account' | 'device' | 'deviceAccount';
-	default: any;
-}>;
-
-type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
-
-export class Storage<T extends StateDef> {
-	public readonly key: string;
-	public readonly keyForLocalStorage: string;
-
-	public readonly def: T;
-
-	// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
-	public readonly state: { [K in keyof T]: T[K]['default'] };
-	public readonly reactiveState: { [K in keyof T]: Ref<T[K]['default']> };
-
-	constructor(key: string, def: T) {
-		this.key = key;
-		this.keyForLocalStorage = 'pizzax::' + key;
-		this.def = def;
-
-		// TODO: indexedDBにする
-		const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
-		const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {};
-		const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {};
-
-		const state = {};
-		const reactiveState = {};
-		for (const [k, v] of Object.entries(def)) {
-			if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
-				state[k] = deviceState[k];
-			} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
-				state[k] = registryCache[k];
-			} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
-				state[k] = deviceAccountState[k];
-			} else {
-				state[k] = v.default;
-				if (_DEV_) console.log('Use default value', k, v.default);
-			}
-		}
-		for (const [k, v] of Object.entries(state)) {
-			reactiveState[k] = ref(v);
-		}
-		this.state = state as any;
-		this.reactiveState = reactiveState as any;
-
-		if ($i) {
-			// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
-			setTimeout(() => {
-				api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => {
-					const cache = {};
-					for (const [k, v] of Object.entries(def)) {
-						if (v.where === 'account') {
-							if (Object.prototype.hasOwnProperty.call(kvs, k)) {
-								state[k] = kvs[k];
-								reactiveState[k].value = kvs[k];
-								cache[k] = kvs[k];
-							} else {
-								state[k] = v.default;
-								reactiveState[k].value = v.default;
-							}
-						}
-					}
-					localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
-				});
-			}, 1);
-
-			// TODO: streamingのuser storage updateイベントを監視して更新
-		}
-	}
-
-	public set<K extends keyof T>(key: K, value: T[K]['default']): void {
-		if (_DEV_) console.log('set', key, value);
-
-		this.state[key] = value;
-		this.reactiveState[key].value = value;
-
-		switch (this.def[key].where) {
-			case 'device': {
-				const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
-				deviceState[key] = value;
-				localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState));
-				break;
-			}
-			case 'deviceAccount': {
-				if ($i == null) break;
-				const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}');
-				deviceAccountState[key] = value;
-				localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState));
-				break;
-			}
-			case 'account': {
-				if ($i == null) break;
-				const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
-				cache[key] = value;
-				localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
-				api('i/registry/set', {
-					scope: ['client', this.key],
-					key: key,
-					value: value
-				});
-				break;
-			}
-		}
-	}
-
-	public push<K extends keyof T>(key: K, value: ArrayElement<T[K]['default']>): void {
-		const currentState = this.state[key];
-		this.set(key, [...currentState, value]);
-	}
-
-	public reset(key: keyof T) {
-		this.set(key, this.def[key].default);
-	}
-
-	/**
-	 * 特定のキーの、簡易的なgetter/setterを作ります
-	 * 主にvue場で設定コントロールのmodelとして使う用
-	 */
-	public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) {
-		const valueRef = ref(this.state[key]);
-
-		const stop = watch(this.reactiveState[key], val => {
-			valueRef.value = val;
-		});
-
-		// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
-		onUnmounted(() => {
-			stop();
-		});
-
-		// TODO: VueのcustomRef使うと良い感じになるかも
-		return {
-			get: () => {
-				if (getter) {
-					return getter(valueRef.value);
-				} else {
-					return valueRef.value;
-				}
-			},
-			set: (value: unknown) => {
-				const val = setter ? setter(value) : value;
-				this.set(key, val);
-				valueRef.value = val;
-			}
-		};
-	}
-}
diff --git a/src/client/plugin.ts b/src/client/plugin.ts
deleted file mode 100644
index 6bdc4fe4d5..0000000000
--- a/src/client/plugin.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { AiScript, utils, values } from '@syuilo/aiscript';
-import { deserialize } from '@syuilo/aiscript/built/serializer';
-import { jsToVal } from '@syuilo/aiscript/built/interpreter/util';
-import { createAiScriptEnv } from '@client/scripts/aiscript/api';
-import { dialog } from '@client/os';
-import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@client/store';
-
-const pluginContexts = new Map<string, AiScript>();
-
-export function install(plugin) {
-	console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
-
-	const aiscript = new AiScript(createPluginEnv({
-		plugin: plugin,
-		storageKey: 'plugins:' + plugin.id
-	}), {
-		in: (q) => {
-			return new Promise(ok => {
-				dialog({
-					title: q,
-					input: {}
-				}).then(({ canceled, result: a }) => {
-					ok(a);
-				});
-			});
-		},
-		out: (value) => {
-			console.log(value);
-		},
-		log: (type, params) => {
-		},
-	});
-
-	initPlugin({ plugin, aiscript });
-
-	aiscript.exec(deserialize(plugin.ast));
-}
-
-function createPluginEnv(opts) {
-	const config = new Map();
-	for (const [k, v] of Object.entries(opts.plugin.config || {})) {
-		config.set(k, jsToVal(opts.plugin.configData.hasOwnProperty(k) ? opts.plugin.configData[k] : v.default));
-	}
-
-	return {
-		...createAiScriptEnv({ ...opts, token: opts.plugin.token }),
-		//#region Deprecated
-		'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
-			registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => {
-			registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
-			registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		//#endregion
-		'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
-			registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
-			registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
-			registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
-			registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler });
-		}),
-		'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
-			registerNotePostInterruptor({ pluginId: opts.plugin.id, handler });
-		}),
-		'Plugin:open_url': values.FN_NATIVE(([url]) => {
-			window.open(url.value, '_blank');
-		}),
-		'Plugin:config': values.OBJ(config),
-	};
-}
-
-function initPlugin({ plugin, aiscript }) {
-	pluginContexts.set(plugin.id, aiscript);
-}
-
-function registerPostFormAction({ pluginId, title, handler }) {
-	postFormActions.push({
-		title, handler: (form, update) => {
-			pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
-				update(key.value, value.value);
-			})]);
-		}
-	});
-}
-
-function registerUserAction({ pluginId, title, handler }) {
-	userActions.push({
-		title, handler: (user) => {
-			pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]);
-		}
-	});
-}
-
-function registerNoteAction({ pluginId, title, handler }) {
-	noteActions.push({
-		title, handler: (note) => {
-			pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
-		}
-	});
-}
-
-function registerNoteViewInterruptor({ pluginId, handler }) {
-	noteViewInterruptors.push({
-		handler: async (note) => {
-			return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]));
-		}
-	});
-}
-
-function registerNotePostInterruptor({ pluginId, handler }) {
-	notePostInterruptors.push({
-		handler: async (note) => {
-			return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]));
-		}
-	});
-}
diff --git a/src/client/router.ts b/src/client/router.ts
deleted file mode 100644
index d57babffe5..0000000000
--- a/src/client/router.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { defineAsyncComponent, markRaw } from 'vue';
-import { createRouter, createWebHistory } from 'vue-router';
-import MkLoading from '@client/pages/_loading_.vue';
-import MkError from '@client/pages/_error_.vue';
-import MkTimeline from '@client/pages/timeline.vue';
-import { $i } from './account';
-import { ui } from '@client/config';
-
-const page = (path: string, ui?: string) => defineAsyncComponent({
-	loader: ui ? () => import(`./ui/${ui}/pages/${path}.vue`) : () => import(`./pages/${path}.vue`),
-	loadingComponent: MkLoading,
-	errorComponent: MkError,
-});
-
-let indexScrollPos = 0;
-
-const defaultRoutes = [
-	// NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる
-	{ path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') },
-	{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
-	{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
-	{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
-	{ path: '/@:acct/room', props: true, component: page('room/room') },
-	{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
-	{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
-	{ path: '/signup-complete/:code', component: page('signup-complete'), props: route => ({ code: route.params.code }) },
-	{ path: '/announcements', component: page('announcements') },
-	{ path: '/about', component: page('about') },
-	{ path: '/about-misskey', component: page('about-misskey') },
-	{ path: '/featured', component: page('featured') },
-	{ path: '/theme-editor', component: page('theme-editor') },
-	{ path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
-	{ path: '/explore', component: page('explore') },
-	{ path: '/explore/tags/:tag', props: true, component: page('explore') },
-	{ path: '/federation', component: page('federation') },
-	{ path: '/emojis', component: page('emojis') },
-	{ path: '/search', component: page('search') },
-	{ path: '/pages', name: 'pages', component: page('pages') },
-	{ path: '/pages/new', component: page('page-editor/page-editor') },
-	{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
-	{ path: '/gallery', component: page('gallery/index') },
-	{ path: '/gallery/new', component: page('gallery/edit') },
-	{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) },
-	{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
-	{ path: '/channels', component: page('channels') },
-	{ path: '/channels/new', component: page('channel-editor') },
-	{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
-	{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
-	{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
-	{ path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) },
-	{ path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) },
-	{ path: '/my/notifications', component: page('notifications') },
-	{ path: '/my/favorites', component: page('favorites') },
-	{ path: '/my/messages', component: page('messages') },
-	{ path: '/my/mentions', component: page('mentions') },
-	{ path: '/my/messaging', name: 'messaging', component: page('messaging/index') },
-	{ path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) },
-	{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
-	{ path: '/my/drive', name: 'drive', component: page('drive') },
-	{ path: '/my/drive/folder/:folder', component: page('drive') },
-	{ path: '/my/follow-requests', component: page('follow-requests') },
-	{ path: '/my/lists', component: page('my-lists/index') },
-	{ path: '/my/lists/:list', component: page('my-lists/list') },
-	{ path: '/my/groups', component: page('my-groups/index') },
-	{ path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) },
-	{ path: '/my/antennas', component: page('my-antennas/index') },
-	{ path: '/my/antennas/create', component: page('my-antennas/create') },
-	{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
-	{ path: '/my/clips', component: page('my-clips/index') },
-	{ path: '/scratchpad', component: page('scratchpad') },
-	{ path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) },
-	{ path: '/admin', component: page('admin/index') },
-	{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
-	{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
-	{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
-	{ path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) },
-	{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
-	{ path: '/games/reversi', component: page('reversi/index') },
-	{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
-	{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
-	{ path: '/api-console', component: page('api-console') },
-	{ path: '/preview', component: page('preview') },
-	{ path: '/test', component: page('test') },
-	{ path: '/auth/:token', component: page('auth') },
-	{ path: '/miauth/:session', component: page('miauth') },
-	{ path: '/authorize-follow', component: page('follow') },
-	{ path: '/share', component: page('share') },
-	{ path: '/:catchAll(.*)', component: page('not-found') }
-];
-
-const chatRoutes = [
-	{ path: '/timeline', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
-	{ path: '/timeline/home', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
-	{ path: '/timeline/local', component: page('timeline', 'chat'), props: route => ({ src: 'local' }) },
-	{ path: '/timeline/social', component: page('timeline', 'chat'), props: route => ({ src: 'social' }) },
-	{ path: '/timeline/global', component: page('timeline', 'chat'), props: route => ({ src: 'global' }) },
-	{ path: '/channels/:channelId', component: page('channel', 'chat'), props: route => ({ channelId: route.params.channelId }) },
-];
-
-function margeRoutes(routes: any[]) {
-	const result = defaultRoutes;
-	for (const route of routes) {
-		const found = result.findIndex(x => x.path === route.path);
-		if (found > -1) {
-			result[found] = route;
-		} else {
-			result.unshift(route);
-		}
-	}
-	return result;
-}
-
-export const router = createRouter({
-	history: createWebHistory(),
-	routes: margeRoutes(ui === 'chat' ? chatRoutes : []),
-	// なんかHacky
-	// 通常の使い方をすると scroll メソッドの behavior を設定できないため、自前で window.scroll するようにする
-	scrollBehavior(to) {
-		window._scroll = () => { // さらにHacky
-			if (to.name === 'index') {
-				window.scroll({ top: indexScrollPos, behavior: 'instant' });
-				const i = setInterval(() => {
-					window.scroll({ top: indexScrollPos, behavior: 'instant' });
-				}, 10);
-				setTimeout(() => {
-					clearInterval(i);
-				}, 500);
-			} else {
-				window.scroll({ top: 0, behavior: 'instant' });
-			}
-		};
-	}
-});
-
-router.afterEach((to, from) => {
-	if (from.name === 'index') {
-		indexScrollPos = window.scrollY;
-	}
-});
-
-export function resolve(path: string) {
-	const resolved = router.resolve(path);
-	const route = resolved.matched[0];
-	return {
-		component: markRaw(route.components.default),
-		// TODO: route.propsには関数以外も入る可能性があるのでよしなにハンドリングする
-		props: route.props?.default ? route.props.default(resolved) : resolved.params
-	};
-}
diff --git a/src/client/scripts/2fa.ts b/src/client/scripts/2fa.ts
deleted file mode 100644
index 00363cffa6..0000000000
--- a/src/client/scripts/2fa.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export function byteify(data: string, encoding: 'ascii' | 'base64' | 'hex') {
-	switch (encoding) {
-		case 'ascii':
-			return Uint8Array.from(data, c => c.charCodeAt(0));
-		case 'base64':
-			return Uint8Array.from(
-				atob(
-					data
-						.replace(/-/g, '+')
-						.replace(/_/g, '/')
-				),
-				c => c.charCodeAt(0)
-			);
-		case 'hex':
-			return new Uint8Array(
-				data
-					.match(/.{1,2}/g)
-					.map(byte => parseInt(byte, 16))
-			);
-	}
-}
-
-export function hexify(buffer: ArrayBuffer) {
-	return Array.from(new Uint8Array(buffer))
-		.reduce(
-			(str, byte) => str + byte.toString(16).padStart(2, '0'),
-			''
-		);
-}
-
-export function stringify(buffer: ArrayBuffer) {
-	return String.fromCharCode(... new Uint8Array(buffer));
-}
diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts
deleted file mode 100644
index 5dd08f34ac..0000000000
--- a/src/client/scripts/aiscript/api.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { utils, values } from '@syuilo/aiscript';
-import * as os from '@client/os';
-import { $i } from '@client/account';
-
-export function createAiScriptEnv(opts) {
-	let apiRequests = 0;
-	return {
-		USER_ID: $i ? values.STR($i.id) : values.NULL,
-		USER_NAME: $i ? values.STR($i.name) : values.NULL,
-		USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
-		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
-			await os.dialog({
-				type: type ? type.value : 'info',
-				title: title.value,
-				text: text.value,
-			});
-		}),
-		'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
-			const confirm = await os.dialog({
-				type: type ? type.value : 'question',
-				showCancelButton: true,
-				title: title.value,
-				text: text.value,
-			});
-			return confirm.canceled ? values.FALSE : values.TRUE;
-		}),
-		'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
-			if (token) utils.assertString(token);
-			apiRequests++;
-			if (apiRequests > 16) return values.NULL;
-			const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
-			return utils.jsToVal(res);
-		}),
-		'Mk:save': values.FN_NATIVE(([key, value]) => {
-			utils.assertString(key);
-			localStorage.setItem('aiscript:' + opts.storageKey + ':' + key.value, JSON.stringify(utils.valToJs(value)));
-			return values.NULL;
-		}),
-		'Mk:load': values.FN_NATIVE(([key]) => {
-			utils.assertString(key);
-			return utils.jsToVal(JSON.parse(localStorage.getItem('aiscript:' + opts.storageKey + ':' + key.value)));
-		}),
-	};
-}
diff --git a/src/client/scripts/autocomplete.ts b/src/client/scripts/autocomplete.ts
deleted file mode 100644
index c0c33b2c7e..0000000000
--- a/src/client/scripts/autocomplete.ts
+++ /dev/null
@@ -1,276 +0,0 @@
-import { Ref, ref } from 'vue';
-import * as getCaretCoordinates from 'textarea-caret';
-import { toASCII } from 'punycode/';
-import { popup } from '@client/os';
-
-export class Autocomplete {
-	private suggestion: {
-		x: Ref<number>;
-		y: Ref<number>;
-		q: Ref<string | null>;
-		close: Function;
-	} | null;
-	private textarea: any;
-	private vm: any;
-	private currentType: string;
-	private opts: {
-		model: string;
-	};
-	private opening: boolean;
-
-	private get text(): string {
-		return this.vm[this.opts.model];
-	}
-
-	private set text(text: string) {
-		this.vm[this.opts.model] = text;
-	}
-
-	/**
-	 * 対象のテキストエリアを与えてインスタンスを初期化します。
-	 */
-	constructor(textarea, vm, opts) {
-		//#region BIND
-		this.onInput = this.onInput.bind(this);
-		this.complete = this.complete.bind(this);
-		this.close = this.close.bind(this);
-		//#endregion
-
-		this.suggestion = null;
-		this.textarea = textarea;
-		this.vm = vm;
-		this.opts = opts;
-		this.opening = false;
-
-		this.attach();
-	}
-
-	/**
-	 * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
-	 */
-	public attach() {
-		this.textarea.addEventListener('input', this.onInput);
-	}
-
-	/**
-	 * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
-	 */
-	public detach() {
-		this.textarea.removeEventListener('input', this.onInput);
-		this.close();
-	}
-
-	/**
-	 * テキスト入力時
-	 */
-	private onInput() {
-		const caretPos = this.textarea.selectionStart;
-		const text = this.text.substr(0, caretPos).split('\n').pop()!;
-
-		const mentionIndex = text.lastIndexOf('@');
-		const hashtagIndex = text.lastIndexOf('#');
-		const emojiIndex = text.lastIndexOf(':');
-		const mfmTagIndex = text.lastIndexOf('$');
-
-		const max = Math.max(
-			mentionIndex,
-			hashtagIndex,
-			emojiIndex,
-			mfmTagIndex);
-
-		if (max == -1) {
-			this.close();
-			return;
-		}
-
-		const isMention = mentionIndex != -1;
-		const isHashtag = hashtagIndex != -1;
-		const isMfmTag = mfmTagIndex != -1;
-		const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
-
-		let opened = false;
-
-		if (isMention) {
-			const username = text.substr(mentionIndex + 1);
-			if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
-				this.open('user', username);
-				opened = true;
-			} else if (username === '') {
-				this.open('user', null);
-				opened = true;
-			}
-		}
-
-		if (isHashtag && !opened) {
-			const hashtag = text.substr(hashtagIndex + 1);
-			if (!hashtag.includes(' ')) {
-				this.open('hashtag', hashtag);
-				opened = true;
-			}
-		}
-
-		if (isEmoji && !opened) {
-			const emoji = text.substr(emojiIndex + 1);
-			if (!emoji.includes(' ')) {
-				this.open('emoji', emoji);
-				opened = true;
-			}
-		}
-
-		if (isMfmTag && !opened) {
-			const mfmTag = text.substr(mfmTagIndex + 1);
-			if (!mfmTag.includes(' ')) {
-				this.open('mfmTag', mfmTag.replace('[', ''));
-				opened = true;
-			}
-		}
-
-		if (!opened) {
-			this.close();
-		}
-	}
-
-	/**
-	 * サジェストを提示します。
-	 */
-	private async open(type: string, q: string | null) {
-		if (type != this.currentType) {
-			this.close();
-		}
-		if (this.opening) return;
-		this.opening = true;
-		this.currentType = type;
-
-		//#region サジェストを表示すべき位置を計算
-		const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
-
-		const rect = this.textarea.getBoundingClientRect();
-
-		const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
-		const y = rect.top + caretPosition.top - this.textarea.scrollTop;
-		//#endregion
-
-		if (this.suggestion) {
-			this.suggestion.x.value = x;
-			this.suggestion.y.value = y;
-			this.suggestion.q.value = q;
-
-			this.opening = false;
-		} else {
-			const _x = ref(x);
-			const _y = ref(y);
-			const _q = ref(q);
-
-			const { dispose } = await popup(import('@client/components/autocomplete.vue'), {
-				textarea: this.textarea,
-				close: this.close,
-				type: type,
-				q: _q,
-				x: _x,
-				y: _y,
-			}, {
-				done: (res) => {
-					this.complete(res);
-				}
-			});
-
-			this.suggestion = {
-				q: _q,
-				x: _x,
-				y: _y,
-				close: () => dispose(),
-			};
-
-			this.opening = false;
-		}
-	}
-
-	/**
-	 * サジェストを閉じます。
-	 */
-	private close() {
-		if (this.suggestion == null) return;
-
-		this.suggestion.close();
-		this.suggestion = null;
-
-		this.textarea.focus();
-	}
-
-	/**
-	 * オートコンプリートする
-	 */
-	private complete({ type, value }) {
-		this.close();
-
-		const caret = this.textarea.selectionStart;
-
-		if (type == 'user') {
-			const source = this.text;
-
-			const before = source.substr(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
-			const after = source.substr(caret);
-
-			const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
-
-			// 挿入
-			this.text = `${trimmedBefore}@${acct} ${after}`;
-
-			// キャレットを戻す
-			this.vm.$nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + (acct.length + 2);
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		} else if (type == 'hashtag') {
-			const source = this.text;
-
-			const before = source.substr(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
-			const after = source.substr(caret);
-
-			// 挿入
-			this.text = `${trimmedBefore}#${value} ${after}`;
-
-			// キャレットを戻す
-			this.vm.$nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + (value.length + 2);
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		} else if (type == 'emoji') {
-			const source = this.text;
-
-			const before = source.substr(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
-			const after = source.substr(caret);
-
-			// 挿入
-			this.text = trimmedBefore + value + after;
-
-			// キャレットを戻す
-			this.vm.$nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + value.length;
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		} else if (type == 'mfmTag') {
-			const source = this.text;
-
-			const before = source.substr(0, caret);
-			const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
-			const after = source.substr(caret);
-
-			// 挿入
-			this.text = `${trimmedBefore}$[${value} ]${after}`;
-
-			// キャレットを戻す
-			this.vm.$nextTick(() => {
-				this.textarea.focus();
-				const pos = trimmedBefore.length + (value.length + 3);
-				this.textarea.setSelectionRange(pos, pos);
-			});
-		}
-	}
-}
diff --git a/src/client/scripts/check-word-mute.ts b/src/client/scripts/check-word-mute.ts
deleted file mode 100644
index 3b1fa75b1e..0000000000
--- a/src/client/scripts/check-word-mute.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> {
-	// 自分自身
-	if (me && (note.userId === me.id)) return false;
-
-	const words = mutedWords
-		// Clean up
-		.map(xs => xs.filter(x => x !== ''))
-		.filter(xs => xs.length > 0);
-
-	if (words.length > 0) {
-		if (note.text == null) return false;
-
-		const matched = words.some(and =>
-			and.every(keyword => {
-				const regexp = keyword.match(/^\/(.+)\/(.*)$/);
-				if (regexp) {
-					return new RegExp(regexp[1], regexp[2]).test(note.text!);
-				}
-				return note.text!.includes(keyword);
-			}));
-
-		if (matched) return true;
-	}
-
-	return false;
-}
diff --git a/src/client/scripts/collect-page-vars.ts b/src/client/scripts/collect-page-vars.ts
deleted file mode 100644
index a4096fb2c2..0000000000
--- a/src/client/scripts/collect-page-vars.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-export function collectPageVars(content) {
-	const pageVars = [];
-	const collect = (xs: any[]) => {
-		for (const x of xs) {
-			if (x.type === 'textInput') {
-				pageVars.push({
-					name: x.name,
-					type: 'string',
-					value: x.default || ''
-				});
-			} else if (x.type === 'textareaInput') {
-				pageVars.push({
-					name: x.name,
-					type: 'string',
-					value: x.default || ''
-				});
-			} else if (x.type === 'numberInput') {
-				pageVars.push({
-					name: x.name,
-					type: 'number',
-					value: x.default || 0
-				});
-			} else if (x.type === 'switch') {
-				pageVars.push({
-					name: x.name,
-					type: 'boolean',
-					value: x.default || false
-				});
-			} else if (x.type === 'counter') {
-				pageVars.push({
-					name: x.name,
-					type: 'number',
-					value: 0
-				});
-			} else if (x.type === 'radioButton') {
-				pageVars.push({
-					name: x.name,
-					type: 'string',
-					value: x.default || ''
-				});
-			} else if (x.children) {
-				collect(x.children);
-			}
-		}
-	};
-	collect(content);
-	return pageVars;
-}
diff --git a/src/client/scripts/contains.ts b/src/client/scripts/contains.ts
deleted file mode 100644
index 770bda63bb..0000000000
--- a/src/client/scripts/contains.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export default (parent, child, checkSame = true) => {
-	if (checkSame && parent === child) return true;
-	let node = child.parentNode;
-	while (node) {
-		if (node == parent) return true;
-		node = node.parentNode;
-	}
-	return false;
-};
diff --git a/src/client/scripts/copy-to-clipboard.ts b/src/client/scripts/copy-to-clipboard.ts
deleted file mode 100644
index ab13cab970..0000000000
--- a/src/client/scripts/copy-to-clipboard.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Clipboardに値をコピー(TODO: 文字列以外も対応)
- */
-export default val => {
-	// 空div 生成
-	const tmp = document.createElement('div');
-	// 選択用のタグ生成
-	const pre = document.createElement('pre');
-
-	// 親要素のCSSで user-select: none だとコピーできないので書き換える
-	pre.style.webkitUserSelect = 'auto';
-	pre.style.userSelect = 'auto';
-
-	tmp.appendChild(pre).textContent = val;
-
-	// 要素を画面外へ
-	const s = tmp.style;
-	s.position = 'fixed';
-	s.right = '200%';
-
-	// body に追加
-	document.body.appendChild(tmp);
-	// 要素を選択
-	document.getSelection().selectAllChildren(tmp);
-
-	// クリップボードにコピー
-	const result = document.execCommand('copy');
-
-	// 要素削除
-	document.body.removeChild(tmp);
-
-	return result;
-};
diff --git a/src/client/scripts/extract-avg-color-from-blurhash.ts b/src/client/scripts/extract-avg-color-from-blurhash.ts
deleted file mode 100644
index 123ab7a06d..0000000000
--- a/src/client/scripts/extract-avg-color-from-blurhash.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export function extractAvgColorFromBlurhash(hash: string) {
-	return typeof hash == 'string'
-		? '#' + [...hash.slice(2, 6)]
-				.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
-				.reduce((a, c) => a * 83 + c, 0)
-				.toString(16)
-				.padStart(6, '0')
-		: undefined;
-}
diff --git a/src/client/scripts/focus.ts b/src/client/scripts/focus.ts
deleted file mode 100644
index 0894877820..0000000000
--- a/src/client/scripts/focus.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-export function focusPrev(el: Element | null, self = false, scroll = true) {
-	if (el == null) return;
-	if (!self) el = el.previousElementSibling;
-	if (el) {
-		if (el.hasAttribute('tabindex')) {
-			(el as HTMLElement).focus({
-				preventScroll: !scroll
-			});
-		} else {
-			focusPrev(el.previousElementSibling, true);
-		}
-	}
-}
-
-export function focusNext(el: Element | null, self = false, scroll = true) {
-	if (el == null) return;
-	if (!self) el = el.nextElementSibling;
-	if (el) {
-		if (el.hasAttribute('tabindex')) {
-			(el as HTMLElement).focus({
-				preventScroll: !scroll
-			});
-		} else {
-			focusPrev(el.nextElementSibling, true);
-		}
-	}
-}
diff --git a/src/client/scripts/form.ts b/src/client/scripts/form.ts
deleted file mode 100644
index 7bf6cec452..0000000000
--- a/src/client/scripts/form.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export type FormItem = {
-	label?: string;
-	type: 'string';
-	default: string | null;
-	hidden?: boolean;
-	multiline?: boolean;
-} | {
-	label?: string;
-	type: 'number';
-	default: number | null;
-	hidden?: boolean;
-	step?: number;
-} | {
-	label?: string;
-	type: 'boolean';
-	default: boolean | null;
-	hidden?: boolean;
-} | {
-	label?: string;
-	type: 'enum';
-	default: string | null;
-	hidden?: boolean;
-	enum: string[];
-} | {
-	label?: string;
-	type: 'array';
-	default: unknown[] | null;
-	hidden?: boolean;
-};
-
-export type Form = Record<string, FormItem>;
diff --git a/src/client/scripts/gen-search-query.ts b/src/client/scripts/gen-search-query.ts
deleted file mode 100644
index cafb3cccfe..0000000000
--- a/src/client/scripts/gen-search-query.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { parseAcct } from '@/misc/acct';
-import { host as localHost } from '@client/config';
-
-export async function genSearchQuery(v: any, q: string) {
-	let host: string;
-	let userId: string;
-	if (q.split(' ').some(x => x.startsWith('@'))) {
-		for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) {
-			if (at.includes('.')) {
-				if (at === localHost || at === '.') {
-					host = null;
-				} else {
-					host = at;
-				}
-			} else {
-				const user = await v.os.api('users/show', parseAcct(at)).catch(x => null);
-				if (user) {
-					userId = user.id;
-				} else {
-					// todo: show error
-				}
-			}
-		}
-
-	}
-	return {
-		query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
-		host: host,
-		userId: userId
-	};
-}
diff --git a/src/client/scripts/get-account-from-id.ts b/src/client/scripts/get-account-from-id.ts
deleted file mode 100644
index 065b41118c..0000000000
--- a/src/client/scripts/get-account-from-id.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { get } from '@client/scripts/idb-proxy';
-
-export async function getAccountFromId(id: string) {
-	const accounts = await get('accounts') as { token: string; id: string; }[];
-	if (!accounts) console.log('Accounts are not recorded');
-	return accounts.find(e => e.id === id);
-}
diff --git a/src/client/scripts/get-md5.ts b/src/client/scripts/get-md5.ts
deleted file mode 100644
index b002d762b1..0000000000
--- a/src/client/scripts/get-md5.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-// スクリプトサイズがデカい
-//import * as crypto from 'crypto';
-
-export default (data: ArrayBuffer) => {
-	//const buf = new Buffer(data);
-	//const hash = crypto.createHash('md5');
-	//hash.update(buf);
-	//return hash.digest('hex');
-	return '';
-};
diff --git a/src/client/scripts/get-static-image-url.ts b/src/client/scripts/get-static-image-url.ts
deleted file mode 100644
index 92c31914c7..0000000000
--- a/src/client/scripts/get-static-image-url.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { url as instanceUrl } from '@client/config';
-import * as url from '../../prelude/url';
-
-export function getStaticImageUrl(baseUrl: string): string {
-	const u = new URL(baseUrl);
-	if (u.href.startsWith(`${instanceUrl}/proxy/`)) {
-		// もう既にproxyっぽそうだったらsearchParams付けるだけ
-		u.searchParams.set('static', '1');
-		return u.href;
-	}
-	const dummy = `${u.host}${u.pathname}`;	// 拡張子がないとキャッシュしてくれないCDNがあるので
-	return `${instanceUrl}/proxy/${dummy}?${url.query({
-		url: u.href,
-		static: '1'
-	})}`;
-}
diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts
deleted file mode 100644
index 3689a93b47..0000000000
--- a/src/client/scripts/get-user-menu.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-import { i18n } from '@client/i18n';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { host } from '@client/config';
-import { getAcct } from '@/misc/acct';
-import * as os from '@client/os';
-import { userActions } from '@client/store';
-import { router } from '@client/router';
-import { $i } from '@client/account';
-
-export function getUserMenu(user) {
-	const meId = $i ? $i.id : null;
-
-	async function pushList() {
-		const t = i18n.locale.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく
-		const lists = await os.api('users/lists/list');
-		if (lists.length === 0) {
-			os.dialog({
-				type: 'error',
-				text: i18n.locale.youHaveNoLists
-			});
-			return;
-		}
-		const { canceled, result: listId } = await os.dialog({
-			type: null,
-			title: t,
-			select: {
-				items: lists.map(list => ({
-					value: list.id, text: list.name
-				}))
-			},
-			showCancelButton: true
-		});
-		if (canceled) return;
-		os.apiWithDialog('users/lists/push', {
-			listId: listId,
-			userId: user.id
-		});
-	}
-
-	async function inviteGroup() {
-		const groups = await os.api('users/groups/owned');
-		if (groups.length === 0) {
-			os.dialog({
-				type: 'error',
-				text: i18n.locale.youHaveNoGroups
-			});
-			return;
-		}
-		const { canceled, result: groupId } = await os.dialog({
-			type: null,
-			title: i18n.locale.group,
-			select: {
-				items: groups.map(group => ({
-					value: group.id, text: group.name
-				}))
-			},
-			showCancelButton: true
-		});
-		if (canceled) return;
-		os.apiWithDialog('users/groups/invite', {
-			groupId: groupId,
-			userId: user.id
-		});
-	}
-
-	async function toggleMute() {
-		os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', {
-			userId: user.id
-		}).then(() => {
-			user.isMuted = !user.isMuted;
-		});
-	}
-
-	async function toggleBlock() {
-		if (!await getConfirmed(user.isBlocking ? i18n.locale.unblockConfirm : i18n.locale.blockConfirm)) return;
-
-		os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', {
-			userId: user.id
-		}).then(() => {
-			user.isBlocking = !user.isBlocking;
-		});
-	}
-
-	async function toggleSilence() {
-		if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return;
-
-		os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
-			userId: user.id
-		}).then(() => {
-			user.isSilenced = !user.isSilenced;
-		});
-	}
-
-	async function toggleSuspend() {
-		if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return;
-
-		os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
-			userId: user.id
-		}).then(() => {
-			user.isSuspended = !user.isSuspended;
-		});
-	}
-
-	function reportAbuse() {
-		os.popup(import('@client/components/abuse-report-window.vue'), {
-			user: user,
-		}, {}, 'closed');
-	}
-
-	async function getConfirmed(text: string): Promise<boolean> {
-		const confirm = await os.dialog({
-			type: 'warning',
-			showCancelButton: true,
-			title: 'confirm',
-			text,
-		});
-
-		return !confirm.canceled;
-	}
-
-	let menu = [{
-		icon: 'fas fa-at',
-		text: i18n.locale.copyUsername,
-		action: () => {
-			copyToClipboard(`@${user.username}@${user.host || host}`);
-		}
-	}, {
-		icon: 'fas fa-info-circle',
-		text: i18n.locale.info,
-		action: () => {
-			os.pageWindow(`/user-info/${user.id}`);
-		}
-	}, {
-		icon: 'fas fa-envelope',
-		text: i18n.locale.sendMessage,
-		action: () => {
-			os.post({ specified: user });
-		}
-	}, meId != user.id ? {
-		type: 'link',
-		icon: 'fas fa-comments',
-		text: i18n.locale.startMessaging,
-		to: '/my/messaging/' + getAcct(user),
-	} : undefined, null, {
-		icon: 'fas fa-list-ul',
-		text: i18n.locale.addToList,
-		action: pushList
-	}, meId != user.id ? {
-		icon: 'fas fa-users',
-		text: i18n.locale.inviteToGroup,
-		action: inviteGroup
-	} : undefined] as any;
-
-	if ($i && meId != user.id) {
-		menu = menu.concat([null, {
-			icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash',
-			text: user.isMuted ? i18n.locale.unmute : i18n.locale.mute,
-			action: toggleMute
-		}, {
-			icon: 'fas fa-ban',
-			text: user.isBlocking ? i18n.locale.unblock : i18n.locale.block,
-			action: toggleBlock
-		}]);
-
-		menu = menu.concat([null, {
-			icon: 'fas fa-exclamation-circle',
-			text: i18n.locale.reportAbuse,
-			action: reportAbuse
-		}]);
-
-		if ($i && ($i.isAdmin || $i.isModerator)) {
-			menu = menu.concat([null, {
-				icon: 'fas fa-microphone-slash',
-				text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence,
-				action: toggleSilence
-			}, {
-				icon: 'fas fa-snowflake',
-				text: user.isSuspended ? i18n.locale.unsuspend : i18n.locale.suspend,
-				action: toggleSuspend
-			}]);
-		}
-	}
-
-	if ($i && meId === user.id) {
-		menu = menu.concat([null, {
-			icon: 'fas fa-pencil-alt',
-			text: i18n.locale.editProfile,
-			action: () => {
-				router.push('/settings/profile');
-			}
-		}]);
-	}
-
-	if (userActions.length > 0) {
-		menu = menu.concat([null, ...userActions.map(action => ({
-			icon: 'fas fa-plug',
-			text: action.title,
-			action: () => {
-				action.handler(user);
-			}
-		}))]);
-	}
-
-	return menu;
-}
diff --git a/src/client/scripts/hotkey.ts b/src/client/scripts/hotkey.ts
deleted file mode 100644
index 2b3f491fd8..0000000000
--- a/src/client/scripts/hotkey.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import keyCode from './keycode';
-
-type Keymap = Record<string, Function>;
-
-type Pattern = {
-	which: string[];
-	ctrl?: boolean;
-	shift?: boolean;
-	alt?: boolean;
-};
-
-type Action = {
-	patterns: Pattern[];
-	callback: Function;
-	allowRepeat: boolean;
-};
-
-const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => {
-	const result = {
-		patterns: [],
-		callback: callback,
-		allowRepeat: true
-	} as Action;
-
-	if (patterns.match(/^\(.*\)$/) !== null) {
-		result.allowRepeat = false;
-		patterns = patterns.slice(1, -1);
-	}
-
-	result.patterns = patterns.split('|').map(part => {
-		const pattern = {
-			which: [],
-			ctrl: false,
-			alt: false,
-			shift: false
-		} as Pattern;
-
-		const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
-		for (const key of keys) {
-			switch (key) {
-				case 'ctrl': pattern.ctrl = true; break;
-				case 'alt': pattern.alt = true; break;
-				case 'shift': pattern.shift = true; break;
-				default: pattern.which = keyCode(key).map(k => k.toLowerCase());
-			}
-		}
-
-		return pattern;
-	});
-
-	return result;
-});
-
-const ignoreElemens = ['input', 'textarea'];
-
-function match(e: KeyboardEvent, patterns: Action['patterns']): boolean {
-	const key = e.code.toLowerCase();
-	return patterns.some(pattern => pattern.which.includes(key) &&
-		pattern.ctrl === e.ctrlKey &&
-		pattern.shift === e.shiftKey &&
-		pattern.alt === e.altKey &&
-		!e.metaKey
-	);
-}
-
-export const makeHotkey = (keymap: Keymap) => {
-	const actions = parseKeymap(keymap);
-
-	return (e: KeyboardEvent) => {
-		if (document.activeElement) {
-			if (ignoreElemens.some(el => document.activeElement!.matches(el))) return;
-			if (document.activeElement.attributes['contenteditable']) return;
-		}
-
-		for (const action of actions) {
-			const matched = match(e, action.patterns);
-
-			if (matched) {
-				if (!action.allowRepeat && e.repeat) return;
-
-				e.preventDefault();
-				e.stopPropagation();
-				action.callback(e);
-				break;
-			}
-		}
-	};
-};
diff --git a/src/client/scripts/hpml/block.ts b/src/client/scripts/hpml/block.ts
deleted file mode 100644
index 804c5c1124..0000000000
--- a/src/client/scripts/hpml/block.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-// blocks
-
-export type BlockBase = {
-	id: string;
-	type: string;
-};
-
-export type TextBlock = BlockBase & {
-	type: 'text';
-	text: string;
-};
-
-export type SectionBlock = BlockBase & {
-	type: 'section';
-	title: string;
-	children: (Block | VarBlock)[];
-};
-
-export type ImageBlock = BlockBase & {
-	type: 'image';
-	fileId: string | null;
-};
-
-export type ButtonBlock = BlockBase & {
-	type: 'button';
-	text: any;
-	primary: boolean;
-	action: string;
-	content: string;
-	event: string;
-	message: string;
-	var: string;
-	fn: string;
-};
-
-export type IfBlock = BlockBase & {
-	type: 'if';
-	var: string;
-	children: Block[];
-};
-
-export type TextareaBlock = BlockBase & {
-	type: 'textarea';
-	text: string;
-};
-
-export type PostBlock = BlockBase & {
-	type: 'post';
-	text: string;
-	attachCanvasImage: boolean;
-	canvasId: string;
-};
-
-export type CanvasBlock = BlockBase & {
-	type: 'canvas';
-	name: string; // canvas id
-	width: number;
-	height: number;
-};
-
-export type NoteBlock = BlockBase & {
-	type: 'note';
-	detailed: boolean;
-	note: string | null;
-};
-
-export type Block =
-	TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock;
-
-// variable blocks
-
-export type VarBlockBase = BlockBase & {
-	name: string;
-};
-
-export type NumberInputVarBlock = VarBlockBase & {
-	type: 'numberInput';
-	text: string;
-};
-
-export type TextInputVarBlock = VarBlockBase & {
-	type: 'textInput';
-	text: string;
-};
-
-export type SwitchVarBlock = VarBlockBase & {
-	type: 'switch';
-	text: string;
-};
-
-export type RadioButtonVarBlock = VarBlockBase & {
-	type: 'radioButton';
-	title: string;
-	values: string[];
-};
-
-export type CounterVarBlock = VarBlockBase & {
-	type: 'counter';
-	text: string;
-	inc: number;
-};
-
-export type VarBlock =
-	NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock;
-
-const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter'];
-export function isVarBlock(block: Block): block is VarBlock {
-	return varBlock.includes(block.type);
-}
diff --git a/src/client/scripts/hpml/evaluator.ts b/src/client/scripts/hpml/evaluator.ts
deleted file mode 100644
index 68d140ff50..0000000000
--- a/src/client/scripts/hpml/evaluator.ts
+++ /dev/null
@@ -1,234 +0,0 @@
-import autobind from 'autobind-decorator';
-import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.';
-import { version } from '@client/config';
-import { AiScript, utils, values } from '@syuilo/aiscript';
-import { createAiScriptEnv } from '../aiscript/api';
-import { collectPageVars } from '../collect-page-vars';
-import { initHpmlLib, initAiLib } from './lib';
-import * as os from '@client/os';
-import { markRaw, ref, Ref, unref } from 'vue';
-import { Expr, isLiteralValue, Variable } from './expr';
-
-/**
- * Hpml evaluator
- */
-export class Hpml {
-	private variables: Variable[];
-	private pageVars: PageVar[];
-	private envVars: Record<keyof typeof envVarsDef, any>;
-	public aiscript?: AiScript;
-	public pageVarUpdatedCallback?: values.VFn;
-	public canvases: Record<string, HTMLCanvasElement> = {};
-	public vars: Ref<Record<string, any>> = ref({});
-	public page: Record<string, any>;
-
-	private opts: {
-		randomSeed: string; visitor?: any; url?: string;
-		enableAiScript: boolean;
-	};
-
-	constructor(page: Hpml['page'], opts: Hpml['opts']) {
-		this.page = page;
-		this.variables = this.page.variables;
-		this.pageVars = collectPageVars(this.page.content);
-		this.opts = opts;
-
-		if (this.opts.enableAiScript) {
-			this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({
-				storageKey: 'pages:' + this.page.id
-			}), ...initAiLib(this)}, {
-				in: (q) => {
-					return new Promise(ok => {
-						os.dialog({
-							title: q,
-							input: {}
-						}).then(({ canceled, result: a }) => {
-							ok(a);
-						});
-					});
-				},
-				out: (value) => {
-					console.log(value);
-				},
-				log: (type, params) => {
-				},
-			}));
-
-			this.aiscript.scope.opts.onUpdated = (name, value) => {
-				this.eval();
-			};
-		}
-
-		const date = new Date();
-
-		this.envVars = {
-			AI: 'kawaii',
-			VERSION: version,
-			URL: this.page ? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}` : '',
-			LOGIN: opts.visitor != null,
-			NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '',
-			USERNAME: opts.visitor ? opts.visitor.username : '',
-			USERID: opts.visitor ? opts.visitor.id : '',
-			NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0,
-			FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0,
-			FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0,
-			IS_CAT: opts.visitor ? opts.visitor.isCat : false,
-			SEED: opts.randomSeed ? opts.randomSeed : '',
-			YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`,
-			AISCRIPT_DISABLED: !this.opts.enableAiScript,
-			NULL: null
-		};
-
-		this.eval();
-	}
-
-	@autobind
-	public eval() {
-		try {
-			this.vars.value = this.evaluateVars();
-		} catch (e) {
-			//this.onError(e);
-		}
-	}
-
-	@autobind
-	public interpolate(str: string) {
-		if (str == null) return null;
-		return str.replace(/{(.+?)}/g, match => {
-			const v = unref(this.vars)[match.slice(1, -1).trim()];
-			return v == null ? 'NULL' : v.toString();
-		});
-	}
-
-	@autobind
-	public callAiScript(fn: string) {
-		try {
-			if (this.aiscript) this.aiscript.execFn(this.aiscript.scope.get(fn), []);
-		} catch (e) {}
-	}
-
-	@autobind
-	public registerCanvas(id: string, canvas: any) {
-		this.canvases[id] = canvas;
-	}
-
-	@autobind
-	public updatePageVar(name: string, value: any) {
-		const pageVar = this.pageVars.find(v => v.name === name);
-		if (pageVar !== undefined) {
-			pageVar.value = value;
-			if (this.pageVarUpdatedCallback) {
-				if (this.aiscript) this.aiscript.execFn(this.pageVarUpdatedCallback, [values.STR(name), utils.jsToVal(value)]);
-			}
-		} else {
-			throw new HpmlError(`No such page var '${name}'`);
-		}
-	}
-
-	@autobind
-	public updateRandomSeed(seed: string) {
-		this.opts.randomSeed = seed;
-		this.envVars.SEED = seed;
-	}
-
-	@autobind
-	private _interpolateScope(str: string, scope: HpmlScope) {
-		return str.replace(/{(.+?)}/g, match => {
-			const v = scope.getState(match.slice(1, -1).trim());
-			return v == null ? 'NULL' : v.toString();
-		});
-	}
-
-	@autobind
-	public evaluateVars(): Record<string, any> {
-		const values: Record<string, any> = {};
-
-		for (const [k, v] of Object.entries(this.envVars)) {
-			values[k] = v;
-		}
-
-		for (const v of this.pageVars) {
-			values[v.name] = v.value;
-		}
-
-		for (const v of this.variables) {
-			values[v.name] = this.evaluate(v, new HpmlScope([values]));
-		}
-
-		return values;
-	}
-
-	@autobind
-	private evaluate(expr: Expr, scope: HpmlScope): any {
-
-		if (isLiteralValue(expr)) {
-			if (expr.type === null) {
-				return null;
-			}
-
-			if (expr.type === 'number') {
-				return parseInt((expr.value as any), 10);
-			}
-
-			if (expr.type === 'text' || expr.type === 'multiLineText') {
-				return this._interpolateScope(expr.value || '', scope);
-			}
-
-			if (expr.type === 'textList') {
-				return this._interpolateScope(expr.value || '', scope).trim().split('\n');
-			}
-
-			if (expr.type === 'ref') {
-				return scope.getState(expr.value);
-			}
-
-			if (expr.type === 'aiScriptVar') {
-				if (this.aiscript) {
-					try {
-						return utils.valToJs(this.aiscript.scope.get(expr.value));
-					} catch (e) {
-						return null;
-					}
-				} else {
-					return null;
-				}
-			}
-
-			// Define user function
-			if (expr.type == 'fn') {
-				return {
-					slots: expr.value.slots.map(x => x.name),
-					exec: (slotArg: Record<string, any>) => {
-						return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id));
-					}
-				} as Fn;
-			}
-			return;
-		}
-
-		// Call user function
-		if (expr.type.startsWith('fn:')) {
-			const fnName = expr.type.split(':')[1];
-			const fn = scope.getState(fnName);
-			const args = {} as Record<string, any>;
-			for (let i = 0; i < fn.slots.length; i++) {
-				const name = fn.slots[i];
-				args[name] = this.evaluate(expr.args[i], scope);
-			}
-			return fn.exec(args);
-		}
-
-		if (expr.args === undefined) return null;
-
-		const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor);
-
-		// Call function
-		const fnName = expr.type;
-		const fn = (funcs as any)[fnName];
-		if (fn == null) {
-			throw new HpmlError(`No such function '${fnName}'`);
-		} else {
-			return fn(...expr.args.map(x => this.evaluate(x, scope)));
-		}
-	}
-}
diff --git a/src/client/scripts/hpml/expr.ts b/src/client/scripts/hpml/expr.ts
deleted file mode 100644
index 00e3ed118b..0000000000
--- a/src/client/scripts/hpml/expr.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { literalDefs, Type } from '.';
-
-export type ExprBase = {
-	id: string;
-};
-
-// value
-
-export type EmptyValue = ExprBase & {
-	type: null;
-	value: null;
-};
-
-export type TextValue = ExprBase & {
-	type: 'text';
-	value: string;
-};
-
-export type MultiLineTextValue = ExprBase  & {
-	type: 'multiLineText';
-	value: string;
-};
-
-export type TextListValue = ExprBase & {
-	type: 'textList';
-	value: string;
-};
-
-export type NumberValue = ExprBase & {
-	type: 'number';
-	value: number;
-};
-
-export type RefValue = ExprBase & {
-	type: 'ref';
-	value: string; // value is variable name
-};
-
-export type AiScriptRefValue = ExprBase & {
-	type: 'aiScriptVar';
-	value: string; // value is variable name
-};
-
-export type UserFnValue = ExprBase & {
-	type: 'fn';
-	value: UserFnInnerValue;
-};
-type UserFnInnerValue = {
-	slots: {
-		name: string;
-		type: Type;
-	}[];
-	expression: Expr;
-};
-
-export type Value =
-	EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue;
-
-export function isLiteralValue(expr: Expr): expr is Value {
-	if (expr.type == null) return true;
-	if (literalDefs[expr.type]) return true;
-	return false;
-}
-
-// call function
-
-export type CallFn = ExprBase & { // "fn:hoge" or string
-	type: string;
-	args: Expr[];
-	value: null;
-};
-
-// variable
-export type Variable = (Value | CallFn) & {
-	name: string;
-};
-
-// expression
-export type Expr = Variable | Value | CallFn;
diff --git a/src/client/scripts/hpml/index.ts b/src/client/scripts/hpml/index.ts
deleted file mode 100644
index ac81eac2d9..0000000000
--- a/src/client/scripts/hpml/index.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * Hpml
- */
-
-import autobind from 'autobind-decorator';
-import { Hpml } from './evaluator';
-import { funcDefs } from './lib';
-
-export type Fn = {
-	slots: string[];
-	exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>;
-};
-
-export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
-
-export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = {
-	text:          { out: 'string',      category: 'value', icon: 'fas fa-quote-right', },
-	multiLineText: { out: 'string',      category: 'value', icon: 'fas fa-align-left', },
-	textList:      { out: 'stringArray', category: 'value', icon: 'fas fa-list', },
-	number:        { out: 'number',      category: 'value', icon: 'fas fa-sort-numeric-up', },
-	ref:           { out: null,          category: 'value', icon: 'fas fa-magic', },
-	aiScriptVar:   { out: null,          category: 'value', icon: 'fas fa-magic', },
-	fn:            { out: 'function',    category: 'value', icon: 'fas fa-square-root-alt', },
-};
-
-export const blockDefs = [
-	...Object.entries(literalDefs).map(([k, v]) => ({
-		type: k, out: v.out, category: v.category, icon: v.icon
-	})),
-	...Object.entries(funcDefs).map(([k, v]) => ({
-		type: k, out: v.out, category: v.category, icon: v.icon
-	}))
-];
-
-export type PageVar = { name: string; value: any; type: Type; };
-
-export const envVarsDef: Record<string, Type> = {
-	AI: 'string',
-	URL: 'string',
-	VERSION: 'string',
-	LOGIN: 'boolean',
-	NAME: 'string',
-	USERNAME: 'string',
-	USERID: 'string',
-	NOTES_COUNT: 'number',
-	FOLLOWERS_COUNT: 'number',
-	FOLLOWING_COUNT: 'number',
-	IS_CAT: 'boolean',
-	SEED: null,
-	YMD: 'string',
-	AISCRIPT_DISABLED: 'boolean',
-	NULL: null,
-};
-
-export class HpmlScope {
-	private layerdStates: Record<string, any>[];
-	public name: string;
-
-	constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) {
-		this.layerdStates = layerdStates;
-		this.name = name || 'anonymous';
-	}
-
-	@autobind
-	public createChildScope(states: Record<string, any>, name?: HpmlScope['name']): HpmlScope {
-		const layer = [states, ...this.layerdStates];
-		return new HpmlScope(layer, name);
-	}
-
-	/**
-	 * 指定した名前の変数の値を取得します
-	 * @param name 変数名
-	 */
-	@autobind
-	public getState(name: string): any {
-		for (const later of this.layerdStates) {
-			const state = later[name];
-			if (state !== undefined) {
-				return state;
-			}
-		}
-
-		throw new HpmlError(
-			`No such variable '${name}' in scope '${this.name}'`, {
-				scope: this.layerdStates
-			});
-	}
-}
-
-export class HpmlError extends Error {
-	public info?: any;
-
-	constructor(message: string, info?: any) {
-		super(message);
-
-		this.info = info;
-
-		// Maintains proper stack trace for where our error was thrown (only available on V8)
-		if (Error.captureStackTrace) {
-			Error.captureStackTrace(this, HpmlError);
-		}
-	}
-}
diff --git a/src/client/scripts/hpml/lib.ts b/src/client/scripts/hpml/lib.ts
deleted file mode 100644
index 2a1ac73a40..0000000000
--- a/src/client/scripts/hpml/lib.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-import * as tinycolor from 'tinycolor2';
-import { Hpml } from './evaluator';
-import { values, utils } from '@syuilo/aiscript';
-import { Fn, HpmlScope } from '.';
-import { Expr } from './expr';
-import * as seedrandom from 'seedrandom';
-
-/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color
-// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
-Chart.pluginService.register({
-	beforeDraw: (chart, easing) => {
-		if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) {
-			const ctx = chart.chart.ctx;
-			ctx.save();
-			ctx.fillStyle = chart.config.options.chartArea.backgroundColor;
-			ctx.fillRect(0, 0, chart.chart.width, chart.chart.height);
-			ctx.restore();
-		}
-	}
-});
-*/
-
-export function initAiLib(hpml: Hpml) {
-	return {
-		'MkPages:updated': values.FN_NATIVE(([callback]) => {
-			hpml.pageVarUpdatedCallback = (callback as values.VFn);
-		}),
-		'MkPages:get_canvas': values.FN_NATIVE(([id]) => {
-			utils.assertString(id);
-			const canvas = hpml.canvases[id.value];
-			const ctx = canvas.getContext('2d');
-			return values.OBJ(new Map([
-				['clear_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.clearRect(x.value, y.value, width.value, height.value); })],
-				['fill_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.fillRect(x.value, y.value, width.value, height.value); })],
-				['stroke_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.strokeRect(x.value, y.value, width.value, height.value); })],
-				['fill_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.fillText(text.value, x.value, y.value, width ? width.value : undefined); })],
-				['stroke_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.strokeText(text.value, x.value, y.value, width ? width.value : undefined); })],
-				['set_line_width', values.FN_NATIVE(([width]) => { ctx.lineWidth = width.value; })],
-				['set_font', values.FN_NATIVE(([font]) => { ctx.font = font.value; })],
-				['set_fill_style', values.FN_NATIVE(([style]) => { ctx.fillStyle = style.value; })],
-				['set_stroke_style', values.FN_NATIVE(([style]) => { ctx.strokeStyle = style.value; })],
-				['begin_path', values.FN_NATIVE(() => { ctx.beginPath(); })],
-				['close_path', values.FN_NATIVE(() => { ctx.closePath(); })],
-				['move_to', values.FN_NATIVE(([x, y]) => { ctx.moveTo(x.value, y.value); })],
-				['line_to', values.FN_NATIVE(([x, y]) => { ctx.lineTo(x.value, y.value); })],
-				['arc', values.FN_NATIVE(([x, y, radius, startAngle, endAngle]) => { ctx.arc(x.value, y.value, radius.value, startAngle.value, endAngle.value); })],
-				['rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.rect(x.value, y.value, width.value, height.value); })],
-				['fill', values.FN_NATIVE(() => { ctx.fill(); })],
-				['stroke', values.FN_NATIVE(() => { ctx.stroke(); })],
-			]));
-		}),
-		'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
-			/* TODO
-			utils.assertString(id);
-			utils.assertObject(opts);
-			const canvas = hpml.canvases[id.value];
-			const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
-			Chart.defaults.color = '#555';
-			const chart = new Chart(canvas, {
-				type: opts.value.get('type').value,
-				data: {
-					labels: opts.value.get('labels').value.map(x => x.value),
-					datasets: opts.value.get('datasets').value.map(x => ({
-						label: x.value.has('label') ? x.value.get('label').value : '',
-						data: x.value.get('data').value.map(x => x.value),
-						pointRadius: 0,
-						lineTension: 0,
-						borderWidth: 2,
-						borderColor: x.value.has('color') ? x.value.get('color') : color,
-						backgroundColor: tinycolor(x.value.has('color') ? x.value.get('color') : color).setAlpha(0.1).toRgbString(),
-					}))
-				},
-				options: {
-					responsive: false,
-					devicePixelRatio: 1.5,
-					title: {
-						display: opts.value.has('title'),
-						text: opts.value.has('title') ? opts.value.get('title').value : '',
-						fontSize: 14,
-					},
-					layout: {
-						padding: {
-							left: 32,
-							right: 32,
-							top: opts.value.has('title') ? 16 : 32,
-							bottom: 16
-						}
-					},
-					legend: {
-						display: opts.value.get('datasets').value.filter(x => x.value.has('label') && x.value.get('label').value).length === 0 ? false : true,
-						position: 'bottom',
-						labels: {
-							boxWidth: 16,
-						}
-					},
-					tooltips: {
-						enabled: false,
-					},
-					chartArea: {
-						backgroundColor: '#fff'
-					},
-					...(opts.value.get('type').value === 'radar' ? {
-						scale: {
-							ticks: {
-								display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : false,
-								min: opts.value.has('min') ? opts.value.get('min').value : undefined,
-								max: opts.value.has('max') ? opts.value.get('max').value : undefined,
-								maxTicksLimit: 8,
-							},
-							pointLabels: {
-								fontSize: 12
-							}
-						}
-					} : {
-						scales: {
-							yAxes: [{
-								ticks: {
-									display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : true,
-									min: opts.value.has('min') ? opts.value.get('min').value : undefined,
-									max: opts.value.has('max') ? opts.value.get('max').value : undefined,
-								}
-							}]
-						}
-					})
-				}
-			});
-			*/
-		})
-	};
-}
-
-export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
-	if:              { in: ['boolean', 0, 0],              out: 0,             category: 'flow',       icon: 'fas fa-share-alt', },
-	for:             { in: ['number', 'function'],         out: null,          category: 'flow',       icon: 'fas fa-recycle', },
-	not:             { in: ['boolean'],                    out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
-	or:              { in: ['boolean', 'boolean'],         out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
-	and:             { in: ['boolean', 'boolean'],         out: 'boolean',     category: 'logical',    icon: 'fas fa-flag', },
-	add:             { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-plus', },
-	subtract:        { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-minus', },
-	multiply:        { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-times', },
-	divide:          { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-divide', },
-	mod:             { in: ['number', 'number'],           out: 'number',      category: 'operation',  icon: 'fas fa-divide', },
-	round:           { in: ['number'],                     out: 'number',      category: 'operation',  icon: 'fas fa-calculator', },
-	eq:              { in: [0, 0],                         out: 'boolean',     category: 'comparison', icon: 'fas fa-equals', },
-	notEq:           { in: [0, 0],                         out: 'boolean',     category: 'comparison', icon: 'fas fa-not-equal', },
-	gt:              { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-greater-than', },
-	lt:              { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-less-than', },
-	gtEq:            { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-greater-than-equal', },
-	ltEq:            { in: ['number', 'number'],           out: 'boolean',     category: 'comparison', icon: 'fas fa-less-than-equal', },
-	strLen:          { in: ['string'],                     out: 'number',      category: 'text',       icon: 'fas fa-quote-right', },
-	strPick:         { in: ['string', 'number'],           out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
-	strReplace:      { in: ['string', 'string', 'string'], out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
-	strReverse:      { in: ['string'],                     out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
-	join:            { in: ['stringArray', 'string'],      out: 'string',      category: 'text',       icon: 'fas fa-quote-right', },
-	stringToNumber:  { in: ['string'],                     out: 'number',      category: 'convert',    icon: 'fas fa-exchange-alt', },
-	numberToString:  { in: ['number'],                     out: 'string',      category: 'convert',    icon: 'fas fa-exchange-alt', },
-	splitStrByLine:  { in: ['string'],                     out: 'stringArray', category: 'convert',    icon: 'fas fa-exchange-alt', },
-	pick:            { in: [null, 'number'],               out: null,          category: 'list',       icon: 'fas fa-indent', },
-	listLen:         { in: [null],                         out: 'number',      category: 'list',       icon: 'fas fa-indent', },
-	rannum:          { in: ['number', 'number'],           out: 'number',      category: 'random',     icon: 'fas fa-dice', },
-	dailyRannum:     { in: ['number', 'number'],           out: 'number',      category: 'random',     icon: 'fas fa-dice', },
-	seedRannum:      { in: [null, 'number', 'number'],     out: 'number',      category: 'random',     icon: 'fas fa-dice', },
-	random:          { in: ['number'],                     out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
-	dailyRandom:     { in: ['number'],                     out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
-	seedRandom:      { in: [null, 'number'],               out: 'boolean',     category: 'random',     icon: 'fas fa-dice', },
-	randomPick:      { in: [0],                            out: 0,             category: 'random',     icon: 'fas fa-dice', },
-	dailyRandomPick: { in: [0],                            out: 0,             category: 'random',     icon: 'fas fa-dice', },
-	seedRandomPick:  { in: [null, 0],                      out: 0,             category: 'random',     icon: 'fas fa-dice', },
-	DRPWPM:      { in: ['stringArray'],                out: 'string',      category: 'random',     icon: 'fas fa-dice', }, // dailyRandomPickWithProbabilityMapping
-};
-
-export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) {
-
-	const date = new Date();
-	const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
-
-	const funcs: Record<string, Function> = {
-		not: (a: boolean) => !a,
-		or: (a: boolean, b: boolean) => a || b,
-		and: (a: boolean, b: boolean) => a && b,
-		eq: (a: any, b: any) => a === b,
-		notEq: (a: any, b: any) => a !== b,
-		gt: (a: number, b: number) => a > b,
-		lt: (a: number, b: number) => a < b,
-		gtEq: (a: number, b: number) => a >= b,
-		ltEq: (a: number, b: number) => a <= b,
-		if: (bool: boolean, a: any, b: any) => bool ? a : b,
-		for: (times: number, fn: Fn) => {
-			const result: any[] = [];
-			for (let i = 0; i < times; i++) {
-				result.push(fn.exec({
-					[fn.slots[0]]: i + 1
-				}));
-			}
-			return result;
-		},
-		add: (a: number, b: number) => a + b,
-		subtract: (a: number, b: number) => a - b,
-		multiply: (a: number, b: number) => a * b,
-		divide: (a: number, b: number) => a / b,
-		mod: (a: number, b: number) => a % b,
-		round: (a: number) => Math.round(a),
-		strLen: (a: string) => a.length,
-		strPick: (a: string, b: number) => a[b - 1],
-		strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
-		strReverse: (a: string) => a.split('').reverse().join(''),
-		join: (texts: string[], separator: string) => texts.join(separator || ''),
-		stringToNumber: (a: string) => parseInt(a),
-		numberToString: (a: number) => a.toString(),
-		splitStrByLine: (a: string) => a.split('\n'),
-		pick: (list: any[], i: number) => list[i - 1],
-		listLen: (list: any[]) => list.length,
-		random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability,
-		rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)),
-		randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)],
-		dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability,
-		dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)),
-		dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)],
-		seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability,
-		seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)),
-		seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)],
-		DRPWPM: (list: string[]) => {
-			const xs: any[] = [];
-			let totalFactor = 0;
-			for (const x of list) {
-				const parts = x.split(' ');
-				const factor = parseInt(parts.pop()!, 10);
-				const text = parts.join(' ');
-				totalFactor += factor;
-				xs.push({ factor, text });
-			}
-			const r = seedrandom(`${day}:${expr.id}`)() * totalFactor;
-			let stackedFactor = 0;
-			for (const x of xs) {
-				if (r >= stackedFactor && r <= stackedFactor + x.factor) {
-					return x.text;
-				} else {
-					stackedFactor += x.factor;
-				}
-			}
-			return xs[0].text;
-		},
-	};
-
-	return funcs;
-}
diff --git a/src/client/scripts/hpml/type-checker.ts b/src/client/scripts/hpml/type-checker.ts
deleted file mode 100644
index 9633b3cd01..0000000000
--- a/src/client/scripts/hpml/type-checker.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import autobind from 'autobind-decorator';
-import { Type, envVarsDef, PageVar } from '.';
-import { Expr, isLiteralValue, Variable } from './expr';
-import { funcDefs } from './lib';
-
-type TypeError = {
-	arg: number;
-	expect: Type;
-	actual: Type;
-};
-
-/**
- * Hpml type checker
- */
-export class HpmlTypeChecker {
-	public variables: Variable[];
-	public pageVars: PageVar[];
-
-	constructor(variables: HpmlTypeChecker['variables'] = [], pageVars: HpmlTypeChecker['pageVars'] = []) {
-		this.variables = variables;
-		this.pageVars = pageVars;
-	}
-
-	@autobind
-	public typeCheck(v: Expr): TypeError | null {
-		if (isLiteralValue(v)) return null;
-
-		const def = funcDefs[v.type || ''];
-		if (def == null) {
-			throw new Error('Unknown type: ' + v.type);
-		}
-
-		const generic: Type[] = [];
-
-		for (let i = 0; i < def.in.length; i++) {
-			const arg = def.in[i];
-			const type = this.infer(v.args[i]);
-			if (type === null) continue;
-
-			if (typeof arg === 'number') {
-				if (generic[arg] === undefined) {
-					generic[arg] = type;
-				} else if (type !== generic[arg]) {
-					return {
-						arg: i,
-						expect: generic[arg],
-						actual: type
-					};
-				}
-			} else if (type !== arg) {
-				return {
-					arg: i,
-					expect: arg,
-					actual: type
-				};
-			}
-		}
-
-		return null;
-	}
-
-	@autobind
-	public getExpectedType(v: Expr, slot: number): Type {
-		const def = funcDefs[v.type || ''];
-		if (def == null) {
-			throw new Error('Unknown type: ' + v.type);
-		}
-
-		const generic: Type[] = [];
-
-		for (let i = 0; i < def.in.length; i++) {
-			const arg = def.in[i];
-			const type = this.infer(v.args[i]);
-			if (type === null) continue;
-
-			if (typeof arg === 'number') {
-				if (generic[arg] === undefined) {
-					generic[arg] = type;
-				}
-			}
-		}
-
-		if (typeof def.in[slot] === 'number') {
-			return generic[def.in[slot]] || null;
-		} else {
-			return def.in[slot];
-		}
-	}
-
-	@autobind
-	public infer(v: Expr): Type {
-		if (v.type === null) return null;
-		if (v.type === 'text') return 'string';
-		if (v.type === 'multiLineText') return 'string';
-		if (v.type === 'textList') return 'stringArray';
-		if (v.type === 'number') return 'number';
-		if (v.type === 'ref') {
-			const variable = this.variables.find(va => va.name === v.value);
-			if (variable) {
-				return this.infer(variable);
-			}
-
-			const pageVar = this.pageVars.find(va => va.name === v.value);
-			if (pageVar) {
-				return pageVar.type;
-			}
-
-			const envVar = envVarsDef[v.value || ''];
-			if (envVar !== undefined) {
-				return envVar;
-			}
-
-			return null;
-		}
-		if (v.type === 'aiScriptVar') return null;
-		if (v.type === 'fn') return null; // todo
-		if (v.type.startsWith('fn:')) return null; // todo
-
-		const generic: Type[] = [];
-
-		const def = funcDefs[v.type];
-
-		for (let i = 0; i < def.in.length; i++) {
-			const arg = def.in[i];
-			if (typeof arg === 'number') {
-				const type = this.infer(v.args[i]);
-
-				if (generic[arg] === undefined) {
-					generic[arg] = type;
-				} else {
-					if (type !== generic[arg]) {
-						generic[arg] = null;
-					}
-				}
-			}
-		}
-
-		if (typeof def.out === 'number') {
-			return generic[def.out];
-		} else {
-			return def.out;
-		}
-	}
-
-	@autobind
-	public getVarByName(name: string): Variable {
-		const v = this.variables.find(x => x.name === name);
-		if (v !== undefined) {
-			return v;
-		} else {
-			throw new Error(`No such variable '${name}'`);
-		}
-	}
-
-	@autobind
-	public getVarsByType(type: Type): Variable[] {
-		if (type == null) return this.variables;
-		return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type));
-	}
-
-	@autobind
-	public getEnvVarsByType(type: Type): string[] {
-		if (type == null) return Object.keys(envVarsDef);
-		return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k);
-	}
-
-	@autobind
-	public getPageVarsByType(type: Type): string[] {
-		if (type == null) return this.pageVars.map(v => v.name);
-		return this.pageVars.filter(v => type === v.type).map(v => v.name);
-	}
-
-	@autobind
-	public isUsedName(name: string) {
-		if (this.variables.some(v => v.name === name)) {
-			return true;
-		}
-
-		if (this.pageVars.some(v => v.name === name)) {
-			return true;
-		}
-
-		if (envVarsDef[name]) {
-			return true;
-		}
-
-		return false;
-	}
-}
diff --git a/src/client/scripts/idb-proxy.ts b/src/client/scripts/idb-proxy.ts
deleted file mode 100644
index 5f76ae30bb..0000000000
--- a/src/client/scripts/idb-proxy.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、
-// indexedDBが使えない環境ではlocalStorageを使う
-import {
-	get as iget,
-	set as iset,
-	del as idel,
-} from 'idb-keyval';
-
-const fallbackName = (key: string) => `idbfallback::${key}`;
-
-let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true;
-
-if (idbAvailable) {
-	try {
-		await iset('idb-test', 'test');
-	} catch (e) {
-		console.error('idb error', e);
-		idbAvailable = false;
-	}
-}
-
-if (!idbAvailable) console.error('indexedDB is unavailable. It will use localStorage.');
-
-export async function get(key: string) {
-	if (idbAvailable) return iget(key);
-	return JSON.parse(localStorage.getItem(fallbackName(key)));
-}
-
-export async function set(key: string, val: any) {
-	if (idbAvailable) return iset(key, val);
-	return localStorage.setItem(fallbackName(key), JSON.stringify(val));
-}
-
-export async function del(key: string) {
-	if (idbAvailable) return idel(key);
-	return localStorage.removeItem(fallbackName(key));
-}
diff --git a/src/client/scripts/initialize-sw.ts b/src/client/scripts/initialize-sw.ts
deleted file mode 100644
index 6f1874572a..0000000000
--- a/src/client/scripts/initialize-sw.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { instance } from '@client/instance';
-import { $i } from '@client/account';
-import { api } from '@client/os';
-import { lang } from '@client/config';
-
-export async function initializeSw() {
-	if (instance.swPublickey &&
-		('serviceWorker' in navigator) &&
-		('PushManager' in window) &&
-		$i && $i.token) {
-		navigator.serviceWorker.register(`/sw.js`);
-
-		navigator.serviceWorker.ready.then(registration => {
-			registration.active?.postMessage({
-				msg: 'initialize',
-				lang,
-			});
-			// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
-			registration.pushManager.subscribe({
-				userVisibleOnly: true,
-				applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
-			}).then(subscription => {
-				function encode(buffer: ArrayBuffer | null) {
-					return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
-				}
-
-				// Register
-				api('sw/register', {
-					endpoint: subscription.endpoint,
-					auth: encode(subscription.getKey('auth')),
-					publickey: encode(subscription.getKey('p256dh'))
-				});
-			})
-			// When subscribe failed
-			.catch(async (err: Error) => {
-				// 通知が許可されていなかったとき
-				if (err.name === 'NotAllowedError') {
-					return;
-				}
-
-				// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
-				// 既に存在していることが原因でエラーになった可能性があるので、
-				// そのサブスクリプションを解除しておく
-				const subscription = await registration.pushManager.getSubscription();
-				if (subscription) subscription.unsubscribe();
-			});
-		});
-	}
-}
-
-/**
- * Convert the URL safe base64 string to a Uint8Array
- * @param base64String base64 string
- */
-function urlBase64ToUint8Array(base64String: string): Uint8Array {
-	const padding = '='.repeat((4 - base64String.length % 4) % 4);
-	const base64 = (base64String + padding)
-		.replace(/-/g, '+')
-		.replace(/_/g, '/');
-
-	const rawData = window.atob(base64);
-	const outputArray = new Uint8Array(rawData.length);
-
-	for (let i = 0; i < rawData.length; ++i) {
-		outputArray[i] = rawData.charCodeAt(i);
-	}
-	return outputArray;
-}
diff --git a/src/client/scripts/is-device-darkmode.ts b/src/client/scripts/is-device-darkmode.ts
deleted file mode 100644
index 854f38e517..0000000000
--- a/src/client/scripts/is-device-darkmode.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function isDeviceDarkmode() {
-	return window.matchMedia('(prefers-color-scheme: dark)').matches;
-}
diff --git a/src/client/scripts/is-device-touch.ts b/src/client/scripts/is-device-touch.ts
deleted file mode 100644
index 3f0bfefed2..0000000000
--- a/src/client/scripts/is-device-touch.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const isDeviceTouch = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
diff --git a/src/client/scripts/is-mobile.ts b/src/client/scripts/is-mobile.ts
deleted file mode 100644
index 60cb59f91e..0000000000
--- a/src/client/scripts/is-mobile.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-const ua = navigator.userAgent.toLowerCase();
-export const isMobile = /mobile|iphone|ipad|android/.test(ua);
diff --git a/src/client/scripts/keycode.ts b/src/client/scripts/keycode.ts
deleted file mode 100644
index c127d54bb2..0000000000
--- a/src/client/scripts/keycode.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export default (input: string): string[] => {
-	if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) {
-		const codes = aliases[input];
-		return Array.isArray(codes) ? codes : [codes];
-	} else {
-		return [input];
-	}
-};
-
-export const aliases = {
-	'esc': 'Escape',
-	'enter': ['Enter', 'NumpadEnter'],
-	'up': 'ArrowUp',
-	'down': 'ArrowDown',
-	'left': 'ArrowLeft',
-	'right': 'ArrowRight',
-	'plus': ['NumpadAdd', 'Semicolon'],
-};
-
-/*!
-* Programatically add the following
-*/
-
-// lower case chars
-for (let i = 97; i < 123; i++) {
-	const char = String.fromCharCode(i);
-	aliases[char] = `Key${char.toUpperCase()}`;
-}
-
-// numbers
-for (let i = 0; i < 10; i++) {
-	aliases[i] = [`Numpad${i}`, `Digit${i}`];
-}
diff --git a/src/client/scripts/loading.ts b/src/client/scripts/loading.ts
deleted file mode 100644
index 4b0a560e34..0000000000
--- a/src/client/scripts/loading.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export default {
-	start: () => {
-		// TODO
-	},
-	done: () => {
-		// TODO
-	},
-	set: val => {
-		// TODO
-	}
-};
diff --git a/src/client/scripts/login-id.ts b/src/client/scripts/login-id.ts
deleted file mode 100644
index 0f9c6be4a9..0000000000
--- a/src/client/scripts/login-id.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export function getUrlWithLoginId(url: string, loginId: string) {
-	const u = new URL(url, origin);
-	u.searchParams.append('loginId', loginId);
-	return u.toString();
-}
-
-export function getUrlWithoutLoginId(url: string) {
-	const u = new URL(url);
-	u.searchParams.delete('loginId');
-	return u.toString();
-}
diff --git a/src/client/scripts/lookup-user.ts b/src/client/scripts/lookup-user.ts
deleted file mode 100644
index c393472ae8..0000000000
--- a/src/client/scripts/lookup-user.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { parseAcct } from '@/misc/acct';
-import { i18n } from '@client/i18n';
-import * as os from '@client/os';
-
-export async function lookupUser() {
-	const { canceled, result } = await os.dialog({
-		title: i18n.locale.usernameOrUserId,
-		input: true
-	});
-	if (canceled) return;
-
-	const show = (user) => {
-		os.pageWindow(`/user-info/${user.id}`);
-	};
-
-	const usernamePromise = os.api('users/show', parseAcct(result));
-	const idPromise = os.api('users/show', { userId: result });
-	let _notFound = false;
-	const notFound = () => {
-		if (_notFound) {
-			os.dialog({
-				type: 'error',
-				text: i18n.locale.noSuchUser
-			});
-		} else {
-			_notFound = true;
-		}
-	};
-	usernamePromise.then(show).catch(e => {
-		if (e.code === 'NO_SUCH_USER') {
-			notFound();
-		}
-	});
-	idPromise.then(show).catch(e => {
-		notFound();
-	});
-}
diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts
deleted file mode 100644
index 1da518efa1..0000000000
--- a/src/client/scripts/paging.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-import { markRaw } from 'vue';
-import * as os from '@client/os';
-import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll';
-
-const SECOND_FETCH_LIMIT = 30;
-
-// reversed: items 配列の中身を逆順にする(新しい方が最後)
-
-export default (opts) => ({
-	emits: ['queue'],
-
-	data() {
-		return {
-			items: [],
-			queue: [],
-			offset: 0,
-			fetching: true,
-			moreFetching: false,
-			inited: false,
-			more: false,
-			backed: false, // 遡り中か否か
-			isBackTop: false,
-		};
-	},
-
-	computed: {
-		empty(): boolean {
-			return this.items.length === 0 && !this.fetching && this.inited;
-		},
-
-		error(): boolean {
-			return !this.fetching && !this.inited;
-		},
-	},
-
-	watch: {
-		pagination: {
-			handler() {
-				this.init();
-			},
-			deep: true
-		},
-
-		queue: {
-			handler(a, b) {
-				if (a.length === 0 && b.length === 0) return;
-				this.$emit('queue', this.queue.length);
-			},
-			deep: true
-		}
-	},
-
-	created() {
-		opts.displayLimit = opts.displayLimit || 30;
-		this.init();
-	},
-
-	activated() {
-		this.isBackTop = false;
-	},
-
-	deactivated() {
-		this.isBackTop = window.scrollY === 0;
-	},
-
-	methods: {
-		reload() {
-			this.items = [];
-			this.init();
-		},
-
-		replaceItem(finder, data) {
-			const i = this.items.findIndex(finder);
-			this.items[i] = data;
-		},
-
-		removeItem(finder) {
-			const i = this.items.findIndex(finder);
-			this.items.splice(i, 1);
-		},
-
-		async init() {
-			this.queue = [];
-			this.fetching = true;
-			if (opts.before) opts.before(this);
-			let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params;
-			if (params && params.then) params = await params;
-			if (params === null) return;
-			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
-			await os.api(endpoint, {
-				...params,
-				limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
-			}).then(items => {
-				for (let i = 0; i < items.length; i++) {
-					const item = items[i];
-					markRaw(item);
-					if (this.pagination.reversed) {
-						if (i === items.length - 2) item._shouldInsertAd_ = true;
-					} else {
-						if (i === 3) item._shouldInsertAd_ = true;
-					}
-				}
-				if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
-					items.pop();
-					this.items = this.pagination.reversed ? [...items].reverse() : items;
-					this.more = true;
-				} else {
-					this.items = this.pagination.reversed ? [...items].reverse() : items;
-					this.more = false;
-				}
-				this.offset = items.length;
-				this.inited = true;
-				this.fetching = false;
-				if (opts.after) opts.after(this, null);
-			}, e => {
-				this.fetching = false;
-				if (opts.after) opts.after(this, e);
-			});
-		},
-
-		async fetchMore() {
-			if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
-			this.moreFetching = true;
-			this.backed = true;
-			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
-			if (params && params.then) params = await params;
-			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
-			await os.api(endpoint, {
-				...params,
-				limit: SECOND_FETCH_LIMIT + 1,
-				...(this.pagination.offsetMode ? {
-					offset: this.offset,
-				} : {
-					untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
-				}),
-			}).then(items => {
-				for (let i = 0; i < items.length; i++) {
-					const item = items[i];
-					markRaw(item);
-					if (this.pagination.reversed) {
-						if (i === items.length - 9) item._shouldInsertAd_ = true;
-					} else {
-						if (i === 10) item._shouldInsertAd_ = true;
-					}
-				}
-				if (items.length > SECOND_FETCH_LIMIT) {
-					items.pop();
-					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
-					this.more = true;
-				} else {
-					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
-					this.more = false;
-				}
-				this.offset += items.length;
-				this.moreFetching = false;
-			}, e => {
-				this.moreFetching = false;
-			});
-		},
-
-		async fetchMoreFeature() {
-			if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
-			this.moreFetching = true;
-			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
-			if (params && params.then) params = await params;
-			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
-			await os.api(endpoint, {
-				...params,
-				limit: SECOND_FETCH_LIMIT + 1,
-				...(this.pagination.offsetMode ? {
-					offset: this.offset,
-				} : {
-					sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
-				}),
-			}).then(items => {
-				for (const item of items) {
-					markRaw(item);
-				}
-				if (items.length > SECOND_FETCH_LIMIT) {
-					items.pop();
-					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
-					this.more = true;
-				} else {
-					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
-					this.more = false;
-				}
-				this.offset += items.length;
-				this.moreFetching = false;
-			}, e => {
-				this.moreFetching = false;
-			});
-		},
-
-		prepend(item) {
-			if (this.pagination.reversed) {
-				const container = getScrollContainer(this.$el);
-				const pos = getScrollPosition(this.$el);
-				const viewHeight = container.clientHeight;
-				const height = container.scrollHeight;
-				const isBottom = (pos + viewHeight > height - 32);
-				if (isBottom) {
-					// オーバーフローしたら古いアイテムは捨てる
-					if (this.items.length >= opts.displayLimit) {
-						// このやり方だとVue 3.2以降アニメーションが動かなくなる
-						//this.items = this.items.slice(-opts.displayLimit);
-						while (this.items.length >= opts.displayLimit) {
-							this.items.shift();
-						}
-						this.more = true;
-					}
-				}
-				this.items.push(item);
-				// TODO
-			} else {
-				const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el));
-
-				if (isTop) {
-					// Prepend the item
-					this.items.unshift(item);
-
-					// オーバーフローしたら古いアイテムは捨てる
-					if (this.items.length >= opts.displayLimit) {
-						// このやり方だとVue 3.2以降アニメーションが動かなくなる
-						//this.items = this.items.slice(0, opts.displayLimit);
-						while (this.items.length >= opts.displayLimit) {
-							this.items.pop();
-						}
-						this.more = true;
-					}
-				} else {
-					this.queue.push(item);
-					onScrollTop(this.$el, () => {
-						for (const item of this.queue) {
-							this.prepend(item);
-						}
-						this.queue = [];
-					});
-				}
-			}
-		},
-
-		append(item) {
-			this.items.push(item);
-		},
-	}
-});
diff --git a/src/client/scripts/physics.ts b/src/client/scripts/physics.ts
deleted file mode 100644
index 445b6296eb..0000000000
--- a/src/client/scripts/physics.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import * as Matter from 'matter-js';
-
-export function physics(container: HTMLElement) {
-	const containerWidth = container.offsetWidth;
-	const containerHeight = container.offsetHeight;
-	const containerCenterX = containerWidth / 2;
-
-	// サイズ固定化(要らないかも？)
-	container.style.position = 'relative';
-	container.style.boxSizing = 'border-box';
-	container.style.width = `${containerWidth}px`;
-	container.style.height = `${containerHeight}px`;
-
-	// create engine
-	const engine = Matter.Engine.create({
-		constraintIterations: 4,
-		positionIterations: 8,
-		velocityIterations: 8,
-	});
-
-	const world = engine.world;
-
-	// create renderer
-	const render = Matter.Render.create({
-		engine: engine,
-		//element: document.getElementById('debug'),
-		options: {
-			width: containerWidth,
-			height: containerHeight,
-			background: 'transparent', // transparent to hide
-			wireframeBackground: 'transparent', // transparent to hide
-		}
-	});
-
-	// Disable to hide debug
-	Matter.Render.run(render);
-
-	// create runner
-	const runner = Matter.Runner.create();
-	Matter.Runner.run(runner, engine);
-
-	const groundThickness = 1024;
-	const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, {
-		isStatic:     true,
-		restitution:  0.1,
-		friction:     2
-	});
-
-	//const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts);
-	//const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts);
-
-	Matter.World.add(world, [
-		ground,
-		//wallRight,
-		//wallLeft,
-	]);
-
-	const objEls = Array.from(container.children);
-	const objs = [];
-	for (const objEl of objEls) {
-		const left = objEl.dataset.physicsX ? parseInt(objEl.dataset.physicsX) : objEl.offsetLeft;
-		const top = objEl.dataset.physicsY ? parseInt(objEl.dataset.physicsY) : objEl.offsetTop;
-
-		let obj;
-		if (objEl.classList.contains('_physics_circle_')) {
-			obj = Matter.Bodies.circle(
-				left + (objEl.offsetWidth / 2),
-				top + (objEl.offsetHeight / 2),
-				Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2,
-				{
-					restitution: 0.5
-				}
-			);
-		} else {
-			const style = window.getComputedStyle(objEl);
-			obj = Matter.Bodies.rectangle(
-				left + (objEl.offsetWidth / 2),
-				top + (objEl.offsetHeight / 2),
-				objEl.offsetWidth,
-				objEl.offsetHeight,
-				{
-					chamfer: { radius: parseInt(style.borderRadius || '0', 10) },
-					restitution: 0.5
-				}
-			);
-		}
-		objEl.id = obj.id;
-		objs.push(obj);
-	}
-
-	Matter.World.add(engine.world, objs);
-
-	// Add mouse control
-
-	const mouse = Matter.Mouse.create(container);
-	const mouseConstraint = Matter.MouseConstraint.create(engine, {
-		mouse: mouse,
-		constraint: {
-			stiffness: 0.1,
-			render: {
-				visible: false
-			}
-		}
-	});
-
-	Matter.World.add(engine.world, mouseConstraint);
-
-	// keep the mouse in sync with rendering
-	render.mouse = mouse;
-
-	for (const objEl of objEls) {
-		objEl.style.position = `absolute`;
-		objEl.style.top = 0;
-		objEl.style.left = 0;
-		objEl.style.margin = 0;
-	}
-
-	window.requestAnimationFrame(update);
-
-	let stop = false;
-
-	function update() {
-		for (const objEl of objEls) {
-			const obj = objs.find(obj => obj.id.toString() === objEl.id.toString());
-			if (obj == null) continue;
-
-			const x = (obj.position.x - objEl.offsetWidth / 2);
-			const y = (obj.position.y - objEl.offsetHeight / 2);
-			const angle = obj.angle;
-			objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`;
-		}
-
-		if (!stop) {
-			window.requestAnimationFrame(update);
-		}
-	}
-
-	// 奈落に落ちたオブジェクトは消す
-	const intervalId = setInterval(() => {
-		for (const obj of objs) {
-			if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj);
-		}
-	}, 1000 * 10);
-
-	return {
-		stop: () => {
-			stop = true;
-			Matter.Runner.stop(runner);
-			clearInterval(intervalId);
-		}
-	};
-}
diff --git a/src/client/scripts/please-login.ts b/src/client/scripts/please-login.ts
deleted file mode 100644
index a584e9fa96..0000000000
--- a/src/client/scripts/please-login.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { $i } from '@client/account';
-import { i18n } from '@client/i18n';
-import { dialog } from '@client/os';
-
-export function pleaseLogin() {
-	if ($i) return;
-
-	dialog({
-		title: i18n.locale.signinRequired,
-		text: null
-	});
-
-	throw new Error('signin required');
-}
diff --git a/src/client/scripts/popout.ts b/src/client/scripts/popout.ts
deleted file mode 100644
index 6d92af4a05..0000000000
--- a/src/client/scripts/popout.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as config from '@client/config';
-
-export function popout(path: string, w?: HTMLElement) {
-	let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
-	url += '?zen'; // TODO: ちゃんとURLパースしてクエリ付ける
-	if (w) {
-		const position = w.getBoundingClientRect();
-		const width = parseInt(getComputedStyle(w, '').width, 10);
-		const height = parseInt(getComputedStyle(w, '').height, 10);
-		const x = window.screenX + position.left;
-		const y = window.screenY + position.top;
-		window.open(url, url,
-			`width=${width}, height=${height}, top=${y}, left=${x}`);
-	} else {
-		const width = 400;
-		const height = 500;
-		const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2);
-		const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2);
-		window.open(url, url,
-			`width=${width}, height=${height}, top=${x}, left=${y}`);
-	}
-}
diff --git a/src/client/scripts/reaction-picker.ts b/src/client/scripts/reaction-picker.ts
deleted file mode 100644
index 38699c0979..0000000000
--- a/src/client/scripts/reaction-picker.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Ref, ref } from 'vue';
-import { popup } from '@client/os';
-
-class ReactionPicker {
-	private src: Ref<HTMLElement | null> = ref(null);
-	private manualShowing = ref(false);
-	private onChosen?: Function;
-	private onClosed?: Function;
-
-	constructor() {
-		// nop
-	}
-
-	public async init() {
-		await popup(import('@client/components/emoji-picker-dialog.vue'), {
-			src: this.src,
-			asReactionPicker: true,
-			manualShowing: this.manualShowing
-		}, {
-			done: reaction => {
-				this.onChosen!(reaction);
-			},
-			close: () => {
-				this.manualShowing.value = false;
-			},
-			closed: () => {
-				this.src.value = null;
-				this.onClosed!();
-			}
-		});
-	}
-
-	public show(src: HTMLElement, onChosen: Function, onClosed: Function) {
-		this.src.value = src;
-		this.manualShowing.value = true;
-		this.onChosen = onChosen;
-		this.onClosed = onClosed;
-	}
-}
-
-export const reactionPicker = new ReactionPicker();
diff --git a/src/client/scripts/room/furniture.ts b/src/client/scripts/room/furniture.ts
deleted file mode 100644
index 7734e32668..0000000000
--- a/src/client/scripts/room/furniture.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export type RoomInfo = {
-	roomType: string;
-	carpetColor: string;
-	furnitures: Furniture[];
-};
-
-export type Furniture = {
-	id: string; // 同じ家具が複数ある場合にそれぞれを識別するためのIDであり、家具IDではない
-	type: string; // こっちが家具ID(chairとか)
-	position: {
-		x: number;
-		y: number;
-		z: number;
-	};
-	rotation: {
-		x: number;
-		y: number;
-		z: number;
-	};
-	props?: Record<string, any>;
-};
diff --git a/src/client/scripts/room/furnitures.json5 b/src/client/scripts/room/furnitures.json5
deleted file mode 100644
index 4a40994107..0000000000
--- a/src/client/scripts/room/furnitures.json5
+++ /dev/null
@@ -1,407 +0,0 @@
-// 家具メタデータ
-
-// 家具IDはglbファイル及びそのディレクトリ名と一致する必要があります
-
-// 家具にはユーザーが設定できるプロパティを設定可能です:
-//
-// props: {
-//   <propname>: <proptype>
-// }
-//
-// proptype一覧:
-// * image ... 画像選択ダイアログを出し、その画像のURLが格納されます
-// * color ... 色選択コントロールを出し、選択された色が格納されます
-
-// 家具にカスタムテクスチャを適用できるようにするには、textureプロパティに以下の追加の情報を含めます:
-// 便宜上そのUVのどの部分にカスタムテクスチャを貼り合わせるかのエリアをテクスチャエリアと呼びます。
-// UVは1024*1024だと仮定します。
-//
-// <key>: {
-//   prop: <プロパティ名>,
-//   uv: {
-//     x: <テクスチャエリアX座標>,
-//     y: <テクスチャエリアY座標>,
-//     width: <テクスチャエリアの幅>,
-//     height: <テクスチャエリアの高さ>,
-//   },
-// }
-//
-// <key>には、カスタムテクスチャを適用したいメッシュ名を指定します
-// <プロパティ名>には、カスタムテクスチャとして使用する画像を格納するプロパティ(前述)名を指定します
-
-// 家具にカスタムカラーを適用できるようにするには、colorプロパティに以下の追加の情報を含めます:
-//
-// <key>: <プロパティ名>
-//
-// <key>には、カスタムカラーを適用したいマテリアル名を指定します
-// <プロパティ名>には、カスタムカラーとして使用する色を格納するプロパティ(前述)名を指定します
-
-[
-	{
-		id: "milk",
-		place: "floor"
-	},
-	{
-		id: "bed",
-		place: "floor"
-	},
-	{
-		id: "low-table",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Table: 'color'
-		}
-	},
-	{
-		id: "desk",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Board: 'color'
-		}
-	},
-	{
-		id: "chair",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Chair: 'color'
-		}
-	},
-	{
-		id: "chair2",
-		place: "floor",
-		props: {
-			color1: 'color',
-			color2: 'color'
-		},
-		color: {
-			Cushion: 'color1',
-			Leg: 'color2'
-		}
-	},
-	{
-		id: "fan",
-		place: "wall"
-	},
-	{
-		id: "pc",
-		place: "floor"
-	},
-	{
-		id: "plant",
-		place: "floor"
-	},
-	{
-		id: "plant2",
-		place: "floor"
-	},
-	{
-		id: "eraser",
-		place: "floor"
-	},
-	{
-		id: "pencil",
-		place: "floor"
-	},
-	{
-		id: "pudding",
-		place: "floor"
-	},
-	{
-		id: "cardboard-box",
-		place: "floor"
-	},
-	{
-		id: "cardboard-box2",
-		place: "floor"
-	},
-	{
-		id: "cardboard-box3",
-		place: "floor"
-	},
-	{
-		id: "book",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Cover: 'color'
-		}
-	},
-	{
-		id: "book2",
-		place: "floor"
-	},
-	{
-		id: "piano",
-		place: "floor"
-	},
-	{
-		id: "facial-tissue",
-		place: "floor"
-	},
-	{
-		id: "server",
-		place: "floor"
-	},
-	{
-		id: "moon",
-		place: "floor"
-	},
-	{
-		id: "corkboard",
-		place: "wall"
-	},
-	{
-		id: "mousepad",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Pad: 'color'
-		}
-	},
-	{
-		id: "monitor",
-		place: "floor",
-		props: {
-			screen: 'image'
-		},
-		texture: {
-			Screen: {
-				prop: 'screen',
-				uv: {
-					x: 0,
-					y: 434,
-					width: 1024,
-					height: 588,
-				},
-			},
-		},
-	},
-	{
-		id: "tv",
-		place: "floor",
-		props: {
-			screen: 'image'
-		},
-		texture: {
-			Screen: {
-				prop: 'screen',
-				uv: {
-					x: 0,
-					y: 434,
-					width: 1024,
-					height: 588,
-				},
-			},
-		},
-	},
-	{
-		id: "keyboard",
-		place: "floor"
-	},
-	{
-		id: "carpet-stripe",
-		place: "floor",
-		props: {
-			color1: 'color',
-			color2: 'color'
-		},
-		color: {
-			CarpetAreaA: 'color1',
-			CarpetAreaB: 'color2'
-		},
-	},
-	{
-		id: "mat",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Mat: 'color'
-		}
-	},
-	{
-		id: "color-box",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			main: 'color'
-		}
-	},
-	{
-		id: "wall-clock",
-		place: "wall"
-	},
-	{
-		id: "cube",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Cube: 'color'
-		}
-	},
-	{
-		id: "photoframe",
-		place: "wall",
-		props: {
-			photo: 'image',
-			color: 'color'
-		},
-		texture: {
-			Photo: {
-				prop: 'photo',
-				uv: {
-					x: 0,
-					y: 342,
-					width: 1024,
-					height: 683,
-				},
-			},
-		},
-		color: {
-			Frame: 'color'
-		}
-	},
-	{
-		id: "pinguin",
-		place: "floor",
-		props: {
-			body: 'color',
-			belly: 'color'
-		},
-		color: {
-			Body: 'body',
-			Belly: 'belly',
-		}
-	},
-	{
-		id: "rubik-cube",
-		place: "floor",
-	},
-	{
-		id: "poster-h",
-		place: "wall",
-		props: {
-			picture: 'image'
-		},
-		texture: {
-			Poster: {
-				prop: 'picture',
-				uv: {
-					x: 0,
-					y: 277,
-					width: 1024,
-					height: 745,
-				},
-			},
-		},
-	},
-	{
-		id: "poster-v",
-		place: "wall",
-		props: {
-			picture: 'image'
-		},
-		texture: {
-			Poster: {
-				prop: 'picture',
-				uv: {
-					x: 0,
-					y: 0,
-					width: 745,
-					height: 1024,
-				},
-			},
-		},
-	},
-	{
-		id: "sofa",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Sofa: 'color'
-		}
-	},
-	{
-		id: "spiral",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Step: 'color'
-		}
-	},
-	{
-		id: "bin",
-		place: "floor",
-		props: {
-			color: 'color'
-		},
-		color: {
-			Bin: 'color'
-		}
-	},
-	{
-		id: "cup-noodle",
-		place: "floor"
-	},
-	{
-		id: "holo-display",
-		place: "floor",
-		props: {
-			image: 'image'
-		},
-		texture: {
-			Image_Front: {
-				prop: 'image',
-				uv: {
-					x: 0,
-					y: 0,
-					width: 1024,
-					height: 1024,
-				},
-			},
-			Image_Back: {
-				prop: 'image',
-				uv: {
-					x: 0,
-					y: 0,
-					width: 1024,
-					height: 1024,
-				},
-			},
-		},
-	},
-	{
-		id: 'energy-drink',
-		place: "floor",
-	},
-	{
-		id: 'doll-ai',
-		place: "floor",
-	},
-	{
-		id: 'banknote',
-		place: "floor",
-	},
-]
diff --git a/src/client/scripts/room/room.ts b/src/client/scripts/room/room.ts
deleted file mode 100644
index 4450210c6c..0000000000
--- a/src/client/scripts/room/room.ts
+++ /dev/null
@@ -1,775 +0,0 @@
-import autobind from 'autobind-decorator';
-import { v4 as uuid } from 'uuid';
-import * as THREE from 'three';
-import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
-import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
-import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
-import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
-import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
-import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js';
-import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js';
-import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
-import { Furniture, RoomInfo } from './furniture';
-import { query as urlQuery } from '../../../prelude/url';
-const furnitureDefs = require('./furnitures.json5');
-
-THREE.ImageUtils.crossOrigin = '';
-
-type Options = {
-	graphicsQuality: Room['graphicsQuality'];
-	onChangeSelect: Room['onChangeSelect'];
-	useOrthographicCamera: boolean;
-};
-
-/**
- * MisskeyRoom Core Engine
- */
-export class Room {
-	private clock: THREE.Clock;
-	private scene: THREE.Scene;
-	private renderer: THREE.WebGLRenderer;
-	private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
-	private controls: OrbitControls;
-	private composer: EffectComposer;
-	private mixers: THREE.AnimationMixer[] = [];
-	private furnitureControl: TransformControls;
-	private roomInfo: RoomInfo;
-	private graphicsQuality: 'cheep' | 'low' | 'medium' | 'high' | 'ultra';
-	private roomObj: THREE.Object3D;
-	private objects: THREE.Object3D[] = [];
-	private selectedObject: THREE.Object3D = null;
-	private onChangeSelect: Function;
-	private isTransformMode = false;
-	private renderFrameRequestId: number;
-
-	private get canvas(): HTMLCanvasElement {
-		return this.renderer.domElement;
-	}
-
-	private get furnitures(): Furniture[] {
-		return this.roomInfo.furnitures;
-	}
-
-	private set furnitures(furnitures: Furniture[]) {
-		this.roomInfo.furnitures = furnitures;
-	}
-
-	private get enableShadow() {
-		return this.graphicsQuality != 'cheep';
-	}
-
-	private get usePostFXs() {
-		return this.graphicsQuality !== 'cheep' && this.graphicsQuality !== 'low';
-	}
-
-	private get shadowQuality() {
-		return (
-			this.graphicsQuality === 'ultra' ? 16384 :
-			this.graphicsQuality === 'high' ? 8192 :
-			this.graphicsQuality === 'medium' ? 4096 :
-			this.graphicsQuality === 'low' ? 1024 :
-			0); // cheep
-	}
-
-	constructor(user, isMyRoom, roomInfo: RoomInfo, container: Element, options: Options) {
-		this.roomInfo = roomInfo;
-		this.graphicsQuality = options.graphicsQuality;
-		this.onChangeSelect = options.onChangeSelect;
-
-		this.clock = new THREE.Clock(true);
-
-		//#region Init a scene
-		this.scene = new THREE.Scene();
-
-		const width = container.clientWidth;
-		const height = container.clientHeight;
-
-		//#region Init a renderer
-		this.renderer = new THREE.WebGLRenderer({
-			antialias: false,
-			stencil: false,
-			alpha: false,
-			powerPreference:
-				this.graphicsQuality === 'ultra' ? 'high-performance' :
-				this.graphicsQuality === 'high' ? 'high-performance' :
-				this.graphicsQuality === 'medium' ? 'default' :
-				this.graphicsQuality === 'low' ? 'low-power' :
-				'low-power' // cheep
-		});
-
-		this.renderer.setPixelRatio(window.devicePixelRatio);
-		this.renderer.setSize(width, height);
-		this.renderer.autoClear = false;
-		this.renderer.setClearColor(new THREE.Color(0x051f2d));
-		this.renderer.shadowMap.enabled = this.enableShadow;
-		this.renderer.shadowMap.type =
-			this.graphicsQuality === 'ultra' ? THREE.PCFSoftShadowMap :
-			this.graphicsQuality === 'high' ? THREE.PCFSoftShadowMap :
-			this.graphicsQuality === 'medium' ? THREE.PCFShadowMap :
-			this.graphicsQuality === 'low' ? THREE.BasicShadowMap :
-			THREE.BasicShadowMap; // cheep
-
-		container.insertBefore(this.canvas, container.firstChild);
-		//#endregion
-
-		//#region Init a camera
-		this.camera = options.useOrthographicCamera
-			? new THREE.OrthographicCamera(
-				width / - 2, width / 2, height / 2, height / - 2, -10, 10)
-			: new THREE.PerspectiveCamera(45, width / height);
-
-		if (options.useOrthographicCamera) {
-			this.camera.position.x = 2;
-			this.camera.position.y = 2;
-			this.camera.position.z = 2;
-			this.camera.zoom = 100;
-			this.camera.updateProjectionMatrix();
-		} else {
-			this.camera.position.x = 5;
-			this.camera.position.y = 2;
-			this.camera.position.z = 5;
-		}
-
-		this.scene.add(this.camera);
-		//#endregion
-
-		//#region AmbientLight
-		const ambientLight = new THREE.AmbientLight(0xffffff, 1);
-		this.scene.add(ambientLight);
-		//#endregion
-
-		if (this.graphicsQuality !== 'cheep') {
-			//#region Room light
-			const roomLight = new THREE.SpotLight(0xffffff, 0.1);
-
-			roomLight.position.set(0, 8, 0);
-			roomLight.castShadow = this.enableShadow;
-			roomLight.shadow.bias = -0.0001;
-			roomLight.shadow.mapSize.width = this.shadowQuality;
-			roomLight.shadow.mapSize.height = this.shadowQuality;
-			roomLight.shadow.camera.near = 0.1;
-			roomLight.shadow.camera.far = 9;
-			roomLight.shadow.camera.fov = 45;
-
-			this.scene.add(roomLight);
-			//#endregion
-		}
-
-		//#region Out light
-		const outLight1 = new THREE.SpotLight(0xffffff, 0.4);
-		outLight1.position.set(9, 3, -2);
-		outLight1.castShadow = this.enableShadow;
-		outLight1.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
-		outLight1.shadow.mapSize.width = this.shadowQuality;
-		outLight1.shadow.mapSize.height = this.shadowQuality;
-		outLight1.shadow.camera.near = 6;
-		outLight1.shadow.camera.far = 15;
-		outLight1.shadow.camera.fov = 45;
-		this.scene.add(outLight1);
-
-		const outLight2 = new THREE.SpotLight(0xffffff, 0.2);
-		outLight2.position.set(-2, 3, 9);
-		outLight2.castShadow = false;
-		outLight2.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
-		outLight2.shadow.camera.near = 6;
-		outLight2.shadow.camera.far = 15;
-		outLight2.shadow.camera.fov = 45;
-		this.scene.add(outLight2);
-		//#endregion
-
-		//#region Init a controller
-		this.controls = new OrbitControls(this.camera, this.canvas);
-
-		this.controls.target.set(0, 1, 0);
-		this.controls.enableZoom = true;
-		this.controls.enablePan = isMyRoom;
-		this.controls.minPolarAngle = 0;
-		this.controls.maxPolarAngle = Math.PI / 2;
-		this.controls.minAzimuthAngle = 0;
-		this.controls.maxAzimuthAngle = Math.PI / 2;
-		this.controls.enableDamping = true;
-		this.controls.dampingFactor = 0.2;
-		//#endregion
-
-		//#region POST FXs
-		if (!this.usePostFXs) {
-			this.composer = null;
-		} else {
-			const renderTarget = new THREE.WebGLRenderTarget(width, height, {
-				minFilter: THREE.LinearFilter,
-				magFilter: THREE.LinearFilter,
-				format: THREE.RGBFormat,
-				stencilBuffer: false,
-			});
-
-			const fxaa = new ShaderPass(FXAAShader);
-			fxaa.uniforms['resolution'].value = new THREE.Vector2(1 / width, 1 / height);
-			fxaa.renderToScreen = true;
-
-			this.composer = new EffectComposer(this.renderer, renderTarget);
-			this.composer.addPass(new RenderPass(this.scene, this.camera));
-			if (this.graphicsQuality === 'ultra') {
-				this.composer.addPass(new BloomPass(0.25, 30, 128.0, 512));
-			}
-			this.composer.addPass(fxaa);
-		}
-		//#endregion
-		//#endregion
-
-		//#region Label
-		//#region Avatar
-		const avatarUrl = `/proxy/?${urlQuery({ url: user.avatarUrl })}`;
-
-		const textureLoader = new THREE.TextureLoader();
-		textureLoader.crossOrigin = 'anonymous';
-
-		const iconTexture = textureLoader.load(avatarUrl);
-		iconTexture.wrapS = THREE.RepeatWrapping;
-		iconTexture.wrapT = THREE.RepeatWrapping;
-		iconTexture.anisotropy = 16;
-
-		const avatarMaterial = new THREE.MeshBasicMaterial({
-			map: iconTexture,
-			side: THREE.DoubleSide,
-			alphaTest: 0.5
-		});
-
-		const iconGeometry = new THREE.PlaneGeometry(1, 1);
-
-		const avatarObject = new THREE.Mesh(iconGeometry, avatarMaterial);
-		avatarObject.position.set(-3, 2.5, 2);
-		avatarObject.rotation.y = Math.PI / 2;
-		avatarObject.castShadow = false;
-
-		this.scene.add(avatarObject);
-		//#endregion
-
-		//#region Username
-		const name = user.username;
-
-		new THREE.FontLoader().load('/assets/fonts/helvetiker_regular.typeface.json', font => {
-			const nameGeometry = new THREE.TextGeometry(name, {
-				size: 0.5,
-				height: 0,
-				curveSegments: 8,
-				font: font,
-				bevelThickness: 0,
-				bevelSize: 0,
-				bevelEnabled: false
-			});
-
-			const nameMaterial = new THREE.MeshLambertMaterial({
-				color: 0xffffff
-			});
-
-			const nameObject = new THREE.Mesh(nameGeometry, nameMaterial);
-			nameObject.position.set(-3, 2.25, 1.25);
-			nameObject.rotation.y = Math.PI / 2;
-			nameObject.castShadow = false;
-
-			this.scene.add(nameObject);
-		});
-		//#endregion
-		//#endregion
-
-		//#region Interaction
-		if (isMyRoom) {
-			this.furnitureControl = new TransformControls(this.camera, this.canvas);
-			this.scene.add(this.furnitureControl);
-
-			// Hover highlight
-			this.canvas.onmousemove = this.onmousemove;
-
-			// Click
-			this.canvas.onmousedown = this.onmousedown;
-		}
-		//#endregion
-
-		//#region Init room
-		this.loadRoom();
-		//#endregion
-
-		//#region Load furnitures
-		for (const furniture of this.furnitures) {
-			this.loadFurniture(furniture).then(obj => {
-				this.scene.add(obj.scene);
-				this.objects.push(obj.scene);
-			});
-		}
-		//#endregion
-
-		// Start render
-		if (this.usePostFXs) {
-			this.renderWithPostFXs();
-		} else {
-			this.renderWithoutPostFXs();
-		}
-	}
-
-	@autobind
-	private renderWithoutPostFXs() {
-		this.renderFrameRequestId =
-			window.requestAnimationFrame(this.renderWithoutPostFXs);
-
-		// Update animations
-		const clock = this.clock.getDelta();
-		for (const mixer of this.mixers) {
-			mixer.update(clock);
-		}
-
-		this.controls.update();
-		this.renderer.render(this.scene, this.camera);
-	}
-
-	@autobind
-	private renderWithPostFXs() {
-		this.renderFrameRequestId =
-			window.requestAnimationFrame(this.renderWithPostFXs);
-
-		// Update animations
-		const clock = this.clock.getDelta();
-		for (const mixer of this.mixers) {
-			mixer.update(clock);
-		}
-
-		this.controls.update();
-		this.renderer.clear();
-		this.composer.render();
-	}
-
-	@autobind
-	private loadRoom() {
-		const type = this.roomInfo.roomType;
-		new GLTFLoader().load(`/static-assets/client/room/rooms/${type}/${type}.glb`, gltf => {
-			gltf.scene.traverse(child => {
-				if (!(child instanceof THREE.Mesh)) return;
-
-				child.receiveShadow = this.enableShadow;
-
-				child.material = new THREE.MeshLambertMaterial({
-					color: (child.material as THREE.MeshStandardMaterial).color,
-					map: (child.material as THREE.MeshStandardMaterial).map,
-					name: (child.material as THREE.MeshStandardMaterial).name,
-				});
-
-				// 異方性フィルタリング
-				if ((child.material as THREE.MeshLambertMaterial).map && this.graphicsQuality !== 'cheep') {
-					(child.material as THREE.MeshLambertMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
-					(child.material as THREE.MeshLambertMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
-					(child.material as THREE.MeshLambertMaterial).map.anisotropy = 8;
-				}
-			});
-
-			gltf.scene.position.set(0, 0, 0);
-
-			this.scene.add(gltf.scene);
-			this.roomObj = gltf.scene;
-			if (this.roomInfo.roomType === 'default') {
-				this.applyCarpetColor();
-			}
-		});
-	}
-
-	@autobind
-	private loadFurniture(furniture: Furniture) {
-		const def = furnitureDefs.find(d => d.id === furniture.type);
-		return new Promise<GLTF>((res, rej) => {
-			const loader = new GLTFLoader();
-			loader.load(`/static-assets/client/room/furnitures/${furniture.type}/${furniture.type}.glb`, gltf => {
-				const model = gltf.scene;
-
-				// Load animation
-				if (gltf.animations.length > 0) {
-					const mixer = new THREE.AnimationMixer(model);
-					this.mixers.push(mixer);
-					for (const clip of gltf.animations) {
-						mixer.clipAction(clip).play();
-					}
-				}
-
-				model.name = furniture.id;
-				model.position.x = furniture.position.x;
-				model.position.y = furniture.position.y;
-				model.position.z = furniture.position.z;
-				model.rotation.x = furniture.rotation.x;
-				model.rotation.y = furniture.rotation.y;
-				model.rotation.z = furniture.rotation.z;
-
-				model.traverse(child => {
-					if (!(child instanceof THREE.Mesh)) return;
-					child.castShadow = this.enableShadow;
-					child.receiveShadow = this.enableShadow;
-					(child.material as THREE.MeshStandardMaterial).metalness = 0;
-
-					// 異方性フィルタリング
-					if ((child.material as THREE.MeshStandardMaterial).map && this.graphicsQuality !== 'cheep') {
-						(child.material as THREE.MeshStandardMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
-						(child.material as THREE.MeshStandardMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
-						(child.material as THREE.MeshStandardMaterial).map.anisotropy = 8;
-					}
-				});
-
-				if (def.color) { // カスタムカラー
-					this.applyCustomColor(model);
-				}
-
-				if (def.texture) { // カスタムテクスチャ
-					this.applyCustomTexture(model);
-				}
-
-				res(gltf);
-			}, null, rej);
-		});
-	}
-
-	@autobind
-	private applyCarpetColor() {
-		this.roomObj.traverse(child => {
-			if (!(child instanceof THREE.Mesh)) return;
-			if (child.material &&
-				(child.material as THREE.MeshStandardMaterial).name &&
-				(child.material as THREE.MeshStandardMaterial).name === 'Carpet'
-			) {
-				const colorHex = parseInt(this.roomInfo.carpetColor.substr(1), 16);
-				(child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
-			}
-		});
-	}
-
-	@autobind
-	private applyCustomColor(model: THREE.Object3D) {
-		const furniture = this.furnitures.find(furniture => furniture.id === model.name);
-		const def = furnitureDefs.find(d => d.id === furniture.type);
-		if (def.color == null) return;
-		model.traverse(child => {
-			if (!(child instanceof THREE.Mesh)) return;
-			for (const t of Object.keys(def.color)) {
-				if (!child.material ||
-					!(child.material as THREE.MeshStandardMaterial).name ||
-					(child.material as THREE.MeshStandardMaterial).name !== t
-				) continue;
-
-				const prop = def.color[t];
-				const val = furniture.props ? furniture.props[prop] : undefined;
-
-				if (val == null) continue;
-
-				const colorHex = parseInt(val.substr(1), 16);
-				(child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
-			}
-		});
-	}
-
-	@autobind
-	private applyCustomTexture(model: THREE.Object3D) {
-		const furniture = this.furnitures.find(furniture => furniture.id === model.name);
-		const def = furnitureDefs.find(d => d.id === furniture.type);
-		if (def.texture == null) return;
-
-		model.traverse(child => {
-			if (!(child instanceof THREE.Mesh)) return;
-			for (const t of Object.keys(def.texture)) {
-				if (child.name !== t) continue;
-
-				const prop = def.texture[t].prop;
-				const val = furniture.props ? furniture.props[prop] : undefined;
-
-				if (val == null) continue;
-
-				const canvas = document.createElement('canvas');
-				canvas.height = 1024;
-				canvas.width = 1024;
-
-				child.material = new THREE.MeshLambertMaterial({
-					emissive: 0x111111,
-					side: THREE.DoubleSide,
-					alphaTest: 0.5,
-				});
-
-				const img = new Image();
-				img.crossOrigin = 'anonymous';
-				img.onload = () => {
-					const uvInfo = def.texture[t].uv;
-
-					const ctx = canvas.getContext('2d');
-					ctx.drawImage(img,
-						0, 0, img.width, img.height,
-						uvInfo.x, uvInfo.y, uvInfo.width, uvInfo.height);
-
-					const texture = new THREE.Texture(canvas);
-					texture.wrapS = THREE.RepeatWrapping;
-					texture.wrapT = THREE.RepeatWrapping;
-					texture.anisotropy = 16;
-					texture.flipY = false;
-
-					(child.material as THREE.MeshLambertMaterial).map = texture;
-					(child.material as THREE.MeshLambertMaterial).needsUpdate = true;
-					(child.material as THREE.MeshLambertMaterial).map.needsUpdate = true;
-				};
-				img.src = val;
-			}
-		});
-	}
-
-	@autobind
-	private onmousemove(ev: MouseEvent) {
-		if (this.isTransformMode) return;
-
-		const rect = (ev.target as HTMLElement).getBoundingClientRect();
-		const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
-		const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
-		const pos = new THREE.Vector2(x, y);
-
-		this.camera.updateMatrixWorld();
-
-		const raycaster = new THREE.Raycaster();
-		raycaster.setFromCamera(pos, this.camera);
-
-		const intersects = raycaster.intersectObjects(this.objects, true);
-
-		for (const object of this.objects) {
-			if (this.isSelectedObject(object)) continue;
-			object.traverse(child => {
-				if (child instanceof THREE.Mesh) {
-					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
-				}
-			});
-		}
-
-		if (intersects.length > 0) {
-			const intersected = this.getRoot(intersects[0].object);
-			if (this.isSelectedObject(intersected)) return;
-			intersected.traverse(child => {
-				if (child instanceof THREE.Mesh) {
-					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x191919);
-				}
-			});
-		}
-	}
-
-	@autobind
-	private onmousedown(ev: MouseEvent) {
-		if (this.isTransformMode) return;
-		if (ev.target !== this.canvas || ev.button !== 0) return;
-
-		const rect = (ev.target as HTMLElement).getBoundingClientRect();
-		const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
-		const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
-		const pos = new THREE.Vector2(x, y);
-
-		this.camera.updateMatrixWorld();
-
-		const raycaster = new THREE.Raycaster();
-		raycaster.setFromCamera(pos, this.camera);
-
-		const intersects = raycaster.intersectObjects(this.objects, true);
-
-		for (const object of this.objects) {
-			object.traverse(child => {
-				if (child instanceof THREE.Mesh) {
-					(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
-				}
-			});
-		}
-
-		if (intersects.length > 0) {
-			const selectedObj = this.getRoot(intersects[0].object);
-			this.selectFurniture(selectedObj);
-		} else {
-			this.selectedObject = null;
-			this.onChangeSelect(null);
-		}
-	}
-
-	@autobind
-	private getRoot(obj: THREE.Object3D): THREE.Object3D {
-		let found = false;
-		let x = obj.parent;
-		while (!found) {
-			if (x.parent.parent == null) {
-				found = true;
-			} else {
-				x = x.parent;
-			}
-		}
-		return x;
-	}
-
-	@autobind
-	private isSelectedObject(obj: THREE.Object3D): boolean {
-		if (this.selectedObject == null) {
-			return false;
-		} else {
-			return obj.name === this.selectedObject.name;
-		}
-	}
-
-	@autobind
-	private selectFurniture(obj: THREE.Object3D) {
-		this.selectedObject = obj;
-		this.onChangeSelect(obj);
-		obj.traverse(child => {
-			if (child instanceof THREE.Mesh) {
-				(child.material as THREE.MeshStandardMaterial).emissive.setHex(0xff0000);
-			}
-		});
-	}
-
-	/**
-	 * 家具の移動/回転モードにします
-	 * @param type 移動か回転か
-	 */
-	@autobind
-	public enterTransformMode(type: 'translate' | 'rotate') {
-		this.isTransformMode = true;
-		this.furnitureControl.setMode(type);
-		this.furnitureControl.attach(this.selectedObject);
-		this.controls.enableRotate = false;
-	}
-
-	/**
-	 * 家具の移動/回転モードを終了します
-	 */
-	@autobind
-	public exitTransformMode() {
-		this.isTransformMode = false;
-		this.furnitureControl.detach();
-		this.controls.enableRotate = true;
-	}
-
-	/**
-	 * 家具プロパティを更新します
-	 * @param key プロパティ名
-	 * @param value 値
-	 */
-	@autobind
-	public updateProp(key: string, value: any) {
-		const furniture = this.furnitures.find(furniture => furniture.id === this.selectedObject.name);
-		if (furniture.props == null) furniture.props = {};
-		furniture.props[key] = value;
-		this.applyCustomColor(this.selectedObject);
-		this.applyCustomTexture(this.selectedObject);
-	}
-
-	/**
-	 * 部屋に家具を追加します
-	 * @param type 家具の種類
-	 */
-	@autobind
-	public addFurniture(type: string) {
-		const furniture = {
-			id: uuid(),
-			type: type,
-			position: {
-				x: 0,
-				y: 0,
-				z: 0,
-			},
-			rotation: {
-				x: 0,
-				y: 0,
-				z: 0,
-			},
-		};
-
-		this.furnitures.push(furniture);
-
-		this.loadFurniture(furniture).then(obj => {
-			this.scene.add(obj.scene);
-			this.objects.push(obj.scene);
-		});
-	}
-
-	/**
-	 * 現在選択されている家具を部屋から削除します
-	 */
-	@autobind
-	public removeFurniture() {
-		this.exitTransformMode();
-		const obj = this.selectedObject;
-		this.scene.remove(obj);
-		this.objects = this.objects.filter(object => object.name !== obj.name);
-		this.furnitures = this.furnitures.filter(furniture => furniture.id !== obj.name);
-		this.selectedObject = null;
-		this.onChangeSelect(null);
-	}
-
-	/**
-	 * 全ての家具を部屋から削除します
-	 */
-	@autobind
-	public removeAllFurnitures() {
-		this.exitTransformMode();
-		for (const obj of this.objects) {
-			this.scene.remove(obj);
-		}
-		this.objects = [];
-		this.furnitures = [];
-		this.selectedObject = null;
-		this.onChangeSelect(null);
-	}
-
-	/**
-	 * 部屋の床の色を変更します
-	 * @param color 色
-	 */
-	@autobind
-	public updateCarpetColor(color: string) {
-		this.roomInfo.carpetColor = color;
-		this.applyCarpetColor();
-	}
-
-	/**
-	 * 部屋の種類を変更します
-	 * @param type 種類
-	 */
-	@autobind
-	public changeRoomType(type: string) {
-		this.roomInfo.roomType = type;
-		this.scene.remove(this.roomObj);
-		this.loadRoom();
-	}
-
-	/**
-	 * 部屋データを取得します
-	 */
-	@autobind
-	public getRoomInfo() {
-		for (const obj of this.objects) {
-			const furniture = this.furnitures.find(f => f.id === obj.name);
-			furniture.position.x = obj.position.x;
-			furniture.position.y = obj.position.y;
-			furniture.position.z = obj.position.z;
-			furniture.rotation.x = obj.rotation.x;
-			furniture.rotation.y = obj.rotation.y;
-			furniture.rotation.z = obj.rotation.z;
-		}
-
-		return this.roomInfo;
-	}
-
-	/**
-	 * 選択されている家具を取得します
-	 */
-	@autobind
-	public getSelectedObject() {
-		return this.selectedObject;
-	}
-
-	@autobind
-	public findFurnitureById(id: string) {
-		return this.furnitures.find(furniture => furniture.id === id);
-	}
-
-	/**
-	 * レンダリングを終了します
-	 */
-	@autobind
-	public destroy() {
-		// Stop render loop
-		window.cancelAnimationFrame(this.renderFrameRequestId);
-
-		this.controls.dispose();
-		this.scene.dispose();
-	}
-}
diff --git a/src/client/scripts/scroll.ts b/src/client/scripts/scroll.ts
deleted file mode 100644
index 621fe88105..0000000000
--- a/src/client/scripts/scroll.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-type ScrollBehavior = 'auto' | 'smooth' | 'instant';
-
-export function getScrollContainer(el: Element | null): Element | null {
-	if (el == null || el.tagName === 'BODY') return null;
-	const overflow = window.getComputedStyle(el).getPropertyValue('overflow');
-	if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる
-		return el;
-	} else {
-		return getScrollContainer(el.parentElement);
-	}
-}
-
-export function getScrollPosition(el: Element | null): number {
-	const container = getScrollContainer(el);
-	return container == null ? window.scrollY : container.scrollTop;
-}
-
-export function isTopVisible(el: Element | null): boolean {
-	const scrollTop = getScrollPosition(el);
-	const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
-
-	return scrollTop <= topPosition;
-}
-
-export function onScrollTop(el: Element, cb) {
-	const container = getScrollContainer(el) || window;
-	const onScroll = ev => {
-		if (!document.body.contains(el)) return;
-		if (isTopVisible(el)) {
-			cb();
-			container.removeEventListener('scroll', onScroll);
-		}
-	};
-	container.addEventListener('scroll', onScroll, { passive: true });
-}
-
-export function onScrollBottom(el: Element, cb) {
-	const container = getScrollContainer(el) || window;
-	const onScroll = ev => {
-		if (!document.body.contains(el)) return;
-		const pos = getScrollPosition(el);
-		if (pos + el.clientHeight > el.scrollHeight - 1) {
-			cb();
-			container.removeEventListener('scroll', onScroll);
-		}
-	};
-	container.addEventListener('scroll', onScroll, { passive: true });
-}
-
-export function scroll(el: Element, options: {
-	top?: number;
-	left?: number;
-	behavior?: ScrollBehavior;
-}) {
-	const container = getScrollContainer(el);
-	if (container == null) {
-		window.scroll(options);
-	} else {
-		container.scroll(options);
-	}
-}
-
-export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
-	scroll(el, { top: 0, ...options });
-}
-
-export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
-	scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する
-}
-
-export function isBottom(el: Element, asobi = 0) {
-	const container = getScrollContainer(el);
-	const current = container
-		? el.scrollTop + el.offsetHeight
-		: window.scrollY + window.innerHeight;
-	const max = container
-		? el.scrollHeight
-		: document.body.offsetHeight;
-	return current >= (max - asobi);
-}
diff --git a/src/client/scripts/search.ts b/src/client/scripts/search.ts
deleted file mode 100644
index 2221f5f279..0000000000
--- a/src/client/scripts/search.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import * as os from '@client/os';
-import { i18n } from '@client/i18n';
-import { router } from '@client/router';
-
-export async function search() {
-	const { canceled, result: query } = await os.dialog({
-		title: i18n.locale.search,
-		input: true
-	});
-	if (canceled || query == null || query === '') return;
-
-	const q = query.trim();
-
-	if (q.startsWith('@') && !q.includes(' ')) {
-		router.push(`/${q}`);
-		return;
-	}
-
-	if (q.startsWith('#')) {
-		router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
-		return;
-	}
-
-	// like 2018/03/12
-	if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) {
-		const date = new Date(q.replace(/-/g, '/'));
-
-		// 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは
-		// 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので
-		// 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の
-		// 結果になってしまい、2018/03/12 のコンテンツは含まれない)
-		if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) {
-			date.setHours(23, 59, 59, 999);
-		}
-
-		// TODO
-		//v.$root.$emit('warp', date);
-		os.dialog({
-			icon: 'fas fa-history',
-			iconOnly: true, autoClose: true
-		});
-		return;
-	}
-
-	if (q.startsWith('https://')) {
-		const promise = os.api('ap/show', {
-			uri: q
-		});
-
-		os.promiseDialog(promise, null, null, i18n.locale.fetchingAsApObject);
-
-		const res = await promise;
-
-		if (res.type === 'User') {
-			router.push(`/@${res.object.username}@${res.object.host}`);
-		} else if (res.type === 'Note') {
-			router.push(`/notes/${res.object.id}`);
-		}
-
-		return;
-	}
-
-	router.push(`/search?q=${encodeURIComponent(q)}`);
-}
diff --git a/src/client/scripts/select-file.ts b/src/client/scripts/select-file.ts
deleted file mode 100644
index f7b971e113..0000000000
--- a/src/client/scripts/select-file.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import * as os from '@client/os';
-import { i18n } from '@client/i18n';
-import { defaultStore } from '@client/store';
-
-export function selectFile(src: any, label: string | null, multiple = false) {
-	return new Promise((res, rej) => {
-		const chooseFileFromPc = () => {
-			const input = document.createElement('input');
-			input.type = 'file';
-			input.multiple = multiple;
-			input.onchange = () => {
-				const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder));
-
-				Promise.all(promises).then(driveFiles => {
-					res(multiple ? driveFiles : driveFiles[0]);
-				}).catch(e => {
-					os.dialog({
-						type: 'error',
-						text: e
-					});
-				});
-
-				// 一応廃棄
-				(window as any).__misskey_input_ref__ = null;
-			};
-
-			// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
-			// iOS Safari で正常に動かす為のおまじない
-			(window as any).__misskey_input_ref__ = input;
-
-			input.click();
-		};
-
-		const chooseFileFromDrive = () => {
-			os.selectDriveFile(multiple).then(files => {
-				res(files);
-			});
-		};
-
-		const chooseFileFromUrl = () => {
-			os.dialog({
-				title: i18n.locale.uploadFromUrl,
-				input: {
-					placeholder: i18n.locale.uploadFromUrlDescription
-				}
-			}).then(({ canceled, result: url }) => {
-				if (canceled) return;
-
-				const marker = Math.random().toString(); // TODO: UUIDとか使う
-
-				const connection = os.stream.useChannel('main');
-				connection.on('urlUploadFinished', data => {
-					if (data.marker === marker) {
-						res(multiple ? [data.file] : data.file);
-						connection.dispose();
-					}
-				});
-
-				os.api('drive/files/upload-from-url', {
-					url: url,
-					folderId: defaultStore.state.uploadFolder,
-					marker
-				});
-
-				os.dialog({
-					title: i18n.locale.uploadFromUrlRequested,
-					text: i18n.locale.uploadFromUrlMayTakeTime
-				});
-			});
-		};
-
-		os.popupMenu([label ? {
-			text: label,
-			type: 'label'
-		} : undefined, {
-			text: i18n.locale.upload,
-			icon: 'fas fa-upload',
-			action: chooseFileFromPc
-		}, {
-			text: i18n.locale.fromDrive,
-			icon: 'fas fa-cloud',
-			action: chooseFileFromDrive
-		}, {
-			text: i18n.locale.fromUrl,
-			icon: 'fas fa-link',
-			action: chooseFileFromUrl
-		}], src);
-	});
-}
diff --git a/src/client/scripts/show-suspended-dialog.ts b/src/client/scripts/show-suspended-dialog.ts
deleted file mode 100644
index dde829cdae..0000000000
--- a/src/client/scripts/show-suspended-dialog.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as os from '@client/os';
-import { i18n } from '@client/i18n';
-
-export function showSuspendedDialog() {
-	return os.dialog({
-		type: 'error',
-		title: i18n.locale.yourAccountSuspendedTitle,
-		text: i18n.locale.yourAccountSuspendedDescription
-	});
-}
diff --git a/src/client/scripts/sound.ts b/src/client/scripts/sound.ts
deleted file mode 100644
index c51fa8f215..0000000000
--- a/src/client/scripts/sound.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { ColdDeviceStorage } from '@client/store';
-
-const cache = new Map<string, HTMLAudioElement>();
-
-export function getAudio(file: string, useCache = true): HTMLAudioElement {
-	let audio: HTMLAudioElement;
-	if (useCache && cache.has(file)) {
-		audio = cache.get(file);
-	} else {
-		audio = new Audio(`/static-assets/client/sounds/${file}.mp3`);
-		if (useCache) cache.set(file, audio);
-	}
-	return audio;
-}
-
-export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
-	const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
-	audio.volume = masterVolume - ((1 - volume) * masterVolume);
-	return audio;
-}
-
-export function play(type: string) {
-	const sound = ColdDeviceStorage.get('sound_' + type as any);
-	if (sound.type == null) return;
-	playFile(sound.type, sound.volume);
-}
-
-export function playFile(file: string, volume: number) {
-	const masterVolume = ColdDeviceStorage.get('sound_masterVolume');
-	if (masterVolume === 0) return;
-
-	const audio = setVolume(getAudio(file), volume);
-	audio.play();
-}
diff --git a/src/client/scripts/sticky-sidebar.ts b/src/client/scripts/sticky-sidebar.ts
deleted file mode 100644
index c67b8f37ac..0000000000
--- a/src/client/scripts/sticky-sidebar.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-export class StickySidebar {
-	private lastScrollTop = 0;
-	private container: HTMLElement;
-	private el: HTMLElement;
-	private spacer: HTMLElement;
-	private marginTop: number;
-	private isTop = false;
-	private isBottom = false;
-	private offsetTop: number;
-	private globalHeaderHeight: number = 59;
-
-	constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) {
-		this.container = container;
-		this.el = this.container.children[0] as HTMLElement;
-		this.el.style.position = 'sticky';
-		this.spacer = document.createElement('div');
-		this.container.prepend(this.spacer);
-		this.marginTop = marginTop;
-		this.offsetTop = this.container.getBoundingClientRect().top;
-		this.globalHeaderHeight = globalHeaderHeight;
-	}
-
-	public calc(scrollTop: number) {
-		if (scrollTop > this.lastScrollTop) { // downscroll
-			const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight);
-			this.el.style.bottom = null;
-			this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`;
-
-			this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight);
-
-			if (this.isTop) {
-				this.isTop = false;
-				this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`;
-			}
-		} else { // upscroll
-			const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight;
-			this.el.style.top = null;
-			this.el.style.bottom = `${-overflow}px`;
-
-			this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop;
-
-			if (this.isBottom) {
-				this.isBottom = false;
-				this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`;
-			}
-		}
-
-		this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
-	}
-}
diff --git a/src/client/scripts/theme-editor.ts b/src/client/scripts/theme-editor.ts
deleted file mode 100644
index 3d69d2836a..0000000000
--- a/src/client/scripts/theme-editor.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { v4 as uuid} from 'uuid';
-
-import { themeProps, Theme } from './theme';
-
-export type Default = null;
-export type Color = string;
-export type FuncName = 'alpha' | 'darken' | 'lighten';
-export type Func = { type: 'func'; name: FuncName; arg: number; value: string; };
-export type RefProp = { type: 'refProp'; key: string; };
-export type RefConst = { type: 'refConst'; key: string; };
-export type Css = { type: 'css'; value: string; };
-
-export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default;
-
-export type ThemeViewModel = [ string, ThemeValue ][];
-
-export const fromThemeString = (str?: string) : ThemeValue => {
-	if (!str) return null;
-	if (str.startsWith(':')) {
-		const parts = str.slice(1).split('<');
-		const name = parts[0] as FuncName;
-		const arg = parseFloat(parts[1]);
-		const value = parts[2].startsWith('@') ? parts[2].slice(1) : '';
-		return { type: 'func', name, arg, value };
-	} else if (str.startsWith('@')) {
-		return {
-			type: 'refProp',
-			key: str.slice(1),
-		};
-	} else if (str.startsWith('$')) {
-		return {
-			type: 'refConst',
-			key: str.slice(1),
-		};
-	} else if (str.startsWith('"')) {
-		return {
-			type: 'css',
-			value: str.substr(1).trim(),
-		};
-	} else {
-		return str;
-	}
-};
-
-export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => {
-	if (typeof value === 'string') return value;
-	switch (value.type) {
-		case 'func': return `:${value.name}<${value.arg}<@${value.value}`;
-		case 'refProp': return `@${value.key}`;
-		case 'refConst': return `$${value.key}`;
-		case 'css': return `" ${value.value}`;
-	}
-};
-
-export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => {
-	const props = { } as { [key: string]: string };
-	for (const [ key, value ] of vm) {
-		if (value === null) continue;
-		props[key] = toThemeString(value);
-	}
-
-	return {
-		id: uuid(),
-		name, desc, author, props, base
-	};
-};
-
-export const convertToViewModel = (theme: Theme): ThemeViewModel => {
-	const vm: ThemeViewModel = [];
-	// プロパティの登録
-	vm.push(...themeProps.map(key => [ key, fromThemeString(theme.props[key])] as [ string, ThemeValue ]));
-
-	// 定数の登録
-	const consts = Object
-		.keys(theme.props)
-		.filter(k => k.startsWith('$'))
-		.map(k => [ k, fromThemeString(theme.props[k]) ] as [ string, ThemeValue ]);
-
-		vm.push(...consts);
-	return vm;
-};
diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts
deleted file mode 100644
index 8b63821293..0000000000
--- a/src/client/scripts/theme.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { globalEvents } from '@client/events';
-import * as tinycolor from 'tinycolor2';
-
-export type Theme = {
-	id: string;
-	name: string;
-	author: string;
-	desc?: string;
-	base?: 'dark' | 'light';
-	props: Record<string, string>;
-};
-
-export const lightTheme: Theme = require('@client/themes/_light.json5');
-export const darkTheme: Theme = require('@client/themes/_dark.json5');
-
-export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
-
-export const builtinThemes = [
-	require('@client/themes/l-light.json5'),
-	require('@client/themes/l-apricot.json5'),
-	require('@client/themes/l-rainy.json5'),
-	require('@client/themes/l-vivid.json5'),
-	require('@client/themes/l-sushi.json5'),
-
-	require('@client/themes/d-dark.json5'),
-	require('@client/themes/d-persimmon.json5'),
-	require('@client/themes/d-astro.json5'),
-	require('@client/themes/d-future.json5'),
-	require('@client/themes/d-botanical.json5'),
-	require('@client/themes/d-pumpkin.json5'),
-	require('@client/themes/d-black.json5'),
-] as Theme[];
-
-let timeout = null;
-
-export function applyTheme(theme: Theme, persist = true) {
-	if (timeout) clearTimeout(timeout);
-
-	document.documentElement.classList.add('_themeChanging_');
-
-	timeout = setTimeout(() => {
-		document.documentElement.classList.remove('_themeChanging_');
-	}, 1000);
-
-	// Deep copy
-	const _theme = JSON.parse(JSON.stringify(theme));
-
-	if (_theme.base) {
-		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
-		_theme.props = Object.assign({}, base.props, _theme.props);
-	}
-
-	const props = compile(_theme);
-
-	for (const tag of document.head.children) {
-		if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
-			tag.setAttribute('content', props['html']);
-			break;
-		}
-	}
-
-	for (const [k, v] of Object.entries(props)) {
-		document.documentElement.style.setProperty(`--${k}`, v.toString());
-	}
-
-	if (persist) {
-		localStorage.setItem('theme', JSON.stringify(props));
-	}
-
-	// 色計算など再度行えるようにクライアント全体に通知
-	globalEvents.emit('themeChanged');
-}
-
-function compile(theme: Theme): Record<string, string> {
-	function getColor(val: string): tinycolor.Instance {
-		// ref (prop)
-		if (val[0] === '@') {
-			return getColor(theme.props[val.substr(1)]);
-		}
-
-		// ref (const)
-		else if (val[0] === '$') {
-			return getColor(theme.props[val]);
-		}
-
-		// func
-		else if (val[0] === ':') {
-			const parts = val.split('<');
-			const func = parts.shift().substr(1);
-			const arg = parseFloat(parts.shift());
-			const color = getColor(parts.join('<'));
-
-			switch (func) {
-				case 'darken': return color.darken(arg);
-				case 'lighten': return color.lighten(arg);
-				case 'alpha': return color.setAlpha(arg);
-				case 'hue': return color.spin(arg);
-				case 'saturate': return color.saturate(arg);
-			}
-		}
-
-		// other case
-		return tinycolor(val);
-	}
-
-	const props = {};
-
-	for (const [k, v] of Object.entries(theme.props)) {
-		if (k.startsWith('$')) continue; // ignore const
-
-		props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
-	}
-
-	return props;
-}
-
-function genValue(c: tinycolor.Instance): string {
-	return c.toRgbString();
-}
-
-export function validateTheme(theme: Record<string, any>): boolean {
-	if (theme.id == null || typeof theme.id !== 'string') return false;
-	if (theme.name == null || typeof theme.name !== 'string') return false;
-	if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
-	if (theme.props == null || typeof theme.props !== 'object') return false;
-	return true;
-}
diff --git a/src/client/scripts/unison-reload.ts b/src/client/scripts/unison-reload.ts
deleted file mode 100644
index 59af584c1b..0000000000
--- a/src/client/scripts/unison-reload.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// SafariがBroadcastChannel未実装なのでライブラリを使う
-import { BroadcastChannel } from 'broadcast-channel';
-
-export const reloadChannel = new BroadcastChannel<string | null>('reload');
-
-// BroadcastChannelを用いて、クライアントが一斉にreloadするようにします。
-export function unisonReload(path?: string) {
-	if (path !== undefined) {
-		reloadChannel.postMessage(path);
-		location.href = path;
-	} else {
-		reloadChannel.postMessage(null);
-		location.reload();
-	}
-}
diff --git a/src/client/store.ts b/src/client/store.ts
deleted file mode 100644
index eea3955fac..0000000000
--- a/src/client/store.ts
+++ /dev/null
@@ -1,318 +0,0 @@
-import { markRaw, ref } from 'vue';
-import { Storage } from './pizzax';
-import { Theme } from './scripts/theme';
-
-export const postFormActions = [];
-export const userActions = [];
-export const noteActions = [];
-export const noteViewInterruptors = [];
-export const notePostInterruptors = [];
-
-// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
-//       あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
-export const defaultStore = markRaw(new Storage('base', {
-	tutorial: {
-		where: 'account',
-		default: 0
-	},
-	keepCw: {
-		where: 'account',
-		default: false
-	},
-	showFullAcct: {
-		where: 'account',
-		default: false
-	},
-	rememberNoteVisibility: {
-		where: 'account',
-		default: false
-	},
-	defaultNoteVisibility: {
-		where: 'account',
-		default: 'public'
-	},
-	defaultNoteLocalOnly: {
-		where: 'account',
-		default: false
-	},
-	uploadFolder: {
-		where: 'account',
-		default: null as string | null
-	},
-	pastedFileName: {
-		where: 'account',
-		default: 'yyyy-MM-dd HH-mm-ss [{{number}}]'
-	},
-	memo: {
-		where: 'account',
-		default: null
-	},
-	reactions: {
-		where: 'account',
-		default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮']
-	},
-	mutedWords: {
-		where: 'account',
-		default: []
-	},
-	mutedAds: {
-		where: 'account',
-		default: [] as string[]
-	},
-
-	menu: {
-		where: 'deviceAccount',
-		default: [
-			'notifications',
-			'messaging',
-			'drive',
-			'followRequests',
-			'-',
-			'gallery',
-			'featured',
-			'explore',
-			'announcements',
-			'search',
-			'-',
-			'ui',
-		]
-	},
-	visibility: {
-		where: 'deviceAccount',
-		default: 'public' as 'public' | 'home' | 'followers' | 'specified'
-	},
-	localOnly: {
-		where: 'deviceAccount',
-		default: false
-	},
-	widgets: {
-		where: 'deviceAccount',
-		default: [] as {
-			name: string;
-			id: string;
-			place: string | null;
-			data: Record<string, any>;
-		}[]
-	},
-	tl: {
-		where: 'deviceAccount',
-		default: {
-			src: 'home',
-			arg: null
-		}
-	},
-
-	serverDisconnectedBehavior: {
-		where: 'device',
-		default: 'quiet' as 'quiet' | 'reload' | 'dialog'
-	},
-	nsfw: {
-		where: 'device',
-		default: 'respect' as 'respect' | 'force' | 'ignore'
-	},
-	animation: {
-		where: 'device',
-		default: true
-	},
-	animatedMfm: {
-		where: 'device',
-		default: true
-	},
-	loadRawImages: {
-		where: 'device',
-		default: false
-	},
-	imageNewTab: {
-		where: 'device',
-		default: false
-	},
-	disableShowingAnimatedImages: {
-		where: 'device',
-		default: false
-	},
-	disablePagesScript: {
-		where: 'device',
-		default: false
-	},
-	useOsNativeEmojis: {
-		where: 'device',
-		default: false
-	},
-	useBlurEffectForModal: {
-		where: 'device',
-		default: true
-	},
-	useBlurEffect: {
-		where: 'device',
-		default: true
-	},
-	showFixedPostForm: {
-		where: 'device',
-		default: false
-	},
-	enableInfiniteScroll: {
-		where: 'device',
-		default: true
-	},
-	useReactionPickerForContextMenu: {
-		where: 'device',
-		default: true
-	},
-	showGapBetweenNotesInTimeline: {
-		where: 'device',
-		default: false
-	},
-	darkMode: {
-		where: 'device',
-		default: false
-	},
-	instanceTicker: {
-		where: 'device',
-		default: 'remote' as 'none' | 'remote' | 'always'
-	},
-	reactionPickerWidth: {
-		where: 'device',
-		default: 1
-	},
-	reactionPickerHeight: {
-		where: 'device',
-		default: 1
-	},
-	recentlyUsedEmojis: {
-		where: 'device',
-		default: [] as string[]
-	},
-	recentlyUsedUsers: {
-		where: 'device',
-		default: [] as string[]
-	},
-	defaultSideView: {
-		where: 'device',
-		default: false
-	},
-	menuDisplay: {
-		where: 'device',
-		default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top'
-	},
-	reportError: {
-		where: 'device',
-		default: false
-	},
-	squareAvatars: {
-		where: 'device',
-		default: false
-	},
-	postFormWithHashtags: {
-		where: 'device',
-		default: false
-	},
-	postFormHashtags: {
-		where: 'device',
-		default: ''
-	},
-	aiChanMode: {
-		where: 'device',
-		default: false
-	},
-}));
-
-// TODO: 他のタブと永続化されたstateを同期
-
-const PREFIX = 'miux:';
-
-type Plugin = {
-	id: string;
-	name: string;
-	active: boolean;
-	configData: Record<string, any>;
-	token: string;
-	ast: any[];
-};
-
-/**
- * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
- */
-export class ColdDeviceStorage {
-	public static default = {
-		lightTheme: require('@client/themes/l-light.json5') as Theme,
-		darkTheme: require('@client/themes/d-dark.json5') as Theme,
-		syncDeviceDarkMode: true,
-		chatOpenBehavior: 'page' as 'page' | 'window' | 'popout',
-		plugins: [] as Plugin[],
-		mediaVolume: 0.5,
-		sound_masterVolume: 0.3,
-		sound_note: { type: 'syuilo/down', volume: 1 },
-		sound_noteMy: { type: 'syuilo/up', volume: 1 },
-		sound_notification: { type: 'syuilo/pope2', volume: 1 },
-		sound_chat: { type: 'syuilo/pope1', volume: 1 },
-		sound_chatBg: { type: 'syuilo/waon', volume: 1 },
-		sound_antenna: { type: 'syuilo/triple', volume: 1 },
-		sound_channel: { type: 'syuilo/square-pico', volume: 1 },
-		sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
-		sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
-		roomGraphicsQuality: 'medium' as 'cheep' | 'low' | 'medium' | 'high' | 'ultra',
-		roomUseOrthographicCamera: true,
-	};
-
-	public static watchers = [];
-
-	public static get<T extends keyof typeof ColdDeviceStorage.default>(key: T): typeof ColdDeviceStorage.default[T] {
-		// TODO: indexedDBにする
-		//       ただしその際はnullチェックではなくキー存在チェックにしないとダメ
-		//       (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
-		const value = localStorage.getItem(PREFIX + key);
-		if (value == null) {
-			return ColdDeviceStorage.default[key];
-		} else {
-			return JSON.parse(value);
-		}
-	}
-
-	public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
-		localStorage.setItem(PREFIX + key, JSON.stringify(value));
-
-		for (const watcher of this.watchers) {
-			if (watcher.key === key) watcher.callback(value);
-		}
-	}
-
-	public static watch(key, callback) {
-		this.watchers.push({ key, callback });
-	}
-
-	// TODO: VueのcustomRef使うと良い感じになるかも
-	public static ref<T extends keyof typeof ColdDeviceStorage.default>(key: T) {
-		const v = ColdDeviceStorage.get(key);
-		const r = ref(v);
-		// TODO: このままではwatcherがリークするので開放する方法を考える
-		this.watch(key, v => {
-			r.value = v;
-		});
-		return r;
-	}
-
-	/**
-	 * 特定のキーの、簡易的なgetter/setterを作ります
-	 * 主にvue場で設定コントロールのmodelとして使う用
-	 */
-	public static makeGetterSetter<K extends keyof typeof ColdDeviceStorage.default>(key: K) {
-		// TODO: VueのcustomRef使うと良い感じになるかも
-		const valueRef = ColdDeviceStorage.ref(key);
-		return {
-			get: () => {
-				return valueRef.value;
-			},
-			set: (value: unknown) => {
-				const val = value;
-				ColdDeviceStorage.set(key, val);
-			}
-		};
-	}
-}
-
-// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
-declare module '@vue/runtime-core' {
-	interface ComponentCustomProperties {
-		$store: typeof defaultStore;
-	}
-}
diff --git a/src/client/style.scss b/src/client/style.scss
deleted file mode 100644
index 951d5a14f3..0000000000
--- a/src/client/style.scss
+++ /dev/null
@@ -1,562 +0,0 @@
-@charset "utf-8";
-
-:root {
-	--radius: 12px;
-	--marginFull: 16px;
-	--marginHalf: 10px;
-
-	--margin: var(--marginFull);
-
-	@media (max-width: 500px) {
-		--margin: var(--marginHalf);
-	}
-
-	//--ad: rgb(255 169 0 / 10%);
-}
-
-::selection {
-	color: #fff;
-	background-color: var(--accent);
-} 
-
-html {
-	touch-action: manipulation;
-	background-color: var(--bg);
-	background-attachment: fixed;
-	background-size: cover;
-	background-position: center;
-	color: var(--fg);
-	overflow: auto;
-	overflow-wrap: break-word;
-	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
-	line-height: 1.35;
-	text-size-adjust: 100%;
-	tab-size: 2;
-
-	&, * {
-		scrollbar-color: var(--scrollbarHandle) inherit;
-		scrollbar-width: thin;
-
-		&:hover {
-			scrollbar-color: var(--scrollbarHandleHover) inherit;
-		}
-
-		&:active {
-			scrollbar-color: var(--accent) inherit;
-		}
-
-		&::-webkit-scrollbar {
-			width: 6px;
-			height: 6px;
-		}
-
-		&::-webkit-scrollbar-track {
-			background: inherit;
-		}
-
-		&::-webkit-scrollbar-thumb {
-			background: var(--scrollbarHandle);
-
-			&:hover {
-				background: var(--scrollbarHandleHover);
-			}
-
-			&:active {
-				background: var(--accent);
-			}
-		}
-	}
-
-	&.f-small {
-		font-size: 0.9em;
-	}
-
-	&.f-large {
-		font-size: 1.1em;
-	}
-
-	&.f-veryLarge {
-		font-size: 1.2em;
-	}
-
-	&.useSystemFont {
-		font-family: sans-serif;
-	}
-}
-
-html._themeChanging_ {
-	&, * {
-		transition: background 1s ease, border 1s ease !important;
-	}
-}
-
-html, body {
-	margin: 0;
-	padding: 0;
-	scroll-behavior: smooth;
-}
-
-a {
-	text-decoration: none;
-	cursor: pointer;
-	color: inherit;
-	tap-highlight-color: transparent;
-	-webkit-tap-highlight-color: transparent;
-
-	&:hover {
-		text-decoration: underline;
-	}
-}
-
-textarea, input {
-	tap-highlight-color: transparent;
-	-webkit-tap-highlight-color: transparent;
-}
-
-optgroup, option {
-	background: var(--panel);
-	color: var(--fg);
-}
-
-hr {
-	margin: var(--margin) 0 var(--margin) 0;
-	border: none;
-	height: 1px;
-	background: var(--divider);
-}
-
-._noSelect {
-	user-select: none;
-	-webkit-user-select: none;
-	-webkit-touch-callout: none;
-}
-
-._ghost {
-	&, * {
-		@extend ._noSelect;
-		pointer-events: none;
-	}
-}
-
-._modalBg {
-	position: fixed;
-	top: 0;
-	left: 0;
-	width: 100%;
-	height: 100%;
-	background: var(--modalBg);
-	-webkit-backdrop-filter: var(--modalBgFilter);
-	backdrop-filter: var(--modalBgFilter);
-}
-
-._shadow {
-	box-shadow: 0px 4px 32px var(--shadow) !important;
-}
-
-._button {
-	appearance: none;
-	display: inline-block;
-	padding: 0;
-	margin: 0; // for Safari
-	background: none;
-	border: none;
-	cursor: pointer;
-	color: inherit;
-	touch-action: manipulation;
-	tap-highlight-color: transparent;
-	-webkit-tap-highlight-color: transparent;
-	font-size: 1em;
-	font-family: inherit;
-	line-height: inherit;
-
-	&, * {
-		@extend ._noSelect;
-	}
-
-	* {
-		pointer-events: none;
-	}
-
-	&:focus-visible {
-		outline: none;
-	}
-
-	&:disabled {
-		opacity: 0.5;
-		cursor: default;
-	}
-}
-
-._buttonPrimary {
-	@extend ._button;
-	color: var(--fgOnAccent);
-	background: var(--accent);
-
-	&:not(:disabled):hover {
-		background: var(--X8);
-	}
-
-	&:not(:disabled):active {
-		background: var(--X9);
-	}
-}
-
-._buttonGradate {
-	@extend ._buttonPrimary;
-	color: var(--fgOnAccent);
-	background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-
-	&:not(:disabled):hover {
-		background: linear-gradient(90deg, var(--X8), var(--X8));
-	}
-
-	&:not(:disabled):active {
-		background: linear-gradient(90deg, var(--X8), var(--X8));
-	}
-}
-
-._help {
-	color: var(--accent);
-	cursor: help
-}
-
-._textButton {
-	@extend ._button;
-	color: var(--accent);
-
-	&:not(:disabled):hover {
-		text-decoration: underline;
-	}
-}
-
-._inputs {
-	display: flex;
-	margin: 32px 0;
-
-	&:first-child {
-		margin-top: 8px;
-	}
-
-	&:last-child {
-		margin-bottom: 8px;
-	}
-
-	> * {
-		flex: 1;
-		margin: 0 !important;
-
-		&:not(:first-child) {
-			margin-left: 8px !important;
-		}
-
-		&:not(:last-child) {
-			margin-right: 8px !important;
-		}
-	}
-}
-
-._panel {
-	background: var(--panel);
-	border-radius: var(--radius);
-	overflow: clip;
-}
-
-._block {
-	@extend ._panel;
-
-	& + ._block {
-		margin-top: var(--margin);
-	}
-}
-
-._gap {
-	margin: var(--margin) 0;
-}
-
-// TODO: 廃止
-._card {
-	@extend ._panel;
-
-	// TODO: _cardTitle に
-	> ._title {
-		margin: 0;
-		padding: 22px 32px;
-		font-size: 1em;
-		border-bottom: solid 1px var(--panelHeaderDivider);
-		font-weight: bold;
-		background: var(--panelHeaderBg);
-		color: var(--panelHeaderFg);
-
-		@media (max-width: 500px) {
-			padding: 16px;
-			font-size: 1em;
-		}
-	}
-
-	// TODO: _cardContent に
-	> ._content {
-		padding: 32px;
-
-		@media (max-width: 500px) {
-			padding: 16px;
-		}
-
-		&._noPad {
-			padding: 0 !important;
-		}
-
-		& + ._content {
-			border-top: solid 0.5px var(--divider);
-		}
-	}
-
-	// TODO: _cardFooter に
-	> ._footer {
-		border-top: solid 0.5px var(--divider);
-		padding: 24px 32px;
-
-		@media (max-width: 500px) {
-			padding: 16px;
-		}
-	}
-}
-
-._borderButton {
-	@extend ._button;
-	display: block;
-	width: 100%;
-	padding: 10px;
-	box-sizing: border-box;
-	text-align: center;
-	border: solid 0.5px var(--divider);
-	border-radius: var(--radius);
-
-	&:active {
-		border-color: var(--accent);
-	}
-}
-
-._window {
-	background: var(--panel);
-	border-radius: var(--radius);
-	contain: content;
-}
-
-._popup {
-	background: var(--popup);
-	border-radius: var(--radius);
-	contain: layout; // ふき出しがボックスから飛び出て表示されるようなデザインをする場合もあるので paint は contain することができない
-}
-
-// TODO: 廃止
-._monolithic_ {
-	._section:not(:empty) {
-		box-sizing: border-box;
-		padding: var(--root-margin, 32px);
-	
-		@media (max-width: 500px) {
-			--root-margin: 10px;
-		}
-	
-		& + ._section:not(:empty) {
-			border-top: solid 0.5px var(--divider);
-		}
-	}
-}
-
-._narrow_ ._card {
-	> ._title {
-		padding: 16px;
-		font-size: 1em;
-	}
-
-	> ._content {
-		padding: 16px;
-	}
-
-	> ._footer {
-		padding: 16px;
-	}
-}
-
-._acrylic {
-	background: var(--acrylicPanel);
-	-webkit-backdrop-filter: var(--blur, blur(15px));
-	backdrop-filter: var(--blur, blur(15px));
-}
-
-._inputSplit {
-	display: grid;
-	grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
-	grid-gap: 8px;
-	margin: 1em 0;
-
-	> * {
-		margin: 0 !important;
-	}
-}
-
-._formBlock {
-	margin: 20px 0;
-}
-
-._formRoot {
-	> ._formBlock:first-child {
-		margin-top: 0;
-	}
-
-	> ._formBlock:last-child {
-		margin-bottom: 0;
-	}
-}
-
-._table {
-	> ._row {
-		display: flex;
-
-		&:not(:last-child) {
-			margin-bottom: 16px;
-
-			@media (max-width: 500px) {
-				margin-bottom: 8px;
-			}
-		}
-
-		> ._cell {
-			flex: 1;
-
-			> ._label {
-				font-size: 80%;
-				opacity: 0.7;
-
-				> ._icon {
-					margin-right: 4px;
-					display: none;
-				}
-			}
-		}
-	}
-}
-
-._fullinfo {
-	padding: 64px 32px;
-	text-align: center;
-
-	> img {
-		vertical-align: bottom;
-		height: 128px;
-		margin-bottom: 16px;
-		border-radius: 16px;
-	}
-}
-
-._keyValue {
-	display: flex;
-
-	> * {
-		flex: 1;
-	}
-}
-
-._link {
-	color: var(--link);
-}
-
-._caption {
-	font-size: 0.8em;
-	opacity: 0.7;
-}
-
-._monospace {
-	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important;
-}
-
-._code {
-	@extend ._monospace;
-	background: #2d2d2d;
-	color: #ccc;
-	font-size: 14px;
-	line-height: 1.5;
-	padding: 5px;
-}
-
-.prism-editor__textarea:focus {
-	outline: none;
-}
-
-._zoom {
-	transition-duration: 0.5s, 0.5s;
-	transition-property: opacity, transform;
-	transition-timing-function: cubic-bezier(0,.5,.5,1);
-}
-
-.zoom-enter-active, .zoom-leave-active {
-	transition: opacity 0.5s, transform 0.5s !important;
-}
-.zoom-enter-from, .zoom-leave-to {
-	opacity: 0;
-	transform: scale(0.9);
-}
-
-@keyframes blink {
-	0% { opacity: 1; transform: scale(1); }
-	30% { opacity: 1; transform: scale(1); }
-	90% { opacity: 0; transform: scale(0.5); }
-}
-
-@keyframes tada {
-	from {
-		transform: scale3d(1, 1, 1);
-	}
-
-	10%,
-	20% {
-		transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
-	}
-
-	30%,
-	50%,
-	70%,
-	90% {
-		transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
-	}
-
-	40%,
-	60%,
-	80% {
-		transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
-	}
-
-	to {
-		transform: scale3d(1, 1, 1);
-	}
-}
-
-._anime_bounce {
-	will-change: transform;
-  animation: bounce ease 0.7s;
-  animation-iteration-count: 1;
-  transform-origin: 50% 50%;
-}
-._anime_bounce_ready {
-	will-change: transform;
-	transform:  scaleX(0.90) scaleY(0.90) ;
-}
-._anime_bounce_standBy {
-	transition: transform 0.1s ease;
-}
-
-@keyframes bounce{
-  0% {
-    transform:  scaleX(0.90) scaleY(0.90) ;
-  }
-  19% {
-    transform:  scaleX(1.10) scaleY(1.10) ;
-  }
-  48% {
-    transform:  scaleX(0.95) scaleY(0.95) ;
-  }
-  100% {
-    transform:  scaleX(1.00) scaleY(1.00) ;
-  }
-}
diff --git a/src/client/sw/compose-notification.ts b/src/client/sw/compose-notification.ts
deleted file mode 100644
index 7ed0a95359..0000000000
--- a/src/client/sw/compose-notification.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * Notification composer of Service Worker
- */
-declare var self: ServiceWorkerGlobalScope;
-
-import { getNoteSummary } from '@/misc/get-note-summary';
-import getUserName from '@/misc/get-user-name';
-
-export default async function(type, data, i18n): Promise<[string, NotificationOptions] | null | undefined> {
-	if (!i18n) {
-		console.log('no i18n');
-		return;
-	}
-
-	switch (type) {
-		case 'driveFileCreated': // TODO (Server Side)
-			return [i18n.t('_notification.fileUploaded'), {
-				body: data.name,
-				icon: data.url
-			}];
-		case 'notification':
-			switch (data.type) {
-				case 'mention':
-					return [i18n.t('_notification.youGotMention', { name: getUserName(data.user) }), {
-						body: getNoteSummary(data.note, i18n.locale),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'reply':
-					return [i18n.t('_notification.youGotReply', { name: getUserName(data.user) }), {
-						body: getNoteSummary(data.note, i18n.locale),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'renote':
-					return [i18n.t('_notification.youRenoted', { name: getUserName(data.user) }), {
-						body: getNoteSummary(data.note, i18n.locale),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'quote':
-					return [i18n.t('_notification.youGotQuote', { name: getUserName(data.user) }), {
-						body: getNoteSummary(data.note, i18n.locale),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'reaction':
-					return [`${data.reaction} ${getUserName(data.user)}`, {
-						body: getNoteSummary(data.note, i18n.locale),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'pollVote':
-					return [i18n.t('_notification.youGotPoll', { name: getUserName(data.user) }), {
-						body: getNoteSummary(data.note, i18n.locale),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'follow':
-					return [i18n.t('_notification.youWereFollowed'), {
-						body: getUserName(data.user),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'receiveFollowRequest':
-					return [i18n.t('_notification.youReceivedFollowRequest'), {
-						body: getUserName(data.user),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'followRequestAccepted':
-					return [i18n.t('_notification.yourFollowRequestAccepted'), {
-						body: getUserName(data.user),
-						icon: data.user.avatarUrl
-					}];
-
-				case 'groupInvited':
-					return [i18n.t('_notification.youWereInvitedToGroup'), {
-						body: data.group.name
-					}];
-
-				default:
-					return null;
-			}
-		case 'unreadMessagingMessage':
-			if (data.groupId === null) {
-				return [i18n.t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.user) }), {
-					icon: data.user.avatarUrl,
-					tag: `messaging:user:${data.user.id}`
-				}];
-			}
-			return [i18n.t('_notification.youGotMessagingMessageFromGroup', { name: data.group.name }), {
-				icon: data.user.avatarUrl,
-				tag: `messaging:group:${data.group.id}`
-			}];
-		default:
-			return null;
-	}
-}
diff --git a/src/client/sw/sw.ts b/src/client/sw/sw.ts
deleted file mode 100644
index 5be4eb9770..0000000000
--- a/src/client/sw/sw.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * Service Worker
- */
-declare var self: ServiceWorkerGlobalScope;
-
-import { get, set } from 'idb-keyval';
-import composeNotification from '@client/sw/compose-notification';
-import { I18n } from '@/misc/i18n';
-
-//#region Variables
-const version = _VERSION_;
-const cacheName = `mk-cache-${version}`;
-
-let lang: string;
-let i18n: I18n<any>;
-let pushesPool: any[] = [];
-//#endregion
-
-//#region Startup
-get('lang').then(async prelang => {
-	if (!prelang) return;
-	lang = prelang;
-	return fetchLocale();
-});
-//#endregion
-
-//#region Lifecycle: Install
-self.addEventListener('install', ev => {
-	self.skipWaiting();
-});
-//#endregion
-
-//#region Lifecycle: Activate
-self.addEventListener('activate', ev => {
-	ev.waitUntil(
-		caches.keys()
-			.then(cacheNames => Promise.all(
-				cacheNames
-					.filter((v) => v !== cacheName)
-					.map(name => caches.delete(name))
-			))
-			.then(() => self.clients.claim())
-	);
-});
-//#endregion
-
-//#region When: Fetching
-self.addEventListener('fetch', ev => {
-	// Nothing to do
-});
-//#endregion
-
-//#region When: Caught Notification
-self.addEventListener('push', ev => {
-	// クライアント取得
-	ev.waitUntil(self.clients.matchAll({
-		includeUncontrolled: true
-	}).then(async clients => {
-		// クライアントがあったらストリームに接続しているということなので通知しない
-		if (clients.length != 0) return;
-
-		const { type, body } = ev.data?.json();
-
-		// localeを読み込めておらずi18nがundefinedだった場合はpushesPoolにためておく
-		if (!i18n) return pushesPool.push({ type, body });
-
-		const n = await composeNotification(type, body, i18n);
-		if (n) return self.registration.showNotification(...n);
-	}));
-});
-//#endregion
-
-//#region When: Caught a message from the client
-self.addEventListener('message', ev => {
-	switch(ev.data) {
-		case 'clear':
-			return; // TODO
-		default:
-			break;
-	}
-
-	if (typeof ev.data === 'object') {
-		// E.g. '[object Array]' → 'array'
-		const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase();
-
-		if (otype === 'object') {
-			if (ev.data.msg === 'initialize') {
-				lang = ev.data.lang;
-				set('lang', lang);
-				fetchLocale();
-			}
-		}
-	}
-});
-//#endregion
-
-//#region Function: (Re)Load i18n instance
-async function fetchLocale() {
-	//#region localeファイルの読み込み
-	// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
-	const localeUrl = `/assets/locales/${lang}.${version}.json`;
-	let localeRes = await caches.match(localeUrl);
-
-	if (!localeRes) {
-		localeRes = await fetch(localeUrl);
-		const clone = localeRes?.clone();
-		if (!clone?.clone().ok) return;
-
-		caches.open(cacheName).then(cache => cache.put(localeUrl, clone));
-	}
-
-	i18n = new I18n(await localeRes.json());
-	//#endregion
-
-	//#region i18nをきちんと読み込んだ後にやりたい処理
-	for (const { type, body } of pushesPool) {
-		const n = await composeNotification(type, body, i18n);
-		if (n) self.registration.showNotification(...n);
-	}
-	pushesPool = [];
-	//#endregion
-}
-//#endregion
diff --git a/src/client/symbols.ts b/src/client/symbols.ts
deleted file mode 100644
index 6913f29c28..0000000000
--- a/src/client/symbols.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const PAGE_INFO = Symbol('Page info');
diff --git a/src/client/theme-store.ts b/src/client/theme-store.ts
deleted file mode 100644
index f291069692..0000000000
--- a/src/client/theme-store.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { api } from '@client/os';
-import { $i } from '@client/account';
-import { Theme } from './scripts/theme';
-
-const lsCacheKey = $i ? `themes:${$i.id}` : '';
-
-export function getThemes(): Theme[] {
-	return JSON.parse(localStorage.getItem(lsCacheKey) || '[]');
-}
-
-export async function fetchThemes(): Promise<void> {
-	if ($i == null) return;
-
-	try {
-		const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
-		localStorage.setItem(lsCacheKey, JSON.stringify(themes));
-	} catch (e) {
-		if (e.code === 'NO_SUCH_KEY') return;
-		throw e;
-	}
-}
-
-export async function addTheme(theme: Theme): Promise<void> {
-	await fetchThemes();
-	const themes = getThemes().concat(theme);
-	await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
-	localStorage.setItem(lsCacheKey, JSON.stringify(themes));
-}
-
-export async function removeTheme(theme: Theme): Promise<void> {
-	const themes = getThemes().filter(t => t.id != theme.id);
-	await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
-	localStorage.setItem(lsCacheKey, JSON.stringify(themes));
-}
diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5
deleted file mode 100644
index d8be16f60a..0000000000
--- a/src/client/themes/_dark.json5
+++ /dev/null
@@ -1,90 +0,0 @@
-// ダークテーマのベーステーマ
-// このテーマが直接使われることは無い
-{
-	id: 'dark',
-
-	name: 'Dark',
-	author: 'syuilo',
-	desc: 'Default dark theme',
-	kind: 'dark',
-
-	props: {
-		accent: '#86b300',
-		accentDarken: ':darken<10<@accent',
-		accentLighten: ':lighten<10<@accent',
-		accentedBg: ':alpha<0.15<@accent',
-		focus: ':alpha<0.3<@accent',
-		bg: '#000',
-		acrylicBg: ':alpha<0.5<@bg',
-		fg: '#dadada',
-		fgTransparentWeak: ':alpha<0.75<@fg',
-		fgTransparent: ':alpha<0.5<@fg',
-		fgHighlighted: ':lighten<3<@fg',
-		fgOnAccent: '#fff',
-		divider: 'rgba(255, 255, 255, 0.1)',
-		indicator: '@accent',
-		panel: ':lighten<3<@bg',
-		panelHighlight: ':lighten<3<@panel',
-		panelHeaderBg: ':lighten<3<@panel',
-		panelHeaderFg: '@fg',
-		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
-		panelBorder: '" solid 1px var(--divider)',
-		acrylicPanel: ':alpha<0.5<@panel',
-		popup: ':lighten<3<@panel',
-		shadow: 'rgba(0, 0, 0, 0.3)',
-		header: ':alpha<0.7<@panel',
-		navBg: '@panel',
-		navFg: '@fg',
-		navHoverFg: ':lighten<17<@fg',
-		navActive: '@accent',
-		navIndicator: '@indicator',
-		link: '#44a4c1',
-		hashtag: '#ff9156',
-		mention: '@accent',
-		mentionMe: '@mention',
-		renote: '#229e82',
-		modalBg: 'rgba(0, 0, 0, 0.5)',
-		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
-		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
-		dateLabelFg: '@fg',
-		infoBg: '#253142',
-		infoFg: '#fff',
-		infoWarnBg: '#42321c',
-		infoWarnFg: '#ffbd3e',
-		switchBg: 'rgba(255, 255, 255, 0.15)',
-		cwBg: '#687390',
-		cwFg: '#393f4f',
-		cwHoverBg: '#707b97',
-		buttonBg: 'rgba(255, 255, 255, 0.05)',
-		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
-		buttonGradateA: '@accent',
-		buttonGradateB: ':hue<20<@accent',
-		inputBorder: 'rgba(255, 255, 255, 0.1)',
-		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
-		driveFolderBg: ':alpha<0.3<@accent',
-		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
-		badge: '#31b1ce',
-		messageBg: '@bg',
-		success: '#86b300',
-		error: '#ec4137',
-		warn: '#ecb637',
-		htmlThemeColor: '@bg',
-		X2: ':darken<2<@panel',
-		X3: 'rgba(255, 255, 255, 0.05)',
-		X4: 'rgba(255, 255, 255, 0.1)',
-		X5: 'rgba(255, 255, 255, 0.05)',
-		X6: 'rgba(255, 255, 255, 0.15)',
-		X7: 'rgba(255, 255, 255, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		X10: ':alpha<0.4<@accent',
-		X11: 'rgba(0, 0, 0, 0.3)',
-		X12: 'rgba(255, 255, 255, 0.1)',
-		X13: 'rgba(255, 255, 255, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
-		X17: ':alpha<0.8<@bg',
-	},
-}
diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5
deleted file mode 100644
index 251aa36c7a..0000000000
--- a/src/client/themes/_light.json5
+++ /dev/null
@@ -1,90 +0,0 @@
-// ライトテーマのベーステーマ
-// このテーマが直接使われることは無い
-{
-	id: 'light',
-
-	name: 'Light',
-	author: 'syuilo',
-	desc: 'Default light theme',
-	kind: 'light',
-
-	props: {
-		accent: '#86b300',
-		accentDarken: ':darken<10<@accent',
-		accentLighten: ':lighten<10<@accent',
-		accentedBg: ':alpha<0.15<@accent',
-		focus: ':alpha<0.3<@accent',
-		bg: '#fff',
-		acrylicBg: ':alpha<0.5<@bg',
-		fg: '#5f5f5f',
-		fgTransparentWeak: ':alpha<0.75<@fg',
-		fgTransparent: ':alpha<0.5<@fg',
-		fgHighlighted: ':darken<3<@fg',
-		fgOnAccent: '#fff',
-		divider: 'rgba(0, 0, 0, 0.1)',
-		indicator: '@accent',
-		panel: ':lighten<3<@bg',
-		panelHighlight: ':darken<3<@panel',
-		panelHeaderBg: ':lighten<3<@panel',
-		panelHeaderFg: '@fg',
-		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
-		panelBorder: '" solid 1px var(--divider)',
-		acrylicPanel: ':alpha<0.5<@panel',
-		popup: ':lighten<3<@panel',
-		shadow: 'rgba(0, 0, 0, 0.1)',
-		header: ':alpha<0.7<@panel',
-		navBg: '@panel',
-		navFg: '@fg',
-		navHoverFg: ':darken<17<@fg',
-		navActive: '@accent',
-		navIndicator: '@indicator',
-		link: '#44a4c1',
-		hashtag: '#ff9156',
-		mention: '@accent',
-		mentionMe: '@mention',
-		renote: '#229e82',
-		modalBg: 'rgba(0, 0, 0, 0.3)',
-		scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
-		scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
-		dateLabelFg: '@fg',
-		infoBg: '#e5f5ff',
-		infoFg: '#72818a',
-		infoWarnBg: '#fff0db',
-		infoWarnFg: '#8f6e31',
-		switchBg: 'rgba(0, 0, 0, 0.15)',
-		cwBg: '#b1b9c1',
-		cwFg: '#fff',
-		cwHoverBg: '#bbc4ce',
-		buttonBg: 'rgba(0, 0, 0, 0.05)',
-		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
-		buttonGradateA: '@accent',
-		buttonGradateB: ':hue<20<@accent',
-		inputBorder: 'rgba(0, 0, 0, 0.1)',
-		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
-		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
-		driveFolderBg: ':alpha<0.3<@accent',
-		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
-		badge: '#31b1ce',
-		messageBg: '@bg',
-		success: '#86b300',
-		error: '#ec4137',
-		warn: '#ecb637',
-		htmlThemeColor: '@bg',
-		X2: ':darken<2<@panel',
-		X3: 'rgba(0, 0, 0, 0.05)',
-		X4: 'rgba(0, 0, 0, 0.1)',
-		X5: 'rgba(0, 0, 0, 0.05)',
-		X6: 'rgba(0, 0, 0, 0.25)',
-		X7: 'rgba(0, 0, 0, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		X10: ':alpha<0.4<@accent',
-		X11: 'rgba(0, 0, 0, 0.1)',
-		X12: 'rgba(0, 0, 0, 0.1)',
-		X13: 'rgba(0, 0, 0, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
-		X17: ':alpha<0.8<@bg',
-	},
-}
diff --git a/src/client/themes/d-astro.json5 b/src/client/themes/d-astro.json5
deleted file mode 100644
index c6a927ec3a..0000000000
--- a/src/client/themes/d-astro.json5
+++ /dev/null
@@ -1,78 +0,0 @@
-{
-	id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
-	base: 'dark',
-	name: 'Mi Astro Dark',
-	author: 'syuilo',
-	props: {
-		bg: '#232125',
-		fg: '#efdab9',
-		cwBg: '#687390',
-		cwFg: '#393f4f',
-		link: '#78b0a0',
-		warn: '#ecb637',
-		badge: '#31b1ce',
-		error: '#ec4137',
-		focus: ':alpha<0.3<@accent',
-		navBg: '@panel',
-		navFg: '@fg',
-		panel: '#2a272b',
-		accent: '#81c08b',
-		header: ':alpha<0.7<@bg',
-		infoBg: '#253142',
-		infoFg: '#fff',
-		renote: '#659CC8',
-		shadow: 'rgba(0, 0, 0, 0.3)',
-		divider: 'rgba(255, 255, 255, 0.1)',
-		hashtag: '#ff9156',
-		mention: '#ffd152',
-		modalBg: 'rgba(0, 0, 0, 0.5)',
-		success: '#86b300',
-		buttonBg: 'rgba(255, 255, 255, 0.05)',
-		acrylicBg: ':alpha<0.5<@bg',
-		cwHoverBg: '#707b97',
-		indicator: '@accent',
-		mentionMe: '#fb5d38',
-		messageBg: '@bg',
-		navActive: '@accent',
-		infoWarnBg: '#42321c',
-		infoWarnFg: '#ffbd3e',
-		navHoverFg: ':lighten<17<@fg',
-		dateLabelFg: '@fg',
-		inputBorder: 'rgba(255, 255, 255, 0.1)',
-		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
-		panelBorder: '" solid 1px var(--divider)',
-		accentDarken: ':darken<10<@accent',
-		acrylicPanel: ':alpha<0.5<@panel',
-		navIndicator: '@accent',
-		accentLighten: ':lighten<10<@accent',
-		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
-		buttonGradateA: '@accent',
-		buttonGradateB: ':hue<-20<@accent',
-		driveFolderBg: ':alpha<0.3<@accent',
-		fgHighlighted: ':lighten<3<@fg',
-		panelHeaderBg: ':lighten<3<@panel',
-		panelHeaderFg: '@fg',
-		htmlThemeColor: '@bg',
-		panelHighlight: ':lighten<3<@panel',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
-		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
-		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
-		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
-		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
-		X2: ':darken<2<@panel',
-		X3: 'rgba(255, 255, 255, 0.05)',
-		X4: 'rgba(255, 255, 255, 0.1)',
-		X5: 'rgba(255, 255, 255, 0.05)',
-		X6: 'rgba(255, 255, 255, 0.15)',
-		X7: 'rgba(255, 255, 255, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		X10: ':alpha<0.4<@accent',
-		X11: 'rgba(0, 0, 0, 0.3)',
-		X12: 'rgba(255, 255, 255, 0.1)',
-		X13: 'rgba(255, 255, 255, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
-	},
-}
diff --git a/src/client/themes/d-black.json5 b/src/client/themes/d-black.json5
deleted file mode 100644
index 3c18ebdaf1..0000000000
--- a/src/client/themes/d-black.json5
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-	id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
-
-	name: 'Mi Black',
-	author: 'syuilo',
-
-	base: 'dark',
-
-	props: {
-		divider: '#2d2d2d',
-		panel: '#131313',
-		panelHeaderBg: '@panel',
-		panelHeaderDivider: '@divider',
-		shadow: 'rgba(255, 255, 255, 0.05)',
-		modalBg: 'rgba(255, 255, 255, 0.1)',
-	},
-}
diff --git a/src/client/themes/d-botanical.json5 b/src/client/themes/d-botanical.json5
deleted file mode 100644
index c03b95e2d7..0000000000
--- a/src/client/themes/d-botanical.json5
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-	id: '504debaf-4912-6a4c-5059-1db08a76b737',
-
-	name: 'Mi Botanical Dark',
-	author: 'syuilo',
-
-	base: 'dark',
-
-	props: {
-		accent: 'rgb(148, 179, 0)',
-		bg: 'rgb(37, 38, 36)',
-		fg: 'rgb(216, 212, 199)',
-		fgHighlighted: '#fff',
-		divider: 'rgba(255, 255, 255, 0.14)',
-		panel: 'rgb(47, 47, 44)',
-		panelHeaderBg: '@panel',
-		panelHeaderDivider: '@divider',
-		header: ':alpha<0.7<@panel',
-		navBg: '#363636',
-		renote: '@accent',
-		mention: 'rgb(212, 153, 76)',
-		mentionMe: 'rgb(212, 210, 76)',
-		hashtag: '#5bcbb0',
-		link: '@accent',
-	},
-}
diff --git a/src/client/themes/d-dark.json5 b/src/client/themes/d-dark.json5
deleted file mode 100644
index d24ce4df69..0000000000
--- a/src/client/themes/d-dark.json5
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-	id: '8050783a-7f63-445a-b270-36d0f6ba1677',
-
-	name: 'Mi Dark',
-	author: 'syuilo',
-	desc: 'Default light theme',
-
-	base: 'dark',
-
-	props: {
-		bg: '#232323',
-		fg: 'rgb(199, 209, 216)',
-		fgHighlighted: '#fff',
-		divider: 'rgba(255, 255, 255, 0.14)',
-		panel: '#2d2d2d',
-		panelHeaderBg: '@panel',
-		panelHeaderDivider: '@divider',
-		header: ':alpha<0.7<@panel',
-		navBg: '#363636',
-		renote: '@accent',
-		mention: '#da6d35',
-		mentionMe: '#d44c4c',
-		hashtag: '#4cb8d4',
-		link: '@accent',
-	},
-}
diff --git a/src/client/themes/d-future.json5 b/src/client/themes/d-future.json5
deleted file mode 100644
index b6fa1ab0c1..0000000000
--- a/src/client/themes/d-future.json5
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-	id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
-
-	name: 'Mi Future Dark',
-	author: 'syuilo',
-
-	base: 'dark',
-
-	props: {
-		accent: '#63e2b7',
-		bg: '#101014',
-		fg: '#D5D5D6',
-		fgHighlighted: '#fff',
-		fgOnAccent: '#000',
-		divider: 'rgba(255, 255, 255, 0.1)',
-		panel: '#18181c',
-		panelHeaderBg: '@panel',
-		panelHeaderDivider: '@divider',
-		renote: '@accent',
-		mention: '#f2c97d',
-		mentionMe: '@accent',
-		hashtag: '#70c0e8',
-		link: '#e88080',
-		buttonGradateA: '@accent',
-		buttonGradateB: ':saturate<30<:hue<30<@accent',
-	},
-}
diff --git a/src/client/themes/d-persimmon.json5 b/src/client/themes/d-persimmon.json5
deleted file mode 100644
index e36265ff10..0000000000
--- a/src/client/themes/d-persimmon.json5
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-	id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
-
-	name: 'Mi Persimmon Dark',
-	author: 'syuilo',
-
-	base: 'dark',
-
-	props: {
-		accent: 'rgb(206, 102, 65)',
-		bg: 'rgb(31, 33, 31)',
-		fg: '#cdd8c7',
-		fgHighlighted: '#fff',
-		divider: 'rgba(255, 255, 255, 0.14)',
-		panel: 'rgb(41, 43, 41)',
-		infoFg: '@fg',
-		infoBg: '#333c3b',
-		navBg: '#141714',
-		renote: '@accent',
-		mention: '@accent',
-		mentionMe: '#de6161',
-		hashtag: '#68bad0',
-		link: '#a1c758',
-	},
-}
diff --git a/src/client/themes/d-pumpkin.json5 b/src/client/themes/d-pumpkin.json5
deleted file mode 100644
index 064ca4577b..0000000000
--- a/src/client/themes/d-pumpkin.json5
+++ /dev/null
@@ -1,88 +0,0 @@
-{
-	id: '0b64fef3-02c7-20b5-dd87-b3f77e2b4301',
-
-	name: 'Mi Pumpkin Dark',
-	author: 'syuilo',
-
-	base: 'dark',
-
-	props: {
-		X2: ':darken<2<@panel',
-		X3: 'rgba(255, 255, 255, 0.05)',
-		X4: 'rgba(255, 255, 255, 0.1)',
-		X5: 'rgba(255, 255, 255, 0.05)',
-		X6: 'rgba(255, 255, 255, 0.15)',
-		X7: 'rgba(255, 255, 255, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		bg: 'rgb(37, 32, 47)',
-		fg: '#e0d5c0',
-		X10: ':alpha<0.4<@accent',
-		X11: 'rgba(0, 0, 0, 0.3)',
-		X12: 'rgba(255, 255, 255, 0.1)',
-		X13: 'rgba(255, 255, 255, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
-		X17: ':alpha<0.8<@bg',
-		cwBg: '#687390',
-		cwFg: '#393f4f',
-		link: 'rgb(172, 193, 68)',
-		warn: '#ecb637',
-		badge: '#31b1ce',
-		error: '#ec4137',
-		focus: ':alpha<0.3<@accent',
-		navBg: '@panel',
-		navFg: '@fg',
-		panel: ':lighten<3<@bg',
-		popup: ':lighten<3<@panel',
-		accent: 'rgb(242, 133, 36)',
-		header: ':alpha<0.7<@panel',
-		infoBg: '#253142',
-		infoFg: '#fff',
-		renote: 'rgb(110, 179, 72)',
-		shadow: 'rgba(0, 0, 0, 0.3)',
-		divider: 'rgba(255, 255, 255, 0.1)',
-		hashtag: 'rgb(188, 90, 255)',
-		mention: 'rgb(72, 179, 139)',
-		modalBg: 'rgba(0, 0, 0, 0.5)',
-		success: '#86b300',
-		buttonBg: 'rgba(255, 255, 255, 0.05)',
-		switchBg: 'rgba(255, 255, 255, 0.15)',
-		acrylicBg: ':alpha<0.5<@bg',
-		cwHoverBg: '#707b97',
-		indicator: '@accent',
-		mentionMe: '@accent',
-		messageBg: '@bg',
-		navActive: '@accent',
-		accentedBg: ':alpha<0.15<@accent',
-		fgOnAccent: '#000',
-		infoWarnBg: '#42321c',
-		infoWarnFg: '#ffbd3e',
-		navHoverFg: ':lighten<17<@fg',
-		dateLabelFg: '@fg',
-		inputBorder: 'rgba(255, 255, 255, 0.1)',
-		panelBorder: '" solid 1px var(--divider)',
-		accentDarken: ':darken<10<@accent',
-		acrylicPanel: ':alpha<0.5<@panel',
-		navIndicator: '@indicator',
-		accentLighten: ':lighten<10<@accent',
-		buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
-		driveFolderBg: ':alpha<0.3<@accent',
-		fgHighlighted: ':lighten<3<@fg',
-		fgTransparent: ':alpha<0.5<@fg',
-		panelHeaderBg: ':lighten<3<@panel',
-		panelHeaderFg: '@fg',
-		buttonGradateA: '@accent',
-		buttonGradateB: ':hue<20<@accent',
-		htmlThemeColor: '@bg',
-		panelHighlight: ':lighten<3<@panel',
-		listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
-		scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
-		inputBorderHover: 'rgba(255, 255, 255, 0.2)',
-		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
-		fgTransparentWeak: ':alpha<0.75<@fg',
-		panelHeaderDivider: 'rgba(0, 0, 0, 0)',
-		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
-	},
-}
diff --git a/src/client/themes/l-apricot.json5 b/src/client/themes/l-apricot.json5
deleted file mode 100644
index 1ed5525575..0000000000
--- a/src/client/themes/l-apricot.json5
+++ /dev/null
@@ -1,22 +0,0 @@
-{
-	id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
-
-	name: 'Mi Apricot Light',
-	author: 'syuilo',
-
-	base: 'light',
-
-	props: {
-		accent: 'rgb(234, 154, 82)',
-		bg: '#e6e5e2',
-		fg: 'rgb(149, 143, 139)',
-		panel: '#EEECE8',
-		renote: '@accent',
-		link: '@accent',
-		mention: '@accent',
-		hashtag: '@accent',
-		inputBorder: 'rgba(0, 0, 0, 0.1)',
-		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
-		infoBg: 'rgb(226, 235, 241)',
-	},
-}
diff --git a/src/client/themes/l-light.json5 b/src/client/themes/l-light.json5
deleted file mode 100644
index 248355c945..0000000000
--- a/src/client/themes/l-light.json5
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-	id: '4eea646f-7afa-4645-83e9-83af0333cd37',
-
-	name: 'Mi Light',
-	author: 'syuilo',
-	desc: 'Default light theme',
-
-	base: 'light',
-
-	props: {
-		bg: '#f9f9f9',
-		fg: '#676767',
-		divider: '#e8e8e8',
-		header: ':alpha<0.7<@panel',
-		navBg: '#fff',
-		panel: '#fff',
-		panelHeaderDivider: '@divider',
-		mentionMe: 'rgb(0, 179, 70)',
-	},
-}
diff --git a/src/client/themes/l-rainy.json5 b/src/client/themes/l-rainy.json5
deleted file mode 100644
index 283dd74c6c..0000000000
--- a/src/client/themes/l-rainy.json5
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-	id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
-
-	name: 'Mi Rainy Light',
-	author: 'syuilo',
-
-	base: 'light',
-
-	props: {
-		accent: '#5db0da',
-		bg: 'rgb(246 248 249)',
-		fg: '#636b71',
-		panel: '#fff',
-		divider: 'rgb(230 233 234)',
-		panelHeaderDivider: '@divider',
-		renote: '@accent',
-		link: '@accent',
-		mention: '@accent',
-		hashtag: '@accent',
-	},
-}
diff --git a/src/client/themes/l-sushi.json5 b/src/client/themes/l-sushi.json5
deleted file mode 100644
index 5846927d65..0000000000
--- a/src/client/themes/l-sushi.json5
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-	id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c',
-
-	name: 'Mi Sushi Light',
-	author: 'syuilo',
-
-	base: 'light',
-
-	props: {
-		accent: '#e36749',
-		bg: '#f0eee9',
-		fg: '#5f5f5f',
-		renote: '@accent',
-		link: '@accent',
-		mention: '@accent',
-		hashtag: '#229e82',
-	},
-}
diff --git a/src/client/themes/l-vivid.json5 b/src/client/themes/l-vivid.json5
deleted file mode 100644
index b3c08f38ae..0000000000
--- a/src/client/themes/l-vivid.json5
+++ /dev/null
@@ -1,82 +0,0 @@
-{
-	id: '6128c2a9-5c54-43fe-a47d-17942356470b',
-
-	name: 'Mi Vivid Light',
-	author: 'syuilo',
-
-	base: 'light',
-
-	props: {
-		bg: '#fafafa',
-		fg: '#444',
-		cwBg: '#b1b9c1',
-		cwFg: '#fff',
-		link: '#ff9400',
-		warn: '#ecb637',
-		badge: '#31b1ce',
-		error: '#ec4137',
-		focus: ':alpha<0.3<@accent',
-		navBg: '@panel',
-		navFg: '@fg',
-		panel: '#fff',
-		accent: '#008cff',
-		header: ':alpha<0.7<@panel',
-		infoBg: '#e5f5ff',
-		infoFg: '#72818a',
-		renote: '@accent',
-		shadow: 'rgba(0, 0, 0, 0.1)',
-		divider: 'rgba(0, 0, 0, 0.08)',
-		hashtag: '#92d400',
-		mention: '@accent',
-		modalBg: 'rgba(0, 0, 0, 0.3)',
-		success: '#86b300',
-		buttonBg: 'rgba(0, 0, 0, 0.05)',
-		acrylicBg: ':alpha<0.5<@bg',
-		cwHoverBg: '#bbc4ce',
-		indicator: '@accent',
-		mentionMe: '@mention',
-		messageBg: '@bg',
-		navActive: '@accent',
-		infoWarnBg: '#fff0db',
-		infoWarnFg: '#8f6e31',
-		navHoverFg: ':darken<17<@fg',
-		dateLabelFg: '@fg',
-		inputBorder: 'rgba(0, 0, 0, 0.1)',
-		inputBorderHover: 'rgba(0, 0, 0, 0.2)',
-		panelBorder: '" solid 1px var(--divider)',
-		accentDarken: ':darken<10<@accent',
-		acrylicPanel: ':alpha<0.5<@panel',
-		navIndicator: '@accent',
-		accentLighten: ':lighten<10<@accent',
-		buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
-		driveFolderBg: ':alpha<0.3<@accent',
-		fgHighlighted: ':darken<3<@fg',
-		fgTransparent: ':alpha<0.5<@fg',
-		panelHeaderBg: ':lighten<3<@panel',
-		panelHeaderFg: '@fg',
-		htmlThemeColor: '@bg',
-		panelHighlight: ':darken<3<@panel',
-		listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
-		scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
-		wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
-		fgTransparentWeak: ':alpha<0.75<@fg',
-		panelHeaderDivider: '@divider',
-		scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
-		X2: ':darken<2<@panel',
-		X3: 'rgba(0, 0, 0, 0.05)',
-		X4: 'rgba(0, 0, 0, 0.1)',
-		X5: 'rgba(0, 0, 0, 0.05)',
-		X6: 'rgba(0, 0, 0, 0.25)',
-		X7: 'rgba(0, 0, 0, 0.05)',
-		X8: ':lighten<5<@accent',
-		X9: ':darken<5<@accent',
-		X10: ':alpha<0.4<@accent',
-		X11: 'rgba(0, 0, 0, 0.1)',
-		X12: 'rgba(0, 0, 0, 0.1)',
-		X13: 'rgba(0, 0, 0, 0.15)',
-		X14: ':alpha<0.5<@navBg',
-		X15: ':alpha<0<@panel',
-		X16: ':alpha<0.7<@panel',
-		X17: ':alpha<0.8<@bg',
-	},
-}
diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json
deleted file mode 100644
index 7a26047ddf..0000000000
--- a/src/client/tsconfig.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
-	"compilerOptions": {
-		"allowJs": true,
-		"noEmitOnError": false,
-		"noImplicitAny": false,
-		"noImplicitReturns": true,
-		"noUnusedParameters": false,
-		"noUnusedLocals": true,
-		"noFallthroughCasesInSwitch": true,
-		"declaration": false,
-		"sourceMap": false,
-		"target": "es2017",
-		"module": "esnext",
-		"moduleResolution": "node",
-		"removeComments": false,
-		"noLib": false,
-		"strict": true,
-		"strictNullChecks": true,
-		"experimentalDecorators": true,
-		"resolveJsonModule": true,
-		"baseUrl": ".",
-		"paths": {
-			"@/*": ["../*"],
-			"@client/*": ["./*"],
-			"@lib/*": ["../../lib/*"],
-		},
-		"typeRoots": [
-			"node_modules/@types",
-			"src/@types",
-			"src/client/@types"
-		],
-		"lib": [
-			"esnext",
-			"dom",
-			"webworker"
-		]
-	},
-	"compileOnSave": false,
-	"include": [
-		"./**/*.ts"
-	]
-}
diff --git a/src/client/ui/_common_/common.vue b/src/client/ui/_common_/common.vue
deleted file mode 100644
index 8da19a0984..0000000000
--- a/src/client/ui/_common_/common.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<template>
-<component v-for="popup in popups"
-	:key="popup.id"
-	:is="popup.component"
-	v-bind="popup.props"
-	v-on="popup.events"
-/>
-
-<XUpload v-if="uploads.length > 0"/>
-
-<XStreamIndicator/>
-
-<div id="wait" v-if="pendingApiRequestsCount > 0"></div>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@client/os';
-import * as sound from '@client/scripts/sound';
-import { $i } from '@client/account';
-
-export default defineComponent({
-	components: {
-		XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
-		XUpload: defineAsyncComponent(() => import('./upload.vue')),
-	},
-
-	setup() {
-		const onNotification = notification => {
-			if ($i.mutingNotificationTypes.includes(notification.type)) return;
-
-			if (document.visibilityState === 'visible') {
-				stream.send('readNotification', {
-					id: notification.id
-				});
-
-				popup(import('@client/components/toast.vue'), {
-					notification
-				}, {}, 'closed');
-			}
-
-			sound.play('notification');
-		};
-
-		if ($i) {
-			const connection = stream.useChannel('main', null, 'UI');
-			connection.on('notification', onNotification);
-		}
-
-		return {
-			uploads,
-			popups,
-			pendingApiRequestsCount,
-		};
-	},
-});
-</script>
-
-<style lang="scss">
-#wait {
-	display: block;
-	position: fixed;
-	z-index: 10000;
-	top: 15px;
-	right: 15px;
-
-	&:before {
-		content: "";
-		display: block;
-		width: 18px;
-		height: 18px;
-		box-sizing: border-box;
-		border: solid 2px transparent;
-		border-top-color: var(--accent);
-		border-left-color: var(--accent);
-		border-radius: 50%;
-		animation: progress-spinner 400ms linear infinite;
-	}
-}
-
-@keyframes progress-spinner {
-	0% {
-		transform: rotate(0deg);
-	}
-	100% {
-		transform: rotate(360deg);
-	}
-}
-</style>
diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue
deleted file mode 100644
index cd78b6ae46..0000000000
--- a/src/client/ui/_common_/sidebar.vue
+++ /dev/null
@@ -1,388 +0,0 @@
-<template>
-<div class="mvcprjjd">
-	<transition name="nav-back">
-		<div class="nav-back _modalBg"
-			v-if="showing"
-			@click="showing = false"
-			@touchstart.passive="showing = false"
-		></div>
-	</transition>
-
-	<transition name="nav">
-		<nav class="nav" :class="{ iconOnly, hidden }" v-show="showing">
-			<div>
-				<button class="item _button account" @click="openAccountMenu" v-click-anime>
-					<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
-				</button>
-				<MkA class="item index" active-class="active" to="/" exact v-click-anime>
-					<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
-				</MkA>
-				<template v-for="item in menu">
-					<div v-if="item === '-'" class="divider"></div>
-					<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime>
-						<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
-						<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
-					</component>
-				</template>
-				<div class="divider"></div>
-				<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" v-click-anime>
-					<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
-				</MkA>
-				<button class="item _button" @click="more" v-click-anime>
-					<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
-					<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
-				</button>
-				<MkA class="item" active-class="active" to="/settings" v-click-anime>
-					<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
-				</MkA>
-				<button class="item _button post" @click="post" data-cy-open-post-form>
-					<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
-				</button>
-			</div>
-		</nav>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { host } from '@client/config';
-import { search } from '@client/scripts/search';
-import * as os from '@client/os';
-import { menuDef } from '@client/menu';
-import { openAccountMenu } from '@client/account';
-
-export default defineComponent({
-	props: {
-		defaultHidden: {
-			type: Boolean,
-			required: false,
-			default: false,
-		}
-	},
-
-	data() {
-		return {
-			host: host,
-			showing: false,
-			accounts: [],
-			connection: null,
-			menuDef: menuDef,
-			iconOnly: false,
-			hidden: this.defaultHidden,
-		};
-	},
-
-	computed: {
-		menu(): string[] {
-			return this.$store.state.menu;
-		},
-
-		otherNavItemIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (this.menu.includes(def)) continue;
-				if (this.menuDef[def].indicated) return true;
-			}
-			return false;
-		},
-	},
-
-	watch: {
-		$route(to, from) {
-			this.showing = false;
-		},
-
-		'$store.reactiveState.menuDisplay.value'() {
-			this.calcViewState();
-		},
-
-		iconOnly() {
-			this.$nextTick(() => {
-				this.$emit('change-view-mode');
-			});
-		},
-
-		hidden() {
-			this.$nextTick(() => {
-				this.$emit('change-view-mode');
-			});
-		}
-	},
-
-	created() {
-		window.addEventListener('resize', this.calcViewState);
-		this.calcViewState();
-	},
-
-	methods: {
-		calcViewState() {
-			this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.menuDisplay === 'sideIcon');
-			if (!this.defaultHidden) {
-				this.hidden = (window.innerWidth <= 650);
-			}
-		},
-
-		show() {
-			this.showing = true;
-		},
-
-		post() {
-			os.post();
-		},
-
-		search() {
-			search();
-		},
-
-		more(ev) {
-			os.popup(import('@client/components/launch-pad.vue'), {}, {
-			}, 'closed');
-		},
-
-		openAccountMenu,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.nav-enter-active,
-.nav-leave-active {
-	opacity: 1;
-	transform: translateX(0);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.nav-enter-from,
-.nav-leave-active {
-	opacity: 0;
-	transform: translateX(-240px);
-}
-
-.nav-back-enter-active,
-.nav-back-leave-active {
-	opacity: 1;
-	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.nav-back-enter-from,
-.nav-back-leave-active {
-	opacity: 0;
-}
-
-.mvcprjjd {
-	$ui-font-size: 1em; // TODO: どこかに集約したい
-	$nav-width: 250px;
-	$nav-icon-only-width: 86px;
-
-	> .nav-back {
-		z-index: 1001;
-	}
-
-	> .nav {
-		$avatar-size: 32px;
-		$avatar-margin: 8px;
-
-		flex: 0 0 $nav-width;
-		width: $nav-width;
-		box-sizing: border-box;
-
-		&.iconOnly {
-			flex: 0 0 $nav-icon-only-width;
-			width: $nav-icon-only-width;
-
-			&:not(.hidden) {
-				> div {
-					width: $nav-icon-only-width;
-
-					> .divider {
-						margin: 8px auto;
-						width: calc(100% - 32px);
-					}
-
-					> .item {
-						padding-left: 0;
-						padding: 18px 0;
-						width: 100%;
-						text-align: center;
-						font-size: $ui-font-size * 1.1;
-						line-height: initial;
-
-						> i,
-						> .avatar {
-							display: block;
-							margin: 0 auto;
-						}
-
-						> i {
-							opacity: 0.7;
-						}
-
-						> .text {
-							display: none;
-						}
-
-						&:hover, &.active {
-							> i, > .text {
-								opacity: 1;
-							}
-						}
-
-						&:first-child {
-							margin-bottom: 8px;
-						}
-
-						&:last-child {
-							margin-top: 8px;
-						}
-					}
-				}
-			}
-		}
-
-		&.hidden {
-			position: fixed;
-			top: 0;
-			left: 0;
-			z-index: 1001;
-		}
-
-		&:not(.hidden) {
-			display: block !important;
-		}
-
-		> div {
-			position: fixed;
-			top: 0;
-			left: 0;
-			z-index: 1001;
-			width: $nav-width;
-			// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-			height: calc(var(--vh, 1vh) * 100);
-			box-sizing: border-box;
-			overflow: auto;
-			overflow-x: clip;
-			background: var(--navBg);
-
-			> .divider {
-				margin: 16px 16px;
-				border-top: solid 0.5px var(--divider);
-			}
-
-			> .item {
-				position: relative;
-				display: block;
-				padding-left: 24px;
-				font-size: $ui-font-size;
-				line-height: 2.85rem;
-				text-overflow: ellipsis;
-				overflow: hidden;
-				white-space: nowrap;
-				width: 100%;
-				text-align: left;
-				box-sizing: border-box;
-				color: var(--navFg);
-
-				> i {
-					position: relative;
-					width: 32px;
-				}
-
-				> i,
-				> .avatar {
-					margin-right: $avatar-margin;
-				}
-
-				> .avatar {
-					width: $avatar-size;
-					height: $avatar-size;
-					vertical-align: middle;
-				}
-
-				> .indicator {
-					position: absolute;
-					top: 0;
-					left: 20px;
-					color: var(--navIndicator);
-					font-size: 8px;
-					animation: blink 1s infinite;
-				}
-
-				> .text {
-					position: relative;
-					font-size: 0.9em;
-				}
-
-				&:hover {
-					text-decoration: none;
-					color: var(--navHoverFg);
-				}
-
-				&.active {
-					color: var(--navActive);
-				}
-
-				&:hover, &.active {
-					&:before {
-						content: "";
-						display: block;
-						width: calc(100% - 24px);
-						height: 100%;
-						margin: auto;
-						position: absolute;
-						top: 0;
-						left: 0;
-						right: 0;
-						bottom: 0;
-						border-radius: 999px;
-						background: var(--accentedBg);
-					}
-				}
-
-				&:first-child, &:last-child {
-					position: sticky;
-					z-index: 1;
-					padding-top: 8px;
-					padding-bottom: 8px;
-					background: var(--X14);
-					-webkit-backdrop-filter: var(--blur, blur(8px));
-					backdrop-filter: var(--blur, blur(8px));
-				}
-
-				&:first-child {
-					top: 0;
-
-					&:hover, &.active {
-						&:before {
-							content: none;
-						}
-					}
-				}
-
-				&:last-child {
-					bottom: 0;
-					color: var(--fgOnAccent);
-
-					&:before {
-						content: "";
-						display: block;
-						width: calc(100% - 20px);
-						height: calc(100% - 20px);
-						margin: auto;
-						position: absolute;
-						top: 0;
-						left: 0;
-						right: 0;
-						bottom: 0;
-						border-radius: 999px;
-						background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
-					}
-					
-					&:hover, &.active {
-						&:before {
-							background: var(--accentLighten);
-						}
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/_common_/stream-indicator.vue b/src/client/ui/_common_/stream-indicator.vue
deleted file mode 100644
index 23f2357d85..0000000000
--- a/src/client/ui/_common_/stream-indicator.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<div class="nsbbhtug" v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" @click="resetDisconnected">
-	<div>{{ $ts.disconnectedFromServer }}</div>
-	<div class="command">
-		<button class="_textButton" @click="reload">{{ $ts.reload }}</button>
-		<button class="_textButton">{{ $ts.doNothing }}</button>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	data() {
-		return {
-			hasDisconnected: false,
-		}
-	},
-	computed: {
-		stream() {
-			return os.stream;
-		},
-	},
-	created() {
-		os.stream.on('_disconnected_', this.onDisconnected);
-	},
-	beforeUnmount() {
-		os.stream.off('_disconnected_', this.onDisconnected);
-	},
-	methods: {
-		onDisconnected() {
-			this.hasDisconnected = true;
-		},
-		resetDisconnected() {
-			this.hasDisconnected = false;
-		},
-		reload() {
-			location.reload();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.nsbbhtug {
-	position: fixed;
-	z-index: 16385;
-	bottom: 8px;
-	right: 8px;
-	margin: 0;
-	padding: 6px 12px;
-	font-size: 0.9em;
-	color: #fff;
-	background: #000;
-	opacity: 0.8;
-	border-radius: 4px;
-	max-width: 320px;
-
-	> .command {
-		display: flex;
-		justify-content: space-around;
-
-		> button {
-			padding: 0.7em;
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/_common_/upload.vue b/src/client/ui/_common_/upload.vue
deleted file mode 100644
index 25a807cd36..0000000000
--- a/src/client/ui/_common_/upload.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<template>
-<div class="mk-uploader _acrylic">
-	<ol v-if="uploads.length > 0">
-		<li v-for="ctx in uploads" :key="ctx.id">
-			<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
-			<div class="top">
-				<p class="name"><i class="fas fa-spinner fa-pulse"></i>{{ ctx.name }}</p>
-				<p class="status">
-					<span class="initing" v-if="ctx.progressValue === undefined">{{ $ts.waiting }}<MkEllipsis/></span>
-					<span class="kb" v-if="ctx.progressValue !== undefined">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
-					<span class="percentage" v-if="ctx.progressValue !== undefined">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
-				</p>
-			</div>
-			<progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === undefined, waiting: ctx.progressValue !== undefined && ctx.progressValue === ctx.progressMax }"></progress>
-		</li>
-	</ol>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	data() {
-		return {
-			uploads: os.uploads,
-		};
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-uploader {
-	position: fixed;
-	z-index: 10000;
-	right: 16px;
-	width: 260px;
-	top: 32px;
-	padding: 16px 20px;
-	pointer-events: none;
-	box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
-	border-radius: 8px;
-}
-.mk-uploader:empty {
-  display: none;
-}
-.mk-uploader > ol {
-  display: block;
-  margin: 0;
-  padding: 0;
-  list-style: none;
-}
-.mk-uploader > ol > li {
-  display: grid;
-  margin: 8px 0 0 0;
-  padding: 0;
-  height: 36px;
-  width: 100%;
-  border-top: solid 8px transparent;
-  grid-template-columns: 36px calc(100% - 44px);
-  grid-template-rows: 1fr 8px;
-  column-gap: 8px;
-  box-sizing: content-box;
-}
-.mk-uploader > ol > li:first-child {
-  margin: 0;
-  box-shadow: none;
-  border-top: none;
-}
-.mk-uploader > ol > li > .img {
-  display: block;
-  background-size: cover;
-  background-position: center center;
-  grid-column: 1/2;
-  grid-row: 1/3;
-}
-.mk-uploader > ol > li > .top {
-  display: flex;
-  grid-column: 2/3;
-  grid-row: 1/2;
-}
-.mk-uploader > ol > li > .top > .name {
-  display: block;
-  padding: 0 8px 0 0;
-  margin: 0;
-  font-size: 0.8em;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  flex-shrink: 1;
-}
-.mk-uploader > ol > li > .top > .name > i {
-  margin-right: 4px;
-}
-.mk-uploader > ol > li > .top > .status {
-  display: block;
-  margin: 0 0 0 auto;
-  padding: 0;
-  font-size: 0.8em;
-  flex-shrink: 0;
-}
-.mk-uploader > ol > li > .top > .status > .initing {
-}
-.mk-uploader > ol > li > .top > .status > .kb {
-}
-.mk-uploader > ol > li > .top > .status > .percentage {
-  display: inline-block;
-  width: 48px;
-  text-align: right;
-}
-.mk-uploader > ol > li > .top > .status > .percentage:after {
-  content: '%';
-}
-.mk-uploader > ol > li > progress {
-  display: block;
-  background: transparent;
-  border: none;
-  border-radius: 4px;
-  overflow: hidden;
-  grid-column: 2/3;
-  grid-row: 2/3;
-  z-index: 2;
-	width: 100%;
-	height: 8px;
-}
-.mk-uploader > ol > li > progress::-webkit-progress-value {
-  background: var(--accent);
-}
-.mk-uploader > ol > li > progress::-webkit-progress-bar {
-  //background: var(--accentAlpha01);
-	background: transparent;
-}
-</style>
diff --git a/src/client/ui/chat/date-separated-list.vue b/src/client/ui/chat/date-separated-list.vue
deleted file mode 100644
index 12638cd230..0000000000
--- a/src/client/ui/chat/date-separated-list.vue
+++ /dev/null
@@ -1,163 +0,0 @@
-<script lang="ts">
-import { defineComponent, h, PropType, TransitionGroup } from 'vue';
-import MkAd from '@client/components/global/ad.vue';
-
-export default defineComponent({
-	props: {
-		items: {
-			type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
-			required: true,
-		},
-		reversed: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		ad: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	methods: {
-		focus() {
-			this.$slots.default[0].elm.focus();
-		}
-	},
-
-	render() {
-		const getDateText = (time: string) => {
-			const date = new Date(time).getDate();
-			const month = new Date(time).getMonth() + 1;
-			return this.$t('monthAndDay', {
-				month: month.toString(),
-				day: date.toString()
-			});
-		}
-
-		return h(this.reversed ? 'div' : TransitionGroup, {
-			class: 'hmjzthxl',
-			name: this.reversed ? 'list-reversed' : 'list',
-			tag: 'div',
-		}, this.items.map((item, i) => {
-			const el = this.$slots.default({
-				item: item
-			})[0];
-			if (el.key == null && item.id) el.key = item.id;
-
-			if (
-				i != this.items.length - 1 &&
-				new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
-			) {
-				const separator = h('div', {
-					class: 'separator',
-					key: item.id + ':separator',
-				}, h('p', {
-					class: 'date'
-				}, [
-					h('span', [
-						h('i', {
-							class: 'fas fa-angle-up icon',
-						}),
-						getDateText(item.createdAt)
-					]),
-					h('span', [
-						getDateText(this.items[i + 1].createdAt),
-						h('i', {
-							class: 'fas fa-angle-down icon',
-						})
-					])
-				]));
-
-				return [el, separator];
-			} else {
-				if (this.ad && item._shouldInsertAd_) {
-					return [h(MkAd, {
-						class: 'a', // advertiseの意(ブロッカー対策)
-						key: item.id + ':ad',
-						prefer: ['horizontal', 'horizontal-big'],
-					}), el];
-				} else {
-					return el;
-				}
-			}
-		}));
-	},
-});
-</script>
-
-<style lang="scss">
-.hmjzthxl {
-	> .list-move {
-		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
-	}
-	> .list-enter-active {
-		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
-	}
-	> .list-enter-from {
-		opacity: 0;
-		transform: translateY(-64px);
-	}
-
-	> .list-reversed-enter-active, > .list-reversed-leave-active {
-		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
-	}
-	> .list-reversed-enter-from {
-		opacity: 0;
-		transform: translateY(64px);
-	}
-}
-</style>
-
-<style lang="scss">
-.hmjzthxl {
-	> .separator {
-		text-align: center;
-		position: relative;
-
-		&:before {
-			content: "";
-			display: block;
-			position: absolute;
-			top: 50%;
-			left: 0;
-			right: 0;
-			margin: auto;
-			width: calc(100% - 32px);
-			height: 1px;
-			background: var(--divider);
-		}
-
-		> .date {
-			display: inline-block;
-			position: relative;
-			margin: 0;
-			padding: 0 16px;
-			line-height: 32px;
-			text-align: center;
-			font-size: 12px;
-			color: var(--dateLabelFg);
-			background: var(--panel);
-
-			> span {
-				&:first-child {
-					margin-right: 8px;
-
-					> .icon {
-						margin-right: 8px;
-					}
-				}
-
-				&:last-child {
-					margin-left: 8px;
-
-					> .icon {
-						margin-left: 8px;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/chat/header-clock.vue b/src/client/ui/chat/header-clock.vue
deleted file mode 100644
index 69ec3cb64b..0000000000
--- a/src/client/ui/chat/header-clock.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="acemodlh _monospace">
-	<div>
-		<span v-text="y"></span>/<span v-text="m"></span>/<span v-text="d"></span>
-	</div>
-	<div>
-		<span v-text="hh"></span>
-		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
-		<span v-text="mm"></span>
-		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
-		<span v-text="ss"></span>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	data() {
-		return {
-			clock: null,
-			y: null,
-			m: null,
-			d: null,
-			hh: null,
-			mm: null,
-			ss: null,
-			showColon: true,
-		};
-	},
-	created() {
-		this.tick();
-		this.clock = setInterval(this.tick, 1000);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		tick() {
-			const now = new Date();
-			this.y = now.getFullYear().toString();
-			this.m = (now.getMonth() + 1).toString().padStart(2, '0');
-			this.d = now.getDate().toString().padStart(2, '0');
-			this.hh = now.getHours().toString().padStart(2, '0');
-			this.mm = now.getMinutes().toString().padStart(2, '0');
-			this.ss = now.getSeconds().toString().padStart(2, '0');
-			this.showColon = now.getSeconds() % 2 === 0;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.acemodlh {
-	opacity: 0.7;
-	font-size: 0.85em;
-	line-height: 1em;
-	text-align: center;
-}
-</style>
diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue
deleted file mode 100644
index 4c068b0d94..0000000000
--- a/src/client/ui/chat/index.vue
+++ /dev/null
@@ -1,467 +0,0 @@
-<template>
-<div class="mk-app" @contextmenu.self.prevent="onContextmenu">
-	<XSidebar ref="menu" class="menu" :default-hidden="true"/>
-
-	<div class="nav">
-		<header class="header">
-			<div class="left">
-				<button class="_button account" @click="openAccountMenu">
-					<MkAvatar :user="$i" class="avatar"/><!--<MkAcct class="text" :user="$i"/>-->
-				</button>
-			</div>
-			<div class="right">
-				<MkA class="item" to="/my/messaging" v-tooltip="$ts.messaging"><i class="fas fa-comments icon"></i><span v-if="$i.hasUnreadMessagingMessage" class="indicator"><i class="fas fa-circle"></i></span></MkA>
-				<MkA class="item" to="/my/messages" v-tooltip="$ts.directNotes"><i class="fas fa-envelope icon"></i><span v-if="$i.hasUnreadSpecifiedNotes" class="indicator"><i class="fas fa-circle"></i></span></MkA>
-				<MkA class="item" to="/my/mentions" v-tooltip="$ts.mentions"><i class="fas fa-at icon"></i><span v-if="$i.hasUnreadMentions" class="indicator"><i class="fas fa-circle"></i></span></MkA>
-				<MkA class="item" to="/my/notifications" v-tooltip="$ts.notifications"><i class="fas fa-bell icon"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></MkA>
-			</div>
-		</header>
-		<div class="body">
-			<div class="container">
-				<div class="header">{{ $ts.timeline }}</div>
-				<div class="body">
-					<MkA to="/timeline/home" class="item" :class="{ active: tl === 'home' }"><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA>
-					<MkA to="/timeline/local" class="item" :class="{ active: tl === 'local' }"><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA>
-					<MkA to="/timeline/social" class="item" :class="{ active: tl === 'social' }"><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA>
-					<MkA to="/timeline/global" class="item" :class="{ active: tl === 'global' }"><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA>
-				</div>
-			</div>
-			<div class="container" v-if="followedChannels">
-				<div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
-				<div class="body">
-					<MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
-				</div>
-			</div>
-			<div class="container" v-if="featuredChannels">
-				<div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
-				<div class="body">
-					<MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
-				</div>
-			</div>
-			<div class="container" v-if="lists">
-				<div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><i class="fas fa-plus"></i></button></div>
-				<div class="body">
-					<MkA v-for="list in lists" :key="list.id" :to="`/my/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA>
-				</div>
-			</div>
-			<div class="container" v-if="antennas">
-				<div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><i class="fas fa-plus"></i></button></div>
-				<div class="body">
-					<MkA v-for="antenna in antennas" :key="antenna.id" :to="`/my/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA>
-				</div>
-			</div>
-			<div class="container">
-				<div class="body">
-					<MkA to="/my/favorites" class="item"><i class="fas fa-star icon"></i>{{ $ts.favorites }}</MkA>
-				</div>
-			</div>
-			<MkAd class="a" :prefer="['square']"/>
-		</div>
-		<footer class="footer">
-			<div class="left">
-				<button class="_button menu" @click="showMenu">
-					<i class="fas fa-bars icon"></i>
-				</button>
-			</div>
-			<div class="right">
-				<button class="_button item search" @click="search" v-tooltip="$ts.search">
-					<i class="fas fa-search icon"></i>
-				</button>
-				<MkA class="item" to="/settings" v-tooltip="$ts.settings"><i class="fas fa-cog icon"></i></MkA>
-			</div>
-		</footer>
-	</div>
-
-	<main class="main" @contextmenu.stop="onContextmenu">
-		<header class="header">
-			<MkHeader class="header" :info="pageInfo" :menu="menu" :center="false" @click="onHeaderClick"/>
-		</header>
-		<router-view v-slot="{ Component }">
-			<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-				<keep-alive :include="['timeline']">
-					<component :is="Component" :ref="changePage" class="body"/>
-				</keep-alive>
-			</transition>
-		</router-view>
-	</main>
-
-	<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
-	<div class="side widgets" :class="{ sideViewOpening }">
-		<XWidgets/>
-	</div>
-
-	<XCommon/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { instanceName, url } from '@client/config';
-import XSidebar from '@client/ui/_common_/sidebar.vue';
-import XWidgets from './widgets.vue';
-import XCommon from '../_common_/common.vue';
-import XSide from './side.vue';
-import XHeaderClock from './header-clock.vue';
-import * as os from '@client/os';
-import { router } from '@client/router';
-import { menuDef } from '@client/menu';
-import { search } from '@client/scripts/search';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { store } from './store';
-import * as symbols from '@client/symbols';
-import { openAccountMenu } from '@client/account';
-
-export default defineComponent({
-	components: {
-		XCommon,
-		XSidebar,
-		XWidgets,
-		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
-		XHeaderClock,
-	},
-
-	provide() {
-		return {
-			sideViewHook: (path) => {
-				this.$refs.side.navigate(path);
-			}
-		};
-	},
-
-	data() {
-		return {
-			pageInfo: null,
-			lists: null,
-			antennas: null,
-			followedChannels: null,
-			featuredChannels: null,
-			currentChannel: null,
-			menuDef: menuDef,
-			sideViewOpening: false,
-			instanceName,
-		};
-	},
-
-	computed: {
-		menu() {
-			return [{
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.$refs.side.navigate(this.$route.path);
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(this.$route.path);
-				}
-			}];
-		}
-	},
-
-	created() {
-		if (window.innerWidth < 1024) {
-			localStorage.setItem('ui', 'default');
-			location.reload();
-		}
-
-		os.api('users/lists/list').then(lists => {
-			this.lists = lists;
-		});
-
-		os.api('antennas/list').then(antennas => {
-			this.antennas = antennas;
-		});
-
-		os.api('channels/followed', { limit: 20 }).then(channels => {
-			this.followedChannels = channels;
-		});
-
-		// TODO: pagination
-		os.api('channels/featured', { limit: 20 }).then(channels => {
-			this.featuredChannels = channels;
-		});
-	},
-
-	methods: {
-		changePage(page) {
-			console.log(page);
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-				document.title = `${this.pageInfo.title} | ${instanceName}`;
-			}
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-
-		showMenu() {
-			this.$refs.menu.show();
-		},
-
-		post() {
-			os.post();
-		},
-
-		search() {
-			search();
-		},
-
-		back() {
-			history.back();
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-
-		onHeaderClick() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
-			if (window.getSelection().toString() !== '') return;
-			const path = this.$route.path;
-			os.contextMenu([{
-				type: 'label',
-				text: path,
-			}, {
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.$refs.side.navigate(path);
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(path);
-				}
-			}], e);
-		},
-
-		openAccountMenu,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-app {
-	$header-height: 54px; // TODO: どこかに集約したい
-	$ui-font-size: 1em; // TODO: どこかに集約したい
-
-	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-	height: calc(var(--vh, 1vh) * 100);
-	display: flex;
-
-	> .nav {
-		display: flex;
-		flex-direction: column;
-		width: 250px;
-		height: 100vh;
-		border-right: solid 4px var(--divider);
-
-		> .header, > .footer {
-			$padding: 8px;
-			display: flex;
-			align-items: center;
-			z-index: 1000;
-			height: $header-height;
-			padding: $padding;
-			box-sizing: border-box;
-			user-select: none;
-
-			&.header {
-				border-bottom: solid 0.5px var(--divider);
-			}
-
-			&.footer {
-				border-top: solid 0.5px var(--divider);
-			}
-
-			> .left, > .right {
-				> .item, > .menu {
-					display: inline-flex;
-					vertical-align: middle;
-					height: ($header-height - ($padding * 2));
-					width: ($header-height - ($padding * 2));
-					box-sizing: border-box;
-					//opacity: 0.6;
-					position: relative;
-					border-radius: 5px;
-
-					&:hover {
-						background: rgba(0, 0, 0, 0.05);
-					}
-
-					> .icon {
-						margin: auto;
-					}
-
-					> .indicator {
-						position: absolute;
-						top: 8px;
-						right: 8px;
-						color: var(--indicator);
-						font-size: 8px;
-						line-height: 8px;
-						animation: blink 1s infinite;
-					}
-				}
-			}
-
-			> .left {
-				flex: 1;
-				min-width: 0;
-
-				> .account {
-					display: flex;
-					align-items: center;
-					padding: 0 8px;
-
-					> .avatar {
-						width: 26px;
-						height: 26px;
-						margin-right: 8px;
-					}
-
-					> .text {
-						white-space: nowrap;
-						overflow: hidden;
-						text-overflow: ellipsis;
-						font-size: 0.9em;
-					}
-				}
-			}
-
-			> .right {
-				margin-left: auto;
-			}
-		}
-
-		> .body {
-			flex: 1;
-			min-width: 0;
-			overflow: auto;
-
-			> .container {
-				margin-top: 8px;
-				margin-bottom: 8px;
-
-				& + .container {
-					margin-top: 16px;
-				}
-
-				> .header {
-					display: flex;
-					font-size: 0.9em;
-					padding: 8px 16px;
-					position: sticky;
-					top: 0;
-					background: var(--X17);
-					-webkit-backdrop-filter: var(--blur, blur(8px));
-					backdrop-filter: var(--blur, blur(8px));
-					z-index: 1;
-					color: var(--fgTransparentWeak);
-
-					> .add {
-						margin-left: auto;
-						color: var(--fgTransparentWeak);
-
-						&:hover {
-							color: var(--fg);
-						}
-					}
-				}
-
-				> .body {
-					padding: 0 8px;
-
-					> .item {
-						display: block;
-						padding: 6px 8px;
-						border-radius: 4px;
-						white-space: nowrap;
-						overflow: hidden;
-						text-overflow: ellipsis;
-
-						&:hover {
-							text-decoration: none;
-							background: rgba(0, 0, 0, 0.05);
-						}
-
-						&.active, &.active:hover {
-							background: var(--accent);
-							color: #fff !important;
-						}
-
-						&.read {
-							color: var(--fgTransparent);
-						}
-
-						> .icon {
-							margin-right: 8px;
-							opacity: 0.6;
-						}
-					}
-				}
-			}
-
-			> .a {
-				margin: 12px;
-			}
-		}
-	}
-
-	> .main {
-		display: flex;
-		flex: 1;
-		flex-direction: column;
-		min-width: 0;
-		height: 100vh;
-		position: relative;
-		background: var(--panel);
-
-		> .header {
-			z-index: 1000;
-			height: $header-height;
-			background-color: var(--panel);
-			border-bottom: solid 0.5px var(--divider);
-			user-select: none;
-		}
-
-		> .body {
-			width: 100%;
-			box-sizing: border-box;
-			overflow: auto;
-		}
-	}
-
-	> .side {
-		width: 350px;
-		border-left: solid 4px var(--divider);
-		background: var(--panel);
-
-		&.widgets.sideViewOpening {
-			@media (max-width: 1400px) {
-				display: none;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/chat/note-header.vue b/src/client/ui/chat/note-header.vue
deleted file mode 100644
index e40f22f588..0000000000
--- a/src/client/ui/chat/note-header.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<template>
-<header class="dehvdgxo">
-	<MkA class="name" :to="userPage(note.user)" v-user-preview="note.user.id">
-		<MkUserName :user="note.user"/>
-	</MkA>
-	<span class="is-bot" v-if="note.user.isBot">bot</span>
-	<span class="username"><MkAcct :user="note.user"/></span>
-	<span class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></span>
-	<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></span>
-	<div class="info">
-		<span class="mobile" v-if="note.viaMobile"><i class="fas fa-mobile-alt"></i></span>
-		<MkA class="created-at" :to="notePage(note)">
-			<MkTime :time="note.createdAt"/>
-		</MkA>
-		<span class="visibility" v-if="note.visibility !== 'public'">
-			<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
-			<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
-			<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
-		</span>
-		<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
-	</div>
-</header>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import notePage from '@client/filters/note';
-import { userPage } from '@client/filters/user';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-		notePage,
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.dehvdgxo {
-	display: flex;
-	align-items: baseline;
-	white-space: nowrap;
-	font-size: 0.9em;
-
-	> .name {
-		display: block;
-		margin: 0 .5em 0 0;
-		padding: 0;
-		overflow: hidden;
-		font-size: 1em;
-		font-weight: bold;
-		text-decoration: none;
-		text-overflow: ellipsis;
-
-		&:hover {
-			text-decoration: underline;
-		}
-	}
-
-	> .is-bot {
-		flex-shrink: 0;
-		align-self: center;
-		margin: 0 .5em 0 0;
-		padding: 1px 6px;
-		font-size: 80%;
-		border: solid 0.5px var(--divider);
-		border-radius: 3px;
-	}
-
-	> .admin,
-	> .moderator {
-		margin-right: 0.5em;
-		color: var(--badge);
-	}
-
-	> .username {
-		margin: 0 .5em 0 0;
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
-
-	> .info {
-		font-size: 0.9em;
-		opacity: 0.7;
-
-		> .mobile {
-			margin-right: 8px;
-		}
-
-		> .visibility {
-			margin-left: 8px;
-		}
-
-		> .localOnly {
-			margin-left: 8px;
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/chat/note-preview.vue b/src/client/ui/chat/note-preview.vue
deleted file mode 100644
index beb38de644..0000000000
--- a/src/client/ui/chat/note-preview.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<template>
-<div class="hduudsxk">
-	<MkAvatar class="avatar" :user="note.user"/>
-	<div class="main">
-		<XNoteHeader class="header" :note="note" :mini="true"/>
-		<div class="body">
-			<p v-if="note.cw != null" class="cw">
-				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
-				<XCwButton v-model="showContent" :note="note"/>
-			</p>
-			<div class="content" v-show="note.cw == null || showContent">
-				<XSubNote-content class="text" :note="note"/>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
-import XCwButton from '@client/components/cw-button.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XNoteHeader,
-		XSubNoteContent,
-		XCwButton,
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			showContent: false
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hduudsxk {
-	display: flex;
-	margin: 0;
-	padding: 0;
-	overflow: hidden;
-	font-size: 0.95em;
-
-	> .avatar {
-
-		@media (min-width: 350px) {
-			margin: 0 10px 0 0;
-			width: 44px;
-			height: 44px;
-		}
-
-		@media (min-width: 500px) {
-			margin: 0 12px 0 0;
-			width: 48px;
-			height: 48px;
-		}
-	}
-
-	> .avatar {
-		flex-shrink: 0;
-		display: block;
-		margin: 0 10px 0 0;
-		width: 40px;
-		height: 40px;
-		border-radius: 8px;
-	}
-
-	> .main {
-		flex: 1;
-		min-width: 0;
-
-		> .header {
-			margin-bottom: 2px;
-		}
-
-		> .body {
-
-			> .cw {
-				cursor: default;
-				display: block;
-				margin: 0;
-				padding: 0;
-				overflow-wrap: break-word;
-
-				> .text {
-					margin-right: 8px;
-				}
-			}
-
-			> .content {
-				> .text {
-					cursor: default;
-					margin: 0;
-					padding: 0;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/chat/note.sub.vue b/src/client/ui/chat/note.sub.vue
deleted file mode 100644
index a284ba2bf4..0000000000
--- a/src/client/ui/chat/note.sub.vue
+++ /dev/null
@@ -1,137 +0,0 @@
-<template>
-<div class="wrpstxzv" :class="{ children }">
-	<div class="main">
-		<MkAvatar class="avatar" :user="note.user"/>
-		<div class="body">
-			<XNoteHeader class="header" :note="note" :mini="true"/>
-			<div class="body">
-				<p v-if="note.cw != null" class="cw">
-					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
-					<XCwButton v-model="showContent" :note="note"/>
-				</p>
-				<div class="content" v-show="note.cw == null || showContent">
-					<XSubNote-content class="text" :note="note"/>
-				</div>
-			</div>
-		</div>
-	</div>
-	<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
-import XCwButton from '@client/components/cw-button.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	name: 'XSub',
-
-	components: {
-		XNoteHeader,
-		XSubNoteContent,
-		XCwButton,
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		detail: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		children: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		// TODO
-		truncate: {
-			type: Boolean,
-			default: true
-		}
-	},
-
-	data() {
-		return {
-			showContent: false,
-			replies: [],
-		};
-	},
-
-	created() {
-		if (this.detail) {
-			os.api('notes/children', {
-				noteId: this.note.id,
-				limit: 5
-			}).then(replies => {
-				this.replies = replies;
-			});
-		}
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.wrpstxzv {
-	padding: 16px 16px;
-	font-size: 0.8em;
-
-	&.children {
-		padding: 10px 0 0 16px;
-		font-size: 1em;
-	}
-
-	> .main {
-		display: flex;
-
-		> .avatar {
-			flex-shrink: 0;
-			display: block;
-			margin: 0 8px 0 0;
-			width: 36px;
-			height: 36px;
-		}
-
-		> .body {
-			flex: 1;
-			min-width: 0;
-
-			> .header {
-				margin-bottom: 2px;
-			}
-
-			> .body {
-				> .cw {
-					cursor: default;
-					display: block;
-					margin: 0;
-					padding: 0;
-					overflow-wrap: break-word;
-
-					> .text {
-						margin-right: 8px;
-					}
-				}
-
-				> .content {
-					> .text {
-						margin: 0;
-						padding: 0;
-					}
-				}
-			}
-		}
-	}
-
-	> .reply {
-		border-left: solid 0.5px var(--divider);
-		margin-top: 10px;
-	}
-}
-</style>
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
deleted file mode 100644
index 0a054d1057..0000000000
--- a/src/client/ui/chat/note.vue
+++ /dev/null
@@ -1,1144 +0,0 @@
-<template>
-<div
-	class="vfzoeqcg"
-	v-if="!muted"
-	v-show="!isDeleted"
-	:tabindex="!isDeleted ? '-1' : null"
-	:class="{ renote: isRenote, highlighted: appearNote._prId_ || appearNote._featuredId_, operating }"
-	v-hotkey="keymap"
->
-	<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
-	<div class="info" v-if="pinned"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
-	<div class="info" v-if="appearNote._prId_"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
-	<div class="info" v-if="appearNote._featuredId_"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
-	<div class="renote" v-if="isRenote">
-		<MkAvatar class="avatar" :user="note.user"/>
-		<i class="fas fa-retweet"></i>
-		<I18n :src="$ts.renotedBy" tag="span">
-			<template #user>
-				<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
-					<MkUserName :user="note.user"/>
-				</MkA>
-			</template>
-		</I18n>
-		<div class="info">
-			<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
-				<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
-				<MkTime :time="note.createdAt"/>
-			</button>
-			<span class="visibility" v-if="note.visibility !== 'public'">
-				<i v-if="note.visibility === 'home'" class="fas fa-home"></i>
-				<i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
-				<i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
-			</span>
-			<span class="localOnly" v-if="note.localOnly"><i class="fas fa-biohazard"></i></span>
-		</div>
-	</div>
-	<article class="article" @contextmenu.stop="onContextmenu">
-		<MkAvatar class="avatar" :user="appearNote.user"/>
-		<div class="main">
-			<XNoteHeader class="header" :note="appearNote" :mini="true"/>
-			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
-			<div class="body">
-				<p v-if="appearNote.cw != null" class="cw">
-					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
-					<XCwButton v-model="showContent" :note="appearNote"/>
-				</p>
-				<div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent">
-					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
-						<MkA class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
-						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
-						<a class="rp" v-if="appearNote.renote != null">RN:</a>
-					</div>
-					<div class="files" v-if="appearNote.files.length > 0">
-						<XMediaList :media-list="appearNote.files"/>
-					</div>
-					<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
-					<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
-					<div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
-					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
-						<span>{{ $ts.showMore }}</span>
-					</button>
-				</div>
-				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
-			</div>
-			<XReactionsViewer :note="appearNote" ref="reactionsViewer"/>
-			<footer class="footer _panel">
-				<button @click="reply()" class="button _button" v-tooltip="$ts.reply">
-					<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
-					<template v-else><i class="fas fa-reply"></i></template>
-					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
-				</button>
-				<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton" v-tooltip="$ts.renote">
-					<i class="fas fa-retweet"></i><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
-				</button>
-				<button v-else class="button _button">
-					<i class="fas fa-ban"></i>
-				</button>
-				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton" v-tooltip="$ts.reaction">
-					<i class="fas fa-plus"></i>
-				</button>
-				<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton" v-tooltip="$ts.reaction">
-					<i class="fas fa-minus"></i>
-				</button>
-				<button class="button _button" @click="menu()" ref="menuButton">
-					<i class="fas fa-ellipsis-h"></i>
-				</button>
-			</footer>
-		</div>
-	</article>
-</div>
-<div v-else class="muted" @click="muted = false">
-	<I18n :src="$ts.userSaysSomething" tag="small">
-		<template #name>
-			<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
-				<MkUserName :user="appearNote.user"/>
-			</MkA>
-		</template>
-	</I18n>
-</div>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
-import * as mfm from 'mfm-js';
-import { sum } from '../../../prelude/array';
-import XSub from './note.sub.vue';
-import XNoteHeader from './note-header.vue';
-import XNoteSimple from './note-preview.vue';
-import XReactionsViewer from '@client/components/reactions-viewer.vue';
-import XMediaList from '@client/components/media-list.vue';
-import XCwButton from '@client/components/cw-button.vue';
-import XPoll from '@client/components/poll.vue';
-import { pleaseLogin } from '@client/scripts/please-login';
-import { focusPrev, focusNext } from '@client/scripts/focus';
-import { url } from '@client/config';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { checkWordMute } from '@client/scripts/check-word-mute';
-import { userPage } from '@client/filters/user';
-import * as os from '@client/os';
-import { noteActions, noteViewInterruptors } from '@client/store';
-import { reactionPicker } from '@client/scripts/reaction-picker';
-import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
-
-export default defineComponent({
-	components: {
-		XSub,
-		XNoteHeader,
-		XNoteSimple,
-		XReactionsViewer,
-		XMediaList,
-		XCwButton,
-		XPoll,
-		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
-		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
-	},
-
-	inject: {
-		inChannel: {
-			default: null
-		},
-	},
-
-	props: {
-		note: {
-			type: Object,
-			required: true
-		},
-		pinned: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['update:note'],
-
-	data() {
-		return {
-			connection: null,
-			replies: [],
-			showContent: false,
-			collapsed: false,
-			isDeleted: false,
-			muted: false,
-			operating: false,
-		};
-	},
-
-	computed: {
-		rs() {
-			return this.$store.state.reactions;
-		},
-		keymap(): any {
-			return {
-				'r': () => this.reply(true),
-				'e|a|plus': () => this.react(true),
-				'q': () => this.renote(true),
-				'f|b': this.favorite,
-				'delete|ctrl+d': this.del,
-				'ctrl+q': this.renoteDirectly,
-				'up|k|shift+tab': this.focusBefore,
-				'down|j|tab': this.focusAfter,
-				'esc': this.blur,
-				'm|o': () => this.menu(true),
-				's': this.toggleShowContent,
-				'1': () => this.reactDirectly(this.rs[0]),
-				'2': () => this.reactDirectly(this.rs[1]),
-				'3': () => this.reactDirectly(this.rs[2]),
-				'4': () => this.reactDirectly(this.rs[3]),
-				'5': () => this.reactDirectly(this.rs[4]),
-				'6': () => this.reactDirectly(this.rs[5]),
-				'7': () => this.reactDirectly(this.rs[6]),
-				'8': () => this.reactDirectly(this.rs[7]),
-				'9': () => this.reactDirectly(this.rs[8]),
-				'0': () => this.reactDirectly(this.rs[9]),
-			};
-		},
-
-		isRenote(): boolean {
-			return (this.note.renote &&
-				this.note.text == null &&
-				this.note.fileIds.length == 0 &&
-				this.note.poll == null);
-		},
-
-		appearNote(): any {
-			return this.isRenote ? this.note.renote : this.note;
-		},
-
-		isMyNote(): boolean {
-			return this.$i && (this.$i.id === this.appearNote.userId);
-		},
-
-		isMyRenote(): boolean {
-			return this.$i && (this.$i.id === this.note.userId);
-		},
-
-		canRenote(): boolean {
-			return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
-		},
-
-		reactionsCount(): number {
-			return this.appearNote.reactions
-				? sum(Object.values(this.appearNote.reactions))
-				: 0;
-		},
-
-		urls(): string[] {
-			if (this.appearNote.text) {
-				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
-			} else {
-				return null;
-			}
-		},
-
-		showTicker() {
-			if (this.$store.state.instanceTicker === 'always') return true;
-			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
-			return false;
-		}
-	},
-
-	async created() {
-		if (this.$i) {
-			this.connection = os.stream;
-		}
-
-		this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
-			(this.appearNote.text.split('\n').length > 9) ||
-			(this.appearNote.text.length > 500)
-		);
-		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
-
-		// plugin
-		if (noteViewInterruptors.length > 0) {
-			let result = this.note;
-			for (const interruptor of noteViewInterruptors) {
-				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
-			}
-			this.$emit('update:note', Object.freeze(result));
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$i) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-	},
-
-	beforeUnmount() {
-		this.decapture(true);
-
-		if (this.$i) {
-			this.connection.off('_connected_', this.onStreamConnected);
-		}
-	},
-
-	methods: {
-		updateAppearNote(v) {
-			this.$emit('update:note', Object.freeze(this.isRenote ? {
-				...this.note,
-				renote: {
-					...this.note.renote,
-					...v
-				}
-			} : {
-				...this.note,
-				...v
-			}));
-		},
-
-		readPromo() {
-			os.api('promo/read', {
-				noteId: this.appearNote.id
-			});
-			this.isDeleted = true;
-		},
-
-		capture(withHandler = false) {
-			if (this.$i) {
-				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
-				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
-				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$i) {
-				this.connection.send('un', {
-					id: this.appearNote.id
-				});
-				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
-			}
-		},
-
-		onStreamConnected() {
-			this.capture();
-		},
-
-		onStreamNoteUpdated(data) {
-			const { type, id, body } = data;
-
-			if (id !== this.appearNote.id) return;
-
-			switch (type) {
-				case 'reacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					if (body.emoji) {
-						const emojis = this.appearNote.emojis || [];
-						if (!emojis.includes(body.emoji)) {
-							n.emojis = [...emojis, body.emoji];
-						}
-					}
-
-					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Increment the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: currentCount + 1
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = reaction;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'unreacted': {
-					const reaction = body.reaction;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					// TODO: reactionsプロパティがない場合ってあったっけ？ なければ || {} は消せる
-					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
-					// Decrement the count
-					n.reactions = {
-						...this.appearNote.reactions,
-						[reaction]: Math.max(0, currentCount - 1)
-					};
-
-					if (body.userId === this.$i.id) {
-						n.myReaction = null;
-					}
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'pollVoted': {
-					const choice = body.choice;
-
-					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
-					let n = {
-						...this.appearNote,
-					};
-
-					const choices = [...this.appearNote.poll.choices];
-					choices[choice] = {
-						...choices[choice],
-						votes: choices[choice].votes + 1,
-						...(body.userId === this.$i.id ? {
-							isVoted: true
-						} : {})
-					};
-
-					n.poll = {
-						...this.appearNote.poll,
-						choices: choices
-					};
-
-					this.updateAppearNote(n);
-					break;
-				}
-
-				case 'deleted': {
-					this.isDeleted = true;
-					break;
-				}
-			}
-		},
-
-		reply(viaKeyboard = false) {
-			pleaseLogin();
-			this.operating = true;
-			os.post({
-				reply: this.appearNote,
-				animation: !viaKeyboard,
-			}, () => {
-				this.operating = false;
-				this.focus();
-			});
-		},
-
-		renote(viaKeyboard = false) {
-			pleaseLogin();
-			this.operating = true;
-			this.blur();
-			os.popupMenu([{
-				text: this.$ts.renote,
-				icon: 'fas fa-retweet',
-				action: () => {
-					os.api('notes/create', {
-						renoteId: this.appearNote.id
-					});
-				}
-			}, {
-				text: this.$ts.quote,
-				icon: 'fas fa-quote-right',
-				action: () => {
-					os.post({
-						renote: this.appearNote,
-					});
-				}
-			}], this.$refs.renoteButton, {
-				viaKeyboard
-			}).then(() => {
-				this.operating = false;
-			});
-		},
-
-		renoteDirectly() {
-			os.apiWithDialog('notes/create', {
-				renoteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.dialog({
-					type: 'success',
-					text: this.$ts.renoted,
-				});
-			}, (e: Error) => {
-				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantRenote,
-					});
-				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantReRenote,
-					});
-				}
-			});
-		},
-
-		async react(viaKeyboard = false) {
-			pleaseLogin();
-			this.operating = true;
-			this.blur();
-			reactionPicker.show(this.$refs.reactButton, reaction => {
-				os.api('notes/reactions/create', {
-					noteId: this.appearNote.id,
-					reaction: reaction
-				});
-			}, () => {
-				this.operating = false;
-				this.focus();
-			});
-		},
-
-		reactDirectly(reaction) {
-			os.api('notes/reactions/create', {
-				noteId: this.appearNote.id,
-				reaction: reaction
-			});
-		},
-
-		undoReact(note) {
-			const oldReaction = note.myReaction;
-			if (!oldReaction) return;
-			os.api('notes/reactions/delete', {
-				noteId: note.id
-			});
-		},
-
-		favorite() {
-			pleaseLogin();
-			os.apiWithDialog('notes/favorites/create', {
-				noteId: this.appearNote.id
-			}, undefined, (res: any) => {
-				os.dialog({
-					type: 'success',
-					text: this.$ts.favorited,
-				});
-			}, (e: Error) => {
-				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.alreadyFavorited,
-					});
-				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.cantFavorite,
-					});
-				}
-			});
-		},
-
-		del() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.noteDeleteConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-			});
-		},
-
-		delEdit() {
-			os.dialog({
-				type: 'warning',
-				text: this.$ts.deleteAndEditConfirm,
-				showCancelButton: true
-			}).then(({ canceled }) => {
-				if (canceled) return;
-
-				os.api('notes/delete', {
-					noteId: this.appearNote.id
-				});
-
-				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
-			});
-		},
-
-		toggleFavorite(favorite: boolean) {
-			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		toggleWatch(watch: boolean) {
-			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
-				noteId: this.appearNote.id
-			});
-		},
-
-		getMenu() {
-			let menu;
-			if (this.$i) {
-				const statePromise = os.api('notes/state', {
-					noteId: this.appearNote.id
-				});
-
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined,
-				{
-					icon: 'fas fa-share-alt',
-					text: this.$ts.share,
-					action: this.share
-				},
-				null,
-				statePromise.then(state => state.isFavorited ? {
-					icon: 'fas fa-star',
-					text: this.$ts.unfavorite,
-					action: () => this.toggleFavorite(false)
-				} : {
-					icon: 'fas fa-star',
-					text: this.$ts.favorite,
-					action: () => this.toggleFavorite(true)
-				}),
-				{
-					icon: 'fas fa-paperclip',
-					text: this.$ts.clip,
-					action: () => this.clip()
-				},
-				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
-					icon: 'fas fa-eye-slash',
-					text: this.$ts.unwatch,
-					action: () => this.toggleWatch(false)
-				} : {
-					icon: 'fas fa-eye',
-					text: this.$ts.watch,
-					action: () => this.toggleWatch(true)
-				}) : undefined,
-				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.unpin,
-					action: () => this.togglePin(false)
-				} : {
-					icon: 'fas fa-thumbtack',
-					text: this.$ts.pin,
-					action: () => this.togglePin(true)
-				} : undefined,
-				...(this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					{
-						icon: 'fas fa-bullhorn',
-						text: this.$ts.promote,
-						action: this.promote
-					}]
-					: []
-				),
-				...(this.appearNote.userId != this.$i.id ? [
-					null,
-					{
-						icon: 'fas fa-exclamation-circle',
-						text: this.$ts.reportAbuse,
-						action: () => {
-							const u = `${url}/notes/${this.appearNote.id}`;
-							os.popup(import('@client/components/abuse-report-window.vue'), {
-								user: this.appearNote.user,
-								initialComment: `Note: ${u}\n-----\n`
-							}, {}, 'closed');
-						}
-					}]
-					: []
-				),
-				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
-					null,
-					this.appearNote.userId == this.$i.id ? {
-						icon: 'fas fa-edit',
-						text: this.$ts.deleteAndEdit,
-						action: this.delEdit
-					} : undefined,
-					{
-						icon: 'fas fa-trash-alt',
-						text: this.$ts.delete,
-						danger: true,
-						action: this.del
-					}]
-					: []
-				)]
-				.filter(x => x !== undefined);
-			} else {
-				menu = [{
-					icon: 'fas fa-copy',
-					text: this.$ts.copyContent,
-					action: this.copyContent
-				}, {
-					icon: 'fas fa-link',
-					text: this.$ts.copyLink,
-					action: this.copyLink
-				}, (this.appearNote.url || this.appearNote.uri) ? {
-					icon: 'fas fa-external-link-square-alt',
-					text: this.$ts.showOnRemote,
-					action: () => {
-						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
-					}
-				} : undefined]
-				.filter(x => x !== undefined);
-			}
-
-			if (noteActions.length > 0) {
-				menu = menu.concat([null, ...noteActions.map(action => ({
-					icon: 'fas fa-plug',
-					text: action.title,
-					action: () => {
-						action.handler(this.appearNote);
-					}
-				}))]);
-			}
-
-			return menu;
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (window.getSelection().toString() !== '') return;
-			
-			if (this.$store.state.useReactionPickerForContextMenu) {
-				e.preventDefault();
-				this.react();
-			} else {
-				os.contextMenu(this.getMenu(), e).then(this.focus);
-			}
-		},
-
-		menu(viaKeyboard = false) {
-			this.operating = true;
-			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
-				viaKeyboard
-			}).then(() => {
-				this.operating = false;
-				this.focus();
-			});
-		},
-
-		showRenoteMenu(viaKeyboard = false) {
-			if (!this.isMyRenote) return;
-			os.popupMenu([{
-				text: this.$ts.unrenote,
-				icon: 'fas fa-trash-alt',
-				danger: true,
-				action: () => {
-					os.api('notes/delete', {
-						noteId: this.note.id
-					});
-					this.isDeleted = true;
-				}
-			}], this.$refs.renoteTime, {
-				viaKeyboard: viaKeyboard
-			});
-		},
-
-		toggleShowContent() {
-			this.showContent = !this.showContent;
-		},
-
-		copyContent() {
-			copyToClipboard(this.appearNote.text);
-			os.success();
-		},
-
-		copyLink() {
-			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
-			os.success();
-		},
-
-		togglePin(pin: boolean) {
-			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
-				noteId: this.appearNote.id
-			}, undefined, null, e => {
-				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
-					os.dialog({
-						type: 'error',
-						text: this.$ts.pinLimitExceeded
-					});
-				}
-			});
-		},
-
-		async clip() {
-			const clips = await os.api('clips/list');
-			os.popupMenu([{
-				icon: 'fas fa-plus',
-				text: this.$ts.createNew,
-				action: async () => {
-					const { canceled, result } = await os.form(this.$ts.createNewClip, {
-						name: {
-							type: 'string',
-							label: this.$ts.name
-						},
-						description: {
-							type: 'string',
-							required: false,
-							multiline: true,
-							label: this.$ts.description
-						},
-						isPublic: {
-							type: 'boolean',
-							label: this.$ts.public,
-							default: false
-						}
-					});
-					if (canceled) return;
-
-					const clip = await os.apiWithDialog('clips/create', result);
-
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}, null, ...clips.map(clip => ({
-				text: clip.name,
-				action: () => {
-					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
-				}
-			}))], this.$refs.menuButton, {
-			}).then(this.focus);
-		},
-
-		async promote() {
-			const { canceled, result: days } = await os.dialog({
-				title: this.$ts.numberOfDays,
-				input: { type: 'number' }
-			});
-
-			if (canceled) return;
-
-			os.apiWithDialog('admin/promo/create', {
-				noteId: this.appearNote.id,
-				expiresAt: Date.now() + (86400000 * days)
-			});
-		},
-
-		share() {
-			navigator.share({
-				title: this.$t('noteOf', { user: this.appearNote.user.name }),
-				text: this.appearNote.text,
-				url: `${url}/notes/${this.appearNote.id}`
-			});
-		},
-
-		focus() {
-			this.$el.focus();
-		},
-
-		blur() {
-			this.$el.blur();
-		},
-
-		focusBefore() {
-			focusPrev(this.$el);
-		},
-
-		focusAfter() {
-			focusNext(this.$el);
-		},
-
-		userPage
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.vfzoeqcg {
-	position: relative;
-	contain: content;
-
-	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
-	// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
-	// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
-	// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
-	// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか？)
-	//content-visibility: auto;
-  //contain-intrinsic-size: 0 128px;
-
-	&:focus-visible {
-		outline: none;
-	}
-
-	&:hover {
-		background: rgba(0, 0, 0, 0.05);
-	}
-
-	&:hover, &.operating {
-		> .article > .main > .footer {
-			display: block;
-		}
-	}
-
-	&.renote {
-		background: rgba(128, 255, 0, 0.05);
-	}
-
-	&.highlighted {
-		background: rgba(255, 128, 0, 0.05);
-	}
-
-	> .info {
-		display: flex;
-		align-items: center;
-		padding: 12px 16px 4px 16px;
-		line-height: 24px;
-		font-size: 85%;
-		white-space: pre;
-		color: #d28a3f;
-
-		> i {
-			margin-right: 4px;
-		}
-
-		> .hide {
-			margin-left: 16px;
-			color: inherit;
-			opacity: 0.7;
-		}
-	}
-
-	> .info + .article {
-		padding-top: 8px;
-	}
-
-	> .reply-to {
-		opacity: 0.7;
-		padding-bottom: 0;
-	}
-
-	> .renote {
-		display: flex;
-		align-items: center;
-		padding: 12px 16px 4px 16px;
-		line-height: 28px;
-		white-space: pre;
-		color: var(--renote);
-		font-size: 0.9em;
-
-		> .avatar {
-			flex-shrink: 0;
-			display: inline-block;
-			width: 28px;
-			height: 28px;
-			margin: 0 8px 0 0;
-			border-radius: 6px;
-		}
-
-		> i {
-			margin-right: 4px;
-		}
-
-		> span {
-			overflow: hidden;
-			flex-shrink: 1;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-
-			> .name {
-				font-weight: bold;
-			}
-		}
-
-		> .info {
-			margin-left: 8px;
-			font-size: 0.9em;
-			opacity: 0.7;
-
-			> .time {
-				flex-shrink: 0;
-				color: inherit;
-
-				> .dropdownIcon {
-					margin-right: 4px;
-				}
-			}
-
-			> .visibility {
-				margin-left: 8px;
-			}
-
-			> .localOnly {
-				margin-left: 8px;
-			}
-		}
-	}
-
-	> .renote + .article {
-		padding-top: 8px;
-	}
-
-	> .article {
-		display: flex;
-		padding: 12px 16px;
-
-		> .avatar {
-			flex-shrink: 0;
-			display: block;
-			position: sticky;
-			top: 0;
-			margin: 0 14px 0 0;
-			width: 46px;
-			height: 46px;
-		}
-
-		> .main {
-			flex: 1;
-			min-width: 0;
-
-			> .body {
-				> .cw {
-					cursor: default;
-					display: block;
-					margin: 0;
-					padding: 0;
-					overflow-wrap: break-word;
-
-					> .text {
-						margin-right: 8px;
-					}
-				}
-
-				> .content {
-					&.collapsed {
-						position: relative;
-						max-height: 9em;
-						overflow: hidden;
-
-						> .fade {
-							display: block;
-							position: absolute;
-							bottom: 0;
-							left: 0;
-							width: 100%;
-							height: 64px;
-							background: linear-gradient(0deg, var(--panel), var(--X15));
-
-							> span {
-								display: inline-block;
-								background: var(--panel);
-								padding: 6px 10px;
-								font-size: 0.8em;
-								border-radius: 999px;
-								box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
-							}
-
-							&:hover {
-								> span {
-									background: var(--panelHighlight);
-								}
-							}
-						}
-					}
-
-					> .text {
-						overflow-wrap: break-word;
-
-						> .reply {
-							color: var(--accent);
-							margin-right: 0.5em;
-						}
-
-						> .rp {
-							margin-left: 4px;
-							font-style: oblique;
-							color: var(--renote);
-						}
-					}
-
-					> .files {
-						max-width: 500px;
-					}
-
-					> .url-preview {
-						margin-top: 8px;
-						max-width: 500px;
-					}
-
-					> .poll {
-						font-size: 80%;
-						max-width: 500px;
-					}
-
-					> .renote {
-						padding: 8px 0;
-
-						> * {
-							padding: 16px;
-							border: dashed 1px var(--renote);
-							border-radius: 8px;
-						}
-					}
-				}
-
-				> .channel {
-					opacity: 0.7;
-					font-size: 80%;
-				}
-			}
-
-			> .footer {
-				display: none;
-				position: absolute;
-				top: 8px;
-				right: 8px;
-				padding: 0 6px;
-				opacity: 0.7;
-
-				&:hover {
-					opacity: 1;
-				}
-
-				> .button {
-					margin: 0;
-					padding: 8px;
-					opacity: 0.7;
-
-					&:hover {
-						color: var(--accent);
-					}
-
-					> .count {
-						display: inline;
-						margin: 0 0 0 8px;
-						opacity: 0.7;
-					}
-
-					&.reacted {
-						color: var(--accent);
-					}
-				}
-			}
-		}
-	}
-
-	> .reply {
-		border-top: solid 0.5px var(--divider);
-	}
-}
-
-.muted {
-	padding: 8px 16px;
-	opacity: 0.7;
-
-	&:hover {
-		background: rgba(0, 0, 0, 0.05);
-	}
-}
-</style>
diff --git a/src/client/ui/chat/notes.vue b/src/client/ui/chat/notes.vue
deleted file mode 100644
index 6690baf584..0000000000
--- a/src/client/ui/chat/notes.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<template>
-<div class="">
-	<div class="_fullinfo" v-if="empty">
-		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
-		<div>{{ $ts.noNotes }}</div>
-	</div>
-
-	<MkLoading v-if="fetching"/>
-
-	<MkError v-if="error" @retry="init()"/>
-
-	<div v-show="more && reversed" style="margin-bottom: var(--margin);">
-		<MkButton style="margin: 0 auto;" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-			<template v-if="moreFetching"><MkLoading inline/></template>
-		</MkButton>
-	</div>
-
-	<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :ad="true">
-		<XNote :note="note" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
-	</XList>
-
-	<div v-show="more && !reversed" style="margin-top: var(--margin);">
-		<MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
-			<template v-if="moreFetching"><MkLoading inline/></template>
-		</MkButton>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@client/scripts/paging';
-import XNote from './note.vue';
-import XList from './date-separated-list.vue';
-import MkButton from '@client/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		XNote, XList, MkButton,
-	},
-
-	mixins: [
-		paging({
-			before: (self) => {
-				self.$emit('before');
-			},
-
-			after: (self, e) => {
-				self.$emit('after', e);
-			}
-		}),
-	],
-
-	props: {
-		pagination: {
-			required: true
-		},
-
-		prop: {
-			type: String,
-			required: false
-		}
-	},
-
-	emits: ['before', 'after'],
-
-	computed: {
-		notes(): any[] {
-			return this.prop ? this.items.map(item => item[this.prop]) : this.items;
-		},
-
-		reversed(): boolean {
-			return this.pagination.reversed;
-		}
-	},
-
-	methods: {
-		updated(oldValue, newValue) {
-			const i = this.notes.findIndex(n => n === oldValue);
-			if (this.prop) {
-				this.items[i][this.prop] = newValue;
-			} else {
-				this.items[i] = newValue;
-			}
-		},
-
-		focus() {
-			this.$refs.notes.focus();
-		}
-	}
-});
-</script>
diff --git a/src/client/ui/chat/pages/channel.vue b/src/client/ui/chat/pages/channel.vue
deleted file mode 100644
index d11d40b210..0000000000
--- a/src/client/ui/chat/pages/channel.vue
+++ /dev/null
@@ -1,259 +0,0 @@
-<template>
-<div v-if="channel" class="hhizbblb">
-	<div class="info" v-if="date">
-		<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
-	</div>
-	<div class="tl" ref="body">
-		<div class="new" v-if="queue > 0" :style="{ width: width + 'px', bottom: bottom + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
-		<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="true"/>
-	</div>
-	<div class="bottom">
-		<div class="typers" v-if="typers.length > 0">
-			<I18n :src="$ts.typingUsers" text-tag="span" class="users">
-				<template #users>
-					<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
-				</template>
-			</I18n>
-			<MkEllipsis/>
-		</div>
-		<XPostForm :channel="channel"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import * as Misskey from 'misskey-js';
-import XNotes from '../notes.vue';
-import * as os from '@client/os';
-import * as sound from '@client/scripts/sound';
-import { scrollToBottom, getScrollPosition, getScrollContainer } from '@client/scripts/scroll';
-import follow from '@client/directives/follow-append';
-import XPostForm from '../post-form.vue';
-import MkInfo from '@client/components/ui/info.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes,
-		XPostForm,
-		MkInfo,
-	},
-
-	directives: {
-		follow
-	},
-	
-	provide() {
-		return {
-			inChannel: true
-		};
-	},
-
-	props: {
-		channelId: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			channel: null as Misskey.entities.Channel | null,
-			connection: null,
-			pagination: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.showLocalRenotes
-			},
-			queue: 0,
-			width: 0,
-			top: 0,
-			bottom: 0,
-			typers: [],
-			date: null,
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.channel ? this.channel.name : '-',
-				subtitle: this.channel ? this.channel.description : '-',
-				icon: 'fas fa-satellite-dish',
-				actions: [{
-					icon: this.channel?.isFollowing ? 'fas fa-star' : 'far fa-star',
-					text: this.channel?.isFollowing ? this.$ts.unfollow : this.$ts.follow,
-					highlighted: this.channel?.isFollowing,
-					handler: this.toggleChannelFollow
-				}, {
-					icon: 'fas fa-search',
-					text: this.$ts.inChannelSearch,
-					handler: this.inChannelSearch
-				}, {
-					icon: 'fas fa-calendar-alt',
-					text: this.$ts.jumpToSpecifiedDate,
-					handler: this.timetravel
-				}]
-			})),
-		};
-	},
-
-	async created() {
-		this.channel = await os.api('channels/show', { channelId: this.channelId });
-
-		const prepend = note => {
-			(this.$refs.tl as any).prepend(note);
-
-			this.$emit('note');
-
-			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
-		};
-
-		this.connection = markRaw(os.stream.useChannel('channel', {
-			channelId: this.channelId
-		}));
-		this.connection.on('note', prepend);
-		this.connection.on('typers', typers => {
-			this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers;
-		});
-
-		this.pagination = {
-			endpoint: 'channels/timeline',
-			reversed: true,
-			limit: 10,
-			params: init => ({
-				channelId: this.channelId,
-				untilDate: this.date?.getTime(),
-				...this.baseQuery
-			})
-		};
-	},
-
-	mounted() {
-
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-
-	methods: {
-		focus() {
-			this.$refs.body.focus();
-		},
-
-		goTop() {
-			const container = getScrollContainer(this.$refs.body);
-			container.scrollTop = 0;
-		},
-
-		queueUpdated(q) {
-			if (this.$refs.body.offsetWidth !== 0) {
-				const rect = this.$refs.body.getBoundingClientRect();
-				this.width = this.$refs.body.offsetWidth;
-				this.top = rect.top;
-				this.bottom = this.$refs.body.offsetHeight;
-			}
-			this.queue = q;
-		},
-
-		async inChannelSearch() {
-			const { canceled, result: query } = await os.dialog({
-				title: this.$ts.inChannelSearch,
-				input: true
-			});
-			if (canceled || query == null || query === '') return;
-			router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.channelId}`);
-		},
-
-		async toggleChannelFollow() {
-			if (this.channel.isFollowing) {
-				await os.apiWithDialog('channels/unfollow', {
-					channelId: this.channel.id
-				});
-				this.channel.isFollowing = false;
-			} else {
-				await os.apiWithDialog('channels/follow', {
-					channelId: this.channel.id
-				});
-				this.channel.isFollowing = true;
-			}
-		},
-
-		openChannelMenu(ev) {
-			os.popupMenu([{
-				text: this.$ts.copyUrl,
-				icon: 'fas fa-link',
-				action: () => {
-					copyToClipboard(`${url}/channels/${this.currentChannel.id}`);
-				}
-			}], ev.currentTarget || ev.target);
-		},
-
-		timetravel(date?: Date) {
-			this.date = date;
-			this.$refs.tl.reload();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hhizbblb {
-	display: flex;
-	flex-direction: column;
-	flex: 1;
-	overflow: auto;
-
-	> .info {
-		padding: 16px 16px 0 16px;
-	}
-
-	> .top {
-		padding: 16px 16px 0 16px;
-	}
-
-	> .bottom {
-		padding: 0 16px 16px 16px;
-		position: relative;
-
-		> .typers {
-			position: absolute;
-			bottom: 100%;
-			padding: 0 8px 0 8px;
-			font-size: 0.9em;
-			background: var(--panel);
-			border-radius: 0 8px 0 0;
-			color: var(--fgTransparentWeak);
-
-			> .users {
-				> .user + .user:before {
-					content: ", ";
-					font-weight: normal;
-				}
-
-				> .user:last-of-type:after {
-					content: " ";
-				}
-			}
-		}
-	}
-
-	> .tl {
-		position: relative;
-		padding: 16px 0;
-		flex: 1;
-		min-width: 0;
-		overflow: auto;
-
-		> .new {
-			position: fixed;
-			z-index: 1000;
-
-			> button {
-				display: block;
-				margin: 16px auto;
-				padding: 8px 16px;
-				border-radius: 32px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/chat/pages/timeline.vue b/src/client/ui/chat/pages/timeline.vue
deleted file mode 100644
index 0f9cd7f11e..0000000000
--- a/src/client/ui/chat/pages/timeline.vue
+++ /dev/null
@@ -1,221 +0,0 @@
-<template>
-<div class="dbiokgaf">
-	<div class="info" v-if="date">
-		<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
-	</div>
-	<div class="top">
-		<XPostForm/>
-	</div>
-	<div class="tl" ref="body">
-		<div class="new" v-if="queue > 0" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
-		<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import XNotes from '../notes.vue';
-import * as os from '@client/os';
-import * as sound from '@client/scripts/sound';
-import { scrollToBottom, getScrollPosition, getScrollContainer } from '@client/scripts/scroll';
-import follow from '@client/directives/follow-append';
-import XPostForm from '../post-form.vue';
-import MkInfo from '@client/components/ui/info.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XNotes,
-		XPostForm,
-		MkInfo,
-	},
-
-	directives: {
-		follow
-	},
-
-	props: {
-		src: {
-			type: String,
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			connection: null,
-			connection2: null,
-			pagination: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.showLocalRenotes
-			},
-			query: {},
-			queue: 0,
-			width: 0,
-			top: 0,
-			bottom: 0,
-			typers: [],
-			date: null,
-			[symbols.PAGE_INFO]: computed(() => ({
-				title: this.$ts.timeline,
-				icon: 'fas fa-home',
-				actions: [{
-					icon: 'fas fa-calendar-alt',
-					text: this.$ts.jumpToSpecifiedDate,
-					handler: this.timetravel
-				}]
-			})),
-		};
-	},
-
-	created() {
-		const prepend = note => {
-			(this.$refs.tl as any).prepend(note);
-
-			this.$emit('note');
-
-			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
-		};
-
-		const onChangeFollowing = () => {
-			if (!this.$refs.tl.backed) {
-				this.$refs.tl.reload();
-			}
-		};
-
-		let endpoint;
-
-		if (this.src == 'home') {
-			endpoint = 'notes/timeline';
-			this.connection = markRaw(os.stream.useChannel('homeTimeline'));
-			this.connection.on('note', prepend);
-
-			this.connection2 = markRaw(os.stream.useChannel('main'));
-			this.connection2.on('follow', onChangeFollowing);
-			this.connection2.on('unfollow', onChangeFollowing);
-		} else if (this.src == 'local') {
-			endpoint = 'notes/local-timeline';
-			this.connection = markRaw(os.stream.useChannel('localTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'social') {
-			endpoint = 'notes/hybrid-timeline';
-			this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'global') {
-			endpoint = 'notes/global-timeline';
-			this.connection = markRaw(os.stream.useChannel('globalTimeline'));
-			this.connection.on('note', prepend);
-		}
-
-		this.pagination = {
-			endpoint: endpoint,
-			limit: 10,
-			params: init => ({
-				untilDate: this.date?.getTime(),
-				...this.baseQuery, ...this.query
-			})
-		};
-	},
-
-	mounted() {
-
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-		if (this.connection2) this.connection2.dispose();
-	},
-
-	methods: {
-		focus() {
-			this.$refs.body.focus();
-		},
-
-		goTop() {
-			const container = getScrollContainer(this.$refs.body);
-			container.scrollTop = 0;
-		},
-
-		queueUpdated(q) {
-			if (this.$refs.body.offsetWidth !== 0) {
-				const rect = this.$refs.body.getBoundingClientRect();
-				this.width = this.$refs.body.offsetWidth;
-				this.top = rect.top;
-				this.bottom = this.$refs.body.offsetHeight;
-			}
-			this.queue = q;
-		},
-
-		timetravel(date?: Date) {
-			this.date = date;
-			this.$refs.tl.reload();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.dbiokgaf {
-	display: flex;
-	flex-direction: column;
-	flex: 1;
-	overflow: auto;
-
-	> .info {
-		padding: 16px 16px 0 16px;
-	}
-
-	> .top {
-		padding: 16px 16px 0 16px;
-	}
-
-	> .bottom {
-		padding: 0 16px 16px 16px;
-		position: relative;
-
-		> .typers {
-			position: absolute;
-			bottom: 100%;
-			padding: 0 8px 0 8px;
-			font-size: 0.9em;
-			background: var(--panel);
-			border-radius: 0 8px 0 0;
-			color: var(--fgTransparentWeak);
-
-			> .users {
-				> .user + .user:before {
-					content: ", ";
-					font-weight: normal;
-				}
-
-				> .user:last-of-type:after {
-					content: " ";
-				}
-			}
-		}
-	}
-
-	> .tl {
-		position: relative;
-		padding: 16px 0;
-		flex: 1;
-		min-width: 0;
-		overflow: auto;
-
-		> .new {
-			position: fixed;
-			z-index: 1000;
-
-			> button {
-				display: block;
-				margin: 16px auto;
-				padding: 8px 16px;
-				border-radius: 32px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue
deleted file mode 100644
index e1e56dee35..0000000000
--- a/src/client/ui/chat/post-form.vue
+++ /dev/null
@@ -1,773 +0,0 @@
-<template>
-<div class="pxiwixjf"
-	@dragover.stop="onDragover"
-	@dragenter="onDragenter"
-	@dragleave="onDragleave"
-	@drop.stop="onDrop"
->
-	<div class="form">
-		<div class="with-quote" v-if="quoteId"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
-		<div v-if="visibility === 'specified'" class="to-specified">
-			<span style="margin-right: 8px;">{{ $ts.recipient }}</span>
-			<div class="visibleUsers">
-				<span v-for="u in visibleUsers" :key="u.id">
-					<MkAcct :user="u"/>
-					<button class="_button" @click="removeVisibleUser(u)"><i class="fas fa-times"></i></button>
-				</span>
-				<button @click="addVisibleUser" class="_buttonPrimary"><i class="fas fa-plus fa-fw"></i></button>
-			</div>
-		</div>
-		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
-		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd" />
-		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
-		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
-		<footer>
-			<div class="left">
-				<button class="_button" @click="chooseFileFrom" v-tooltip="$ts.attachFile"><i class="fas fa-photo-video"></i></button>
-				<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$ts.poll"><i class="fas fa-poll-h"></i></button>
-				<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$ts.useCw"><i class="fas fa-eye-slash"></i></button>
-				<button class="_button" @click="insertMention" v-tooltip="$ts.mention"><i class="fas fa-at"></i></button>
-				<button class="_button" @click="insertEmoji" v-tooltip="$ts.emoji"><i class="fas fa-laugh-squint"></i></button>
-				<button class="_button" @click="showActions" v-tooltip="$ts.plugin" v-if="postFormActions.length > 0"><i class="fas fa-plug"></i></button>
-			</div>
-			<div class="right">
-				<span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
-				<span class="local-only" v-if="localOnly"><i class="fas fa-biohazard"></i></span>
-				<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$ts.visibility" :disabled="channel != null">
-					<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
-					<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
-					<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
-					<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
-				</button>
-				<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
-			</div>
-		</footer>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import insertTextAtCursor from 'insert-text-at-cursor';
-import { length } from 'stringz';
-import { toASCII } from 'punycode/';
-import * as mfm from 'mfm-js';
-import { host, url } from '@client/config';
-import { erase, unique } from '../../../prelude/array';
-import { extractMentions } from '@/misc/extract-mentions';
-import { getAcct } from '@/misc/acct';
-import { formatTimeString } from '@/misc/format-time-string';
-import { Autocomplete } from '@client/scripts/autocomplete';
-import { noteVisibilities } from '../../../types';
-import * as os from '@client/os';
-import { selectFile } from '@client/scripts/select-file';
-import { notePostInterruptors, postFormActions } from '@client/store';
-import { isMobile } from '@client/scripts/is-mobile';
-import { throttle } from 'throttle-debounce';
-
-export default defineComponent({
-	components: {
-		XPostFormAttaches: defineAsyncComponent(() => import('@client/components/post-form-attaches.vue')),
-		XPollEditor: defineAsyncComponent(() => import('@client/components/poll-editor.vue'))
-	},
-
-	props: {
-		reply: {
-			type: Object,
-			required: false
-		},
-		renote: {
-			type: Object,
-			required: false
-		},
-		channel: {
-			type: String,
-			required: false
-		},
-		mention: {
-			type: Object,
-			required: false
-		},
-		specified: {
-			type: Object,
-			required: false
-		},
-		initialText: {
-			type: String,
-			required: false
-		},
-		initialNote: {
-			type: Object,
-			required: false
-		},
-		share: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		autofocus: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	emits: ['posted', 'cancel', 'esc'],
-
-	data() {
-		return {
-			posting: false,
-			text: '',
-			files: [],
-			poll: null,
-			useCw: false,
-			cw: null,
-			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
-			visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
-			visibleUsers: [],
-			autocomplete: null,
-			draghover: false,
-			quoteId: null,
-			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
-			imeText: '',
-			typing: throttle(3000, () => {
-				if (this.channel) {
-					os.stream.send('typingOnChannel', { channel: this.channel });
-				}
-			}),
-			postFormActions,
-		};
-	},
-
-	computed: {
-		draftKey(): string {
-			let key = this.channel ? `channel:${this.channel}` : '';
-
-			if (this.renote) {
-				key += `renote:${this.renote.id}`;
-			} else if (this.reply) {
-				key += `reply:${this.reply.id}`;
-			} else {
-				key += 'note';
-			}
-
-			return key;
-		},
-
-		placeholder(): string {
-			if (this.renote) {
-				return this.$ts._postForm.quotePlaceholder;
-			} else if (this.reply) {
-				return this.$ts._postForm.replyPlaceholder;
-			} else if (this.channel) {
-				return this.$ts._postForm.channelPlaceholder;
-			} else {
-				const xs = [
-					this.$ts._postForm._placeholders.a,
-					this.$ts._postForm._placeholders.b,
-					this.$ts._postForm._placeholders.c,
-					this.$ts._postForm._placeholders.d,
-					this.$ts._postForm._placeholders.e,
-					this.$ts._postForm._placeholders.f
-				];
-				return xs[Math.floor(Math.random() * xs.length)];
-			}
-		},
-
-		submitText(): string {
-			return this.renote
-				? this.$ts.quote
-				: this.reply
-					? this.$ts.reply
-					: this.$ts.note;
-		},
-
-		textLength(): number {
-			return length((this.text + this.imeText).trim());
-		},
-
-		canPost(): boolean {
-			return !this.posting &&
-				(1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
-				(this.textLength <= this.max) &&
-				(!this.poll || this.poll.choices.length >= 2);
-		},
-
-		max(): number {
-			return this.$instance ? this.$instance.maxNoteTextLength : 1000;
-		}
-	},
-
-	mounted() {
-		if (this.initialText) {
-			this.text = this.initialText;
-		}
-
-		if (this.mention) {
-			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
-			this.text += ' ';
-		}
-
-		if (this.reply && this.reply.user.host != null) {
-			this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
-		}
-
-		if (this.reply && this.reply.text != null) {
-			const ast = mfm.parse(this.reply.text);
-
-			for (const x of extractMentions(ast)) {
-				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
-
-				// 自分は除外
-				if (this.$i.username == x.username && x.host == null) continue;
-				if (this.$i.username == x.username && x.host == host) continue;
-
-				// 重複は除外
-				if (this.text.indexOf(`${mention} `) != -1) continue;
-
-				this.text += `${mention} `;
-			}
-		}
-
-		if (this.channel) {
-			this.visibility = 'public';
-			this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
-		}
-
-		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
-		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
-			this.visibility = this.reply.visibility;
-			if (this.reply.visibility === 'specified') {
-				os.api('users/show', {
-					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
-				}).then(users => {
-					this.visibleUsers.push(...users);
-				});
-
-				if (this.reply.userId !== this.$i.id) {
-					os.api('users/show', { userId: this.reply.userId }).then(user => {
-						this.visibleUsers.push(user);
-					});
-				}
-			}
-		}
-
-		if (this.specified) {
-			this.visibility = 'specified';
-			this.visibleUsers.push(this.specified);
-		}
-
-		// keep cw when reply
-		if (this.$store.state.keepCw && this.reply && this.reply.cw) {
-			this.useCw = true;
-			this.cw = this.reply.cw;
-		}
-
-		if (this.autofocus) {
-			this.focus();
-
-			this.$nextTick(() => {
-				this.focus();
-			});
-		}
-
-		// TODO: detach when unmount
-		new Autocomplete(this.$refs.text, this, { model: 'text' });
-		new Autocomplete(this.$refs.cw, this, { model: 'cw' });
-
-		this.$nextTick(() => {
-			// 書きかけの投稿を復元
-			if (!this.share && !this.mention && !this.specified) {
-				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
-				if (draft) {
-					this.text = draft.data.text;
-					this.useCw = draft.data.useCw;
-					this.cw = draft.data.cw;
-					this.visibility = draft.data.visibility;
-					this.localOnly = draft.data.localOnly;
-					this.files = (draft.data.files || []).filter(e => e);
-					if (draft.data.poll) {
-						this.poll = draft.data.poll;
-					}
-				}
-			}
-
-			// 削除して編集
-			if (this.initialNote) {
-				const init = this.initialNote;
-				this.text = init.text ? init.text : '';
-				this.files = init.files;
-				this.cw = init.cw;
-				this.useCw = init.cw != null;
-				if (init.poll) {
-					this.poll = init.poll;
-				}
-				this.visibility = init.visibility;
-				this.localOnly = init.localOnly;
-				this.quoteId = init.renote ? init.renote.id : null;
-			}
-
-			this.$nextTick(() => this.watch());
-		});
-	},
-
-	methods: {
-		watch() {
-			this.$watch('text', () => this.saveDraft());
-			this.$watch('useCw', () => this.saveDraft());
-			this.$watch('cw', () => this.saveDraft());
-			this.$watch('poll', () => this.saveDraft());
-			this.$watch('files', () => this.saveDraft(), { deep: true });
-			this.$watch('visibility', () => this.saveDraft());
-			this.$watch('localOnly', () => this.saveDraft());
-		},
-
-		togglePoll() {
-			if (this.poll) {
-				this.poll = null;
-			} else {
-				this.poll = {
-					choices: ['', ''],
-					multiple: false,
-					expiresAt: null,
-					expiredAfter: null,
-				};
-			}
-		},
-
-		addTag(tag: string) {
-			insertTextAtCursor(this.$refs.text, ` #${tag} `);
-		},
-
-		focus() {
-			(this.$refs.text as any).focus();
-		},
-
-		chooseFileFrom(ev) {
-			selectFile(ev.currentTarget || ev.target, this.$ts.attachFile, true).then(files => {
-				for (const file of files) {
-					this.files.push(file);
-				}
-			});
-		},
-
-		detachFile(id) {
-			this.files = this.files.filter(x => x.id != id);
-		},
-
-		updateFiles(files) {
-			this.files = files;
-		},
-
-		updateFileSensitive(file, sensitive) {
-			this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
-		},
-
-		updateFileName(file, name) {
-			this.files[this.files.findIndex(x => x.id === file.id)].name = name;
-		},
-
-		upload(file: File, name?: string) {
-			os.upload(file, this.$store.state.uploadFolder, name).then(res => {
-				this.files.push(res);
-			});
-		},
-
-		onPollUpdate(poll) {
-			this.poll = poll;
-			this.saveDraft();
-		},
-
-		setVisibility() {
-			if (this.channel) {
-				// TODO: information dialog
-				return;
-			}
-
-			os.popup(import('@client/components/visibility-picker.vue'), {
-				currentVisibility: this.visibility,
-				currentLocalOnly: this.localOnly,
-				src: this.$refs.visibilityButton
-			}, {
-				changeVisibility: visibility => {
-					this.visibility = visibility;
-					if (this.$store.state.rememberNoteVisibility) {
-						this.$store.set('visibility', visibility);
-					}
-				},
-				changeLocalOnly: localOnly => {
-					this.localOnly = localOnly;
-					if (this.$store.state.rememberNoteVisibility) {
-						this.$store.set('localOnly', localOnly);
-					}
-				}
-			}, 'closed');
-		},
-
-		addVisibleUser() {
-			os.selectUser().then(user => {
-				this.visibleUsers.push(user);
-			});
-		},
-
-		removeVisibleUser(user) {
-			this.visibleUsers = erase(user, this.visibleUsers);
-		},
-
-		clear() {
-			this.text = '';
-			this.files = [];
-			this.poll = null;
-			this.quoteId = null;
-		},
-
-		onKeydown(e: KeyboardEvent) {
-			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
-			if (e.which === 27) this.$emit('esc');
-			this.typing();
-		},
-
-		onCompositionUpdate(e: CompositionEvent) {
-			this.imeText = e.data;
-			this.typing();
-		},
-
-		onCompositionEnd(e: CompositionEvent) {
-			this.imeText = '';
-		},
-
-		async onPaste(e: ClipboardEvent) {
-			for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
-				if (item.kind == 'file') {
-					const file = item.getAsFile();
-					const lio = file.name.lastIndexOf('.');
-					const ext = lio >= 0 ? file.name.slice(lio) : '';
-					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
-					this.upload(file, formatted);
-				}
-			}
-
-			const paste = e.clipboardData.getData('text');
-
-			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
-				e.preventDefault();
-
-				os.dialog({
-					type: 'info',
-					text: this.$ts.quoteQuestion,
-					showCancelButton: true
-				}).then(({ canceled }) => {
-					if (canceled) {
-						insertTextAtCursor(this.$refs.text, paste);
-						return;
-					}
-
-					this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
-				});
-			}
-		},
-
-		onDragover(e) {
-			if (!e.dataTransfer.items[0]) return;
-			const isFile = e.dataTransfer.items[0].kind == 'file';
-			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
-			if (isFile || isDriveFile) {
-				e.preventDefault();
-				this.draghover = true;
-				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
-			}
-		},
-
-		onDragenter(e) {
-			this.draghover = true;
-		},
-
-		onDragleave(e) {
-			this.draghover = false;
-		},
-
-		onDrop(e): void {
-			this.draghover = false;
-
-			// ファイルだったら
-			if (e.dataTransfer.files.length > 0) {
-				e.preventDefault();
-				for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
-				return;
-			}
-
-			//#region ドライブのファイル
-			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
-			if (driveFile != null && driveFile != '') {
-				const file = JSON.parse(driveFile);
-				this.files.push(file);
-				e.preventDefault();
-			}
-			//#endregion
-		},
-
-		saveDraft() {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			data[this.draftKey] = {
-				updatedAt: new Date(),
-				data: {
-					text: this.text,
-					useCw: this.useCw,
-					cw: this.cw,
-					visibility: this.visibility,
-					localOnly: this.localOnly,
-					files: this.files,
-					poll: this.poll
-				}
-			};
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		},
-
-		deleteDraft() {
-			const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
-			delete data[this.draftKey];
-
-			localStorage.setItem('drafts', JSON.stringify(data));
-		},
-
-		async post() {
-			let data = {
-				text: this.text == '' ? undefined : this.text,
-				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
-				replyId: this.reply ? this.reply.id : undefined,
-				renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
-				channelId: this.channel ? this.channel : undefined,
-				poll: this.poll,
-				cw: this.useCw ? this.cw || '' : undefined,
-				localOnly: this.localOnly,
-				visibility: this.visibility,
-				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
-				viaMobile: isMobile
-			};
-
-			// plugin
-			if (notePostInterruptors.length > 0) {
-				for (const interruptor of notePostInterruptors) {
-					data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
-				}
-			}
-
-			this.posting = true;
-			os.api('notes/create', data).then(() => {
-				this.clear();
-				this.$nextTick(() => {
-					this.deleteDraft();
-					this.$emit('posted');
-					if (this.text && this.text != '') {
-						const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
-						const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
-						localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
-					}
-					this.posting = false;
-				});
-			}).catch(err => {
-				this.posting = false;
-				os.dialog({
-					type: 'error',
-					text: err.message + '\n' + (err as any).id,
-				});
-			});
-		},
-
-		cancel() {
-			this.$emit('cancel');
-		},
-
-		insertMention() {
-			os.selectUser().then(user => {
-				insertTextAtCursor(this.$refs.text, '@' + getAcct(user) + ' ');
-			});
-		},
-
-		async insertEmoji(ev) {
-			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
-		},
-
-		showActions(ev) {
-			os.popupMenu(postFormActions.map(action => ({
-				text: action.title,
-				action: () => {
-					action.handler({
-						text: this.text
-					}, (key, value) => {
-						if (key === 'text') { this.text = value; }
-					});
-				}
-			})), ev.currentTarget || ev.target);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.pxiwixjf {
-	position: relative;
-	border: solid 0.5px var(--divider);
-	border-radius: 8px;
-
-	> .form {
-		> .preview {
-			padding: 16px;
-		}
-
-		> .with-quote {
-			margin: 0 0 8px 0;
-			color: var(--accent);
-
-			> button {
-				padding: 4px 8px;
-				color: var(--accentAlpha04);
-
-				&:hover {
-					color: var(--accentAlpha06);
-				}
-
-				&:active {
-					color: var(--accentDarken30);
-				}
-			}
-		}
-
-		> .to-specified {
-			padding: 6px 24px;
-			margin-bottom: 8px;
-			overflow: auto;
-			white-space: nowrap;
-
-			> .visibleUsers {
-				display: inline;
-				top: -1px;
-				font-size: 14px;
-
-				> button {
-					padding: 4px;
-					border-radius: 8px;
-				}
-
-				> span {
-					margin-right: 14px;
-					padding: 8px 0 8px 8px;
-					border-radius: 8px;
-					background: var(--X4);
-
-					> button {
-						padding: 4px 8px;
-					}
-				}
-			}
-		}
-
-		> .cw,
-		> .text {
-			display: block;
-			box-sizing: border-box;
-			padding: 16px;
-			margin: 0;
-			width: 100%;
-			font-size: 16px;
-			border: none;
-			border-radius: 0;
-			background: transparent;
-			color: var(--fg);
-			font-family: inherit;
-
-			&:focus {
-				outline: none;
-			}
-
-			&:disabled {
-				opacity: 0.5;
-			}
-		}
-
-		> .cw {
-			z-index: 1;
-			padding-bottom: 8px;
-			border-bottom: solid 0.5px var(--divider);
-		}
-
-		> .text {
-			max-width: 100%;
-			min-width: 100%;
-			min-height: 60px;
-
-			&.withCw {
-				padding-top: 8px;
-			}
-		}
-
-		> footer {
-			$height: 44px;
-			display: flex;
-			padding: 0 8px 8px 8px;
-			line-height: $height;
-
-			> .left {
-				> button {
-					display: inline-block;
-					padding: 0;
-					margin: 0;
-					font-size: 16px;
-					width: $height;
-					height: $height;
-					border-radius: 6px;
-
-					&:hover {
-						background: var(--X5);
-					}
-
-					&.active {
-						color: var(--accent);
-					}
-				}
-			}
-
-			> .right {
-				margin-left: auto;
-
-				> .text-count {
-					opacity: 0.7;
-				}
-
-				> .visibility {
-					width: $height;
-					margin: 0 8px;
-
-					& + .localOnly {
-						margin-left: 0 !important;
-					}
-				}
-				
-				> .local-only {
-					margin: 0 0 0 12px;
-					opacity: 0.7;
-				}
-
-				> .submit {
-					margin: 0;
-					padding: 0 12px;
-					line-height: 34px;
-					font-weight: bold;
-					border-radius: 4px;
-
-					&:disabled {
-						opacity: 0.7;
-					}
-
-					> i {
-						margin-left: 6px;
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/chat/side.vue b/src/client/ui/chat/side.vue
deleted file mode 100644
index a8a538c75b..0000000000
--- a/src/client/ui/chat/side.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<template>
-<div class="mrajymqm _narrow_" v-if="component">
-	<header class="header" @contextmenu.prevent.stop="onContextmenu">
-		<MkHeader class="title" :info="pageInfo" :center="false"/>
-	</header>
-	<component :is="component" v-bind="props" :ref="changePage" class="body"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { resolve } from '@client/router';
-import { url } from '@client/config';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-	},
-
-	provide() {
-		return {
-			navHook: (path) => {
-				this.navigate(path);
-			}
-		};
-	},
-
-	data() {
-		return {
-			path: null,
-			component: null,
-			props: {},
-			pageInfo: null,
-			history: [],
-		};
-	},
-
-	computed: {
-		url(): string {
-			return url + this.path;
-		}
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		navigate(path, record = true) {
-			if (record && this.path) this.history.push(this.path);
-			this.path = path;
-			const { component, props } = resolve(path);
-			this.component = component;
-			this.props = props;
-			this.$emit('open');
-		},
-
-		back() {
-			this.navigate(this.history.pop(), false);
-		},
-
-		close() {
-			this.path = null;
-			this.component = null;
-			this.props = {};
-			this.$emit('close');
-		},
-
-		onContextmenu(e) {
-			os.contextMenu([{
-				type: 'label',
-				text: this.path,
-			}, {
-				icon: 'fas fa-expand-alt',
-				text: this.$ts.showInPage,
-				action: () => {
-					this.$router.push(this.path);
-					this.close();
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(this.path);
-					this.close();
-				}
-			}, null, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.openInNewTab,
-				action: () => {
-					window.open(this.url, '_blank');
-					this.close();
-				}
-			}, {
-				icon: 'fas fa-link',
-				text: this.$ts.copyLink,
-				action: () => {
-					copyToClipboard(this.url);
-				}
-			}], e);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mrajymqm {
-	$header-height: 54px; // TODO: どこかに集約したい
-
-	--root-margin: 16px;
-	--margin: var(--marginHalf);
-
-	height: 100%;
-	overflow: auto;
-	box-sizing: border-box;
-
-	> .header {
-		display: flex;
-		position: sticky;
-		z-index: 1000;
-		top: 0;
-		height: $header-height;
-		width: 100%;
-		font-weight: bold;
-		//background-color: var(--panel);
-		-webkit-backdrop-filter: var(--blur, blur(32px));
-		backdrop-filter: var(--blur, blur(32px));
-		background-color: var(--header);
-		border-bottom: solid 0.5px var(--divider);
-		box-sizing: border-box;
-
-		> ._button {
-			height: $header-height;
-			width: $header-height;
-
-			&:hover {
-				color: var(--fgHighlighted);
-			}
-		}
-
-		> .title {
-			flex: 1;
-			position: relative;
-		}
-	}
-
-	> .body {
-
-	}
-}
-</style>
-
diff --git a/src/client/ui/chat/store.ts b/src/client/ui/chat/store.ts
deleted file mode 100644
index 389d56afb6..0000000000
--- a/src/client/ui/chat/store.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { markRaw } from 'vue';
-import { Storage } from '../../pizzax';
-
-export const store = markRaw(new Storage('chatUi', {
-	widgets: {
-		where: 'account',
-		default: [] as {
-			name: string;
-			id: string;
-			data: Record<string, any>;
-		}[]
-	},
-	tl: {
-		where: 'deviceAccount',
-		default: 'home'
-	},
-}));
diff --git a/src/client/ui/chat/sub-note-content.vue b/src/client/ui/chat/sub-note-content.vue
deleted file mode 100644
index 8a3cf1160f..0000000000
--- a/src/client/ui/chat/sub-note-content.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="wrmlmaau">
-	<div class="body">
-		<span v-if="note.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
-		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $ts.deleted }})</span>
-		<MkA class="reply" v-if="note.replyId" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
-		<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
-		<MkA class="rp" v-if="note.renoteId" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
-	</div>
-	<details v-if="note.files.length > 0">
-		<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
-		<XMediaList :media-list="note.files"/>
-	</details>
-	<details v-if="note.poll">
-		<summary>{{ $ts.poll }}</summary>
-		<XPoll :note="note"/>
-	</details>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XPoll from '@client/components/poll.vue';
-import XMediaList from '@client/components/media-list.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XPoll,
-		XMediaList,
-	},
-	props: {
-		note: {
-			type: Object,
-			required: true
-		}
-	},
-	data() {
-		return {
-		};
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.wrmlmaau {
-	overflow-wrap: break-word;
-
-	> .body {
-		> .reply {
-			margin-right: 6px;
-			color: var(--accent);
-		}
-
-		> .rp {
-			margin-left: 4px;
-			font-style: oblique;
-			color: var(--renote);
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/chat/widgets.vue b/src/client/ui/chat/widgets.vue
deleted file mode 100644
index 4d1865f616..0000000000
--- a/src/client/ui/chat/widgets.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="qydbhufi">
-	<XWidgets :edit="edit" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
-
-	<button v-if="edit" @click="edit = false" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgetsExit }}</button>
-	<button v-else @click="edit = true" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgets }}</button>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import XWidgets from '@client/components/widgets.vue';
-import { store } from './store';
-
-export default defineComponent({
-	components: {
-		XWidgets,
-	},
-
-	data() {
-		return {
-			edit: false,
-			widgets: store.reactiveState.widgets
-		};
-	},
-
-	methods: {
-		addWidget(widget) {
-			store.set('widgets', [widget, ...store.state.widgets]);
-		},
-
-		removeWidget(widget) {
-			store.set('widgets', store.state.widgets.filter(w => w.id != widget.id));
-		},
-
-		updateWidget({ id, data }) {
-			// TODO: throttleしたい
-			store.set('widgets', store.state.widgets.map(w => w.id === id ? {
-				...w,
-				data: data
-			} : w));
-		},
-
-		updateWidgets(widgets) {
-			store.set('widgets', widgets);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.qydbhufi {
-	height: 100%;
-	box-sizing: border-box;
-	overflow: auto;
-	padding: var(--margin);
-
-	::v-deep(._panel) {
-		box-shadow: none;
-	}
-}
-</style>
diff --git a/src/client/ui/classic.header.vue b/src/client/ui/classic.header.vue
deleted file mode 100644
index ad5080e3f6..0000000000
--- a/src/client/ui/classic.header.vue
+++ /dev/null
@@ -1,210 +0,0 @@
-<template>
-<div class="azykntjl">
-	<div class="body">
-		<div class="left">
-			<MkA class="item index" active-class="active" to="/" exact v-click-anime v-tooltip="$ts.timeline">
-				<i class="fas fa-home fa-fw"></i>
-			</MkA>
-			<template v-for="item in menu">
-				<div v-if="item === '-'" class="divider"></div>
-				<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime v-tooltip="$ts[menuDef[item].title]">
-					<i class="fa-fw" :class="menuDef[item].icon"></i>
-					<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
-				</component>
-			</template>
-			<div class="divider"></div>
-			<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.controlPanel">
-				<i class="fas fa-door-open fa-fw"></i>
-			</MkA>
-			<button class="item _button" @click="more" v-click-anime>
-				<i class="fas fa-ellipsis-h fa-fw"></i>
-				<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
-			</button>
-		</div>
-		<div class="right">
-			<MkA class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.settings">
-				<i class="fas fa-cog fa-fw"></i>
-			</MkA>
-			<button class="item _button account" @click="openAccountMenu" v-click-anime>
-				<MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/>
-			</button>
-			<div class="post" @click="post">
-				<MkButton class="button" gradate full rounded>
-					<i class="fas fa-pencil-alt fa-fw"></i>
-				</MkButton>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { host } from '@client/config';
-import { search } from '@client/scripts/search';
-import * as os from '@client/os';
-import { menuDef } from '@client/menu';
-import { openAccountMenu } from '@client/account';
-import MkButton from '@client/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		MkButton,
-	},
-
-	data() {
-		return {
-			host: host,
-			accounts: [],
-			connection: null,
-			menuDef: menuDef,
-			settingsWindowed: false,
-		};
-	},
-
-	computed: {
-		menu(): string[] {
-			return this.$store.state.menu;
-		},
-
-		otherNavItemIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (this.menu.includes(def)) continue;
-				if (this.menuDef[def].indicated) return true;
-			}
-			return false;
-		},
-	},
-
-	watch: {
-		'$store.reactiveState.menuDisplay.value'() {
-			this.calcViewState();
-		},
-	},
-
-	created() {
-		window.addEventListener('resize', this.calcViewState);
-		this.calcViewState();
-	},
-
-	methods: {
-		calcViewState() {
-			this.settingsWindowed = (window.innerWidth > 1400);
-		},
-
-		post() {
-			os.post();
-		},
-
-		search() {
-			search();
-		},
-
-		more(ev) {
-			os.popup(import('@client/components/launch-pad.vue'), {}, {
-			}, 'closed');
-		},
-
-		openAccountMenu,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.azykntjl {
-	$height: 60px;
-	$avatar-size: 32px;
-	$avatar-margin: 8px;
-
-	position: sticky;
-	top: 0;
-	z-index: 1000;
-	width: 100%;
-	height: $height;
-	background-color: var(--bg);
-
-	> .body {
-		max-width: 1380px;
-		margin: 0 auto;
-		display: flex;
-
-		> .right,
-		> .left {
-
-			> .item {
-				position: relative;
-				font-size: 0.9em;
-				display: inline-block;
-				padding: 0 12px;
-				line-height: $height;
-
-				> i,
-				> .avatar {
-					margin-right: 0;
-				}
-
-				> i {
-					left: 10px;
-				}
-
-				> .avatar {
-					width: $avatar-size;
-					height: $avatar-size;
-					vertical-align: middle;
-				}
-
-				> .indicator {
-					position: absolute;
-					top: 0;
-					left: 0;
-					color: var(--navIndicator);
-					font-size: 8px;
-					animation: blink 1s infinite;
-				}
-
-				&:hover {
-					text-decoration: none;
-					color: var(--navHoverFg);
-				}
-
-				&.active {
-					color: var(--navActive);
-				}
-			}
-
-			> .divider {
-				display: inline-block;
-				height: 16px;
-				margin: 0 10px;
-				border-right: solid 0.5px var(--divider);
-			}
-
-			> .post {
-				display: inline-block;
-			
-				> .button {
-					width: 40px;
-					height: 40px;
-					padding: 0;
-					min-width: 0;
-				}
-			}
-
-			> .account {
-				display: inline-flex;
-				align-items: center;
-				vertical-align: top;
-				margin-right: 8px;
-
-				> .acct {
-					margin-left: 8px;
-				}
-			}
-		}
-
-		> .right {
-			margin-left: auto;
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/classic.side.vue b/src/client/ui/classic.side.vue
deleted file mode 100644
index c7d2abff26..0000000000
--- a/src/client/ui/classic.side.vue
+++ /dev/null
@@ -1,158 +0,0 @@
-<template>
-<div class="qvzfzxam _narrow_" v-if="component">
-	<div class="container">
-		<header class="header" @contextmenu.prevent.stop="onContextmenu">
-			<button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
-			<button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
-			<span class="title">{{ pageInfo.title }}</span>
-			<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
-		</header>
-		<MkHeader class="pageHeader" :info="pageInfo"/>
-		<component :is="component" v-bind="props" :ref="changePage"/>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import copyToClipboard from '@client/scripts/copy-to-clipboard';
-import { resolve } from '@client/router';
-import { url } from '@client/config';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	provide() {
-		return {
-			navHook: (path) => {
-				this.navigate(path);
-			}
-		};
-	},
-
-	data() {
-		return {
-			path: null,
-			component: null,
-			props: {},
-			pageInfo: null,
-			history: [],
-		};
-	},
-
-	computed: {
-		url(): string {
-			return url + this.path;
-		}
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		navigate(path, record = true) {
-			if (record && this.path) this.history.push(this.path);
-			this.path = path;
-			const { component, props } = resolve(path);
-			this.component = component;
-			this.props = props;
-		},
-
-		back() {
-			this.navigate(this.history.pop(), false);
-		},
-
-		close() {
-			this.path = null;
-			this.component = null;
-			this.props = {};
-		},
-
-		onContextmenu(e) {
-			os.contextMenu([{
-				type: 'label',
-				text: this.path,
-			}, {
-				icon: 'fas fa-expand-alt',
-				text: this.$ts.showInPage,
-				action: () => {
-					this.$router.push(this.path);
-					this.close();
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(this.path);
-					this.close();
-				}
-			}, null, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.openInNewTab,
-				action: () => {
-					window.open(this.url, '_blank');
-					this.close();
-				}
-			}, {
-				icon: 'fas fa-link',
-				text: this.$ts.copyLink,
-				action: () => {
-					copyToClipboard(this.url);
-				}
-			}], e);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.qvzfzxam {
-	$header-height: 58px; // TODO: どこかに集約したい
-
-	--root-margin: 16px;
-	--margin: var(--marginHalf);
-
-	> .container {
-		position: fixed;
-		width: 370px;
-		height: 100vh;
-		overflow: auto;
-		box-sizing: border-box;
-
-		> .header {
-			display: flex;
-			position: sticky;
-			z-index: 1000;
-			top: 0;
-			height: $header-height;
-			width: 100%;
-			line-height: $header-height;
-			text-align: center;
-			font-weight: bold;
-			//background-color: var(--panel);
-			-webkit-backdrop-filter: var(--blur, blur(32px));
-			backdrop-filter: var(--blur, blur(32px));
-			background-color: var(--header);
-
-			> ._button {
-				height: $header-height;
-				width: $header-height;
-
-				&:hover {
-					color: var(--fgHighlighted);
-				}
-			}
-
-			> .title {
-				flex: 1;
-				position: relative;
-			}
-		}
-	}
-}
-</style>
-
diff --git a/src/client/ui/classic.sidebar.vue b/src/client/ui/classic.sidebar.vue
deleted file mode 100644
index ac061d446b..0000000000
--- a/src/client/ui/classic.sidebar.vue
+++ /dev/null
@@ -1,263 +0,0 @@
-<template>
-<div class="npcljfve" :class="{ iconOnly }">
-	<button class="item _button account" @click="openAccountMenu" v-click-anime>
-		<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
-	</button>
-	<div class="post" @click="post" data-cy-open-post-form>
-		<MkButton class="button" gradate full rounded>
-			<i class="fas fa-pencil-alt fa-fw"></i><span class="text" v-if="!iconOnly">{{ $ts.note }}</span>
-		</MkButton>
-	</div>
-	<div class="divider"></div>
-	<MkA class="item index" active-class="active" to="/" exact v-click-anime>
-		<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
-	</MkA>
-	<template v-for="item in menu">
-		<div v-if="item === '-'" class="divider"></div>
-		<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime>
-			<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
-			<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
-		</component>
-	</template>
-	<div class="divider"></div>
-	<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
-		<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
-	</MkA>
-	<button class="item _button" @click="more" v-click-anime>
-		<i class="fas fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
-		<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
-	</button>
-	<MkA class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
-		<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
-	</MkA>
-	<div class="divider"></div>
-	<div class="about">
-		<MkA class="link" to="/about" v-click-anime>
-			<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" class="_ghost"/>
-		</MkA>
-	</div>
-	<!--<MisskeyLogo class="misskey"/>-->
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { host } from '@client/config';
-import { search } from '@client/scripts/search';
-import * as os from '@client/os';
-import { menuDef } from '@client/menu';
-import { openAccountMenu } from '@client/account';
-import MkButton from '@client/components/ui/button.vue';
-import { StickySidebar } from '@client/scripts/sticky-sidebar';
-import MisskeyLogo from '@/../assets/client/misskey.svg';
-
-export default defineComponent({
-	components: {
-		MkButton,
-		MisskeyLogo,
-	},
-
-	data() {
-		return {
-			host: host,
-			accounts: [],
-			connection: null,
-			menuDef: menuDef,
-			iconOnly: false,
-			settingsWindowed: false,
-		};
-	},
-
-	computed: {
-		menu(): string[] {
-			return this.$store.state.menu;
-		},
-
-		otherNavItemIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (this.menu.includes(def)) continue;
-				if (this.menuDef[def].indicated) return true;
-			}
-			return false;
-		},
-	},
-
-	watch: {
-		'$store.reactiveState.menuDisplay.value'() {
-			this.calcViewState();
-		},
-
-		iconOnly() {
-			this.$nextTick(() => {
-				this.$emit('change-view-mode');
-			});
-		},
-	},
-
-	created() {
-		window.addEventListener('resize', this.calcViewState);
-		this.calcViewState();
-	},
-
-	mounted() {
-		const sticky = new StickySidebar(this.$el.parentElement, 16);
-		window.addEventListener('scroll', () => {
-			sticky.calc(window.scrollY);
-		}, { passive: true });
-	},
-
-	methods: {
-		calcViewState() {
-			this.iconOnly = (window.innerWidth <= 1400) || (this.$store.state.menuDisplay === 'sideIcon');
-			this.settingsWindowed = (window.innerWidth > 1400);
-		},
-
-		post() {
-			os.post();
-		},
-
-		search() {
-			search();
-		},
-
-		more(ev) {
-			os.popup(import('@client/components/launch-pad.vue'), {}, {
-			}, 'closed');
-		},
-
-		openAccountMenu,
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.npcljfve {
-	$ui-font-size: 1em; // TODO: どこかに集約したい
-	$nav-icon-only-width: 78px; // TODO: どこかに集約したい
-	$avatar-size: 32px;
-	$avatar-margin: 8px;
-
-	padding: 0 16px;
-	box-sizing: border-box;
-	width: 260px;
-
-	&.iconOnly {
-		flex: 0 0 $nav-icon-only-width;
-		width: $nav-icon-only-width !important;
-
-		> .divider {
-			margin: 8px auto;
-			width: calc(100% - 32px);
-		}
-
-		> .post {
-			> .button {
-				width: 46px;
-				height: 46px;
-				padding: 0;
-			}
-		}
-
-		> .item {
-			padding-left: 0;
-			width: 100%;
-			text-align: center;
-			font-size: $ui-font-size * 1.1;
-			line-height: 3.7rem;
-
-			> i,
-			> .avatar {
-				margin-right: 0;
-			}
-
-			> i {
-				left: 10px;
-			}
-
-			> .text {
-				display: none;
-			}
-		}
-	}
-
-	> .divider {
-		margin: 10px 0;
-		border-top: solid 0.5px var(--divider);
-	}
-
-	> .post {
-		position: sticky;
-		top: 0;
-		z-index: 1;
-		padding: 16px 0;
-		background: var(--bg);
-
-		> .button {
-			min-width: 0;
-		}
-	}
-
-	> .about {
-		fill: currentColor;
-		padding: 8px 0 16px 0;
-		text-align: center;
-
-		> .link {
-			display: block;
-			width: 32px;
-			margin: 0 auto;
-
-			img {
-				display: block;
-				width: 100%;
-			}
-		}
-	}
-
-	> .item {
-		position: relative;
-		display: block;
-		font-size: $ui-font-size;
-		line-height: 2.6rem;
-		text-overflow: ellipsis;
-		overflow: hidden;
-		white-space: nowrap;
-		width: 100%;
-		text-align: left;
-		box-sizing: border-box;
-
-		> i {
-			width: 32px;
-		}
-
-		> i,
-		> .avatar {
-			margin-right: $avatar-margin;
-		}
-
-		> .avatar {
-			width: $avatar-size;
-			height: $avatar-size;
-			vertical-align: middle;
-		}
-
-		> .indicator {
-			position: absolute;
-			top: 0;
-			left: 0;
-			color: var(--navIndicator);
-			font-size: 8px;
-			animation: blink 1s infinite;
-		}
-
-		&:hover {
-			text-decoration: none;
-			color: var(--navHoverFg);
-		}
-
-		&.active {
-			color: var(--navActive);
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/classic.vue b/src/client/ui/classic.vue
deleted file mode 100644
index 79cdbe3af1..0000000000
--- a/src/client/ui/classic.vue
+++ /dev/null
@@ -1,471 +0,0 @@
-<template>
-<div class="mk-app" :class="{ wallpaper, isMobile }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
-	<XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/>
-
-	<div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }">
-		<template v-if="!isMobile">
-			<div class="sidebar" v-if="!showMenuOnTop">
-				<XSidebar/>
-			</div>
-			<div class="widgets left" ref="widgetsLeft" v-else>
-				<XWidgets @mounted="attachSticky('widgetsLeft')" :place="'left'"/>
-			</div>
-		</template>
-
-		<main class="main" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }">
-			<div class="content">
-				<MkStickyContainer>
-					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
-					<router-view v-slot="{ Component }">
-						<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-							<keep-alive :include="['timeline']">
-								<component :is="Component" :ref="changePage"/>
-							</keep-alive>
-						</transition>
-					</router-view>
-				</MkStickyContainer>
-			</div>
-		</main>
-
-		<div v-if="isDesktop" class="widgets right" ref="widgetsRight">
-			<XWidgets @mounted="attachSticky('widgetsRight')" :place="null"/>
-		</div>
-	</div>
-
-	<div class="buttons" v-if="isMobile">
-		<button class="button nav _button" @click="showDrawerNav" ref="navButton"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
-		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
-		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
-		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
-		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
-	</div>
-
-	<XDrawerSidebar ref="drawerNav" class="sidebar" v-if="isMobile"/>
-
-	<transition name="tray-back">
-		<div class="tray-back _modalBg"
-			v-if="widgetsShowing"
-			@click="widgetsShowing = false"
-			@touchstart.passive="widgetsShowing = false"
-		></div>
-	</transition>
-
-	<transition name="tray">
-		<XWidgets v-if="widgetsShowing" class="tray"/>
-	</transition>
-
-	<iframe v-if="$store.state.aiChanMode" class="ivnzpscs" ref="live2d" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe>
-
-	<XCommon/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
-import { instanceName } from '@client/config';
-import { StickySidebar } from '@client/scripts/sticky-sidebar';
-import XSidebar from './classic.sidebar.vue';
-import XDrawerSidebar from '@client/ui/_common_/sidebar.vue';
-import XCommon from './_common_/common.vue';
-import * as os from '@client/os';
-import { menuDef } from '@client/menu';
-import * as symbols from '@client/symbols';
-
-const DESKTOP_THRESHOLD = 1100;
-const MOBILE_THRESHOLD = 600;
-
-export default defineComponent({
-	components: {
-		XCommon,
-		XSidebar,
-		XDrawerSidebar,
-		XHeaderMenu: defineAsyncComponent(() => import('./classic.header.vue')),
-		XWidgets: defineAsyncComponent(() => import('./classic.widgets.vue')),
-	},
-
-	provide() {
-		return {
-			shouldHeaderThin: this.showMenuOnTop,
-		};
-	},
-
-	data() {
-		return {
-			pageInfo: null,
-			menuDef: menuDef,
-			globalHeaderHeight: 0,
-			isMobile: window.innerWidth <= MOBILE_THRESHOLD,
-			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
-			widgetsShowing: false,
-			fullView: false,
-			wallpaper: localStorage.getItem('wallpaper') != null,
-		};
-	},
-
-	computed: {
-		navIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
-				if (this.menuDef[def].indicated) return true;
-			}
-			return false;
-		},
-
-		showMenuOnTop(): boolean {
-			return !this.isMobile && this.$store.state.menuDisplay === 'top';
-		}
-	},
-
-	created() {
-		document.documentElement.style.overflowY = 'scroll';
-
-		if (this.$store.state.widgets.length === 0) {
-			this.$store.set('widgets', [{
-				name: 'calendar',
-				id: 'a', place: null, data: {}
-			}, {
-				name: 'notifications',
-				id: 'b', place: null, data: {}
-			}, {
-				name: 'trends',
-				id: 'c', place: null, data: {}
-			}]);
-		}
-	},
-
-	mounted() {
-		window.addEventListener('resize', () => {
-			this.isMobile = (window.innerWidth <= MOBILE_THRESHOLD);
-			this.isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD);
-		}, { passive: true });
-
-		if (this.$store.state.aiChanMode) {
-			const iframeRect = this.$refs.live2d.getBoundingClientRect();
-			window.addEventListener('mousemove', ev => {
-				this.$refs.live2d.contentWindow.postMessage({
-					type: 'moveCursor',
-					body: {
-						x: ev.clientX - iframeRect.left,
-						y: ev.clientY - iframeRect.top,
-					}
-				}, '*');
-			}, { passive: true });
-			window.addEventListener('touchmove', ev => {
-				this.$refs.live2d.contentWindow.postMessage({
-					type: 'moveCursor',
-					body: {
-						x: ev.touches[0].clientX - iframeRect.left,
-						y: ev.touches[0].clientY - iframeRect.top,
-					}
-				}, '*');
-			}, { passive: true });
-		}
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-				document.title = `${this.pageInfo.title} | ${instanceName}`;
-			}
-		},
-
-		attachSticky(ref) {
-			const sticky = new StickySidebar(this.$refs[ref], this.$store.state.menuDisplay === 'top' ? 0 : 16, this.$store.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
-			window.addEventListener('scroll', () => {
-				sticky.calc(window.scrollY);
-			}, { passive: true });
-		},
-
-		post() {
-			os.post();
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		back() {
-			history.back();
-		},
-
-		showDrawerNav() {
-			this.$refs.drawerNav.show();
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
-			if (window.getSelection().toString() !== '') return;
-			const path = this.$route.path;
-			os.contextMenu([{
-				type: 'label',
-				text: path,
-			}, {
-				icon: this.fullView ? 'fas fa-compress' : 'fas fa-expand',
-				text: this.fullView ? this.$ts.quitFullView : this.$ts.fullView,
-				action: () => {
-					this.fullView = !this.fullView;
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(path);
-				}
-			}], e);
-		},
-
-		onAiClick(ev) {
-			//if (this.live2d) this.live2d.click(ev);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.tray-enter-active,
-.tray-leave-active {
-	opacity: 1;
-	transform: translateX(0);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.tray-enter-from,
-.tray-leave-active {
-	opacity: 0;
-	transform: translateX(240px);
-}
-
-.tray-back-enter-active,
-.tray-back-leave-active {
-	opacity: 1;
-	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.tray-back-enter-from,
-.tray-back-leave-active {
-	opacity: 0;
-}
-
-.mk-app {
-	$ui-font-size: 1em;
-	$widgets-hide-threshold: 1200px;
-	$nav-icon-only-width: 78px; // TODO: どこかに集約したい
-
-	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-	min-height: calc(var(--vh, 1vh) * 100);
-	box-sizing: border-box;
-
-	&.wallpaper {
-		background: var(--wallpaperOverlay);
-		//backdrop-filter: var(--blur, blur(4px));
-	}
-
-	&.isMobile {
-		> .columns {
-			display: block;
-			margin: 0;
-
-			> .main {
-				margin: 0;
-				padding-bottom: 92px;
-				border: none;
-				width: 100%;
-				border-radius: 0;
-			}
-		}
-	}
-
-	> .columns {
-		display: flex;
-		justify-content: center;
-		max-width: 100%;
-		//margin: 32px 0;
-
-		&.fullView {
-			margin: 0;
-		
-			> .sidebar {
-				display: none;
-			}
-
-			> .widgets {
-				display: none;
-			}
-
-			> .main {
-				margin: 0;
-				border-radius: 0;
-				box-shadow: none;
-				width: 100%;
-			}
-		}
-
-		> .main {
-			min-width: 0;
-			width: 750px;
-			margin: 0 16px 0 0;
-			background: var(--panel);
-			border-left: solid 1px var(--divider);
-			border-right: solid 1px var(--divider);
-			border-radius: 0;
-			overflow: clip;
-			--margin: 12px;
-		}
-
-		> .widgets {
-			//--panelBorder: none;
-			width: 300px;
-			margin-top: 16px;
-
-			@media (max-width: $widgets-hide-threshold) {
-				display: none;
-			}
-
-			&.left {
-				margin-right: 16px;
-			}
-		}
-
-		> .sidebar {
-			margin-top: 16px;
-		}
-
-		&.withGlobalHeader {
-			> .main {
-				margin-top: 0;
-				border: solid 1px var(--divider);
-				border-radius: var(--radius);
-				--stickyTop: var(--globalHeaderHeight);
-			}
-
-			> .widgets {
-				--stickyTop: var(--globalHeaderHeight);
-				margin-top: 0;
-			}
-		}
-
-		@media (max-width: 850px) {
-			margin: 0;
-
-			> .sidebar {
-				border-right: solid 0.5px var(--divider);
-			}
-
-			> .main {
-				margin: 0;
-				border-radius: 0;
-				box-shadow: none;
-				width: 100%;
-			}
-		}
-	}
-
-	> .buttons {
-		position: fixed;
-		z-index: 1000;
-		bottom: 0;
-		padding: 16px;
-		display: flex;
-		width: 100%;
-		box-sizing: border-box;
-		-webkit-backdrop-filter: var(--blur, blur(32px));
-		backdrop-filter: var(--blur, blur(32px));
-		background-color: var(--header);
-		border-top: solid 0.5px var(--divider);
-
-		> .button {
-			position: relative;
-			flex: 1;
-			padding: 0;
-			margin: auto;
-			height: 64px;
-			border-radius: 8px;
-			background: var(--panel);
-			color: var(--fg);
-
-			&:not(:last-child) {
-				margin-right: 12px;
-			}
-
-			@media (max-width: 400px) {
-				height: 60px;
-
-				&:not(:last-child) {
-					margin-right: 8px;
-				}
-			}
-
-			&:hover {
-				background: var(--X2);
-			}
-
-			> .indicator {
-				position: absolute;
-				top: 0;
-				left: 0;
-				color: var(--indicator);
-				font-size: 16px;
-				animation: blink 1s infinite;
-			}
-
-			&:first-child {
-				margin-left: 0;
-			}
-
-			&:last-child {
-				margin-right: 0;
-			}
-
-			> * {
-				font-size: 22px;
-			}
-
-			&:disabled {
-				cursor: default;
-
-				> * {
-					opacity: 0.5;
-				}
-			}
-		}
-	}
-
-	> .tray-back {
-		z-index: 1001;
-	}
-
-	> .tray {
-		position: fixed;
-		top: 0;
-		right: 0;
-		z-index: 1001;
-		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-		height: calc(var(--vh, 1vh) * 100);
-		padding: var(--margin);
-		box-sizing: border-box;
-		overflow: auto;
-		background: var(--bg);
-	}
-
-	> .ivnzpscs {
-		position: fixed;
-		bottom: 0;
-		right: 0;
-		width: 300px;
-		height: 600px;
-		border: none;
-		pointer-events: none;
-	}
-}
-</style>
diff --git a/src/client/ui/classic.widgets.vue b/src/client/ui/classic.widgets.vue
deleted file mode 100644
index f9584402a2..0000000000
--- a/src/client/ui/classic.widgets.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<template>
-<div class="ddiqwdnk">
-	<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value.filter(w => w.place === place)" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
-	<MkAd class="a" :prefer="['square']"/>
-
-	<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
-	<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import XWidgets from '@client/components/widgets.vue';
-
-export default defineComponent({
-	components: {
-		XWidgets
-	},
-
-	props: {
-		place: {
-			type: String,
-		}
-	},
-
-	emits: ['mounted'],
-
-	data() {
-		return {
-			editMode: false,
-		};
-	},
-
-	mounted() {
-		this.$emit('mounted', this.$el);
-	},
-
-	methods: {
-		addWidget(widget) {
-			this.$store.set('widgets', [{
-				...widget,
-				place: this.place,
-			}, ...this.$store.state.widgets]);
-		},
-
-		removeWidget(widget) {
-			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
-		},
-
-		updateWidget({ id, data }) {
-			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? {
-				...w,
-				data: data
-			} : w));
-		},
-
-		updateWidgets(widgets) {
-			this.$store.set('widgets', [
-				...this.$store.state.widgets.filter(w => w.place !== this.place),
-				...widgets
-			]);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ddiqwdnk {
-	position: sticky;
-	height: min-content;
-	box-sizing: border-box;
-	padding-bottom: 8px;
-
-	> .widgets,
-	> .a {
-		width: 300px;
-	}
-
-	> .edit {
-		display: block;
-		margin: 16px auto;
-	}
-}
-</style>
diff --git a/src/client/ui/deck.vue b/src/client/ui/deck.vue
deleted file mode 100644
index 4b0189ba77..0000000000
--- a/src/client/ui/deck.vue
+++ /dev/null
@@ -1,229 +0,0 @@
-<template>
-<div class="mk-deck" :class="`${deckStore.reactiveState.columnAlign.value}`" @contextmenu.self.prevent="onContextmenu"
-	:style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
->
-	<XSidebar ref="nav"/>
-
-	<template v-for="ids in layout">
-		<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
-		<section v-if="ids.length > 1"
-			class="folder column"
-			:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
-		>
-			<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
-		</section>
-		<DeckColumnCore v-else
-			class="column"
-			:ref="ids[0]"
-			:key="ids[0]"
-			:column="columns.find(c => c.id === ids[0])"
-			@parent-focus="moveFocus(ids[0], $event)"
-			:style="columns.find(c => c.id === ids[0]).flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0]).width + 'px' }"
-		/>
-	</template>
-
-	<button v-if="$i" class="nav _button" @click="showNav()"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
-	<button v-if="$i" class="post _buttonPrimary" @click="post()"><i class="fas fa-pencil-alt"></i></button>
-
-	<XCommon/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-import { host } from '@client/config';
-import DeckColumnCore from '@client/ui/deck/column-core.vue';
-import XSidebar from '@client/ui/_common_/sidebar.vue';
-import { getScrollContainer } from '@client/scripts/scroll';
-import * as os from '@client/os';
-import { menuDef } from '@client/menu';
-import XCommon from './_common_/common.vue';
-import { deckStore, addColumn, loadDeck } from './deck/deck-store';
-
-export default defineComponent({
-	components: {
-		XCommon,
-		XSidebar,
-		DeckColumnCore,
-	},
-
-	provide() {
-		return deckStore.state.navWindow ? {
-			navHook: (url) => {
-				os.pageWindow(url);
-			}
-		} : {};
-	},
-
-	data() {
-		return {
-			deckStore,
-			host: host,
-			menuDef: menuDef,
-			wallpaper: localStorage.getItem('wallpaper') != null,
-		};
-	},
-
-	computed: {
-		columns() {
-			return deckStore.reactiveState.columns.value;
-		},
-		layout() {
-			return deckStore.reactiveState.layout.value;
-		},
-		navIndicated(): boolean {
-			if (!this.$i) return false;
-			for (const def in this.menuDef) {
-				if (this.menuDef[def].indicated) return true;
-			}
-			return false;
-		},
-	},
-
-	created() {
-		document.documentElement.style.overflowY = 'hidden';
-		document.documentElement.style.scrollBehavior = 'auto';
-		window.addEventListener('wheel', this.onWheel);
-		loadDeck();
-	},
-
-	mounted() {
-	},
-
-	methods: {
-		onWheel(e) {
-			if (getScrollContainer(e.target) == null) {
-				document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
-			}
-		},
-
-		showNav() {
-			this.$refs.nav.show();
-		},
-
-		post() {
-			os.post();
-		},
-
-		async addColumn(ev) {
-			const columns = [
-				'main',
-				'widgets',
-				'notifications',
-				'tl',
-				'antenna',
-				'list',
-				'mentions',
-				'direct',
-			];
-
-			const { canceled, result: column } = await os.dialog({
-				title: this.$ts._deck.addColumn,
-				type: null,
-				select: {
-					items: columns.map(column => ({
-						value: column, text: this.$t('_deck._columns.' + column)
-					}))
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-
-			addColumn({
-				type: column,
-				id: uuid(),
-				name: this.$t('_deck._columns.' + column),
-				width: 330,
-			});
-		},
-
-		onContextmenu(e) {
-			os.contextMenu([{
-				text: this.$ts._deck.addColumn,
-				icon: null,
-				action: this.addColumn
-			}], e);
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-deck {
-	$nav-hide-threshold: 650px; // TODO: どこかに集約したい
-
-	// TODO: ここではなくて、各カラムで自身の幅に応じて上書きするようにしたい
-	--margin: var(--marginHalf);
-
-	display: flex;
-	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-	height: calc(var(--vh, 1vh) * 100);
-	box-sizing: border-box;
-	flex: 1;
-	padding: var(--deckMargin);
-
-	&.center {
-		> .column:first-of-type {
-			margin-left: auto;
-		}
-
-		> .column:last-of-type {
-			margin-right: auto;
-		}
-	}
-
-	> .column {
-		flex-shrink: 0;
-		margin-right: var(--deckMargin);
-
-		&.folder {
-			display: flex;
-			flex-direction: column;
-
-			> *:not(:last-child) {
-				margin-bottom: var(--deckMargin);
-			}
-		}
-	}
-
-	> .post,
-	> .nav {
-		position: fixed;
-		z-index: 1000;
-		bottom: 32px;
-		width: 64px;
-		height: 64px;
-		border-radius: 100%;
-		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
-		font-size: 22px;
-
-		@media (min-width: ($nav-hide-threshold + 1px)) {
-			display: none;
-		}
-	}
-
-	> .post {
-		right: 32px;
-	}
-
-	> .nav {
-		left: 32px;
-		background: var(--panel);
-		color: var(--fg);
-
-		&:hover {
-			background: var(--X2);
-		}
-
-		> .indicator {
-			position: absolute;
-			top: 0;
-			left: 0;
-			color: var(--indicator);
-			font-size: 16px;
-			animation: blink 1s infinite;
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/deck/antenna-column.vue b/src/client/ui/deck/antenna-column.vue
deleted file mode 100644
index 3abd3d3a45..0000000000
--- a/src/client/ui/deck/antenna-column.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked">
-	<template #header>
-		<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
-	</template>
-
-	<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
-</XColumn>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XColumn from './column.vue';
-import XTimeline from '@client/components/timeline.vue';
-import * as os from '@client/os';
-import { updateColumn } from './deck-store';
-
-export default defineComponent({
-	components: {
-		XColumn,
-		XTimeline,
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	watch: {
-		mediaOnly() {
-			(this.$refs.timeline as any).reload();
-		}
-	},
-
-	mounted() {
-		if (this.column.antennaId == null) {
-			this.setAntenna();
-		}
-	},
-
-	methods: {
-		async setAntenna() {
-			const antennas = await os.api('antennas/list');
-			const { canceled, result: antenna } = await os.dialog({
-				title: this.$ts.selectAntenna,
-				type: null,
-				select: {
-					items: antennas.map(x => ({
-						value: x, text: x.name
-					})),
-				default: this.column.antennaId
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			updateColumn(this.column.id, {
-				antennaId: antenna.id
-			});
-		},
-
-		focus() {
-			(this.$refs.timeline as any).focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/src/client/ui/deck/column-core.vue b/src/client/ui/deck/column-core.vue
deleted file mode 100644
index 5393bac736..0000000000
--- a/src/client/ui/deck/column-core.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
-<!-- TODO: リファクタの余地がありそう -->
-<XMainColumn v-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="$emit('parent-focus', $event)"/>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XMainColumn from './main-column.vue';
-import XTlColumn from './tl-column.vue';
-import XAntennaColumn from './antenna-column.vue';
-import XListColumn from './list-column.vue';
-import XNotificationsColumn from './notifications-column.vue';
-import XWidgetsColumn from './widgets-column.vue';
-import XMentionsColumn from './mentions-column.vue';
-import XDirectColumn from './direct-column.vue';
-
-export default defineComponent({
-	components: {
-		XMainColumn,
-		XTlColumn,
-		XAntennaColumn,
-		XListColumn,
-		XNotificationsColumn,
-		XWidgetsColumn,
-		XMentionsColumn,
-		XDirectColumn
-	},
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: false,
-			default: false
-		}
-	},
-	methods: {
-		focus() {
-			this.$children[0].focus();
-		}
-	}
-});
-</script>
diff --git a/src/client/ui/deck/column.vue b/src/client/ui/deck/column.vue
deleted file mode 100644
index c04297e384..0000000000
--- a/src/client/ui/deck/column.vue
+++ /dev/null
@@ -1,408 +0,0 @@
-<template>
-<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
-<section class="dnpfarvg _panel _narrow_" :class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
-	@dragover.prevent.stop="onDragover"
-	@dragleave="onDragleave"
-	@drop.prevent.stop="onDrop"
-	v-hotkey="keymap"
-	:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
->
-	<header :class="{ indicated }"
-		draggable="true"
-		@click="goTop"
-		@dragstart="onDragstart"
-		@dragend="onDragend"
-		@contextmenu.prevent.stop="onContextmenu"
-	>
-		<button class="toggleActive _button" @click="toggleActive" v-if="isStacked && !isMainColumn">
-			<template v-if="active"><i class="fas fa-angle-up"></i></template>
-			<template v-else><i class="fas fa-angle-down"></i></template>
-		</button>
-		<div class="action">
-			<slot name="action"></slot>
-		</div>
-		<span class="header"><slot name="header"></slot></span>
-		<button v-if="func" class="menu _button" v-tooltip="func.title" @click.stop="func.handler"><i :class="func.icon || 'fas fa-cog'"></i></button>
-	</header>
-	<div ref="body" v-show="active">
-		<slot></slot>
-	</div>
-</section>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from './deck-store';
-import { deckStore } from './deck-store';
-
-export default defineComponent({
-	provide: {
-		shouldHeaderThin: true,
-		shouldOmitHeaderTitle: true,
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: false,
-			default: null
-		},
-		isStacked: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		func: {
-			type: Object,
-			required: false,
-			default: null
-		},
-		naked: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		indicated: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-
-	data() {
-		return {
-			deckStore,
-			dragging: false,
-			draghover: false,
-			dropready: false,
-		};
-	},
-
-	computed: {
-		isMainColumn(): boolean {
-			return this.column.type === 'main';
-		},
-
-		active(): boolean {
-			return this.column.active !== false;
-		},
-
-		keymap(): any {
-			return {
-				'shift+up': () => this.$parent.$emit('parent-focus', 'up'),
-				'shift+down': () => this.$parent.$emit('parent-focus', 'down'),
-				'shift+left': () => this.$parent.$emit('parent-focus', 'left'),
-				'shift+right': () => this.$parent.$emit('parent-focus', 'right'),
-			};
-		}
-	},
-
-	watch: {
-		active(v) {
-			this.$emit('change-active-state', v);
-		},
-
-		dragging(v) {
-			os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd');
-		}
-	},
-
-	mounted() {
-		os.deckGlobalEvents.on('column.dragStart', this.onOtherDragStart);
-		os.deckGlobalEvents.on('column.dragEnd', this.onOtherDragEnd);
-	},
-
-	beforeUnmount() {
-		os.deckGlobalEvents.off('column.dragStart', this.onOtherDragStart);
-		os.deckGlobalEvents.off('column.dragEnd', this.onOtherDragEnd);
-	},
-
-	methods: {
-		onOtherDragStart() {
-			this.dropready = true;
-		},
-
-		onOtherDragEnd() {
-			this.dropready = false;
-		},
-
-		toggleActive() {
-			if (!this.isStacked) return;
-			updateColumn(this.column.id, {
-				active: !this.column.active
-			});
-		},
-
-		getMenu() {
-			const items = [{
-				icon: 'fas fa-pencil-alt',
-				text: this.$ts.edit,
-				action: async () => {
-					const { canceled, result } = await os.form(this.column.name, {
-						name: {
-							type: 'string',
-							label: this.$ts.name,
-							default: this.column.name
-						},
-						width: {
-							type: 'number',
-							label: this.$ts.width,
-							default: this.column.width
-						},
-						flexible: {
-							type: 'boolean',
-							label: this.$ts.flexible,
-							default: this.column.flexible
-						}
-					});
-					if (canceled) return;
-					updateColumn(this.column.id, result);
-				}
-			}, null, {
-				icon: 'fas fa-arrow-left',
-				text: this.$ts._deck.swapLeft,
-				action: () => {
-					swapLeftColumn(this.column.id);
-				}
-			}, {
-				icon: 'fas fa-arrow-right',
-				text: this.$ts._deck.swapRight,
-				action: () => {
-					swapRightColumn(this.column.id);
-				}
-			}, this.isStacked ? {
-				icon: 'fas fa-arrow-up',
-				text: this.$ts._deck.swapUp,
-				action: () => {
-					swapUpColumn(this.column.id);
-				}
-			} : undefined, this.isStacked ? {
-				icon: 'fas fa-arrow-down',
-				text: this.$ts._deck.swapDown,
-				action: () => {
-					swapDownColumn(this.column.id);
-				}
-			} : undefined, null, {
-				icon: 'fas fa-window-restore',
-				text: this.$ts._deck.stackLeft,
-				action: () => {
-					stackLeftColumn(this.column.id);
-				}
-			}, this.isStacked ? {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts._deck.popRight,
-				action: () => {
-					popRightColumn(this.column.id);
-				}
-			} : undefined, null, {
-				icon: 'fas fa-trash-alt',
-				text: this.$ts.remove,
-				danger: true,
-				action: () => {
-					removeColumn(this.column.id);
-				}
-			}];
-
-			return items;
-		},
-
-		onContextmenu(e) {
-			os.contextMenu(this.getMenu(), e);
-		},
-
-		goTop() {
-			this.$refs.body.scrollTo({
-				top: 0,
-				behavior: 'smooth'
-			});
-		},
-
-		onDragstart(e) {
-			e.dataTransfer.effectAllowed = 'move';
-			e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, this.column.id);
-
-			// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
-			// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
-			setTimeout(() => {
-				this.dragging = true;
-			}, 10);
-		},
-
-		onDragend(e) {
-			this.dragging = false;
-		},
-
-		onDragover(e) {
-			// 自分自身がドラッグされている場合
-			if (this.dragging) {
-				// 自分自身にはドロップさせない
-				e.dataTransfer.dropEffect = 'none';
-				return;
-			}
-
-			const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_;
-
-			e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
-
-			if (!this.dragging && isDeckColumn) this.draghover = true;
-		},
-
-		onDragleave() {
-			this.draghover = false;
-		},
-
-		onDrop(e) {
-			this.draghover = false;
-			os.deckGlobalEvents.emit('column.dragEnd');
-
-			const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
-			if (id != null && id != '') {
-				swapColumn(this.column.id, id);
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.dnpfarvg {
-	--root-margin: 10px;
-
-	height: 100%;
-	overflow: hidden;
-	contain: content;
-	box-shadow: 0 0 8px 0 var(--shadow);
-
-	&.draghover {
-		box-shadow: 0 0 0 2px var(--focus);
-
-		&:after {
-			content: "";
-			display: block;
-			position: absolute;
-			z-index: 1000;
-			top: 0;
-			left: 0;
-			width: 100%;
-			height: 100%;
-			background: var(--focus);
-		}
-	}
-
-	&.dragging {
-		box-shadow: 0 0 0 2px var(--focus);
-	}
-
-	&.dropready {
-		* {
-			pointer-events: none;
-		}
-	}
-
-	&:not(.active) {
-		flex-basis: var(--deckColumnHeaderHeight);
-		min-height: var(--deckColumnHeaderHeight);
-
-		> header.indicated {
-			box-shadow: 4px 0px var(--accent) inset;
-		}
-	}
-
-	&.naked {
-		background: var(--acrylicBg) !important;
-		-webkit-backdrop-filter: var(--blur, blur(10px));
-		backdrop-filter: var(--blur, blur(10px));
-
-		> header {
-			background: transparent;
-			box-shadow: none;
-
-			> button {
-				color: var(--fg);
-			}
-		}
-	}
-
-	&.paged {
-		background: var(--bg) !important;
-	}
-
-	> header {
-		position: relative;
-		display: flex;
-		z-index: 2;
-		line-height: var(--deckColumnHeaderHeight);
-		height: var(--deckColumnHeaderHeight);
-		padding: 0 16px;
-		font-size: 0.9em;
-		color: var(--panelHeaderFg);
-		background: var(--panelHeaderBg);
-		box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
-		cursor: pointer;
-
-		&, * {
-			user-select: none;
-		}
-
-		&.indicated {
-			box-shadow: 0 3px 0 0 var(--accent);
-		}
-
-		> .header {
-			display: inline-block;
-			align-items: center;
-			overflow: hidden;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-		}
-
-		> span:only-of-type {
-			width: 100%;
-		}
-
-		> .toggleActive,
-		> .action > ::v-deep(*),
-		> .menu {
-			z-index: 1;
-			width: var(--deckColumnHeaderHeight);
-			line-height: var(--deckColumnHeaderHeight);
-			font-size: 16px;
-			color: var(--faceTextButton);
-
-			&:hover {
-				color: var(--faceTextButtonHover);
-			}
-
-			&:active {
-				color: var(--faceTextButtonActive);
-			}
-		}
-
-		> .toggleActive, > .action {
-			margin-left: -16px;
-		}
-
-		> .action {
-			z-index: 1;
-		}
-
-		> .action:empty {
-			display: none;
-		}
-
-		> .menu {
-			margin-left: auto;
-			margin-right: -16px;
-		}
-	}
-
-	> div {
-		height: calc(100% - var(--deckColumnHeaderHeight));
-		overflow: auto;
-		overflow-x: hidden;
-		-webkit-overflow-scrolling: touch;
-		box-sizing: border-box;
-	}
-}
-</style>
diff --git a/src/client/ui/deck/deck-store.ts b/src/client/ui/deck/deck-store.ts
deleted file mode 100644
index 6c61bf5539..0000000000
--- a/src/client/ui/deck/deck-store.ts
+++ /dev/null
@@ -1,298 +0,0 @@
-import { throttle } from 'throttle-debounce';
-import { i18n } from '@client/i18n';
-import { api } from '@client/os';
-import { markRaw, watch } from 'vue';
-import { Storage } from '../../pizzax';
-
-type ColumnWidget = {
-	name: string;
-	id: string;
-	data: Record<string, any>;
-};
-
-type Column = {
-	id: string;
-	type: string;
-	name: string | null;
-	width: number;
-	widgets?: ColumnWidget[];
-	active?: boolean;
-};
-
-function copy<T>(x: T): T {
-	return JSON.parse(JSON.stringify(x));
-}
-
-export const deckStore = markRaw(new Storage('deck', {
-	profile: {
-		where: 'deviceAccount',
-		default: 'default'
-	},
-	columns: {
-		where: 'deviceAccount',
-		default: [] as Column[]
-	},
-	layout: {
-		where: 'deviceAccount',
-		default: [] as Column['id'][][]
-	},
-	columnAlign: {
-		where: 'deviceAccount',
-		default: 'left' as 'left' | 'right' | 'center'
-	},
-	alwaysShowMainColumn: {
-		where: 'deviceAccount',
-		default: true
-	},
-	navWindow: {
-		where: 'deviceAccount',
-		default: true
-	},
-	columnMargin: {
-		where: 'deviceAccount',
-		default: 16
-	},
-	columnHeaderHeight: {
-		where: 'deviceAccount',
-		default: 42
-	},
-}));
-
-export const loadDeck = async () => {
-	let deck;
-
-	try {
-		deck = await api('i/registry/get', {
-			scope: ['client', 'deck', 'profiles'],
-			key: deckStore.state.profile,
-		});
-	} catch (e) {
-		if (e.code === 'NO_SUCH_KEY') {
-			// 後方互換性のため
-			if (deckStore.state.profile === 'default') {
-				saveDeck();
-				return;
-			}
-
-			deckStore.set('columns', [{
-				id: 'a',
-				type: 'main',
-				name: i18n.locale._deck._columns.main,
-				width: 350,
-			}, {
-				id: 'b',
-				type: 'notifications',
-				name: i18n.locale._deck._columns.notifications,
-				width: 330,
-			}]);
-			deckStore.set('layout', [['a'], ['b']]);
-			return;
-		}
-		throw e;
-	}
-
-	deckStore.set('columns', deck.columns);
-	deckStore.set('layout', deck.layout);
-};
-
-// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
-export const saveDeck = throttle(1000, () => {
-	api('i/registry/set', {
-		scope: ['client', 'deck', 'profiles'],
-		key: deckStore.state.profile,
-		value: {
-			columns: deckStore.reactiveState.columns.value,
-			layout: deckStore.reactiveState.layout.value,
-		}
-	});
-});
-
-export function addColumn(column: Column) {
-	if (column.name == undefined) column.name = null;
-	deckStore.push('columns', column);
-	deckStore.push('layout', [column.id]);
-	saveDeck();
-}
-
-export function removeColumn(id: Column['id']) {
-	deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id));
-	deckStore.set('layout', deckStore.state.layout
-		.map(ids => ids.filter(_id => _id !== id))
-		.filter(ids => ids.length > 0));
-	saveDeck();
-}
-
-export function swapColumn(a: Column['id'], b: Column['id']) {
-	const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) != -1);
-	const aY = deckStore.state.layout[aX].findIndex(id => id == a);
-	const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) != -1);
-	const bY = deckStore.state.layout[bX].findIndex(id => id == b);
-	const layout = copy(deckStore.state.layout);
-	layout[aX][aY] = b;
-	layout[bX][bY] = a;
-	deckStore.set('layout', layout);
-	saveDeck();
-}
-
-export function swapLeftColumn(id: Column['id']) {
-	const layout = copy(deckStore.state.layout);
-	deckStore.state.layout.some((ids, i) => {
-		if (ids.includes(id)) {
-			const left = deckStore.state.layout[i - 1];
-			if (left) {
-				layout[i - 1] = deckStore.state.layout[i];
-				layout[i] = left;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-	});
-	saveDeck();
-}
-
-export function swapRightColumn(id: Column['id']) {
-	const layout = copy(deckStore.state.layout);
-	deckStore.state.layout.some((ids, i) => {
-		if (ids.includes(id)) {
-			const right = deckStore.state.layout[i + 1];
-			if (right) {
-				layout[i + 1] = deckStore.state.layout[i];
-				layout[i] = right;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-	});
-	saveDeck();
-}
-
-export function swapUpColumn(id: Column['id']) {
-	const layout = copy(deckStore.state.layout);
-	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const ids = copy(deckStore.state.layout[idsIndex]);
-	ids.some((x, i) => {
-		if (x === id) {
-			const up = ids[i - 1];
-			if (up) {
-				ids[i - 1] = id;
-				ids[i] = up;
-
-				layout[idsIndex] = ids;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-	});
-	saveDeck();
-}
-
-export function swapDownColumn(id: Column['id']) {
-	const layout = copy(deckStore.state.layout);
-	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const ids = copy(deckStore.state.layout[idsIndex]);
-	ids.some((x, i) => {
-		if (x === id) {
-			const down = ids[i + 1];
-			if (down) {
-				ids[i + 1] = id;
-				ids[i] = down;
-
-				layout[idsIndex] = ids;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-	});
-	saveDeck();
-}
-
-export function stackLeftColumn(id: Column['id']) {
-	let layout = copy(deckStore.state.layout);
-	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	layout = layout.map(ids => ids.filter(_id => _id !== id));
-	layout[i - 1].push(id);
-	layout = layout.filter(ids => ids.length > 0);
-	deckStore.set('layout', layout);
-	saveDeck();
-}
-
-export function popRightColumn(id: Column['id']) {
-	let layout = copy(deckStore.state.layout);
-	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const affected = layout[i];
-	layout = layout.map(ids => ids.filter(_id => _id !== id));
-	layout.splice(i + 1, 0, [id]);
-	layout = layout.filter(ids => ids.length > 0);
-	deckStore.set('layout', layout);
-
-	const columns = copy(deckStore.state.columns);
-	for (const column of columns) {
-		if (affected.includes(column.id)) {
-			column.active = true;
-		}
-	}
-	deckStore.set('columns', columns);
-
-	saveDeck();
-}
-
-export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
-	const columns = copy(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = copy(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	if (column.widgets == null) column.widgets = [];
-	column.widgets.unshift(widget);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
-	const columns = copy(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = copy(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	column.widgets = column.widgets.filter(w => w.id != widget.id);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
-	const columns = copy(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = copy(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	column.widgets = widgets;
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function updateColumnWidget(id: Column['id'], widgetId: string, data: any) {
-	const columns = copy(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = copy(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	column.widgets = column.widgets.map(w => w.id === widgetId ? {
-		...w,
-		data: data
-	} : w);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function updateColumn(id: Column['id'], column: Partial<Column>) {
-	const columns = copy(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const currentColumn = copy(deckStore.state.columns[columnIndex]);
-	if (currentColumn == null) return;
-	for (const [k, v] of Object.entries(column)) {
-		currentColumn[k] = v;
-	}
-	columns[columnIndex] = currentColumn;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
diff --git a/src/client/ui/deck/direct-column.vue b/src/client/ui/deck/direct-column.vue
deleted file mode 100644
index 5b4b02932b..0000000000
--- a/src/client/ui/deck/direct-column.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<template>
-<XColumn :column="column" :is-stacked="isStacked">
-	<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
-
-	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
-</XColumn>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XColumn from './column.vue';
-import XNotes from '@client/components/notes.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XColumn,
-		XNotes
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/mentions',
-				limit: 10,
-				params: () => ({
-					visibility: 'specified'
-				})
-			},
-		}
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		}
-	}
-});
-</script>
diff --git a/src/client/ui/deck/list-column.vue b/src/client/ui/deck/list-column.vue
deleted file mode 100644
index 450280b863..0000000000
--- a/src/client/ui/deck/list-column.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<template>
-<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked">
-	<template #header>
-		<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
-	</template>
-
-	<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/>
-</XColumn>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XColumn from './column.vue';
-import XTimeline from '@client/components/timeline.vue';
-import * as os from '@client/os';
-import { updateColumn } from './deck-store';
-
-export default defineComponent({
-	components: {
-		XColumn,
-		XTimeline,
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	watch: {
-		mediaOnly() {
-			(this.$refs.timeline as any).reload();
-		}
-	},
-
-	mounted() {
-		if (this.column.listId == null) {
-			this.setList();
-		}
-	},
-
-	methods: {
-		async setList() {
-			const lists = await os.api('users/lists/list');
-			const { canceled, result: list } = await os.dialog({
-				title: this.$ts.selectList,
-				type: null,
-				select: {
-					items: lists.map(x => ({
-						value: x, text: x.name
-					})),
-					default: this.column.listId
-				},
-				showCancelButton: true
-			});
-			if (canceled) return;
-			updateColumn(this.column.id, {
-				listId: list.id
-			});
-		},
-
-		focus() {
-			(this.$refs.timeline as any).focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/src/client/ui/deck/main-column.vue b/src/client/ui/deck/main-column.vue
deleted file mode 100644
index 8e36caa3ce..0000000000
--- a/src/client/ui/deck/main-column.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<template>
-<XColumn v-if="deckStore.state.alwaysShowMainColumn || $route.name !== 'index'" :column="column" :is-stacked="isStacked">
-	<template #header>
-		<template v-if="pageInfo">
-			<i :class="pageInfo.icon"></i>
-			{{ pageInfo.title }}
-		</template>
-	</template>
-
-	<MkStickyContainer>
-		<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
-		<router-view v-slot="{ Component }">
-			<transition>
-				<keep-alive :include="['timeline']">
-					<component :is="Component" :ref="changePage" @contextmenu.stop="onContextmenu"/>
-				</keep-alive>
-			</transition>
-		</router-view>
-	</MkStickyContainer>
-</XColumn>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XColumn from './column.vue';
-import XNotes from '@client/components/notes.vue';
-import { deckStore } from '@client/ui/deck/deck-store';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XColumn,
-		XNotes
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			deckStore,
-			pageInfo: null,
-		}
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		back() {
-			history.back();
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
-			if (window.getSelection().toString() !== '') return;
-			const path = this.$route.path;
-			os.contextMenu([{
-				type: 'label',
-				text: path,
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(path);
-				}
-			}], e);
-		},
-	}
-});
-</script>
diff --git a/src/client/ui/deck/mentions-column.vue b/src/client/ui/deck/mentions-column.vue
deleted file mode 100644
index c625bb3ea1..0000000000
--- a/src/client/ui/deck/mentions-column.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
-<XColumn :column="column" :is-stacked="isStacked">
-	<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
-
-	<XNotes :pagination="pagination" @before="before()" @after="after()"/>
-</XColumn>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import Progress from '@client/scripts/loading';
-import XColumn from './column.vue';
-import XNotes from '@client/components/notes.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XColumn,
-		XNotes
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			pagination: {
-				endpoint: 'notes/mentions',
-				limit: 10,
-			},
-		}
-	},
-
-	methods: {
-		before() {
-			Progress.start();
-		},
-
-		after() {
-			Progress.done();
-		}
-	}
-});
-</script>
diff --git a/src/client/ui/deck/notifications-column.vue b/src/client/ui/deck/notifications-column.vue
deleted file mode 100644
index c24bf7ab10..0000000000
--- a/src/client/ui/deck/notifications-column.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<template>
-<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }">
-	<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
-
-	<XNotifications :include-types="column.includingTypes"/>
-</XColumn>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XColumn from './column.vue';
-import XNotifications from '@client/components/notifications.vue';
-import * as os from '@client/os';
-import { updateColumn } from './deck-store';
-
-export default defineComponent({
-	components: {
-		XColumn,
-		XNotifications
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-		}
-	},
-
-	methods: {
-		func() {
-			os.popup(import('@client/components/notification-setting-window.vue'), {
-				includingTypes: this.column.includingTypes,
-			}, {
-				done: async (res) => {
-					const { includingTypes } = res;
-					updateColumn(this.column.id, {
-						includingTypes: includingTypes
-					});
-				},
-			}, 'closed');
-		}
-	}
-});
-</script>
diff --git a/src/client/ui/deck/tl-column.vue b/src/client/ui/deck/tl-column.vue
deleted file mode 100644
index 370f7d507f..0000000000
--- a/src/client/ui/deck/tl-column.vue
+++ /dev/null
@@ -1,137 +0,0 @@
-<template>
-<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState">
-	<template #header>
-		<i v-if="column.tl === 'home'" class="fas fa-home"></i>
-		<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
-		<i v-else-if="column.tl === 'social'" class="fas fa-share-alt"></i>
-		<i v-else-if="column.tl === 'global'" class="fas fa-globe"></i>
-		<span style="margin-left: 8px;">{{ column.name }}</span>
-	</template>
-
-	<div class="iwaalbte" v-if="disabled">
-		<p>
-			<i class="fas fa-minus-circle"></i>
-			{{ $t('disabled-timeline.title') }}
-		</p>
-		<p class="desc">{{ $t('disabled-timeline.description') }}</p>
-	</div>
-	<XTimeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/>
-</XColumn>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XColumn from './column.vue';
-import XTimeline from '@client/components/timeline.vue';
-import * as os from '@client/os';
-import { removeColumn, updateColumn } from './deck-store';
-
-export default defineComponent({
-	components: {
-		XColumn,
-		XTimeline,
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true
-		},
-		isStacked: {
-			type: Boolean,
-			required: true
-		}
-	},
-
-	data() {
-		return {
-			disabled: false,
-			indicated: false,
-			columnActive: true,
-		};
-	},
-
-	watch: {
-		mediaOnly() {
-			(this.$refs.timeline as any).reload();
-		}
-	},
-
-	mounted() {
-		if (this.column.tl == null) {
-			this.setType();
-		} else {
-			this.disabled = !this.$i.isModerator && !this.$i.isAdmin && (
-				this.$instance.disableLocalTimeline && ['local', 'social'].includes(this.column.tl) ||
-				this.$instance.disableGlobalTimeline && ['global'].includes(this.column.tl));
-		}
-	},
-
-	methods: {
-		async setType() {
-			const { canceled, result: src } = await os.dialog({
-				title: this.$ts.timeline,
-				type: null,
-				select: {
-					items: [{
-						value: 'home', text: this.$ts._timelines.home
-					}, {
-						value: 'local', text: this.$ts._timelines.local
-					}, {
-						value: 'social', text: this.$ts._timelines.social
-					}, {
-						value: 'global', text: this.$ts._timelines.global
-					}]
-				},
-			});
-			if (canceled) {
-				if (this.column.tl == null) {
-					removeColumn(this.column.id);
-				}
-				return;
-			}
-			updateColumn(this.column.id, {
-				tl: src
-			});
-		},
-
-		queueUpdated(q) {
-			if (this.columnActive) {
-				this.indicated = q !== 0;
-			}
-		},
-
-		onNote() {
-			if (!this.columnActive) {
-				this.indicated = true;
-			}
-		},
-
-		onChangeActiveState(state) {
-			this.columnActive = state;
-
-			if (this.columnActive) {
-				this.indicated = false;
-			}
-		},
-
-		focus() {
-			(this.$refs.timeline as any).focus();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.iwaalbte {
-	text-align: center;
-
-	> p {
-		margin: 16px;
-
-		&.desc {
-			font-size: 14px;
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/deck/widgets-column.vue b/src/client/ui/deck/widgets-column.vue
deleted file mode 100644
index 22b1a38287..0000000000
--- a/src/client/ui/deck/widgets-column.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<template>
-<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked">
-	<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
-
-	<div class="wtdtxvec">
-		<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
-	</div>
-</XColumn>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import XWidgets from '@client/components/widgets.vue';
-import XColumn from './column.vue';
-import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
-
-export default defineComponent({
-	components: {
-		XColumn,
-		XWidgets,
-	},
-
-	props: {
-		column: {
-			type: Object,
-			required: true,
-		},
-		isStacked: {
-			type: Boolean,
-			required: true,
-		},
-	},
-
-	data() {
-		return {
-			edit: false,
-		};
-	},
-
-	methods: {
-		addWidget(widget) {
-			addColumnWidget(this.column.id, widget);
-		},
-
-		removeWidget(widget) {
-			removeColumnWidget(this.column.id, widget);
-		},
-
-		updateWidget({ id, data }) {
-			updateColumnWidget(this.column.id, id, data);
-		},
-
-		updateWidgets(widgets) {
-			setColumnWidgets(this.column.id, widgets);
-		},
-
-		func() {
-			this.edit = !this.edit;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.wtdtxvec {
-	--margin: 8px;
-	--panelBorder: none;
-
-	padding: 0 var(--margin);
-}
-</style>
diff --git a/src/client/ui/desktop.vue b/src/client/ui/desktop.vue
deleted file mode 100644
index bff43e18b5..0000000000
--- a/src/client/ui/desktop.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<div class="mk-app" :class="{ wallpaper }" @contextmenu.prevent="() => {}">
-	<XSidebar ref="nav" class="sidebar"/>
-
-	<XCommon/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { host } from '@client/config';
-import { search } from '@client/scripts/search';
-import XCommon from './_common_/common.vue';
-import * as os from '@client/os';
-import XSidebar from '@client/ui/_common_/sidebar.vue';
-import { menuDef } from '@client/menu';
-import { ColdDeviceStorage } from '@client/store';
-
-export default defineComponent({
-	components: {
-		XCommon,
-		XSidebar
-	},
-
-	provide() {
-		return {
-			navHook: (url) => {
-				os.pageWindow(url);
-			}
-		};
-	},
-
-	data() {
-		return {
-			host: host,
-			menuDef: menuDef,
-			wallpaper: localStorage.getItem('wallpaper') != null,
-		};
-	},
-
-	computed: {
-		menu(): string[] {
-			return this.$store.state.menu;
-		},
-	},
-
-	created() {
-		if (window.innerWidth < 1024) {
-			localStorage.setItem('ui', 'default');
-			location.reload();
-		}
-	},
-
-	methods: {
-		help() {
-			window.open(`https://misskey-hub.net/docs/keyboard-shortcut.md`, '_blank');
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-app {
-	height: 100vh;
-	width: 100vw;
-}
-</style>
-
-<style lang="scss">
-</style>
diff --git a/src/client/ui/universal.vue b/src/client/ui/universal.vue
deleted file mode 100644
index d8f1c2a934..0000000000
--- a/src/client/ui/universal.vue
+++ /dev/null
@@ -1,402 +0,0 @@
-<template>
-<div class="mk-app" :class="{ wallpaper }">
-	<XSidebar ref="nav" class="sidebar"/>
-
-	<div class="contents" ref="contents" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }">
-		<main ref="main">
-			<div class="content">
-				<MkStickyContainer>
-					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
-					<router-view v-slot="{ Component }">
-						<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-							<keep-alive :include="['timeline']">
-								<component :is="Component" :ref="changePage"/>
-							</keep-alive>
-						</transition>
-					</router-view>
-				</MkStickyContainer>
-			</div>
-			<div class="spacer"></div>
-		</main>
-	</div>
-
-	<XSide v-if="isDesktop" class="side" ref="side"/>
-
-	<div v-if="isDesktop" class="widgets" ref="widgets">
-		<XWidgets @mounted="attachSticky"/>
-	</div>
-
-	<div class="buttons" :class="{ navHidden }">
-		<button class="button nav _button" @click="showNav" ref="navButton"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
-		<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
-		<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
-		<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
-		<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
-	</div>
-
-	<button class="widgetButton _button" :class="{ navHidden }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
-
-	<transition name="tray-back">
-		<div class="tray-back _modalBg"
-			v-if="widgetsShowing"
-			@click="widgetsShowing = false"
-			@touchstart.passive="widgetsShowing = false"
-		></div>
-	</transition>
-
-	<transition name="tray">
-		<XWidgets v-if="widgetsShowing" class="tray"/>
-	</transition>
-
-	<XCommon/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { instanceName } from '@client/config';
-import { StickySidebar } from '@client/scripts/sticky-sidebar';
-import XSidebar from '@client/ui/_common_/sidebar.vue';
-import XCommon from './_common_/common.vue';
-import XSide from './classic.side.vue';
-import * as os from '@client/os';
-import { menuDef } from '@client/menu';
-import * as symbols from '@client/symbols';
-
-const DESKTOP_THRESHOLD = 1100;
-
-export default defineComponent({
-	components: {
-		XCommon,
-		XSidebar,
-		XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
-		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
-	},
-
-	provide() {
-		return {
-			sideViewHook: this.isDesktop ? (url) => {
-				this.$refs.side.navigate(url);
-			} : null
-		};
-	},
-
-	data() {
-		return {
-			pageInfo: null,
-			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
-			menuDef: menuDef,
-			navHidden: false,
-			widgetsShowing: false,
-			wallpaper: localStorage.getItem('wallpaper') != null,
-		};
-	},
-
-	computed: {
-		navIndicated(): boolean {
-			for (const def in this.menuDef) {
-				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから
-				if (this.menuDef[def].indicated) return true;
-			}
-			return false;
-		}
-	},
-
-	created() {
-		document.documentElement.style.overflowY = 'scroll';
-
-		if (this.$store.state.widgets.length === 0) {
-			this.$store.set('widgets', [{
-				name: 'calendar',
-				id: 'a', place: 'right', data: {}
-			}, {
-				name: 'notifications',
-				id: 'b', place: 'right', data: {}
-			}, {
-				name: 'trends',
-				id: 'c', place: 'right', data: {}
-			}]);
-		}
-	},
-
-	mounted() {
-		this.adjustUI();
-
-		const ro = new ResizeObserver((entries, observer) => {
-			this.adjustUI();
-		});
-
-		ro.observe(this.$refs.contents);
-
-		window.addEventListener('resize', this.adjustUI, { passive: true });
-
-		if (!this.isDesktop) {
-			window.addEventListener('resize', () => {
-				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
-			}, { passive: true });
-		}
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-				document.title = `${this.pageInfo.title} | ${instanceName}`;
-			}
-		},
-
-		adjustUI() {
-			const navWidth = this.$refs.nav.$el.offsetWidth;
-			this.navHidden = navWidth === 0;
-		},
-
-		showNav() {
-			this.$refs.nav.show();
-		},
-
-		attachSticky(el) {
-			const sticky = new StickySidebar(this.$refs.widgets);
-			window.addEventListener('scroll', () => {
-				sticky.calc(window.scrollY);
-			}, { passive: true });
-		},
-
-		post() {
-			os.post();
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		back() {
-			history.back();
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-
-		onContextmenu(e) {
-			const isLink = (el: HTMLElement) => {
-				if (el.tagName === 'A') return true;
-				if (el.parentElement) {
-					return isLink(el.parentElement);
-				}
-			};
-			if (isLink(e.target)) return;
-			if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
-			if (window.getSelection().toString() !== '') return;
-			const path = this.$route.path;
-			os.contextMenu([{
-				type: 'label',
-				text: path,
-			}, {
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.$refs.side.navigate(path);
-				}
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(path);
-				}
-			}], e);
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.tray-enter-active,
-.tray-leave-active {
-	opacity: 1;
-	transform: translateX(0);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.tray-enter-from,
-.tray-leave-active {
-	opacity: 0;
-	transform: translateX(240px);
-}
-
-.tray-back-enter-active,
-.tray-back-leave-active {
-	opacity: 1;
-	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.tray-back-enter-from,
-.tray-back-leave-active {
-	opacity: 0;
-}
-
-.mk-app {
-	$ui-font-size: 1em; // TODO: どこかに集約したい
-	$widgets-hide-threshold: 1090px;
-
-	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-	min-height: calc(var(--vh, 1vh) * 100);
-	box-sizing: border-box;
-	display: flex;
-
-	&.wallpaper {
-		background: var(--wallpaperOverlay);
-		//backdrop-filter: var(--blur, blur(4px));
-	}
-
-	> .sidebar {
-	}
-
-	> .contents {
-		width: 100%;
-		min-width: 0;
-		background: var(--panel);
-
-		> main {
-			min-width: 0;
-
-			> .spacer {
-				height: 82px;
-
-				@media (min-width: ($widgets-hide-threshold + 1px)) {
-					display: none;
-				}
-			}
-		}
-	}
-
-	> .side {
-		min-width: 370px;
-		max-width: 370px;
-		border-left: solid 0.5px var(--divider);
-	}
-
-	> .widgets {
-		padding: 0 var(--margin);
-		border-left: solid 0.5px var(--divider);
-		background: var(--bg);
-
-		@media (max-width: $widgets-hide-threshold) {
-			display: none;
-		}
-	}
-
-	> .widgetButton {
-		display: block;
-		position: fixed;
-		z-index: 1000;
-		bottom: 32px;
-		right: 32px;
-		width: 64px;
-		height: 64px;
-		border-radius: 100%;
-		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
-		font-size: 22px;
-		background: var(--panel);
-
-		&.navHidden {
-			display: none;
-		}
-
-		@media (min-width: ($widgets-hide-threshold + 1px)) {
-			display: none;
-		}
-	}
-
-	> .buttons {
-		position: fixed;
-		z-index: 1000;
-		bottom: 0;
-		padding: 16px;
-		display: flex;
-		width: 100%;
-		box-sizing: border-box;
-		-webkit-backdrop-filter: var(--blur, blur(32px));
-		backdrop-filter: var(--blur, blur(32px));
-		background-color: var(--header);
-
-		&:not(.navHidden) {
-			display: none;
-		}
-
-		> .button {
-			position: relative;
-			flex: 1;
-			padding: 0;
-			margin: auto;
-			height: 64px;
-			border-radius: 8px;
-			background: var(--panel);
-			color: var(--fg);
-
-			&:not(:last-child) {
-				margin-right: 12px;
-			}
-
-			@media (max-width: 400px) {
-				height: 60px;
-
-				&:not(:last-child) {
-					margin-right: 8px;
-				}
-			}
-
-			&:hover {
-				background: var(--X2);
-			}
-
-			> .indicator {
-				position: absolute;
-				top: 0;
-				left: 0;
-				color: var(--indicator);
-				font-size: 16px;
-				animation: blink 1s infinite;
-			}
-
-			&:first-child {
-				margin-left: 0;
-			}
-
-			&:last-child {
-				margin-right: 0;
-			}
-
-			> * {
-				font-size: 22px;
-			}
-
-			&:disabled {
-				cursor: default;
-
-				> * {
-					opacity: 0.5;
-				}
-			}
-		}
-	}
-
-	> .tray-back {
-		z-index: 1001;
-	}
-
-	> .tray {
-		position: fixed;
-		top: 0;
-		right: 0;
-		z-index: 1001;
-		// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-		height: calc(var(--vh, 1vh) * 100);
-		padding: var(--margin);
-		box-sizing: border-box;
-		overflow: auto;
-		background: var(--bg);
-	}
-}
-</style>
-
-<style lang="scss">
-</style>
diff --git a/src/client/ui/universal.widgets.vue b/src/client/ui/universal.widgets.vue
deleted file mode 100644
index 28b14749d1..0000000000
--- a/src/client/ui/universal.widgets.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<template>
-<div class="efzpzdvf">
-	<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
-
-	<button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button>
-	<button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import XWidgets from '@client/components/widgets.vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	components: {
-		XWidgets
-	},
-
-	emits: ['mounted'],
-
-	data() {
-		return {
-			editMode: false,
-		};
-	},
-
-	mounted() {
-		this.$emit('mounted', this.$el);
-	},
-
-	methods: {
-		addWidget(widget) {
-			this.$store.set('widgets', [{
-				...widget,
-				place: null,
-			}, ...this.$store.state.widgets]);
-		},
-
-		removeWidget(widget) {
-			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id));
-		},
-
-		updateWidget({ id, data }) {
-			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? {
-				...w,
-				data: data
-			} : w));
-		},
-
-		updateWidgets(widgets) {
-			this.$store.set('widgets', widgets);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.efzpzdvf {
-	position: sticky;
-	height: min-content;
-	min-height: 100vh;
-	padding: var(--margin) 0;
-	box-sizing: border-box;
-
-	> * {
-		margin: var(--margin) 0;
-		width: 300px;
-
-		&:first-child {
-			margin-top: 0;
-		}
-	}
-
-	> .add {
-		margin: 0 auto;
-	}
-}
-</style>
diff --git a/src/client/ui/visitor.vue b/src/client/ui/visitor.vue
deleted file mode 100644
index ec9150d346..0000000000
--- a/src/client/ui/visitor.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<template>
-<DesignB/>
-<XCommon/>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import DesignA from './visitor/a.vue';
-import DesignB from './visitor/b.vue';
-import XCommon from './_common_/common.vue';
-
-export default defineComponent({
-	components: {
-		XCommon,
-		DesignA,
-		DesignB,
-	},
-});
-</script>
diff --git a/src/client/ui/visitor/a.vue b/src/client/ui/visitor/a.vue
deleted file mode 100644
index ed015c6b07..0000000000
--- a/src/client/ui/visitor/a.vue
+++ /dev/null
@@ -1,260 +0,0 @@
-<template>
-<div class="mk-app">
-	<div class="banner" v-if="$route.path === '/'" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
-		<div>
-			<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
-			<div class="about" v-if="meta">
-				<div class="desc" v-html="meta.description || $ts.introMisskey"></div>
-			</div>
-			<div class="action">
-				<button class="_button primary" @click="signup()">{{ $ts.signup }}</button>
-				<button class="_button" @click="signin()">{{ $ts.login }}</button>
-			</div>
-		</div>
-	</div>
-	<div class="banner-mini" v-else :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
-		<div>
-			<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
-		</div>
-	</div>
-
-	<div class="main">
-		<div class="contents" ref="contents" :class="{ wallpaper }">
-			<header class="header" ref="header" v-show="$route.path !== '/'">
-				<XHeader :info="pageInfo"/>
-			</header>
-			<main ref="main">
-				<router-view v-slot="{ Component }">
-					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-						<component :is="Component" :ref="changePage"/>
-					</transition>
-				</router-view>
-			</main>
-			<div class="powered-by">
-				<b><MkA to="/">{{ host }}</MkA></b>
-				<small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { host, instanceName } from '@client/config';
-import { search } from '@client/scripts/search';
-import * as os from '@client/os';
-import MkPagination from '@client/components/ui/pagination.vue';
-import MkButton from '@client/components/ui/button.vue';
-import XHeader from './header.vue';
-import { ColdDeviceStorage } from '@client/store';
-import * as symbols from '@client/symbols';
-
-const DESKTOP_THRESHOLD = 1100;
-
-export default defineComponent({
-	components: {
-		XHeader,
-		MkPagination,
-		MkButton,
-	},
-
-	data() {
-		return {
-			host,
-			instanceName,
-			pageInfo: null,
-			meta: null,
-			narrow: window.innerWidth < 1280,
-			announcements: {
-				endpoint: 'announcements',
-				limit: 10,
-			},
-			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
-		};
-	},
-
-	computed: {
-		keymap(): any {
-			return {
-				'd': () => {
-					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
-					this.$store.set('darkMode', !this.$store.state.darkMode);
-				},
-				's': search,
-				'h|/': this.help
-			};
-		},
-	},
-
-	created() {
-		document.documentElement.style.overflowY = 'scroll';
-
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-	},
-
-	mounted() {
-		if (!this.isDesktop) {
-			window.addEventListener('resize', () => {
-				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
-			}, { passive: true });
-		}
-	},
-
-	methods: {
-		setParallax(el) {
-			//new simpleParallax(el);
-		},
-
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		help() {
-			window.open(`https://misskey-hub.net/docs/keyboard-shortcut.md`, '_blank');
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-app {
-	min-height: 100vh;
-
-	> .banner {
-		position: relative;
-		width: 100%;
-		text-align: center;
-		background-position: center;
-		background-size: cover;
-
-		> div {
-			height: 100%;
-			background: rgba(0, 0, 0, 0.3);
-
-			* {
-				color: #fff;
-			}
-					
-			> h1 {
-				margin: 0;
-				padding: 96px 32px 0 32px;
-				text-shadow: 0 0 8px black;
-
-				> .logo {
-					vertical-align: bottom;
-					max-height: 150px;
-				}
-			}
-
-			> .about {
-				padding: 32px;
-				max-width: 580px;
-				margin: 0 auto;
-				box-sizing: border-box;
-				text-shadow: 0 0 8px black;
-			}
-
-			> .action {
-				padding-bottom: 64px;
-				
-				> button {
-					display: inline-block;
-					padding: 10px 20px;
-					box-sizing: border-box;
-					text-align: center;
-					border-radius: 999px;
-					background: var(--panel);
-					color: var(--fg);
-
-					&.primary {
-						background: var(--accent);
-						color: #fff;
-					}
-
-					&:first-child {
-						margin-right: 16px;
-					}
-				}
-			}
-		}
-	}
-
-	> .banner-mini {
-		position: relative;
-		width: 100%;
-		text-align: center;
-		background-position: center;
-		background-size: cover;
-
-		> div {
-			position: relative;
-			z-index: 1;
-			height: 100%;
-			background: rgba(0, 0, 0, 0.3);
-
-			* {
-				color: #fff !important;
-			}
-
-			> header {
-				
-			}
-					
-			> h1 {
-				margin: 0;
-				padding: 32px;
-				text-shadow: 0 0 8px black;
-
-				> .logo {
-					vertical-align: bottom;
-					max-height: 100px;
-				}
-			}
-		}
-	}
-
-	> .main {
-		> .contents {
-			position: relative;
-			z-index: 1;
-
-			> .header {
-				position: sticky;
-				top: 0;
-				left: 0;
-				z-index: 1000;
-			}
-
-			> .powered-by {
-				padding: 28px;
-				font-size: 14px;
-				text-align: center;
-				border-top: 1px solid var(--divider);
-
-				> small {
-					display: block;
-					margin-top: 8px;
-					opacity: 0.5;
-				}
-			}
-		}
-	}
-}
-</style>
-
-<style lang="scss">
-</style>
diff --git a/src/client/ui/visitor/b.vue b/src/client/ui/visitor/b.vue
deleted file mode 100644
index 0eefb3192a..0000000000
--- a/src/client/ui/visitor/b.vue
+++ /dev/null
@@ -1,282 +0,0 @@
-<template>
-<div class="mk-app">
-	<a v-if="root" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
-
-	<div class="side" v-if="!narrow && !root">
-		<XKanban class="kanban" full/>
-	</div>
-
-	<div class="main">
-		<XKanban class="banner" :powered-by="root" v-if="narrow && !root"/>
-
-		<div class="contents">
-			<XHeader class="header" :info="pageInfo" v-if="!root"/>
-			<main>
-				<router-view v-slot="{ Component }">
-					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-						<component :is="Component" :ref="changePage"/>
-					</transition>
-				</router-view>
-			</main>
-			<div class="powered-by" v-if="!root">
-				<b><MkA to="/">{{ host }}</MkA></b>
-				<small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small>
-			</div>
-		</div>
-	</div>
-
-	<transition name="tray-back">
-		<div class="menu-back _modalBg"
-			v-if="showMenu"
-			@click="showMenu = false"
-			@touchstart.passive="showMenu = false"
-		></div>
-	</transition>
-
-	<transition name="tray">
-		<div v-if="showMenu" class="menu">
-			<MkA to="/" class="link" active-class="active"><i class="fas fa-home icon"></i>{{ $ts.home }}</MkA>
-			<MkA to="/explore" class="link" active-class="active"><i class="fas fa-hashtag icon"></i>{{ $ts.explore }}</MkA>
-			<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
-			<MkA to="/channels" class="link" active-class="active"><i class="fas fa-satellite-dish icon"></i>{{ $ts.channel }}</MkA>
-			<div class="action">
-				<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
-				<button class="_button" @click="signin()">{{ $ts.login }}</button>
-			</div>
-		</div>
-	</transition>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { host, instanceName } from '@client/config';
-import { search } from '@client/scripts/search';
-import * as os from '@client/os';
-import MkPagination from '@client/components/ui/pagination.vue';
-import XSigninDialog from '@client/components/signin-dialog.vue';
-import XSignupDialog from '@client/components/signup-dialog.vue';
-import MkButton from '@client/components/ui/button.vue';
-import XHeader from './header.vue';
-import XKanban from './kanban.vue';
-import { ColdDeviceStorage } from '@client/store';
-import * as symbols from '@client/symbols';
-
-const DESKTOP_THRESHOLD = 1100;
-
-export default defineComponent({
-	components: {
-		XHeader,
-		XKanban,
-		MkPagination,
-		MkButton,
-	},
-
-	data() {
-		return {
-			host,
-			instanceName,
-			pageInfo: null,
-			meta: null,
-			showMenu: false,
-			narrow: window.innerWidth < 1280,
-			announcements: {
-				endpoint: 'announcements',
-				limit: 10,
-			},
-			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
-		};
-	},
-
-	computed: {
-		keymap(): any {
-			return {
-				'd': () => {
-					if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
-					this.$store.set('darkMode', !this.$store.state.darkMode);
-				},
-				's': search,
-				'h|/': this.help
-			};
-		},
-
-		root(): boolean {
-			return this.$route.path === '/';
-		},
-	},
-
-	created() {
-		//document.documentElement.style.overflowY = 'scroll';
-
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-	},
-
-	mounted() {
-		if (!this.isDesktop) {
-			window.addEventListener('resize', () => {
-				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
-			}, { passive: true });
-		}
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		help() {
-			window.open(`https://misskey-hub.net/docs/keyboard-shortcut.md`, '_blank');
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-
-		signin() {
-			os.popup(XSigninDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		signup() {
-			os.popup(XSignupDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		}
-	}
-});
-</script>
-
-<style>
-.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}
-</style>
-
-<style lang="scss" scoped>
-.tray-enter-active,
-.tray-leave-active {
-	opacity: 1;
-	transform: translateX(0);
-	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.tray-enter-from,
-.tray-leave-active {
-	opacity: 0;
-	transform: translateX(-240px);
-}
-
-.tray-back-enter-active,
-.tray-back-leave-active {
-	opacity: 1;
-	transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
-}
-.tray-back-enter-from,
-.tray-back-leave-active {
-	opacity: 0;
-}
-
-.mk-app {
-	display: flex;
-	min-height: 100vh;
-	background-position: center;
-	background-size: cover;
-	background-attachment: fixed;
-
-	> .side {
-		width: 500px;
-		height: 100vh;
-
-		> .kanban {
-			position: fixed;
-			top: 0;
-			left: 0;
-			width: 500px;
-			height: 100vh;
-			overflow: auto;
-		}
-	}
-
-	> .main {
-		flex: 1;
-		min-width: 0;
-
-		> .banner {
-		}
-
-		> .contents {
-			position: relative;
-			z-index: 1;
-
-			> .powered-by {
-				padding: 28px;
-				font-size: 14px;
-				text-align: center;
-				border-top: 1px solid var(--divider);
-
-				> small {
-					display: block;
-					margin-top: 8px;
-					opacity: 0.5;
-				}
-			}
-		}
-	}
-
-	> .menu-back {
-		position: fixed;
-		z-index: 1001;
-		top: 0;
-		left: 0;
-		width: 100vw;
-		height: 100vh;
-	}
-
-	> .menu {
-		position: fixed;
-		z-index: 1001;
-		top: 0;
-		left: 0;
-		width: 240px;
-		height: 100vh;
-		background: var(--panel);
-
-		> .link {
-			display: block;
-			padding: 16px;
-
-			> .icon {
-				margin-right: 1em;
-			}
-		}
-
-		> .action {
-			padding: 16px;
-
-			> button {
-				display: block;
-				width: 100%;
-				padding: 10px;
-				box-sizing: border-box;
-				text-align: center;
-				border-radius: 999px;
-
-				&._button {
-					background: var(--panel);
-				}
-
-				&:first-child {
-					margin-bottom: 16px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/visitor/header.vue b/src/client/ui/visitor/header.vue
deleted file mode 100644
index 6d2ebc880f..0000000000
--- a/src/client/ui/visitor/header.vue
+++ /dev/null
@@ -1,228 +0,0 @@
-<template>
-<div class="sqxihjet">
-	<div class="wide" v-if="narrow === false">
-		<div class="content">
-			<MkA to="/" class="link" active-class="active"><i class="fas fa-home icon"></i>{{ $ts.home }}</MkA>
-			<MkA to="/explore" class="link" active-class="active"><i class="fas fa-hashtag icon"></i>{{ $ts.explore }}</MkA>
-			<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
-			<MkA to="/channels" class="link" active-class="active"><i class="fas fa-satellite-dish icon"></i>{{ $ts.channel }}</MkA>
-			<div class="page active link" v-if="info">
-				<div class="title">
-					<i v-if="info.icon" class="icon" :class="info.icon"></i>
-					<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
-					<span v-if="info.title" class="text">{{ info.title }}</span>
-					<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
-				</div>
-				<button class="_button action" v-if="info.action" @click.stop="info.action.handler"><!-- TODO --></button>
-			</div>
-			<div class="right">
-				<button class="_button search" @click="search()"><i class="fas fa-search icon"></i><span>{{ $ts.search }}</span></button>
-				<button class="_buttonPrimary signup" @click="signup()">{{ $ts.signup }}</button>
-				<button class="_button login" @click="signin()">{{ $ts.login }}</button>
-			</div>
-		</div>
-	</div>
-	<div class="narrow" v-else-if="narrow === true">
-		<button class="menu _button" @click="$parent.showMenu = true">
-			<i class="fas fa-bars icon"></i>
-		</button>
-		<div class="title" v-if="info">
-			<i v-if="info.icon" class="icon" :class="info.icon"></i>
-			<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
-			<span v-if="info.title" class="text">{{ info.title }}</span>
-			<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
-		</div>
-		<button class="action _button" v-if="info && info.action" @click.stop="info.action.handler">
-			<!-- TODO -->
-		</button>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XSigninDialog from '@client/components/signin-dialog.vue';
-import XSignupDialog from '@client/components/signup-dialog.vue';
-import * as os from '@client/os';
-import { search } from '@client/scripts/search';
-
-export default defineComponent({
-	props: {
-		info: {
-			required: true
-		},
-	},
-
-	data() {
-		return {
-			narrow: null,
-			showMenu: false,
-		};
-	},
-
-	mounted() {
-		this.narrow = this.$el.clientWidth < 1300;
-	},
-
-	methods: {
-		signin() {
-			os.popup(XSigninDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		signup() {
-			os.popup(XSignupDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		search
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.sqxihjet {
-	$height: 60px;
-	position: sticky;
-	top: 0;
-	left: 0;
-	z-index: 1000;
-	line-height: $height;
-	-webkit-backdrop-filter: var(--blur, blur(32px));
-	backdrop-filter: var(--blur, blur(32px));
-	background-color: var(--X16);
-
-	> .wide {
-		> .content {
-			max-width: 1400px;
-			margin: 0 auto;
-			display: flex;
-			align-items: center;
-
-			> .link {
-				$line: 3px;
-				display: inline-block;
-				padding: 0 16px;
-				line-height: $height - ($line * 2);
-				border-top: solid $line transparent;
-				border-bottom: solid $line transparent;
-
-				> .icon {
-					margin-right: 0.5em;
-				}
-
-				&.page {
-					border-bottom-color: var(--accent);
-				}
-			}
-
-			> .page {
-				> .title {
-					display: inline-block;
-					vertical-align: bottom;
-					white-space: nowrap;
-					overflow: hidden;
-					text-overflow: ellipsis;
-					position: relative;
-
-					> .icon + .text {
-						margin-left: 8px;
-					}
-
-					> .avatar {
-						$size: 32px;
-						display: inline-block;
-						width: $size;
-						height: $size;
-						vertical-align: middle;
-						margin-right: 8px;
-						pointer-events: none;
-					}
-
-					&._button {
-						&:hover {
-							color: var(--fgHighlighted);
-						}
-					}
-
-					&.selected {
-						box-shadow: 0 -2px 0 0 var(--accent) inset;
-						color: var(--fgHighlighted);
-					}
-				}
-
-				> .action {
-					padding: 0 0 0 16px;
-				}
-			}
-
-			> .right {
-				margin-left: auto;
-
-				> .search {
-					background: var(--bg);
-					border-radius: 999px;
-					width: 230px;
-					line-height: $height - 20px;
-					margin-right: 16px;
-					text-align: left;
-
-					> * {
-						opacity: 0.7;
-					}
-
-					> .icon {
-						padding: 0 16px;
-					}
-				}
-
-				> .signup {
-					border-radius: 999px;
-					padding: 0 24px;
-					line-height: $height - 20px;
-				}
-
-				> .login {
-					padding: 0 16px;
-				}
-			}
-		}
-	}
-
-	> .narrow {
-		display: flex;
-
-		> .menu,
-		> .action {
-			width: $height;
-			height: $height;
-			font-size: 20px;
-		}
-
-		> .title {
-			flex: 1;
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-			position: relative;
-			text-align: center;
-
-			> .icon + .text {
-				margin-left: 8px;
-			}
-
-			> .avatar {
-				$size: 32px;
-				display: inline-block;
-				width: $size;
-				height: $size;
-				vertical-align: middle;
-				margin-right: 8px;
-				pointer-events: none;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/visitor/kanban.vue b/src/client/ui/visitor/kanban.vue
deleted file mode 100644
index 5fbbff3d32..0000000000
--- a/src/client/ui/visitor/kanban.vue
+++ /dev/null
@@ -1,256 +0,0 @@
-<template>
-<div class="rwqkcmrc" :style="{ backgroundImage: transparent ? 'none' : `url(${ $instance.backgroundImageUrl })` }">
-	<div class="back" :class="{ transparent }"></div>
-	<div class="contents">
-		<div class="wrapper">
-			<h1 v-if="meta" :class="{ full }">
-				<MkA to="/" class="link"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></MkA>
-			</h1>
-			<template v-if="full">
-				<div class="about" v-if="meta">
-					<div class="desc" v-html="meta.description || $ts.introMisskey"></div>
-				</div>
-				<div class="action">
-					<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
-					<button class="_button" @click="signin()">{{ $ts.login }}</button>
-				</div>
-				<div class="announcements panel">
-					<header>{{ $ts.announcements }}</header>
-					<MkPagination :pagination="announcements" #default="{items}" class="list">
-						<section class="item" v-for="(announcement, i) in items" :key="announcement.id">
-							<div class="title">{{ announcement.title }}</div>
-							<div class="content">
-								<Mfm :text="announcement.text"/>
-								<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
-							</div>
-						</section>
-					</MkPagination>
-				</div>
-				<div class="powered-by" v-if="poweredBy">
-					<b><MkA to="/">{{ host }}</MkA></b>
-					<small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small>
-				</div>
-			</template>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { host, instanceName } from '@client/config';
-import * as os from '@client/os';
-import MkPagination from '@client/components/ui/pagination.vue';
-import XSigninDialog from '@client/components/signin-dialog.vue';
-import XSignupDialog from '@client/components/signup-dialog.vue';
-import MkButton from '@client/components/ui/button.vue';
-
-export default defineComponent({
-	components: {
-		MkPagination,
-		MkButton,
-	},
-
-	props: {
-		full: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		transparent: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		poweredBy: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
-
-	data() {
-		return {
-			host,
-			instanceName,
-			pageInfo: null,
-			meta: null,
-			narrow: window.innerWidth < 1280,
-			announcements: {
-				endpoint: 'announcements',
-				limit: 10,
-			},
-		};
-	},
-
-	created() {
-		os.api('meta', { detail: true }).then(meta => {
-			this.meta = meta;
-		});
-	},
-
-	methods: {
-		signin() {
-			os.popup(XSigninDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		},
-
-		signup() {
-			os.popup(XSignupDialog, {
-				autoSet: true
-			}, {}, 'closed');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.rwqkcmrc {
-	position: relative;
-	text-align: center;
-	background-position: center;
-	background-size: cover;
-	// TODO: パララックスにしたい
-
-	> .back {
-		position: absolute;
-		top: 0;
-		left: 0;
-		width: 100%;
-		height: 100%;
-		background: rgba(0, 0, 0, 0.3);
-
-		&.transparent {
-			-webkit-backdrop-filter: var(--blur, blur(12px));
-			backdrop-filter: var(--blur, blur(12px));
-		}
-	}
-
-	> .contents {
-		position: relative;
-		z-index: 1;
-		height: inherit;
-		overflow: auto;
-
-		> .wrapper {
-			max-width: 380px;
-			padding: 0 16px;
-			box-sizing: border-box;
-			margin: 0 auto;
-
-			> .panel {
-				-webkit-backdrop-filter: var(--blur, blur(8px));
-				backdrop-filter: var(--blur, blur(8px));
-				background: rgba(0, 0, 0, 0.5);
-				border-radius: var(--radius);
-
-				&, * {
-					color: #fff !important;
-				}
-			}
-
-			> h1 {
-				display: block;
-				margin: 0;
-				padding: 32px 0 32px 0;
-				color: #fff;
-
-				&.full {
-					padding: 64px 0 0 0;
-
-					> .link {
-						> ::v-deep(.logo) {
-							max-height: 130px;
-						}
-					}
-				}
-
-				> .link {
-					display: block;
-
-					> ::v-deep(.logo) {
-						vertical-align: bottom;
-						max-height: 100px;
-					}
-				}
-			}
-
-			> .about {
-				display: block;
-				margin: 24px 0;
-				text-align: center;
-				box-sizing: border-box;
-				text-shadow: 0 0 8px black;
-				color: #fff;
-			}
-
-			> .action {
-				> button {
-					display: block;
-					width: 100%;
-					padding: 10px;
-					box-sizing: border-box;
-					text-align: center;
-					border-radius: 999px;
-
-					&._button {
-						background: var(--panel);
-					}
-
-					&:first-child {
-						margin-bottom: 16px;
-					}
-				}
-			}
-
-			> .announcements {
-				margin: 32px 0;
-				text-align: left;
-
-				> header {
-					padding: 12px 16px;
-					border-bottom: solid 1px rgba(255, 255, 255, 0.5);
-				}
-
-				> .list {
-					max-height: 300px;
-					overflow: auto;
-
-					> .item {
-						padding: 12px 16px;
-
-						& + .item {
-							border-top: solid 1px rgba(255, 255, 255, 0.5);
-						}
-
-						> .title {
-							font-weight: bold;
-						}
-
-						> .content {
-							> img {
-								max-width: 100%;
-							}
-						}
-					}
-				}
-			}
-
-			> .powered-by {
-				padding: 28px;
-				font-size: 14px;
-				text-align: center;
-				border-top: 1px solid rgba(255, 255, 255, 0.5);
-				color: #fff;
-
-				> small {
-					display: block;
-					margin-top: 8px;
-					opacity: 0.5;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/ui/zen.vue b/src/client/ui/zen.vue
deleted file mode 100644
index ebbf72bca7..0000000000
--- a/src/client/ui/zen.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<template>
-<div class="mk-app">
-	<div class="contents">
-		<header class="header">
-			<MkHeader :info="pageInfo"/>
-		</header>
-		<main ref="main">
-			<div class="content">
-				<router-view v-slot="{ Component }">
-					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-						<keep-alive :include="['timeline']">
-							<component :is="Component" :ref="changePage"/>
-						</keep-alive>
-					</transition>
-				</router-view>
-			</div>
-		</main>
-	</div>
-
-	<XCommon/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { host } from '@client/config';
-import XCommon from './_common_/common.vue';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		XCommon,
-	},
-
-	data() {
-		return {
-			host: host,
-			pageInfo: null,
-		};
-	},
-
-	created() {
-		document.documentElement.style.overflowY = 'scroll';
-	},
-
-	methods: {
-		changePage(page) {
-			if (page == null) return;
-			if (page[symbols.PAGE_INFO]) {
-				this.pageInfo = page[symbols.PAGE_INFO];
-			}
-		},
-
-		top() {
-			window.scroll({ top: 0, behavior: 'smooth' });
-		},
-
-		help() {
-			window.open(`https://misskey-hub.net/docs/keyboard-shortcut.md`, '_blank');
-		},
-
-		onTransition() {
-			if (window._scroll) window._scroll();
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-app {
-	$header-height: 52px;
-	$ui-font-size: 1em; // TODO: どこかに集約したい
-
-	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-	min-height: calc(var(--vh, 1vh) * 100);
-	box-sizing: border-box;
-
-	> .contents {
-		padding-top: $header-height;
-
-		> .header {
-			position: fixed;
-			z-index: 1000;
-			top: 0;
-			height: $header-height;
-			width: 100%;
-			line-height: $header-height;
-			text-align: center;
-			//background-color: var(--panel);
-			-webkit-backdrop-filter: var(--blur, blur(32px));
-			backdrop-filter: var(--blur, blur(32px));
-			background-color: var(--header);
-			border-bottom: solid 0.5px var(--divider);
-		}
-
-		> main {
-			> .content {
-				> * {
-					// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
-					min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/activity.calendar.vue b/src/client/widgets/activity.calendar.vue
deleted file mode 100644
index ff767190f6..0000000000
--- a/src/client/widgets/activity.calendar.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<svg viewBox="0 0 21 7">
-	<rect v-for="record in data" class="day"
-		width="1" height="1"
-		:x="record.x" :y="record.date.weekday"
-		rx="1" ry="1"
-		fill="transparent">
-		<title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title>
-	</rect>
-	<rect v-for="record in data" class="day"
-		:width="record.v" :height="record.v"
-		:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
-		rx="1" ry="1"
-		:fill="record.color"
-		style="pointer-events: none;"/>
-	<rect class="today"
-		width="1" height="1"
-		:x="data[0].x" :y="data[0].date.weekday"
-		rx="1" ry="1"
-		fill="none"
-		stroke-width="0.1"
-		stroke="#f73520"/>
-</svg>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-export default defineComponent({
-	props: ['data'],
-	created() {
-		for (const d of this.data) {
-			d.total = d.notes + d.replies + d.renotes;
-		}
-		const peak = Math.max.apply(null, this.data.map(d => d.total));
-
-		const now = new Date();
-		const year = now.getFullYear();
-		const month = now.getMonth();
-		const day = now.getDate();
-
-		let x = 20;
-		this.data.slice().forEach((d, i) => {
-			d.x = x;
-
-			const date = new Date(year, month, day - i);
-			d.date = {
-				year: date.getFullYear(),
-				month: date.getMonth(),
-				day: date.getDate(),
-				weekday: date.getDay()
-			};
-
-			d.v = peak === 0 ? 0 : d.total / (peak / 2);
-			if (d.v > 1) d.v = 1;
-			const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170;
-			const cs = d.v * 100;
-			const cl = 15 + ((1 - d.v) * 80);
-			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
-
-			if (d.date.weekday === 0) x--;
-		});
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-svg {
-	display: block;
-	padding: 16px;
-	width: 100%;
-	box-sizing: border-box;
-
-	> rect {
-		transform-origin: center;
-
-		&.day {
-			&:hover {
-				fill: rgba(#000, 0.05);
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/activity.chart.vue b/src/client/widgets/activity.chart.vue
deleted file mode 100644
index ee5bc25113..0000000000
--- a/src/client/widgets/activity.chart.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<template>
-<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
-	<polyline
-		:points="pointsNote"
-		fill="none"
-		stroke-width="1"
-		stroke="#41ddde"/>
-	<polyline
-		:points="pointsReply"
-		fill="none"
-		stroke-width="1"
-		stroke="#f7796c"/>
-	<polyline
-		:points="pointsRenote"
-		fill="none"
-		stroke-width="1"
-		stroke="#a1de41"/>
-	<polyline
-		:points="pointsTotal"
-		fill="none"
-		stroke-width="1"
-		stroke="#555"
-		stroke-dasharray="2 2"/>
-</svg>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@client/os';
-
-function dragListen(fn) {
-	window.addEventListener('mousemove',  fn);
-	window.addEventListener('mouseleave', dragClear.bind(null, fn));
-	window.addEventListener('mouseup',    dragClear.bind(null, fn));
-}
-
-function dragClear(fn) {
-	window.removeEventListener('mousemove',  fn);
-	window.removeEventListener('mouseleave', dragClear);
-	window.removeEventListener('mouseup',    dragClear);
-}
-
-export default defineComponent({
-	props: ['data'],
-	data() {
-		return {
-			viewBoxX: 147,
-			viewBoxY: 60,
-			zoom: 1,
-			pos: 0,
-			pointsNote: null,
-			pointsReply: null,
-			pointsRenote: null,
-			pointsTotal: null
-		};
-	},
-	created() {
-		for (const d of this.data) {
-			d.total = d.notes + d.replies + d.renotes;
-		}
-
-		this.render();
-	},
-	methods: {
-		render() {
-			const peak = Math.max.apply(null, this.data.map(d => d.total));
-			if (peak != 0) {
-				const data = this.data.slice().reverse();
-				this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
-				this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
-				this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
-				this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
-			}
-		},
-		onMousedown(e) {
-			const clickX = e.clientX;
-			const clickY = e.clientY;
-			const baseZoom = this.zoom;
-			const basePos = this.pos;
-
-			// 動かした時
-			dragListen(me => {
-				let moveLeft = me.clientX - clickX;
-				let moveTop = me.clientY - clickY;
-
-				this.zoom = baseZoom + (-moveTop / 20);
-				this.pos = basePos + moveLeft;
-				if (this.zoom < 1) this.zoom = 1;
-				if (this.pos > 0) this.pos = 0;
-				if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
-
-				this.render();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-svg {
-	display: block;
-	padding: 16px;
-	width: 100%;
-	box-sizing: border-box;
-	cursor: all-scroll;
-}
-</style>
diff --git a/src/client/widgets/activity.vue b/src/client/widgets/activity.vue
deleted file mode 100644
index cc8d4debd0..0000000000
--- a/src/client/widgets/activity.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent">
-	<template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template>
-	<template #func><button @click="toggleView()" class="_button"><i class="fas fa-sort"></i></button></template>
-
-	<div>
-		<MkLoading v-if="fetching"/>
-		<template v-else>
-			<XCalendar v-show="props.view === 0" :data="[].concat(activity)"/>
-			<XChart v-show="props.view === 1" :data="[].concat(activity)"/>
-		</template>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import define from './define';
-import XCalendar from './activity.calendar.vue';
-import XChart from './activity.chart.vue';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'activity',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		view: {
-			type: 'number',
-			default: 0,
-			hidden: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer,
-		XCalendar,
-		XChart,
-	},
-	data() {
-		return {
-			fetching: true,
-			activity: null,
-		};
-	},
-	mounted() {
-		os.api('charts/user/notes', {
-			userId: this.$i.id,
-			span: 'day',
-			limit: 7 * 21
-		}).then(activity => {
-			this.activity = activity.diffs.normal.map((_, i) => ({
-				total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i],
-				notes: activity.diffs.normal[i],
-				replies: activity.diffs.reply[i],
-				renotes: activity.diffs.renote[i]
-			}));
-			this.fetching = false;
-		});
-	},
-	methods: {
-		toggleView() {
-			if (this.props.view === 1) {
-				this.props.view = 0;
-			} else {
-				this.props.view++;
-			}
-			this.save();
-		}
-	}
-});
-</script>
diff --git a/src/client/widgets/aichan.vue b/src/client/widgets/aichan.vue
deleted file mode 100644
index 06c49090a1..0000000000
--- a/src/client/widgets/aichan.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<template>
-<MkContainer :naked="props.transparent" :show-header="false">
-	<iframe class="dedjhjmo" ref="live2d" @click="touched" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100"></iframe>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from './define';
-import MkContainer from '@client/components/ui/container.vue';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'ai',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer,
-	},
-	data() {
-		return {
-		};
-	},
-	mounted() {
-		window.addEventListener('mousemove', ev => {
-			const iframeRect = this.$refs.live2d.getBoundingClientRect();
-			this.$refs.live2d.contentWindow.postMessage({
-				type: 'moveCursor',
-				body: {
-					x: ev.clientX - iframeRect.left,
-					y: ev.clientY - iframeRect.top,
-				}
-			}, '*');
-		}, { passive: true });
-	},
-	methods: {
-		touched() {
-			//if (this.live2d) this.live2d.changeExpression('gurugurume');
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.dedjhjmo {
-	width: 100%;
-	height: 350px;
-	border: none;
-	pointer-events: none;
-}
-</style>
diff --git a/src/client/widgets/aiscript.vue b/src/client/widgets/aiscript.vue
deleted file mode 100644
index aaf0a0372e..0000000000
--- a/src/client/widgets/aiscript.vue
+++ /dev/null
@@ -1,163 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader">
-	<template #header><i class="fas fa-terminal"></i>{{ $ts._widgets.aiscript }}</template>
-
-	<div class="uylguesu _monospace">
-		<textarea v-model="props.script" placeholder="(1 + 1)"></textarea>
-		<button @click="run" class="_buttonPrimary">RUN</button>
-		<div class="logs">
-			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div>
-		</div>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import define from './define';
-import * as os from '@client/os';
-import { AiScript, parse, utils } from '@syuilo/aiscript';
-import { createAiScriptEnv } from '@client/scripts/aiscript/api';
-
-const widget = define({
-	name: 'aiscript',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		script: {
-			type: 'string',
-			multiline: true,
-			default: '(1 + 1)',
-			hidden: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer
-	},
-
-	data() {
-		return {
-			logs: [],
-		};
-	},
-
-	methods: {
-		async run() {
-			this.logs = [];
-			const aiscript = new AiScript(createAiScriptEnv({
-				storageKey: 'widget',
-				token: this.$i?.token,
-			}), {
-				in: (q) => {
-					return new Promise(ok => {
-						os.dialog({
-							title: q,
-							input: {}
-						}).then(({ canceled, result: a }) => {
-							ok(a);
-						});
-					});
-				},
-				out: (value) => {
-					this.logs.push({
-						id: Math.random(),
-						text: value.type === 'str' ? value.value : utils.valToString(value),
-						print: true
-					});
-				},
-				log: (type, params) => {
-					switch (type) {
-						case 'end': this.logs.push({
-							id: Math.random(),
-							text: utils.valToString(params.val, true),
-							print: false
-						}); break;
-						default: break;
-					}
-				}
-			});
-
-			let ast;
-			try {
-				ast = parse(this.props.script);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: 'Syntax error :('
-				});
-				return;
-			}
-			try {
-				await aiscript.exec(ast);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: e
-				});
-			}
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.uylguesu {
-	text-align: right;
-
-	> textarea {
-		display: block;
-		width: 100%;
-		max-width: 100%;
-		min-width: 100%;
-		padding: 16px;
-		color: var(--fg);
-		background: transparent;
-		border: none;
-		border-bottom: solid 0.5px var(--divider);
-		border-radius: 0;
-		box-sizing: border-box;
-		font: inherit;
-
-		&:focus-visible {
-			outline: none;
-		}
-	}
-
-	> button {
-		display: inline-block;
-		margin: 8px;
-		padding: 0 10px;
-		height: 28px;
-		outline: none;
-		border-radius: 4px;
-
-		&:disabled {
-			opacity: 0.7;
-			cursor: default;
-		}
-	}
-
-	> .logs {
-		border-top: solid 0.5px var(--divider);
-		text-align: left;
-		padding: 16px;
-
-		&:empty {
-			display: none;
-		}
-
-		> .log {
-			&:not(.print) {
-				opacity: 0.7;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/button.vue b/src/client/widgets/button.vue
deleted file mode 100644
index af6718c507..0000000000
--- a/src/client/widgets/button.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<template>
-<div class="mkw-button">
-	<MkButton :primary="props.colored" full @click="run">
-		{{ props.label }}
-	</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import MkButton from '@client/components/ui/button.vue';
-import * as os from '@client/os';
-import { AiScript, parse, utils } from '@syuilo/aiscript';
-import { createAiScriptEnv } from '@client/scripts/aiscript/api';
-
-const widget = define({
-	name: 'button',
-	props: () => ({
-		label: {
-			type: 'string',
-			default: 'BUTTON',
-		},
-		colored: {
-			type: 'boolean',
-			default: true,
-		},
-		script: {
-			type: 'string',
-			multiline: true,
-			default: 'Mk:dialog("hello" "world")',
-		},
-	})
-});
-
-export default defineComponent({
-	components: {
-		MkButton
-	},
-	extends: widget,
-	data() {
-		return {
-		};
-	},
-	methods: {
-		async run() {
-			const aiscript = new AiScript(createAiScriptEnv({
-				storageKey: 'widget',
-				token: this.$i?.token,
-			}), {
-				in: (q) => {
-					return new Promise(ok => {
-						os.dialog({
-							title: q,
-							input: {}
-						}).then(({ canceled, result: a }) => {
-							ok(a);
-						});
-					});
-				},
-				out: (value) => {
-					// nop
-				},
-				log: (type, params) => {
-					// nop
-				}
-			});
-
-			let ast;
-			try {
-				ast = parse(this.props.script);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: 'Syntax error :('
-				});
-				return;
-			}
-			try {
-				await aiscript.exec(ast);
-			} catch (e) {
-				os.dialog({
-					type: 'error',
-					text: e
-				});
-			}
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mkw-button {
-}
-</style>
diff --git a/src/client/widgets/calendar.vue b/src/client/widgets/calendar.vue
deleted file mode 100644
index fe39145f0d..0000000000
--- a/src/client/widgets/calendar.vue
+++ /dev/null
@@ -1,204 +0,0 @@
-<template>
-<div class="mkw-calendar" :class="{ _panel: !props.transparent }">
-	<div class="calendar" :class="{ isHoliday }">
-		<p class="month-and-year">
-			<span class="year">{{ $t('yearX', { year }) }}</span>
-			<span class="month">{{ $t('monthX', { month }) }}</span>
-		</p>
-		<p class="day">{{ $t('dayX', { day }) }}</p>
-		<p class="week-day">{{ weekDay }}</p>
-	</div>
-	<div class="info">
-		<div>
-			<p>{{ $ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p>
-			<div class="meter">
-				<div class="val" :style="{ width: `${dayP}%` }"></div>
-			</div>
-		</div>
-		<div>
-			<p>{{ $ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p>
-			<div class="meter">
-				<div class="val" :style="{ width: `${monthP}%` }"></div>
-			</div>
-		</div>
-		<div>
-			<p>{{ $ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p>
-			<div class="meter">
-				<div class="val" :style="{ width: `${yearP}%` }"></div>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'calendar',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			now: new Date(),
-			year: null,
-			month: null,
-			day: null,
-			weekDay: null,
-			yearP: null,
-			dayP: null,
-			monthP: null,
-			isHoliday: null,
-			clock: null
-		};
-	},
-	created() {
-		this.tick();
-		this.clock = setInterval(this.tick, 1000);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		tick() {
-			const now = new Date();
-			const nd = now.getDate();
-			const nm = now.getMonth();
-			const ny = now.getFullYear();
-
-			this.year = ny;
-			this.month = nm + 1;
-			this.day = nd;
-			this.weekDay = [
-				this.$ts._weekday.sunday,
-				this.$ts._weekday.monday,
-				this.$ts._weekday.tuesday,
-				this.$ts._weekday.wednesday,
-				this.$ts._weekday.thursday,
-				this.$ts._weekday.friday,
-				this.$ts._weekday.saturday
-			][now.getDay()];
-
-			const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime();
-			const dayDenom   = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
-			const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
-			const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
-			const yearNumer  = now.getTime() - new Date(ny, 0, 1).getTime();
-			const yearDenom  = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
-
-			this.dayP   = dayNumer   / dayDenom   * 100;
-			this.monthP = monthNumer / monthDenom * 100;
-			this.yearP  = yearNumer  / yearDenom  * 100;
-
-			this.isHoliday = now.getDay() === 0 || now.getDay() === 6;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mkw-calendar {
-	padding: 16px 0;
-
-	&:after {
-		content: "";
-		display: block;
-		clear: both;
-	}
-
-	> .calendar {
-		float: left;
-		width: 60%;
-		text-align: center;
-
-		&.isHoliday {
-			> .day {
-				color: #ef95a0;
-			}
-		}
-
-		> p {
-			margin: 0;
-			line-height: 18px;
-			font-size: 0.9em;
-
-			> span {
-				margin: 0 4px;
-			}
-		}
-
-		> .day {
-			margin: 10px 0;
-			line-height: 32px;
-			font-size: 1.75em;
-		}
-	}
-
-	> .info {
-		display: block;
-		float: left;
-		width: 40%;
-		padding: 0 16px 0 0;
-		box-sizing: border-box;
-
-		> div {
-			margin-bottom: 8px;
-
-			&:last-child {
-				margin-bottom: 4px;
-			}
-
-			> p {
-				margin: 0 0 2px 0;
-				font-size: 0.75em;
-				line-height: 18px;
-				opacity: 0.8;
-
-				> b {
-					margin-left: 2px;
-				}
-			}
-
-			> .meter {
-				width: 100%;
-				overflow: hidden;
-				background: var(--X11);
-				border-radius: 8px;
-
-				> .val {
-					height: 4px;
-					transition: width .3s cubic-bezier(0.23, 1, 0.32, 1);
-				}
-			}
-
-			&:nth-child(1) {
-				> .meter > .val {
-					background: #f7796c;
-				}
-			}
-
-			&:nth-child(2) {
-				> .meter > .val {
-					background: #a1de41;
-				}
-			}
-
-			&:nth-child(3) {
-				> .meter > .val {
-					background: #41ddde;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/clock.vue b/src/client/widgets/clock.vue
deleted file mode 100644
index d960c3809a..0000000000
--- a/src/client/widgets/clock.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<template>
-<MkContainer :naked="props.transparent" :show-header="false">
-	<div class="vubelbmv">
-		<MkAnalogClock class="clock" :thickness="props.thickness"/>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import MkContainer from '@client/components/ui/container.vue';
-import MkAnalogClock from '@client/components/analog-clock.vue';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'clock',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		thickness: {
-			type: 'radio',
-			default: 0.1,
-			options: [{
-				value: 0.1, label: 'thin'
-			}, {
-				value: 0.2, label: 'medium'
-			}, {
-				value: 0.3, label: 'thick'
-			}]
-		}
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer,
-		MkAnalogClock
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.vubelbmv {
-	padding: 8px;
-
-	> .clock {
-		height: 150px;
-		margin: auto;
-	}
-}
-</style>
diff --git a/src/client/widgets/define.ts b/src/client/widgets/define.ts
deleted file mode 100644
index 22b7fb30a1..0000000000
--- a/src/client/widgets/define.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { defineComponent } from 'vue';
-import { throttle } from 'throttle-debounce';
-import { Form } from '@client/scripts/form';
-import * as os from '@client/os';
-
-export default function <T extends Form>(data: {
-	name: string;
-	props?: () => T;
-}) {
-	return defineComponent({
-		props: {
-			widget: {
-				type: Object,
-				required: false
-			},
-			settingCallback: {
-				required: false
-			}
-		},
-
-		emits: ['updateProps'],
-
-		data() {
-			return {
-				props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {},
-				save: throttle(3000, () => {
-					this.$emit('updateProps', this.props);
-				}),
-			};
-		},
-
-		computed: {
-			id(): string {
-				return this.widget ? this.widget.id : null;
-			},
-		},
-
-		created() {
-			this.mergeProps();
-
-			this.$watch('props', () => {
-				this.mergeProps();
-			}, { deep: true });
-
-			if (this.settingCallback) this.settingCallback(this.setting);
-		},
-
-		methods: {
-			mergeProps() {
-				if (data.props) {
-					const defaultProps = data.props();
-					for (const prop of Object.keys(defaultProps)) {
-						if (this.props.hasOwnProperty(prop)) continue;
-						this.props[prop] = defaultProps[prop].default;
-					}
-				}
-			},
-
-			async setting() {
-				const form = data.props();
-				for (const item of Object.keys(form)) {
-					form[item].default = this.props[item];
-				}
-				const { canceled, result } = await os.form(data.name, form);
-				if (canceled) return;
-
-				for (const key of Object.keys(result)) {
-					this.props[key] = result[key];
-				}
-
-				this.save();
-			},
-		}
-	});
-}
diff --git a/src/client/widgets/digital-clock.vue b/src/client/widgets/digital-clock.vue
deleted file mode 100644
index 2202c9ed4b..0000000000
--- a/src/client/widgets/digital-clock.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<template>
-<div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
-	<span>
-		<span v-text="hh"></span>
-		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
-		<span v-text="mm"></span>
-		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
-		<span v-text="ss"></span>
-		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }" v-if="props.showMs">:</span>
-		<span v-text="ms" v-if="props.showMs"></span>
-	</span>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'digitalClock',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		fontSize: {
-			type: 'number',
-			default: 1.5,
-			step: 0.1,
-		},
-		showMs: {
-			type: 'boolean',
-			default: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			clock: null,
-			hh: null,
-			mm: null,
-			ss: null,
-			ms: null,
-			showColon: true,
-		};
-	},
-	created() {
-		this.tick();
-		this.$watch(() => this.props.showMs, () => {
-			if (this.clock) clearInterval(this.clock);
-			this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000);
-		}, { immediate: true });
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		tick() {
-			const now = new Date();
-			this.hh = now.getHours().toString().padStart(2, '0');
-			this.mm = now.getMinutes().toString().padStart(2, '0');
-			this.ss = now.getSeconds().toString().padStart(2, '0');
-			this.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
-			this.showColon = now.getSeconds() % 2 === 0;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mkw-digitalClock {
-	padding: 16px 0;
-	text-align: center;
-}
-</style>
diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
deleted file mode 100644
index 8ab7f594a2..0000000000
--- a/src/client/widgets/federation.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable">
-	<template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template>
-
-	<div class="wbrkwalb">
-		<MkLoading v-if="fetching"/>
-		<transition-group tag="div" name="chart" class="instances" v-else>
-			<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
-				<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
-				<div class="body">
-					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
-					<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
-				</div>
-				<MkMiniChart class="chart" :src="charts[i].requests.received"/>
-			</div>
-		</transition-group>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import define from './define';
-import MkMiniChart from '@client/components/mini-chart.vue';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'federation',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer, MkMiniChart
-	},
-	props: {
-		foldable: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		scrollable: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	},
-	data() {
-		return {
-			instances: [],
-			charts: [],
-			fetching: true,
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 1000 * 60);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		async fetch() {
-			const instances = await os.api('federation/instances', {
-				sort: '+lastCommunicatedAt',
-				limit: 5
-			});
-			const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
-			this.instances = instances;
-			this.charts = charts;
-			this.fetching = false;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.wbrkwalb {
-	$bodyTitleHieght: 18px;
-	$bodyInfoHieght: 16px;
-
-	height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
-	overflow: hidden;
-
-	> .instances {
-		.chart-move {
-			transition: transform 1s ease;
-		}
-
-		> .instance {
-			display: flex;
-			align-items: center;
-			padding: 14px 16px;
-			border-bottom: solid 0.5px var(--divider);
-
-			> img {
-				display: block;
-				width: ($bodyTitleHieght + $bodyInfoHieght);
-				height: ($bodyTitleHieght + $bodyInfoHieght);
-				object-fit: cover;
-				border-radius: 4px;
-				margin-right: 8px;
-			}
-
-			> .body {
-				flex: 1;
-				overflow: hidden;
-				font-size: 0.9em;
-				color: var(--fg);
-				padding-right: 8px;
-
-				> .a {
-					display: block;
-					width: 100%;
-					white-space: nowrap;
-					overflow: hidden;
-					text-overflow: ellipsis;
-					line-height: $bodyTitleHieght;
-				}
-
-				> p {
-					margin: 0;
-					font-size: 75%;
-					opacity: 0.7;
-					line-height: $bodyInfoHieght;
-					white-space: nowrap;
-					overflow: hidden;
-					text-overflow: ellipsis;
-				}
-			}
-
-			> .chart {
-				height: 30px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts
deleted file mode 100644
index 51a82af080..0000000000
--- a/src/client/widgets/index.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { App, defineAsyncComponent } from 'vue';
-
-export default function(app: App) {
-	app.component('MkwMemo', defineAsyncComponent(() => import('./memo.vue')));
-	app.component('MkwNotifications', defineAsyncComponent(() => import('./notifications.vue')));
-	app.component('MkwTimeline', defineAsyncComponent(() => import('./timeline.vue')));
-	app.component('MkwCalendar', defineAsyncComponent(() => import('./calendar.vue')));
-	app.component('MkwRss', defineAsyncComponent(() => import('./rss.vue')));
-	app.component('MkwTrends', defineAsyncComponent(() => import('./trends.vue')));
-	app.component('MkwClock', defineAsyncComponent(() => import('./clock.vue')));
-	app.component('MkwActivity', defineAsyncComponent(() => import('./activity.vue')));
-	app.component('MkwPhotos', defineAsyncComponent(() => import('./photos.vue')));
-	app.component('MkwDigitalClock', defineAsyncComponent(() => import('./digital-clock.vue')));
-	app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue')));
-	app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue')));
-	app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue')));
-	app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue')));
-	app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
-	app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue')));
-	app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
-	app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
-	app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
-}
-
-export const widgets = [
-	'memo',
-	'notifications',
-	'timeline',
-	'calendar',
-	'rss',
-	'trends',
-	'clock',
-	'activity',
-	'photos',
-	'digitalClock',
-	'federation',
-	'postForm',
-	'slideshow',
-	'serverMetric',
-	'onlineUsers',
-	'jobQueue',
-	'button',
-	'aiscript',
-	'aichan',
-];
diff --git a/src/client/widgets/job-queue.vue b/src/client/widgets/job-queue.vue
deleted file mode 100644
index 327d8ede6d..0000000000
--- a/src/client/widgets/job-queue.vue
+++ /dev/null
@@ -1,183 +0,0 @@
-<template>
-<div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }">
-	<div class="inbox">
-		<div class="label">Inbox queue<i v-if="inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
-		<div class="values">
-			<div>
-				<div>Process</div>
-				<div :class="{ inc: inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(inbox.activeSincePrevTick) }}</div>
-			</div>
-			<div>
-				<div>Active</div>
-				<div :class="{ inc: inbox.active > prev.inbox.active, dec: inbox.active < prev.inbox.active }">{{ number(inbox.active) }}</div>
-			</div>
-			<div>
-				<div>Delayed</div>
-				<div :class="{ inc: inbox.delayed > prev.inbox.delayed, dec: inbox.delayed < prev.inbox.delayed }">{{ number(inbox.delayed) }}</div>
-			</div>
-			<div>
-				<div>Waiting</div>
-				<div :class="{ inc: inbox.waiting > prev.inbox.waiting, dec: inbox.waiting < prev.inbox.waiting }">{{ number(inbox.waiting) }}</div>
-			</div>
-		</div>
-	</div>
-	<div class="deliver">
-		<div class="label">Deliver queue<i v-if="deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
-		<div class="values">
-			<div>
-				<div>Process</div>
-				<div :class="{ inc: deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(deliver.activeSincePrevTick) }}</div>
-			</div>
-			<div>
-				<div>Active</div>
-				<div :class="{ inc: deliver.active > prev.deliver.active, dec: deliver.active < prev.deliver.active }">{{ number(deliver.active) }}</div>
-			</div>
-			<div>
-				<div>Delayed</div>
-				<div :class="{ inc: deliver.delayed > prev.deliver.delayed, dec: deliver.delayed < prev.deliver.delayed }">{{ number(deliver.delayed) }}</div>
-			</div>
-			<div>
-				<div>Waiting</div>
-				<div :class="{ inc: deliver.waiting > prev.deliver.waiting, dec: deliver.waiting < prev.deliver.waiting }">{{ number(deliver.waiting) }}</div>
-			</div>
-		</div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from './define';
-import * as os from '@client/os';
-import number from '@client/filters/number';
-import * as sound from '@client/scripts/sound';
-
-const widget = define({
-	name: 'jobQueue',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		sound: {
-			type: 'boolean',
-			default: false,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			connection: markRaw(os.stream.useChannel('queueStats')),
-			inbox: {
-				activeSincePrevTick: 0,
-				active: 0,
-				waiting: 0,
-				delayed: 0,
-			},
-			deliver: {
-				activeSincePrevTick: 0,
-				active: 0,
-				waiting: 0,
-				delayed: 0,
-			},
-			prev: {},
-			sound: sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1)
-		};
-	},
-	created() {
-		for (const domain of ['inbox', 'deliver']) {
-			this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
-		}
-	
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
-
-		this.connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8),
-			length: 1
-		});
-	},
-	beforeUnmount() {
-		this.connection.off('stats', this.onStats);
-		this.connection.off('statsLog', this.onStatsLog);
-		this.connection.dispose();
-	},
-	methods: {
-		onStats(stats) {
-			for (const domain of ['inbox', 'deliver']) {
-				this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
-				this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
-				this[domain].active = stats[domain].active;
-				this[domain].waiting = stats[domain].waiting;
-				this[domain].delayed = stats[domain].delayed;
-
-				if (this[domain].waiting > 0 && this.props.sound && this.sound.paused) {
-					this.sound.play();
-				}
-			}
-		},
-
-		onStatsLog(statsLog) {
-			for (const stats of [...statsLog].reverse()) {
-				this.onStats(stats);
-			}
-		},
-
-		number
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-@keyframes warnBlink {
-	0% { opacity: 1; }
-	50% { opacity: 0; }
-}
-
-.mkw-jobQueue {
-	font-size: 0.9em;
-
-	> div {
-		padding: 16px;
-
-		&:not(:first-child) {
-			border-top: solid 0.5px var(--divider);
-		}
-
-		> .label {
-			display: flex;
-
-			> .icon {
-				color: var(--warn);
-				margin-left: auto;
-				animation: warnBlink 1s infinite;
-			}
-		}
-
-		> .values {
-			display: flex;
-
-			> div {
-				flex: 1;
-
-				> div:first-child {
-					opacity: 0.7;
-				}
-
-				> div:last-child {
-					&.inc {
-						color: var(--warn);
-					}
-
-					&.dec {
-						color: var(--success);
-					}
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/memo.vue b/src/client/widgets/memo.vue
deleted file mode 100644
index 3f11e6409e..0000000000
--- a/src/client/widgets/memo.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader">
-	<template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template>
-
-	<div class="otgbylcu">
-		<textarea v-model="text" :placeholder="$ts.placeholder" @input="onChange"></textarea>
-		<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $ts.save }}</button>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import define from './define';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'memo',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer
-	},
-
-	data() {
-		return {
-			text: null,
-			changed: false,
-			timeoutId: null,
-		};
-	},
-
-	created() {
-		this.text = this.$store.state.memo;
-
-		this.$watch(() => this.$store.reactiveState.memo, text => {
-			this.text = text;
-		});
-	},
-
-	methods: {
-		onChange() {
-			this.changed = true;
-			clearTimeout(this.timeoutId);
-			this.timeoutId = setTimeout(this.saveMemo, 1000);
-		},
-
-		saveMemo() {
-			this.$store.set('memo', this.text);
-			this.changed = false;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.otgbylcu {
-	padding-bottom: 28px + 16px;
-
-	> textarea {
-		display: block;
-		width: 100%;
-		max-width: 100%;
-		min-width: 100%;
-		padding: 16px;
-		color: var(--fg);
-		background: transparent;
-		border: none;
-		border-bottom: solid 0.5px var(--divider);
-		border-radius: 0;
-		box-sizing: border-box;
-		font: inherit;
-		font-size: 0.9em;
-
-		&:focus-visible {
-			outline: none;
-		}
-	}
-
-	> button {
-		display: block;
-		position: absolute;
-		bottom: 8px;
-		right: 8px;
-		margin: 0;
-		padding: 0 10px;
-		height: 28px;
-		outline: none;
-		border-radius: 4px;
-
-		&:disabled {
-			opacity: 0.7;
-			cursor: default;
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue
deleted file mode 100644
index 5e2648f5b9..0000000000
--- a/src/client/widgets/notifications.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<MkContainer :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
-	<template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template>
-	<template #func><button @click="configure()" class="_button"><i class="fas fa-cog"></i></button></template>
-
-	<div>
-		<XNotifications :include-types="props.includingTypes"/>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import XNotifications from '@client/components/notifications.vue';
-import define from './define';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'notifications',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		height: {
-			type: 'number',
-			default: 300,
-		},
-		includingTypes: {
-			type: 'array',
-			hidden: true,
-			default: null,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-
-	components: {
-		MkContainer,
-		XNotifications,
-	},
-
-	data() {
-		return {
-		};
-	},
-
-	methods: {
-		configure() {
-			os.popup(import('@client/components/notification-setting-window.vue'), {
-				includingTypes: this.props.includingTypes,
-			}, {
-				done: async (res) => {
-					const { includingTypes } = res;
-					this.props.includingTypes = includingTypes;
-					this.save();
-				}
-			}, 'closed');
-		}
-	}
-});
-</script>
diff --git a/src/client/widgets/online-users.vue b/src/client/widgets/online-users.vue
deleted file mode 100644
index 37060fca43..0000000000
--- a/src/client/widgets/online-users.vue
+++ /dev/null
@@ -1,67 +0,0 @@
-<template>
-<div class="mkw-onlineUsers" :class="{ _panel: !props.transparent, pad: !props.transparent }">
-	<I18n v-if="onlineUsersCount" :src="$ts.onlineUsersCount" text-tag="span" class="text">
-		<template #n><b>{{ onlineUsersCount }}</b></template>
-	</I18n>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'onlineUsers',
-	props: () => ({
-		transparent: {
-			type: 'boolean',
-			default: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			onlineUsersCount: null,
-			clock: null,
-		};
-	},
-	created() {
-		this.tick();
-		this.clock = setInterval(this.tick, 1000 * 15);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		tick() {
-			os.api('get-online-users-count').then(res => {
-				this.onlineUsersCount = res.count;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.mkw-onlineUsers {
-	text-align: center;
-
-	&.pad {
-		padding: 16px 0;
-	}
-
-	> .text {
-		::v-deep(b) {
-			color: #41b781;
-		}
-
-		::v-deep(span) {
-			opacity: 0.7;
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/photos.vue b/src/client/widgets/photos.vue
deleted file mode 100644
index 25365d6b87..0000000000
--- a/src/client/widgets/photos.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent ? true : null">
-	<template #header><i class="fas fa-camera"></i>{{ $ts._widgets.photos }}</template>
-
-	<div class="">
-		<MkLoading v-if="fetching"/>
-		<div v-else :class="$style.stream">
-			<div v-for="(image, i) in images" :key="i"
-				:class="$style.img"
-				:style="`background-image: url(${thumbnail(image)})`"
-			></div>
-		</div>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import define from './define';
-import { getStaticImageUrl } from '@client/scripts/get-static-image-url';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'photos',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer,
-	},
-	data() {
-		return {
-			images: [],
-			fetching: true,
-			connection: null,
-		};
-	},
-	mounted() {
-		this.connection = markRaw(os.stream.useChannel('main'));
-
-		this.connection.on('driveFileCreated', this.onDriveFileCreated);
-
-		os.api('drive/stream', {
-			type: 'image/*',
-			limit: 9
-		}).then(images => {
-			this.images = images;
-			this.fetching = false;
-		});
-	},
-	beforeUnmount() {
-		this.connection.dispose();
-	},
-	methods: {
-		onDriveFileCreated(file) {
-			if (/^image\/.+$/.test(file.type)) {
-				this.images.unshift(file);
-				if (this.images.length > 9) this.images.pop();
-			}
-		},
-
-		thumbnail(image: any): string {
-			return this.$store.state.disableShowingAnimatedImages
-				? getStaticImageUrl(image.thumbnailUrl)
-				: image.thumbnailUrl;
-		},
-	}
-});
-</script>
-
-<style lang="scss" module>
-.root[data-transparent] {
-	.stream {
-		padding: 0;
-	}
-
-	.img {
-		border: solid 4px transparent;
-		border-radius: 8px;
-	}
-}
-
-.stream {
-	display: flex;
-	justify-content: center;
-	flex-wrap: wrap;
-	padding: 8px;
-
-	.img {
-		flex: 1 1 33%;
-		width: 33%;
-		height: 80px;
-		box-sizing: border-box;
-		background-position: center center;
-		background-size: cover;
-		background-clip: content-box;
-		border: solid 2px transparent;
-		border-radius: 4px;
-	}
-}
-</style>
diff --git a/src/client/widgets/post-form.vue b/src/client/widgets/post-form.vue
deleted file mode 100644
index 1f260c20d9..0000000000
--- a/src/client/widgets/post-form.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-<template>
-<XPostForm class="_panel" :fixed="true" :autofocus="false"/>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XPostForm from '@client/components/post-form.vue';
-import define from './define';
-
-const widget = define({
-	name: 'postForm',
-	props: () => ({
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-
-	components: {
-		XPostForm,
-	},
-});
-</script>
diff --git a/src/client/widgets/rss.vue b/src/client/widgets/rss.vue
deleted file mode 100644
index 6d19a86dff..0000000000
--- a/src/client/widgets/rss.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader">
-	<template #header><i class="fas fa-rss-square"></i>RSS</template>
-	<template #func><button class="_button" @click="setting"><i class="fas fa-cog"></i></button></template>
-
-	<div class="ekmkgxbj">
-		<MkLoading v-if="fetching"/>
-		<div class="feed" v-else>
-			<a v-for="item in items" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a>
-		</div>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import define from './define';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'rss',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		url: {
-			type: 'string',
-			default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer
-	},
-	data() {
-		return {
-			items: [],
-			fetching: true,
-			clock: null,
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 60000);
-		this.$watch(() => this.props.url, this.fetch);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		fetch() {
-			fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
-			}).then(res => {
-				res.json().then(feed => {
-					this.items = feed.items;
-					this.fetching = false;
-				});
-			});
-		},
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.ekmkgxbj {
-	> .feed {
-		padding: 0;
-		font-size: 0.9em;
-
-		> a {
-			display: block;
-			padding: 8px 16px;
-			color: var(--fg);
-			white-space: nowrap;
-			text-overflow: ellipsis;
-			overflow: hidden;
-
-			&:nth-child(even) {
-				background: rgba(#000, 0.05);
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/server-metric/cpu-mem.vue b/src/client/widgets/server-metric/cpu-mem.vue
deleted file mode 100644
index ad9e6a8b0f..0000000000
--- a/src/client/widgets/server-metric/cpu-mem.vue
+++ /dev/null
@@ -1,174 +0,0 @@
-<template>
-<div class="lcfyofjk">
-	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-		<defs>
-			<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
-				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
-				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
-			</linearGradient>
-			<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
-				<polygon
-					:points="cpuPolygonPoints"
-					fill="#fff"
-					fill-opacity="0.5"
-				/>
-				<polyline
-					:points="cpuPolylinePoints"
-					fill="none"
-					stroke="#fff"
-					stroke-width="1"
-				/>
-				<circle
-					:cx="cpuHeadX"
-					:cy="cpuHeadY"
-					r="1.5"
-					fill="#fff"
-				/>
-			</mask>
-		</defs>
-		<rect
-			x="-2" y="-2"
-			:width="viewBoxX + 4" :height="viewBoxY + 4"
-			:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"
-		/>
-		<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
-	</svg>
-	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-		<defs>
-			<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
-				<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
-				<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
-			</linearGradient>
-			<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
-				<polygon
-					:points="memPolygonPoints"
-					fill="#fff"
-					fill-opacity="0.5"
-				/>
-				<polyline
-					:points="memPolylinePoints"
-					fill="none"
-					stroke="#fff"
-					stroke-width="1"
-				/>
-				<circle
-					:cx="memHeadX"
-					:cy="memHeadY"
-					r="1.5"
-					fill="#fff"
-				/>
-			</mask>
-		</defs>
-		<rect
-			x="-2" y="-2"
-			:width="viewBoxX + 4" :height="viewBoxY + 4"
-			:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"
-		/>
-		<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
-	</svg>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-
-export default defineComponent({
-	props: {
-		connection: {
-			required: true,
-		},
-		meta: {
-			required: true,
-		}
-	},
-	data() {
-		return {
-			viewBoxX: 50,
-			viewBoxY: 30,
-			stats: [],
-			cpuGradientId: uuid(),
-			cpuMaskId: uuid(),
-			memGradientId: uuid(),
-			memMaskId: uuid(),
-			cpuPolylinePoints: '',
-			memPolylinePoints: '',
-			cpuPolygonPoints: '',
-			memPolygonPoints: '',
-			cpuHeadX: null,
-			cpuHeadY: null,
-			memHeadX: null,
-			memHeadY: null,
-			cpuP: '',
-			memP: ''
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
-		this.connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8)
-		});
-	},
-	beforeUnmount() {
-		this.connection.off('stats', this.onStats);
-		this.connection.off('statsLog', this.onStatsLog);
-	},
-	methods: {
-		onStats(stats) {
-			this.stats.push(stats);
-			if (this.stats.length > 50) this.stats.shift();
-
-			const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu) * this.viewBoxY]);
-			const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.active / this.meta.mem.total)) * this.viewBoxY]);
-			this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-			this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-
-			this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
-			this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
-
-			this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
-			this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
-			this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
-			this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
-
-			this.cpuP = (stats.cpu * 100).toFixed(0);
-			this.memP = (stats.mem.active / this.meta.mem.total * 100).toFixed(0);
-		},
-		onStatsLog(statsLog) {
-			for (const stats of [...statsLog].reverse()) {
-				this.onStats(stats);
-			}
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.lcfyofjk {
-	display: flex;
-
-	> svg {
-		display: block;
-		padding: 10px;
-		width: 50%;
-
-		&:first-child {
-			padding-right: 5px;
-		}
-
-		&:last-child {
-			padding-left: 5px;
-		}
-
-		> text {
-			font-size: 5px;
-			fill: currentColor;
-
-			> tspan {
-				opacity: 0.5;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/server-metric/cpu.vue b/src/client/widgets/server-metric/cpu.vue
deleted file mode 100644
index 4478ee3065..0000000000
--- a/src/client/widgets/server-metric/cpu.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<div class="vrvdvrys">
-	<XPie class="pie" :value="usage"/>
-	<div>
-		<p><i class="fas fa-microchip"></i>CPU</p>
-		<p>{{ meta.cpu.cores }} Logical cores</p>
-		<p>{{ meta.cpu.model }}</p>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XPie from './pie.vue';
-
-export default defineComponent({
-	components: {
-		XPie
-	},
-	props: {
-		connection: {
-			required: true,
-		},
-		meta: {
-			required: true,
-		}
-	},
-	data() {
-		return {
-			usage: 0,
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-	},
-	beforeUnmount() {
-		this.connection.off('stats', this.onStats);
-	},
-	methods: {
-		onStats(stats) {
-			this.usage = stats.cpu;
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.vrvdvrys {
-	display: flex;
-	padding: 16px;
-
-	> .pie {
-		height: 82px;
-		flex-shrink: 0;
-		margin-right: 16px;
-	}
-
-	> div {
-		flex: 1;
-
-		> p {
-			margin: 0;
-			font-size: 0.8em;
-
-			&:first-child {
-				font-weight: bold;
-				margin-bottom: 4px;
-
-				> i {
-					margin-right: 4px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/server-metric/disk.vue b/src/client/widgets/server-metric/disk.vue
deleted file mode 100644
index a3f5d0376b..0000000000
--- a/src/client/widgets/server-metric/disk.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<div class="zbwaqsat">
-	<XPie class="pie" :value="usage"/>
-	<div>
-		<p><i class="fas fa-hdd"></i>Disk</p>
-		<p>Total: {{ bytes(total, 1) }}</p>
-		<p>Free: {{ bytes(available, 1) }}</p>
-		<p>Used: {{ bytes(used, 1) }}</p>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XPie from './pie.vue';
-import bytes from '@client/filters/bytes';
-
-export default defineComponent({
-	components: {
-		XPie
-	},
-	props: {
-		meta: {
-			required: true,
-		}
-	},
-	data() {
-		return {
-			usage: this.meta.fs.used / this.meta.fs.total,
-			total: this.meta.fs.total,
-			used: this.meta.fs.used,
-			available: this.meta.fs.total - this.meta.fs.used,
-		};
-	},
-	methods: {
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.zbwaqsat {
-	display: flex;
-	padding: 16px;
-
-	> .pie {
-		height: 82px;
-		flex-shrink: 0;
-		margin-right: 16px;
-	}
-
-	> div {
-		flex: 1;
-
-		> p {
-			margin: 0;
-			font-size: 0.8em;
-
-			&:first-child {
-				font-weight: bold;
-				margin-bottom: 4px;
-
-				> i {
-					margin-right: 4px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/server-metric/index.vue b/src/client/widgets/server-metric/index.vue
deleted file mode 100644
index 45cd8cebf2..0000000000
--- a/src/client/widgets/server-metric/index.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent">
-	<template #header><i class="fas fa-server"></i>{{ $ts._widgets.serverMetric }}</template>
-	<template #func><button @click="toggleView()" class="_button"><i class="fas fa-sort"></i></button></template>
-
-	<div class="mkw-serverMetric" v-if="meta">
-		<XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/>
-		<XNet v-if="props.view === 1" :connection="connection" :meta="meta"/>
-		<XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/>
-		<XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/>
-		<XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from '../define';
-import MkContainer from '@client/components/ui/container.vue';
-import XCpuMemory from './cpu-mem.vue';
-import XNet from './net.vue';
-import XCpu from './cpu.vue';
-import XMemory from './mem.vue';
-import XDisk from './disk.vue';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'serverMetric',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		transparent: {
-			type: 'boolean',
-			default: false,
-		},
-		view: {
-			type: 'number',
-			default: 0,
-			hidden: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer,
-		XCpuMemory,
-		XNet,
-		XCpu,
-		XMemory,
-		XDisk,
-	},
-	data() {
-		return {
-			meta: null,
-			connection: null,
-		};
-	},
-	created() {
-		os.api('server-info', {}).then(res => {
-			this.meta = res;
-		});
-		this.connection = markRaw(os.stream.useChannel('serverStats'));
-	},
-	unmounted() {
-		this.connection.dispose();
-	},
-	methods: {
-		toggleView() {
-			if (this.props.view == 4) {
-				this.props.view = 0;
-			} else {
-				this.props.view++;
-			}
-			this.save();
-		},
-	}
-});
-</script>
diff --git a/src/client/widgets/server-metric/mem.vue b/src/client/widgets/server-metric/mem.vue
deleted file mode 100644
index 92c0aa0c77..0000000000
--- a/src/client/widgets/server-metric/mem.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<div class="zlxnikvl">
-	<XPie class="pie" :value="usage"/>
-	<div>
-		<p><i class="fas fa-memory"></i>RAM</p>
-		<p>Total: {{ bytes(total, 1) }}</p>
-		<p>Used: {{ bytes(used, 1) }}</p>
-		<p>Free: {{ bytes(free, 1) }}</p>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XPie from './pie.vue';
-import bytes from '@client/filters/bytes';
-
-export default defineComponent({
-	components: {
-		XPie
-	},
-	props: {
-		connection: {
-			required: true,
-		},
-		meta: {
-			required: true,
-		}
-	},
-	data() {
-		return {
-			usage: 0,
-			total: 0,
-			used: 0,
-			free: 0,
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-	},
-	beforeUnmount() {
-		this.connection.off('stats', this.onStats);
-	},
-	methods: {
-		onStats(stats) {
-			this.usage = stats.mem.active / this.meta.mem.total;
-			this.total = this.meta.mem.total;
-			this.used = stats.mem.active;
-			this.free = this.meta.mem.total - stats.mem.active;
-		},
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.zlxnikvl {
-	display: flex;
-	padding: 16px;
-
-	> .pie {
-		height: 82px;
-		flex-shrink: 0;
-		margin-right: 16px;
-	}
-
-	> div {
-		flex: 1;
-
-		> p {
-			margin: 0;
-			font-size: 0.8em;
-
-			&:first-child {
-				font-weight: bold;
-				margin-bottom: 4px;
-
-				> i {
-					margin-right: 4px;
-				}
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/server-metric/net.vue b/src/client/widgets/server-metric/net.vue
deleted file mode 100644
index 569c15b58b..0000000000
--- a/src/client/widgets/server-metric/net.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<template>
-<div class="oxxrhrto">
-	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-		<polygon
-			:points="inPolygonPoints"
-			fill="#94a029"
-			fill-opacity="0.5"
-		/>
-		<polyline
-			:points="inPolylinePoints"
-			fill="none"
-			stroke="#94a029"
-			stroke-width="1"
-		/>
-		<circle
-			:cx="inHeadX"
-			:cy="inHeadY"
-			r="1.5"
-			fill="#94a029"
-		/>
-		<text x="1" y="5">NET rx <tspan>{{ bytes(inRecent) }}</tspan></text>
-	</svg>
-	<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
-		<polygon
-			:points="outPolygonPoints"
-			fill="#ff9156"
-			fill-opacity="0.5"
-		/>
-		<polyline
-			:points="outPolylinePoints"
-			fill="none"
-			stroke="#ff9156"
-			stroke-width="1"
-		/>
-		<circle
-			:cx="outHeadX"
-			:cy="outHeadY"
-			r="1.5"
-			fill="#ff9156"
-		/>
-		<text x="1" y="5">NET tx <tspan>{{ bytes(outRecent) }}</tspan></text>
-	</svg>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import bytes from '@client/filters/bytes';
-
-export default defineComponent({
-	props: {
-		connection: {
-			required: true,
-		},
-		meta: {
-			required: true,
-		}
-	},
-	data() {
-		return {
-			viewBoxX: 50,
-			viewBoxY: 30,
-			stats: [],
-			inPolylinePoints: '',
-			outPolylinePoints: '',
-			inPolygonPoints: '',
-			outPolygonPoints: '',
-			inHeadX: null,
-			inHeadY: null,
-			outHeadX: null,
-			outHeadY: null,
-			inRecent: 0,
-			outRecent: 0
-		};
-	},
-	mounted() {
-		this.connection.on('stats', this.onStats);
-		this.connection.on('statsLog', this.onStatsLog);
-		this.connection.send('requestLog', {
-			id: Math.random().toString().substr(2, 8)
-		});
-	},
-	beforeUnmount() {
-		this.connection.off('stats', this.onStats);
-		this.connection.off('statsLog', this.onStatsLog);
-	},
-	methods: {
-		onStats(stats) {
-			this.stats.push(stats);
-			if (this.stats.length > 50) this.stats.shift();
-
-			const inPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.rx)));
-			const outPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.tx)));
-
-			const inPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * this.viewBoxY]);
-			const outPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * this.viewBoxY]);
-			this.inPolylinePoints = inPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-			this.outPolylinePoints = outPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
-
-			this.inPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.inPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
-			this.outPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.outPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
-
-			this.inHeadX = inPolylinePoints[inPolylinePoints.length - 1][0];
-			this.inHeadY = inPolylinePoints[inPolylinePoints.length - 1][1];
-			this.outHeadX = outPolylinePoints[outPolylinePoints.length - 1][0];
-			this.outHeadY = outPolylinePoints[outPolylinePoints.length - 1][1];
-
-			this.inRecent = stats.net.rx;
-			this.outRecent = stats.net.tx;
-		},
-		onStatsLog(statsLog) {
-			for (const stats of [...statsLog].reverse()) {
-				this.onStats(stats);
-			}
-		},
-		bytes
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.oxxrhrto {
-	display: flex;
-
-	> svg {
-		display: block;
-		padding: 10px;
-		width: 50%;
-
-		&:first-child {
-			padding-right: 5px;
-		}
-
-		&:last-child {
-			padding-left: 5px;
-		}
-
-		> text {
-			font-size: 5px;
-			fill: currentColor;
-
-			> tspan {
-				opacity: 0.5;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/server-metric/pie.vue b/src/client/widgets/server-metric/pie.vue
deleted file mode 100644
index 38dcf6fcd9..0000000000
--- a/src/client/widgets/server-metric/pie.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<svg class="hsalcinq" viewBox="0 0 1 1" preserveAspectRatio="none">
-	<circle
-		:r="r"
-		cx="50%" cy="50%"
-		fill="none"
-		stroke-width="0.1"
-		stroke="rgba(0, 0, 0, 0.05)"
-	/>
-	<circle
-		:r="r"
-		cx="50%" cy="50%"
-		:stroke-dasharray="Math.PI * (r * 2)"
-		:stroke-dashoffset="strokeDashoffset"
-		fill="none"
-		stroke-width="0.1"
-		:stroke="color"
-	/>
-	<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text>
-</svg>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	props: {
-		value: {
-			type: Number,
-			required: true
-		}
-	},
-	data() {
-		return {
-			r: 0.45
-		};
-	},
-	computed: {
-		color(): string {
-			return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
-		},
-		strokeDashoffset(): number {
-			return (1 - this.value) * (Math.PI * (this.r * 2));
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.hsalcinq {
-	display: block;
-	height: 100%;
-
-	> circle {
-		transform-origin: center;
-		transform: rotate(-90deg);
-		transition: stroke-dashoffset 0.5s ease;
-	}
-
-	> text {
-		font-size: 0.15px;
-		fill: currentColor;
-	}
-}
-</style>
diff --git a/src/client/widgets/slideshow.vue b/src/client/widgets/slideshow.vue
deleted file mode 100644
index 2f079e0d42..0000000000
--- a/src/client/widgets/slideshow.vue
+++ /dev/null
@@ -1,167 +0,0 @@
-<template>
-<div class="kvausudm _panel">
-	<div @click="choose">
-		<p v-if="props.folderId == null">
-			<template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template>
-			<template v-else>{{ $ts.folder }}</template>
-		</p>
-		<p v-if="props.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
-		<div ref="slideA" class="slide a"></div>
-		<div ref="slideB" class="slide b"></div>
-	</div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'slideshow',
-	props: () => ({
-		height: {
-			type: 'number',
-			default: 300,
-		},
-		folderId: {
-			type: 'string',
-			default: null,
-			hidden: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	data() {
-		return {
-			images: [],
-			fetching: true,
-			clock: null
-		};
-	},
-	mounted() {
-		this.$nextTick(() => {
-			this.applySize();
-		});
-
-		if (this.props.folderId != null) {
-			this.fetch();
-		}
-
-		this.clock = setInterval(this.change, 10000);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		applySize() {
-			let h;
-
-			if (this.props.size == 1) {
-				h = 250;
-			} else {
-				h = 170;
-			}
-
-			this.$el.style.height = `${h}px`;
-		},
-		resize() {
-			if (this.props.size == 1) {
-				this.props.size = 0;
-			} else {
-				this.props.size++;
-			}
-			this.save();
-
-			this.applySize();
-		},
-		change() {
-			if (this.images.length == 0) return;
-
-			const index = Math.floor(Math.random() * this.images.length);
-			const img = `url(${ this.images[index].url })`;
-
-			(this.$refs.slideB as any).style.backgroundImage = img;
-
-			this.$refs.slideB.classList.add('anime');
-			setTimeout(() => {
-				// 既にこのウィジェットがunmountされていたら要素がない
-				if ((this.$refs.slideA as any) == null) return;
-
-				(this.$refs.slideA as any).style.backgroundImage = img;
-
-				this.$refs.slideB.classList.remove('anime');
-			}, 1000);
-		},
-		fetch() {
-			this.fetching = true;
-
-			os.api('drive/files', {
-				folderId: this.props.folderId,
-				type: 'image/*',
-				limit: 100
-			}).then(images => {
-				this.images = images;
-				this.fetching = false;
-				(this.$refs.slideA as any).style.backgroundImage = '';
-				(this.$refs.slideB as any).style.backgroundImage = '';
-				this.change();
-			});
-		},
-		choose() {
-			os.selectDriveFolder(false).then(folder => {
-				if (folder == null) {
-					return;
-				}
-				this.props.folderId = folder.id;
-				this.save();
-				this.fetch();
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.kvausudm {
-	position: relative;
-
-	> div {
-		width: 100%;
-		height: 100%;
-		cursor: pointer;
-
-		> p {
-			display: block;
-			margin: 1em;
-			text-align: center;
-			color: #888;
-		}
-
-		> * {
-			pointer-events: none;
-		}
-
-		> .slide {
-			position: absolute;
-			top: 0;
-			left: 0;
-			width: 100%;
-			height: 100%;
-			background-size: cover;
-			background-position: center;
-
-			&.b {
-				opacity: 0;
-			}
-
-			&.anime {
-				transition: opacity 1s;
-				opacity: 1;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/client/widgets/timeline.vue b/src/client/widgets/timeline.vue
deleted file mode 100644
index bd951d8565..0000000000
--- a/src/client/widgets/timeline.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true">
-	<template #header>
-		<button @click="choose" class="_button">
-			<i v-if="props.src === 'home'" class="fas fa-home"></i>
-			<i v-else-if="props.src === 'local'" class="fas fa-comments"></i>
-			<i v-else-if="props.src === 'social'" class="fas fa-share-alt"></i>
-			<i v-else-if="props.src === 'global'" class="fas fa-globe"></i>
-			<i v-else-if="props.src === 'list'" class="fas fa-list-ul"></i>
-			<i v-else-if="props.src === 'antenna'" class="fas fa-satellite"></i>
-			<span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
-			<i :class="menuOpened ? 'fas fa-angle-up' : 'fas fa-angle-down'" style="margin-left: 8px;"></i>
-		</button>
-	</template>
-
-	<div>
-		<XTimeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import XTimeline from '@client/components/timeline.vue';
-import define from './define';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'timeline',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-		height: {
-			type: 'number',
-			default: 300,
-		},
-		src: {
-			type: 'string',
-			default: 'home',
-			hidden: true,
-		},
-		list: {
-			type: 'object',
-			default: null,
-			hidden: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer,
-		XTimeline,
-	},
-
-	data() {
-		return {
-			menuOpened: false,
-		};
-	},
-
-	methods: {
-		async choose(ev) {
-			this.menuOpened = true;
-			const [antennas, lists] = await Promise.all([
-				os.api('antennas/list'),
-				os.api('users/lists/list')
-			]);
-			const antennaItems = antennas.map(antenna => ({
-				text: antenna.name,
-				icon: 'fas fa-satellite',
-				action: () => {
-					this.props.antenna = antenna;
-					this.setSrc('antenna');
-				}
-			}));
-			const listItems = lists.map(list => ({
-				text: list.name,
-				icon: 'fas fa-list-ul',
-				action: () => {
-					this.props.list = list;
-					this.setSrc('list');
-				}
-			}));
-			os.popupMenu([{
-				text: this.$ts._timelines.home,
-				icon: 'fas fa-home',
-				action: () => { this.setSrc('home') }
-			}, {
-				text: this.$ts._timelines.local,
-				icon: 'fas fa-comments',
-				action: () => { this.setSrc('local') }
-			}, {
-				text: this.$ts._timelines.social,
-				icon: 'fas fa-share-alt',
-				action: () => { this.setSrc('social') }
-			}, {
-				text: this.$ts._timelines.global,
-				icon: 'fas fa-globe',
-				action: () => { this.setSrc('global') }
-			}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {
-				this.menuOpened = false;
-			});
-		},
-
-		setSrc(src) {
-			this.props.src = src;
-			this.save();
-		},
-	}
-});
-</script>
diff --git a/src/client/widgets/trends.vue b/src/client/widgets/trends.vue
deleted file mode 100644
index 8511bc718f..0000000000
--- a/src/client/widgets/trends.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-<template>
-<MkContainer :show-header="props.showHeader">
-	<template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template>
-
-	<div class="wbrkwala">
-		<MkLoading v-if="fetching"/>
-		<transition-group tag="div" name="chart" class="tags" v-else>
-			<div v-for="stat in stats" :key="stat.tag">
-				<div class="tag">
-					<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
-					<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
-				</div>
-				<MkMiniChart class="chart" :src="stat.chart"/>
-			</div>
-		</transition-group>
-	</div>
-</MkContainer>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@client/components/ui/container.vue';
-import define from './define';
-import MkMiniChart from '@client/components/mini-chart.vue';
-import * as os from '@client/os';
-
-const widget = define({
-	name: 'hashtags',
-	props: () => ({
-		showHeader: {
-			type: 'boolean',
-			default: true,
-		},
-	})
-});
-
-export default defineComponent({
-	extends: widget,
-	components: {
-		MkContainer, MkMiniChart
-	},
-	data() {
-		return {
-			stats: [],
-			fetching: true,
-		};
-	},
-	mounted() {
-		this.fetch();
-		this.clock = setInterval(this.fetch, 1000 * 60);
-	},
-	beforeUnmount() {
-		clearInterval(this.clock);
-	},
-	methods: {
-		fetch() {
-			os.api('hashtags/trend').then(stats => {
-				this.stats = stats;
-				this.fetching = false;
-			});
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.wbrkwala {
-	height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
-	overflow: hidden;
-
-	> .tags {
-		.chart-move {
-			transition: transform 1s ease;
-		}
-
-		> div {
-			display: flex;
-			align-items: center;
-			padding: 14px 16px;
-			border-bottom: solid 0.5px var(--divider);
-
-			> .tag {
-				flex: 1;
-				overflow: hidden;
-				font-size: 0.9em;
-				color: var(--fg);
-
-				> .a {
-					display: block;
-					width: 100%;
-					white-space: nowrap;
-					overflow: hidden;
-					text-overflow: ellipsis;
-					line-height: 18px;
-				}
-
-				> p {
-					margin: 0;
-					font-size: 75%;
-					opacity: 0.7;
-					line-height: 16px;
-				}
-			}
-
-			> .chart {
-				height: 30px;
-			}
-		}
-	}
-}
-</style>
diff --git a/src/config/index.ts b/src/config/index.ts
deleted file mode 100644
index 7bfdca4612..0000000000
--- a/src/config/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import load from './load';
-
-export default load();
diff --git a/src/config/load.ts b/src/config/load.ts
deleted file mode 100644
index c7965e6c41..0000000000
--- a/src/config/load.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Config loader
- */
-
-import * as fs from 'fs';
-import { fileURLToPath } from 'url';
-import { dirname } from 'path';
-import * as yaml from 'js-yaml';
-import { Source, Mixin } from './types';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-/**
- * Path of configuration directory
- */
-const dir = `${_dirname}/../../.config`;
-
-/**
- * Path of configuration file
- */
-const path = process.env.NODE_ENV === 'test'
-	? `${dir}/test.yml`
-	: `${dir}/default.yml`;
-
-export default function load() {
-	const meta = JSON.parse(fs.readFileSync(`${_dirname}/../meta.json`, 'utf-8'));
-	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
-
-	const mixin = {} as Mixin;
-
-	const url = tryCreateUrl(config.url);
-
-	config.url = url.origin;
-
-	config.port = config.port || parseInt(process.env.PORT || '', 10);
-
-	mixin.version = meta.version;
-	mixin.host = url.host;
-	mixin.hostname = url.hostname;
-	mixin.scheme = url.protocol.replace(/:$/, '');
-	mixin.wsScheme = mixin.scheme.replace('http', 'ws');
-	mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
-	mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`;
-	mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
-	mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
-	mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
-
-	if (!config.redis.prefix) config.redis.prefix = mixin.host;
-
-	return Object.assign(config, mixin);
-}
-
-function tryCreateUrl(url: string) {
-	try {
-		return new URL(url);
-	} catch (e) {
-		throw `url="${url}" is not a valid URL.`;
-	}
-}
diff --git a/src/config/types.ts b/src/config/types.ts
deleted file mode 100644
index e3ca6c1ab6..0000000000
--- a/src/config/types.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * ユーザーが設定する必要のある情報
- */
-export type Source = {
-	repository_url?: string;
-	feedback_url?: string;
-	url: string;
-	port: number;
-	https?: { [x: string]: string };
-	disableHsts?: boolean;
-	db: {
-		host: string;
-		port: number;
-		db: string;
-		user: string;
-		pass: string;
-		disableCache?: boolean;
-		extra?: { [x: string]: string };
-	};
-	redis: {
-		host: string;
-		port: number;
-		pass: string;
-		db?: number;
-		prefix?: string;
-	};
-	elasticsearch: {
-		host: string;
-		port: number;
-		ssl?: boolean;
-		user?: string;
-		pass?: string;
-		index?: string;
-	};
-
-	proxy?: string;
-	proxySmtp?: string;
-	proxyBypassHosts?: string[];
-
-	allowedPrivateNetworks?: string[];
-
-	maxFileSize?: number;
-
-	accesslog?: string;
-
-	clusterLimit?: number;
-
-	id: string;
-
-	outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual';
-
-	deliverJobConcurrency?: number;
-	inboxJobConcurrency?: number;
-	deliverJobPerSec?: number;
-	inboxJobPerSec?: number;
-	deliverJobMaxAttempts?: number;
-	inboxJobMaxAttempts?: number;
-
-	syslog: {
-		host: string;
-		port: number;
-	};
-
-	mediaProxy?: string;
-
-	signToActivityPubGet?: boolean;
-};
-
-/**
- * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報
- */
-export type Mixin = {
-	version: string;
-	host: string;
-	hostname: string;
-	scheme: string;
-	wsScheme: string;
-	apiUrl: string;
-	wsUrl: string;
-	authUrl: string;
-	driveUrl: string;
-	userAgent: string;
-};
-
-export type Config = Source & Mixin;
diff --git a/src/const.ts b/src/const.ts
deleted file mode 100644
index 43f59f1e4f..0000000000
--- a/src/const.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
-export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
diff --git a/src/daemons/janitor.ts b/src/daemons/janitor.ts
deleted file mode 100644
index 72568cfe18..0000000000
--- a/src/daemons/janitor.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// TODO: 消したい
-
-const interval = 30 * 60 * 1000;
-import { AttestationChallenges } from '@/models/index';
-import { LessThan } from 'typeorm';
-
-/**
- * Clean up database occasionally
- */
-export default function() {
-	async function tick() {
-		await AttestationChallenges.delete({
-			createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000))
-		});
-	}
-
-	tick();
-
-	setInterval(tick, interval);
-}
diff --git a/src/daemons/queue-stats.ts b/src/daemons/queue-stats.ts
deleted file mode 100644
index 77f09b18d6..0000000000
--- a/src/daemons/queue-stats.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import Xev from 'xev';
-import { deliverQueue, inboxQueue } from '../queue/queues';
-
-const ev = new Xev();
-
-const interval = 10000;
-
-/**
- * Report queue stats regularly
- */
-export default function() {
-	const log = [] as any[];
-
-	ev.on('requestQueueStatsLog', x => {
-		ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50));
-	});
-
-	let activeDeliverJobs = 0;
-	let activeInboxJobs = 0;
-
-	deliverQueue.on('global:active', () => {
-		activeDeliverJobs++;
-	});
-
-	inboxQueue.on('global:active', () => {
-		activeInboxJobs++;
-	});
-
-	async function tick() {
-		const deliverJobCounts = await deliverQueue.getJobCounts();
-		const inboxJobCounts = await inboxQueue.getJobCounts();
-
-		const stats = {
-			deliver: {
-				activeSincePrevTick: activeDeliverJobs,
-				active: deliverJobCounts.active,
-				waiting: deliverJobCounts.waiting,
-				delayed: deliverJobCounts.delayed
-			},
-			inbox: {
-				activeSincePrevTick: activeInboxJobs,
-				active: inboxJobCounts.active,
-				waiting: inboxJobCounts.waiting,
-				delayed: inboxJobCounts.delayed
-			},
-		};
-
-		ev.emit('queueStats', stats);
-
-		log.unshift(stats);
-		if (log.length > 200) log.pop();
-
-		activeDeliverJobs = 0;
-		activeInboxJobs = 0;
-	}
-
-	tick();
-
-	setInterval(tick, interval);
-}
diff --git a/src/daemons/server-stats.ts b/src/daemons/server-stats.ts
deleted file mode 100644
index 8dfa946250..0000000000
--- a/src/daemons/server-stats.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import * as si from 'systeminformation';
-import Xev from 'xev';
-import * as osUtils from 'os-utils';
-
-const ev = new Xev();
-
-const interval = 2000;
-
-const roundCpu = (num: number) => Math.round(num * 1000) / 1000;
-const round = (num: number) => Math.round(num * 10) / 10;
-
-/**
- * Report server stats regularly
- */
-export default function() {
-	const log = [] as any[];
-
-	ev.on('requestServerStatsLog', x => {
-		ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
-	});
-
-	async function tick() {
-		const cpu = await cpuUsage();
-		const memStats = await mem();
-		const netStats = await net();
-		const fsStats = await fs();
-
-		const stats = {
-			cpu: roundCpu(cpu),
-			mem: {
-				used: round(memStats.used - memStats.buffers - memStats.cached),
-				active: round(memStats.active),
-			},
-			net: {
-				rx: round(Math.max(0, netStats.rx_sec)),
-				tx: round(Math.max(0, netStats.tx_sec)),
-			},
-			fs: {
-				r: round(Math.max(0, fsStats.rIO_sec)),
-				w: round(Math.max(0, fsStats.wIO_sec)),
-			}
-		};
-		ev.emit('serverStats', stats);
-		log.unshift(stats);
-		if (log.length > 200) log.pop();
-	}
-
-	tick();
-
-	setInterval(tick, interval);
-}
-
-// CPU STAT
-function cpuUsage() {
-	return new Promise((res, rej) => {
-		osUtils.cpuUsage((cpuUsage: number) => {
-			res(cpuUsage);
-		});
-	});
-}
-
-// MEMORY STAT
-async function mem() {
-	const data = await si.mem();
-	return data;
-}
-
-// NETWORK STAT
-async function net() {
-	const iface = await si.networkInterfaceDefault();
-	const data = await si.networkStats(iface);
-	return data[0];
-}
-
-// FS STAT
-async function fs() {
-	const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
-	return data || { rIO_sec: 0, wIO_sec: 0 };
-}
diff --git a/src/db/elasticsearch.ts b/src/db/elasticsearch.ts
deleted file mode 100644
index c99183007a..0000000000
--- a/src/db/elasticsearch.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as elasticsearch from '@elastic/elasticsearch';
-import config from '@/config/index';
-
-const index = {
-	settings: {
-		analysis: {
-			analyzer: {
-				ngram: {
-					tokenizer: 'ngram'
-				}
-			}
-		}
-	},
-	mappings: {
-		properties: {
-			text: {
-				type: 'text',
-				index: true,
-				analyzer: 'ngram',
-			},
-			userId: {
-				type: 'keyword',
-				index: true,
-			},
-			userHost: {
-				type: 'keyword',
-				index: true,
-			}
-		}
-	}
-};
-
-// Init ElasticSearch connection
-const client = config.elasticsearch ? new elasticsearch.Client({
-	node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`,
-	auth: (config.elasticsearch.user && config.elasticsearch.pass) ? {
-		username: config.elasticsearch.user,
-		password: config.elasticsearch.pass
-	} : undefined,
-	pingTimeout: 30000
-}) : null;
-
-if (client) {
-	client.indices.exists({
-		index: config.elasticsearch.index || 'misskey_note',
-	}).then(exist => {
-		if (!exist.body) {
-			client.indices.create({
-				index: config.elasticsearch.index || 'misskey_note',
-				body: index
-			});
-		}
-	});
-}
-
-export default client;
diff --git a/src/db/logger.ts b/src/db/logger.ts
deleted file mode 100644
index 62f90555a0..0000000000
--- a/src/db/logger.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Logger from '@/services/logger';
-
-export const dbLogger = new Logger('db');
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
deleted file mode 100644
index f52c2ab722..0000000000
--- a/src/db/postgre.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-// https://github.com/typeorm/typeorm/issues/2400
-const types = require('pg').types;
-types.setTypeParser(20, Number);
-
-import { createConnection, Logger, getConnection } from 'typeorm';
-import config from '@/config/index';
-import { entities as charts } from '@/services/chart/entities';
-import { dbLogger } from './logger';
-import * as highlight from 'cli-highlight';
-
-import { User } from '@/models/entities/user';
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFolder } from '@/models/entities/drive-folder';
-import { AccessToken } from '@/models/entities/access-token';
-import { App } from '@/models/entities/app';
-import { PollVote } from '@/models/entities/poll-vote';
-import { Note } from '@/models/entities/note';
-import { NoteReaction } from '@/models/entities/note-reaction';
-import { NoteWatching } from '@/models/entities/note-watching';
-import { NoteThreadMuting } from '@/models/entities/note-thread-muting';
-import { NoteUnread } from '@/models/entities/note-unread';
-import { Notification } from '@/models/entities/notification';
-import { Meta } from '@/models/entities/meta';
-import { Following } from '@/models/entities/following';
-import { Instance } from '@/models/entities/instance';
-import { Muting } from '@/models/entities/muting';
-import { SwSubscription } from '@/models/entities/sw-subscription';
-import { Blocking } from '@/models/entities/blocking';
-import { UserList } from '@/models/entities/user-list';
-import { UserListJoining } from '@/models/entities/user-list-joining';
-import { UserGroup } from '@/models/entities/user-group';
-import { UserGroupJoining } from '@/models/entities/user-group-joining';
-import { UserGroupInvitation } from '@/models/entities/user-group-invitation';
-import { Hashtag } from '@/models/entities/hashtag';
-import { NoteFavorite } from '@/models/entities/note-favorite';
-import { AbuseUserReport } from '@/models/entities/abuse-user-report';
-import { RegistrationTicket } from '@/models/entities/registration-tickets';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-import { Signin } from '@/models/entities/signin';
-import { AuthSession } from '@/models/entities/auth-session';
-import { FollowRequest } from '@/models/entities/follow-request';
-import { Emoji } from '@/models/entities/emoji';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { ReversiMatching } from '@/models/entities/games/reversi/matching';
-import { UserNotePining } from '@/models/entities/user-note-pining';
-import { Poll } from '@/models/entities/poll';
-import { UserKeypair } from '@/models/entities/user-keypair';
-import { UserPublickey } from '@/models/entities/user-publickey';
-import { UserProfile } from '@/models/entities/user-profile';
-import { UserSecurityKey } from '@/models/entities/user-security-key';
-import { AttestationChallenge } from '@/models/entities/attestation-challenge';
-import { Page } from '@/models/entities/page';
-import { PageLike } from '@/models/entities/page-like';
-import { GalleryPost } from '@/models/entities/gallery-post';
-import { GalleryLike } from '@/models/entities/gallery-like';
-import { ModerationLog } from '@/models/entities/moderation-log';
-import { UsedUsername } from '@/models/entities/used-username';
-import { Announcement } from '@/models/entities/announcement';
-import { AnnouncementRead } from '@/models/entities/announcement-read';
-import { Clip } from '@/models/entities/clip';
-import { ClipNote } from '@/models/entities/clip-note';
-import { Antenna } from '@/models/entities/antenna';
-import { AntennaNote } from '@/models/entities/antenna-note';
-import { PromoNote } from '@/models/entities/promo-note';
-import { PromoRead } from '@/models/entities/promo-read';
-import { envOption } from '../env';
-import { Relay } from '@/models/entities/relay';
-import { MutedNote } from '@/models/entities/muted-note';
-import { Channel } from '@/models/entities/channel';
-import { ChannelFollowing } from '@/models/entities/channel-following';
-import { ChannelNotePining } from '@/models/entities/channel-note-pining';
-import { RegistryItem } from '@/models/entities/registry-item';
-import { Ad } from '@/models/entities/ad';
-import { PasswordResetRequest } from '@/models/entities/password-reset-request';
-import { UserPending } from '@/models/entities/user-pending';
-
-const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
-
-class MyCustomLogger implements Logger {
-	private highlight(sql: string) {
-		return highlight.highlight(sql, {
-			language: 'sql', ignoreIllegals: true,
-		});
-	}
-
-	public logQuery(query: string, parameters?: any[]) {
-		if (envOption.verbose) {
-			sqlLogger.info(this.highlight(query));
-		}
-	}
-
-	public logQueryError(error: string, query: string, parameters?: any[]) {
-		sqlLogger.error(this.highlight(query));
-	}
-
-	public logQuerySlow(time: number, query: string, parameters?: any[]) {
-		sqlLogger.warn(this.highlight(query));
-	}
-
-	public logSchemaBuild(message: string) {
-		sqlLogger.info(message);
-	}
-
-	public log(message: string) {
-		sqlLogger.info(message);
-	}
-
-	public logMigration(message: string) {
-		sqlLogger.info(message);
-	}
-}
-
-export const entities = [
-	Announcement,
-	AnnouncementRead,
-	Meta,
-	Instance,
-	App,
-	AuthSession,
-	AccessToken,
-	User,
-	UserProfile,
-	UserKeypair,
-	UserPublickey,
-	UserList,
-	UserListJoining,
-	UserGroup,
-	UserGroupJoining,
-	UserGroupInvitation,
-	UserNotePining,
-	UserSecurityKey,
-	UsedUsername,
-	AttestationChallenge,
-	Following,
-	FollowRequest,
-	Muting,
-	Blocking,
-	Note,
-	NoteFavorite,
-	NoteReaction,
-	NoteWatching,
-	NoteThreadMuting,
-	NoteUnread,
-	Page,
-	PageLike,
-	GalleryPost,
-	GalleryLike,
-	DriveFile,
-	DriveFolder,
-	Poll,
-	PollVote,
-	Notification,
-	Emoji,
-	Hashtag,
-	SwSubscription,
-	AbuseUserReport,
-	RegistrationTicket,
-	MessagingMessage,
-	Signin,
-	ModerationLog,
-	Clip,
-	ClipNote,
-	Antenna,
-	AntennaNote,
-	PromoNote,
-	PromoRead,
-	ReversiGame,
-	ReversiMatching,
-	Relay,
-	MutedNote,
-	Channel,
-	ChannelFollowing,
-	ChannelNotePining,
-	RegistryItem,
-	Ad,
-	PasswordResetRequest,
-	UserPending,
-	...charts as any
-];
-
-export function initDb(justBorrow = false, sync = false, forceRecreate = false) {
-	if (!forceRecreate) {
-		try {
-			const conn = getConnection();
-			return Promise.resolve(conn);
-		} catch (e) {}
-	}
-
-	const log = process.env.NODE_ENV != 'production';
-
-	return createConnection({
-		type: 'postgres',
-		host: config.db.host,
-		port: config.db.port,
-		username: config.db.user,
-		password: config.db.pass,
-		database: config.db.db,
-		extra: config.db.extra,
-		synchronize: process.env.NODE_ENV === 'test' || sync,
-		dropSchema: process.env.NODE_ENV === 'test' && !justBorrow,
-		cache: !config.db.disableCache ? {
-			type: 'redis',
-			options: {
-				host: config.redis.host,
-				port: config.redis.port,
-				password: config.redis.pass,
-				prefix: `${config.redis.prefix}:query:`,
-				db: config.redis.db || 0
-			}
-		} : false,
-		logging: log,
-		logger: log ? new MyCustomLogger() : undefined,
-		entities: entities
-	});
-}
-
-export async function resetDb() {
-	const conn = await getConnection();
-	const tables = await conn.query(`SELECT relname AS "table"
-	FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
-	WHERE nspname NOT IN ('pg_catalog', 'information_schema')
-		AND C.relkind = 'r'
-		AND nspname !~ '^pg_toast';`);
-	await Promise.all(tables.map(t => t.table).map(x => conn.query(`DELETE FROM "${x}" CASCADE`)));
-}
diff --git a/src/db/redis.ts b/src/db/redis.ts
deleted file mode 100644
index 7e0ee1e3ce..0000000000
--- a/src/db/redis.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as redis from 'redis';
-import config from '@/config/index';
-
-export function createConnection() {
-	return redis.createClient(
-		config.redis.port,
-		config.redis.host,
-		{
-			password: config.redis.pass,
-			prefix: config.redis.prefix,
-			db: config.redis.db || 0
-		}
-	);
-}
-
-export const subsdcriber = createConnection();
-subsdcriber.subscribe(config.host);
-
-export const redisClient = createConnection();
diff --git a/src/emojilist.json b/src/emojilist.json
deleted file mode 100644
index 75c424ab4b..0000000000
--- a/src/emojilist.json
+++ /dev/null
@@ -1,1749 +0,0 @@
-[
-	{ "category": "face", "char": "😀", "name": "grinning", "keywords": ["face", "smile", "happy", "joy", ": D", "grin"] },
-	{ "category": "face", "char": "😬", "name": "grimacing", "keywords": ["face", "grimace", "teeth"] },
-	{ "category": "face", "char": "😁", "name": "grin", "keywords": ["face", "happy", "smile", "joy", "kawaii"] },
-	{ "category": "face", "char": "😂", "name": "joy", "keywords": ["face", "cry", "tears", "weep", "happy", "happytears", "haha"] },
-	{ "category": "face", "char": "🤣", "name": "rofl", "keywords": ["face", "rolling", "floor", "laughing", "lol", "haha"] },
-	{ "category": "face", "char": "🥳", "name": "partying", "keywords": ["face", "celebration", "woohoo"] },
-	{ "category": "face", "char": "😃", "name": "smiley", "keywords": ["face", "happy", "joy", "haha", ": D", ": )", "smile", "funny"] },
-	{ "category": "face", "char": "😄", "name": "smile", "keywords": ["face", "happy", "joy", "funny", "haha", "laugh", "like", ": D", ": )"] },
-	{ "category": "face", "char": "😅", "name": "sweat_smile", "keywords": ["face", "hot", "happy", "laugh", "sweat", "smile", "relief"] },
-	{ "category": "face", "char": "🥲", "name": "smiling_face_with_tear", "keywords": ["face"] },
-	{ "category": "face", "char": "😆", "name": "laughing", "keywords": ["happy", "joy", "lol", "satisfied", "haha", "face", "glad", "XD", "laugh"] },
-	{ "category": "face", "char": "😇", "name": "innocent", "keywords": ["face", "angel", "heaven", "halo"] },
-	{ "category": "face", "char": "😉", "name": "wink", "keywords": ["face", "happy", "mischievous", "secret", ";)", "smile", "eye"] },
-	{ "category": "face", "char": "😊", "name": "blush", "keywords": ["face", "smile", "happy", "flushed", "crush", "embarrassed", "shy", "joy"] },
-	{ "category": "face", "char": "🙂", "name": "slightly_smiling_face", "keywords": ["face", "smile"] },
-	{ "category": "face", "char": "🙃", "name": "upside_down_face", "keywords": ["face", "flipped", "silly", "smile"] },
-	{ "category": "face", "char": "☺️", "name": "relaxed", "keywords": ["face", "blush", "massage", "happiness"] },
-	{ "category": "face", "char": "😋", "name": "yum", "keywords": ["happy", "joy", "tongue", "smile", "face", "silly", "yummy", "nom", "delicious", "savouring"] },
-	{ "category": "face", "char": "😌", "name": "relieved", "keywords": ["face", "relaxed", "phew", "massage", "happiness"] },
-	{ "category": "face", "char": "😍", "name": "heart_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "heart"] },
-	{ "category": "face", "char": "🥰", "name": "smiling_face_with_three_hearts", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "hearts", "adore"] },
-	{ "category": "face", "char": "😘", "name": "kissing_heart", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] },
-	{ "category": "face", "char": "😗", "name": "kissing", "keywords": ["love", "like", "face", "3", "valentines", "infatuation", "kiss"] },
-	{ "category": "face", "char": "😙", "name": "kissing_smiling_eyes", "keywords": ["face", "affection", "valentines", "infatuation", "kiss"] },
-	{ "category": "face", "char": "😚", "name": "kissing_closed_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] },
-	{ "category": "face", "char": "😜", "name": "stuck_out_tongue_winking_eye", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "wink", "tongue"] },
-	{ "category": "face", "char": "🤪", "name": "zany", "keywords": ["face", "goofy", "crazy"] },
-	{ "category": "face", "char": "🤨", "name": "raised_eyebrow", "keywords": ["face", "distrust", "scepticism", "disapproval", "disbelief", "surprise"] },
-	{ "category": "face", "char": "🧐", "name": "monocle", "keywords": ["face", "stuffy", "wealthy"] },
-	{ "category": "face", "char": "😝", "name": "stuck_out_tongue_closed_eyes", "keywords": ["face", "prank", "playful", "mischievous", "smile", "tongue"] },
-	{ "category": "face", "char": "😛", "name": "stuck_out_tongue", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "tongue"] },
-	{ "category": "face", "char": "🤑", "name": "money_mouth_face", "keywords": ["face", "rich", "dollar", "money"] },
-	{ "category": "face", "char": "🤓", "name": "nerd_face", "keywords": ["face", "nerdy", "geek", "dork"] },
-	{ "category": "face", "char": "🥸", "name": "disguised_face", "keywords": ["face", "nose", "glasses", "incognito"] },
-	{ "category": "face", "char": "😎", "name": "sunglasses", "keywords": ["face", "cool", "smile", "summer", "beach", "sunglass"] },
-	{ "category": "face", "char": "🤩", "name": "star_struck", "keywords": ["face", "smile", "starry", "eyes", "grinning"] },
-	{ "category": "face", "char": "🤡", "name": "clown_face", "keywords": ["face"] },
-	{ "category": "face", "char": "🤠", "name": "cowboy_hat_face", "keywords": ["face", "cowgirl", "hat"] },
-	{ "category": "face", "char": "🤗", "name": "hugs", "keywords": ["face", "smile", "hug"] },
-	{ "category": "face", "char": "😏", "name": "smirk", "keywords": ["face", "smile", "mean", "prank", "smug", "sarcasm"] },
-	{ "category": "face", "char": "😶", "name": "no_mouth", "keywords": ["face", "hellokitty"] },
-	{ "category": "face", "char": "😐", "name": "neutral_face", "keywords": ["indifference", "meh", ": |", "neutral"] },
-	{ "category": "face", "char": "😑", "name": "expressionless", "keywords": ["face", "indifferent", "-_-", "meh", "deadpan"] },
-	{ "category": "face", "char": "😒", "name": "unamused", "keywords": ["indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"] },
-	{ "category": "face", "char": "🙄", "name": "roll_eyes", "keywords": ["face", "eyeroll", "frustrated"] },
-	{ "category": "face", "char": "🤔", "name": "thinking", "keywords": ["face", "hmmm", "think", "consider"] },
-	{ "category": "face", "char": "🤥", "name": "lying_face", "keywords": ["face", "lie", "pinocchio"] },
-	{ "category": "face", "char": "🤭", "name": "hand_over_mouth", "keywords": ["face", "whoops", "shock", "surprise"] },
-	{ "category": "face", "char": "🤫", "name": "shushing", "keywords": ["face", "quiet", "shhh"] },
-	{ "category": "face", "char": "🤬", "name": "symbols_over_mouth", "keywords": ["face", "swearing", "cursing", "cussing", "profanity", "expletive"] },
-	{ "category": "face", "char": "🤯", "name": "exploding_head", "keywords": ["face", "shocked", "mind", "blown"] },
-	{ "category": "face", "char": "😳", "name": "flushed", "keywords": ["face", "blush", "shy", "flattered"] },
-	{ "category": "face", "char": "😞", "name": "disappointed", "keywords": ["face", "sad", "upset", "depressed", ": ("] },
-	{ "category": "face", "char": "😟", "name": "worried", "keywords": ["face", "concern", "nervous", ": ("] },
-	{ "category": "face", "char": "😠", "name": "angry", "keywords": ["mad", "face", "annoyed", "frustrated"] },
-	{ "category": "face", "char": "😡", "name": "rage", "keywords": ["angry", "mad", "hate", "despise"] },
-	{ "category": "face", "char": "😔", "name": "pensive", "keywords": ["face", "sad", "depressed", "upset"] },
-	{ "category": "face", "char": "😕", "name": "confused", "keywords": ["face", "indifference", "huh", "weird", "hmmm", ": /"] },
-	{ "category": "face", "char": "🙁", "name": "slightly_frowning_face", "keywords": ["face", "frowning", "disappointed", "sad", "upset"] },
-	{ "category": "face", "char": "☹", "name": "frowning_face", "keywords": ["face", "sad", "upset", "frown"] },
-	{ "category": "face", "char": "😣", "name": "persevere", "keywords": ["face", "sick", "no", "upset", "oops"] },
-	{ "category": "face", "char": "😖", "name": "confounded", "keywords": ["face", "confused", "sick", "unwell", "oops", ": S"] },
-	{ "category": "face", "char": "😫", "name": "tired_face", "keywords": ["sick", "whine", "upset", "frustrated"] },
-	{ "category": "face", "char": "😩", "name": "weary", "keywords": ["face", "tired", "sleepy", "sad", "frustrated", "upset"] },
-	{ "category": "face", "char": "🥺", "name": "pleading", "keywords": ["face", "begging", "mercy"] },
-	{ "category": "face", "char": "😤", "name": "triumph", "keywords": ["face", "gas", "phew", "proud", "pride"] },
-	{ "category": "face", "char": "😮", "name": "open_mouth", "keywords": ["face", "surprise", "impressed", "wow", "whoa", ": O"] },
-	{ "category": "face", "char": "😱", "name": "scream", "keywords": ["face", "munch", "scared", "omg"] },
-	{ "category": "face", "char": "😨", "name": "fearful", "keywords": ["face", "scared", "terrified", "nervous", "oops", "huh"] },
-	{ "category": "face", "char": "😰", "name": "cold_sweat", "keywords": ["face", "nervous", "sweat"] },
-	{ "category": "face", "char": "😯", "name": "hushed", "keywords": ["face", "woo", "shh"] },
-	{ "category": "face", "char": "😦", "name": "frowning", "keywords": ["face", "aw", "what"] },
-	{ "category": "face", "char": "😧", "name": "anguished", "keywords": ["face", "stunned", "nervous"] },
-	{ "category": "face", "char": "😢", "name": "cry", "keywords": ["face", "tears", "sad", "depressed", "upset", ": '("] },
-	{ "category": "face", "char": "😥", "name": "disappointed_relieved", "keywords": ["face", "phew", "sweat", "nervous"] },
-	{ "category": "face", "char": "🤤", "name": "drooling_face", "keywords": ["face"] },
-	{ "category": "face", "char": "😪", "name": "sleepy", "keywords": ["face", "tired", "rest", "nap"] },
-	{ "category": "face", "char": "😓", "name": "sweat", "keywords": ["face", "hot", "sad", "tired", "exercise"] },
-	{ "category": "face", "char": "🥵", "name": "hot", "keywords": ["face", "feverish", "heat", "red", "sweating"] },
-	{ "category": "face", "char": "🥶", "name": "cold", "keywords": ["face", "blue", "freezing", "frozen", "frostbite", "icicles"] },
-	{ "category": "face", "char": "😭", "name": "sob", "keywords": ["face", "cry", "tears", "sad", "upset", "depressed"] },
-	{ "category": "face", "char": "😵", "name": "dizzy_face", "keywords": ["spent", "unconscious", "xox", "dizzy"] },
-	{ "category": "face", "char": "😲", "name": "astonished", "keywords": ["face", "xox", "surprised", "poisoned"] },
-	{ "category": "face", "char": "🤐", "name": "zipper_mouth_face", "keywords": ["face", "sealed", "zipper", "secret"] },
-	{ "category": "face", "char": "🤢", "name": "nauseated_face", "keywords": ["face", "vomit", "gross", "green", "sick", "throw up", "ill"] },
-	{ "category": "face", "char": "🤧", "name": "sneezing_face", "keywords": ["face", "gesundheit", "sneeze", "sick", "allergy"] },
-	{ "category": "face", "char": "🤮", "name": "vomiting", "keywords": ["face", "sick"] },
-	{ "category": "face", "char": "😷", "name": "mask", "keywords": ["face", "sick", "ill", "disease"] },
-	{ "category": "face", "char": "🤒", "name": "face_with_thermometer", "keywords": ["sick", "temperature", "thermometer", "cold", "fever"] },
-	{ "category": "face", "char": "🤕", "name": "face_with_head_bandage", "keywords": ["injured", "clumsy", "bandage", "hurt"] },
-	{ "category": "face", "char": "🥴", "name": "woozy", "keywords": ["face", "dizzy", "intoxicated", "tipsy", "wavy"] },
-	{ "category": "face", "char": "🥱", "name": "yawning", "keywords": ["face", "tired", "yawning"] },
-	{ "category": "face", "char": "😴", "name": "sleeping", "keywords": ["face", "tired", "sleepy", "night", "zzz"] },
-	{ "category": "face", "char": "💤", "name": "zzz", "keywords": ["sleepy", "tired", "dream"] },
-	{ "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] },
-	{ "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] },
-	{ "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] },
-	{ "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] },
-	{ "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] },
-	{ "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] },
-	{ "category": "face", "char": "👹", "name": "japanese_ogre", "keywords": ["monster", "red", "mask", "halloween", "scary", "creepy", "devil", "demon", "japanese", "ogre"] },
-	{ "category": "face", "char": "👺", "name": "japanese_goblin", "keywords": ["red", "evil", "mask", "monster", "scary", "creepy", "japanese", "goblin"] },
-	{ "category": "face", "char": "💀", "name": "skull", "keywords": ["dead", "skeleton", "creepy", "death"] },
-	{ "category": "face", "char": "👻", "name": "ghost", "keywords": ["halloween", "spooky", "scary"] },
-	{ "category": "face", "char": "👽", "name": "alien", "keywords": ["UFO", "paul", "weird", "outer_space"] },
-	{ "category": "face", "char": "🤖", "name": "robot", "keywords": ["computer", "machine", "bot"] },
-	{ "category": "face", "char": "😺", "name": "smiley_cat", "keywords": ["animal", "cats", "happy", "smile"] },
-	{ "category": "face", "char": "😸", "name": "smile_cat", "keywords": ["animal", "cats", "smile"] },
-	{ "category": "face", "char": "😹", "name": "joy_cat", "keywords": ["animal", "cats", "haha", "happy", "tears"] },
-	{ "category": "face", "char": "😻", "name": "heart_eyes_cat", "keywords": ["animal", "love", "like", "affection", "cats", "valentines", "heart"] },
-	{ "category": "face", "char": "😼", "name": "smirk_cat", "keywords": ["animal", "cats", "smirk"] },
-	{ "category": "face", "char": "😽", "name": "kissing_cat", "keywords": ["animal", "cats", "kiss"] },
-	{ "category": "face", "char": "🙀", "name": "scream_cat", "keywords": ["animal", "cats", "munch", "scared", "scream"] },
-	{ "category": "face", "char": "😿", "name": "crying_cat_face", "keywords": ["animal", "tears", "weep", "sad", "cats", "upset", "cry"] },
-	{ "category": "face", "char": "😾", "name": "pouting_cat", "keywords": ["animal", "cats"] },
-	{ "category": "people", "char": "🤲", "name": "palms_up", "keywords": ["hands", "gesture", "cupped", "prayer"] },
-	{ "category": "people", "char": "🙌", "name": "raised_hands", "keywords": ["gesture", "hooray", "yea", "celebration", "hands"] },
-	{ "category": "people", "char": "👏", "name": "clap", "keywords": ["hands", "praise", "applause", "congrats", "yay"] },
-	{ "category": "people", "char": "👋", "name": "wave", "keywords": ["hands", "gesture", "goodbye", "solong", "farewell", "hello", "hi", "palm"] },
-	{ "category": "people", "char": "🤙", "name": "call_me_hand", "keywords": ["hands", "gesture"] },
-	{ "category": "people", "char": "👍", "name": "+1", "keywords": ["thumbsup", "yes", "awesome", "good", "agree", "accept", "cool", "hand", "like"] },
-	{ "category": "people", "char": "👎", "name": "-1", "keywords": ["thumbsdown", "no", "dislike", "hand"] },
-	{ "category": "people", "char": "👊", "name": "facepunch", "keywords": ["angry", "violence", "fist", "hit", "attack", "hand"] },
-	{ "category": "people", "char": "✊", "name": "fist", "keywords": ["fingers", "hand", "grasp"] },
-	{ "category": "people", "char": "🤛", "name": "fist_left", "keywords": ["hand", "fistbump"] },
-	{ "category": "people", "char": "🤜", "name": "fist_right", "keywords": ["hand", "fistbump"] },
-	{ "category": "people", "char": "✌", "name": "v", "keywords": ["fingers", "ohyeah", "hand", "peace", "victory", "two"] },
-	{ "category": "people", "char": "👌", "name": "ok_hand", "keywords": ["fingers", "limbs", "perfect", "ok", "okay"] },
-	{ "category": "people", "char": "✋", "name": "raised_hand", "keywords": ["fingers", "stop", "highfive", "palm", "ban"] },
-	{ "category": "people", "char": "🤚", "name": "raised_back_of_hand", "keywords": ["fingers", "raised", "backhand"] },
-	{ "category": "people", "char": "👐", "name": "open_hands", "keywords": ["fingers", "butterfly", "hands", "open"] },
-	{ "category": "people", "char": "💪", "name": "muscle", "keywords": ["arm", "flex", "hand", "summer", "strong", "biceps"] },
-	{ "category": "people", "char": "🦾", "name": "mechanical_arm", "keywords": ["flex", "hand", "strong", "biceps"] },
-	{ "category": "people", "char": "🙏", "name": "pray", "keywords": ["please", "hope", "wish", "namaste", "highfive"] },
-	{ "category": "people", "char": "🦶", "name": "foot", "keywords": ["kick", "stomp"] },
-	{ "category": "people", "char": "🦵", "name": "leg", "keywords": ["kick", "limb"] },
-	{ "category": "people", "char": "🦿", "name": "mechanical_leg", "keywords": ["kick", "limb"] },
-	{ "category": "people", "char": "🤝", "name": "handshake", "keywords": ["agreement", "shake"] },
-	{ "category": "people", "char": "☝", "name": "point_up", "keywords": ["hand", "fingers", "direction", "up"] },
-	{ "category": "people", "char": "👆", "name": "point_up_2", "keywords": ["fingers", "hand", "direction", "up"] },
-	{ "category": "people", "char": "👇", "name": "point_down", "keywords": ["fingers", "hand", "direction", "down"] },
-	{ "category": "people", "char": "👈", "name": "point_left", "keywords": ["direction", "fingers", "hand", "left"] },
-	{ "category": "people", "char": "👉", "name": "point_right", "keywords": ["fingers", "hand", "direction", "right"] },
-	{ "category": "people", "char": "🖕", "name": "fu", "keywords": ["hand", "fingers", "rude", "middle", "flipping"] },
-	{ "category": "people", "char": "🖐", "name": "raised_hand_with_fingers_splayed", "keywords": ["hand", "fingers", "palm"] },
-	{ "category": "people", "char": "🤟", "name": "love_you", "keywords": ["hand", "fingers", "gesture"] },
-	{ "category": "people", "char": "🤘", "name": "metal", "keywords": ["hand", "fingers", "evil_eye", "sign_of_horns", "rock_on"] },
-	{ "category": "people", "char": "🤞", "name": "crossed_fingers", "keywords": ["good", "lucky"] },
-	{ "category": "people", "char": "🖖", "name": "vulcan_salute", "keywords": ["hand", "fingers", "spock", "star trek"] },
-	{ "category": "people", "char": "✍", "name": "writing_hand", "keywords": ["lower_left_ballpoint_pen", "stationery", "write", "compose"] },
-	{ "category": "people", "char": "🤏", "name": "pinching_hand", "keywords": ["hand", "fingers"] },
-	{ "category": "people", "char": "🤌", "name": "pinched_fingers", "keywords": ["hand", "fingers"] },
-	{ "category": "people", "char": "🤳", "name": "selfie", "keywords": ["camera", "phone"] },
-	{ "category": "people", "char": "💅", "name": "nail_care", "keywords": ["beauty", "manicure", "finger", "fashion", "nail"] },
-	{ "category": "people", "char": "👄", "name": "lips", "keywords": ["mouth", "kiss"] },
-	{ "category": "people", "char": "🦷", "name": "tooth", "keywords": ["teeth", "dentist"] },
-	{ "category": "people", "char": "👅", "name": "tongue", "keywords": ["mouth", "playful"] },
-	{ "category": "people", "char": "👂", "name": "ear", "keywords": ["face", "hear", "sound", "listen"] },
-	{ "category": "people", "char": "🦻", "name": "ear_with_hearing_aid", "keywords": ["face", "hear", "sound", "listen"] },
-	{ "category": "people", "char": "👃", "name": "nose", "keywords": ["smell", "sniff"] },
-	{ "category": "people", "char": "👁", "name": "eye", "keywords": ["face", "look", "see", "watch", "stare"] },
-	{ "category": "people", "char": "👀", "name": "eyes", "keywords": ["look", "watch", "stalk", "peek", "see"] },
-	{ "category": "people", "char": "🧠", "name": "brain", "keywords": ["smart", "intelligent"] },
-	{ "category": "people", "char": "🫀", "name": "anatomical_heart", "keywords": [] },
-	{ "category": "people", "char": "🫁", "name": "lungs", "keywords": [] },
-	{ "category": "people", "char": "👤", "name": "bust_in_silhouette", "keywords": ["user", "person", "human"] },
-	{ "category": "people", "char": "👥", "name": "busts_in_silhouette", "keywords": ["user", "person", "human", "group", "team"] },
-	{ "category": "people", "char": "🗣", "name": "speaking_head", "keywords": ["user", "person", "human", "sing", "say", "talk"] },
-	{ "category": "people", "char": "👶", "name": "baby", "keywords": ["child", "boy", "girl", "toddler"] },
-	{ "category": "people", "char": "🧒", "name": "child", "keywords": ["gender-neutral", "young"] },
-	{ "category": "people", "char": "👦", "name": "boy", "keywords": ["man", "male", "guy", "teenager"] },
-	{ "category": "people", "char": "👧", "name": "girl", "keywords": ["female", "woman", "teenager"] },
-	{ "category": "people", "char": "🧑", "name": "adult", "keywords": ["gender-neutral", "person"] },
-	{ "category": "people", "char": "👨", "name": "man", "keywords": ["mustache", "father", "dad", "guy", "classy", "sir", "moustache"] },
-	{ "category": "people", "char": "👩", "name": "woman", "keywords": ["female", "girls", "lady"] },
-	{ "category": "people", "char": "🧑‍🦱", "name": "curly_hair", "keywords": ["curly", "afro", "braids", "ringlets"] },
-	{ "category": "people", "char": "👩‍🦱", "name": "curly_hair_woman", "keywords": ["woman", "female", "girl", "curly", "afro", "braids", "ringlets"] },
-	{ "category": "people", "char": "👨‍🦱", "name": "curly_hair_man", "keywords": ["man", "male", "boy", "guy", "curly", "afro", "braids", "ringlets"] },
-	{ "category": "people", "char": "🧑‍🦰", "name": "red_hair", "keywords": ["redhead"] },
-	{ "category": "people", "char": "👩‍🦰", "name": "red_hair_woman", "keywords": ["woman", "female", "girl", "ginger", "redhead"] },
-	{ "category": "people", "char": "👨‍🦰", "name": "red_hair_man", "keywords": ["man", "male", "boy", "guy", "ginger", "redhead"] },
-	{ "category": "people", "char": "👱‍♀️", "name": "blonde_woman", "keywords": ["woman", "female", "girl", "blonde", "person"] },
-	{ "category": "people", "char": "👱", "name": "blonde_man", "keywords": ["man", "male", "boy", "blonde", "guy", "person"] },
-	{ "category": "people", "char": "🧑‍🦳", "name": "white_hair", "keywords": ["gray", "old", "white"] },
-	{ "category": "people", "char": "👩‍🦳", "name": "white_hair_woman", "keywords": ["woman", "female", "girl", "gray", "old", "white"] },
-	{ "category": "people", "char": "👨‍🦳", "name": "white_hair_man", "keywords": ["man", "male", "boy", "guy", "gray", "old", "white"] },
-	{ "category": "people", "char": "🧑‍🦲", "name": "bald", "keywords": ["bald", "chemotherapy", "hairless", "shaven"] },
-	{ "category": "people", "char": "👩‍🦲", "name": "bald_woman", "keywords": ["woman", "female", "girl", "bald", "chemotherapy", "hairless", "shaven"] },
-	{ "category": "people", "char": "👨‍🦲", "name": "bald_man", "keywords": ["man", "male", "boy", "guy", "bald", "chemotherapy", "hairless", "shaven"] },
-	{ "category": "people", "char": "🧔", "name": "bearded_person", "keywords": ["person", "bewhiskered"] },
-	{ "category": "people", "char": "🧓", "name": "older_adult", "keywords": ["human", "elder", "senior", "gender-neutral"] },
-	{ "category": "people", "char": "👴", "name": "older_man", "keywords": ["human", "male", "men", "old", "elder", "senior"] },
-	{ "category": "people", "char": "👵", "name": "older_woman", "keywords": ["human", "female", "women", "lady", "old", "elder", "senior"] },
-	{ "category": "people", "char": "👲", "name": "man_with_gua_pi_mao", "keywords": ["male", "boy", "chinese"] },
-	{ "category": "people", "char": "🧕", "name": "woman_with_headscarf", "keywords": ["female", "hijab", "mantilla", "tichel"] },
-	{ "category": "people", "char": "👳‍♀️", "name": "woman_with_turban", "keywords": ["female", "indian", "hinduism", "arabs", "woman"] },
-	{ "category": "people", "char": "👳", "name": "man_with_turban", "keywords": ["male", "indian", "hinduism", "arabs"] },
-	{ "category": "people", "char": "👮‍♀️", "name": "policewoman", "keywords": ["woman", "police", "law", "legal", "enforcement", "arrest", "911", "female"] },
-	{ "category": "people", "char": "👮", "name": "policeman", "keywords": ["man", "police", "law", "legal", "enforcement", "arrest", "911"] },
-	{ "category": "people", "char": "👷‍♀️", "name": "construction_worker_woman", "keywords": ["female", "human", "wip", "build", "construction", "worker", "labor", "woman"] },
-	{ "category": "people", "char": "👷", "name": "construction_worker_man", "keywords": ["male", "human", "wip", "guy", "build", "construction", "worker", "labor"] },
-	{ "category": "people", "char": "💂‍♀️", "name": "guardswoman", "keywords": ["uk", "gb", "british", "female", "royal", "woman"] },
-	{ "category": "people", "char": "💂", "name": "guardsman", "keywords": ["uk", "gb", "british", "male", "guy", "royal"] },
-	{ "category": "people", "char": "🕵️‍♀️", "name": "female_detective", "keywords": ["human", "spy", "detective", "female", "woman"] },
-	{ "category": "people", "char": "🕵", "name": "male_detective", "keywords": ["human", "spy", "detective"] },
-	{ "category": "people", "char": "🧑‍⚕️", "name": "health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "human"] },
-	{ "category": "people", "char": "👩‍⚕️", "name": "woman_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "woman", "human"] },
-	{ "category": "people", "char": "👨‍⚕️", "name": "man_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🌾", "name": "farmer", "keywords": ["rancher", "gardener", "human"] },
-	{ "category": "people", "char": "👩‍🌾", "name": "woman_farmer", "keywords": ["rancher", "gardener", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🌾", "name": "man_farmer", "keywords": ["rancher", "gardener", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🍳", "name": "cook", "keywords": ["chef", "human"] },
-	{ "category": "people", "char": "👩‍🍳", "name": "woman_cook", "keywords": ["chef", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🍳", "name": "man_cook", "keywords": ["chef", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🎓", "name": "student", "keywords": ["graduate", "human"] },
-	{ "category": "people", "char": "👩‍🎓", "name": "woman_student", "keywords": ["graduate", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🎓", "name": "man_student", "keywords": ["graduate", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🎤", "name": "singer", "keywords": ["rockstar", "entertainer", "human"] },
-	{ "category": "people", "char": "👩‍🎤", "name": "woman_singer", "keywords": ["rockstar", "entertainer", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🎤", "name": "man_singer", "keywords": ["rockstar", "entertainer", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🏫", "name": "teacher", "keywords": ["instructor", "professor", "human"] },
-	{ "category": "people", "char": "👩‍🏫", "name": "woman_teacher", "keywords": ["instructor", "professor", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🏫", "name": "man_teacher", "keywords": ["instructor", "professor", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🏭", "name": "factory_worker", "keywords": ["assembly", "industrial", "human"] },
-	{ "category": "people", "char": "👩‍🏭", "name": "woman_factory_worker", "keywords": ["assembly", "industrial", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🏭", "name": "man_factory_worker", "keywords": ["assembly", "industrial", "man", "human"] },
-	{ "category": "people", "char": "🧑‍💻", "name": "technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "human", "laptop", "computer"] },
-	{ "category": "people", "char": "👩‍💻", "name": "woman_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "woman", "human", "laptop", "computer"] },
-	{ "category": "people", "char": "👨‍💻", "name": "man_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "man", "human", "laptop", "computer"] },
-	{ "category": "people", "char": "🧑‍💼", "name": "office_worker", "keywords": ["business", "manager", "human"] },
-	{ "category": "people", "char": "👩‍💼", "name": "woman_office_worker", "keywords": ["business", "manager", "woman", "human"] },
-	{ "category": "people", "char": "👨‍💼", "name": "man_office_worker", "keywords": ["business", "manager", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🔧", "name": "mechanic", "keywords": ["plumber", "human", "wrench"] },
-	{ "category": "people", "char": "👩‍🔧", "name": "woman_mechanic", "keywords": ["plumber", "woman", "human", "wrench"] },
-	{ "category": "people", "char": "👨‍🔧", "name": "man_mechanic", "keywords": ["plumber", "man", "human", "wrench"] },
-	{ "category": "people", "char": "🧑‍🔬", "name": "scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "human"] },
-	{ "category": "people", "char": "👩‍🔬", "name": "woman_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🔬", "name": "man_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🎨", "name": "artist", "keywords": ["painter", "human"] },
-	{ "category": "people", "char": "👩‍🎨", "name": "woman_artist", "keywords": ["painter", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🎨", "name": "man_artist", "keywords": ["painter", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🚒", "name": "firefighter", "keywords": ["fireman", "human"] },
-	{ "category": "people", "char": "👩‍🚒", "name": "woman_firefighter", "keywords": ["fireman", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🚒", "name": "man_firefighter", "keywords": ["fireman", "man", "human"] },
-	{ "category": "people", "char": "🧑‍✈️", "name": "pilot", "keywords": ["aviator", "plane", "human"] },
-	{ "category": "people", "char": "👩‍✈️", "name": "woman_pilot", "keywords": ["aviator", "plane", "woman", "human"] },
-	{ "category": "people", "char": "👨‍✈️", "name": "man_pilot", "keywords": ["aviator", "plane", "man", "human"] },
-	{ "category": "people", "char": "🧑‍🚀", "name": "astronaut", "keywords": ["space", "rocket", "human"] },
-	{ "category": "people", "char": "👩‍🚀", "name": "woman_astronaut", "keywords": ["space", "rocket", "woman", "human"] },
-	{ "category": "people", "char": "👨‍🚀", "name": "man_astronaut", "keywords": ["space", "rocket", "man", "human"] },
-	{ "category": "people", "char": "🧑‍⚖️", "name": "judge", "keywords": ["justice", "court", "human"] },
-	{ "category": "people", "char": "👩‍⚖️", "name": "woman_judge", "keywords": ["justice", "court", "woman", "human"] },
-	{ "category": "people", "char": "👨‍⚖️", "name": "man_judge", "keywords": ["justice", "court", "man", "human"] },
-	{ "category": "people", "char": "🦸‍♀️", "name": "woman_superhero", "keywords": ["woman", "female", "good", "heroine", "superpowers"] },
-	{ "category": "people", "char": "🦸‍♂️", "name": "man_superhero", "keywords": ["man", "male", "good", "hero", "superpowers"] },
-	{ "category": "people", "char": "🦹‍♀️", "name": "woman_supervillain", "keywords": ["woman", "female", "evil", "bad", "criminal", "heroine", "superpowers"] },
-	{ "category": "people", "char": "🦹‍♂️", "name": "man_supervillain", "keywords": ["man", "male", "evil", "bad", "criminal", "hero", "superpowers"] },
-	{ "category": "people", "char": "🤶", "name": "mrs_claus", "keywords": ["woman", "female", "xmas", "mother christmas"] },
-	{ "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF84", "name": "mx_claus", "keywords": ["xmas", "christmas"] },
-	{ "category": "people", "char": "🎅", "name": "santa", "keywords": ["festival", "man", "male", "xmas", "father christmas"] },
-	{ "category": "people", "char": "🥷", "name": "ninja", "keywords": [] },
-	{ "category": "people", "char": "🧙‍♀️", "name": "sorceress", "keywords": ["woman", "female", "mage", "witch"] },
-	{ "category": "people", "char": "🧙‍♂️", "name": "wizard", "keywords": ["man", "male", "mage", "sorcerer"] },
-	{ "category": "people", "char": "🧝‍♀️", "name": "woman_elf", "keywords": ["woman", "female"] },
-	{ "category": "people", "char": "🧝‍♂️", "name": "man_elf", "keywords": ["man", "male"] },
-	{ "category": "people", "char": "🧛‍♀️", "name": "woman_vampire", "keywords": ["woman", "female"] },
-	{ "category": "people", "char": "🧛‍♂️", "name": "man_vampire", "keywords": ["man", "male", "dracula"] },
-	{ "category": "people", "char": "🧟‍♀️", "name": "woman_zombie", "keywords": ["woman", "female", "undead", "walking dead"] },
-	{ "category": "people", "char": "🧟‍♂️", "name": "man_zombie", "keywords": ["man", "male", "dracula", "undead", "walking dead"] },
-	{ "category": "people", "char": "🧞‍♀️", "name": "woman_genie", "keywords": ["woman", "female"] },
-	{ "category": "people", "char": "🧞‍♂️", "name": "man_genie", "keywords": ["man", "male"] },
-	{ "category": "people", "char": "🧜‍♀️", "name": "mermaid", "keywords": ["woman", "female", "merwoman", "ariel"] },
-	{ "category": "people", "char": "🧜‍♂️", "name": "merman", "keywords": ["man", "male", "triton"] },
-	{ "category": "people", "char": "🧚‍♀️", "name": "woman_fairy", "keywords": ["woman", "female"] },
-	{ "category": "people", "char": "🧚‍♂️", "name": "man_fairy", "keywords": ["man", "male"] },
-	{ "category": "people", "char": "👼", "name": "angel", "keywords": ["heaven", "wings", "halo"] },
-	{ "category": "people", "char": "🤰", "name": "pregnant_woman", "keywords": ["baby"] },
-	{ "category": "people", "char": "🤱", "name": "breastfeeding", "keywords": ["nursing", "baby"] },
-	{ "category": "people", "char": "\uD83D\uDC69\u200D\uD83C\uDF7C", "name": "woman_feeding_baby", "keywords": [] },
-	{ "category": "people", "char": "\uD83D\uDC68\u200D\uD83C\uDF7C", "name": "man_feeding_baby", "keywords": [] },
-	{ "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF7C", "name": "person_feeding_baby", "keywords": [] },
-	{ "category": "people", "char": "👸", "name": "princess", "keywords": ["girl", "woman", "female", "blond", "crown", "royal", "queen"] },
-	{ "category": "people", "char": "🤴", "name": "prince", "keywords": ["boy", "man", "male", "crown", "royal", "king"] },
-	{ "category": "people", "char": "👰", "name": "person_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] },
-	{ "category": "people", "char": "👰", "name": "bride_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] },
-	{ "category": "people", "char": "🤵", "name": "person_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] },
-	{ "category": "people", "char": "🤵", "name": "man_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] },
-	{ "category": "people", "char": "🏃‍♀️", "name": "running_woman", "keywords": ["woman", "walking", "exercise", "race", "running", "female"] },
-	{ "category": "people", "char": "🏃", "name": "running_man", "keywords": ["man", "walking", "exercise", "race", "running"] },
-	{ "category": "people", "char": "🚶‍♀️", "name": "walking_woman", "keywords": ["human", "feet", "steps", "woman", "female"] },
-	{ "category": "people", "char": "🚶", "name": "walking_man", "keywords": ["human", "feet", "steps"] },
-	{ "category": "people", "char": "💃", "name": "dancer", "keywords": ["female", "girl", "woman", "fun"] },
-	{ "category": "people", "char": "🕺", "name": "man_dancing", "keywords": ["male", "boy", "fun", "dancer"] },
-	{ "category": "people", "char": "👯", "name": "dancing_women", "keywords": ["female", "bunny", "women", "girls"] },
-	{ "category": "people", "char": "👯‍♂️", "name": "dancing_men", "keywords": ["male", "bunny", "men", "boys"] },
-	{ "category": "people", "char": "👫", "name": "couple", "keywords": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"] },
-	{ "category": "people", "char": "\uD83E\uDDD1\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1", "name": "people_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"] },
-	{ "category": "people", "char": "👬", "name": "two_men_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "man", "human"] },
-	{ "category": "people", "char": "👭", "name": "two_women_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "female", "human"] },
-	{ "category": "people", "char": "🫂", "name": "people_hugging", "keywords": [] },
-	{ "category": "people", "char": "🙇‍♀️", "name": "bowing_woman", "keywords": ["woman", "female", "girl"] },
-	{ "category": "people", "char": "🙇", "name": "bowing_man", "keywords": ["man", "male", "boy"] },
-	{ "category": "people", "char": "🤦‍♂️", "name": "man_facepalming", "keywords": ["man", "male", "boy", "disbelief"] },
-	{ "category": "people", "char": "🤦‍♀️", "name": "woman_facepalming", "keywords": ["woman", "female", "girl", "disbelief"] },
-	{ "category": "people", "char": "🤷", "name": "woman_shrugging", "keywords": ["woman", "female", "girl", "confused", "indifferent", "doubt"] },
-	{ "category": "people", "char": "🤷‍♂️", "name": "man_shrugging", "keywords": ["man", "male", "boy", "confused", "indifferent", "doubt"] },
-	{ "category": "people", "char": "💁", "name": "tipping_hand_woman", "keywords": ["female", "girl", "woman", "human", "information"] },
-	{ "category": "people", "char": "💁‍♂️", "name": "tipping_hand_man", "keywords": ["male", "boy", "man", "human", "information"] },
-	{ "category": "people", "char": "🙅", "name": "no_good_woman", "keywords": ["female", "girl", "woman", "nope"] },
-	{ "category": "people", "char": "🙅‍♂️", "name": "no_good_man", "keywords": ["male", "boy", "man", "nope"] },
-	{ "category": "people", "char": "🙆", "name": "ok_woman", "keywords": ["women", "girl", "female", "pink", "human", "woman"] },
-	{ "category": "people", "char": "🙆‍♂️", "name": "ok_man", "keywords": ["men", "boy", "male", "blue", "human", "man"] },
-	{ "category": "people", "char": "🙋", "name": "raising_hand_woman", "keywords": ["female", "girl", "woman"] },
-	{ "category": "people", "char": "🙋‍♂️", "name": "raising_hand_man", "keywords": ["male", "boy", "man"] },
-	{ "category": "people", "char": "🙎", "name": "pouting_woman", "keywords": ["female", "girl", "woman"] },
-	{ "category": "people", "char": "🙎‍♂️", "name": "pouting_man", "keywords": ["male", "boy", "man"] },
-	{ "category": "people", "char": "🙍", "name": "frowning_woman", "keywords": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"] },
-	{ "category": "people", "char": "🙍‍♂️", "name": "frowning_man", "keywords": ["male", "boy", "man", "sad", "depressed", "discouraged", "unhappy"] },
-	{ "category": "people", "char": "💇", "name": "haircut_woman", "keywords": ["female", "girl", "woman"] },
-	{ "category": "people", "char": "💇‍♂️", "name": "haircut_man", "keywords": ["male", "boy", "man"] },
-	{ "category": "people", "char": "💆", "name": "massage_woman", "keywords": ["female", "girl", "woman", "head"] },
-	{ "category": "people", "char": "💆‍♂️", "name": "massage_man", "keywords": ["male", "boy", "man", "head"] },
-	{ "category": "people", "char": "🧖‍♀️", "name": "woman_in_steamy_room", "keywords": ["female", "woman", "spa", "steamroom", "sauna"] },
-	{ "category": "people", "char": "🧖‍♂️", "name": "man_in_steamy_room", "keywords": ["male", "man", "spa", "steamroom", "sauna"] },
-	{ "category": "people", "char": "🧏‍♀️", "name": "woman_deaf", "keywords": ["woman", "female"] },
-	{ "category": "people", "char": "🧏‍♂️", "name": "man_deaf", "keywords": ["man", "male"] },
-	{ "category": "people", "char": "🧍‍♀️", "name": "woman_standing", "keywords": ["woman", "female"] },
-	{ "category": "people", "char": "🧍‍♂️", "name": "man_standing", "keywords": ["man", "male"] },
-	{ "category": "people", "char": "🧎‍♀️", "name": "woman_kneeling", "keywords": ["woman", "female"] },
-	{ "category": "people", "char": "🧎‍♂️", "name": "man_kneeling", "keywords": ["man", "male"] },
-	{ "category": "people", "char": "🧑‍🦯", "name": "person_with_probing_cane", "keywords": ["accessibility", "blind"] },
-	{ "category": "people", "char": "👩‍🦯", "name": "woman_with_probing_cane", "keywords": ["woman", "female", "accessibility", "blind"] },
-	{ "category": "people", "char": "👨‍🦯", "name": "man_with_probing_cane", "keywords": ["man", "male", "accessibility", "blind"] },
-	{ "category": "people", "char": "🧑‍🦼", "name": "person_in_motorized_wheelchair", "keywords": ["accessibility"] },
-	{ "category": "people", "char": "👩‍🦼", "name": "woman_in_motorized_wheelchair", "keywords": ["woman", "female", "accessibility"] },
-	{ "category": "people", "char": "👨‍🦼", "name": "man_in_motorized_wheelchair", "keywords": ["man", "male", "accessibility"] },
-	{ "category": "people", "char": "🧑‍🦽", "name": "person_in_manual_wheelchair", "keywords": ["accessibility"] },
-	{ "category": "people", "char": "👩‍🦽", "name": "woman_in_manual_wheelchair", "keywords": ["woman", "female", "accessibility"] },
-	{ "category": "people", "char": "👨‍🦽", "name": "man_in_manual_wheelchair", "keywords": ["man", "male", "accessibility"] },
-	{ "category": "people", "char": "💑", "name": "couple_with_heart_woman_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] },
-	{ "category": "people", "char": "👩‍❤️‍👩", "name": "couple_with_heart_woman_woman", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] },
-	{ "category": "people", "char": "👨‍❤️‍👨", "name": "couple_with_heart_man_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] },
-	{ "category": "people", "char": "💏", "name": "couplekiss_man_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] },
-	{ "category": "people", "char": "👩‍❤️‍💋‍👩", "name": "couplekiss_woman_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] },
-	{ "category": "people", "char": "👨‍❤️‍💋‍👨", "name": "couplekiss_man_man", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] },
-	{ "category": "people", "char": "👪", "name": "family_man_woman_boy", "keywords": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"] },
-	{ "category": "people", "char": "👨‍👩‍👧", "name": "family_man_woman_girl", "keywords": ["home", "parents", "people", "human", "child"] },
-	{ "category": "people", "char": "👨‍👩‍👧‍👦", "name": "family_man_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👩‍👦‍👦", "name": "family_man_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👩‍👧‍👧", "name": "family_man_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👩‍👩‍👦", "name": "family_woman_woman_boy", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👩‍👩‍👧", "name": "family_woman_woman_girl", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👩‍👩‍👧‍👦", "name": "family_woman_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👩‍👩‍👦‍👦", "name": "family_woman_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👩‍👩‍👧‍👧", "name": "family_woman_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👨‍👦", "name": "family_man_man_boy", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👨‍👧", "name": "family_man_man_girl", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👨‍👧‍👦", "name": "family_man_man_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👨‍👦‍👦", "name": "family_man_man_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👨‍👧‍👧", "name": "family_man_man_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] },
-	{ "category": "people", "char": "👩‍👦", "name": "family_woman_boy", "keywords": ["home", "parent", "people", "human", "child"] },
-	{ "category": "people", "char": "👩‍👧", "name": "family_woman_girl", "keywords": ["home", "parent", "people", "human", "child"] },
-	{ "category": "people", "char": "👩‍👧‍👦", "name": "family_woman_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] },
-	{ "category": "people", "char": "👩‍👦‍👦", "name": "family_woman_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] },
-	{ "category": "people", "char": "👩‍👧‍👧", "name": "family_woman_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👦", "name": "family_man_boy", "keywords": ["home", "parent", "people", "human", "child"] },
-	{ "category": "people", "char": "👨‍👧", "name": "family_man_girl", "keywords": ["home", "parent", "people", "human", "child"] },
-	{ "category": "people", "char": "👨‍👧‍👦", "name": "family_man_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👦‍👦", "name": "family_man_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] },
-	{ "category": "people", "char": "👨‍👧‍👧", "name": "family_man_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] },
-	{ "category": "people", "char": "🧶", "name": "yarn", "keywords": ["ball", "crochet", "knit"] },
-	{ "category": "people", "char": "🧵", "name": "thread", "keywords": ["needle", "sewing", "spool", "string"] },
-	{ "category": "people", "char": "🧥", "name": "coat", "keywords": ["jacket"] },
-	{ "category": "people", "char": "🥼", "name": "labcoat", "keywords": ["doctor", "experiment", "scientist", "chemist"] },
-	{ "category": "people", "char": "👚", "name": "womans_clothes", "keywords": ["fashion", "shopping_bags", "female"] },
-	{ "category": "people", "char": "👕", "name": "tshirt", "keywords": ["fashion", "cloth", "casual", "shirt", "tee"] },
-	{ "category": "people", "char": "👖", "name": "jeans", "keywords": ["fashion", "shopping"] },
-	{ "category": "people", "char": "👔", "name": "necktie", "keywords": ["shirt", "suitup", "formal", "fashion", "cloth", "business"] },
-	{ "category": "people", "char": "👗", "name": "dress", "keywords": ["clothes", "fashion", "shopping"] },
-	{ "category": "people", "char": "👙", "name": "bikini", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] },
-	{ "category": "people", "char": "🩱", "name": "one_piece_swimsuit", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] },
-	{ "category": "people", "char": "👘", "name": "kimono", "keywords": ["dress", "fashion", "women", "female", "japanese"] },
-	{ "category": "people", "char": "🥻", "name": "sari", "keywords": ["dress", "fashion", "women", "female"] },
-	{ "category": "people", "char": "🩲", "name": "briefs", "keywords": ["dress", "fashion"] },
-	{ "category": "people", "char": "🩳", "name": "shorts", "keywords": ["dress", "fashion"] },
-	{ "category": "people", "char": "💄", "name": "lipstick", "keywords": ["female", "girl", "fashion", "woman"] },
-	{ "category": "people", "char": "💋", "name": "kiss", "keywords": ["face", "lips", "love", "like", "affection", "valentines"] },
-	{ "category": "people", "char": "👣", "name": "footprints", "keywords": ["feet", "tracking", "walking", "beach"] },
-	{ "category": "people", "char": "🥿", "name": "flat_shoe", "keywords": ["ballet", "slip-on", "slipper"] },
-	{ "category": "people", "char": "👠", "name": "high_heel", "keywords": ["fashion", "shoes", "female", "pumps", "stiletto"] },
-	{ "category": "people", "char": "👡", "name": "sandal", "keywords": ["shoes", "fashion", "flip flops"] },
-	{ "category": "people", "char": "👢", "name": "boot", "keywords": ["shoes", "fashion"] },
-	{ "category": "people", "char": "👞", "name": "mans_shoe", "keywords": ["fashion", "male"] },
-	{ "category": "people", "char": "👟", "name": "athletic_shoe", "keywords": ["shoes", "sports", "sneakers"] },
-	{ "category": "people", "char": "🩴", "name": "thong_sandal", "keywords": [] },
-	{ "category": "people", "char": "🩰", "name": "ballet_shoes", "keywords": ["shoes", "sports"] },
-	{ "category": "people", "char": "🧦", "name": "socks", "keywords": ["stockings", "clothes"] },
-	{ "category": "people", "char": "🧤", "name": "gloves", "keywords": ["hands", "winter", "clothes"] },
-	{ "category": "people", "char": "🧣", "name": "scarf", "keywords": ["neck", "winter", "clothes"] },
-	{ "category": "people", "char": "👒", "name": "womans_hat", "keywords": ["fashion", "accessories", "female", "lady", "spring"] },
-	{ "category": "people", "char": "🎩", "name": "tophat", "keywords": ["magic", "gentleman", "classy", "circus"] },
-	{ "category": "people", "char": "🧢", "name": "billed_hat", "keywords": ["cap", "baseball"] },
-	{ "category": "people", "char": "⛑", "name": "rescue_worker_helmet", "keywords": ["construction", "build"] },
-	{ "category": "people", "char": "🪖", "name": "military_helmet", "keywords": [] },
-	{ "category": "people", "char": "🎓", "name": "mortar_board", "keywords": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"] },
-	{ "category": "people", "char": "👑", "name": "crown", "keywords": ["king", "kod", "leader", "royalty", "lord"] },
-	{ "category": "people", "char": "🎒", "name": "school_satchel", "keywords": ["student", "education", "bag", "backpack"] },
-	{ "category": "people", "char": "🧳", "name": "luggage", "keywords": ["packing", "travel"] },
-	{ "category": "people", "char": "👝", "name": "pouch", "keywords": ["bag", "accessories", "shopping"] },
-	{ "category": "people", "char": "👛", "name": "purse", "keywords": ["fashion", "accessories", "money", "sales", "shopping"] },
-	{ "category": "people", "char": "👜", "name": "handbag", "keywords": ["fashion", "accessory", "accessories", "shopping"] },
-	{ "category": "people", "char": "💼", "name": "briefcase", "keywords": ["business", "documents", "work", "law", "legal", "job", "career"] },
-	{ "category": "people", "char": "👓", "name": "eyeglasses", "keywords": ["fashion", "accessories", "eyesight", "nerdy", "dork", "geek"] },
-	{ "category": "people", "char": "🕶", "name": "dark_sunglasses", "keywords": ["face", "cool", "accessories"] },
-	{ "category": "people", "char": "🥽", "name": "goggles", "keywords": ["eyes", "protection", "safety"] },
-	{ "category": "people", "char": "💍", "name": "ring", "keywords": ["wedding", "propose", "marriage", "valentines", "diamond", "fashion", "jewelry", "gem", "engagement"] },
-	{ "category": "people", "char": "🌂", "name": "closed_umbrella", "keywords": ["weather", "rain", "drizzle"] },
-	{ "category": "animals_and_nature", "char": "🐶", "name": "dog", "keywords": ["animal", "friend", "nature", "woof", "puppy", "pet", "faithful"] },
-	{ "category": "animals_and_nature", "char": "🐱", "name": "cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] },
-	{ "category": "animals_and_nature", "char": "🐈‍⬛", "name": "black_cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] },
-	{ "category": "animals_and_nature", "char": "🐭", "name": "mouse", "keywords": ["animal", "nature", "cheese_wedge", "rodent"] },
-	{ "category": "animals_and_nature", "char": "🐹", "name": "hamster", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐰", "name": "rabbit", "keywords": ["animal", "nature", "pet", "spring", "magic", "bunny"] },
-	{ "category": "animals_and_nature", "char": "🦊", "name": "fox_face", "keywords": ["animal", "nature", "face"] },
-	{ "category": "animals_and_nature", "char": "🐻", "name": "bear", "keywords": ["animal", "nature", "wild"] },
-	{ "category": "animals_and_nature", "char": "🐼", "name": "panda_face", "keywords": ["animal", "nature", "panda"] },
-	{ "category": "animals_and_nature", "char": "🐨", "name": "koala", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐯", "name": "tiger", "keywords": ["animal", "cat", "danger", "wild", "nature", "roar"] },
-	{ "category": "animals_and_nature", "char": "🦁", "name": "lion", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐮", "name": "cow", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] },
-	{ "category": "animals_and_nature", "char": "🐷", "name": "pig", "keywords": ["animal", "oink", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐽", "name": "pig_nose", "keywords": ["animal", "oink"] },
-	{ "category": "animals_and_nature", "char": "🐸", "name": "frog", "keywords": ["animal", "nature", "croak", "toad"] },
-	{ "category": "animals_and_nature", "char": "🦑", "name": "squid", "keywords": ["animal", "nature", "ocean", "sea"] },
-	{ "category": "animals_and_nature", "char": "🐙", "name": "octopus", "keywords": ["animal", "creature", "ocean", "sea", "nature", "beach"] },
-	{ "category": "animals_and_nature", "char": "🦐", "name": "shrimp", "keywords": ["animal", "ocean", "nature", "seafood"] },
-	{ "category": "animals_and_nature", "char": "🐵", "name": "monkey_face", "keywords": ["animal", "nature", "circus"] },
-	{ "category": "animals_and_nature", "char": "🦍", "name": "gorilla", "keywords": ["animal", "nature", "circus"] },
-	{ "category": "animals_and_nature", "char": "🙈", "name": "see_no_evil", "keywords": ["monkey", "animal", "nature", "haha"] },
-	{ "category": "animals_and_nature", "char": "🙉", "name": "hear_no_evil", "keywords": ["animal", "monkey", "nature"] },
-	{ "category": "animals_and_nature", "char": "🙊", "name": "speak_no_evil", "keywords": ["monkey", "animal", "nature", "omg"] },
-	{ "category": "animals_and_nature", "char": "🐒", "name": "monkey", "keywords": ["animal", "nature", "banana", "circus"] },
-	{ "category": "animals_and_nature", "char": "🐔", "name": "chicken", "keywords": ["animal", "cluck", "nature", "bird"] },
-	{ "category": "animals_and_nature", "char": "🐧", "name": "penguin", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐦", "name": "bird", "keywords": ["animal", "nature", "fly", "tweet", "spring"] },
-	{ "category": "animals_and_nature", "char": "🐤", "name": "baby_chick", "keywords": ["animal", "chicken", "bird"] },
-	{ "category": "animals_and_nature", "char": "🐣", "name": "hatching_chick", "keywords": ["animal", "chicken", "egg", "born", "baby", "bird"] },
-	{ "category": "animals_and_nature", "char": "🐥", "name": "hatched_chick", "keywords": ["animal", "chicken", "baby", "bird"] },
-	{ "category": "animals_and_nature", "char": "🦆", "name": "duck", "keywords": ["animal", "nature", "bird", "mallard"] },
-	{ "category": "animals_and_nature", "char": "🦅", "name": "eagle", "keywords": ["animal", "nature", "bird"] },
-	{ "category": "animals_and_nature", "char": "🦉", "name": "owl", "keywords": ["animal", "nature", "bird", "hoot"] },
-	{ "category": "animals_and_nature", "char": "🦇", "name": "bat", "keywords": ["animal", "nature", "blind", "vampire"] },
-	{ "category": "animals_and_nature", "char": "🐺", "name": "wolf", "keywords": ["animal", "nature", "wild"] },
-	{ "category": "animals_and_nature", "char": "🐗", "name": "boar", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐴", "name": "horse", "keywords": ["animal", "brown", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦄", "name": "unicorn", "keywords": ["animal", "nature", "mystical"] },
-	{ "category": "animals_and_nature", "char": "🐝", "name": "honeybee", "keywords": ["animal", "insect", "nature", "bug", "spring", "honey"] },
-	{ "category": "animals_and_nature", "char": "🐛", "name": "bug", "keywords": ["animal", "insect", "nature", "worm"] },
-	{ "category": "animals_and_nature", "char": "🦋", "name": "butterfly", "keywords": ["animal", "insect", "nature", "caterpillar"] },
-	{ "category": "animals_and_nature", "char": "🐌", "name": "snail", "keywords": ["slow", "animal", "shell"] },
-	{ "category": "animals_and_nature", "char": "🐞", "name": "beetle", "keywords": ["animal", "insect", "nature", "ladybug"] },
-	{ "category": "animals_and_nature", "char": "🐜", "name": "ant", "keywords": ["animal", "insect", "nature", "bug"] },
-	{ "category": "animals_and_nature", "char": "🦗", "name": "grasshopper", "keywords": ["animal", "cricket", "chirp"] },
-	{ "category": "animals_and_nature", "char": "🕷", "name": "spider", "keywords": ["animal", "arachnid"] },
-	{ "category": "animals_and_nature", "char": "🪲", "name": "beetle", "keywords": ["animal"] },
-	{ "category": "animals_and_nature", "char": "🪳", "name": "cockroach", "keywords": ["animal"] },
-	{ "category": "animals_and_nature", "char": "🪰", "name": "fly", "keywords": ["animal"] },
-	{ "category": "animals_and_nature", "char": "🪱", "name": "worm", "keywords": ["animal"] },
-	{ "category": "animals_and_nature", "char": "🦂", "name": "scorpion", "keywords": ["animal", "arachnid"] },
-	{ "category": "animals_and_nature", "char": "🦀", "name": "crab", "keywords": ["animal", "crustacean"] },
-	{ "category": "animals_and_nature", "char": "🐍", "name": "snake", "keywords": ["animal", "evil", "nature", "hiss", "python"] },
-	{ "category": "animals_and_nature", "char": "🦎", "name": "lizard", "keywords": ["animal", "nature", "reptile"] },
-	{ "category": "animals_and_nature", "char": "🦖", "name": "t-rex", "keywords": ["animal", "nature", "dinosaur", "tyrannosaurus", "extinct"] },
-	{ "category": "animals_and_nature", "char": "🦕", "name": "sauropod", "keywords": ["animal", "nature", "dinosaur", "brachiosaurus", "brontosaurus", "diplodocus", "extinct"] },
-	{ "category": "animals_and_nature", "char": "🐢", "name": "turtle", "keywords": ["animal", "slow", "nature", "tortoise"] },
-	{ "category": "animals_and_nature", "char": "🐠", "name": "tropical_fish", "keywords": ["animal", "swim", "ocean", "beach", "nemo"] },
-	{ "category": "animals_and_nature", "char": "🐟", "name": "fish", "keywords": ["animal", "food", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐡", "name": "blowfish", "keywords": ["animal", "nature", "food", "sea", "ocean"] },
-	{ "category": "animals_and_nature", "char": "🐬", "name": "dolphin", "keywords": ["animal", "nature", "fish", "sea", "ocean", "flipper", "fins", "beach"] },
-	{ "category": "animals_and_nature", "char": "🦈", "name": "shark", "keywords": ["animal", "nature", "fish", "sea", "ocean", "jaws", "fins", "beach"] },
-	{ "category": "animals_and_nature", "char": "🐳", "name": "whale", "keywords": ["animal", "nature", "sea", "ocean"] },
-	{ "category": "animals_and_nature", "char": "🐋", "name": "whale2", "keywords": ["animal", "nature", "sea", "ocean"] },
-	{ "category": "animals_and_nature", "char": "🐊", "name": "crocodile", "keywords": ["animal", "nature", "reptile", "lizard", "alligator"] },
-	{ "category": "animals_and_nature", "char": "🐆", "name": "leopard", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦓", "name": "zebra", "keywords": ["animal", "nature", "stripes", "safari"] },
-	{ "category": "animals_and_nature", "char": "🐅", "name": "tiger2", "keywords": ["animal", "nature", "roar"] },
-	{ "category": "animals_and_nature", "char": "🐃", "name": "water_buffalo", "keywords": ["animal", "nature", "ox", "cow"] },
-	{ "category": "animals_and_nature", "char": "🐂", "name": "ox", "keywords": ["animal", "cow", "beef"] },
-	{ "category": "animals_and_nature", "char": "🐄", "name": "cow2", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] },
-	{ "category": "animals_and_nature", "char": "🦌", "name": "deer", "keywords": ["animal", "nature", "horns", "venison"] },
-	{ "category": "animals_and_nature", "char": "🐪", "name": "dromedary_camel", "keywords": ["animal", "hot", "desert", "hump"] },
-	{ "category": "animals_and_nature", "char": "🐫", "name": "camel", "keywords": ["animal", "nature", "hot", "desert", "hump"] },
-	{ "category": "animals_and_nature", "char": "🦒", "name": "giraffe", "keywords": ["animal", "nature", "spots", "safari"] },
-	{ "category": "animals_and_nature", "char": "🐘", "name": "elephant", "keywords": ["animal", "nature", "nose", "th", "circus"] },
-	{ "category": "animals_and_nature", "char": "🦏", "name": "rhinoceros", "keywords": ["animal", "nature", "horn"] },
-	{ "category": "animals_and_nature", "char": "🐐", "name": "goat", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐏", "name": "ram", "keywords": ["animal", "sheep", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐑", "name": "sheep", "keywords": ["animal", "nature", "wool", "shipit"] },
-	{ "category": "animals_and_nature", "char": "🐎", "name": "racehorse", "keywords": ["animal", "gamble", "luck"] },
-	{ "category": "animals_and_nature", "char": "🐖", "name": "pig2", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐀", "name": "rat", "keywords": ["animal", "mouse", "rodent"] },
-	{ "category": "animals_and_nature", "char": "🐁", "name": "mouse2", "keywords": ["animal", "nature", "rodent"] },
-	{ "category": "animals_and_nature", "char": "🐓", "name": "rooster", "keywords": ["animal", "nature", "chicken"] },
-	{ "category": "animals_and_nature", "char": "🦃", "name": "turkey", "keywords": ["animal", "bird"] },
-	{ "category": "animals_and_nature", "char": "🕊", "name": "dove", "keywords": ["animal", "bird"] },
-	{ "category": "animals_and_nature", "char": "🐕", "name": "dog2", "keywords": ["animal", "nature", "friend", "doge", "pet", "faithful"] },
-	{ "category": "animals_and_nature", "char": "🐩", "name": "poodle", "keywords": ["dog", "animal", "101", "nature", "pet"] },
-	{ "category": "animals_and_nature", "char": "🐈", "name": "cat2", "keywords": ["animal", "meow", "pet", "cats"] },
-	{ "category": "animals_and_nature", "char": "🐇", "name": "rabbit2", "keywords": ["animal", "nature", "pet", "magic", "spring"] },
-	{ "category": "animals_and_nature", "char": "🐿", "name": "chipmunk", "keywords": ["animal", "nature", "rodent", "squirrel"] },
-	{ "category": "animals_and_nature", "char": "🦔", "name": "hedgehog", "keywords": ["animal", "nature", "spiny"] },
-	{ "category": "animals_and_nature", "char": "🦝", "name": "raccoon", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦙", "name": "llama", "keywords": ["animal", "nature", "alpaca"] },
-	{ "category": "animals_and_nature", "char": "🦛", "name": "hippopotamus", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦘", "name": "kangaroo", "keywords": ["animal", "nature", "australia", "joey", "hop", "marsupial"] },
-	{ "category": "animals_and_nature", "char": "🦡", "name": "badger", "keywords": ["animal", "nature", "honey"] },
-	{ "category": "animals_and_nature", "char": "🦢", "name": "swan", "keywords": ["animal", "nature", "bird"] },
-	{ "category": "animals_and_nature", "char": "🦚", "name": "peacock", "keywords": ["animal", "nature", "peahen", "bird"] },
-	{ "category": "animals_and_nature", "char": "🦜", "name": "parrot", "keywords": ["animal", "nature", "bird", "pirate", "talk"] },
-	{ "category": "animals_and_nature", "char": "🦞", "name": "lobster", "keywords": ["animal", "nature", "bisque", "claws", "seafood"] },
-	{ "category": "animals_and_nature", "char": "🦠", "name": "microbe", "keywords": ["amoeba", "bacteria", "germs"] },
-	{ "category": "animals_and_nature", "char": "🦟", "name": "mosquito", "keywords": ["animal", "nature", "insect", "malaria"] },
-	{ "category": "animals_and_nature", "char": "🦬", "name": "bison", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦣", "name": "mammoth", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦫", "name": "beaver", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐻‍❄️", "name": "polar_bear", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦤", "name": "dodo", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🪶", "name": "feather", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦭", "name": "seal", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐾", "name": "paw_prints", "keywords": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"] },
-	{ "category": "animals_and_nature", "char": "🐉", "name": "dragon", "keywords": ["animal", "myth", "nature", "chinese", "green"] },
-	{ "category": "animals_and_nature", "char": "🐲", "name": "dragon_face", "keywords": ["animal", "myth", "nature", "chinese", "green"] },
-	{ "category": "animals_and_nature", "char": "🦧", "name": "orangutan", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦮", "name": "guide_dog", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🐕‍🦺", "name": "service_dog", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦥", "name": "sloth", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦦", "name": "otter", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦨", "name": "skunk", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🦩", "name": "flamingo", "keywords": ["animal", "nature"] },
-	{ "category": "animals_and_nature", "char": "🌵", "name": "cactus", "keywords": ["vegetable", "plant", "nature"] },
-	{ "category": "animals_and_nature", "char": "🎄", "name": "christmas_tree", "keywords": ["festival", "vacation", "december", "xmas", "celebration"] },
-	{ "category": "animals_and_nature", "char": "🌲", "name": "evergreen_tree", "keywords": ["plant", "nature"] },
-	{ "category": "animals_and_nature", "char": "🌳", "name": "deciduous_tree", "keywords": ["plant", "nature"] },
-	{ "category": "animals_and_nature", "char": "🌴", "name": "palm_tree", "keywords": ["plant", "vegetable", "nature", "summer", "beach", "mojito", "tropical"] },
-	{ "category": "animals_and_nature", "char": "🌱", "name": "seedling", "keywords": ["plant", "nature", "grass", "lawn", "spring"] },
-	{ "category": "animals_and_nature", "char": "🌿", "name": "herb", "keywords": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"] },
-	{ "category": "animals_and_nature", "char": "☘", "name": "shamrock", "keywords": ["vegetable", "plant", "nature", "irish", "clover"] },
-	{ "category": "animals_and_nature", "char": "🍀", "name": "four_leaf_clover", "keywords": ["vegetable", "plant", "nature", "lucky", "irish"] },
-	{ "category": "animals_and_nature", "char": "🎍", "name": "bamboo", "keywords": ["plant", "nature", "vegetable", "panda", "pine_decoration"] },
-	{ "category": "animals_and_nature", "char": "🎋", "name": "tanabata_tree", "keywords": ["plant", "nature", "branch", "summer"] },
-	{ "category": "animals_and_nature", "char": "🍃", "name": "leaves", "keywords": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"] },
-	{ "category": "animals_and_nature", "char": "🍂", "name": "fallen_leaf", "keywords": ["nature", "plant", "vegetable", "leaves"] },
-	{ "category": "animals_and_nature", "char": "🍁", "name": "maple_leaf", "keywords": ["nature", "plant", "vegetable", "ca", "fall"] },
-	{ "category": "animals_and_nature", "char": "🌾", "name": "ear_of_rice", "keywords": ["nature", "plant"] },
-	{ "category": "animals_and_nature", "char": "🌺", "name": "hibiscus", "keywords": ["plant", "vegetable", "flowers", "beach"] },
-	{ "category": "animals_and_nature", "char": "🌻", "name": "sunflower", "keywords": ["nature", "plant", "fall"] },
-	{ "category": "animals_and_nature", "char": "🌹", "name": "rose", "keywords": ["flowers", "valentines", "love", "spring"] },
-	{ "category": "animals_and_nature", "char": "🥀", "name": "wilted_flower", "keywords": ["plant", "nature", "flower"] },
-	{ "category": "animals_and_nature", "char": "🌷", "name": "tulip", "keywords": ["flowers", "plant", "nature", "summer", "spring"] },
-	{ "category": "animals_and_nature", "char": "🌼", "name": "blossom", "keywords": ["nature", "flowers", "yellow"] },
-	{ "category": "animals_and_nature", "char": "🌸", "name": "cherry_blossom", "keywords": ["nature", "plant", "spring", "flower"] },
-	{ "category": "animals_and_nature", "char": "💐", "name": "bouquet", "keywords": ["flowers", "nature", "spring"] },
-	{ "category": "animals_and_nature", "char": "🍄", "name": "mushroom", "keywords": ["plant", "vegetable"] },
-	{ "category": "animals_and_nature", "char": "🪴", "name": "potted_plant", "keywords": ["plant"] },
-	{ "category": "animals_and_nature", "char": "🌰", "name": "chestnut", "keywords": ["food", "squirrel"] },
-	{ "category": "animals_and_nature", "char": "🎃", "name": "jack_o_lantern", "keywords": ["halloween", "light", "pumpkin", "creepy", "fall"] },
-	{ "category": "animals_and_nature", "char": "🐚", "name": "shell", "keywords": ["nature", "sea", "beach"] },
-	{ "category": "animals_and_nature", "char": "🕸", "name": "spider_web", "keywords": ["animal", "insect", "arachnid", "silk"] },
-	{ "category": "animals_and_nature", "char": "🌎", "name": "earth_americas", "keywords": ["globe", "world", "USA", "international"] },
-	{ "category": "animals_and_nature", "char": "🌍", "name": "earth_africa", "keywords": ["globe", "world", "international"] },
-	{ "category": "animals_and_nature", "char": "🌏", "name": "earth_asia", "keywords": ["globe", "world", "east", "international"] },
-	{ "category": "animals_and_nature", "char": "🪐", "name": "ringed_planet", "keywords": ["saturn"] },
-	{ "category": "animals_and_nature", "char": "🌕", "name": "full_moon", "keywords": ["nature", "yellow", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌖", "name": "waning_gibbous_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep", "waxing_gibbous_moon"] },
-	{ "category": "animals_and_nature", "char": "🌗", "name": "last_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌘", "name": "waning_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌑", "name": "new_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌒", "name": "waxing_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌓", "name": "first_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌔", "name": "waxing_gibbous_moon", "keywords": ["nature", "night", "sky", "gray", "twilight", "planet", "space", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌚", "name": "new_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌝", "name": "full_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌛", "name": "first_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌜", "name": "last_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] },
-	{ "category": "animals_and_nature", "char": "🌞", "name": "sun_with_face", "keywords": ["nature", "morning", "sky"] },
-	{ "category": "animals_and_nature", "char": "🌙", "name": "crescent_moon", "keywords": ["night", "sleep", "sky", "evening", "magic"] },
-	{ "category": "animals_and_nature", "char": "⭐", "name": "star", "keywords": ["night", "yellow"] },
-	{ "category": "animals_and_nature", "char": "🌟", "name": "star2", "keywords": ["night", "sparkle", "awesome", "good", "magic"] },
-	{ "category": "animals_and_nature", "char": "💫", "name": "dizzy", "keywords": ["star", "sparkle", "shoot", "magic"] },
-	{ "category": "animals_and_nature", "char": "✨", "name": "sparkles", "keywords": ["stars", "shine", "shiny", "cool", "awesome", "good", "magic"] },
-	{ "category": "animals_and_nature", "char": "☄", "name": "comet", "keywords": ["space"] },
-	{ "category": "animals_and_nature", "char": "☀️", "name": "sunny", "keywords": ["weather", "nature", "brightness", "summer", "beach", "spring"] },
-	{ "category": "animals_and_nature", "char": "🌤", "name": "sun_behind_small_cloud", "keywords": ["weather"] },
-	{ "category": "animals_and_nature", "char": "⛅", "name": "partly_sunny", "keywords": ["weather", "nature", "cloudy", "morning", "fall", "spring"] },
-	{ "category": "animals_and_nature", "char": "🌥", "name": "sun_behind_large_cloud", "keywords": ["weather"] },
-	{ "category": "animals_and_nature", "char": "🌦", "name": "sun_behind_rain_cloud", "keywords": ["weather"] },
-	{ "category": "animals_and_nature", "char": "☁️", "name": "cloud", "keywords": ["weather", "sky"] },
-	{ "category": "animals_and_nature", "char": "🌧", "name": "cloud_with_rain", "keywords": ["weather"] },
-	{ "category": "animals_and_nature", "char": "⛈", "name": "cloud_with_lightning_and_rain", "keywords": ["weather", "lightning"] },
-	{ "category": "animals_and_nature", "char": "🌩", "name": "cloud_with_lightning", "keywords": ["weather", "thunder"] },
-	{ "category": "animals_and_nature", "char": "⚡", "name": "zap", "keywords": ["thunder", "weather", "lightning bolt", "fast"] },
-	{ "category": "animals_and_nature", "char": "🔥", "name": "fire", "keywords": ["hot", "cook", "flame"] },
-	{ "category": "animals_and_nature", "char": "💥", "name": "boom", "keywords": ["bomb", "explode", "explosion", "collision", "blown"] },
-	{ "category": "animals_and_nature", "char": "❄️", "name": "snowflake", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas"] },
-	{ "category": "animals_and_nature", "char": "🌨", "name": "cloud_with_snow", "keywords": ["weather"] },
-	{ "category": "animals_and_nature", "char": "⛄", "name": "snowman", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen", "without_snow"] },
-	{ "category": "animals_and_nature", "char": "☃", "name": "snowman_with_snow", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen"] },
-	{ "category": "animals_and_nature", "char": "🌬", "name": "wind_face", "keywords": ["gust", "air"] },
-	{ "category": "animals_and_nature", "char": "💨", "name": "dash", "keywords": ["wind", "air", "fast", "shoo", "fart", "smoke", "puff"] },
-	{ "category": "animals_and_nature", "char": "🌪", "name": "tornado", "keywords": ["weather", "cyclone", "twister"] },
-	{ "category": "animals_and_nature", "char": "🌫", "name": "fog", "keywords": ["weather"] },
-	{ "category": "animals_and_nature", "char": "☂", "name": "open_umbrella", "keywords": ["weather", "spring"] },
-	{ "category": "animals_and_nature", "char": "☔", "name": "umbrella", "keywords": ["rainy", "weather", "spring"] },
-	{ "category": "animals_and_nature", "char": "💧", "name": "droplet", "keywords": ["water", "drip", "faucet", "spring"] },
-	{ "category": "animals_and_nature", "char": "💦", "name": "sweat_drops", "keywords": ["water", "drip", "oops"] },
-	{ "category": "animals_and_nature", "char": "🌊", "name": "ocean", "keywords": ["sea", "water", "wave", "nature", "tsunami", "disaster"] },
-	{ "category": "food_and_drink", "char": "🍏", "name": "green_apple", "keywords": ["fruit", "nature"] },
-	{ "category": "food_and_drink", "char": "🍎", "name": "apple", "keywords": ["fruit", "mac", "school"] },
-	{ "category": "food_and_drink", "char": "🍐", "name": "pear", "keywords": ["fruit", "nature", "food"] },
-	{ "category": "food_and_drink", "char": "🍊", "name": "tangerine", "keywords": ["food", "fruit", "nature", "orange"] },
-	{ "category": "food_and_drink", "char": "🍋", "name": "lemon", "keywords": ["fruit", "nature"] },
-	{ "category": "food_and_drink", "char": "🍌", "name": "banana", "keywords": ["fruit", "food", "monkey"] },
-	{ "category": "food_and_drink", "char": "🍉", "name": "watermelon", "keywords": ["fruit", "food", "picnic", "summer"] },
-	{ "category": "food_and_drink", "char": "🍇", "name": "grapes", "keywords": ["fruit", "food", "wine"] },
-	{ "category": "food_and_drink", "char": "🍓", "name": "strawberry", "keywords": ["fruit", "food", "nature"] },
-	{ "category": "food_and_drink", "char": "🍈", "name": "melon", "keywords": ["fruit", "nature", "food"] },
-	{ "category": "food_and_drink", "char": "🍒", "name": "cherries", "keywords": ["food", "fruit"] },
-	{ "category": "food_and_drink", "char": "🍑", "name": "peach", "keywords": ["fruit", "nature", "food"] },
-	{ "category": "food_and_drink", "char": "🍍", "name": "pineapple", "keywords": ["fruit", "nature", "food"] },
-	{ "category": "food_and_drink", "char": "🥥", "name": "coconut", "keywords": ["fruit", "nature", "food", "palm"] },
-	{ "category": "food_and_drink", "char": "🥝", "name": "kiwi_fruit", "keywords": ["fruit", "food"] },
-	{ "category": "food_and_drink", "char": "🥭", "name": "mango", "keywords": ["fruit", "food", "tropical"] },
-	{ "category": "food_and_drink", "char": "🥑", "name": "avocado", "keywords": ["fruit", "food"] },
-	{ "category": "food_and_drink", "char": "🥦", "name": "broccoli", "keywords": ["fruit", "food", "vegetable"] },
-	{ "category": "food_and_drink", "char": "🍅", "name": "tomato", "keywords": ["fruit", "vegetable", "nature", "food"] },
-	{ "category": "food_and_drink", "char": "🍆", "name": "eggplant", "keywords": ["vegetable", "nature", "food", "aubergine"] },
-	{ "category": "food_and_drink", "char": "🥒", "name": "cucumber", "keywords": ["fruit", "food", "pickle"] },
-	{ "category": "food_and_drink", "char": "🫐", "name": "blueberries", "keywords": ["fruit", "food"] },
-	{ "category": "food_and_drink", "char": "🫒", "name": "olive", "keywords": ["fruit", "food"] },
-	{ "category": "food_and_drink", "char": "🫑", "name": "bell_pepper", "keywords": ["fruit", "food"] },
-	{ "category": "food_and_drink", "char": "🥕", "name": "carrot", "keywords": ["vegetable", "food", "orange"] },
-	{ "category": "food_and_drink", "char": "🌶", "name": "hot_pepper", "keywords": ["food", "spicy", "chilli", "chili"] },
-	{ "category": "food_and_drink", "char": "🥔", "name": "potato", "keywords": ["food", "tuber", "vegatable", "starch"] },
-	{ "category": "food_and_drink", "char": "🌽", "name": "corn", "keywords": ["food", "vegetable", "plant"] },
-	{ "category": "food_and_drink", "char": "🥬", "name": "leafy_greens", "keywords": ["food", "vegetable", "plant", "bok choy", "cabbage", "kale", "lettuce"] },
-	{ "category": "food_and_drink", "char": "🍠", "name": "sweet_potato", "keywords": ["food", "nature"] },
-	{ "category": "food_and_drink", "char": "🥜", "name": "peanuts", "keywords": ["food", "nut"] },
-	{ "category": "food_and_drink", "char": "🧄", "name": "garlic", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🧅", "name": "onion", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🍯", "name": "honey_pot", "keywords": ["bees", "sweet", "kitchen"] },
-	{ "category": "food_and_drink", "char": "🥐", "name": "croissant", "keywords": ["food", "bread", "french"] },
-	{ "category": "food_and_drink", "char": "🍞", "name": "bread", "keywords": ["food", "wheat", "breakfast", "toast"] },
-	{ "category": "food_and_drink", "char": "🥖", "name": "baguette_bread", "keywords": ["food", "bread", "french"] },
-	{ "category": "food_and_drink", "char": "🥯", "name": "bagel", "keywords": ["food", "bread", "bakery", "schmear"] },
-	{ "category": "food_and_drink", "char": "🥨", "name": "pretzel", "keywords": ["food", "bread", "twisted"] },
-	{ "category": "food_and_drink", "char": "🧀", "name": "cheese", "keywords": ["food", "chadder"] },
-	{ "category": "food_and_drink", "char": "🥚", "name": "egg", "keywords": ["food", "chicken", "breakfast"] },
-	{ "category": "food_and_drink", "char": "🥓", "name": "bacon", "keywords": ["food", "breakfast", "pork", "pig", "meat"] },
-	{ "category": "food_and_drink", "char": "🥩", "name": "steak", "keywords": ["food", "cow", "meat", "cut", "chop", "lambchop", "porkchop"] },
-	{ "category": "food_and_drink", "char": "🥞", "name": "pancakes", "keywords": ["food", "breakfast", "flapjacks", "hotcakes"] },
-	{ "category": "food_and_drink", "char": "🍗", "name": "poultry_leg", "keywords": ["food", "meat", "drumstick", "bird", "chicken", "turkey"] },
-	{ "category": "food_and_drink", "char": "🍖", "name": "meat_on_bone", "keywords": ["good", "food", "drumstick"] },
-	{ "category": "food_and_drink", "char": "🦴", "name": "bone", "keywords": ["skeleton"] },
-	{ "category": "food_and_drink", "char": "🍤", "name": "fried_shrimp", "keywords": ["food", "animal", "appetizer", "summer"] },
-	{ "category": "food_and_drink", "char": "🍳", "name": "fried_egg", "keywords": ["food", "breakfast", "kitchen", "egg"] },
-	{ "category": "food_and_drink", "char": "🍔", "name": "hamburger", "keywords": ["meat", "fast food", "beef", "cheeseburger", "mcdonalds", "burger king"] },
-	{ "category": "food_and_drink", "char": "🍟", "name": "fries", "keywords": ["chips", "snack", "fast food"] },
-	{ "category": "food_and_drink", "char": "🥙", "name": "stuffed_flatbread", "keywords": ["food", "flatbread", "stuffed", "gyro"] },
-	{ "category": "food_and_drink", "char": "🌭", "name": "hotdog", "keywords": ["food", "frankfurter"] },
-	{ "category": "food_and_drink", "char": "🍕", "name": "pizza", "keywords": ["food", "party"] },
-	{ "category": "food_and_drink", "char": "🥪", "name": "sandwich", "keywords": ["food", "lunch", "bread"] },
-	{ "category": "food_and_drink", "char": "🥫", "name": "canned_food", "keywords": ["food", "soup"] },
-	{ "category": "food_and_drink", "char": "🍝", "name": "spaghetti", "keywords": ["food", "italian", "noodle"] },
-	{ "category": "food_and_drink", "char": "🌮", "name": "taco", "keywords": ["food", "mexican"] },
-	{ "category": "food_and_drink", "char": "🌯", "name": "burrito", "keywords": ["food", "mexican"] },
-	{ "category": "food_and_drink", "char": "🥗", "name": "green_salad", "keywords": ["food", "healthy", "lettuce"] },
-	{ "category": "food_and_drink", "char": "🥘", "name": "shallow_pan_of_food", "keywords": ["food", "cooking", "casserole", "paella"] },
-	{ "category": "food_and_drink", "char": "🍜", "name": "ramen", "keywords": ["food", "japanese", "noodle", "chopsticks"] },
-	{ "category": "food_and_drink", "char": "🍲", "name": "stew", "keywords": ["food", "meat", "soup"] },
-	{ "category": "food_and_drink", "char": "🍥", "name": "fish_cake", "keywords": ["food", "japan", "sea", "beach", "narutomaki", "pink", "swirl", "kamaboko", "surimi", "ramen"] },
-	{ "category": "food_and_drink", "char": "🥠", "name": "fortune_cookie", "keywords": ["food", "prophecy"] },
-	{ "category": "food_and_drink", "char": "🍣", "name": "sushi", "keywords": ["food", "fish", "japanese", "rice"] },
-	{ "category": "food_and_drink", "char": "🍱", "name": "bento", "keywords": ["food", "japanese", "box"] },
-	{ "category": "food_and_drink", "char": "🍛", "name": "curry", "keywords": ["food", "spicy", "hot", "indian"] },
-	{ "category": "food_and_drink", "char": "🍙", "name": "rice_ball", "keywords": ["food", "japanese"] },
-	{ "category": "food_and_drink", "char": "🍚", "name": "rice", "keywords": ["food", "china", "asian"] },
-	{ "category": "food_and_drink", "char": "🍘", "name": "rice_cracker", "keywords": ["food", "japanese"] },
-	{ "category": "food_and_drink", "char": "🍢", "name": "oden", "keywords": ["food", "japanese"] },
-	{ "category": "food_and_drink", "char": "🍡", "name": "dango", "keywords": ["food", "dessert", "sweet", "japanese", "barbecue", "meat"] },
-	{ "category": "food_and_drink", "char": "🍧", "name": "shaved_ice", "keywords": ["hot", "dessert", "summer"] },
-	{ "category": "food_and_drink", "char": "🍨", "name": "ice_cream", "keywords": ["food", "hot", "dessert"] },
-	{ "category": "food_and_drink", "char": "🍦", "name": "icecream", "keywords": ["food", "hot", "dessert", "summer"] },
-	{ "category": "food_and_drink", "char": "🥧", "name": "pie", "keywords": ["food", "dessert", "pastry"] },
-	{ "category": "food_and_drink", "char": "🍰", "name": "cake", "keywords": ["food", "dessert"] },
-	{ "category": "food_and_drink", "char": "🧁", "name": "cupcake", "keywords": ["food", "dessert", "bakery", "sweet"] },
-	{ "category": "food_and_drink", "char": "🥮", "name": "moon_cake", "keywords": ["food", "autumn"] },
-	{ "category": "food_and_drink", "char": "🎂", "name": "birthday", "keywords": ["food", "dessert", "cake"] },
-	{ "category": "food_and_drink", "char": "🍮", "name": "custard", "keywords": ["dessert", "food"] },
-	{ "category": "food_and_drink", "char": "🍬", "name": "candy", "keywords": ["snack", "dessert", "sweet", "lolly"] },
-	{ "category": "food_and_drink", "char": "🍭", "name": "lollipop", "keywords": ["food", "snack", "candy", "sweet"] },
-	{ "category": "food_and_drink", "char": "🍫", "name": "chocolate_bar", "keywords": ["food", "snack", "dessert", "sweet"] },
-	{ "category": "food_and_drink", "char": "🍿", "name": "popcorn", "keywords": ["food", "movie theater", "films", "snack"] },
-	{ "category": "food_and_drink", "char": "🥟", "name": "dumpling", "keywords": ["food", "empanada", "pierogi", "potsticker"] },
-	{ "category": "food_and_drink", "char": "🍩", "name": "doughnut", "keywords": ["food", "dessert", "snack", "sweet", "donut"] },
-	{ "category": "food_and_drink", "char": "🍪", "name": "cookie", "keywords": ["food", "snack", "oreo", "chocolate", "sweet", "dessert"] },
-	{ "category": "food_and_drink", "char": "🧇", "name": "waffle", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🧆", "name": "falafel", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🧈", "name": "butter", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🦪", "name": "oyster", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🫓", "name": "flatbread", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🫔", "name": "tamale", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🫕", "name": "fondue", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🥛", "name": "milk_glass", "keywords": ["beverage", "drink", "cow"] },
-	{ "category": "food_and_drink", "char": "🍺", "name": "beer", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] },
-	{ "category": "food_and_drink", "char": "🍻", "name": "beers", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] },
-	{ "category": "food_and_drink", "char": "🥂", "name": "clinking_glasses", "keywords": ["beverage", "drink", "party", "alcohol", "celebrate", "cheers", "wine", "champagne", "toast"] },
-	{ "category": "food_and_drink", "char": "🍷", "name": "wine_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "booze"] },
-	{ "category": "food_and_drink", "char": "🥃", "name": "tumbler_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "liquor", "booze", "bourbon", "scotch", "whisky", "glass", "shot"] },
-	{ "category": "food_and_drink", "char": "🍸", "name": "cocktail", "keywords": ["drink", "drunk", "alcohol", "beverage", "booze", "mojito"] },
-	{ "category": "food_and_drink", "char": "🍹", "name": "tropical_drink", "keywords": ["beverage", "cocktail", "summer", "beach", "alcohol", "booze", "mojito"] },
-	{ "category": "food_and_drink", "char": "🍾", "name": "champagne", "keywords": ["drink", "wine", "bottle", "celebration"] },
-	{ "category": "food_and_drink", "char": "🍶", "name": "sake", "keywords": ["wine", "drink", "drunk", "beverage", "japanese", "alcohol", "booze"] },
-	{ "category": "food_and_drink", "char": "🍵", "name": "tea", "keywords": ["drink", "bowl", "breakfast", "green", "british"] },
-	{ "category": "food_and_drink", "char": "🥤", "name": "cup_with_straw", "keywords": ["drink", "soda"] },
-	{ "category": "food_and_drink", "char": "☕", "name": "coffee", "keywords": ["beverage", "caffeine", "latte", "espresso"] },
-	{ "category": "food_and_drink", "char": "🫖", "name": "teapot", "keywords": [] },
-	{ "category": "food_and_drink", "char": "🧋", "name": "bubble_tea", "keywords": ["tapioca"] },
-	{ "category": "food_and_drink", "char": "🍼", "name": "baby_bottle", "keywords": ["food", "container", "milk"] },
-	{ "category": "food_and_drink", "char": "🧃", "name": "beverage_box", "keywords": ["food", "drink"] },
-	{ "category": "food_and_drink", "char": "🧉", "name": "mate", "keywords": ["food", "drink"] },
-	{ "category": "food_and_drink", "char": "🧊", "name": "ice_cube", "keywords": ["food"] },
-	{ "category": "food_and_drink", "char": "🧂", "name": "salt", "keywords": ["condiment", "shaker"] },
-	{ "category": "food_and_drink", "char": "🥄", "name": "spoon", "keywords": ["cutlery", "kitchen", "tableware"] },
-	{ "category": "food_and_drink", "char": "🍴", "name": "fork_and_knife", "keywords": ["cutlery", "kitchen"] },
-	{ "category": "food_and_drink", "char": "🍽", "name": "plate_with_cutlery", "keywords": ["food", "eat", "meal", "lunch", "dinner", "restaurant"] },
-	{ "category": "food_and_drink", "char": "🥣", "name": "bowl_with_spoon", "keywords": ["food", "breakfast", "cereal", "oatmeal", "porridge"] },
-	{ "category": "food_and_drink", "char": "🥡", "name": "takeout_box", "keywords": ["food", "leftovers"] },
-	{ "category": "food_and_drink", "char": "🥢", "name": "chopsticks", "keywords": ["food"] },
-	{ "category": "activity", "char": "⚽", "name": "soccer", "keywords": ["sports", "football"] },
-	{ "category": "activity", "char": "🏀", "name": "basketball", "keywords": ["sports", "balls", "NBA"] },
-	{ "category": "activity", "char": "🏈", "name": "football", "keywords": ["sports", "balls", "NFL"] },
-	{ "category": "activity", "char": "⚾", "name": "baseball", "keywords": ["sports", "balls"] },
-	{ "category": "activity", "char": "🥎", "name": "softball", "keywords": ["sports", "balls"] },
-	{ "category": "activity", "char": "🎾", "name": "tennis", "keywords": ["sports", "balls", "green"] },
-	{ "category": "activity", "char": "🏐", "name": "volleyball", "keywords": ["sports", "balls"] },
-	{ "category": "activity", "char": "🏉", "name": "rugby_football", "keywords": ["sports", "team"] },
-	{ "category": "activity", "char": "🥏", "name": "flying_disc", "keywords": ["sports", "frisbee", "ultimate"] },
-	{ "category": "activity", "char": "🎱", "name": "8ball", "keywords": ["pool", "hobby", "game", "luck", "magic"] },
-	{ "category": "activity", "char": "⛳", "name": "golf", "keywords": ["sports", "business", "flag", "hole", "summer"] },
-	{ "category": "activity", "char": "🏌️‍♀️", "name": "golfing_woman", "keywords": ["sports", "business", "woman", "female"] },
-	{ "category": "activity", "char": "🏌", "name": "golfing_man", "keywords": ["sports", "business"] },
-	{ "category": "activity", "char": "🏓", "name": "ping_pong", "keywords": ["sports", "pingpong"] },
-	{ "category": "activity", "char": "🏸", "name": "badminton", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🥅", "name": "goal_net", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🏒", "name": "ice_hockey", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🏑", "name": "field_hockey", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🥍", "name": "lacrosse", "keywords": ["sports", "ball", "stick"] },
-	{ "category": "activity", "char": "🏏", "name": "cricket", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🎿", "name": "ski", "keywords": ["sports", "winter", "cold", "snow"] },
-	{ "category": "activity", "char": "⛷", "name": "skier", "keywords": ["sports", "winter", "snow"] },
-	{ "category": "activity", "char": "🏂", "name": "snowboarder", "keywords": ["sports", "winter"] },
-	{ "category": "activity", "char": "🤺", "name": "person_fencing", "keywords": ["sports", "fencing", "sword"] },
-	{ "category": "activity", "char": "🤼‍♀️", "name": "women_wrestling", "keywords": ["sports", "wrestlers"] },
-	{ "category": "activity", "char": "🤼‍♂️", "name": "men_wrestling", "keywords": ["sports", "wrestlers"] },
-	{ "category": "activity", "char": "🤸‍♀️", "name": "woman_cartwheeling", "keywords": ["gymnastics"] },
-	{ "category": "activity", "char": "🤸‍♂️", "name": "man_cartwheeling", "keywords": ["gymnastics"] },
-	{ "category": "activity", "char": "🤾‍♀️", "name": "woman_playing_handball", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🤾‍♂️", "name": "man_playing_handball", "keywords": ["sports"] },
-	{ "category": "activity", "char": "⛸", "name": "ice_skate", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🥌", "name": "curling_stone", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🛹", "name": "skateboard", "keywords": ["board"] },
-	{ "category": "activity", "char": "🛷", "name": "sled", "keywords": ["sleigh", "luge", "toboggan"] },
-	{ "category": "activity", "char": "🏹", "name": "bow_and_arrow", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🎣", "name": "fishing_pole_and_fish", "keywords": ["food", "hobby", "summer"] },
-	{ "category": "activity", "char": "🥊", "name": "boxing_glove", "keywords": ["sports", "fighting"] },
-	{ "category": "activity", "char": "🥋", "name": "martial_arts_uniform", "keywords": ["judo", "karate", "taekwondo"] },
-	{ "category": "activity", "char": "🚣‍♀️", "name": "rowing_woman", "keywords": ["sports", "hobby", "water", "ship", "woman", "female"] },
-	{ "category": "activity", "char": "🚣", "name": "rowing_man", "keywords": ["sports", "hobby", "water", "ship"] },
-	{ "category": "activity", "char": "🧗‍♀️", "name": "climbing_woman", "keywords": ["sports", "hobby", "woman", "female", "rock"] },
-	{ "category": "activity", "char": "🧗‍♂️", "name": "climbing_man", "keywords": ["sports", "hobby", "man", "male", "rock"] },
-	{ "category": "activity", "char": "🏊‍♀️", "name": "swimming_woman", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer", "woman", "female"] },
-	{ "category": "activity", "char": "🏊", "name": "swimming_man", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer"] },
-	{ "category": "activity", "char": "🤽‍♀️", "name": "woman_playing_water_polo", "keywords": ["sports", "pool"] },
-	{ "category": "activity", "char": "🤽‍♂️", "name": "man_playing_water_polo", "keywords": ["sports", "pool"] },
-	{ "category": "activity", "char": "🧘‍♀️", "name": "woman_in_lotus_position", "keywords": ["woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"] },
-	{ "category": "activity", "char": "🧘‍♂️", "name": "man_in_lotus_position", "keywords": ["man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"] },
-	{ "category": "activity", "char": "🏄‍♀️", "name": "surfing_woman", "keywords": ["sports", "ocean", "sea", "summer", "beach", "woman", "female"] },
-	{ "category": "activity", "char": "🏄", "name": "surfing_man", "keywords": ["sports", "ocean", "sea", "summer", "beach"] },
-	{ "category": "activity", "char": "🛀", "name": "bath", "keywords": ["clean", "shower", "bathroom"] },
-	{ "category": "activity", "char": "⛹️‍♀️", "name": "basketball_woman", "keywords": ["sports", "human", "woman", "female"] },
-	{ "category": "activity", "char": "⛹", "name": "basketball_man", "keywords": ["sports", "human"] },
-	{ "category": "activity", "char": "🏋️‍♀️", "name": "weight_lifting_woman", "keywords": ["sports", "training", "exercise", "woman", "female"] },
-	{ "category": "activity", "char": "🏋", "name": "weight_lifting_man", "keywords": ["sports", "training", "exercise"] },
-	{ "category": "activity", "char": "🚴‍♀️", "name": "biking_woman", "keywords": ["sports", "bike", "exercise", "hipster", "woman", "female"] },
-	{ "category": "activity", "char": "🚴", "name": "biking_man", "keywords": ["sports", "bike", "exercise", "hipster"] },
-	{ "category": "activity", "char": "🚵‍♀️", "name": "mountain_biking_woman", "keywords": ["transportation", "sports", "human", "race", "bike", "woman", "female"] },
-	{ "category": "activity", "char": "🚵", "name": "mountain_biking_man", "keywords": ["transportation", "sports", "human", "race", "bike"] },
-	{ "category": "activity", "char": "🏇", "name": "horse_racing", "keywords": ["animal", "betting", "competition", "gambling", "luck"] },
-	{ "category": "activity", "char": "🤿", "name": "diving_mask", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🪀", "name": "yo_yo", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🪁", "name": "kite", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🦺", "name": "safety_vest", "keywords": ["sports"] },
-	{ "category": "activity", "char": "🪡", "name": "sewing_needle", "keywords": [] },
-	{ "category": "activity", "char": "🪢", "name": "knot", "keywords": [] },
-	{ "category": "activity", "char": "🕴", "name": "business_suit_levitating", "keywords": ["suit", "business", "levitate", "hover", "jump"] },
-	{ "category": "activity", "char": "🏆", "name": "trophy", "keywords": ["win", "award", "contest", "place", "ftw", "ceremony"] },
-	{ "category": "activity", "char": "🎽", "name": "running_shirt_with_sash", "keywords": ["play", "pageant"] },
-	{ "category": "activity", "char": "🏅", "name": "medal_sports", "keywords": ["award", "winning"] },
-	{ "category": "activity", "char": "🎖", "name": "medal_military", "keywords": ["award", "winning", "army"] },
-	{ "category": "activity", "char": "🥇", "name": "1st_place_medal", "keywords": ["award", "winning", "first"] },
-	{ "category": "activity", "char": "🥈", "name": "2nd_place_medal", "keywords": ["award", "second"] },
-	{ "category": "activity", "char": "🥉", "name": "3rd_place_medal", "keywords": ["award", "third"] },
-	{ "category": "activity", "char": "🎗", "name": "reminder_ribbon", "keywords": ["sports", "cause", "support", "awareness"] },
-	{ "category": "activity", "char": "🏵", "name": "rosette", "keywords": ["flower", "decoration", "military"] },
-	{ "category": "activity", "char": "🎫", "name": "ticket", "keywords": ["event", "concert", "pass"] },
-	{ "category": "activity", "char": "🎟", "name": "tickets", "keywords": ["sports", "concert", "entrance"] },
-	{ "category": "activity", "char": "🎭", "name": "performing_arts", "keywords": ["acting", "theater", "drama"] },
-	{ "category": "activity", "char": "🎨", "name": "art", "keywords": ["design", "paint", "draw", "colors"] },
-	{ "category": "activity", "char": "🎪", "name": "circus_tent", "keywords": ["festival", "carnival", "party"] },
-	{ "category": "activity", "char": "🤹‍♀️", "name": "woman_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] },
-	{ "category": "activity", "char": "🤹‍♂️", "name": "man_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] },
-	{ "category": "activity", "char": "🎤", "name": "microphone", "keywords": ["sound", "music", "PA", "sing", "talkshow"] },
-	{ "category": "activity", "char": "🎧", "name": "headphones", "keywords": ["music", "score", "gadgets"] },
-	{ "category": "activity", "char": "🎼", "name": "musical_score", "keywords": ["treble", "clef", "compose"] },
-	{ "category": "activity", "char": "🎹", "name": "musical_keyboard", "keywords": ["piano", "instrument", "compose"] },
-	{ "category": "activity", "char": "🥁", "name": "drum", "keywords": ["music", "instrument", "drumsticks", "snare"] },
-	{ "category": "activity", "char": "🎷", "name": "saxophone", "keywords": ["music", "instrument", "jazz", "blues"] },
-	{ "category": "activity", "char": "🎺", "name": "trumpet", "keywords": ["music", "brass"] },
-	{ "category": "activity", "char": "🎸", "name": "guitar", "keywords": ["music", "instrument"] },
-	{ "category": "activity", "char": "🎻", "name": "violin", "keywords": ["music", "instrument", "orchestra", "symphony"] },
-	{ "category": "activity", "char": "🪕", "name": "banjo", "keywords": ["music", "instrument"] },
-	{ "category": "activity", "char": "🪗", "name": "accordion", "keywords": ["music", "instrument"] },
-	{ "category": "activity", "char": "🪘", "name": "long_drum", "keywords": ["music", "instrument"] },
-	{ "category": "activity", "char": "🎬", "name": "clapper", "keywords": ["movie", "film", "record"] },
-	{ "category": "activity", "char": "🎮", "name": "video_game", "keywords": ["play", "console", "PS4", "controller"] },
-	{ "category": "activity", "char": "👾", "name": "space_invader", "keywords": ["game", "arcade", "play"] },
-	{ "category": "activity", "char": "🎯", "name": "dart", "keywords": ["game", "play", "bar", "target", "bullseye"] },
-	{ "category": "activity", "char": "🎲", "name": "game_die", "keywords": ["dice", "random", "tabletop", "play", "luck"] },
-	{ "category": "activity", "char": "♟️", "name": "chess_pawn", "keywords": ["expendable"] },
-	{ "category": "activity", "char": "🎰", "name": "slot_machine", "keywords": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"] },
-	{ "category": "activity", "char": "🧩", "name": "jigsaw", "keywords": ["interlocking", "puzzle", "piece"] },
-	{ "category": "activity", "char": "🎳", "name": "bowling", "keywords": ["sports", "fun", "play"] },
-	{ "category": "activity", "char": "🪄", "name": "magic_wand", "keywords": [] },
-	{ "category": "activity", "char": "🪅", "name": "pinata", "keywords": [] },
-	{ "category": "activity", "char": "🪆", "name": "nesting_dolls", "keywords": [] },
-	{ "category": "travel_and_places", "char": "🚗", "name": "red_car", "keywords": ["red", "transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚕", "name": "taxi", "keywords": ["uber", "vehicle", "cars", "transportation"] },
-	{ "category": "travel_and_places", "char": "🚙", "name": "blue_car", "keywords": ["transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚌", "name": "bus", "keywords": ["car", "vehicle", "transportation"] },
-	{ "category": "travel_and_places", "char": "🚎", "name": "trolleybus", "keywords": ["bart", "transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🏎", "name": "racing_car", "keywords": ["sports", "race", "fast", "formula", "f1"] },
-	{ "category": "travel_and_places", "char": "🚓", "name": "police_car", "keywords": ["vehicle", "cars", "transportation", "law", "legal", "enforcement"] },
-	{ "category": "travel_and_places", "char": "🚑", "name": "ambulance", "keywords": ["health", "911", "hospital"] },
-	{ "category": "travel_and_places", "char": "🚒", "name": "fire_engine", "keywords": ["transportation", "cars", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚐", "name": "minibus", "keywords": ["vehicle", "car", "transportation"] },
-	{ "category": "travel_and_places", "char": "🚚", "name": "truck", "keywords": ["cars", "transportation"] },
-	{ "category": "travel_and_places", "char": "🚛", "name": "articulated_lorry", "keywords": ["vehicle", "cars", "transportation", "express"] },
-	{ "category": "travel_and_places", "char": "🚜", "name": "tractor", "keywords": ["vehicle", "car", "farming", "agriculture"] },
-	{ "category": "travel_and_places", "char": "🛴", "name": "kick_scooter", "keywords": ["vehicle", "kick", "razor"] },
-	{ "category": "travel_and_places", "char": "🏍", "name": "motorcycle", "keywords": ["race", "sports", "fast"] },
-	{ "category": "travel_and_places", "char": "🚲", "name": "bike", "keywords": ["sports", "bicycle", "exercise", "hipster"] },
-	{ "category": "travel_and_places", "char": "🛵", "name": "motor_scooter", "keywords": ["vehicle", "vespa", "sasha"] },
-	{ "category": "travel_and_places", "char": "🦽", "name": "manual_wheelchair", "keywords": ["vehicle"] },
-	{ "category": "travel_and_places", "char": "🦼", "name": "motorized_wheelchair", "keywords": ["vehicle"] },
-	{ "category": "travel_and_places", "char": "🛺", "name": "auto_rickshaw", "keywords": ["vehicle"] },
-	{ "category": "travel_and_places", "char": "🪂", "name": "parachute", "keywords": ["vehicle"] },
-	{ "category": "travel_and_places", "char": "🚨", "name": "rotating_light", "keywords": ["police", "ambulance", "911", "emergency", "alert", "error", "pinged", "law", "legal"] },
-	{ "category": "travel_and_places", "char": "🚔", "name": "oncoming_police_car", "keywords": ["vehicle", "law", "legal", "enforcement", "911"] },
-	{ "category": "travel_and_places", "char": "🚍", "name": "oncoming_bus", "keywords": ["vehicle", "transportation"] },
-	{ "category": "travel_and_places", "char": "🚘", "name": "oncoming_automobile", "keywords": ["car", "vehicle", "transportation"] },
-	{ "category": "travel_and_places", "char": "🚖", "name": "oncoming_taxi", "keywords": ["vehicle", "cars", "uber"] },
-	{ "category": "travel_and_places", "char": "🚡", "name": "aerial_tramway", "keywords": ["transportation", "vehicle", "ski"] },
-	{ "category": "travel_and_places", "char": "🚠", "name": "mountain_cableway", "keywords": ["transportation", "vehicle", "ski"] },
-	{ "category": "travel_and_places", "char": "🚟", "name": "suspension_railway", "keywords": ["vehicle", "transportation"] },
-	{ "category": "travel_and_places", "char": "🚃", "name": "railway_car", "keywords": ["transportation", "vehicle", "train"] },
-	{ "category": "travel_and_places", "char": "🚋", "name": "train", "keywords": ["transportation", "vehicle", "carriage", "public", "travel"] },
-	{ "category": "travel_and_places", "char": "🚝", "name": "monorail", "keywords": ["transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚄", "name": "bullettrain_side", "keywords": ["transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚅", "name": "bullettrain_front", "keywords": ["transportation", "vehicle", "speed", "fast", "public", "travel"] },
-	{ "category": "travel_and_places", "char": "🚈", "name": "light_rail", "keywords": ["transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚞", "name": "mountain_railway", "keywords": ["transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚂", "name": "steam_locomotive", "keywords": ["transportation", "vehicle", "train"] },
-	{ "category": "travel_and_places", "char": "🚆", "name": "train2", "keywords": ["transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚇", "name": "metro", "keywords": ["transportation", "blue-square", "mrt", "underground", "tube"] },
-	{ "category": "travel_and_places", "char": "🚊", "name": "tram", "keywords": ["transportation", "vehicle"] },
-	{ "category": "travel_and_places", "char": "🚉", "name": "station", "keywords": ["transportation", "vehicle", "public"] },
-	{ "category": "travel_and_places", "char": "🛸", "name": "flying_saucer", "keywords": ["transportation", "vehicle", "ufo"] },
-	{ "category": "travel_and_places", "char": "🚁", "name": "helicopter", "keywords": ["transportation", "vehicle", "fly"] },
-	{ "category": "travel_and_places", "char": "🛩", "name": "small_airplane", "keywords": ["flight", "transportation", "fly", "vehicle"] },
-	{ "category": "travel_and_places", "char": "✈️", "name": "airplane", "keywords": ["vehicle", "transportation", "flight", "fly"] },
-	{ "category": "travel_and_places", "char": "🛫", "name": "flight_departure", "keywords": ["airport", "flight", "landing"] },
-	{ "category": "travel_and_places", "char": "🛬", "name": "flight_arrival", "keywords": ["airport", "flight", "boarding"] },
-	{ "category": "travel_and_places", "char": "⛵", "name": "sailboat", "keywords": ["ship", "summer", "transportation", "water", "sailing"] },
-	{ "category": "travel_and_places", "char": "🛥", "name": "motor_boat", "keywords": ["ship"] },
-	{ "category": "travel_and_places", "char": "🚤", "name": "speedboat", "keywords": ["ship", "transportation", "vehicle", "summer"] },
-	{ "category": "travel_and_places", "char": "⛴", "name": "ferry", "keywords": ["boat", "ship", "yacht"] },
-	{ "category": "travel_and_places", "char": "🛳", "name": "passenger_ship", "keywords": ["yacht", "cruise", "ferry"] },
-	{ "category": "travel_and_places", "char": "🚀", "name": "rocket", "keywords": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"] },
-	{ "category": "travel_and_places", "char": "🛰", "name": "artificial_satellite", "keywords": ["communication", "gps", "orbit", "spaceflight", "NASA", "ISS"] },
-	{ "category": "travel_and_places", "char": "🛻", "name": "pickup_truck", "keywords": ["car"] },
-	{ "category": "travel_and_places", "char": "🛼", "name": "roller_skate", "keywords": [] },
-	{ "category": "travel_and_places", "char": "💺", "name": "seat", "keywords": ["sit", "airplane", "transport", "bus", "flight", "fly"] },
-	{ "category": "travel_and_places", "char": "🛶", "name": "canoe", "keywords": ["boat", "paddle", "water", "ship"] },
-	{ "category": "travel_and_places", "char": "⚓", "name": "anchor", "keywords": ["ship", "ferry", "sea", "boat"] },
-	{ "category": "travel_and_places", "char": "🚧", "name": "construction", "keywords": ["wip", "progress", "caution", "warning"] },
-	{ "category": "travel_and_places", "char": "⛽", "name": "fuelpump", "keywords": ["gas station", "petroleum"] },
-	{ "category": "travel_and_places", "char": "🚏", "name": "busstop", "keywords": ["transportation", "wait"] },
-	{ "category": "travel_and_places", "char": "🚦", "name": "vertical_traffic_light", "keywords": ["transportation", "driving"] },
-	{ "category": "travel_and_places", "char": "🚥", "name": "traffic_light", "keywords": ["transportation", "signal"] },
-	{ "category": "travel_and_places", "char": "🏁", "name": "checkered_flag", "keywords": ["contest", "finishline", "race", "gokart"] },
-	{ "category": "travel_and_places", "char": "🚢", "name": "ship", "keywords": ["transportation", "titanic", "deploy"] },
-	{ "category": "travel_and_places", "char": "🎡", "name": "ferris_wheel", "keywords": ["photo", "carnival", "londoneye"] },
-	{ "category": "travel_and_places", "char": "🎢", "name": "roller_coaster", "keywords": ["carnival", "playground", "photo", "fun"] },
-	{ "category": "travel_and_places", "char": "🎠", "name": "carousel_horse", "keywords": ["photo", "carnival"] },
-	{ "category": "travel_and_places", "char": "🏗", "name": "building_construction", "keywords": ["wip", "working", "progress"] },
-	{ "category": "travel_and_places", "char": "🌁", "name": "foggy", "keywords": ["photo", "mountain"] },
-	{ "category": "travel_and_places", "char": "🏭", "name": "factory", "keywords": ["building", "industry", "pollution", "smoke"] },
-	{ "category": "travel_and_places", "char": "⛲", "name": "fountain", "keywords": ["photo", "summer", "water", "fresh"] },
-	{ "category": "travel_and_places", "char": "🎑", "name": "rice_scene", "keywords": ["photo", "japan", "asia", "tsukimi"] },
-	{ "category": "travel_and_places", "char": "⛰", "name": "mountain", "keywords": ["photo", "nature", "environment"] },
-	{ "category": "travel_and_places", "char": "🏔", "name": "mountain_snow", "keywords": ["photo", "nature", "environment", "winter", "cold"] },
-	{ "category": "travel_and_places", "char": "🗻", "name": "mount_fuji", "keywords": ["photo", "mountain", "nature", "japanese"] },
-	{ "category": "travel_and_places", "char": "🌋", "name": "volcano", "keywords": ["photo", "nature", "disaster"] },
-	{ "category": "travel_and_places", "char": "🗾", "name": "japan", "keywords": ["nation", "country", "japanese", "asia"] },
-	{ "category": "travel_and_places", "char": "🏕", "name": "camping", "keywords": ["photo", "outdoors", "tent"] },
-	{ "category": "travel_and_places", "char": "⛺", "name": "tent", "keywords": ["photo", "camping", "outdoors"] },
-	{ "category": "travel_and_places", "char": "🏞", "name": "national_park", "keywords": ["photo", "environment", "nature"] },
-	{ "category": "travel_and_places", "char": "🛣", "name": "motorway", "keywords": ["road", "cupertino", "interstate", "highway"] },
-	{ "category": "travel_and_places", "char": "🛤", "name": "railway_track", "keywords": ["train", "transportation"] },
-	{ "category": "travel_and_places", "char": "🌅", "name": "sunrise", "keywords": ["morning", "view", "vacation", "photo"] },
-	{ "category": "travel_and_places", "char": "🌄", "name": "sunrise_over_mountains", "keywords": ["view", "vacation", "photo"] },
-	{ "category": "travel_and_places", "char": "🏜", "name": "desert", "keywords": ["photo", "warm", "saharah"] },
-	{ "category": "travel_and_places", "char": "🏖", "name": "beach_umbrella", "keywords": ["weather", "summer", "sunny", "sand", "mojito"] },
-	{ "category": "travel_and_places", "char": "🏝", "name": "desert_island", "keywords": ["photo", "tropical", "mojito"] },
-	{ "category": "travel_and_places", "char": "🌇", "name": "city_sunrise", "keywords": ["photo", "good morning", "dawn"] },
-	{ "category": "travel_and_places", "char": "🌆", "name": "city_sunset", "keywords": ["photo", "evening", "sky", "buildings"] },
-	{ "category": "travel_and_places", "char": "🏙", "name": "cityscape", "keywords": ["photo", "night life", "urban"] },
-	{ "category": "travel_and_places", "char": "🌃", "name": "night_with_stars", "keywords": ["evening", "city", "downtown"] },
-	{ "category": "travel_and_places", "char": "🌉", "name": "bridge_at_night", "keywords": ["photo", "sanfrancisco"] },
-	{ "category": "travel_and_places", "char": "🌌", "name": "milky_way", "keywords": ["photo", "space", "stars"] },
-	{ "category": "travel_and_places", "char": "🌠", "name": "stars", "keywords": ["night", "photo"] },
-	{ "category": "travel_and_places", "char": "🎇", "name": "sparkler", "keywords": ["stars", "night", "shine"] },
-	{ "category": "travel_and_places", "char": "🎆", "name": "fireworks", "keywords": ["photo", "festival", "carnival", "congratulations"] },
-	{ "category": "travel_and_places", "char": "🌈", "name": "rainbow", "keywords": ["nature", "happy", "unicorn_face", "photo", "sky", "spring"] },
-	{ "category": "travel_and_places", "char": "🏘", "name": "houses", "keywords": ["buildings", "photo"] },
-	{ "category": "travel_and_places", "char": "🏰", "name": "european_castle", "keywords": ["building", "royalty", "history"] },
-	{ "category": "travel_and_places", "char": "🏯", "name": "japanese_castle", "keywords": ["photo", "building"] },
-	{ "category": "travel_and_places", "char": "🗼", "name": "tokyo_tower", "keywords": ["photo", "japanese"] },
-	{ "category": "travel_and_places", "char": "", "name": "shibuya_109", "keywords": ["photo", "japanese"] },
-	{ "category": "travel_and_places", "char": "🏟", "name": "stadium", "keywords": ["photo", "place", "sports", "concert", "venue"] },
-	{ "category": "travel_and_places", "char": "🗽", "name": "statue_of_liberty", "keywords": ["american", "newyork"] },
-	{ "category": "travel_and_places", "char": "🏠", "name": "house", "keywords": ["building", "home"] },
-	{ "category": "travel_and_places", "char": "🏡", "name": "house_with_garden", "keywords": ["home", "plant", "nature"] },
-	{ "category": "travel_and_places", "char": "🏚", "name": "derelict_house", "keywords": ["abandon", "evict", "broken", "building"] },
-	{ "category": "travel_and_places", "char": "🏢", "name": "office", "keywords": ["building", "bureau", "work"] },
-	{ "category": "travel_and_places", "char": "🏬", "name": "department_store", "keywords": ["building", "shopping", "mall"] },
-	{ "category": "travel_and_places", "char": "🏣", "name": "post_office", "keywords": ["building", "envelope", "communication"] },
-	{ "category": "travel_and_places", "char": "🏤", "name": "european_post_office", "keywords": ["building", "email"] },
-	{ "category": "travel_and_places", "char": "🏥", "name": "hospital", "keywords": ["building", "health", "surgery", "doctor"] },
-	{ "category": "travel_and_places", "char": "🏦", "name": "bank", "keywords": ["building", "money", "sales", "cash", "business", "enterprise"] },
-	{ "category": "travel_and_places", "char": "🏨", "name": "hotel", "keywords": ["building", "accomodation", "checkin"] },
-	{ "category": "travel_and_places", "char": "🏪", "name": "convenience_store", "keywords": ["building", "shopping", "groceries"] },
-	{ "category": "travel_and_places", "char": "🏫", "name": "school", "keywords": ["building", "student", "education", "learn", "teach"] },
-	{ "category": "travel_and_places", "char": "🏩", "name": "love_hotel", "keywords": ["like", "affection", "dating"] },
-	{ "category": "travel_and_places", "char": "💒", "name": "wedding", "keywords": ["love", "like", "affection", "couple", "marriage", "bride", "groom"] },
-	{ "category": "travel_and_places", "char": "🏛", "name": "classical_building", "keywords": ["art", "culture", "history"] },
-	{ "category": "travel_and_places", "char": "⛪", "name": "church", "keywords": ["building", "religion", "christ"] },
-	{ "category": "travel_and_places", "char": "🕌", "name": "mosque", "keywords": ["islam", "worship", "minaret"] },
-	{ "category": "travel_and_places", "char": "🕍", "name": "synagogue", "keywords": ["judaism", "worship", "temple", "jewish"] },
-	{ "category": "travel_and_places", "char": "🕋", "name": "kaaba", "keywords": ["mecca", "mosque", "islam"] },
-	{ "category": "travel_and_places", "char": "⛩", "name": "shinto_shrine", "keywords": ["temple", "japan", "kyoto"] },
-	{ "category": "travel_and_places", "char": "🛕", "name": "hindu_temple", "keywords": ["temple"] },
-
-	{ "category": "travel_and_places", "char": "🪨", "name": "rock", "keywords": [] },
-	{ "category": "travel_and_places", "char": "🪵", "name": "wood", "keywords": [] },
-	{ "category": "travel_and_places", "char": "🛖", "name": "hut", "keywords": [] },
-
-	{ "category": "objects", "char": "⌚", "name": "watch", "keywords": ["time", "accessories"] },
-	{ "category": "objects", "char": "📱", "name": "iphone", "keywords": ["technology", "apple", "gadgets", "dial"] },
-	{ "category": "objects", "char": "📲", "name": "calling", "keywords": ["iphone", "incoming"] },
-	{ "category": "objects", "char": "💻", "name": "computer", "keywords": ["technology", "laptop", "screen", "display", "monitor"] },
-	{ "category": "objects", "char": "⌨", "name": "keyboard", "keywords": ["technology", "computer", "type", "input", "text"] },
-	{ "category": "objects", "char": "🖥", "name": "desktop_computer", "keywords": ["technology", "computing", "screen"] },
-	{ "category": "objects", "char": "🖨", "name": "printer", "keywords": ["paper", "ink"] },
-	{ "category": "objects", "char": "🖱", "name": "computer_mouse", "keywords": ["click"] },
-	{ "category": "objects", "char": "🖲", "name": "trackball", "keywords": ["technology", "trackpad"] },
-	{ "category": "objects", "char": "🕹", "name": "joystick", "keywords": ["game", "play"] },
-	{ "category": "objects", "char": "🗜", "name": "clamp", "keywords": ["tool"] },
-	{ "category": "objects", "char": "💽", "name": "minidisc", "keywords": ["technology", "record", "data", "disk", "90s"] },
-	{ "category": "objects", "char": "💾", "name": "floppy_disk", "keywords": ["oldschool", "technology", "save", "90s", "80s"] },
-	{ "category": "objects", "char": "💿", "name": "cd", "keywords": ["technology", "dvd", "disk", "disc", "90s"] },
-	{ "category": "objects", "char": "📀", "name": "dvd", "keywords": ["cd", "disk", "disc"] },
-	{ "category": "objects", "char": "📼", "name": "vhs", "keywords": ["record", "video", "oldschool", "90s", "80s"] },
-	{ "category": "objects", "char": "📷", "name": "camera", "keywords": ["gadgets", "photography"] },
-	{ "category": "objects", "char": "📸", "name": "camera_flash", "keywords": ["photography", "gadgets"] },
-	{ "category": "objects", "char": "📹", "name": "video_camera", "keywords": ["film", "record"] },
-	{ "category": "objects", "char": "🎥", "name": "movie_camera", "keywords": ["film", "record"] },
-	{ "category": "objects", "char": "📽", "name": "film_projector", "keywords": ["video", "tape", "record", "movie"] },
-	{ "category": "objects", "char": "🎞", "name": "film_strip", "keywords": ["movie"] },
-	{ "category": "objects", "char": "📞", "name": "telephone_receiver", "keywords": ["technology", "communication", "dial"] },
-	{ "category": "objects", "char": "☎️", "name": "phone", "keywords": ["technology", "communication", "dial", "telephone"] },
-	{ "category": "objects", "char": "📟", "name": "pager", "keywords": ["bbcall", "oldschool", "90s"] },
-	{ "category": "objects", "char": "📠", "name": "fax", "keywords": ["communication", "technology"] },
-	{ "category": "objects", "char": "📺", "name": "tv", "keywords": ["technology", "program", "oldschool", "show", "television"] },
-	{ "category": "objects", "char": "📻", "name": "radio", "keywords": ["communication", "music", "podcast", "program"] },
-	{ "category": "objects", "char": "🎙", "name": "studio_microphone", "keywords": ["sing", "recording", "artist", "talkshow"] },
-	{ "category": "objects", "char": "🎚", "name": "level_slider", "keywords": ["scale"] },
-	{ "category": "objects", "char": "🎛", "name": "control_knobs", "keywords": ["dial"] },
-	{ "category": "objects", "char": "🧭", "name": "compass", "keywords": ["magnetic", "navigation", "orienteering"] },
-	{ "category": "objects", "char": "⏱", "name": "stopwatch", "keywords": ["time", "deadline"] },
-	{ "category": "objects", "char": "⏲", "name": "timer_clock", "keywords": ["alarm"] },
-	{ "category": "objects", "char": "⏰", "name": "alarm_clock", "keywords": ["time", "wake"] },
-	{ "category": "objects", "char": "🕰", "name": "mantelpiece_clock", "keywords": ["time"] },
-	{ "category": "objects", "char": "⏳", "name": "hourglass_flowing_sand", "keywords": ["oldschool", "time", "countdown"] },
-	{ "category": "objects", "char": "⌛", "name": "hourglass", "keywords": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"] },
-	{ "category": "objects", "char": "📡", "name": "satellite", "keywords": ["communication", "future", "radio", "space"] },
-	{ "category": "objects", "char": "🔋", "name": "battery", "keywords": ["power", "energy", "sustain"] },
-	{ "category": "objects", "char": "🔌", "name": "electric_plug", "keywords": ["charger", "power"] },
-	{ "category": "objects", "char": "💡", "name": "bulb", "keywords": ["light", "electricity", "idea"] },
-	{ "category": "objects", "char": "🔦", "name": "flashlight", "keywords": ["dark", "camping", "sight", "night"] },
-	{ "category": "objects", "char": "🕯", "name": "candle", "keywords": ["fire", "wax"] },
-	{ "category": "objects", "char": "🧯", "name": "fire_extinguisher", "keywords": ["quench"] },
-	{ "category": "objects", "char": "🗑", "name": "wastebasket", "keywords": ["bin", "trash", "rubbish", "garbage", "toss"] },
-	{ "category": "objects", "char": "🛢", "name": "oil_drum", "keywords": ["barrell"] },
-	{ "category": "objects", "char": "💸", "name": "money_with_wings", "keywords": ["dollar", "bills", "payment", "sale"] },
-	{ "category": "objects", "char": "💵", "name": "dollar", "keywords": ["money", "sales", "bill", "currency"] },
-	{ "category": "objects", "char": "💴", "name": "yen", "keywords": ["money", "sales", "japanese", "dollar", "currency"] },
-	{ "category": "objects", "char": "💶", "name": "euro", "keywords": ["money", "sales", "dollar", "currency"] },
-	{ "category": "objects", "char": "💷", "name": "pound", "keywords": ["british", "sterling", "money", "sales", "bills", "uk", "england", "currency"] },
-	{ "category": "objects", "char": "💰", "name": "moneybag", "keywords": ["dollar", "payment", "coins", "sale"] },
-	{ "category": "objects", "char": "🪙", "name": "coin", "keywords": ["dollar", "payment", "coins", "sale"] },
-	{ "category": "objects", "char": "💳", "name": "credit_card", "keywords": ["money", "sales", "dollar", "bill", "payment", "shopping"] },
-	{ "category": "objects", "char": "💎", "name": "gem", "keywords": ["blue", "ruby", "diamond", "jewelry"] },
-	{ "category": "objects", "char": "⚖", "name": "balance_scale", "keywords": ["law", "fairness", "weight"] },
-	{ "category": "objects", "char": "🧰", "name": "toolbox", "keywords": ["tools", "diy", "fix", "maintainer", "mechanic"] },
-	{ "category": "objects", "char": "🔧", "name": "wrench", "keywords": ["tools", "diy", "ikea", "fix", "maintainer"] },
-	{ "category": "objects", "char": "🔨", "name": "hammer", "keywords": ["tools", "build", "create"] },
-	{ "category": "objects", "char": "⚒", "name": "hammer_and_pick", "keywords": ["tools", "build", "create"] },
-	{ "category": "objects", "char": "🛠", "name": "hammer_and_wrench", "keywords": ["tools", "build", "create"] },
-	{ "category": "objects", "char": "⛏", "name": "pick", "keywords": ["tools", "dig"] },
-	{ "category": "objects", "char": "🪓", "name": "axe", "keywords": ["tools"] },
-	{ "category": "objects", "char": "🦯", "name": "probing_cane", "keywords": ["tools"] },
-	{ "category": "objects", "char": "🔩", "name": "nut_and_bolt", "keywords": ["handy", "tools", "fix"] },
-	{ "category": "objects", "char": "⚙", "name": "gear", "keywords": ["cog"] },
-	{ "category": "objects", "char": "🪃", "name": "boomerang", "keywords": ["tool"] },
-	{ "category": "objects", "char": "🪚", "name": "carpentry_saw", "keywords": ["tool"] },
-	{ "category": "objects", "char": "🪛", "name": "screwdriver", "keywords": ["tool"] },
-	{ "category": "objects", "char": "🪝", "name": "hook", "keywords": ["tool"] },
-	{ "category": "objects", "char": "🪜", "name": "ladder", "keywords": ["tool"] },
-	{ "category": "objects", "char": "🧱", "name": "brick", "keywords": ["bricks"] },
-	{ "category": "objects", "char": "⛓", "name": "chains", "keywords": ["lock", "arrest"] },
-	{ "category": "objects", "char": "🧲", "name": "magnet", "keywords": ["attraction", "magnetic"] },
-	{ "category": "objects", "char": "🔫", "name": "gun", "keywords": ["violence", "weapon", "pistol", "revolver"] },
-	{ "category": "objects", "char": "💣", "name": "bomb", "keywords": ["boom", "explode", "explosion", "terrorism"] },
-	{ "category": "objects", "char": "🧨", "name": "firecracker", "keywords": ["dynamite", "boom", "explode", "explosion", "explosive"] },
-	{ "category": "objects", "char": "🔪", "name": "hocho", "keywords": ["knife", "blade", "cutlery", "kitchen", "weapon"] },
-	{ "category": "objects", "char": "🗡", "name": "dagger", "keywords": ["weapon"] },
-	{ "category": "objects", "char": "⚔", "name": "crossed_swords", "keywords": ["weapon"] },
-	{ "category": "objects", "char": "🛡", "name": "shield", "keywords": ["protection", "security"] },
-	{ "category": "objects", "char": "🚬", "name": "smoking", "keywords": ["kills", "tobacco", "cigarette", "joint", "smoke"] },
-	{ "category": "objects", "char": "☠", "name": "skull_and_crossbones", "keywords": ["poison", "danger", "deadly", "scary", "death", "pirate", "evil"] },
-	{ "category": "objects", "char": "⚰", "name": "coffin", "keywords": ["vampire", "dead", "die", "death", "rip", "graveyard", "cemetery", "casket", "funeral", "box"] },
-	{ "category": "objects", "char": "⚱", "name": "funeral_urn", "keywords": ["dead", "die", "death", "rip", "ashes"] },
-	{ "category": "objects", "char": "🏺", "name": "amphora", "keywords": ["vase", "jar"] },
-	{ "category": "objects", "char": "🔮", "name": "crystal_ball", "keywords": ["disco", "party", "magic", "circus", "fortune_teller"] },
-	{ "category": "objects", "char": "📿", "name": "prayer_beads", "keywords": ["dhikr", "religious"] },
-	{ "category": "objects", "char": "🧿", "name": "nazar_amulet", "keywords": ["bead", "charm"] },
-	{ "category": "objects", "char": "💈", "name": "barber", "keywords": ["hair", "salon", "style"] },
-	{ "category": "objects", "char": "⚗", "name": "alembic", "keywords": ["distilling", "science", "experiment", "chemistry"] },
-	{ "category": "objects", "char": "🔭", "name": "telescope", "keywords": ["stars", "space", "zoom", "science", "astronomy"] },
-	{ "category": "objects", "char": "🔬", "name": "microscope", "keywords": ["laboratory", "experiment", "zoomin", "science", "study"] },
-	{ "category": "objects", "char": "🕳", "name": "hole", "keywords": ["embarrassing"] },
-	{ "category": "objects", "char": "💊", "name": "pill", "keywords": ["health", "medicine", "doctor", "pharmacy", "drug"] },
-	{ "category": "objects", "char": "💉", "name": "syringe", "keywords": ["health", "hospital", "drugs", "blood", "medicine", "needle", "doctor", "nurse"] },
-	{ "category": "objects", "char": "🩸", "name": "drop_of_blood", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
-	{ "category": "objects", "char": "🩹", "name": "adhesive_bandage", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
-	{ "category": "objects", "char": "🩺", "name": "stethoscope", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
-	{ "category": "objects", "char": "🪒", "name": "razor", "keywords": ["health"] },
-	{ "category": "objects", "char": "🧬", "name": "dna", "keywords": ["biologist", "genetics", "life"] },
-	{ "category": "objects", "char": "🧫", "name": "petri_dish", "keywords": ["bacteria", "biology", "culture", "lab"] },
-	{ "category": "objects", "char": "🧪", "name": "test_tube", "keywords": ["chemistry", "experiment", "lab", "science"] },
-	{ "category": "objects", "char": "🌡", "name": "thermometer", "keywords": ["weather", "temperature", "hot", "cold"] },
-	{ "category": "objects", "char": "🧹", "name": "broom", "keywords": ["cleaning", "sweeping", "witch"] },
-	{ "category": "objects", "char": "🧺", "name": "basket", "keywords": ["laundry"] },
-	{ "category": "objects", "char": "🧻", "name": "toilet_paper", "keywords": ["roll"] },
-	{ "category": "objects", "char": "🏷", "name": "label", "keywords": ["sale", "tag"] },
-	{ "category": "objects", "char": "🔖", "name": "bookmark", "keywords": ["favorite", "label", "save"] },
-	{ "category": "objects", "char": "🚽", "name": "toilet", "keywords": ["restroom", "wc", "washroom", "bathroom", "potty"] },
-	{ "category": "objects", "char": "🚿", "name": "shower", "keywords": ["clean", "water", "bathroom"] },
-	{ "category": "objects", "char": "🛁", "name": "bathtub", "keywords": ["clean", "shower", "bathroom"] },
-	{ "category": "objects", "char": "🧼", "name": "soap", "keywords": ["bar", "bathing", "cleaning", "lather"] },
-	{ "category": "objects", "char": "🧽", "name": "sponge", "keywords": ["absorbing", "cleaning", "porous"] },
-	{ "category": "objects", "char": "🧴", "name": "lotion_bottle", "keywords": ["moisturizer", "sunscreen"] },
-	{ "category": "objects", "char": "🔑", "name": "key", "keywords": ["lock", "door", "password"] },
-	{ "category": "objects", "char": "🗝", "name": "old_key", "keywords": ["lock", "door", "password"] },
-	{ "category": "objects", "char": "🛋", "name": "couch_and_lamp", "keywords": ["read", "chill"] },
-	{ "category": "objects", "char": "🪔", "name": "diya_Lamp", "keywords": ["light", "oil"] },
-	{ "category": "objects", "char": "🛌", "name": "sleeping_bed", "keywords": ["bed", "rest"] },
-	{ "category": "objects", "char": "🛏", "name": "bed", "keywords": ["sleep", "rest"] },
-	{ "category": "objects", "char": "🚪", "name": "door", "keywords": ["house", "entry", "exit"] },
-	{ "category": "objects", "char": "🪑", "name": "chair", "keywords": ["house", "desk"] },
-	{ "category": "objects", "char": "🛎", "name": "bellhop_bell", "keywords": ["service"] },
-	{ "category": "objects", "char": "🧸", "name": "teddy_bear", "keywords": ["plush", "stuffed"] },
-	{ "category": "objects", "char": "🖼", "name": "framed_picture", "keywords": ["photography"] },
-	{ "category": "objects", "char": "🗺", "name": "world_map", "keywords": ["location", "direction"] },
-	{ "category": "objects", "char": "🛗", "name": "elevator", "keywords": ["household"] },
-	{ "category": "objects", "char": "🪞", "name": "mirror", "keywords": ["household"] },
-	{ "category": "objects", "char": "🪟", "name": "window", "keywords": ["household"] },
-	{ "category": "objects", "char": "🪠", "name": "plunger", "keywords": ["household"] },
-	{ "category": "objects", "char": "🪤", "name": "mouse_trap", "keywords": ["household"] },
-	{ "category": "objects", "char": "🪣", "name": "bucket", "keywords": ["household"] },
-	{ "category": "objects", "char": "🪥", "name": "toothbrush", "keywords": ["household"] },
-	{ "category": "objects", "char": "⛱", "name": "parasol_on_ground", "keywords": ["weather", "summer"] },
-	{ "category": "objects", "char": "🗿", "name": "moyai", "keywords": ["rock", "easter island", "moai"] },
-	{ "category": "objects", "char": "🛍", "name": "shopping", "keywords": ["mall", "buy", "purchase"] },
-	{ "category": "objects", "char": "🛒", "name": "shopping_cart", "keywords": ["trolley"] },
-	{ "category": "objects", "char": "🎈", "name": "balloon", "keywords": ["party", "celebration", "birthday", "circus"] },
-	{ "category": "objects", "char": "🎏", "name": "flags", "keywords": ["fish", "japanese", "koinobori", "carp", "banner"] },
-	{ "category": "objects", "char": "🎀", "name": "ribbon", "keywords": ["decoration", "pink", "girl", "bowtie"] },
-	{ "category": "objects", "char": "🎁", "name": "gift", "keywords": ["present", "birthday", "christmas", "xmas"] },
-	{ "category": "objects", "char": "🎊", "name": "confetti_ball", "keywords": ["festival", "party", "birthday", "circus"] },
-	{ "category": "objects", "char": "🎉", "name": "tada", "keywords": ["party", "congratulations", "birthday", "magic", "circus", "celebration"] },
-	{ "category": "objects", "char": "🎎", "name": "dolls", "keywords": ["japanese", "toy", "kimono"] },
-	{ "category": "objects", "char": "🎐", "name": "wind_chime", "keywords": ["nature", "ding", "spring", "bell"] },
-	{ "category": "objects", "char": "🎌", "name": "crossed_flags", "keywords": ["japanese", "nation", "country", "border"] },
-	{ "category": "objects", "char": "🏮", "name": "izakaya_lantern", "keywords": ["light", "paper", "halloween", "spooky"] },
-	{ "category": "objects", "char": "🧧", "name": "red_envelope", "keywords": ["gift"] },
-	{ "category": "objects", "char": "✉️", "name": "email", "keywords": ["letter", "postal", "inbox", "communication"] },
-	{ "category": "objects", "char": "📩", "name": "envelope_with_arrow", "keywords": ["email", "communication"] },
-	{ "category": "objects", "char": "📨", "name": "incoming_envelope", "keywords": ["email", "inbox"] },
-	{ "category": "objects", "char": "📧", "name": "e-mail", "keywords": ["communication", "inbox"] },
-	{ "category": "objects", "char": "💌", "name": "love_letter", "keywords": ["email", "like", "affection", "envelope", "valentines"] },
-	{ "category": "objects", "char": "📮", "name": "postbox", "keywords": ["email", "letter", "envelope"] },
-	{ "category": "objects", "char": "📪", "name": "mailbox_closed", "keywords": ["email", "communication", "inbox"] },
-	{ "category": "objects", "char": "📫", "name": "mailbox", "keywords": ["email", "inbox", "communication"] },
-	{ "category": "objects", "char": "📬", "name": "mailbox_with_mail", "keywords": ["email", "inbox", "communication"] },
-	{ "category": "objects", "char": "📭", "name": "mailbox_with_no_mail", "keywords": ["email", "inbox"] },
-	{ "category": "objects", "char": "📦", "name": "package", "keywords": ["mail", "gift", "cardboard", "box", "moving"] },
-	{ "category": "objects", "char": "📯", "name": "postal_horn", "keywords": ["instrument", "music"] },
-	{ "category": "objects", "char": "📥", "name": "inbox_tray", "keywords": ["email", "documents"] },
-	{ "category": "objects", "char": "📤", "name": "outbox_tray", "keywords": ["inbox", "email"] },
-	{ "category": "objects", "char": "📜", "name": "scroll", "keywords": ["documents", "ancient", "history", "paper"] },
-	{ "category": "objects", "char": "📃", "name": "page_with_curl", "keywords": ["documents", "office", "paper"] },
-	{ "category": "objects", "char": "📑", "name": "bookmark_tabs", "keywords": ["favorite", "save", "order", "tidy"] },
-	{ "category": "objects", "char": "🧾", "name": "receipt", "keywords": ["accounting", "expenses"] },
-	{ "category": "objects", "char": "📊", "name": "bar_chart", "keywords": ["graph", "presentation", "stats"] },
-	{ "category": "objects", "char": "📈", "name": "chart_with_upwards_trend", "keywords": ["graph", "presentation", "stats", "recovery", "business", "economics", "money", "sales", "good", "success"] },
-	{ "category": "objects", "char": "📉", "name": "chart_with_downwards_trend", "keywords": ["graph", "presentation", "stats", "recession", "business", "economics", "money", "sales", "bad", "failure"] },
-	{ "category": "objects", "char": "📄", "name": "page_facing_up", "keywords": ["documents", "office", "paper", "information"] },
-	{ "category": "objects", "char": "📅", "name": "date", "keywords": ["calendar", "schedule"] },
-	{ "category": "objects", "char": "📆", "name": "calendar", "keywords": ["schedule", "date", "planning"] },
-	{ "category": "objects", "char": "🗓", "name": "spiral_calendar", "keywords": ["date", "schedule", "planning"] },
-	{ "category": "objects", "char": "📇", "name": "card_index", "keywords": ["business", "stationery"] },
-	{ "category": "objects", "char": "🗃", "name": "card_file_box", "keywords": ["business", "stationery"] },
-	{ "category": "objects", "char": "🗳", "name": "ballot_box", "keywords": ["election", "vote"] },
-	{ "category": "objects", "char": "🗄", "name": "file_cabinet", "keywords": ["filing", "organizing"] },
-	{ "category": "objects", "char": "📋", "name": "clipboard", "keywords": ["stationery", "documents"] },
-	{ "category": "objects", "char": "🗒", "name": "spiral_notepad", "keywords": ["memo", "stationery"] },
-	{ "category": "objects", "char": "📁", "name": "file_folder", "keywords": ["documents", "business", "office"] },
-	{ "category": "objects", "char": "📂", "name": "open_file_folder", "keywords": ["documents", "load"] },
-	{ "category": "objects", "char": "🗂", "name": "card_index_dividers", "keywords": ["organizing", "business", "stationery"] },
-	{ "category": "objects", "char": "🗞", "name": "newspaper_roll", "keywords": ["press", "headline"] },
-	{ "category": "objects", "char": "📰", "name": "newspaper", "keywords": ["press", "headline"] },
-	{ "category": "objects", "char": "📓", "name": "notebook", "keywords": ["stationery", "record", "notes", "paper", "study"] },
-	{ "category": "objects", "char": "📕", "name": "closed_book", "keywords": ["read", "library", "knowledge", "textbook", "learn"] },
-	{ "category": "objects", "char": "📗", "name": "green_book", "keywords": ["read", "library", "knowledge", "study"] },
-	{ "category": "objects", "char": "📘", "name": "blue_book", "keywords": ["read", "library", "knowledge", "learn", "study"] },
-	{ "category": "objects", "char": "📙", "name": "orange_book", "keywords": ["read", "library", "knowledge", "textbook", "study"] },
-	{ "category": "objects", "char": "📔", "name": "notebook_with_decorative_cover", "keywords": ["classroom", "notes", "record", "paper", "study"] },
-	{ "category": "objects", "char": "📒", "name": "ledger", "keywords": ["notes", "paper"] },
-	{ "category": "objects", "char": "📚", "name": "books", "keywords": ["literature", "library", "study"] },
-	{ "category": "objects", "char": "📖", "name": "open_book", "keywords": ["book", "read", "library", "knowledge", "literature", "learn", "study"] },
-	{ "category": "objects", "char": "🧷", "name": "safety_pin", "keywords": ["diaper"] },
-	{ "category": "objects", "char": "🔗", "name": "link", "keywords": ["rings", "url"] },
-	{ "category": "objects", "char": "📎", "name": "paperclip", "keywords": ["documents", "stationery"] },
-	{ "category": "objects", "char": "🖇", "name": "paperclips", "keywords": ["documents", "stationery"] },
-	{ "category": "objects", "char": "✂️", "name": "scissors", "keywords": ["stationery", "cut"] },
-	{ "category": "objects", "char": "📐", "name": "triangular_ruler", "keywords": ["stationery", "math", "architect", "sketch"] },
-	{ "category": "objects", "char": "📏", "name": "straight_ruler", "keywords": ["stationery", "calculate", "length", "math", "school", "drawing", "architect", "sketch"] },
-	{ "category": "objects", "char": "🧮", "name": "abacus", "keywords": ["calculation"] },
-	{ "category": "objects", "char": "📌", "name": "pushpin", "keywords": ["stationery", "mark", "here"] },
-	{ "category": "objects", "char": "📍", "name": "round_pushpin", "keywords": ["stationery", "location", "map", "here"] },
-	{ "category": "objects", "char": "🚩", "name": "triangular_flag_on_post", "keywords": ["mark", "milestone", "place"] },
-	{ "category": "objects", "char": "🏳", "name": "white_flag", "keywords": ["losing", "loser", "lost", "surrender", "give up", "fail"] },
-	{ "category": "objects", "char": "🏴", "name": "black_flag", "keywords": ["pirate"] },
-	{ "category": "objects", "char": "🏳️‍🌈", "name": "rainbow_flag", "keywords": ["flag", "rainbow", "pride", "gay", "lgbt", "glbt", "queer", "homosexual", "lesbian", "bisexual", "transgender"] },
-	{ "category": "objects", "char": "🏳️‍⚧️", "name": "transgender_flag", "keywords": ["flag", "transgender"] },
-	{ "category": "objects", "char": "🔐", "name": "closed_lock_with_key", "keywords": ["security", "privacy"] },
-	{ "category": "objects", "char": "🔒", "name": "lock", "keywords": ["security", "password", "padlock"] },
-	{ "category": "objects", "char": "🔓", "name": "unlock", "keywords": ["privacy", "security"] },
-	{ "category": "objects", "char": "🔏", "name": "lock_with_ink_pen", "keywords": ["security", "secret"] },
-	{ "category": "objects", "char": "🖊", "name": "pen", "keywords": ["stationery", "writing", "write"] },
-	{ "category": "objects", "char": "🖋", "name": "fountain_pen", "keywords": ["stationery", "writing", "write"] },
-	{ "category": "objects", "char": "✒️", "name": "black_nib", "keywords": ["pen", "stationery", "writing", "write"] },
-	{ "category": "objects", "char": "📝", "name": "memo", "keywords": ["write", "documents", "stationery", "pencil", "paper", "writing", "legal", "exam", "quiz", "test", "study", "compose"] },
-	{ "category": "objects", "char": "✏️", "name": "pencil2", "keywords": ["stationery", "write", "paper", "writing", "school", "study"] },
-	{ "category": "objects", "char": "🖍", "name": "crayon", "keywords": ["drawing", "creativity"] },
-	{ "category": "objects", "char": "🖌", "name": "paintbrush", "keywords": ["drawing", "creativity", "art"] },
-	{ "category": "objects", "char": "🔍", "name": "mag", "keywords": ["search", "zoom", "find", "detective"] },
-	{ "category": "objects", "char": "🔎", "name": "mag_right", "keywords": ["search", "zoom", "find", "detective"] },
-	{ "category": "objects", "char": "🪦", "name": "headstone", "keywords": [] },
-	{ "category": "objects", "char": "🪧", "name": "placard", "keywords": [] },
-	{ "category": "symbols", "char": "💯", "name": "100", "keywords": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"] },
-	{ "category": "symbols", "char": "🔢", "name": "1234", "keywords": ["numbers", "blue-square"] },
-	{ "category": "symbols", "char": "❤️", "name": "heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "🧡", "name": "orange_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "💛", "name": "yellow_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "💚", "name": "green_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "💙", "name": "blue_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "💜", "name": "purple_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "🤎", "name": "brown_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "🖤", "name": "black_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "🤍", "name": "white_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "💔", "name": "broken_heart", "keywords": ["sad", "sorry", "break", "heart", "heartbreak"] },
-	{ "category": "symbols", "char": "❣", "name": "heavy_heart_exclamation", "keywords": ["decoration", "love"] },
-	{ "category": "symbols", "char": "💕", "name": "two_hearts", "keywords": ["love", "like", "affection", "valentines", "heart"] },
-	{ "category": "symbols", "char": "💞", "name": "revolving_hearts", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "💓", "name": "heartbeat", "keywords": ["love", "like", "affection", "valentines", "pink", "heart"] },
-	{ "category": "symbols", "char": "💗", "name": "heartpulse", "keywords": ["like", "love", "affection", "valentines", "pink"] },
-	{ "category": "symbols", "char": "💖", "name": "sparkling_heart", "keywords": ["love", "like", "affection", "valentines"] },
-	{ "category": "symbols", "char": "💘", "name": "cupid", "keywords": ["love", "like", "heart", "affection", "valentines"] },
-	{ "category": "symbols", "char": "💝", "name": "gift_heart", "keywords": ["love", "valentines"] },
-	{ "category": "symbols", "char": "💟", "name": "heart_decoration", "keywords": ["purple-square", "love", "like"] },
-	{ "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83D\uDD25", "name": "heart_on_fire", "keywords": [] },
-	{ "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83E\uDE79", "name": "mending_heart", "keywords": [] },
-	{ "category": "symbols", "char": "☮", "name": "peace_symbol", "keywords": ["hippie"] },
-	{ "category": "symbols", "char": "✝", "name": "latin_cross", "keywords": ["christianity"] },
-	{ "category": "symbols", "char": "☪", "name": "star_and_crescent", "keywords": ["islam"] },
-	{ "category": "symbols", "char": "🕉", "name": "om", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] },
-	{ "category": "symbols", "char": "☸", "name": "wheel_of_dharma", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] },
-	{ "category": "symbols", "char": "✡", "name": "star_of_david", "keywords": ["judaism"] },
-	{ "category": "symbols", "char": "🔯", "name": "six_pointed_star", "keywords": ["purple-square", "religion", "jewish", "hexagram"] },
-	{ "category": "symbols", "char": "🕎", "name": "menorah", "keywords": ["hanukkah", "candles", "jewish"] },
-	{ "category": "symbols", "char": "☯", "name": "yin_yang", "keywords": ["balance"] },
-	{ "category": "symbols", "char": "☦", "name": "orthodox_cross", "keywords": ["suppedaneum", "religion"] },
-	{ "category": "symbols", "char": "🛐", "name": "place_of_worship", "keywords": ["religion", "church", "temple", "prayer"] },
-	{ "category": "symbols", "char": "⛎", "name": "ophiuchus", "keywords": ["sign", "purple-square", "constellation", "astrology"] },
-	{ "category": "symbols", "char": "♈", "name": "aries", "keywords": ["sign", "purple-square", "zodiac", "astrology"] },
-	{ "category": "symbols", "char": "♉", "name": "taurus", "keywords": ["purple-square", "sign", "zodiac", "astrology"] },
-	{ "category": "symbols", "char": "♊", "name": "gemini", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
-	{ "category": "symbols", "char": "♋", "name": "cancer", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
-	{ "category": "symbols", "char": "♌", "name": "leo", "keywords": ["sign", "purple-square", "zodiac", "astrology"] },
-	{ "category": "symbols", "char": "♍", "name": "virgo", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
-	{ "category": "symbols", "char": "♎", "name": "libra", "keywords": ["sign", "purple-square", "zodiac", "astrology"] },
-	{ "category": "symbols", "char": "♏", "name": "scorpius", "keywords": ["sign", "zodiac", "purple-square", "astrology", "scorpio"] },
-	{ "category": "symbols", "char": "♐", "name": "sagittarius", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
-	{ "category": "symbols", "char": "♑", "name": "capricorn", "keywords": ["sign", "zodiac", "purple-square", "astrology"] },
-	{ "category": "symbols", "char": "♒", "name": "aquarius", "keywords": ["sign", "purple-square", "zodiac", "astrology"] },
-	{ "category": "symbols", "char": "♓", "name": "pisces", "keywords": ["purple-square", "sign", "zodiac", "astrology"] },
-	{ "category": "symbols", "char": "🆔", "name": "id", "keywords": ["purple-square", "words"] },
-	{ "category": "symbols", "char": "⚛", "name": "atom_symbol", "keywords": ["science", "physics", "chemistry"] },
-	{ "category": "symbols", "char": "⚧️", "name": "transgender_symbol", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] },
-	{ "category": "symbols", "char": "🈳", "name": "u7a7a", "keywords": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square", "aki"] },
-	{ "category": "symbols", "char": "🈹", "name": "u5272", "keywords": ["cut", "divide", "chinese", "kanji", "pink-square", "waribiki"] },
-	{ "category": "symbols", "char": "☢", "name": "radioactive", "keywords": ["nuclear", "danger"] },
-	{ "category": "symbols", "char": "☣", "name": "biohazard", "keywords": ["danger"] },
-	{ "category": "symbols", "char": "📴", "name": "mobile_phone_off", "keywords": ["mute", "orange-square", "silence", "quiet"] },
-	{ "category": "symbols", "char": "📳", "name": "vibration_mode", "keywords": ["orange-square", "phone"] },
-	{ "category": "symbols", "char": "🈶", "name": "u6709", "keywords": ["orange-square", "chinese", "have", "kanji", "ari"] },
-	{ "category": "symbols", "char": "🈚", "name": "u7121", "keywords": ["nothing", "chinese", "kanji", "japanese", "orange-square", "nashi"] },
-	{ "category": "symbols", "char": "🈸", "name": "u7533", "keywords": ["chinese", "japanese", "kanji", "orange-square", "moushikomi"] },
-	{ "category": "symbols", "char": "🈺", "name": "u55b6", "keywords": ["japanese", "opening hours", "orange-square", "eigyo"] },
-	{ "category": "symbols", "char": "🈷️", "name": "u6708", "keywords": ["chinese", "month", "moon", "japanese", "orange-square", "kanji", "tsuki", "tsukigime", "getsugaku"] },
-	{ "category": "symbols", "char": "✴️", "name": "eight_pointed_black_star", "keywords": ["orange-square", "shape", "polygon"] },
-	{ "category": "symbols", "char": "🆚", "name": "vs", "keywords": ["words", "orange-square"] },
-	{ "category": "symbols", "char": "🉑", "name": "accept", "keywords": ["ok", "good", "chinese", "kanji", "agree", "yes", "orange-circle"] },
-	{ "category": "symbols", "char": "💮", "name": "white_flower", "keywords": ["japanese", "spring"] },
-	{ "category": "symbols", "char": "🉐", "name": "ideograph_advantage", "keywords": ["chinese", "kanji", "obtain", "get", "circle"] },
-	{ "category": "symbols", "char": "㊙️", "name": "secret", "keywords": ["privacy", "chinese", "sshh", "kanji", "red-circle"] },
-	{ "category": "symbols", "char": "㊗️", "name": "congratulations", "keywords": ["chinese", "kanji", "japanese", "red-circle"] },
-	{ "category": "symbols", "char": "🈴", "name": "u5408", "keywords": ["japanese", "chinese", "join", "kanji", "red-square", "goukaku", "pass"] },
-	{ "category": "symbols", "char": "🈵", "name": "u6e80", "keywords": ["full", "chinese", "japanese", "red-square", "kanji", "man"] },
-	{ "category": "symbols", "char": "🈲", "name": "u7981", "keywords": ["kanji", "japanese", "chinese", "forbidden", "limit", "restricted", "red-square", "kinshi"] },
-	{ "category": "symbols", "char": "🅰️", "name": "a", "keywords": ["red-square", "alphabet", "letter"] },
-	{ "category": "symbols", "char": "🅱️", "name": "b", "keywords": ["red-square", "alphabet", "letter"] },
-	{ "category": "symbols", "char": "🆎", "name": "ab", "keywords": ["red-square", "alphabet"] },
-	{ "category": "symbols", "char": "🆑", "name": "cl", "keywords": ["alphabet", "words", "red-square"] },
-	{ "category": "symbols", "char": "🅾️", "name": "o2", "keywords": ["alphabet", "red-square", "letter"] },
-	{ "category": "symbols", "char": "🆘", "name": "sos", "keywords": ["help", "red-square", "words", "emergency", "911"] },
-	{ "category": "symbols", "char": "⛔", "name": "no_entry", "keywords": ["limit", "security", "privacy", "bad", "denied", "stop", "circle"] },
-	{ "category": "symbols", "char": "📛", "name": "name_badge", "keywords": ["fire", "forbid"] },
-	{ "category": "symbols", "char": "🚫", "name": "no_entry_sign", "keywords": ["forbid", "stop", "limit", "denied", "disallow", "circle"] },
-	{ "category": "symbols", "char": "❌", "name": "x", "keywords": ["no", "delete", "remove", "cancel", "red"] },
-	{ "category": "symbols", "char": "⭕", "name": "o", "keywords": ["circle", "round"] },
-	{ "category": "symbols", "char": "🛑", "name": "stop_sign", "keywords": ["stop"] },
-	{ "category": "symbols", "char": "💢", "name": "anger", "keywords": ["angry", "mad"] },
-	{ "category": "symbols", "char": "♨️", "name": "hotsprings", "keywords": ["bath", "warm", "relax"] },
-	{ "category": "symbols", "char": "🚷", "name": "no_pedestrians", "keywords": ["rules", "crossing", "walking", "circle"] },
-	{ "category": "symbols", "char": "🚯", "name": "do_not_litter", "keywords": ["trash", "bin", "garbage", "circle"] },
-	{ "category": "symbols", "char": "🚳", "name": "no_bicycles", "keywords": ["cyclist", "prohibited", "circle"] },
-	{ "category": "symbols", "char": "🚱", "name": "non-potable_water", "keywords": ["drink", "faucet", "tap", "circle"] },
-	{ "category": "symbols", "char": "🔞", "name": "underage", "keywords": ["18", "drink", "pub", "night", "minor", "circle"] },
-	{ "category": "symbols", "char": "📵", "name": "no_mobile_phones", "keywords": ["iphone", "mute", "circle"] },
-	{ "category": "symbols", "char": "❗", "name": "exclamation", "keywords": ["heavy_exclamation_mark", "danger", "surprise", "punctuation", "wow", "warning"] },
-	{ "category": "symbols", "char": "❕", "name": "grey_exclamation", "keywords": ["surprise", "punctuation", "gray", "wow", "warning"] },
-	{ "category": "symbols", "char": "❓", "name": "question", "keywords": ["doubt", "confused"] },
-	{ "category": "symbols", "char": "❔", "name": "grey_question", "keywords": ["doubts", "gray", "huh", "confused"] },
-	{ "category": "symbols", "char": "‼️", "name": "bangbang", "keywords": ["exclamation", "surprise"] },
-	{ "category": "symbols", "char": "⁉️", "name": "interrobang", "keywords": ["wat", "punctuation", "surprise"] },
-	{ "category": "symbols", "char": "🔅", "name": "low_brightness", "keywords": ["sun", "afternoon", "warm", "summer"] },
-	{ "category": "symbols", "char": "🔆", "name": "high_brightness", "keywords": ["sun", "light"] },
-	{ "category": "symbols", "char": "🔱", "name": "trident", "keywords": ["weapon", "spear"] },
-	{ "category": "symbols", "char": "⚜", "name": "fleur_de_lis", "keywords": ["decorative", "scout"] },
-	{ "category": "symbols", "char": "〽️", "name": "part_alternation_mark", "keywords": ["graph", "presentation", "stats", "business", "economics", "bad"] },
-	{ "category": "symbols", "char": "⚠️", "name": "warning", "keywords": ["exclamation", "wip", "alert", "error", "problem", "issue"] },
-	{ "category": "symbols", "char": "🚸", "name": "children_crossing", "keywords": ["school", "warning", "danger", "sign", "driving", "yellow-diamond"] },
-	{ "category": "symbols", "char": "🔰", "name": "beginner", "keywords": ["badge", "shield"] },
-	{ "category": "symbols", "char": "♻️", "name": "recycle", "keywords": ["arrow", "environment", "garbage", "trash"] },
-	{ "category": "symbols", "char": "🈯", "name": "u6307", "keywords": ["chinese", "point", "green-square", "kanji", "reserved", "shiteiseki"] },
-	{ "category": "symbols", "char": "💹", "name": "chart", "keywords": ["green-square", "graph", "presentation", "stats"] },
-	{ "category": "symbols", "char": "❇️", "name": "sparkle", "keywords": ["stars", "green-square", "awesome", "good", "fireworks"] },
-	{ "category": "symbols", "char": "✳️", "name": "eight_spoked_asterisk", "keywords": ["star", "sparkle", "green-square"] },
-	{ "category": "symbols", "char": "❎", "name": "negative_squared_cross_mark", "keywords": ["x", "green-square", "no", "deny"] },
-	{ "category": "symbols", "char": "✅", "name": "white_check_mark", "keywords": ["green-square", "ok", "agree", "vote", "election", "answer", "tick"] },
-	{ "category": "symbols", "char": "💠", "name": "diamond_shape_with_a_dot_inside", "keywords": ["jewel", "blue", "gem", "crystal", "fancy"] },
-	{ "category": "symbols", "char": "🌀", "name": "cyclone", "keywords": ["weather", "swirl", "blue", "cloud", "vortex", "spiral", "whirlpool", "spin", "tornado", "hurricane", "typhoon"] },
-	{ "category": "symbols", "char": "➿", "name": "loop", "keywords": ["tape", "cassette"] },
-	{ "category": "symbols", "char": "🌐", "name": "globe_with_meridians", "keywords": ["earth", "international", "world", "internet", "interweb", "i18n"] },
-	{ "category": "symbols", "char": "Ⓜ️", "name": "m", "keywords": ["alphabet", "blue-circle", "letter"] },
-	{ "category": "symbols", "char": "🏧", "name": "atm", "keywords": ["money", "sales", "cash", "blue-square", "payment", "bank"] },
-	{ "category": "symbols", "char": "🈂️", "name": "sa", "keywords": ["japanese", "blue-square", "katakana"] },
-	{ "category": "symbols", "char": "🛂", "name": "passport_control", "keywords": ["custom", "blue-square"] },
-	{ "category": "symbols", "char": "🛃", "name": "customs", "keywords": ["passport", "border", "blue-square"] },
-	{ "category": "symbols", "char": "🛄", "name": "baggage_claim", "keywords": ["blue-square", "airport", "transport"] },
-	{ "category": "symbols", "char": "🛅", "name": "left_luggage", "keywords": ["blue-square", "travel"] },
-	{ "category": "symbols", "char": "♿", "name": "wheelchair", "keywords": ["blue-square", "disabled", "a11y", "accessibility"] },
-	{ "category": "symbols", "char": "🚭", "name": "no_smoking", "keywords": ["cigarette", "blue-square", "smell", "smoke"] },
-	{ "category": "symbols", "char": "🚾", "name": "wc", "keywords": ["toilet", "restroom", "blue-square"] },
-	{ "category": "symbols", "char": "🅿️", "name": "parking", "keywords": ["cars", "blue-square", "alphabet", "letter"] },
-	{ "category": "symbols", "char": "🚰", "name": "potable_water", "keywords": ["blue-square", "liquid", "restroom", "cleaning", "faucet"] },
-	{ "category": "symbols", "char": "🚹", "name": "mens", "keywords": ["toilet", "restroom", "wc", "blue-square", "gender", "male"] },
-	{ "category": "symbols", "char": "🚺", "name": "womens", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] },
-	{ "category": "symbols", "char": "🚼", "name": "baby_symbol", "keywords": ["orange-square", "child"] },
-	{ "category": "symbols", "char": "🚻", "name": "restroom", "keywords": ["blue-square", "toilet", "refresh", "wc", "gender"] },
-	{ "category": "symbols", "char": "🚮", "name": "put_litter_in_its_place", "keywords": ["blue-square", "sign", "human", "info"] },
-	{ "category": "symbols", "char": "🎦", "name": "cinema", "keywords": ["blue-square", "record", "film", "movie", "curtain", "stage", "theater"] },
-	{ "category": "symbols", "char": "📶", "name": "signal_strength", "keywords": ["blue-square", "reception", "phone", "internet", "connection", "wifi", "bluetooth", "bars"] },
-	{ "category": "symbols", "char": "🈁", "name": "koko", "keywords": ["blue-square", "here", "katakana", "japanese", "destination"] },
-	{ "category": "symbols", "char": "🆖", "name": "ng", "keywords": ["blue-square", "words", "shape", "icon"] },
-	{ "category": "symbols", "char": "🆗", "name": "ok", "keywords": ["good", "agree", "yes", "blue-square"] },
-	{ "category": "symbols", "char": "🆙", "name": "up", "keywords": ["blue-square", "above", "high"] },
-	{ "category": "symbols", "char": "🆒", "name": "cool", "keywords": ["words", "blue-square"] },
-	{ "category": "symbols", "char": "🆕", "name": "new", "keywords": ["blue-square", "words", "start"] },
-	{ "category": "symbols", "char": "🆓", "name": "free", "keywords": ["blue-square", "words"] },
-	{ "category": "symbols", "char": "0️⃣", "name": "zero", "keywords": ["0", "numbers", "blue-square", "null"] },
-	{ "category": "symbols", "char": "1️⃣", "name": "one", "keywords": ["blue-square", "numbers", "1"] },
-	{ "category": "symbols", "char": "2️⃣", "name": "two", "keywords": ["numbers", "2", "prime", "blue-square"] },
-	{ "category": "symbols", "char": "3️⃣", "name": "three", "keywords": ["3", "numbers", "prime", "blue-square"] },
-	{ "category": "symbols", "char": "4️⃣", "name": "four", "keywords": ["4", "numbers", "blue-square"] },
-	{ "category": "symbols", "char": "5️⃣", "name": "five", "keywords": ["5", "numbers", "blue-square", "prime"] },
-	{ "category": "symbols", "char": "6️⃣", "name": "six", "keywords": ["6", "numbers", "blue-square"] },
-	{ "category": "symbols", "char": "7️⃣", "name": "seven", "keywords": ["7", "numbers", "blue-square", "prime"] },
-	{ "category": "symbols", "char": "8️⃣", "name": "eight", "keywords": ["8", "blue-square", "numbers"] },
-	{ "category": "symbols", "char": "9️⃣", "name": "nine", "keywords": ["blue-square", "numbers", "9"] },
-	{ "category": "symbols", "char": "🔟", "name": "keycap_ten", "keywords": ["numbers", "10", "blue-square"] },
-	{ "category": "symbols", "char": "*⃣", "name": "asterisk", "keywords": ["star", "keycap"] },
-	{ "category": "symbols", "char": "⏏️", "name": "eject_button", "keywords": ["blue-square"] },
-	{ "category": "symbols", "char": "▶️", "name": "arrow_forward", "keywords": ["blue-square", "right", "direction", "play"] },
-	{ "category": "symbols", "char": "⏸", "name": "pause_button", "keywords": ["pause", "blue-square"] },
-	{ "category": "symbols", "char": "⏭", "name": "next_track_button", "keywords": ["forward", "next", "blue-square"] },
-	{ "category": "symbols", "char": "⏹", "name": "stop_button", "keywords": ["blue-square"] },
-	{ "category": "symbols", "char": "⏺", "name": "record_button", "keywords": ["blue-square"] },
-	{ "category": "symbols", "char": "⏯", "name": "play_or_pause_button", "keywords": ["blue-square", "play", "pause"] },
-	{ "category": "symbols", "char": "⏮", "name": "previous_track_button", "keywords": ["backward"] },
-	{ "category": "symbols", "char": "⏩", "name": "fast_forward", "keywords": ["blue-square", "play", "speed", "continue"] },
-	{ "category": "symbols", "char": "⏪", "name": "rewind", "keywords": ["play", "blue-square"] },
-	{ "category": "symbols", "char": "🔀", "name": "twisted_rightwards_arrows", "keywords": ["blue-square", "shuffle", "music", "random"] },
-	{ "category": "symbols", "char": "🔁", "name": "repeat", "keywords": ["loop", "record"] },
-	{ "category": "symbols", "char": "🔂", "name": "repeat_one", "keywords": ["blue-square", "loop"] },
-	{ "category": "symbols", "char": "◀️", "name": "arrow_backward", "keywords": ["blue-square", "left", "direction"] },
-	{ "category": "symbols", "char": "🔼", "name": "arrow_up_small", "keywords": ["blue-square", "triangle", "direction", "point", "forward", "top"] },
-	{ "category": "symbols", "char": "🔽", "name": "arrow_down_small", "keywords": ["blue-square", "direction", "bottom"] },
-	{ "category": "symbols", "char": "⏫", "name": "arrow_double_up", "keywords": ["blue-square", "direction", "top"] },
-	{ "category": "symbols", "char": "⏬", "name": "arrow_double_down", "keywords": ["blue-square", "direction", "bottom"] },
-	{ "category": "symbols", "char": "➡️", "name": "arrow_right", "keywords": ["blue-square", "next"] },
-	{ "category": "symbols", "char": "⬅️", "name": "arrow_left", "keywords": ["blue-square", "previous", "back"] },
-	{ "category": "symbols", "char": "⬆️", "name": "arrow_up", "keywords": ["blue-square", "continue", "top", "direction"] },
-	{ "category": "symbols", "char": "⬇️", "name": "arrow_down", "keywords": ["blue-square", "direction", "bottom"] },
-	{ "category": "symbols", "char": "↗️", "name": "arrow_upper_right", "keywords": ["blue-square", "point", "direction", "diagonal", "northeast"] },
-	{ "category": "symbols", "char": "↘️", "name": "arrow_lower_right", "keywords": ["blue-square", "direction", "diagonal", "southeast"] },
-	{ "category": "symbols", "char": "↙️", "name": "arrow_lower_left", "keywords": ["blue-square", "direction", "diagonal", "southwest"] },
-	{ "category": "symbols", "char": "↖️", "name": "arrow_upper_left", "keywords": ["blue-square", "point", "direction", "diagonal", "northwest"] },
-	{ "category": "symbols", "char": "↕️", "name": "arrow_up_down", "keywords": ["blue-square", "direction", "way", "vertical"] },
-	{ "category": "symbols", "char": "↔️", "name": "left_right_arrow", "keywords": ["shape", "direction", "horizontal", "sideways"] },
-	{ "category": "symbols", "char": "🔄", "name": "arrows_counterclockwise", "keywords": ["blue-square", "sync", "cycle"] },
-	{ "category": "symbols", "char": "↪️", "name": "arrow_right_hook", "keywords": ["blue-square", "return", "rotate", "direction"] },
-	{ "category": "symbols", "char": "↩️", "name": "leftwards_arrow_with_hook", "keywords": ["back", "return", "blue-square", "undo", "enter"] },
-	{ "category": "symbols", "char": "⤴️", "name": "arrow_heading_up", "keywords": ["blue-square", "direction", "top"] },
-	{ "category": "symbols", "char": "⤵️", "name": "arrow_heading_down", "keywords": ["blue-square", "direction", "bottom"] },
-	{ "category": "symbols", "char": "#️⃣", "name": "hash", "keywords": ["symbol", "blue-square", "twitter"] },
-	{ "category": "symbols", "char": "ℹ️", "name": "information_source", "keywords": ["blue-square", "alphabet", "letter"] },
-	{ "category": "symbols", "char": "🔤", "name": "abc", "keywords": ["blue-square", "alphabet"] },
-	{ "category": "symbols", "char": "🔡", "name": "abcd", "keywords": ["blue-square", "alphabet"] },
-	{ "category": "symbols", "char": "🔠", "name": "capital_abcd", "keywords": ["alphabet", "words", "blue-square"] },
-	{ "category": "symbols", "char": "🔣", "name": "symbols", "keywords": ["blue-square", "music", "note", "ampersand", "percent", "glyphs", "characters"] },
-	{ "category": "symbols", "char": "🎵", "name": "musical_note", "keywords": ["score", "tone", "sound"] },
-	{ "category": "symbols", "char": "🎶", "name": "notes", "keywords": ["music", "score"] },
-	{ "category": "symbols", "char": "〰️", "name": "wavy_dash", "keywords": ["draw", "line", "moustache", "mustache", "squiggle", "scribble"] },
-	{ "category": "symbols", "char": "➰", "name": "curly_loop", "keywords": ["scribble", "draw", "shape", "squiggle"] },
-	{ "category": "symbols", "char": "✔️", "name": "heavy_check_mark", "keywords": ["ok", "nike", "answer", "yes", "tick"] },
-	{ "category": "symbols", "char": "🔃", "name": "arrows_clockwise", "keywords": ["sync", "cycle", "round", "repeat"] },
-	{ "category": "symbols", "char": "➕", "name": "heavy_plus_sign", "keywords": ["math", "calculation", "addition", "more", "increase"] },
-	{ "category": "symbols", "char": "➖", "name": "heavy_minus_sign", "keywords": ["math", "calculation", "subtract", "less"] },
-	{ "category": "symbols", "char": "➗", "name": "heavy_division_sign", "keywords": ["divide", "math", "calculation"] },
-	{ "category": "symbols", "char": "✖️", "name": "heavy_multiplication_x", "keywords": ["math", "calculation"] },
-	{ "category": "symbols", "char": "♾", "name": "infinity", "keywords": ["forever"] },
-	{ "category": "symbols", "char": "💲", "name": "heavy_dollar_sign", "keywords": ["money", "sales", "payment", "currency", "buck"] },
-	{ "category": "symbols", "char": "💱", "name": "currency_exchange", "keywords": ["money", "sales", "dollar", "travel"] },
-	{ "category": "symbols", "char": "©️", "name": "copyright", "keywords": ["ip", "license", "circle", "law", "legal"] },
-	{ "category": "symbols", "char": "®️", "name": "registered", "keywords": ["alphabet", "circle"] },
-	{ "category": "symbols", "char": "™️", "name": "tm", "keywords": ["trademark", "brand", "law", "legal"] },
-	{ "category": "symbols", "char": "🔚", "name": "end", "keywords": ["words", "arrow"] },
-	{ "category": "symbols", "char": "🔙", "name": "back", "keywords": ["arrow", "words", "return"] },
-	{ "category": "symbols", "char": "🔛", "name": "on", "keywords": ["arrow", "words"] },
-	{ "category": "symbols", "char": "🔝", "name": "top", "keywords": ["words", "blue-square"] },
-	{ "category": "symbols", "char": "🔜", "name": "soon", "keywords": ["arrow", "words"] },
-	{ "category": "symbols", "char": "☑️", "name": "ballot_box_with_check", "keywords": ["ok", "agree", "confirm", "black-square", "vote", "election", "yes", "tick"] },
-	{ "category": "symbols", "char": "🔘", "name": "radio_button", "keywords": ["input", "old", "music", "circle"] },
-	{ "category": "symbols", "char": "⚫", "name": "black_circle", "keywords": ["shape", "button", "round"] },
-	{ "category": "symbols", "char": "⚪", "name": "white_circle", "keywords": ["shape", "round"] },
-	{ "category": "symbols", "char": "🔴", "name": "red_circle", "keywords": ["shape", "error", "danger"] },
-	{ "category": "symbols", "char": "🟠", "name": "orange_circle", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟡", "name": "yellow_circle", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟢", "name": "green_circle", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🔵", "name": "large_blue_circle", "keywords": ["shape", "icon", "button"] },
-	{ "category": "symbols", "char": "🟣", "name": "purple_circle", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟤", "name": "brown_circle", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🔸", "name": "small_orange_diamond", "keywords": ["shape", "jewel", "gem"] },
-	{ "category": "symbols", "char": "🔹", "name": "small_blue_diamond", "keywords": ["shape", "jewel", "gem"] },
-	{ "category": "symbols", "char": "🔶", "name": "large_orange_diamond", "keywords": ["shape", "jewel", "gem"] },
-	{ "category": "symbols", "char": "🔷", "name": "large_blue_diamond", "keywords": ["shape", "jewel", "gem"] },
-	{ "category": "symbols", "char": "🔺", "name": "small_red_triangle", "keywords": ["shape", "direction", "up", "top"] },
-	{ "category": "symbols", "char": "▪️", "name": "black_small_square", "keywords": ["shape", "icon"] },
-	{ "category": "symbols", "char": "▫️", "name": "white_small_square", "keywords": ["shape", "icon"] },
-	{ "category": "symbols", "char": "⬛", "name": "black_large_square", "keywords": ["shape", "icon", "button"] },
-	{ "category": "symbols", "char": "⬜", "name": "white_large_square", "keywords": ["shape", "icon", "stone", "button"] },
-	{ "category": "symbols", "char": "🟥", "name": "red_square", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟧", "name": "orange_square", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟨", "name": "yellow_square", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟩", "name": "green_square", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟦", "name": "blue_square", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟪", "name": "purple_square", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🟫", "name": "brown_square", "keywords": ["shape"] },
-	{ "category": "symbols", "char": "🔻", "name": "small_red_triangle_down", "keywords": ["shape", "direction", "bottom"] },
-	{ "category": "symbols", "char": "◼️", "name": "black_medium_square", "keywords": ["shape", "button", "icon"] },
-	{ "category": "symbols", "char": "◻️", "name": "white_medium_square", "keywords": ["shape", "stone", "icon"] },
-	{ "category": "symbols", "char": "◾", "name": "black_medium_small_square", "keywords": ["icon", "shape", "button"] },
-	{ "category": "symbols", "char": "◽", "name": "white_medium_small_square", "keywords": ["shape", "stone", "icon", "button"] },
-	{ "category": "symbols", "char": "🔲", "name": "black_square_button", "keywords": ["shape", "input", "frame"] },
-	{ "category": "symbols", "char": "🔳", "name": "white_square_button", "keywords": ["shape", "input"] },
-	{ "category": "symbols", "char": "🔈", "name": "speaker", "keywords": ["sound", "volume", "silence", "broadcast"] },
-	{ "category": "symbols", "char": "🔉", "name": "sound", "keywords": ["volume", "speaker", "broadcast"] },
-	{ "category": "symbols", "char": "🔊", "name": "loud_sound", "keywords": ["volume", "noise", "noisy", "speaker", "broadcast"] },
-	{ "category": "symbols", "char": "🔇", "name": "mute", "keywords": ["sound", "volume", "silence", "quiet"] },
-	{ "category": "symbols", "char": "📣", "name": "mega", "keywords": ["sound", "speaker", "volume"] },
-	{ "category": "symbols", "char": "📢", "name": "loudspeaker", "keywords": ["volume", "sound"] },
-	{ "category": "symbols", "char": "🔔", "name": "bell", "keywords": ["sound", "notification", "christmas", "xmas", "chime"] },
-	{ "category": "symbols", "char": "🔕", "name": "no_bell", "keywords": ["sound", "volume", "mute", "quiet", "silent"] },
-	{ "category": "symbols", "char": "🃏", "name": "black_joker", "keywords": ["poker", "cards", "game", "play", "magic"] },
-	{ "category": "symbols", "char": "🀄", "name": "mahjong", "keywords": ["game", "play", "chinese", "kanji"] },
-	{ "category": "symbols", "char": "♠️", "name": "spades", "keywords": ["poker", "cards", "suits", "magic"] },
-	{ "category": "symbols", "char": "♣️", "name": "clubs", "keywords": ["poker", "cards", "magic", "suits"] },
-	{ "category": "symbols", "char": "♥️", "name": "hearts", "keywords": ["poker", "cards", "magic", "suits"] },
-	{ "category": "symbols", "char": "♦️", "name": "diamonds", "keywords": ["poker", "cards", "magic", "suits"] },
-	{ "category": "symbols", "char": "🎴", "name": "flower_playing_cards", "keywords": ["game", "sunset", "red"] },
-	{ "category": "symbols", "char": "💭", "name": "thought_balloon", "keywords": ["bubble", "cloud", "speech", "thinking", "dream"] },
-	{ "category": "symbols", "char": "🗯", "name": "right_anger_bubble", "keywords": ["caption", "speech", "thinking", "mad"] },
-	{ "category": "symbols", "char": "💬", "name": "speech_balloon", "keywords": ["bubble", "words", "message", "talk", "chatting"] },
-	{ "category": "symbols", "char": "🗨", "name": "left_speech_bubble", "keywords": ["words", "message", "talk", "chatting"] },
-	{ "category": "symbols", "char": "🕐", "name": "clock1", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕑", "name": "clock2", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕒", "name": "clock3", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕓", "name": "clock4", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕔", "name": "clock5", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕕", "name": "clock6", "keywords": ["time", "late", "early", "schedule", "dawn", "dusk"] },
-	{ "category": "symbols", "char": "🕖", "name": "clock7", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕗", "name": "clock8", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕘", "name": "clock9", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕙", "name": "clock10", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕚", "name": "clock11", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕛", "name": "clock12", "keywords": ["time", "noon", "midnight", "midday", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕜", "name": "clock130", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕝", "name": "clock230", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕞", "name": "clock330", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕟", "name": "clock430", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕠", "name": "clock530", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕡", "name": "clock630", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕢", "name": "clock730", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕣", "name": "clock830", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕤", "name": "clock930", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕥", "name": "clock1030", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕦", "name": "clock1130", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "symbols", "char": "🕧", "name": "clock1230", "keywords": ["time", "late", "early", "schedule"] },
-	{ "category": "flags", "char": "🇦🇫", "name": "afghanistan", "keywords": ["af", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇽", "name": "aland_islands", "keywords": ["Åland", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇱", "name": "albania", "keywords": ["al", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇩🇿", "name": "algeria", "keywords": ["dz", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇸", "name": "american_samoa", "keywords": ["american", "ws", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇩", "name": "andorra", "keywords": ["ad", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇴", "name": "angola", "keywords": ["ao", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇮", "name": "anguilla", "keywords": ["ai", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇶", "name": "antarctica", "keywords": ["aq", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇬", "name": "antigua_barbuda", "keywords": ["antigua", "barbuda", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇷", "name": "argentina", "keywords": ["ar", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇲", "name": "armenia", "keywords": ["am", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇼", "name": "aruba", "keywords": ["aw", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇨", "name": "ascension_island", "keywords": ["flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇺", "name": "australia", "keywords": ["au", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇹", "name": "austria", "keywords": ["at", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇿", "name": "azerbaijan", "keywords": ["az", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇸", "name": "bahamas", "keywords": ["bs", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇭", "name": "bahrain", "keywords": ["bh", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇩", "name": "bangladesh", "keywords": ["bd", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇧", "name": "barbados", "keywords": ["bb", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇾", "name": "belarus", "keywords": ["by", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇪", "name": "belgium", "keywords": ["be", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇿", "name": "belize", "keywords": ["bz", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇯", "name": "benin", "keywords": ["bj", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇲", "name": "bermuda", "keywords": ["bm", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇹", "name": "bhutan", "keywords": ["bt", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇴", "name": "bolivia", "keywords": ["bo", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇶", "name": "caribbean_netherlands", "keywords": ["bonaire", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇦", "name": "bosnia_herzegovina", "keywords": ["bosnia", "herzegovina", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇼", "name": "botswana", "keywords": ["bw", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇷", "name": "brazil", "keywords": ["br", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇴", "name": "british_indian_ocean_territory", "keywords": ["british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇻🇬", "name": "british_virgin_islands", "keywords": ["british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇳", "name": "brunei", "keywords": ["bn", "darussalam", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇬", "name": "bulgaria", "keywords": ["bg", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇫", "name": "burkina_faso", "keywords": ["burkina", "faso", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇮", "name": "burundi", "keywords": ["bi", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇻", "name": "cape_verde", "keywords": ["cabo", "verde", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇭", "name": "cambodia", "keywords": ["kh", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇲", "name": "cameroon", "keywords": ["cm", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇦", "name": "canada", "keywords": ["ca", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇨", "name": "canary_islands", "keywords": ["canary", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇾", "name": "cayman_islands", "keywords": ["cayman", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇫", "name": "central_african_republic", "keywords": ["central", "african", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇩", "name": "chad", "keywords": ["td", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇱", "name": "chile", "keywords": ["flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇳", "name": "cn", "keywords": ["china", "chinese", "prc", "flag", "country", "nation", "banner"] },
-	{ "category": "flags", "char": "🇨🇽", "name": "christmas_island", "keywords": ["christmas", "island", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇨", "name": "cocos_islands", "keywords": ["cocos", "keeling", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇴", "name": "colombia", "keywords": ["co", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇲", "name": "comoros", "keywords": ["km", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇬", "name": "congo_brazzaville", "keywords": ["congo", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇩", "name": "congo_kinshasa", "keywords": ["congo", "democratic", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇰", "name": "cook_islands", "keywords": ["cook", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇷", "name": "costa_rica", "keywords": ["costa", "rica", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇭🇷", "name": "croatia", "keywords": ["hr", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇺", "name": "cuba", "keywords": ["cu", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇼", "name": "curacao", "keywords": ["curaçao", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇾", "name": "cyprus", "keywords": ["cy", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇿", "name": "czech_republic", "keywords": ["cz", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇩🇰", "name": "denmark", "keywords": ["dk", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇩🇯", "name": "djibouti", "keywords": ["dj", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇩🇲", "name": "dominica", "keywords": ["dm", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇩🇴", "name": "dominican_republic", "keywords": ["dominican", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇪🇨", "name": "ecuador", "keywords": ["ec", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇪🇬", "name": "egypt", "keywords": ["eg", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇻", "name": "el_salvador", "keywords": ["el", "salvador", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇶", "name": "equatorial_guinea", "keywords": ["equatorial", "gn", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇪🇷", "name": "eritrea", "keywords": ["er", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇪🇪", "name": "estonia", "keywords": ["ee", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇪🇹", "name": "ethiopia", "keywords": ["et", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇪🇺", "name": "eu", "keywords": ["european", "union", "flag", "banner"] },
-	{ "category": "flags", "char": "🇫🇰", "name": "falkland_islands", "keywords": ["falkland", "islands", "malvinas", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇫🇴", "name": "faroe_islands", "keywords": ["faroe", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇫🇯", "name": "fiji", "keywords": ["fj", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇫🇮", "name": "finland", "keywords": ["fi", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇫🇷", "name": "fr", "keywords": ["banner", "flag", "nation", "france", "french", "country"] },
-	{ "category": "flags", "char": "🇬🇫", "name": "french_guiana", "keywords": ["french", "guiana", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇫", "name": "french_polynesia", "keywords": ["french", "polynesia", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇫", "name": "french_southern_territories", "keywords": ["french", "southern", "territories", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇦", "name": "gabon", "keywords": ["ga", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇲", "name": "gambia", "keywords": ["gm", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇪", "name": "georgia", "keywords": ["ge", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇩🇪", "name": "de", "keywords": ["german", "nation", "flag", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇭", "name": "ghana", "keywords": ["gh", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇮", "name": "gibraltar", "keywords": ["gi", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇷", "name": "greece", "keywords": ["gr", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇱", "name": "greenland", "keywords": ["gl", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇩", "name": "grenada", "keywords": ["gd", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇵", "name": "guadeloupe", "keywords": ["gp", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇺", "name": "guam", "keywords": ["gu", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇹", "name": "guatemala", "keywords": ["gt", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇬", "name": "guernsey", "keywords": ["gg", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇳", "name": "guinea", "keywords": ["gn", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇼", "name": "guinea_bissau", "keywords": ["gw", "bissau", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇾", "name": "guyana", "keywords": ["gy", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇭🇹", "name": "haiti", "keywords": ["ht", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇭🇳", "name": "honduras", "keywords": ["hn", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇭🇰", "name": "hong_kong", "keywords": ["hong", "kong", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇭🇺", "name": "hungary", "keywords": ["hu", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇸", "name": "iceland", "keywords": ["is", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇳", "name": "india", "keywords": ["in", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇩", "name": "indonesia", "keywords": ["flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇷", "name": "iran", "keywords": ["iran, ", "islamic", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇶", "name": "iraq", "keywords": ["iq", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇪", "name": "ireland", "keywords": ["ie", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇲", "name": "isle_of_man", "keywords": ["isle", "man", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇱", "name": "israel", "keywords": ["il", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇮🇹", "name": "it", "keywords": ["italy", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇮", "name": "cote_divoire", "keywords": ["ivory", "coast", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇯🇲", "name": "jamaica", "keywords": ["jm", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇯🇵", "name": "jp", "keywords": ["japanese", "nation", "flag", "country", "banner"] },
-	{ "category": "flags", "char": "🇯🇪", "name": "jersey", "keywords": ["je", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇯🇴", "name": "jordan", "keywords": ["jo", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇿", "name": "kazakhstan", "keywords": ["kz", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇪", "name": "kenya", "keywords": ["ke", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇮", "name": "kiribati", "keywords": ["ki", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇽🇰", "name": "kosovo", "keywords": ["xk", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇼", "name": "kuwait", "keywords": ["kw", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇬", "name": "kyrgyzstan", "keywords": ["kg", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇦", "name": "laos", "keywords": ["lao", "democratic", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇻", "name": "latvia", "keywords": ["lv", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇧", "name": "lebanon", "keywords": ["lb", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇸", "name": "lesotho", "keywords": ["ls", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇷", "name": "liberia", "keywords": ["lr", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇾", "name": "libya", "keywords": ["ly", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇮", "name": "liechtenstein", "keywords": ["li", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇹", "name": "lithuania", "keywords": ["lt", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇺", "name": "luxembourg", "keywords": ["lu", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇴", "name": "macau", "keywords": ["macao", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇰", "name": "macedonia", "keywords": ["macedonia, ", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇬", "name": "madagascar", "keywords": ["mg", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇼", "name": "malawi", "keywords": ["mw", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇾", "name": "malaysia", "keywords": ["my", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇻", "name": "maldives", "keywords": ["mv", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇱", "name": "mali", "keywords": ["ml", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇹", "name": "malta", "keywords": ["mt", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇭", "name": "marshall_islands", "keywords": ["marshall", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇶", "name": "martinique", "keywords": ["mq", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇷", "name": "mauritania", "keywords": ["mr", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇺", "name": "mauritius", "keywords": ["mu", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇾🇹", "name": "mayotte", "keywords": ["yt", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇽", "name": "mexico", "keywords": ["mx", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇫🇲", "name": "micronesia", "keywords": ["micronesia, ", "federated", "states", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇩", "name": "moldova", "keywords": ["moldova, ", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇨", "name": "monaco", "keywords": ["mc", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇳", "name": "mongolia", "keywords": ["mn", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇪", "name": "montenegro", "keywords": ["me", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇸", "name": "montserrat", "keywords": ["ms", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇦", "name": "morocco", "keywords": ["ma", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇿", "name": "mozambique", "keywords": ["mz", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇲", "name": "myanmar", "keywords": ["mm", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇦", "name": "namibia", "keywords": ["na", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇷", "name": "nauru", "keywords": ["nr", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇵", "name": "nepal", "keywords": ["np", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇱", "name": "netherlands", "keywords": ["nl", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇨", "name": "new_caledonia", "keywords": ["new", "caledonia", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇿", "name": "new_zealand", "keywords": ["new", "zealand", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇮", "name": "nicaragua", "keywords": ["ni", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇪", "name": "niger", "keywords": ["ne", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇬", "name": "nigeria", "keywords": ["flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇺", "name": "niue", "keywords": ["nu", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇫", "name": "norfolk_island", "keywords": ["norfolk", "island", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇲🇵", "name": "northern_mariana_islands", "keywords": ["northern", "mariana", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇵", "name": "north_korea", "keywords": ["north", "korea", "nation", "flag", "country", "banner"] },
-	{ "category": "flags", "char": "🇳🇴", "name": "norway", "keywords": ["no", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇴🇲", "name": "oman", "keywords": ["om_symbol", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇰", "name": "pakistan", "keywords": ["pk", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇼", "name": "palau", "keywords": ["pw", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇸", "name": "palestinian_territories", "keywords": ["palestine", "palestinian", "territories", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇦", "name": "panama", "keywords": ["pa", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇬", "name": "papua_new_guinea", "keywords": ["papua", "new", "guinea", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇾", "name": "paraguay", "keywords": ["py", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇪", "name": "peru", "keywords": ["pe", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇭", "name": "philippines", "keywords": ["ph", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇳", "name": "pitcairn_islands", "keywords": ["pitcairn", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇱", "name": "poland", "keywords": ["pl", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇹", "name": "portugal", "keywords": ["pt", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇷", "name": "puerto_rico", "keywords": ["puerto", "rico", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇶🇦", "name": "qatar", "keywords": ["qa", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇷🇪", "name": "reunion", "keywords": ["réunion", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇷🇴", "name": "romania", "keywords": ["ro", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇷🇺", "name": "ru", "keywords": ["russian", "federation", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇷🇼", "name": "rwanda", "keywords": ["rw", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇧🇱", "name": "st_barthelemy", "keywords": ["saint", "barthélemy", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇭", "name": "st_helena", "keywords": ["saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇳", "name": "st_kitts_nevis", "keywords": ["saint", "kitts", "nevis", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇨", "name": "st_lucia", "keywords": ["saint", "lucia", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇵🇲", "name": "st_pierre_miquelon", "keywords": ["saint", "pierre", "miquelon", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇻🇨", "name": "st_vincent_grenadines", "keywords": ["saint", "vincent", "grenadines", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇼🇸", "name": "samoa", "keywords": ["ws", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇲", "name": "san_marino", "keywords": ["san", "marino", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇹", "name": "sao_tome_principe", "keywords": ["sao", "tome", "principe", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇦", "name": "saudi_arabia", "keywords": ["flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇳", "name": "senegal", "keywords": ["sn", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇷🇸", "name": "serbia", "keywords": ["rs", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇨", "name": "seychelles", "keywords": ["sc", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇱", "name": "sierra_leone", "keywords": ["sierra", "leone", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇬", "name": "singapore", "keywords": ["sg", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇽", "name": "sint_maarten", "keywords": ["sint", "maarten", "dutch", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇰", "name": "slovakia", "keywords": ["sk", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇮", "name": "slovenia", "keywords": ["si", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇧", "name": "solomon_islands", "keywords": ["solomon", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇴", "name": "somalia", "keywords": ["so", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇿🇦", "name": "south_africa", "keywords": ["south", "africa", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇸", "name": "south_georgia_south_sandwich_islands", "keywords": ["south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇰🇷", "name": "kr", "keywords": ["south", "korea", "nation", "flag", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇸", "name": "south_sudan", "keywords": ["south", "sd", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇪🇸", "name": "es", "keywords": ["spain", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇱🇰", "name": "sri_lanka", "keywords": ["sri", "lanka", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇩", "name": "sudan", "keywords": ["sd", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇷", "name": "suriname", "keywords": ["sr", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇿", "name": "swaziland", "keywords": ["sz", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇪", "name": "sweden", "keywords": ["se", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇨🇭", "name": "switzerland", "keywords": ["ch", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇸🇾", "name": "syria", "keywords": ["syrian", "arab", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇼", "name": "taiwan", "keywords": ["tw", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇯", "name": "tajikistan", "keywords": ["tj", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇿", "name": "tanzania", "keywords": ["tanzania, ", "united", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇭", "name": "thailand", "keywords": ["th", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇱", "name": "timor_leste", "keywords": ["timor", "leste", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇬", "name": "togo", "keywords": ["tg", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇰", "name": "tokelau", "keywords": ["tk", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇴", "name": "tonga", "keywords": ["to", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇹", "name": "trinidad_tobago", "keywords": ["trinidad", "tobago", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇦", "name": "tristan_da_cunha", "keywords": ["flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇳", "name": "tunisia", "keywords": ["tn", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇷", "name": "tr", "keywords": ["turkey", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇲", "name": "turkmenistan", "keywords": ["flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇨", "name": "turks_caicos_islands", "keywords": ["turks", "caicos", "islands", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇹🇻", "name": "tuvalu", "keywords": ["flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇺🇬", "name": "uganda", "keywords": ["ug", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇺🇦", "name": "ukraine", "keywords": ["ua", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇦🇪", "name": "united_arab_emirates", "keywords": ["united", "arab", "emirates", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇬🇧", "name": "uk", "keywords": ["united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "UK", "english", "england", "union jack"] },
-	{ "category": "flags", "char": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", "name": "england", "keywords": ["flag", "english"] },
-	{ "category": "flags", "char": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "name": "scotland", "keywords": ["flag", "scottish"] },
-	{ "category": "flags", "char": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", "name": "wales", "keywords": ["flag", "welsh"] },
-	{ "category": "flags", "char": "🇺🇸", "name": "us", "keywords": ["united", "states", "america", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇻🇮", "name": "us_virgin_islands", "keywords": ["virgin", "islands", "us", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇺🇾", "name": "uruguay", "keywords": ["uy", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇺🇿", "name": "uzbekistan", "keywords": ["uz", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇻🇺", "name": "vanuatu", "keywords": ["vu", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇻🇦", "name": "vatican_city", "keywords": ["vatican", "city", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇻🇪", "name": "venezuela", "keywords": ["ve", "bolivarian", "republic", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇻🇳", "name": "vietnam", "keywords": ["viet", "nam", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇼🇫", "name": "wallis_futuna", "keywords": ["wallis", "futuna", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇪🇭", "name": "western_sahara", "keywords": ["western", "sahara", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇾🇪", "name": "yemen", "keywords": ["ye", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇿🇲", "name": "zambia", "keywords": ["zm", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇿🇼", "name": "zimbabwe", "keywords": ["zw", "flag", "nation", "country", "banner"] },
-	{ "category": "flags", "char": "🇺🇳", "name": "united_nations", "keywords": ["un", "flag", "banner"] },
-	{ "category": "flags", "char": "🏴‍☠️", "name": "pirate_flag", "keywords": ["skull", "crossbones", "flag", "banner"] }
-]
diff --git a/src/env.ts b/src/env.ts
deleted file mode 100644
index 1b678edc44..0000000000
--- a/src/env.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-const envOption = {
-	onlyQueue: false,
-	onlyServer: false,
-	noDaemons: false,
-	disableClustering: false,
-	verbose: false,
-	withLogTime: false,
-	quiet: false,
-	slow: false,
-};
-
-for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
-	if (process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]) envOption[key] = true;
-}
-
-if (process.env.NODE_ENV === 'test') envOption.disableClustering = true;
-if (process.env.NODE_ENV === 'test') envOption.quiet = true;
-if (process.env.NODE_ENV === 'test') envOption.noDaemons = true;
-
-export { envOption };
diff --git a/src/games/reversi/core.ts b/src/games/reversi/core.ts
deleted file mode 100644
index 9bfce9834a..0000000000
--- a/src/games/reversi/core.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-import { count, concat } from '@/prelude/array';
-
-// MISSKEY REVERSI ENGINE
-
-/**
- * true ... 黒
- * false ... 白
- */
-export type Color = boolean;
-const BLACK = true;
-const WHITE = false;
-
-export type MapPixel = 'null' | 'empty';
-
-export type Options = {
-	isLlotheo: boolean;
-	canPutEverywhere: boolean;
-	loopedBoard: boolean;
-};
-
-export type Undo = {
-	/**
-	 * 色
-	 */
-	color: Color;
-
-	/**
-	 * どこに打ったか
-	 */
-	pos: number;
-
-	/**
-	 * 反転した石の位置の配列
-	 */
-	effects: number[];
-
-	/**
-	 * ターン
-	 */
-	turn: Color | null;
-};
-
-/**
- * リバーシエンジン
- */
-export default class Reversi {
-	public map: MapPixel[];
-	public mapWidth: number;
-	public mapHeight: number;
-	public board: (Color | null | undefined)[];
-	public turn: Color | null = BLACK;
-	public opts: Options;
-
-	public prevPos = -1;
-	public prevColor: Color | null = null;
-
-	private logs: Undo[] = [];
-
-	/**
-	 * ゲームを初期化します
-	 */
-	constructor(map: string[], opts: Options) {
-		//#region binds
-		this.put = this.put.bind(this);
-		//#endregion
-
-		//#region Options
-		this.opts = opts;
-		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
-		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
-		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
-		//#endregion
-
-		//#region Parse map data
-		this.mapWidth = map[0].length;
-		this.mapHeight = map.length;
-		const mapData = map.join('');
-
-		this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
-
-		this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
-		//#endregion
-
-		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
-		if (!this.canPutSomewhere(BLACK))
-			this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
-	}
-
-	/**
-	 * 黒石の数
-	 */
-	public get blackCount() {
-		return count(BLACK, this.board);
-	}
-
-	/**
-	 * 白石の数
-	 */
-	public get whiteCount() {
-		return count(WHITE, this.board);
-	}
-
-	public transformPosToXy(pos: number): number[] {
-		const x = pos % this.mapWidth;
-		const y = Math.floor(pos / this.mapWidth);
-		return [x, y];
-	}
-
-	public transformXyToPos(x: number, y: number): number {
-		return x + (y * this.mapWidth);
-	}
-
-	/**
-	 * 指定のマスに石を打ちます
-	 * @param color 石の色
-	 * @param pos 位置
-	 */
-	public put(color: Color, pos: number) {
-		this.prevPos = pos;
-		this.prevColor = color;
-
-		this.board[pos] = color;
-
-		// 反転させられる石を取得
-		const effects = this.effects(color, pos);
-
-		// 反転させる
-		for (const pos of effects) {
-			this.board[pos] = color;
-		}
-
-		const turn = this.turn;
-
-		this.logs.push({
-			color,
-			pos,
-			effects,
-			turn
-		});
-
-		this.calcTurn();
-	}
-
-	private calcTurn() {
-		// ターン計算
-		this.turn =
-			this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
-			this.canPutSomewhere(this.prevColor!) ? this.prevColor :
-			null;
-	}
-
-	public undo() {
-		const undo = this.logs.pop()!;
-		this.prevColor = undo.color;
-		this.prevPos = undo.pos;
-		this.board[undo.pos] = null;
-		for (const pos of undo.effects) {
-			const color = this.board[pos];
-			this.board[pos] = !color;
-		}
-		this.turn = undo.turn;
-	}
-
-	/**
-	 * 指定した位置のマップデータのマスを取得します
-	 * @param pos 位置
-	 */
-	public mapDataGet(pos: number): MapPixel {
-		const [x, y] = this.transformPosToXy(pos);
-		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
-	}
-
-	/**
-	 * 打つことができる場所を取得します
-	 */
-	public puttablePlaces(color: Color): number[] {
-		return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
-	}
-
-	/**
-	 * 打つことができる場所があるかどうかを取得します
-	 */
-	public canPutSomewhere(color: Color): boolean {
-		return this.puttablePlaces(color).length > 0;
-	}
-
-	/**
-	 * 指定のマスに石を打つことができるかどうかを取得します
-	 * @param color 自分の色
-	 * @param pos 位置
-	 */
-	public canPut(color: Color, pos: number): boolean {
-		return (
-			this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
-			this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
-			this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
-	}
-
-	/**
-	 * 指定のマスに石を置いた時の、反転させられる石を取得します
-	 * @param color 自分の色
-	 * @param initPos 位置
-	 */
-	public effects(color: Color, initPos: number): number[] {
-		const enemyColor = !color;
-
-		const diffVectors: [number, number][] = [
-			[  0,  -1], // 上
-			[ +1,  -1], // 右上
-			[ +1,   0], // 右
-			[ +1,  +1], // 右下
-			[  0,  +1], // 下
-			[ -1,  +1], // 左下
-			[ -1,   0], // 左
-			[ -1,  -1]  // 左上
-		];
-
-		const effectsInLine = ([dx, dy]: [number, number]): number[] => {
-			const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
-
-			const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
-			let [x, y] = this.transformPosToXy(initPos);
-			while (true) {
-				[x, y] = nextPos(x, y);
-
-				// 座標が指し示す位置がボード外に出たとき
-				if (this.opts.loopedBoard && this.transformXyToPos(
-					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
-					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos)
-						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
-					return found;
-				else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight)
-					return []; // 挟めないことが確定 (盤面外に到達)
-
-				const pos = this.transformXyToPos(x, y);
-				if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
-				const stone = this.board[pos];
-				if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
-				if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
-				if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
-			}
-		};
-
-		return concat(diffVectors.map(effectsInLine));
-	}
-
-	/**
-	 * ゲームが終了したか否か
-	 */
-	public get isEnded(): boolean {
-		return this.turn === null;
-	}
-
-	/**
-	 * ゲームの勝者 (null = 引き分け)
-	 */
-	public get winner(): Color | null {
-		return this.isEnded ?
-			this.blackCount == this.whiteCount ? null :
-			this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
-			undefined as never;
-	}
-}
diff --git a/src/games/reversi/maps.ts b/src/games/reversi/maps.ts
deleted file mode 100644
index dc0d1bf9d0..0000000000
--- a/src/games/reversi/maps.ts
+++ /dev/null
@@ -1,896 +0,0 @@
-/**
- * 組み込みマップ定義
- *
- * データ値:
- * (スペース) ... マス無し
- * - ... マス
- * b ... 初期配置される黒石
- * w ... 初期配置される白石
- */
-
-export type Map = {
-	name?: string;
-	category?: string;
-	author?: string;
-	data: string[];
-};
-
-export const fourfour: Map = {
-	name: '4x4',
-	category: '4x4',
-	data: [
-		'----',
-		'-wb-',
-		'-bw-',
-		'----'
-	]
-};
-
-export const sixsix: Map = {
-	name: '6x6',
-	category: '6x6',
-	data: [
-		'------',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		'------'
-	]
-};
-
-export const roundedSixsix: Map = {
-	name: '6x6 rounded',
-	category: '6x6',
-	author: 'syuilo',
-	data: [
-		' ---- ',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		' ---- '
-	]
-};
-
-export const roundedSixsix2: Map = {
-	name: '6x6 rounded 2',
-	category: '6x6',
-	author: 'syuilo',
-	data: [
-		'  --  ',
-		' ---- ',
-		'--wb--',
-		'--bw--',
-		' ---- ',
-		'  --  '
-	]
-};
-
-export const eighteight: Map = {
-	name: '8x8',
-	category: '8x8',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------'
-	]
-};
-
-export const eighteightH1: Map = {
-	name: '8x8 handicap 1',
-	category: '8x8',
-	data: [
-		'b-------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------'
-	]
-};
-
-export const eighteightH2: Map = {
-	name: '8x8 handicap 2',
-	category: '8x8',
-	data: [
-		'b-------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'-------b'
-	]
-};
-
-export const eighteightH3: Map = {
-	name: '8x8 handicap 3',
-	category: '8x8',
-	data: [
-		'b------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'-------b'
-	]
-};
-
-export const eighteightH4: Map = {
-	name: '8x8 handicap 4',
-	category: '8x8',
-	data: [
-		'b------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'b------b'
-	]
-};
-
-export const eighteightH28: Map = {
-	name: '8x8 handicap 28',
-	category: '8x8',
-	data: [
-		'bbbbbbbb',
-		'b------b',
-		'b------b',
-		'b--wb--b',
-		'b--bw--b',
-		'b------b',
-		'b------b',
-		'bbbbbbbb'
-	]
-};
-
-export const roundedEighteight: Map = {
-	name: '8x8 rounded',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		' ------ ',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		' ------ '
-	]
-};
-
-export const roundedEighteight2: Map = {
-	name: '8x8 rounded 2',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'  ----  ',
-		' ------ ',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		' ------ ',
-		'  ----  '
-	]
-};
-
-export const roundedEighteight3: Map = {
-	name: '8x8 rounded 3',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'   --   ',
-		'  ----  ',
-		' ------ ',
-		'---wb---',
-		'---bw---',
-		' ------ ',
-		'  ----  ',
-		'   --   '
-	]
-};
-
-export const eighteightWithNotch: Map = {
-	name: '8x8 with notch',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'---  ---',
-		'--------',
-		'--------',
-		' --wb-- ',
-		' --bw-- ',
-		'--------',
-		'--------',
-		'---  ---'
-	]
-};
-
-export const eighteightWithSomeHoles: Map = {
-	name: '8x8 with some holes',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--- ----',
-		'----- --',
-		'-- -----',
-		'---wb---',
-		'---bw- -',
-		' -------',
-		'--- ----',
-		'--------'
-	]
-};
-
-export const circle: Map = {
-	name: 'Circle',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'   --   ',
-		' ------ ',
-		' ------ ',
-		'---wb---',
-		'---bw---',
-		' ------ ',
-		' ------ ',
-		'   --   '
-	]
-};
-
-export const smile: Map = {
-	name: 'Smile',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		' ------ ',
-		'--------',
-		'-- -- --',
-		'---wb---',
-		'-- bw --',
-		'---  ---',
-		'--------',
-		' ------ '
-	]
-};
-
-export const window: Map = {
-	name: 'Window',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--------',
-		'-  --  -',
-		'-  --  -',
-		'---wb---',
-		'---bw---',
-		'-  --  -',
-		'-  --  -',
-		'--------'
-	]
-};
-
-export const reserved: Map = {
-	name: 'Reserved',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'w------b',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'b------w'
-	]
-};
-
-export const x: Map = {
-	name: 'X',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'w------b',
-		'-w----b-',
-		'--w--b--',
-		'---wb---',
-		'---bw---',
-		'--b--w--',
-		'-b----w-',
-		'b------w'
-	]
-};
-
-export const parallel: Map = {
-	name: 'Parallel',
-	category: '8x8',
-	author: 'Aya',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---bb---',
-		'---ww---',
-		'--------',
-		'--------',
-		'--------'
-	]
-};
-
-export const lackOfBlack: Map = {
-	name: 'Lack of Black',
-	category: '8x8',
-	data: [
-		'--------',
-		'--------',
-		'--------',
-		'---w----',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------'
-	]
-};
-
-export const squareParty: Map = {
-	name: 'Square Party',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'--------',
-		'-wwwbbb-',
-		'-w-wb-b-',
-		'-wwwbbb-',
-		'-bbbwww-',
-		'-b-bw-w-',
-		'-bbbwww-',
-		'--------'
-	]
-};
-
-export const minesweeper: Map = {
-	name: 'Minesweeper',
-	category: '8x8',
-	author: 'syuilo',
-	data: [
-		'b-b--w-w',
-		'-w-wb-b-',
-		'w-b--w-b',
-		'-b-wb-w-',
-		'-w-bw-b-',
-		'b-w--b-w',
-		'-b-bw-w-',
-		'w-w--b-b'
-	]
-};
-
-export const tenthtenth: Map = {
-	name: '10x10',
-	category: '10x10',
-	data: [
-		'----------',
-		'----------',
-		'----------',
-		'----------',
-		'----wb----',
-		'----bw----',
-		'----------',
-		'----------',
-		'----------',
-		'----------'
-	]
-};
-
-export const hole: Map = {
-	name: 'The Hole',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'----------',
-		'----------',
-		'--wb--wb--',
-		'--bw--bw--',
-		'----  ----',
-		'----  ----',
-		'--wb--wb--',
-		'--bw--bw--',
-		'----------',
-		'----------'
-	]
-};
-
-export const grid: Map = {
-	name: 'Grid',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'----------',
-		'- - -- - -',
-		'----------',
-		'- - -- - -',
-		'----wb----',
-		'----bw----',
-		'- - -- - -',
-		'----------',
-		'- - -- - -',
-		'----------'
-	]
-};
-
-export const cross: Map = {
-	name: 'Cross',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		'   ----   ',
-		'   ----   ',
-		'   ----   ',
-		'----------',
-		'----wb----',
-		'----bw----',
-		'----------',
-		'   ----   ',
-		'   ----   ',
-		'   ----   '
-	]
-};
-
-export const charX: Map = {
-	name: 'Char X',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'---    ---',
-		'----  ----',
-		'----------',
-		' -------- ',
-		'  --wb--  ',
-		'  --bw--  ',
-		' -------- ',
-		'----------',
-		'----  ----',
-		'---    ---'
-	]
-};
-
-export const charY: Map = {
-	name: 'Char Y',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'---    ---',
-		'----  ----',
-		'----------',
-		' -------- ',
-		'  --wb--  ',
-		'  --bw--  ',
-		'  ------  ',
-		'  ------  ',
-		'  ------  ',
-		'  ------  '
-	]
-};
-
-export const walls: Map = {
-	name: 'Walls',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		' bbbbbbbb ',
-		'w--------w',
-		'w--------w',
-		'w--------w',
-		'w---wb---w',
-		'w---bw---w',
-		'w--------w',
-		'w--------w',
-		'w--------w',
-		' bbbbbbbb '
-	]
-};
-
-export const cpu: Map = {
-	name: 'CPU',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		' b b  b b ',
-		'w--------w',
-		' -------- ',
-		'w--------w',
-		' ---wb--- ',
-		' ---bw--- ',
-		'w--------w',
-		' -------- ',
-		'w--------w',
-		' b b  b b '
-	]
-};
-
-export const checker: Map = {
-	name: 'Checker',
-	category: '10x10',
-	author: 'Aya',
-	data: [
-		'----------',
-		'----------',
-		'----------',
-		'---wbwb---',
-		'---bwbw---',
-		'---wbwb---',
-		'---bwbw---',
-		'----------',
-		'----------',
-		'----------'
-	]
-};
-
-export const japaneseCurry: Map = {
-	name: 'Japanese curry',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'w-b-b-b-b-',
-		'-w-b-b-b-b',
-		'w-w-b-b-b-',
-		'-w-w-b-b-b',
-		'w-w-wwb-b-',
-		'-w-wbb-b-b',
-		'w-w-w-b-b-',
-		'-w-w-w-b-b',
-		'w-w-w-w-b-',
-		'-w-w-w-w-b'
-	]
-};
-
-export const mosaic: Map = {
-	name: 'Mosaic',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'- - - - - ',
-		' - - - - -',
-		'- - - - - ',
-		' - w w - -',
-		'- - b b - ',
-		' - w w - -',
-		'- - b b - ',
-		' - - - - -',
-		'- - - - - ',
-		' - - - - -',
-	]
-};
-
-export const arena: Map = {
-	name: 'Arena',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'- - -- - -',
-		' - -  - - ',
-		'- ------ -',
-		' -------- ',
-		'- --wb-- -',
-		'- --bw-- -',
-		' -------- ',
-		'- ------ -',
-		' - -  - - ',
-		'- - -- - -'
-	]
-};
-
-export const reactor: Map = {
-	name: 'Reactor',
-	category: '10x10',
-	author: 'syuilo',
-	data: [
-		'-w------b-',
-		'b- -  - -w',
-		'- --wb-- -',
-		'---b  w---',
-		'- b wb w -',
-		'- w bw b -',
-		'---w  b---',
-		'- --bw-- -',
-		'w- -  - -b',
-		'-b------w-'
-	]
-};
-
-export const sixeight: Map = {
-	name: '6x8',
-	category: 'Special',
-	data: [
-		'------',
-		'------',
-		'------',
-		'--wb--',
-		'--bw--',
-		'------',
-		'------',
-		'------'
-	]
-};
-
-export const spark: Map = {
-	name: 'Spark',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' -      - ',
-		'----------',
-		' -------- ',
-		' -------- ',
-		' ---wb--- ',
-		' ---bw--- ',
-		' -------- ',
-		' -------- ',
-		'----------',
-		' -      - '
-	]
-};
-
-export const islands: Map = {
-	name: 'Islands',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'--------  ',
-		'---wb---  ',
-		'---bw---  ',
-		'--------  ',
-		'  -    -  ',
-		'  -    -  ',
-		'  --------',
-		'  --------',
-		'  --------',
-		'  --------'
-	]
-};
-
-export const galaxy: Map = {
-	name: 'Galaxy',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'   ------   ',
-		'  --www---  ',
-		' ------w--- ',
-		'---bbb--w---',
-		'--b---b-w-b-',
-		'-b--wwb-w-b-',
-		'-b-w-bww--b-',
-		'-b-w-b---b--',
-		'---w--bbb---',
-		' ---w------ ',
-		'  ---www--  ',
-		'   ------   '
-	]
-};
-
-export const triangle: Map = {
-	name: 'Triangle',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'    --    ',
-		'    --    ',
-		'   ----   ',
-		'   ----   ',
-		'  --wb--  ',
-		'  --bw--  ',
-		' -------- ',
-		' -------- ',
-		'----------',
-		'----------'
-	]
-};
-
-export const iphonex: Map = {
-	name: 'iPhone X',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' --  -- ',
-		'--------',
-		'--------',
-		'--------',
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------',
-		'--------',
-		'--------',
-		'--------',
-		' ------ '
-	]
-};
-
-export const dealWithIt: Map = {
-	name: 'Deal with it!',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		'------------',
-		'--w-b-------',
-		' --b-w------',
-		'  --w-b---- ',
-		'   -------  '
-	]
-};
-
-export const experiment: Map = {
-	name: 'Let\'s experiment',
-	category: 'Special',
-	author: 'syuilo',
-	data: [
-		' ------------ ',
-		'------wb------',
-		'------bw------',
-		'--------------',
-		'    -    -    ',
-		'------  ------',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'bbbbbb  wwwwww',
-		'wwwwww  bbbbbb'
-	]
-};
-
-export const bigBoard: Map = {
-	name: 'Big board',
-	category: 'Special',
-	data: [
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'-------wb-------',
-		'-------bw-------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------',
-		'----------------'
-	]
-};
-
-export const twoBoard: Map = {
-	name: 'Two board',
-	category: 'Special',
-	author: 'Aya',
-	data: [
-		'-------- --------',
-		'-------- --------',
-		'-------- --------',
-		'---wb--- ---wb---',
-		'---bw--- ---bw---',
-		'-------- --------',
-		'-------- --------',
-		'-------- --------'
-	]
-};
-
-export const test1: Map = {
-	name: 'Test1',
-	category: 'Test',
-	data: [
-		'--------',
-		'---wb---',
-		'---bw---',
-		'--------'
-	]
-};
-
-export const test2: Map = {
-	name: 'Test2',
-	category: 'Test',
-	data: [
-		'------',
-		'------',
-		'-b--w-',
-		'-w--b-',
-		'-w--b-'
-	]
-};
-
-export const test3: Map = {
-	name: 'Test3',
-	category: 'Test',
-	data: [
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'--w',
-		'w--',
-		'-w-',
-		'---',
-		'b--',
-	]
-};
-
-export const test4: Map = {
-	name: 'Test4',
-	category: 'Test',
-	data: [
-		'-w--b-',
-		'-w--b-',
-		'------',
-		'-w--b-',
-		'-w--b-'
-	]
-};
-
-// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)A1に打ってしまう
-export const test6: Map = {
-	name: 'Test6',
-	category: 'Test',
-	data: [
-		'--wwwww-',
-		'wwwwwwww',
-		'wbbbwbwb',
-		'wbbbbwbb',
-		'wbwbbwbb',
-		'wwbwbbbb',
-		'--wbbbbb',
-		'-wwwww--',
-	]
-};
-
-// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)G7に打ってしまう
-export const test7: Map = {
-	name: 'Test7',
-	category: 'Test',
-	data: [
-		'b--w----',
-		'b-wwww--',
-		'bwbwwwbb',
-		'wbwwwwb-',
-		'wwwwwww-',
-		'-wwbbwwb',
-		'--wwww--',
-		'--wwww--',
-	]
-};
-
-// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
-export const test8: Map = {
-	name: 'Test8',
-	category: 'Test',
-	data: [
-		'--------',
-		'-----w--',
-		'w--www--',
-		'wwwwww--',
-		'bbbbwww-',
-		'wwwwww--',
-		'--www---',
-		'--ww----',
-	]
-};
diff --git a/src/games/reversi/package.json b/src/games/reversi/package.json
deleted file mode 100644
index a4415ad141..0000000000
--- a/src/games/reversi/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "name": "misskey-reversi",
-  "version": "0.0.5",
-  "description": "Misskey reversi engine",
-  "keywords": [
-    "misskey"
-  ],
-  "author": "syuilo <i@syuilo.com>",
-  "license": "MIT",
-  "repository": "https://github.com/misskey-dev/misskey.git",
-  "bugs": "https://github.com/misskey-dev/misskey/issues",
-  "main": "./built/core.js",
-  "types": "./built/core.d.ts",
-  "scripts": {
-    "build": "tsc"
-  },
-  "dependencies": {}
-}
diff --git a/src/games/reversi/tsconfig.json b/src/games/reversi/tsconfig.json
deleted file mode 100644
index 851fb6b7e4..0000000000
--- a/src/games/reversi/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-	"compilerOptions": {
-		"noEmitOnError": false,
-		"noImplicitAny": false,
-		"noImplicitReturns": true,
-		"noFallthroughCasesInSwitch": true,
-		"experimentalDecorators": true,
-		"declaration": true,
-		"sourceMap": false,
-		"target": "es2017",
-		"module": "commonjs",
-		"removeComments": false,
-		"noLib": false,
-		"outDir": "./built",
-		"rootDir": "./"
-	},
-	"compileOnSave": false,
-	"include": [
-		"./core.ts"
-	]
-}
diff --git a/src/global.d.ts b/src/global.d.ts
deleted file mode 100644
index 7343aa1994..0000000000
--- a/src/global.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-type FIXME = any;
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index db1b53f51b..0000000000
--- a/src/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Misskey Entry Point!
- */
-
-Error.stackTraceLimit = Infinity;
-
-require('events').EventEmitter.defaultMaxListeners = 128;
-
-import boot from './boot/index';
-
-export default function() {
-	return boot();
-}
diff --git a/src/meta.json b/src/meta.json
deleted file mode 100644
index 9afdfcf1e1..0000000000
--- a/src/meta.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-	"version": "unknown"
-}
diff --git a/src/mfm/fn-name-list.ts b/src/mfm/fn-name-list.ts
deleted file mode 100644
index 1203bfffde..0000000000
--- a/src/mfm/fn-name-list.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// NOTE: client/components/autocomplete.vueにも関数のリスト(MFM_TAGS)があるので統合？
-
-const fnNameList = [
-	'tada',
-	'jelly',
-	'twitch',
-	'shake',
-	'spin',
-	'jump',
-	'bounce',
-	'flip',
-	'x2',
-	'x3',
-	'x4',
-	'font',
-	'blur',
-	'rainbow',
-	'sparkle',
-];
-
-export {
-	fnNameList
-};
diff --git a/src/mfm/from-html.ts b/src/mfm/from-html.ts
deleted file mode 100644
index de6aa3d0cc..0000000000
--- a/src/mfm/from-html.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import * as parse5 from 'parse5';
-import treeAdapter = require('parse5/lib/tree-adapters/default');
-import { URL } from 'url';
-
-const urlRegex     = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
-const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
-
-export function fromHtml(html: string, hashtagNames?: string[]): string | null {
-	if (html == null) return null;
-
-	const dom = parse5.parseFragment(html);
-
-	let text = '';
-
-	for (const n of dom.childNodes) {
-		analyze(n);
-	}
-
-	return text.trim();
-
-	function getText(node: parse5.Node): string {
-		if (treeAdapter.isTextNode(node)) return node.value;
-		if (!treeAdapter.isElementNode(node)) return '';
-		if (node.nodeName === 'br') return '\n';
-
-		if (node.childNodes) {
-			return node.childNodes.map(n => getText(n)).join('');
-		}
-
-		return '';
-	}
-
-	function appendChildren(childNodes: parse5.ChildNode[]): void {
-		if (childNodes) {
-			for (const n of childNodes) {
-				analyze(n);
-			}
-		}
-	}
-
-	function analyze(node: parse5.Node) {
-		if (treeAdapter.isTextNode(node)) {
-			text += node.value;
-			return;
-		}
-
-		// Skip comment or document type node
-		if (!treeAdapter.isElementNode(node)) return;
-
-		switch (node.nodeName) {
-			case 'br':
-				text += '\n';
-				break;
-
-			case 'a':
-			{
-				const txt = getText(node);
-				const rel = node.attrs.find(x => x.name === 'rel');
-				const href = node.attrs.find(x => x.name === 'href');
-
-				// ハッシュタグ
-				if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
-					text += txt;
-				// メンション
-				} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
-					const part = txt.split('@');
-
-					if (part.length === 2 && href) {
-						//#region ホスト名部分が省略されているので復元する
-						const acct = `${txt}@${(new URL(href.value)).hostname}`;
-						text += acct;
-						//#endregion
-					} else if (part.length === 3) {
-						text += txt;
-					}
-				// その他
-				} else {
-					const generateLink = () => {
-						if (!href && !txt) {
-							return '';
-						}
-						if (!href) {
-							return txt;
-						}
-						if (!txt || txt === href.value) {	// #6383: Missing text node
-							if (href.value.match(urlRegexFull)) {
-								return href.value;
-							} else {
-								return `<${href.value}>`;
-							}
-						}
-						if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) {
-							return `[${txt}](<${href.value}>)`;	// #6846
-						} else {
-							return `[${txt}](${href.value})`;
-						}
-					};
-
-					text += generateLink();
-				}
-				break;
-			}
-
-			case 'h1':
-			{
-				text += '【';
-				appendChildren(node.childNodes);
-				text += '】\n';
-				break;
-			}
-
-			case 'b':
-			case 'strong':
-			{
-				text += '**';
-				appendChildren(node.childNodes);
-				text += '**';
-				break;
-			}
-
-			case 'small':
-			{
-				text += '<small>';
-				appendChildren(node.childNodes);
-				text += '</small>';
-				break;
-			}
-
-			case 's':
-			case 'del':
-			{
-				text += '~~';
-				appendChildren(node.childNodes);
-				text += '~~';
-				break;
-			}
-
-			case 'i':
-			case 'em':
-			{
-				text += '<i>';
-				appendChildren(node.childNodes);
-				text += '</i>';
-				break;
-			}
-
-			// block code (<pre><code>)
-			case 'pre': {
-				if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
-					text += '\n```\n';
-					text += getText(node.childNodes[0]);
-					text += '\n```\n';
-				} else {
-					appendChildren(node.childNodes);
-				}
-				break;
-			}
-
-			// inline code (<code>)
-			case 'code': {
-				text += '`';
-				appendChildren(node.childNodes);
-				text += '`';
-				break;
-			}
-
-			case 'blockquote': {
-				const t = getText(node);
-				if (t) {
-					text += '> ';
-					text += t.split('\n').join(`\n> `);
-				}
-				break;
-			}
-
-			case 'p':
-			case 'h2':
-			case 'h3':
-			case 'h4':
-			case 'h5':
-			case 'h6':
-			{
-				text += '\n\n';
-				appendChildren(node.childNodes);
-				break;
-			}
-
-			// other block elements
-			case 'div':
-			case 'header':
-			case 'footer':
-			case 'article':
-			case 'li':
-			case 'dt':
-			case 'dd':
-			{
-				text += '\n';
-				appendChildren(node.childNodes);
-				break;
-			}
-
-			default:	// includes inline elements
-			{
-				appendChildren(node.childNodes);
-				break;
-			}
-		}
-	}
-}
diff --git a/src/mfm/to-html.ts b/src/mfm/to-html.ts
deleted file mode 100644
index b3678a0dda..0000000000
--- a/src/mfm/to-html.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { JSDOM } from 'jsdom';
-import * as mfm from 'mfm-js';
-import config from '@/config/index';
-import { intersperse } from '@/prelude/array';
-import { IMentionedRemoteUsers } from '@/models/entities/note';
-import { wellKnownServices } from '../well-known-services';
-
-export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
-	if (nodes == null) {
-		return null;
-	}
-
-	const { window } = new JSDOM('');
-
-	const doc = window.document;
-
-	function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
-		if (children) {
-			for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
-		}
-	}
-
-	const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
-		bold(node) {
-			const el = doc.createElement('b');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		small(node) {
-			const el = doc.createElement('small');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		strike(node) {
-			const el = doc.createElement('del');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		italic(node) {
-			const el = doc.createElement('i');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		fn(node) {
-			const el = doc.createElement('i');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		blockCode(node) {
-			const pre = doc.createElement('pre');
-			const inner = doc.createElement('code');
-			inner.textContent = node.props.code;
-			pre.appendChild(inner);
-			return pre;
-		},
-
-		center(node) {
-			const el = doc.createElement('div');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		emojiCode(node) {
-			return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
-		},
-
-		unicodeEmoji(node) {
-			return doc.createTextNode(node.props.emoji);
-		},
-
-		hashtag(node) {
-			const a = doc.createElement('a');
-			a.href = `${config.url}/tags/${node.props.hashtag}`;
-			a.textContent = `#${node.props.hashtag}`;
-			a.setAttribute('rel', 'tag');
-			return a;
-		},
-
-		inlineCode(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.code;
-			return el;
-		},
-
-		mathInline(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.formula;
-			return el;
-		},
-
-		mathBlock(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.formula;
-			return el;
-		},
-
-		link(node) {
-			const a = doc.createElement('a');
-			a.href = node.props.url;
-			appendChildren(node.children, a);
-			return a;
-		},
-
-		mention(node) {
-			const a = doc.createElement('a');
-			const { username, host, acct } = node.props;
-			const wellKnown = wellKnownServices.find(x => x[0] === host);
-			if (wellKnown) {
-				a.href = wellKnown[1](username);
-			} else {
-				const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
-				a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${config.url}/${acct}`;
-				a.className = 'u-url mention';
-			}
-			a.textContent = acct;
-			return a;
-		},
-
-		quote(node) {
-			const el = doc.createElement('blockquote');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		text(node) {
-			const el = doc.createElement('span');
-			const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
-
-			for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
-				el.appendChild(x === 'br' ? doc.createElement('br') : x);
-			}
-
-			return el;
-		},
-
-		url(node) {
-			const a = doc.createElement('a');
-			a.href = node.props.url;
-			a.textContent = node.props.url;
-			return a;
-		},
-
-		search(node) {
-			const a = doc.createElement('a');
-			a.href = `https://www.google.com/search?q=${node.props.query}`;
-			a.textContent = node.props.content;
-			return a;
-		}
-	};
-
-	appendChildren(nodes, doc.body);
-
-	return `<p>${doc.body.innerHTML}</p>`;
-}
diff --git a/src/misc/acct.ts b/src/misc/acct.ts
deleted file mode 100644
index 5106b1a09e..0000000000
--- a/src/misc/acct.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import * as Misskey from 'misskey-js';
-
-export const getAcct = (user: Misskey.Acct) => {
-	return user.host == null ? user.username : `${user.username}@${user.host}`;
-};
-
-export const parseAcct = (acct: string): Misskey.Acct => {
-	if (acct.startsWith('@')) acct = acct.substr(1);
-	const split = acct.split('@', 2);
-	return { username: split[0], host: split[1] || null };
-};
diff --git a/src/misc/antenna-cache.ts b/src/misc/antenna-cache.ts
deleted file mode 100644
index a23eeb45ec..0000000000
--- a/src/misc/antenna-cache.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Antennas } from '@/models/index';
-import { Antenna } from '@/models/entities/antenna';
-import { subsdcriber } from '../db/redis';
-
-let antennasFetched = false;
-let antennas: Antenna[] = [];
-
-export async function getAntennas() {
-	if (!antennasFetched) {
-		antennas = await Antennas.find();
-		antennasFetched = true;
-	}
-
-	return antennas;
-}
-
-subsdcriber.on('message', async (_, data) => {
-	const obj = JSON.parse(data);
-
-	if (obj.channel === 'internal') {
-		const { type, body } = obj.message;
-		switch (type) {
-			case 'antennaCreated':
-				antennas.push(body);
-				break;
-			case 'antennaUpdated':
-				antennas[antennas.findIndex(a => a.id === body.id)] = body;
-				break;
-			case 'antennaDeleted':
-				antennas = antennas.filter(a => a.id !== body.id);
-				break;
-			default:
-				break;
-		}
-	}
-});
diff --git a/src/misc/api-permissions.ts b/src/misc/api-permissions.ts
deleted file mode 100644
index 160cdf9fd6..0000000000
--- a/src/misc/api-permissions.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-export const kinds = [
-	'read:account',
-	'write:account',
-	'read:blocks',
-	'write:blocks',
-	'read:drive',
-	'write:drive',
-	'read:favorites',
-	'write:favorites',
-	'read:following',
-	'write:following',
-	'read:messaging',
-	'write:messaging',
-	'read:mutes',
-	'write:mutes',
-	'write:notes',
-	'read:notifications',
-	'write:notifications',
-	'read:reactions',
-	'write:reactions',
-	'write:votes',
-	'read:pages',
-	'write:pages',
-	'write:page-likes',
-	'read:page-likes',
-	'read:user-groups',
-	'write:user-groups',
-	'read:channels',
-	'write:channels',
-	'read:gallery',
-	'write:gallery',
-	'read:gallery-likes',
-	'write:gallery-likes',
-];
-// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).
diff --git a/src/misc/app-lock.ts b/src/misc/app-lock.ts
deleted file mode 100644
index a32b600612..0000000000
--- a/src/misc/app-lock.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { redisClient } from '../db/redis';
-import { promisify } from 'util';
-import * as redisLock from 'redis-lock';
-
-/**
- * Retry delay (ms) for lock acquisition
- */
-const retryDelay = 100;
-
-const lock: (key: string, timeout?: number) => Promise<() => void>
-	= redisClient
-	? promisify(redisLock(redisClient, retryDelay))
-	: async () => () => { };
-
-/**
- * Get AP Object lock
- * @param uri AP object ID
- * @param timeout Lock timeout (ms), The timeout releases previous lock.
- * @returns Unlock function
- */
-export function getApLock(uri: string, timeout = 30 * 1000) {
-	return lock(`ap-object:${uri}`, timeout);
-}
-
-export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) {
-	return lock(`instance:${host}`, timeout);
-}
-
-export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) {
-	return lock(`chart-insert:${lockKey}`, timeout);
-}
diff --git a/src/misc/before-shutdown.ts b/src/misc/before-shutdown.ts
deleted file mode 100644
index 33abf5fb4d..0000000000
--- a/src/misc/before-shutdown.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-// https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58
-
-'use strict';
-
-/**
- * @callback BeforeShutdownListener
- * @param {string} [signalOrEvent] The exit signal or event name received on the process.
- */
-
-/**
- * System signals the app will listen to initiate shutdown.
- * @const {string[]}
- */
-const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
-
-/**
- * Time in milliseconds to wait before forcing shutdown.
- * @const {number}
- */
-const SHUTDOWN_TIMEOUT = 15000;
-
-/**
- * A queue of listener callbacks to execute before shutting
- * down the process.
- * @type {BeforeShutdownListener[]}
- */
-const shutdownListeners = [];
-
-/**
- * Listen for signals and execute given `fn` function once.
- * @param  {string[]} signals System signals to listen to.
- * @param  {function(string)} fn Function to execute on shutdown.
- */
-const processOnce = (signals, fn) => {
-	for (const sig of signals) {
-		process.once(sig, fn);
-	}
-};
-
-/**
- * Sets a forced shutdown mechanism that will exit the process after `timeout` milliseconds.
- * @param {number} timeout Time to wait before forcing shutdown (milliseconds)
- */
-const forceExitAfter = timeout => () => {
-	setTimeout(() => {
-		// Force shutdown after timeout
-		console.warn(`Could not close resources gracefully after ${timeout}ms: forcing shutdown`);
-		return process.exit(1);
-	}, timeout).unref();
-};
-
-/**
- * Main process shutdown handler. Will invoke every previously registered async shutdown listener
- * in the queue and exit with a code of `0`. Any `Promise` rejections from any listener will
- * be logged out as a warning, but won't prevent other callbacks from executing.
- * @param {string} signalOrEvent The exit signal or event name received on the process.
- */
-async function shutdownHandler(signalOrEvent) {
-	if (process.env.NODE_ENV === 'test') return process.exit(0);
-
-	console.warn(`Shutting down: received [${signalOrEvent}] signal`);
-
-	for (const listener of shutdownListeners) {
-		try {
-			await listener(signalOrEvent);
-		} catch (err) {
-			console.warn(`A shutdown handler failed before completing with: ${err.message || err}`);
-		}
-	}
-
-	return process.exit(0);
-}
-
-/**
- * Registers a new shutdown listener to be invoked before exiting
- * the main process. Listener handlers are guaranteed to be called in the order
- * they were registered.
- * @param {BeforeShutdownListener} listener The shutdown listener to register.
- * @returns {BeforeShutdownListener} Echoes back the supplied `listener`.
- */
-export function beforeShutdown(listener) {
-	shutdownListeners.push(listener);
-	return listener;
-}
-
-// Register shutdown callback that kills the process after `SHUTDOWN_TIMEOUT` milliseconds
-// This prevents custom shutdown handlers from hanging the process indefinitely
-processOnce(SHUTDOWN_SIGNALS, forceExitAfter(SHUTDOWN_TIMEOUT));
-
-// Register process shutdown callback
-// Will listen to incoming signal events and execute all registered handlers in the stack
-processOnce(SHUTDOWN_SIGNALS, shutdownHandler);
diff --git a/src/misc/cache.ts b/src/misc/cache.ts
deleted file mode 100644
index 71fbbd8a4c..0000000000
--- a/src/misc/cache.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-export class Cache<T> {
-	private cache: Map<string | null, { date: number; value: T; }>;
-	private lifetime: number;
-
-	constructor(lifetime: Cache<never>['lifetime']) {
-		this.cache = new Map();
-		this.lifetime = lifetime;
-	}
-
-	public set(key: string | null, value: T): void {
-		this.cache.set(key, {
-			date: Date.now(),
-			value
-		});
-	}
-
-	public get(key: string | null): T | undefined {
-		const cached = this.cache.get(key);
-		if (cached == null) return undefined;
-		if ((Date.now() - cached.date) > this.lifetime) {
-			this.cache.delete(key);
-			return undefined;
-		}
-		return cached.value;
-	}
-
-	public delete(key: string | null) {
-		this.cache.delete(key);
-	}
-
-	public async fetch(key: string | null, fetcher: () => Promise<T>): Promise<T> {
-		const cachedValue = this.get(key);
-		if (cachedValue !== undefined) {
-			// Cache HIT
-			return cachedValue;
-		}
-
-		// Cache MISS
-		const value = await fetcher();
-		this.set(key, value);
-		return value;
-	}
-}
diff --git a/src/misc/cafy-id.ts b/src/misc/cafy-id.ts
deleted file mode 100644
index 39886611e1..0000000000
--- a/src/misc/cafy-id.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Context } from 'cafy';
-
-export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> {
-	public readonly name = 'ID';
-
-	constructor(optional = false, nullable = false) {
-		super(optional, nullable);
-
-		this.push((v: any) => {
-			if (typeof v !== 'string') {
-				return new Error('must-be-an-id');
-			}
-			return true;
-		});
-	}
-
-	public getType() {
-		return super.getType('String');
-	}
-
-	public makeOptional(): ID<undefined> {
-		return new ID(true, false);
-	}
-
-	public makeNullable(): ID<null> {
-		return new ID(false, true);
-	}
-
-	public makeOptionalNullable(): ID<undefined | null> {
-		return new ID(true, true);
-	}
-}
diff --git a/src/misc/captcha.ts b/src/misc/captcha.ts
deleted file mode 100644
index f36943b589..0000000000
--- a/src/misc/captcha.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import fetch from 'node-fetch';
-import { URLSearchParams } from 'url';
-import { getAgentByUrl } from './fetch';
-import config from '@/config/index';
-
-export async function verifyRecaptcha(secret: string, response: string) {
-	const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
-		throw `recaptcha-request-failed: ${e}`;
-	});
-
-	if (result.success !== true) {
-		const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
-		throw `recaptcha-failed: ${errorCodes}`;
-	}
-}
-
-export async function verifyHcaptcha(secret: string, response: string) {
-	const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => {
-		throw `hcaptcha-request-failed: ${e}`;
-	});
-
-	if (result.success !== true) {
-		const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
-		throw `hcaptcha-failed: ${errorCodes}`;
-	}
-}
-
-type CaptchaResponse = {
-	success: boolean;
-	'error-codes'?: string[];
-};
-
-async function getCaptchaResponse(url: string, secret: string, response: string): Promise<CaptchaResponse> {
-	const params = new URLSearchParams({
-		secret,
-		response
-	});
-
-	const res = await fetch(url, {
-		method: 'POST',
-		body: params,
-		headers: {
-			'User-Agent': config.userAgent
-		},
-		timeout: 10 * 1000,
-		agent: getAgentByUrl
-	}).catch(e => {
-		throw `${e.message || e}`;
-	});
-
-	if (!res.ok) {
-		throw `${res.status}`;
-	}
-
-	return await res.json() as CaptchaResponse;
-}
diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts
deleted file mode 100644
index 3789054b26..0000000000
--- a/src/misc/check-hit-antenna.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Antenna } from '@/models/entities/antenna';
-import { Note } from '@/models/entities/note';
-import { User } from '@/models/entities/user';
-import { UserListJoinings, UserGroupJoinings } from '@/models/index';
-import { getFullApAccount } from './convert-host';
-import { parseAcct } from '@/misc/acct';
-import { Packed } from './schema';
-
-/**
- * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
- */
-export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
-	if (note.visibility === 'specified') return false;
-
-	if (note.visibility === 'followers') {
-		if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
-		if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
-	}
-
-	if (!antenna.withReplies && note.replyId != null) return false;
-
-	if (antenna.src === 'home') {
-		if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
-		if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
-	} else if (antenna.src === 'list') {
-		const listUsers = (await UserListJoinings.find({
-			userListId: antenna.userListId!
-		})).map(x => x.userId);
-
-		if (!listUsers.includes(note.userId)) return false;
-	} else if (antenna.src === 'group') {
-		const joining = await UserGroupJoinings.findOneOrFail(antenna.userGroupJoiningId!);
-
-		const groupUsers = (await UserGroupJoinings.find({
-			userGroupId: joining.userGroupId
-		})).map(x => x.userId);
-
-		if (!groupUsers.includes(note.userId)) return false;
-	} else if (antenna.src === 'users') {
-		const accts = antenna.users.map(x => {
-			const { username, host } = parseAcct(x);
-			return getFullApAccount(username, host).toLowerCase();
-		});
-		if (!accts.includes(getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
-	}
-
-	const keywords = antenna.keywords
-		// Clean up
-		.map(xs => xs.filter(x => x !== ''))
-		.filter(xs => xs.length > 0);
-
-	if (keywords.length > 0) {
-		if (note.text == null) return false;
-
-		const matched = keywords.some(and =>
-			and.every(keyword =>
-				antenna.caseSensitive
-					? note.text!.includes(keyword)
-					: note.text!.toLowerCase().includes(keyword.toLowerCase())
-			));
-
-		if (!matched) return false;
-	}
-
-	const excludeKeywords = antenna.excludeKeywords
-		// Clean up
-		.map(xs => xs.filter(x => x !== ''))
-		.filter(xs => xs.length > 0);
-
-	if (excludeKeywords.length > 0) {
-		if (note.text == null) return false;
-
-		const matched = excludeKeywords.some(and =>
-			and.every(keyword =>
-				antenna.caseSensitive
-					? note.text!.includes(keyword)
-					: note.text!.toLowerCase().includes(keyword.toLowerCase())
-			));
-
-		if (matched) return false;
-	}
-
-	if (antenna.withFile) {
-		if (note.fileIds && note.fileIds.length === 0) return false;
-	}
-
-	// TODO: eval expression
-
-	return true;
-}
diff --git a/src/misc/check-word-mute.ts b/src/misc/check-word-mute.ts
deleted file mode 100644
index e2e871dd2b..0000000000
--- a/src/misc/check-word-mute.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-const RE2 = require('re2');
-import { Note } from '@/models/entities/note';
-import { User } from '@/models/entities/user';
-
-type NoteLike = {
-	userId: Note['userId'];
-	text: Note['text'];
-};
-
-type UserLike = {
-	id: User['id'];
-};
-
-export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: string[][]): Promise<boolean> {
-	// 自分自身
-	if (me && (note.userId === me.id)) return false;
-
-	const words = mutedWords
-		// Clean up
-		.map(xs => xs.filter(x => x !== ''))
-		.filter(xs => xs.length > 0);
-
-	if (words.length > 0) {
-		if (note.text == null) return false;
-
-		const matched = words.some(and =>
-			and.every(keyword => {
-				const regexp = keyword.match(/^\/(.+)\/(.*)$/);
-				if (regexp) {
-					return new RE2(regexp[1], regexp[2]).test(note.text!);
-				}
-				return note.text!.includes(keyword);
-			}));
-
-		if (matched) return true;
-	}
-
-	return false;
-}
diff --git a/src/misc/content-disposition.ts b/src/misc/content-disposition.ts
deleted file mode 100644
index 9df7ed4688..0000000000
--- a/src/misc/content-disposition.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-const cd = require('content-disposition');
-
-export function contentDisposition(type: 'inline' | 'attachment', filename: string): string {
-	const fallback = filename.replace(/[^\w.-]/g, '_');
-	return cd(filename, { type, fallback });
-}
diff --git a/src/misc/convert-host.ts b/src/misc/convert-host.ts
deleted file mode 100644
index 6e9f6ed3e9..0000000000
--- a/src/misc/convert-host.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { URL } from 'url';
-import config from '@/config/index';
-import { toASCII } from 'punycode/';
-
-export function getFullApAccount(username: string, host: string | null) {
-	return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`;
-}
-
-export function isSelfHost(host: string) {
-	if (host == null) return true;
-	return toPuny(config.host) === toPuny(host);
-}
-
-export function extractDbHost(uri: string) {
-	const url = new URL(uri);
-	return toPuny(url.hostname);
-}
-
-export function toPuny(host: string) {
-	return toASCII(host.toLowerCase());
-}
-
-export function toPunyNullable(host: string | null | undefined): string | null {
-	if (host == null) return null;
-	return toASCII(host.toLowerCase());
-}
diff --git a/src/misc/count-same-renotes.ts b/src/misc/count-same-renotes.ts
deleted file mode 100644
index 6628761182..0000000000
--- a/src/misc/count-same-renotes.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Notes } from '@/models/index';
-
-export async function countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
-	// 指定したユーザーの指定したノートのリノートがいくつあるか数える
-	const query = Notes.createQueryBuilder('note')
-		.where('note.userId = :userId', { userId })
-		.andWhere('note.renoteId = :renoteId', { renoteId });
-
-	// 指定した投稿を除く
-	if (excludeNoteId) {
-		query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
-	}
-
-	return await query.getCount();
-}
diff --git a/src/misc/create-temp.ts b/src/misc/create-temp.ts
deleted file mode 100644
index 04604cf7d0..0000000000
--- a/src/misc/create-temp.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as tmp from 'tmp';
-
-export function createTemp(): Promise<[string, any]> {
-	return new Promise<[string, any]>((res, rej) => {
-		tmp.file((e, path, fd, cleanup) => {
-			if (e) return rej(e);
-			res([path, cleanup]);
-		});
-	});
-}
diff --git a/src/misc/detect-url-mime.ts b/src/misc/detect-url-mime.ts
deleted file mode 100644
index 274c291737..0000000000
--- a/src/misc/detect-url-mime.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { createTemp } from './create-temp';
-import { downloadUrl } from './download-url';
-import { detectType } from './get-file-info';
-
-export async function detectUrlMime(url: string) {
-	const [path, cleanup] = await createTemp();
-
-	try {
-		await downloadUrl(url, path);
-		const { mime } = await detectType(path);
-		return mime;
-	} finally {
-		cleanup();
-	}
-}
diff --git a/src/misc/download-text-file.ts b/src/misc/download-text-file.ts
deleted file mode 100644
index e8e23cc120..0000000000
--- a/src/misc/download-text-file.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as fs from 'fs';
-import * as util from 'util';
-import Logger from '@/services/logger';
-import { createTemp } from './create-temp';
-import { downloadUrl } from './download-url';
-
-const logger = new Logger('download-text-file');
-
-export async function downloadTextFile(url: string): Promise<string> {
-	// Create temp file
-	const [path, cleanup] = await createTemp();
-
-	logger.info(`Temp file is ${path}`);
-
-	try {
-		// write content at URL to temp file
-		await downloadUrl(url, path);
-
-		const text = await util.promisify(fs.readFile)(path, 'utf8');
-
-		return text;
-	} finally {
-		cleanup();
-	}
-}
diff --git a/src/misc/download-url.ts b/src/misc/download-url.ts
deleted file mode 100644
index c96b4fd1d6..0000000000
--- a/src/misc/download-url.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import * as fs from 'fs';
-import * as stream from 'stream';
-import * as util from 'util';
-import got, * as Got from 'got';
-import { httpAgent, httpsAgent, StatusError } from './fetch';
-import config from '@/config/index';
-import * as chalk from 'chalk';
-import Logger from '@/services/logger';
-import * as IPCIDR from 'ip-cidr';
-const PrivateIp = require('private-ip');
-
-const pipeline = util.promisify(stream.pipeline);
-
-export async function downloadUrl(url: string, path: string) {
-	const logger = new Logger('download');
-
-	logger.info(`Downloading ${chalk.cyan(url)} ...`);
-
-	const timeout = 30 * 1000;
-	const operationTimeout = 60 * 1000;
-	const maxSize = config.maxFileSize || 262144000;
-
-	const req = got.stream(url, {
-		headers: {
-			'User-Agent': config.userAgent
-		},
-		timeout: {
-			lookup: timeout,
-			connect: timeout,
-			secureConnect: timeout,
-			socket: timeout,	// read timeout
-			response: timeout,
-			send: timeout,
-			request: operationTimeout,	// whole operation timeout
-		},
-		agent: {
-			http: httpAgent,
-			https: httpsAgent,
-		},
-		http2: false,	// default
-		retry: 0,
-	}).on('response', (res: Got.Response) => {
-		if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
-			if (isPrivateIp(res.ip)) {
-				logger.warn(`Blocked address: ${res.ip}`);
-				req.destroy();
-			}
-		}
-
-		const contentLength = res.headers['content-length'];
-		if (contentLength != null) {
-			const size = Number(contentLength);
-			if (size > maxSize) {
-				logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`);
-				req.destroy();
-			}
-		}
-	}).on('downloadProgress', (progress: Got.Progress) => {
-		if (progress.transferred > maxSize) {
-			logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
-			req.destroy();
-		}
-	});
-
-	try {
-		await pipeline(req, fs.createWriteStream(path));
-	} catch (e) {
-		if (e instanceof Got.HTTPError) {
-			throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
-		} else {
-			throw e;
-		}
-	}
-
-	logger.succ(`Download finished: ${chalk.cyan(url)}`);
-}
-
-function isPrivateIp(ip: string) {
-	for (const net of config.allowedPrivateNetworks || []) {
-		const cidr = new IPCIDR(net);
-		if (cidr.contains(ip)) {
-			return false;
-		}
-	}
-
-	return PrivateIp(ip);
-}
diff --git a/src/misc/emoji-regex.ts b/src/misc/emoji-regex.ts
deleted file mode 100644
index 8b07fbd8f2..0000000000
--- a/src/misc/emoji-regex.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-const twemojiRegex = require('twemoji-parser/dist/lib/regex').default;
-
-export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);
diff --git a/src/misc/emojilist.ts b/src/misc/emojilist.ts
deleted file mode 100644
index de7591f5a0..0000000000
--- a/src/misc/emojilist.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
-export const emojilist = require('../emojilist.json') as {
-	name: string;
-	keywords: string[];
-	char: string;
-	category: 'people' | 'animals_and_nature' | 'food_and_drink' | 'activity' | 'travel_and_places' | 'objects' | 'symbols' | 'flags';
-}[];
diff --git a/src/misc/extract-custom-emojis-from-mfm.ts b/src/misc/extract-custom-emojis-from-mfm.ts
deleted file mode 100644
index b29ce281b3..0000000000
--- a/src/misc/extract-custom-emojis-from-mfm.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as mfm from 'mfm-js';
-import { unique } from '@/prelude/array';
-
-export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
-	const emojiNodes = mfm.extract(nodes, (node) => {
-		return (node.type === 'emojiCode' && node.props.name.length <= 100);
-	});
-
-	return unique(emojiNodes.map(x => x.props.name));
-}
diff --git a/src/misc/extract-hashtags.ts b/src/misc/extract-hashtags.ts
deleted file mode 100644
index b0a74df219..0000000000
--- a/src/misc/extract-hashtags.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as mfm from 'mfm-js';
-import { unique } from '@/prelude/array';
-
-export function extractHashtags(nodes: mfm.MfmNode[]): string[] {
-	const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag');
-	const hashtags = unique(hashtagNodes.map(x => x.props.hashtag));
-
-	return hashtags;
-}
diff --git a/src/misc/extract-mentions.ts b/src/misc/extract-mentions.ts
deleted file mode 100644
index cc19b161a8..0000000000
--- a/src/misc/extract-mentions.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// test is located in test/extract-mentions
-
-import * as mfm from 'mfm-js';
-
-export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] {
-	// TODO: 重複を削除
-	const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention');
-	const mentions = mentionNodes.map(x => x.props);
-
-	return mentions;
-}
diff --git a/src/misc/extract-url-from-mfm.ts b/src/misc/extract-url-from-mfm.ts
deleted file mode 100644
index e6d5d0104a..0000000000
--- a/src/misc/extract-url-from-mfm.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as mfm from 'mfm-js';
-import { unique } from '@/prelude/array';
-
-// unique without hash
-// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
-const removeHash = (x: string) => x.replace(/#[^#]*$/, '');
-
-export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] {
-	const urlNodes = mfm.extract(nodes, (node) => {
-		return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent));
-	});
-	const urls: string[] = unique(urlNodes.map(x => x.props.url));
-
-	return urls.reduce((array, url) => {
-		const urlWithoutHash = removeHash(url);
-		if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url);
-		return array;
-	}, [] as string[]);
-}
diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts
deleted file mode 100644
index a0bcdd4d48..0000000000
--- a/src/misc/fetch-meta.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Meta } from '@/models/entities/meta';
-import { getConnection } from 'typeorm';
-
-let cache: Meta;
-
-export async function fetchMeta(noCache = false): Promise<Meta> {
-	if (!noCache && cache) return cache;
-
-	return await getConnection().transaction(async transactionalEntityManager => {
-		// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
-		const meta = await transactionalEntityManager.findOne(Meta, {
-			order: {
-				id: 'DESC'
-			}
-		});
-
-		if (meta) {
-			cache = meta;
-			return meta;
-		} else {
-			const saved = await transactionalEntityManager.save(Meta, {
-				id: 'x'
-			}) as Meta;
-
-			cache = saved;
-			return saved;
-		}
-	});
-}
-
-setInterval(() => {
-	fetchMeta(true).then(meta => {
-		cache = meta;
-	});
-}, 1000 * 10);
diff --git a/src/misc/fetch-proxy-account.ts b/src/misc/fetch-proxy-account.ts
deleted file mode 100644
index e0eedea4c8..0000000000
--- a/src/misc/fetch-proxy-account.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { fetchMeta } from './fetch-meta';
-import { ILocalUser } from '@/models/entities/user';
-import { Users } from '@/models/index';
-
-export async function fetchProxyAccount(): Promise<ILocalUser | null> {
-	const meta = await fetchMeta();
-	if (meta.proxyAccountId == null) return null;
-	return await Users.findOneOrFail(meta.proxyAccountId) as ILocalUser;
-}
diff --git a/src/misc/fetch.ts b/src/misc/fetch.ts
deleted file mode 100644
index f4f16a27e2..0000000000
--- a/src/misc/fetch.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import * as http from 'http';
-import * as https from 'https';
-import CacheableLookup from 'cacheable-lookup';
-import fetch from 'node-fetch';
-import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
-import config from '@/config/index';
-import { URL } from 'url';
-
-export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) {
-	const res = await getResponse({
-		url,
-		method: 'GET',
-		headers: Object.assign({
-			'User-Agent': config.userAgent,
-			Accept: accept
-		}, headers || {}),
-		timeout
-	});
-
-	return await res.json();
-}
-
-export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record<string, string>) {
-	const res = await getResponse({
-		url,
-		method: 'GET',
-		headers: Object.assign({
-			'User-Agent': config.userAgent,
-			Accept: accept
-		}, headers || {}),
-		timeout
-	});
-
-	return await res.text();
-}
-
-export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) {
-	const timeout = args?.timeout || 10 * 1000;
-
-	const controller = new AbortController();
-	setTimeout(() => {
-		controller.abort();
-	}, timeout * 6);
-
-	const res = await fetch(args.url, {
-		method: args.method,
-		headers: args.headers,
-		body: args.body,
-		timeout,
-		size: args?.size || 10 * 1024 * 1024,
-		agent: getAgentByUrl,
-		signal: controller.signal,
-	});
-
-	if (!res.ok) {
-		throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
-	}
-
-	return res;
-}
-
-const cache = new CacheableLookup({
-	maxTtl: 3600,	// 1hours
-	errorTtl: 30,	// 30secs
-	lookup: false,	// nativeのdns.lookupにfallbackしない
-});
-
-/**
- * Get http non-proxy agent
- */
-const _http = new http.Agent({
-	keepAlive: true,
-	keepAliveMsecs: 30 * 1000,
-	lookup: cache.lookup,
-} as http.AgentOptions);
-
-/**
- * Get https non-proxy agent
- */
-const _https = new https.Agent({
-	keepAlive: true,
-	keepAliveMsecs: 30 * 1000,
-	lookup: cache.lookup,
-} as https.AgentOptions);
-
-const maxSockets = Math.max(256, config.deliverJobConcurrency || 128);
-
-/**
- * Get http proxy or non-proxy agent
- */
-export const httpAgent = config.proxy
-	? new HttpProxyAgent({
-		keepAlive: true,
-		keepAliveMsecs: 30 * 1000,
-		maxSockets,
-		maxFreeSockets: 256,
-		scheduling: 'lifo',
-		proxy: config.proxy
-	})
-	: _http;
-
-/**
- * Get https proxy or non-proxy agent
- */
-export const httpsAgent = config.proxy
-	? new HttpsProxyAgent({
-		keepAlive: true,
-		keepAliveMsecs: 30 * 1000,
-		maxSockets,
-		maxFreeSockets: 256,
-		scheduling: 'lifo',
-		proxy: config.proxy
-	})
-	: _https;
-
-/**
- * Get agent by URL
- * @param url URL
- * @param bypassProxy Allways bypass proxy
- */
-export function getAgentByUrl(url: URL, bypassProxy = false) {
-	if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) {
-		return url.protocol == 'http:' ? _http : _https;
-	} else {
-		return url.protocol == 'http:' ? httpAgent : httpsAgent;
-	}
-}
-
-export class StatusError extends Error {
-	public statusCode: number;
-	public statusMessage?: string;
-	public isClientError: boolean;
-
-	constructor(message: string, statusCode: number, statusMessage?: string) {
-		super(message);
-		this.name = 'StatusError';
-		this.statusCode = statusCode;
-		this.statusMessage = statusMessage;
-		this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
-	}
-}
diff --git a/src/misc/format-time-string.ts b/src/misc/format-time-string.ts
deleted file mode 100644
index bfb2c397ae..0000000000
--- a/src/misc/format-time-string.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-const defaultLocaleStringFormats: {[index: string]: string} = {
-	'weekday': 'narrow',
-	'era': 'narrow',
-	'year': 'numeric',
-	'month': 'numeric',
-	'day': 'numeric',
-	'hour': 'numeric',
-	'minute': 'numeric',
-	'second': 'numeric',
-	'timeZoneName': 'short'
-};
-
-function formatLocaleString(date: Date, format: string): string {
-	return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => {
-		if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) {
-			return date.toLocaleString(window.navigator.language, {[kind]: option ? option : defaultLocaleStringFormats[kind]});
-		} else {
-			return match;
-		}
-	});
-}
-
-export function formatDateTimeString(date: Date, format: string): string {
-	return format
-		.replace(/yyyy/g, date.getFullYear().toString())
-		.replace(/yy/g, date.getFullYear().toString().slice(-2))
-		.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long'}))
-		.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short'}))
-		.replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2))
-		.replace(/M/g, (date.getMonth() + 1).toString())
-		.replace(/dd/g, (`0${date.getDate()}`).slice(-2))
-		.replace(/d/g, date.getDate().toString())
-		.replace(/HH/g, (`0${date.getHours()}`).slice(-2))
-		.replace(/H/g, date.getHours().toString())
-		.replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2))
-		.replace(/h/g, ((date.getHours() % 12) || 12).toString())
-		.replace(/mm/g, (`0${date.getMinutes()}`).slice(-2))
-		.replace(/m/g, date.getMinutes().toString())
-		.replace(/ss/g, (`0${date.getSeconds()}`).slice(-2))
-		.replace(/s/g, date.getSeconds().toString())
-		.replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM');
-}
-
-export function formatTimeString(date: Date, format: string): string {
-	return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
-		if (localeformat) return formatLocaleString(date, localeformat);
-		if (datetimeformat) return formatDateTimeString(date, datetimeformat);
-		return match;
-	});
-}
diff --git a/src/misc/gen-avatar.ts b/src/misc/gen-avatar.ts
deleted file mode 100644
index f03ca9f96d..0000000000
--- a/src/misc/gen-avatar.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * Random avatar generator
- */
-
-import * as p from 'pureimage';
-import * as gen from 'random-seed';
-import { WriteStream } from 'fs';
-
-const size = 256; // px
-const n = 5; // resolution
-const margin = (size / n);
-const colors = [
-	'#e57373',
-	'#F06292',
-	'#BA68C8',
-	'#9575CD',
-	'#7986CB',
-	'#64B5F6',
-	'#4FC3F7',
-	'#4DD0E1',
-	'#4DB6AC',
-	'#81C784',
-	'#8BC34A',
-	'#AFB42B',
-	'#F57F17',
-	'#FF5722',
-	'#795548',
-	'#455A64',
-];
-const bg = '#e9e9e9';
-
-const actualSize = size - (margin * 2);
-const cellSize = actualSize / n;
-const sideN = Math.floor(n / 2);
-
-/**
- * Generate buffer of random avatar by seed
- */
-export function genAvatar(seed: string, stream: WriteStream): Promise<void> {
-	const rand = gen.create(seed);
-	const canvas = p.make(size, size);
-	const ctx = canvas.getContext('2d');
-
-	ctx.fillStyle = bg;
-	ctx.beginPath();
-	ctx.fillRect(0, 0, size, size);
-
-	ctx.fillStyle = colors[rand(colors.length)];
-
-	// side bitmap (filled by false)
-	const side: boolean[][] = new Array(sideN);
-	for (let i = 0; i < side.length; i++) {
-		side[i] = new Array(n).fill(false);
-	}
-
-	// 1*n (filled by false)
-	const center: boolean[] = new Array(n).fill(false);
-
-	// tslint:disable-next-line:prefer-for-of
-	for (let x = 0; x < side.length; x++) {
-		for (let y = 0; y < side[x].length; y++) {
-			side[x][y] = rand(3) === 0;
-		}
-	}
-
-	for (let i = 0; i < center.length; i++) {
-		center[i] = rand(3) === 0;
-	}
-
-	// Draw
-	for (let x = 0; x < n; x++) {
-		for (let y = 0; y < n; y++) {
-			const isXCenter = x === ((n - 1) / 2);
-			if (isXCenter && !center[y]) continue;
-
-			const isLeftSide = x < ((n - 1) / 2);
-			if (isLeftSide && !side[x][y]) continue;
-
-			const isRightSide = x > ((n - 1) / 2);
-			if (isRightSide && !side[sideN - (x - sideN)][y]) continue;
-
-			const actualX = margin + (cellSize * x);
-			const actualY = margin + (cellSize * y);
-			ctx.beginPath();
-			ctx.fillRect(actualX, actualY, cellSize, cellSize);
-		}
-	}
-
-	return p.encodePNGToStream(canvas, stream);
-}
diff --git a/src/misc/gen-id.ts b/src/misc/gen-id.ts
deleted file mode 100644
index b1b542dc4b..0000000000
--- a/src/misc/gen-id.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ulid } from 'ulid';
-import { genAid } from './id/aid';
-import { genMeid } from './id/meid';
-import { genMeidg } from './id/meidg';
-import { genObjectId } from './id/object-id';
-import config from '@/config/index';
-
-const metohd = config.id.toLowerCase();
-
-export function genId(date?: Date): string {
-	if (!date || (date > new Date())) date = new Date();
-
-	switch (metohd) {
-		case 'aid': return genAid(date);
-		case 'meid': return genMeid(date);
-		case 'meidg': return genMeidg(date);
-		case 'ulid': return ulid(date.getTime());
-		case 'objectid': return genObjectId(date);
-		default: throw new Error('unrecognized id generation method');
-	}
-}
diff --git a/src/misc/gen-key-pair.ts b/src/misc/gen-key-pair.ts
deleted file mode 100644
index d4a8fa7534..0000000000
--- a/src/misc/gen-key-pair.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as crypto from 'crypto';
-import * as util from 'util';
-
-const generateKeyPair = util.promisify(crypto.generateKeyPair);
-
-export async function genRsaKeyPair(modulusLength = 2048) {
-	return await generateKeyPair('rsa', {
-		modulusLength,
-		publicKeyEncoding: {
-			type: 'spki',
-			format: 'pem'
-		},
-		privateKeyEncoding: {
-			type: 'pkcs8',
-			format: 'pem',
-			cipher: undefined,
-			passphrase: undefined
-		}
-	});
-}
-
-export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'secp521r1' | 'curve25519' = 'prime256v1') {
-	return await generateKeyPair('ec', {
-		namedCurve,
-		publicKeyEncoding: {
-			type: 'spki',
-			format: 'pem'
-		},
-		privateKeyEncoding: {
-			type: 'pkcs8',
-			format: 'pem',
-			cipher: undefined,
-			passphrase: undefined
-		}
-	});
-}
diff --git a/src/misc/get-file-info.ts b/src/misc/get-file-info.ts
deleted file mode 100644
index 39ba541395..0000000000
--- a/src/misc/get-file-info.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import * as fs from 'fs';
-import * as crypto from 'crypto';
-import * as stream from 'stream';
-import * as util from 'util';
-import * as fileType from 'file-type';
-import isSvg from 'is-svg';
-import * as probeImageSize from 'probe-image-size';
-import * as sharp from 'sharp';
-import { encode } from 'blurhash';
-
-const pipeline = util.promisify(stream.pipeline);
-
-export type FileInfo = {
-	size: number;
-	md5: string;
-	type: {
-		mime: string;
-		ext: string | null;
-	};
-	width?: number;
-	height?: number;
-	blurhash?: string;
-	warnings: string[];
-};
-
-const TYPE_OCTET_STREAM = {
-	mime: 'application/octet-stream',
-	ext: null
-};
-
-const TYPE_SVG = {
-	mime: 'image/svg+xml',
-	ext: 'svg'
-};
-
-/**
- * Get file information
- */
-export async function getFileInfo(path: string): Promise<FileInfo> {
-	const warnings = [] as string[];
-
-	const size = await getFileSize(path);
-	const md5 = await calcHash(path);
-
-	let type = await detectType(path);
-
-	// image dimensions
-	let width: number | undefined;
-	let height: number | undefined;
-
-	if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) {
-		const imageSize = await detectImageSize(path).catch(e => {
-			warnings.push(`detectImageSize failed: ${e}`);
-			return undefined;
-		});
-
-		// うまく判定できない画像は octet-stream にする
-		if (!imageSize) {
-			warnings.push(`cannot detect image dimensions`);
-			type = TYPE_OCTET_STREAM;
-		} else if (imageSize.wUnits === 'px') {
-			width = imageSize.width;
-			height = imageSize.height;
-
-			// 制限を超えている画像は octet-stream にする
-			if (imageSize.width > 16383 || imageSize.height > 16383) {
-				warnings.push(`image dimensions exceeds limits`);
-				type = TYPE_OCTET_STREAM;
-			}
-		} else {
-			warnings.push(`unsupported unit type: ${imageSize.wUnits}`);
-		}
-	}
-
-	let blurhash: string | undefined;
-
-	if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) {
-		blurhash = await getBlurhash(path).catch(e => {
-			warnings.push(`getBlurhash failed: ${e}`);
-			return undefined;
-		});
-	}
-
-	return {
-		size,
-		md5,
-		type,
-		width,
-		height,
-		blurhash,
-		warnings,
-	};
-}
-
-/**
- * Detect MIME Type and extension
- */
-export async function detectType(path: string) {
-	// Check 0 byte
-	const fileSize = await getFileSize(path);
-	if (fileSize === 0) {
-		return TYPE_OCTET_STREAM;
-	}
-
-	const type = await fileType.fromFile(path);
-
-	if (type) {
-		// XMLはSVGかもしれない
-		if (type.mime === 'application/xml' && await checkSvg(path)) {
-			return TYPE_SVG;
-		}
-
-		return {
-			mime: type.mime,
-			ext: type.ext
-		};
-	}
-
-	// 種類が不明でもSVGかもしれない
-	if (await checkSvg(path)) {
-		return TYPE_SVG;
-	}
-
-	// それでも種類が不明なら application/octet-stream にする
-	return TYPE_OCTET_STREAM;
-}
-
-/**
- * Check the file is SVG or not
- */
-export async function checkSvg(path: string) {
-	try {
-		const size = await getFileSize(path);
-		if (size > 1 * 1024 * 1024) return false;
-		return isSvg(fs.readFileSync(path));
-	} catch {
-		return false;
-	}
-}
-
-/**
- * Get file size
- */
-export async function getFileSize(path: string): Promise<number> {
-	const getStat = util.promisify(fs.stat);
-	return (await getStat(path)).size;
-}
-
-/**
- * Calculate MD5 hash
- */
-async function calcHash(path: string): Promise<string> {
-	const hash = crypto.createHash('md5').setEncoding('hex');
-	await pipeline(fs.createReadStream(path), hash);
-	return hash.read();
-}
-
-/**
- * Detect dimensions of image
- */
-async function detectImageSize(path: string): Promise<{
-	width: number;
-	height: number;
-	wUnits: string;
-	hUnits: string;
-}> {
-	const readable = fs.createReadStream(path);
-	const imageSize = await probeImageSize(readable);
-	readable.destroy();
-	return imageSize;
-}
-
-/**
- * Calculate average color of image
- */
-function getBlurhash(path: string): Promise<string> {
-	return new Promise((resolve, reject) => {
-		sharp(path)
-			.raw()
-			.ensureAlpha()
-			.resize(64, 64, { fit: 'inside' })
-			.toBuffer((err, buffer, { width, height }) => {
-				if (err) return reject(err);
-
-				let hash;
-
-				try {
-					hash = encode(new Uint8ClampedArray(buffer), width, height, 7, 7);
-				} catch (e) {
-					return reject(e);
-				}
-
-				resolve(hash);
-			});
-	});
-}
diff --git a/src/misc/get-note-summary.ts b/src/misc/get-note-summary.ts
deleted file mode 100644
index a1866fb209..0000000000
--- a/src/misc/get-note-summary.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * 投稿を表す文字列を取得します。
- * @param {*} note (packされた)投稿
- */
-export const getNoteSummary = (note: any, locale: any): string => {
-	if (note.deletedAt) {
-		return `(${locale['deletedNote']})`;
-	}
-
-	if (note.isHidden) {
-		return `(${locale['invisibleNote']})`;
-	}
-
-	let summary = '';
-
-	// 本文
-	if (note.cw != null) {
-		summary += note.cw;
-	} else {
-		summary += note.text ? note.text : '';
-	}
-
-	// ファイルが添付されているとき
-	if ((note.files || []).length != 0) {
-		summary += ` (${locale['withNFiles'].replace('{n}', note.files.length)})`;
-	}
-
-	// 投票が添付されているとき
-	if (note.poll) {
-		summary += ` (${locale['poll']})`;
-	}
-
-	// 返信のとき
-	if (note.replyId) {
-		if (note.reply) {
-			summary += `\n\nRE: ${getNoteSummary(note.reply, locale)}`;
-		} else {
-			summary += '\n\nRE: ...';
-		}
-	}
-
-	// Renoteのとき
-	if (note.renoteId) {
-		if (note.renote) {
-			summary += `\n\nRN: ${getNoteSummary(note.renote, locale)}`;
-		} else {
-			summary += '\n\nRN: ...';
-		}
-	}
-
-	return summary.trim();
-};
diff --git a/src/misc/get-reaction-emoji.ts b/src/misc/get-reaction-emoji.ts
deleted file mode 100644
index c2e0b98582..0000000000
--- a/src/misc/get-reaction-emoji.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-export default function(reaction: string): string {
-	switch (reaction) {
-		case 'like': return '👍';
-		case 'love': return '❤️';
-		case 'laugh': return '😆';
-		case 'hmm': return '🤔';
-		case 'surprise': return '😮';
-		case 'congrats': return '🎉';
-		case 'angry': return '💢';
-		case 'confused': return '😥';
-		case 'rip': return '😇';
-		case 'pudding': return '🍮';
-		case 'star': return '⭐';
-		default: return reaction;
-	}
-}
diff --git a/src/misc/get-user-name.ts b/src/misc/get-user-name.ts
deleted file mode 100644
index 3545e986e8..0000000000
--- a/src/misc/get-user-name.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { User } from '@/models/entities/user';
-
-export default function(user: User): string {
-	return user.name || user.username;
-}
diff --git a/src/misc/hard-limits.ts b/src/misc/hard-limits.ts
deleted file mode 100644
index 1039f7335a..0000000000
--- a/src/misc/hard-limits.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-
-// If you change DB_* values, you must also change the DB schema.
-
-/**
- * Maximum note text length that can be stored in DB.
- * Surrogate pairs count as one
- */
-export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
-
-/**
- * Maximum image description length that can be stored in DB.
- * Surrogate pairs count as one
- */
-export const DB_MAX_IMAGE_COMMENT_LENGTH = 512;
diff --git a/src/misc/i18n.ts b/src/misc/i18n.ts
deleted file mode 100644
index 4fa398763a..0000000000
--- a/src/misc/i18n.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-export class I18n<T extends Record<string, any>> {
-	public locale: T;
-
-	constructor(locale: T) {
-		this.locale = locale;
-
-		//#region BIND
-		this.t = this.t.bind(this);
-		//#endregion
-	}
-
-	// string にしているのは、ドット区切りでのパス指定を許可するため
-	// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
-	public t(key: string, args?: Record<string, any>): string {
-		try {
-			let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
-
-			if (args) {
-				for (const [k, v] of Object.entries(args)) {
-					str = str.replace(`{${k}}`, v);
-				}
-			}
-			return str;
-		} catch (e) {
-			console.warn(`missing localization '${key}'`);
-			return key;
-		}
-	}
-}
diff --git a/src/misc/id/aid.ts b/src/misc/id/aid.ts
deleted file mode 100644
index 2bcde90bff..0000000000
--- a/src/misc/id/aid.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// AID
-// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列]
-
-import * as crypto from 'crypto';
-
-const TIME2000 = 946684800000;
-let counter = crypto.randomBytes(2).readUInt16LE(0);
-
-function getTime(time: number) {
-	time = time - TIME2000;
-	if (time < 0) time = 0;
-
-	return time.toString(36).padStart(8, '0');
-}
-
-function getNoise() {
-	return counter.toString(36).padStart(2, '0').slice(-2);
-}
-
-export function genAid(date: Date): string {
-	const t = date.getTime();
-	if (isNaN(t)) throw 'Failed to create AID: Invalid Date';
-	counter++;
-	return getTime(t) + getNoise();
-}
diff --git a/src/misc/id/meid.ts b/src/misc/id/meid.ts
deleted file mode 100644
index 30bbdf1698..0000000000
--- a/src/misc/id/meid.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-const CHARS = '0123456789abcdef';
-
-function getTime(time: number) {
-	if (time < 0) time = 0;
-	if (time === 0) {
-		return CHARS[0];
-	}
-
-	time += 0x800000000000;
-
-	return time.toString(16).padStart(12, CHARS[0]);
-}
-
-function getRandom() {
-	let str = '';
-
-	for (let i = 0; i < 12; i++) {
-		str += CHARS[Math.floor(Math.random() * CHARS.length)];
-	}
-
-	return str;
-}
-
-export function genMeid(date: Date): string {
-	return getTime(date.getTime()) + getRandom();
-}
diff --git a/src/misc/id/meidg.ts b/src/misc/id/meidg.ts
deleted file mode 100644
index d4aaaea1ba..0000000000
--- a/src/misc/id/meidg.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-const CHARS = '0123456789abcdef';
-
-//  4bit Fixed hex value 'g'
-// 44bit UNIX Time ms in Hex
-// 48bit Random value in Hex
-
-function getTime(time: number) {
-	if (time < 0) time = 0;
-	if (time === 0) {
-		return CHARS[0];
-	}
-
-	return time.toString(16).padStart(11, CHARS[0]);
-}
-
-function getRandom() {
-	let str = '';
-
-	for (let i = 0; i < 12; i++) {
-		str += CHARS[Math.floor(Math.random() * CHARS.length)];
-	}
-
-	return str;
-}
-
-export function genMeidg(date: Date): string {
-	return 'g' + getTime(date.getTime()) + getRandom();
-}
diff --git a/src/misc/id/object-id.ts b/src/misc/id/object-id.ts
deleted file mode 100644
index 392ea43301..0000000000
--- a/src/misc/id/object-id.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-const CHARS = '0123456789abcdef';
-
-function getTime(time: number) {
-	if (time < 0) time = 0;
-	if (time === 0) {
-		return CHARS[0];
-	}
-
-	time = Math.floor(time / 1000);
-
-	return time.toString(16).padStart(8, CHARS[0]);
-}
-
-function getRandom() {
-	let str = '';
-
-	for (let i = 0; i < 16; i++) {
-		str += CHARS[Math.floor(Math.random() * CHARS.length)];
-	}
-
-	return str;
-}
-
-export function genObjectId(date: Date): string {
-	return getTime(date.getTime()) + getRandom();
-}
diff --git a/src/misc/identifiable-error.ts b/src/misc/identifiable-error.ts
deleted file mode 100644
index 2d7c6bd0c6..0000000000
--- a/src/misc/identifiable-error.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * ID付きエラー
- */
-export class IdentifiableError extends Error {
-	public message: string;
-	public id: string;
-
-	constructor(id: string, message?: string) {
-		super(message);
-		this.message = message || '';
-		this.id = id;
-	}
-}
diff --git a/src/misc/is-blocker-user-related.ts b/src/misc/is-blocker-user-related.ts
deleted file mode 100644
index 8c0ebfad9b..0000000000
--- a/src/misc/is-blocker-user-related.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export function isBlockerUserRelated(note: any, blockerUserIds: Set<string>): boolean {
-	if (blockerUserIds.has(note.userId)) {
-		return true;
-	}
-
-	if (note.reply != null && blockerUserIds.has(note.reply.userId)) {
-		return true;
-	}
-
-	if (note.renote != null && blockerUserIds.has(note.renote.userId)) {
-		return true;
-	}
-
-	return false;
-}
diff --git a/src/misc/is-duplicate-key-value-error.ts b/src/misc/is-duplicate-key-value-error.ts
deleted file mode 100644
index 23d8ceb1b7..0000000000
--- a/src/misc/is-duplicate-key-value-error.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function isDuplicateKeyValueError(e: Error): boolean {
-	return e.message.startsWith('duplicate key value');
-}
diff --git a/src/misc/is-muted-user-related.ts b/src/misc/is-muted-user-related.ts
deleted file mode 100644
index 2caa743f95..0000000000
--- a/src/misc/is-muted-user-related.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export function isMutedUserRelated(note: any, mutedUserIds: Set<string>): boolean {
-	if (mutedUserIds.has(note.userId)) {
-		return true;
-	}
-
-	if (note.reply != null && mutedUserIds.has(note.reply.userId)) {
-		return true;
-	}
-
-	if (note.renote != null && mutedUserIds.has(note.renote.userId)) {
-		return true;
-	}
-
-	return false;
-}
diff --git a/src/misc/is-quote.ts b/src/misc/is-quote.ts
deleted file mode 100644
index 2b57f036a2..0000000000
--- a/src/misc/is-quote.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Note } from '@/models/entities/note';
-
-export default function(note: Note): boolean {
-	return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
-}
diff --git a/src/misc/keypair-store.ts b/src/misc/keypair-store.ts
deleted file mode 100644
index c018013b7b..0000000000
--- a/src/misc/keypair-store.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { UserKeypairs } from '@/models/index';
-import { User } from '@/models/entities/user';
-import { UserKeypair } from '@/models/entities/user-keypair';
-import { Cache } from './cache';
-
-const cache = new Cache<UserKeypair>(Infinity);
-
-export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
-	return await cache.fetch(userId, () => UserKeypairs.findOneOrFail(userId));
-}
diff --git a/src/misc/license.ts b/src/misc/license.ts
deleted file mode 100644
index 8b12923ca1..0000000000
--- a/src/misc/license.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as fs from 'fs';
-import { fileURLToPath } from 'url';
-import { dirname } from 'path';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-const license = fs.readFileSync(_dirname + '/../../LICENSE', 'utf-8');
-
-const licenseHtml = license
-	.replace(/\r\n/g, '\n')
-	.replace(/(.)\n(.)/g, '$1 $2')
-	.replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
-
-export {
-	license,
-	licenseHtml
-};
diff --git a/src/misc/normalize-for-search.ts b/src/misc/normalize-for-search.ts
deleted file mode 100644
index 200540566e..0000000000
--- a/src/misc/normalize-for-search.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export function normalizeForSearch(tag: string): string {
-	// ref.
-	// - https://analytics-note.xyz/programming/unicode-normalization-forms/
-	// - https://maku77.github.io/js/string/normalize.html
-	return tag.normalize('NFKC').toLowerCase();
-}
diff --git a/src/misc/nyaize.ts b/src/misc/nyaize.ts
deleted file mode 100644
index 500d1db2cb..0000000000
--- a/src/misc/nyaize.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export function nyaize(text: string): string {
-	return text
-		// ja-JP
-		.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ﾅ/g, 'ﾆｬ')
-		// en-US
-		.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
-		.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
-		.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
-		// ko-KR
-		.replace(/[나-낳]/g, match => String.fromCharCode(
-			match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
-		))
-		.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
-		.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
-}
diff --git a/src/misc/populate-emojis.ts b/src/misc/populate-emojis.ts
deleted file mode 100644
index f0a8bde31e..0000000000
--- a/src/misc/populate-emojis.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { In } from 'typeorm';
-import { Emojis } from '@/models/index';
-import { Emoji } from '@/models/entities/emoji';
-import { Note } from '@/models/entities/note';
-import { Cache } from './cache';
-import { isSelfHost, toPunyNullable } from './convert-host';
-import { decodeReaction } from './reaction-lib';
-import config from '@/config/index';
-import { query } from '@/prelude/url';
-
-const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
-
-/**
- * 添付用絵文字情報
- */
-type PopulatedEmoji = {
-	name: string;
-	url: string;
-};
-
-function normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
-	// クエリに使うホスト
-	let host = src === '.' ? null	// .はローカルホスト (ここがマッチするのはリアクションのみ)
-		: src === undefined ? noteUserHost	// ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
-		: isSelfHost(src) ? null	// 自ホスト指定
-		: (src || noteUserHost);	// 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
-
-	host = toPunyNullable(host);
-
-	return host;
-}
-
-function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
-	const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
-	if (!match) return { name: null, host: null };
-
-	const name = match[1];
-
-	// ホスト正規化
-	const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
-
-	return { name, host };
-}
-
-/**
- * 添付用絵文字情報を解決する
- * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
- * @param noteUserHost ノートやユーザープロフィールの所有者のホスト
- * @returns 絵文字情報, nullは未マッチを意味する
- */
-export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
-	const { name, host } = parseEmojiStr(emojiName, noteUserHost);
-	if (name == null) return null;
-
-	const queryOrNull = async () => (await Emojis.findOne({
-		name,
-		host
-	})) || null;
-
-	const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
-
-	if (emoji == null) return null;
-
-	const isLocal = emoji.host == null;
-	const url = isLocal ? emoji.url : `${config.url}/proxy/image.png?${query({url: emoji.url})}`;
-
-	return {
-		name: emojiName,
-		url,
-	};
-}
-
-/**
- * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される)
- */
-export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> {
-	const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost)));
-	return emojis.filter((x): x is PopulatedEmoji => x != null);
-}
-
-export function aggregateNoteEmojis(notes: Note[]) {
-	let emojis: { name: string | null; host: string | null; }[] = [];
-	for (const note of notes) {
-		emojis = emojis.concat(note.emojis
-			.map(e => parseEmojiStr(e, note.userHost)));
-		if (note.renote) {
-			emojis = emojis.concat(note.renote.emojis
-				.map(e => parseEmojiStr(e, note.renote!.userHost)));
-			if (note.renote.user) {
-				emojis = emojis.concat(note.renote.user.emojis
-					.map(e => parseEmojiStr(e, note.renote!.userHost)));
-			}
-		}
-		const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
-		emojis = emojis.concat(customReactions);
-		if (note.user) {
-			emojis = emojis.concat(note.user.emojis
-				.map(e => parseEmojiStr(e, note.userHost)));
-		}
-	}
-	return emojis.filter(x => x.name != null) as { name: string; host: string | null; }[];
-}
-
-/**
- * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
- */
-export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
-	const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null);
-	const emojisQuery: any[] = [];
-	const hosts = new Set(notCachedEmojis.map(e => e.host));
-	for (const host of hosts) {
-		emojisQuery.push({
-			name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)),
-			host: host
-		});
-	}
-	const _emojis = emojisQuery.length > 0 ? await Emojis.find({
-		where: emojisQuery,
-		select: ['name', 'host', 'url']
-	}) : [];
-	for (const emoji of _emojis) {
-		cache.set(`${emoji.name} ${emoji.host}`, emoji);
-	}
-}
diff --git a/src/misc/reaction-lib.ts b/src/misc/reaction-lib.ts
deleted file mode 100644
index 46dedfa24b..0000000000
--- a/src/misc/reaction-lib.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { emojiRegex } from './emoji-regex';
-import { fetchMeta } from './fetch-meta';
-import { Emojis } from '@/models/index';
-import { toPunyNullable } from './convert-host';
-
-const legacies: Record<string, string> = {
-	'like':     '👍',
-	'love':     '❤', // ここに記述する場合は異体字セレクタを入れない
-	'laugh':    '😆',
-	'hmm':      '🤔',
-	'surprise': '😮',
-	'congrats': '🎉',
-	'angry':    '💢',
-	'confused': '😥',
-	'rip':      '😇',
-	'pudding':  '🍮',
-	'star':     '⭐',
-};
-
-export async function getFallbackReaction(): Promise<string> {
-	const meta = await fetchMeta();
-	return meta.useStarForReactionFallback ? '⭐' : '👍';
-}
-
-export function convertLegacyReactions(reactions: Record<string, number>) {
-	const _reactions = {} as Record<string, number>;
-
-	for (const reaction of Object.keys(reactions)) {
-		if (reactions[reaction] <= 0) continue;
-
-		if (Object.keys(legacies).includes(reaction)) {
-			if (_reactions[legacies[reaction]]) {
-				_reactions[legacies[reaction]] += reactions[reaction];
-			} else {
-				_reactions[legacies[reaction]] = reactions[reaction];
-			}
-		} else {
-			if (_reactions[reaction]) {
-				_reactions[reaction] += reactions[reaction];
-			} else {
-				_reactions[reaction] = reactions[reaction];
-			}
-		}
-	}
-
-	const _reactions2 = {} as Record<string, number>;
-
-	for (const reaction of Object.keys(_reactions)) {
-		_reactions2[decodeReaction(reaction).reaction] = _reactions[reaction];
-	}
-
-	return _reactions2;
-}
-
-export async function toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
-	if (reaction == null) return await getFallbackReaction();
-
-	reacterHost = toPunyNullable(reacterHost);
-
-	// 文字列タイプのリアクションを絵文字に変換
-	if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
-
-	// Unicode絵文字
-	const match = emojiRegex.exec(reaction);
-	if (match) {
-		// 合字を含む1つの絵文字
-		const unicode = match[0];
-
-		// 異体字セレクタ除去
-		return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
-	}
-
-	const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
-	if (custom) {
-		const name = custom[1];
-		const emoji = await Emojis.findOne({
-			host: reacterHost || null,
-			name,
-		});
-
-		if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
-	}
-
-	return await getFallbackReaction();
-}
-
-type DecodedReaction = {
-	/**
-	 * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.')
-	 */
-	reaction: string;
-
-	/**
-	 * name (カスタム絵文字の場合name, Emojiクエリに使う)
-	 */
-	name?: string;
-
-	/**
-	 * host (カスタム絵文字の場合host, Emojiクエリに使う)
-	 */
-	host?: string | null;
-};
-
-export function decodeReaction(str: string): DecodedReaction {
-	const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
-
-	if (custom) {
-		const name = custom[1];
-		const host = custom[2] || null;
-
-		return {
-			reaction: `:${name}@${host || '.'}:`,	// ローカル分は@以降を省略するのではなく.にする
-			name,
-			host
-		};
-	}
-
-	return {
-		reaction: str,
-		name: undefined,
-		host: undefined
-	};
-}
-
-export function convertLegacyReaction(reaction: string): string {
-	reaction = decodeReaction(reaction).reaction;
-	if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
-	return reaction;
-}
diff --git a/src/misc/safe-for-sql.ts b/src/misc/safe-for-sql.ts
deleted file mode 100644
index 02eb7f0a26..0000000000
--- a/src/misc/safe-for-sql.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function safeForSql(text: string): boolean {
-	return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text);
-}
diff --git a/src/misc/schema.ts b/src/misc/schema.ts
deleted file mode 100644
index 4131875ef7..0000000000
--- a/src/misc/schema.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { SimpleObj, SimpleSchema } from './simple-schema';
-import { packedUserSchema } from '@/models/repositories/user';
-import { packedNoteSchema } from '@/models/repositories/note';
-import { packedUserListSchema } from '@/models/repositories/user-list';
-import { packedAppSchema } from '@/models/repositories/app';
-import { packedMessagingMessageSchema } from '@/models/repositories/messaging-message';
-import { packedNotificationSchema } from '@/models/repositories/notification';
-import { packedDriveFileSchema } from '@/models/repositories/drive-file';
-import { packedDriveFolderSchema } from '@/models/repositories/drive-folder';
-import { packedFollowingSchema } from '@/models/repositories/following';
-import { packedMutingSchema } from '@/models/repositories/muting';
-import { packedBlockingSchema } from '@/models/repositories/blocking';
-import { packedNoteReactionSchema } from '@/models/repositories/note-reaction';
-import { packedHashtagSchema } from '@/models/repositories/hashtag';
-import { packedPageSchema } from '@/models/repositories/page';
-import { packedUserGroupSchema } from '@/models/repositories/user-group';
-import { packedNoteFavoriteSchema } from '@/models/repositories/note-favorite';
-import { packedChannelSchema } from '@/models/repositories/channel';
-import { packedAntennaSchema } from '@/models/repositories/antenna';
-import { packedClipSchema } from '@/models/repositories/clip';
-import { packedFederationInstanceSchema } from '@/models/repositories/federation-instance';
-import { packedQueueCountSchema } from '@/models/repositories/queue';
-import { packedGalleryPostSchema } from '@/models/repositories/gallery-post';
-import { packedEmojiSchema } from '@/models/repositories/emoji';
-import { packedReversiGameSchema } from '@/models/repositories/games/reversi/game';
-import { packedReversiMatchingSchema } from '@/models/repositories/games/reversi/matching';
-
-export const refs = {
-	User: packedUserSchema,
-	UserList: packedUserListSchema,
-	UserGroup: packedUserGroupSchema,
-	App: packedAppSchema,
-	MessagingMessage: packedMessagingMessageSchema,
-	Note: packedNoteSchema,
-	NoteReaction: packedNoteReactionSchema,
-	NoteFavorite: packedNoteFavoriteSchema,
-	Notification: packedNotificationSchema,
-	DriveFile: packedDriveFileSchema,
-	DriveFolder: packedDriveFolderSchema,
-	Following: packedFollowingSchema,
-	Muting: packedMutingSchema,
-	Blocking: packedBlockingSchema,
-	Hashtag: packedHashtagSchema,
-	Page: packedPageSchema,
-	Channel: packedChannelSchema,
-	QueueCount: packedQueueCountSchema,
-	Antenna: packedAntennaSchema,
-	Clip: packedClipSchema,
-	FederationInstance: packedFederationInstanceSchema,
-	GalleryPost: packedGalleryPostSchema,
-	Emoji: packedEmojiSchema,
-	ReversiGame: packedReversiGameSchema,
-	ReversiMatching: packedReversiMatchingSchema,
-};
-
-export type Packed<x extends keyof typeof refs> = ObjType<(typeof refs[x])['properties']>;
-
-export interface Schema extends SimpleSchema {
-	items?: Schema;
-	properties?: Obj;
-	ref?: keyof typeof refs;
-}
-
-type NonUndefinedPropertyNames<T extends Obj> = {
-	[K in keyof T]: T[K]['optional'] extends true ? never : K
-}[keyof T];
-
-type UndefinedPropertyNames<T extends Obj> = {
-	[K in keyof T]: T[K]['optional'] extends true ? K : never
-}[keyof T];
-
-type OnlyRequired<T extends Obj> = Pick<T, NonUndefinedPropertyNames<T>>;
-type OnlyOptional<T extends Obj> = Pick<T, UndefinedPropertyNames<T>>;
-
-export interface Obj extends SimpleObj { [key: string]: Schema; }
-
-export type ObjType<s extends Obj> =
-	{ [P in keyof OnlyOptional<s>]?: SchemaType<s[P]> } &
-	{ [P in keyof OnlyRequired<s>]: SchemaType<s[P]> };
-
-// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2
-type MyType<T extends Schema> = {
-	0: any;
-	1: SchemaType<T>;
-}[T extends Schema ? 1 : 0];
-
-type NullOrUndefined<p extends Schema, T> =
-	p['nullable'] extends true
-		?	p['optional'] extends true
-			? (T | null | undefined)
-			: (T | null)
-		: p['optional'] extends true
-			? (T | undefined)
-			: T;
-
-export type SchemaType<p extends Schema> =
-	p['type'] extends 'number' ? NullOrUndefined<p, number> :
-	p['type'] extends 'string' ? NullOrUndefined<p, string> :
-	p['type'] extends 'boolean' ? NullOrUndefined<p, boolean> :
-	p['type'] extends 'array' ? NullOrUndefined<p, MyType<NonNullable<p['items']>>[]> :
-	p['type'] extends 'object' ? (
-		p['ref'] extends keyof typeof refs
-			? NullOrUndefined<p, Packed<p['ref']>>
-			: NullOrUndefined<p, ObjType<NonNullable<p['properties']>>>
-	) :
-	p['type'] extends 'any' ? NullOrUndefined<p, any> :
-	any;
diff --git a/src/misc/secure-rndstr.ts b/src/misc/secure-rndstr.ts
deleted file mode 100644
index 76ee1225eb..0000000000
--- a/src/misc/secure-rndstr.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as crypto from 'crypto';
-
-const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
-const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-export function secureRndstr(length = 32, useLU = true): string {
-	const chars = useLU ? LU_CHARS : L_CHARS;
-	const chars_len = chars.length;
-
-	let str = '';
-
-	for (let i = 0; i < length; i++) {
-		let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len);
-		if (rand === chars_len) {
-			rand = chars_len - 1;
-		}
-		str += chars.charAt(rand);
-	}
-
-	return str;
-}
diff --git a/src/misc/show-machine-info.ts b/src/misc/show-machine-info.ts
deleted file mode 100644
index 58747c1152..0000000000
--- a/src/misc/show-machine-info.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as os from 'os';
-import * as sysUtils from 'systeminformation';
-import Logger from '@/services/logger';
-
-export async function showMachineInfo(parentLogger: Logger) {
-	const logger = parentLogger.createSubLogger('machine');
-	logger.debug(`Hostname: ${os.hostname()}`);
-	logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
-	const mem = await sysUtils.mem();
-	const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
-	const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
-	logger.debug(`CPU: ${os.cpus().length} core MEM: ${totalmem}GB (available: ${availmem}GB)`);
-}
diff --git a/src/misc/simple-schema.ts b/src/misc/simple-schema.ts
deleted file mode 100644
index abbb348e24..0000000000
--- a/src/misc/simple-schema.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export interface SimpleSchema {
-	type: 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
-	nullable: boolean;
-	optional: boolean;
-	items?: SimpleSchema;
-	properties?: SimpleObj;
-	description?: string;
-	example?: any;
-	format?: string;
-	ref?: string;
-	enum?: string[];
-	default?: boolean | null;
-}
-
-export interface SimpleObj { [key: string]: SimpleSchema; }
diff --git a/src/misc/truncate.ts b/src/misc/truncate.ts
deleted file mode 100644
index cb120331a1..0000000000
--- a/src/misc/truncate.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { substring } from 'stringz';
-
-export function truncate(input: string, size: number): string;
-export function truncate(input: string | undefined, size: number): string | undefined;
-export function truncate(input: string | undefined, size: number): string | undefined {
-	if (!input) {
-		return input;
-	} else {
-		return substring(input, 0, size);
-	}
-}
diff --git a/src/misc/twemoji-base.ts b/src/misc/twemoji-base.ts
deleted file mode 100644
index cd50311b15..0000000000
--- a/src/misc/twemoji-base.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const twemojiSvgBase = '/twemoji';
diff --git a/src/models/entities/abuse-user-report.ts b/src/models/entities/abuse-user-report.ts
deleted file mode 100644
index c0cff139f6..0000000000
--- a/src/models/entities/abuse-user-report.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class AbuseUserReport {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the AbuseUserReport.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public targetUserId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public targetUser: User | null;
-
-	@Index()
-	@Column(id())
-	public reporterId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public reporter: User | null;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public assigneeId: User['id'] | null;
-
-	@ManyToOne(type => User, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public assignee: User | null;
-
-	@Index()
-	@Column('boolean', {
-		default: false
-	})
-	public resolved: boolean;
-
-	@Column('varchar', {
-		length: 2048,
-	})
-	public comment: string;
-
-	//#region Denormalized fields
-	@Index()
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public targetUserHost: string | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public reporterHost: string | null;
-	//#endregion
-}
diff --git a/src/models/entities/access-token.ts b/src/models/entities/access-token.ts
deleted file mode 100644
index 5f41b3c1fc..0000000000
--- a/src/models/entities/access-token.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
-import { User } from './user';
-import { App } from './app';
-import { id } from '../id';
-
-@Entity()
-export class AccessToken {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the AccessToken.'
-	})
-	public createdAt: Date;
-
-	@Column('timestamp with time zone', {
-		nullable: true,
-		default: null,
-	})
-	public lastUsedAt: Date | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 128
-	})
-	public token: string;
-
-	@Index()
-	@Column('varchar', {
-		length: 128,
-		nullable: true,
-		default: null
-	})
-	public session: string | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 128
-	})
-	public hash: string;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column({
-		...id(),
-		nullable: true,
-		default: null
-	})
-	public appId: App['id'] | null;
-
-	@ManyToOne(type => App, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public app: App | null;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true,
-		default: null
-	})
-	public name: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true,
-		default: null
-	})
-	public description: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true,
-		default: null
-	})
-	public iconUrl: string | null;
-
-	@Column('varchar', {
-		length: 64, array: true,
-		default: '{}'
-	})
-	public permission: string[];
-
-	@Column('boolean', {
-		default: false
-	})
-	public fetched: boolean;
-}
diff --git a/src/models/entities/ad.ts b/src/models/entities/ad.ts
deleted file mode 100644
index b2fc04c4f0..0000000000
--- a/src/models/entities/ad.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
-import { id } from '../id';
-
-@Entity()
-export class Ad {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Ad.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The expired date of the Ad.'
-	})
-	public expiresAt: Date;
-
-	@Column('varchar', {
-		length: 32, nullable: false
-	})
-	public place: string;
-
-	// 今は使われていないが将来的に活用される可能性はある
-	@Column('varchar', {
-		length: 32, nullable: false
-	})
-	public priority: string;
-
-	@Column('integer', {
-		default: 1, nullable: false
-	})
-	public ratio: number;
-
-	@Column('varchar', {
-		length: 1024, nullable: false
-	})
-	public url: string;
-
-	@Column('varchar', {
-		length: 1024, nullable: false
-	})
-	public imageUrl: string;
-
-	@Column('varchar', {
-		length: 8192, nullable: false
-	})
-	public memo: string;
-
-	constructor(data: Partial<Ad>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/announcement-read.ts b/src/models/entities/announcement-read.ts
deleted file mode 100644
index 892beb826f..0000000000
--- a/src/models/entities/announcement-read.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { Announcement } from './announcement';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'announcementId'], { unique: true })
-export class AnnouncementRead {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the AnnouncementRead.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column(id())
-	public announcementId: Announcement['id'];
-
-	@ManyToOne(type => Announcement, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public announcement: Announcement | null;
-}
diff --git a/src/models/entities/announcement.ts b/src/models/entities/announcement.ts
deleted file mode 100644
index 06d379c229..0000000000
--- a/src/models/entities/announcement.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
-import { id } from '../id';
-
-@Entity()
-export class Announcement {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Announcement.'
-	})
-	public createdAt: Date;
-
-	@Column('timestamp with time zone', {
-		comment: 'The updated date of the Announcement.',
-		nullable: true
-	})
-	public updatedAt: Date | null;
-
-	@Column('varchar', {
-		length: 8192, nullable: false
-	})
-	public text: string;
-
-	@Column('varchar', {
-		length: 256, nullable: false
-	})
-	public title: string;
-
-	@Column('varchar', {
-		length: 1024, nullable: true
-	})
-	public imageUrl: string | null;
-
-	constructor(data: Partial<Announcement>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/antenna-note.ts b/src/models/entities/antenna-note.ts
deleted file mode 100644
index 9b911524ef..0000000000
--- a/src/models/entities/antenna-note.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
-import { Note } from './note';
-import { Antenna } from './antenna';
-import { id } from '../id';
-
-@Entity()
-@Index(['noteId', 'antennaId'], { unique: true })
-export class AntennaNote {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The note ID.'
-	})
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The antenna ID.'
-	})
-	public antennaId: Antenna['id'];
-
-	@ManyToOne(type => Antenna, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public antenna: Antenna | null;
-
-	@Index()
-	@Column('boolean', {
-		default: false
-	})
-	public read: boolean;
-}
diff --git a/src/models/entities/antenna.ts b/src/models/entities/antenna.ts
deleted file mode 100644
index bcfe09a829..0000000000
--- a/src/models/entities/antenna.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { UserList } from './user-list';
-import { UserGroupJoining } from './user-group-joining';
-
-@Entity()
-export class Antenna {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Antenna.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The owner ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 128,
-		comment: 'The name of the Antenna.'
-	})
-	public name: string;
-
-	@Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] })
-	public src: 'home' | 'all' | 'users' | 'list' | 'group';
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public userListId: UserList['id'] | null;
-
-	@ManyToOne(type => UserList, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public userList: UserList | null;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public userGroupJoiningId: UserGroupJoining['id'] | null;
-
-	@ManyToOne(type => UserGroupJoining, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public userGroupJoining: UserGroupJoining | null;
-
-	@Column('varchar', {
-		length: 1024, array: true,
-		default: '{}'
-	})
-	public users: string[];
-
-	@Column('jsonb', {
-		default: []
-	})
-	public keywords: string[][];
-
-	@Column('jsonb', {
-		default: []
-	})
-	public excludeKeywords: string[][];
-
-	@Column('boolean', {
-		default: false
-	})
-	public caseSensitive: boolean;
-
-	@Column('boolean', {
-		default: false
-	})
-	public withReplies: boolean;
-
-	@Column('boolean')
-	public withFile: boolean;
-
-	@Column('varchar', {
-		length: 2048, nullable: true,
-	})
-	public expression: string | null;
-
-	@Column('boolean')
-	public notify: boolean;
-}
diff --git a/src/models/entities/app.ts b/src/models/entities/app.ts
deleted file mode 100644
index ea87546311..0000000000
--- a/src/models/entities/app.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class App {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the App.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The owner ID.'
-	})
-	public userId: User['id'] | null;
-
-	@ManyToOne(type => User, {
-		onDelete: 'SET NULL',
-		nullable: true,
-	})
-	public user: User | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 64,
-		comment: 'The secret key of the App.'
-	})
-	public secret: string;
-
-	@Column('varchar', {
-		length: 128,
-		comment: 'The name of the App.'
-	})
-	public name: string;
-
-	@Column('varchar', {
-		length: 512,
-		comment: 'The description of the App.'
-	})
-	public description: string;
-
-	@Column('varchar', {
-		length: 64, array: true,
-		comment: 'The permission of the App.'
-	})
-	public permission: string[];
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The callbackUrl of the App.'
-	})
-	public callbackUrl: string | null;
-}
diff --git a/src/models/entities/attestation-challenge.ts b/src/models/entities/attestation-challenge.ts
deleted file mode 100644
index 942747c02f..0000000000
--- a/src/models/entities/attestation-challenge.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class AttestationChallenge {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@PrimaryColumn(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 64,
-		comment: 'Hex-encoded sha256 hash of the challenge.'
-	})
-	public challenge: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The date challenge was created for expiry purposes.'
-	})
-	public createdAt: Date;
-
-	@Column('boolean', {
-		comment:
-			'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.',
-		default: false
-	})
-	public registrationChallenge: boolean;
-
-	constructor(data: Partial<AttestationChallenge>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/auth-session.ts b/src/models/entities/auth-session.ts
deleted file mode 100644
index 4eec27e3f6..0000000000
--- a/src/models/entities/auth-session.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
-import { User } from './user';
-import { App } from './app';
-import { id } from '../id';
-
-@Entity()
-export class AuthSession {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the AuthSession.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column('varchar', {
-		length: 128
-	})
-	public token: string;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE',
-		nullable: true
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column(id())
-	public appId: App['id'];
-
-	@ManyToOne(type => App, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public app: App | null;
-}
diff --git a/src/models/entities/blocking.ts b/src/models/entities/blocking.ts
deleted file mode 100644
index 48487cb086..0000000000
--- a/src/models/entities/blocking.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-@Index(['blockerId', 'blockeeId'], { unique: true })
-export class Blocking {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Blocking.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The blockee user ID.'
-	})
-	public blockeeId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public blockee: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The blocker user ID.'
-	})
-	public blockerId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public blocker: User | null;
-}
diff --git a/src/models/entities/channel-following.ts b/src/models/entities/channel-following.ts
deleted file mode 100644
index fca801e5ab..0000000000
--- a/src/models/entities/channel-following.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { Channel } from './channel';
-
-@Entity()
-@Index(['followerId', 'followeeId'], { unique: true })
-export class ChannelFollowing {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the ChannelFollowing.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The followee channel ID.'
-	})
-	public followeeId: Channel['id'];
-
-	@ManyToOne(type => Channel, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public followee: Channel | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The follower user ID.'
-	})
-	public followerId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public follower: User | null;
-}
diff --git a/src/models/entities/channel-note-pining.ts b/src/models/entities/channel-note-pining.ts
deleted file mode 100644
index 26a7eb501f..0000000000
--- a/src/models/entities/channel-note-pining.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { Note } from './note';
-import { Channel } from './channel';
-import { id } from '../id';
-
-@Entity()
-@Index(['channelId', 'noteId'], { unique: true })
-export class ChannelNotePining {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the ChannelNotePining.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public channelId: Channel['id'];
-
-	@ManyToOne(type => Channel, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public channel: Channel | null;
-
-	@Column(id())
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-}
diff --git a/src/models/entities/channel.ts b/src/models/entities/channel.ts
deleted file mode 100644
index f2d713612d..0000000000
--- a/src/models/entities/channel.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { DriveFile } from './drive-file';
-
-@Entity()
-export class Channel {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Channel.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		nullable: true
-	})
-	public lastNotedAt: Date | null;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The owner ID.'
-	})
-	public userId: User['id'] | null;
-
-	@ManyToOne(type => User, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 128,
-		comment: 'The name of the Channel.'
-	})
-	public name: string;
-
-	@Column('varchar', {
-		length: 2048, nullable: true,
-		comment: 'The description of the Channel.'
-	})
-	public description: string | null;
-
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The ID of banner Channel.'
-	})
-	public bannerId: DriveFile['id'] | null;
-
-	@ManyToOne(type => DriveFile, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public banner: DriveFile | null;
-
-	@Index()
-	@Column('integer', {
-		default: 0,
-		comment: 'The count of notes.'
-	})
-	public notesCount: number;
-
-	@Index()
-	@Column('integer', {
-		default: 0,
-		comment: 'The count of users.'
-	})
-	public usersCount: number;
-}
diff --git a/src/models/entities/clip-note.ts b/src/models/entities/clip-note.ts
deleted file mode 100644
index 7d96b2ef7a..0000000000
--- a/src/models/entities/clip-note.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
-import { Note } from './note';
-import { Clip } from './clip';
-import { id } from '../id';
-
-@Entity()
-@Index(['noteId', 'clipId'], { unique: true })
-export class ClipNote {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The note ID.'
-	})
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The clip ID.'
-	})
-	public clipId: Clip['id'];
-
-	@ManyToOne(type => Clip, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public clip: Clip | null;
-}
diff --git a/src/models/entities/clip.ts b/src/models/entities/clip.ts
deleted file mode 100644
index 66b5b8847e..0000000000
--- a/src/models/entities/clip.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class Clip {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Clip.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The owner ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 128,
-		comment: 'The name of the Clip.'
-	})
-	public name: string;
-
-	@Column('boolean', {
-		default: false
-	})
-	public isPublic: boolean;
-
-	@Column('varchar', {
-		length: 2048, nullable: true, default: null,
-		comment: 'The description of the Clip.'
-	})
-	public description: string | null;
-}
diff --git a/src/models/entities/drive-file.ts b/src/models/entities/drive-file.ts
deleted file mode 100644
index 698dfac222..0000000000
--- a/src/models/entities/drive-file.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { DriveFolder } from './drive-folder';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'folderId', 'id'])
-export class DriveFile {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the DriveFile.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The owner ID.'
-	})
-	public userId: User['id'] | null;
-
-	@ManyToOne(type => User, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: 'The host of owner. It will be null if the user in local.'
-	})
-	public userHost: string | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 32,
-		comment: 'The MD5 hash of the DriveFile.'
-	})
-	public md5: string;
-
-	@Column('varchar', {
-		length: 256,
-		comment: 'The file name of the DriveFile.'
-	})
-	public name: string;
-
-	@Index()
-	@Column('varchar', {
-		length: 128,
-		comment: 'The content type (MIME) of the DriveFile.'
-	})
-	public type: string;
-
-	@Column('integer', {
-		comment: 'The file size (bytes) of the DriveFile.'
-	})
-	public size: number;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The comment of the DriveFile.'
-	})
-	public comment: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: 'The BlurHash string.'
-	})
-	public blurhash: string | null;
-
-	@Column('jsonb', {
-		default: {},
-		comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
-	})
-	public properties: { width?: number; height?: number; avgColor?: string };
-
-	@Index()
-	@Column('boolean')
-	public storedInternal: boolean;
-
-	@Column('varchar', {
-		length: 512,
-		comment: 'The URL of the DriveFile.'
-	})
-	public url: string;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The URL of the thumbnail of the DriveFile.'
-	})
-	public thumbnailUrl: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The URL of the webpublic of the DriveFile.'
-	})
-	public webpublicUrl: string | null;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 256, nullable: true,
-	})
-	public accessKey: string | null;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 256, nullable: true,
-	})
-	public thumbnailAccessKey: string | null;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 256, nullable: true,
-	})
-	public webpublicAccessKey: string | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.'
-	})
-	public uri: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-	})
-	public src: string | null;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The parent folder ID. If null, it means the DriveFile is located in root.'
-	})
-	public folderId: DriveFolder['id'] | null;
-
-	@ManyToOne(type => DriveFolder, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public folder: DriveFolder | null;
-
-	@Index()
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the DriveFile is NSFW.'
-	})
-	public isSensitive: boolean;
-
-	/**
-	 * 外部の(信頼されていない)URLへの直リンクか否か
-	 */
-	@Index()
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the DriveFile is direct link to remote server.'
-	})
-	public isLink: boolean;
-}
diff --git a/src/models/entities/drive-folder.ts b/src/models/entities/drive-folder.ts
deleted file mode 100644
index a80d075855..0000000000
--- a/src/models/entities/drive-folder.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class DriveFolder {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the DriveFolder.'
-	})
-	public createdAt: Date;
-
-	@Column('varchar', {
-		length: 128,
-		comment: 'The name of the DriveFolder.'
-	})
-	public name: string;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The owner ID.'
-	})
-	public userId: User['id'] | null;
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.'
-	})
-	public parentId: DriveFolder['id'] | null;
-
-	@ManyToOne(type => DriveFolder, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public parent: DriveFolder | null;
-}
diff --git a/src/models/entities/emoji.ts b/src/models/entities/emoji.ts
deleted file mode 100644
index d6080ae099..0000000000
--- a/src/models/entities/emoji.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
-import { id } from '../id';
-
-@Entity()
-@Index(['name', 'host'], { unique: true })
-export class Emoji {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		nullable: true
-	})
-	public updatedAt: Date | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 128
-	})
-	public name: string;
-
-	@Index()
-	@Column('varchar', {
-		length: 128, nullable: true
-	})
-	public host: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true
-	})
-	public category: string | null;
-
-	@Column('varchar', {
-		length: 512,
-	})
-	public url: string;
-
-	@Column('varchar', {
-		length: 512, nullable: true
-	})
-	public uri: string | null;
-
-	@Column('varchar', {
-		length: 64, nullable: true
-	})
-	public type: string | null;
-
-	@Column('varchar', {
-		array: true, length: 128, default: '{}'
-	})
-	public aliases: string[];
-}
diff --git a/src/models/entities/follow-request.ts b/src/models/entities/follow-request.ts
deleted file mode 100644
index 22ec263962..0000000000
--- a/src/models/entities/follow-request.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-@Index(['followerId', 'followeeId'], { unique: true })
-export class FollowRequest {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the FollowRequest.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The followee user ID.'
-	})
-	public followeeId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public followee: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The follower user ID.'
-	})
-	public followerId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public follower: User | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: 'id of Follow Activity.'
-	})
-	public requestId: string | null;
-
-	//#region Denormalized fields
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followerHost: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followerInbox: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followerSharedInbox: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followeeHost: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followeeInbox: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followeeSharedInbox: string | null;
-	//#endregion
-}
diff --git a/src/models/entities/following.ts b/src/models/entities/following.ts
deleted file mode 100644
index ee3286a1a1..0000000000
--- a/src/models/entities/following.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-@Index(['followerId', 'followeeId'], { unique: true })
-export class Following {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Following.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The followee user ID.'
-	})
-	public followeeId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public followee: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The follower user ID.'
-	})
-	public followerId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public follower: User | null;
-
-	//#region Denormalized fields
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followerHost: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followerInbox: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followerSharedInbox: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followeeHost: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followeeInbox: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public followeeSharedInbox: string | null;
-	//#endregion
-}
diff --git a/src/models/entities/gallery-like.ts b/src/models/entities/gallery-like.ts
deleted file mode 100644
index 7d084a2275..0000000000
--- a/src/models/entities/gallery-like.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { GalleryPost } from './gallery-post';
-
-@Entity()
-@Index(['userId', 'postId'], { unique: true })
-export class GalleryLike {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone')
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column(id())
-	public postId: GalleryPost['id'];
-
-	@ManyToOne(type => GalleryPost, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public post: GalleryPost | null;
-}
diff --git a/src/models/entities/gallery-post.ts b/src/models/entities/gallery-post.ts
deleted file mode 100644
index f59cd671f3..0000000000
--- a/src/models/entities/gallery-post.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { DriveFile } from './drive-file';
-
-@Entity()
-export class GalleryPost {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the GalleryPost.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The updated date of the GalleryPost.'
-	})
-	public updatedAt: Date;
-
-	@Column('varchar', {
-		length: 256,
-	})
-	public title: string;
-
-	@Column('varchar', {
-		length: 2048, nullable: true
-	})
-	public description: string | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The ID of author.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		array: true, default: '{}'
-	})
-	public fileIds: DriveFile['id'][];
-
-	@Index()
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the post is sensitive.'
-	})
-	public isSensitive: boolean;
-
-	@Index()
-	@Column('integer', {
-		default: 0
-	})
-	public likedCount: number;
-
-	@Index()
-	@Column('varchar', {
-		length: 128, array: true, default: '{}'
-	})
-	public tags: string[];
-
-	constructor(data: Partial<GalleryPost>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/games/reversi/game.ts b/src/models/entities/games/reversi/game.ts
deleted file mode 100644
index 9deacaf5c6..0000000000
--- a/src/models/entities/games/reversi/game.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from '../../user';
-import { id } from '../../../id';
-
-@Entity()
-export class ReversiGame {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the ReversiGame.'
-	})
-	public createdAt: Date;
-
-	@Column('timestamp with time zone', {
-		nullable: true,
-		comment: 'The started date of the ReversiGame.'
-	})
-	public startedAt: Date | null;
-
-	@Column(id())
-	public user1Id: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user1: User | null;
-
-	@Column(id())
-	public user2Id: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user2: User | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public user1Accepted: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public user2Accepted: boolean;
-
-	/**
-	 * どちらのプレイヤーが先行(黒)か
-	 * 1 ... user1
-	 * 2 ... user2
-	 */
-	@Column('integer', {
-		nullable: true,
-	})
-	public black: number | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public isStarted: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public isEnded: boolean;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public winnerId: User['id'] | null;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public surrendered: User['id'] | null;
-
-	@Column('jsonb', {
-		default: [],
-	})
-	public logs: {
-		at: Date;
-		color: boolean;
-		pos: number;
-	}[];
-
-	@Column('varchar', {
-		array: true, length: 64,
-	})
-	public map: string[];
-
-	@Column('varchar', {
-		length: 32
-	})
-	public bw: string;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public isLlotheo: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public canPutEverywhere: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public loopedBoard: boolean;
-
-	@Column('jsonb', {
-		nullable: true, default: null,
-	})
-	public form1: any | null;
-
-	@Column('jsonb', {
-		nullable: true, default: null,
-	})
-	public form2: any | null;
-
-	/**
-	 * ログのposを文字列としてすべて連結したもののCRC32値
-	 */
-	@Column('varchar', {
-		length: 32, nullable: true
-	})
-	public crc32: string | null;
-}
diff --git a/src/models/entities/games/reversi/matching.ts b/src/models/entities/games/reversi/matching.ts
deleted file mode 100644
index 477a29316e..0000000000
--- a/src/models/entities/games/reversi/matching.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from '../../user';
-import { id } from '../../../id';
-
-@Entity()
-export class ReversiMatching {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the ReversiMatching.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public parentId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public parent: User | null;
-
-	@Index()
-	@Column(id())
-	public childId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public child: User | null;
-}
diff --git a/src/models/entities/hashtag.ts b/src/models/entities/hashtag.ts
deleted file mode 100644
index 842cdaa562..0000000000
--- a/src/models/entities/hashtag.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class Hashtag {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 128
-	})
-	public name: string;
-
-	@Column({
-		...id(),
-		array: true,
-	})
-	public mentionedUserIds: User['id'][];
-
-	@Index()
-	@Column('integer', {
-		default: 0
-	})
-	public mentionedUsersCount: number;
-
-	@Column({
-		...id(),
-		array: true,
-	})
-	public mentionedLocalUserIds: User['id'][];
-
-	@Index()
-	@Column('integer', {
-		default: 0
-	})
-	public mentionedLocalUsersCount: number;
-
-	@Column({
-		...id(),
-		array: true,
-	})
-	public mentionedRemoteUserIds: User['id'][];
-
-	@Index()
-	@Column('integer', {
-		default: 0
-	})
-	public mentionedRemoteUsersCount: number;
-
-	@Column({
-		...id(),
-		array: true,
-	})
-	public attachedUserIds: User['id'][];
-
-	@Index()
-	@Column('integer', {
-		default: 0
-	})
-	public attachedUsersCount: number;
-
-	@Column({
-		...id(),
-		array: true,
-	})
-	public attachedLocalUserIds: User['id'][];
-
-	@Index()
-	@Column('integer', {
-		default: 0
-	})
-	public attachedLocalUsersCount: number;
-
-	@Column({
-		...id(),
-		array: true,
-	})
-	public attachedRemoteUserIds: User['id'][];
-
-	@Index()
-	@Column('integer', {
-		default: 0
-	})
-	public attachedRemoteUsersCount: number;
-}
diff --git a/src/models/entities/instance.ts b/src/models/entities/instance.ts
deleted file mode 100644
index 7c8719e06a..0000000000
--- a/src/models/entities/instance.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
-import { id } from '../id';
-
-@Entity()
-export class Instance {
-	@PrimaryColumn(id())
-	public id: string;
-
-	/**
-	 * このインスタンスを捕捉した日時
-	 */
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The caught date of the Instance.'
-	})
-	public caughtAt: Date;
-
-	/**
-	 * ホスト
-	 */
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 128,
-		comment: 'The host of the Instance.'
-	})
-	public host: string;
-
-	/**
-	 * インスタンスのユーザー数
-	 */
-	@Column('integer', {
-		default: 0,
-		comment: 'The count of the users of the Instance.'
-	})
-	public usersCount: number;
-
-	/**
-	 * インスタンスの投稿数
-	 */
-	@Column('integer', {
-		default: 0,
-		comment: 'The count of the notes of the Instance.'
-	})
-	public notesCount: number;
-
-	/**
-	 * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数
-	 */
-	@Column('integer', {
-		default: 0,
-	})
-	public followingCount: number;
-
-	/**
-	 * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数
-	 */
-	@Column('integer', {
-		default: 0,
-	})
-	public followersCount: number;
-
-	/**
-	 * ドライブ使用量
-	 */
-	@Column('bigint', {
-		default: 0,
-	})
-	public driveUsage: number;
-
-	/**
-	 * ドライブのファイル数
-	 */
-	@Column('integer', {
-		default: 0,
-	})
-	public driveFiles: number;
-
-	/**
-	 * 直近のリクエスト送信日時
-	 */
-	@Column('timestamp with time zone', {
-		nullable: true,
-	})
-	public latestRequestSentAt: Date | null;
-
-	/**
-	 * 直近のリクエスト送信時のHTTPステータスコード
-	 */
-	@Column('integer', {
-		nullable: true,
-	})
-	public latestStatus: number | null;
-
-	/**
-	 * 直近のリクエスト受信日時
-	 */
-	@Column('timestamp with time zone', {
-		nullable: true,
-	})
-	public latestRequestReceivedAt: Date | null;
-
-	/**
-	 * このインスタンスと最後にやり取りした日時
-	 */
-	@Column('timestamp with time zone')
-	public lastCommunicatedAt: Date;
-
-	/**
-	 * このインスタンスと不通かどうか
-	 */
-	@Column('boolean', {
-		default: false
-	})
-	public isNotResponding: boolean;
-
-	/**
-	 * このインスタンスへの配信を停止するか
-	 */
-	@Index()
-	@Column('boolean', {
-		default: false
-	})
-	public isSuspended: boolean;
-
-	@Column('varchar', {
-		length: 64, nullable: true, default: null,
-		comment: 'The software of the Instance.'
-	})
-	public softwareName: string | null;
-
-	@Column('varchar', {
-		length: 64, nullable: true, default: null,
-	})
-	public softwareVersion: string | null;
-
-	@Column('boolean', {
-		nullable: true, default: null,
-	})
-	public openRegistrations: boolean | null;
-
-	@Column('varchar', {
-		length: 256, nullable: true, default: null,
-	})
-	public name: string | null;
-
-	@Column('varchar', {
-		length: 4096, nullable: true, default: null,
-	})
-	public description: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true, default: null,
-	})
-	public maintainerName: string | null;
-
-	@Column('varchar', {
-		length: 256, nullable: true, default: null,
-	})
-	public maintainerEmail: string | null;
-
-	@Column('varchar', {
-		length: 256, nullable: true, default: null,
-	})
-	public iconUrl: string | null;
-
-	@Column('varchar', {
-		length: 256, nullable: true, default: null,
-	})
-	public faviconUrl: string | null;
-
-	@Column('varchar', {
-		length: 64, nullable: true, default: null,
-	})
-	public themeColor: string | null;
-
-	@Column('timestamp with time zone', {
-		nullable: true,
-	})
-	public infoUpdatedAt: Date | null;
-}
diff --git a/src/models/entities/messaging-message.ts b/src/models/entities/messaging-message.ts
deleted file mode 100644
index ac0764674c..0000000000
--- a/src/models/entities/messaging-message.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { DriveFile } from './drive-file';
-import { id } from '../id';
-import { UserGroup } from './user-group';
-
-@Entity()
-export class MessagingMessage {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the MessagingMessage.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The sender user ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column({
-		...id(), nullable: true,
-		comment: 'The recipient user ID.'
-	})
-	public recipientId: User['id'] | null;
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public recipient: User | null;
-
-	@Index()
-	@Column({
-		...id(), nullable: true,
-		comment: 'The recipient group ID.'
-	})
-	public groupId: UserGroup['id'] | null;
-
-	@ManyToOne(type => UserGroup, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public group: UserGroup | null;
-
-	@Column('varchar', {
-		length: 4096, nullable: true
-	})
-	public text: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public isRead: boolean;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-	})
-	public uri: string | null;
-
-	@Column({
-		...id(),
-		array: true, default: '{}'
-	})
-	public reads: User['id'][];
-
-	@Column({
-		...id(),
-		nullable: true,
-	})
-	public fileId: DriveFile['id'] | null;
-
-	@ManyToOne(type => DriveFile, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public file: DriveFile | null;
-}
diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts
deleted file mode 100644
index 9a1a87c155..0000000000
--- a/src/models/entities/meta.ts
+++ /dev/null
@@ -1,423 +0,0 @@
-import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { Clip } from './clip';
-
-@Entity()
-export class Meta {
-	@PrimaryColumn({
-		type: 'varchar',
-		length: 32
-	})
-	public id: string;
-
-	@Column('varchar', {
-		length: 128, nullable: true
-	})
-	public name: string | null;
-
-	@Column('varchar', {
-		length: 1024, nullable: true
-	})
-	public description: string | null;
-
-	/**
-	 * メンテナの名前
-	 */
-	@Column('varchar', {
-		length: 128, nullable: true
-	})
-	public maintainerName: string | null;
-
-	/**
-	 * メンテナの連絡先
-	 */
-	@Column('varchar', {
-		length: 128, nullable: true
-	})
-	public maintainerEmail: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public disableRegistration: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public disableLocalTimeline: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public disableGlobalTimeline: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public useStarForReactionFallback: boolean;
-
-	@Column('varchar', {
-		length: 64, array: true, default: '{}'
-	})
-	public langs: string[];
-
-	@Column('varchar', {
-		length: 256, array: true, default: '{}'
-	})
-	public pinnedUsers: string[];
-
-	@Column('varchar', {
-		length: 256, array: true, default: '{}'
-	})
-	public hiddenTags: string[];
-
-	@Column('varchar', {
-		length: 256, array: true, default: '{}'
-	})
-	public blockedHosts: string[];
-
-	@Column('varchar', {
-		length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'
-	})
-	public pinnedPages: string[];
-
-	@Column({
-		...id(),
-		nullable: true,
-	})
-	public pinnedClipId: Clip['id'] | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true,
-		default: '/assets/ai.png'
-	})
-	public mascotImageUrl: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public bannerUrl: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public backgroundImageUrl: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public logoImageUrl: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true,
-		default: 'https://xn--931a.moe/aiart/yubitun.png'
-	})
-	public errorImageUrl: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public iconUrl: string | null;
-
-	@Column('boolean', {
-		default: true,
-	})
-	public cacheRemoteFiles: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public proxyRemoteFiles: boolean;
-
-	@Column({
-		...id(),
-		nullable: true,
-	})
-	public proxyAccountId: User['id'] | null;
-
-	@ManyToOne(type => User, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public proxyAccount: User | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public emailRequiredForSignup: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public enableHcaptcha: boolean;
-
-	@Column('varchar', {
-		length: 64,
-		nullable: true
-	})
-	public hcaptchaSiteKey: string | null;
-
-	@Column('varchar', {
-		length: 64,
-		nullable: true
-	})
-	public hcaptchaSecretKey: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public enableRecaptcha: boolean;
-
-	@Column('varchar', {
-		length: 64,
-		nullable: true
-	})
-	public recaptchaSiteKey: string | null;
-
-	@Column('varchar', {
-		length: 64,
-		nullable: true
-	})
-	public recaptchaSecretKey: string | null;
-
-	@Column('integer', {
-		default: 1024,
-		comment: 'Drive capacity of a local user (MB)'
-	})
-	public localDriveCapacityMb: number;
-
-	@Column('integer', {
-		default: 32,
-		comment: 'Drive capacity of a remote user (MB)'
-	})
-	public remoteDriveCapacityMb: number;
-
-	@Column('integer', {
-		default: 500,
-		comment: 'Max allowed note text length in characters'
-	})
-	public maxNoteTextLength: number;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public summalyProxy: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public enableEmail: boolean;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public email: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public smtpSecure: boolean;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public smtpHost: string | null;
-
-	@Column('integer', {
-		nullable: true
-	})
-	public smtpPort: number | null;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public smtpUser: string | null;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public smtpPass: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public enableServiceWorker: boolean;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public swPublicKey: string | null;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public swPrivateKey: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public enableTwitterIntegration: boolean;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public twitterConsumerKey: string | null;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public twitterConsumerSecret: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public enableGithubIntegration: boolean;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public githubClientId: string | null;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public githubClientSecret: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public enableDiscordIntegration: boolean;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public discordClientId: string | null;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public discordClientSecret: string | null;
-
-	@Column('varchar', {
-		length: 128,
-		nullable: true
-	})
-	public deeplAuthKey: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public deeplIsPro: boolean;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public ToSUrl: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		default: 'https://github.com/misskey-dev/misskey',
-		nullable: false
-	})
-	public repositoryUrl: string;
-
-	@Column('varchar', {
-		length: 512,
-		default: 'https://github.com/misskey-dev/misskey/issues/new',
-		nullable: true
-	})
-	public feedbackUrl: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public useObjectStorage: boolean;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public objectStorageBucket: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public objectStoragePrefix: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public objectStorageBaseUrl: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public objectStorageEndpoint: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public objectStorageRegion: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public objectStorageAccessKey: string | null;
-
-	@Column('varchar', {
-		length: 512,
-		nullable: true
-	})
-	public objectStorageSecretKey: string | null;
-
-	@Column('integer', {
-		nullable: true
-	})
-	public objectStoragePort: number | null;
-
-	@Column('boolean', {
-		default: true,
-	})
-	public objectStorageUseSSL: boolean;
-
-	@Column('boolean', {
-		default: true,
-	})
-	public objectStorageUseProxy: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public objectStorageSetPublicRead: boolean;
-
-	@Column('boolean', {
-		default: true,
-	})
-	public objectStorageS3ForcePathStyle: boolean;
-}
diff --git a/src/models/entities/moderation-log.ts b/src/models/entities/moderation-log.ts
deleted file mode 100644
index 33d3d683ae..0000000000
--- a/src/models/entities/moderation-log.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class ModerationLog {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the ModerationLog.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 128,
-	})
-	public type: string;
-
-	@Column('jsonb')
-	public info: Record<string, any>;
-}
diff --git a/src/models/entities/muted-note.ts b/src/models/entities/muted-note.ts
deleted file mode 100644
index 521876688c..0000000000
--- a/src/models/entities/muted-note.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
-import { Note } from './note';
-import { User } from './user';
-import { id } from '../id';
-import { mutedNoteReasons } from '../../types';
-
-@Entity()
-@Index(['noteId', 'userId'], { unique: true })
-export class MutedNote {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The note ID.'
-	})
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The user ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	/**
-	 * ミュートされた理由。
-	 */
-	@Index()
-	@Column('enum', {
-		enum: mutedNoteReasons,
-		comment: 'The reason of the MutedNote.'
-	})
-	public reason: typeof mutedNoteReasons[number];
-}
diff --git a/src/models/entities/muting.ts b/src/models/entities/muting.ts
deleted file mode 100644
index 0084213bcc..0000000000
--- a/src/models/entities/muting.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-@Index(['muterId', 'muteeId'], { unique: true })
-export class Muting {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Muting.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The mutee user ID.'
-	})
-	public muteeId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public mutee: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The muter user ID.'
-	})
-	public muterId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public muter: User | null;
-}
diff --git a/src/models/entities/note-favorite.ts b/src/models/entities/note-favorite.ts
deleted file mode 100644
index 0713c3ae56..0000000000
--- a/src/models/entities/note-favorite.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { Note } from './note';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'noteId'], { unique: true })
-export class NoteFavorite {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the NoteFavorite.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column(id())
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-}
diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts
deleted file mode 100644
index 674dc3639e..0000000000
--- a/src/models/entities/note-reaction.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { Note } from './note';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'noteId'], { unique: true })
-export class NoteReaction {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the NoteReaction.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user?: User | null;
-
-	@Index()
-	@Column(id())
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note?: Note | null;
-
-	// TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため)
-
-	@Column('varchar', {
-		length: 260
-	})
-	public reaction: string;
-}
diff --git a/src/models/entities/note-thread-muting.ts b/src/models/entities/note-thread-muting.ts
deleted file mode 100644
index b438522a4c..0000000000
--- a/src/models/entities/note-thread-muting.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { Note } from './note';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'threadId'], { unique: true })
-export class NoteThreadMuting {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 256,
-	})
-	public threadId: string;
-}
diff --git a/src/models/entities/note-unread.ts b/src/models/entities/note-unread.ts
deleted file mode 100644
index 57dda4fafd..0000000000
--- a/src/models/entities/note-unread.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { Note } from './note';
-import { id } from '../id';
-import { Channel } from './channel';
-
-@Entity()
-@Index(['userId', 'noteId'], { unique: true })
-export class NoteUnread {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column(id())
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	/**
-	 * メンションか否か
-	 */
-	@Index()
-	@Column('boolean')
-	public isMentioned: boolean;
-
-	/**
-	 * ダイレクト投稿か否か
-	 */
-	@Index()
-	@Column('boolean')
-	public isSpecified: boolean;
-
-	//#region Denormalized fields
-	@Index()
-	@Column({
-		...id(),
-		comment: '[Denormalized]'
-	})
-	public noteUserId: User['id'];
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: '[Denormalized]'
-	})
-	public noteChannelId: Channel['id'] | null;
-	//#endregion
-}
diff --git a/src/models/entities/note-watching.ts b/src/models/entities/note-watching.ts
deleted file mode 100644
index 741a1c0c8b..0000000000
--- a/src/models/entities/note-watching.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { Note } from './note';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'noteId'], { unique: true })
-export class NoteWatching {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the NoteWatching.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The watcher ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The target Note ID.'
-	})
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	//#region Denormalized fields
-	@Index()
-	@Column({
-		...id(),
-		comment: '[Denormalized]'
-	})
-	public noteUserId: Note['userId'];
-	//#endregion
-}
diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts
deleted file mode 100644
index 4a5411f93d..0000000000
--- a/src/models/entities/note.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { DriveFile } from './drive-file';
-import { id } from '../id';
-import { noteVisibilities } from '../../types';
-import { Channel } from './channel';
-
-@Entity()
-@Index('IDX_NOTE_TAGS', { synchronize: false })
-@Index('IDX_NOTE_MENTIONS', { synchronize: false })
-@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
-export class Note {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Note.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The ID of reply target.'
-	})
-	public replyId: Note['id'] | null;
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public reply: Note | null;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The ID of renote target.'
-	})
-	public renoteId: Note['id'] | null;
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public renote: Note | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 256, nullable: true
-	})
-	public threadId: string | null;
-
-	@Column('varchar', {
-		length: 8192, nullable: true
-	})
-	public text: string | null;
-
-	@Column('varchar', {
-		length: 256, nullable: true
-	})
-	public name: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true
-	})
-	public cw: string | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The ID of author.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('boolean', {
-		default: false
-	})
-	public viaMobile: boolean;
-
-	@Column('boolean', {
-		default: false
-	})
-	public localOnly: boolean;
-
-	@Column('smallint', {
-		default: 0
-	})
-	public renoteCount: number;
-
-	@Column('smallint', {
-		default: 0
-	})
-	public repliesCount: number;
-
-	@Column('jsonb', {
-		default: {}
-	})
-	public reactions: Record<string, number>;
-
-	/**
-	 * public ... 公開
-	 * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
-	 * followers ... フォロワーのみ
-	 * specified ... visibleUserIds で指定したユーザーのみ
-	 */
-	@Column('enum', { enum: noteVisibilities })
-	public visibility: typeof noteVisibilities[number];
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The URI of a note. it will be null when the note is local.'
-	})
-	public uri: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The human readable url of a note. it will be null when the note is local.'
-	})
-	public url: string | null;
-
-	@Column('integer', {
-		default: 0, select: false
-	})
-	public score: number;
-
-	@Index()
-	@Column({
-		...id(),
-		array: true, default: '{}'
-	})
-	public fileIds: DriveFile['id'][];
-
-	@Index()
-	@Column('varchar', {
-		length: 256, array: true, default: '{}'
-	})
-	public attachedFileTypes: string[];
-
-	@Index()
-	@Column({
-		...id(),
-		array: true, default: '{}'
-	})
-	public visibleUserIds: User['id'][];
-
-	@Index()
-	@Column({
-		...id(),
-		array: true, default: '{}'
-	})
-	public mentions: User['id'][];
-
-	@Column('text', {
-		default: '[]'
-	})
-	public mentionedRemoteUsers: string;
-
-	@Column('varchar', {
-		length: 128, array: true, default: '{}'
-	})
-	public emojis: string[];
-
-	@Index()
-	@Column('varchar', {
-		length: 128, array: true, default: '{}'
-	})
-	public tags: string[];
-
-	@Column('boolean', {
-		default: false
-	})
-	public hasPoll: boolean;
-
-	@Index()
-	@Column({
-		...id(),
-		nullable: true, default: null,
-		comment: 'The ID of source channel.'
-	})
-	public channelId: Channel['id'] | null;
-
-	@ManyToOne(type => Channel, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public channel: Channel | null;
-
-	//#region Denormalized fields
-	@Index()
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public userHost: string | null;
-
-	@Column({
-		...id(),
-		nullable: true,
-		comment: '[Denormalized]'
-	})
-	public replyUserId: User['id'] | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public replyUserHost: string | null;
-
-	@Column({
-		...id(),
-		nullable: true,
-		comment: '[Denormalized]'
-	})
-	public renoteUserId: User['id'] | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public renoteUserHost: string | null;
-	//#endregion
-
-	constructor(data: Partial<Note>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
-
-export type IMentionedRemoteUsers = {
-	uri: string;
-	url?: string;
-	username: string;
-	host: string;
-}[];
diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts
deleted file mode 100644
index 47184caacc..0000000000
--- a/src/models/entities/notification.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { Note } from './note';
-import { FollowRequest } from './follow-request';
-import { UserGroupInvitation } from './user-group-invitation';
-import { AccessToken } from './access-token';
-import { notificationTypes } from '@/types';
-
-@Entity()
-export class Notification {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Notification.'
-	})
-	public createdAt: Date;
-
-	/**
-	 * 通知の受信者
-	 */
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The ID of recipient user of the Notification.'
-	})
-	public notifieeId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public notifiee: User | null;
-
-	/**
-	 * 通知の送信者(initiator)
-	 */
-	@Index()
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The ID of sender user of the Notification.'
-	})
-	public notifierId: User['id'] | null;
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public notifier: User | null;
-
-	/**
-	 * 通知の種類。
-	 * follow - フォローされた
-	 * mention - 投稿で自分が言及された
-	 * reply - (自分または自分がWatchしている)投稿が返信された
-	 * renote - (自分または自分がWatchしている)投稿がRenoteされた
-	 * quote - (自分または自分がWatchしている)投稿が引用Renoteされた
-	 * reaction - (自分または自分がWatchしている)投稿にリアクションされた
-	 * pollVote - (自分または自分がWatchしている)投稿の投票に投票された
-	 * receiveFollowRequest - フォローリクエストされた
-	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された
-	 * groupInvited - グループに招待された
-	 * app - アプリ通知
-	 */
-	@Index()
-	@Column('enum', {
-		enum: notificationTypes,
-		comment: 'The type of the Notification.'
-	})
-	public type: typeof notificationTypes[number];
-
-	/**
-	 * 通知が読まれたかどうか
-	 */
-	@Index()
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the Notification is read.'
-	})
-	public isRead: boolean;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public noteId: Note['id'] | null;
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public followRequestId: FollowRequest['id'] | null;
-
-	@ManyToOne(type => FollowRequest, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public followRequest: FollowRequest | null;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public userGroupInvitationId: UserGroupInvitation['id'] | null;
-
-	@ManyToOne(type => UserGroupInvitation, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public userGroupInvitation: UserGroupInvitation | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true
-	})
-	public reaction: string | null;
-
-	@Column('integer', {
-		nullable: true
-	})
-	public choice: number | null;
-
-	/**
-	 * アプリ通知のbody
-	 */
-	@Column('varchar', {
-		length: 2048, nullable: true
-	})
-	public customBody: string | null;
-
-	/**
-	 * アプリ通知のheader
-	 * (省略時はアプリ名で表示されることを期待)
-	 */
-	@Column('varchar', {
-		length: 256, nullable: true
-	})
-	public customHeader: string | null;
-
-	/**
-	 * アプリ通知のicon(URL)
-	 * (省略時はアプリアイコンで表示されることを期待)
-	 */
-	@Column('varchar', {
-		length: 1024, nullable: true
-	})
-	public customIcon: string | null;
-
-	/**
-	 * アプリ通知のアプリ(のトークン)
-	 */
-	@Index()
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public appAccessTokenId: AccessToken['id'] | null;
-
-	@ManyToOne(type => AccessToken, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public appAccessToken: AccessToken | null;
-}
diff --git a/src/models/entities/page-like.ts b/src/models/entities/page-like.ts
deleted file mode 100644
index ca84ece8fd..0000000000
--- a/src/models/entities/page-like.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { Page } from './page';
-
-@Entity()
-@Index(['userId', 'pageId'], { unique: true })
-export class PageLike {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone')
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column(id())
-	public pageId: Page['id'];
-
-	@ManyToOne(type => Page, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public page: Page | null;
-}
diff --git a/src/models/entities/page.ts b/src/models/entities/page.ts
deleted file mode 100644
index ed0411a3d0..0000000000
--- a/src/models/entities/page.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-import { DriveFile } from './drive-file';
-
-@Entity()
-@Index(['userId', 'name'], { unique: true })
-export class Page {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Page.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The updated date of the Page.'
-	})
-	public updatedAt: Date;
-
-	@Column('varchar', {
-		length: 256,
-	})
-	public title: string;
-
-	@Index()
-	@Column('varchar', {
-		length: 256,
-	})
-	public name: string;
-
-	@Column('varchar', {
-		length: 256, nullable: true
-	})
-	public summary: string | null;
-
-	@Column('boolean')
-	public alignCenter: boolean;
-
-	@Column('boolean', {
-		default: false
-	})
-	public hideTitleWhenPinned: boolean;
-
-	@Column('varchar', {
-		length: 32,
-	})
-	public font: string;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The ID of author.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column({
-		...id(),
-		nullable: true,
-	})
-	public eyeCatchingImageId: DriveFile['id'] | null;
-
-	@ManyToOne(type => DriveFile, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public eyeCatchingImage: DriveFile | null;
-
-	@Column('jsonb', {
-		default: []
-	})
-	public content: Record<string, any>[];
-
-	@Column('jsonb', {
-		default: []
-	})
-	public variables: Record<string, any>[];
-
-	@Column('varchar', {
-		length: 16384,
-		default: ''
-	})
-	public script: string;
-
-	/**
-	 * public ... 公開
-	 * followers ... フォロワーのみ
-	 * specified ... visibleUserIds で指定したユーザーのみ
-	 */
-	@Column('enum', { enum: ['public', 'followers', 'specified'] })
-	public visibility: 'public' | 'followers' | 'specified';
-
-	@Index()
-	@Column({
-		...id(),
-		array: true, default: '{}'
-	})
-	public visibleUserIds: User['id'][];
-
-	@Column('integer', {
-		default: 0
-	})
-	public likedCount: number;
-
-	constructor(data: Partial<Page>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/password-reset-request.ts b/src/models/entities/password-reset-request.ts
deleted file mode 100644
index 6d41d38a93..0000000000
--- a/src/models/entities/password-reset-request.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
-import { id } from '../id';
-import { User } from './user';
-
-@Entity()
-export class PasswordResetRequest {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone')
-	public createdAt: Date;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 256,
-	})
-	public token: string;
-
-	@Index()
-	@Column({
-		...id(),
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-}
diff --git a/src/models/entities/poll-vote.ts b/src/models/entities/poll-vote.ts
deleted file mode 100644
index 709376f909..0000000000
--- a/src/models/entities/poll-vote.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { Note } from './note';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'noteId', 'choice'], { unique: true })
-export class PollVote {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the PollVote.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column(id())
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	@Column('integer')
-	public choice: number;
-}
diff --git a/src/models/entities/poll.ts b/src/models/entities/poll.ts
deleted file mode 100644
index e3bbb1c3f2..0000000000
--- a/src/models/entities/poll.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
-import { id } from '../id';
-import { Note } from './note';
-import { User } from './user';
-import { noteVisibilities } from '../../types';
-
-@Entity()
-export class Poll {
-	@PrimaryColumn(id())
-	public noteId: Note['id'];
-
-	@OneToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	@Column('timestamp with time zone', {
-		nullable: true
-	})
-	public expiresAt: Date | null;
-
-	@Column('boolean')
-	public multiple: boolean;
-
-	@Column('varchar', {
-		length: 128, array: true, default: '{}'
-	})
-	public choices: string[];
-
-	@Column('integer', {
-		array: true,
-	})
-	public votes: number[];
-
-	//#region Denormalized fields
-	@Column('enum', {
-		enum: noteVisibilities,
-		comment: '[Denormalized]'
-	})
-	public noteVisibility: typeof noteVisibilities[number];
-
-	@Index()
-	@Column({
-		...id(),
-		comment: '[Denormalized]'
-	})
-	public userId: User['id'];
-
-	@Index()
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public userHost: string | null;
-	//#endregion
-
-	constructor(data: Partial<Poll>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
-
-export type IPoll = {
-	choices: string[];
-	votes?: number[];
-	multiple: boolean;
-	expiresAt: Date | null;
-};
diff --git a/src/models/entities/promo-note.ts b/src/models/entities/promo-note.ts
deleted file mode 100644
index 474f1cb235..0000000000
--- a/src/models/entities/promo-note.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
-import { Note } from './note';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class PromoNote {
-	@PrimaryColumn(id())
-	public noteId: Note['id'];
-
-	@OneToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	@Column('timestamp with time zone')
-	public expiresAt: Date;
-
-	//#region Denormalized fields
-	@Index()
-	@Column({
-		...id(),
-		comment: '[Denormalized]'
-	})
-	public userId: User['id'];
-	//#endregion
-}
diff --git a/src/models/entities/promo-read.ts b/src/models/entities/promo-read.ts
deleted file mode 100644
index 2e0977b6b5..0000000000
--- a/src/models/entities/promo-read.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { Note } from './note';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'noteId'], { unique: true })
-export class PromoRead {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the PromoRead.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column(id())
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-}
diff --git a/src/models/entities/registration-tickets.ts b/src/models/entities/registration-tickets.ts
deleted file mode 100644
index d962f78a78..0000000000
--- a/src/models/entities/registration-tickets.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
-import { id } from '../id';
-
-@Entity()
-export class RegistrationTicket {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone')
-	public createdAt: Date;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 64,
-	})
-	public code: string;
-}
diff --git a/src/models/entities/registry-item.ts b/src/models/entities/registry-item.ts
deleted file mode 100644
index 54d2ef2082..0000000000
--- a/src/models/entities/registry-item.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい
-@Entity()
-export class RegistryItem {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the RegistryItem.'
-	})
-	public createdAt: Date;
-
-	@Column('timestamp with time zone', {
-		comment: 'The updated date of the RegistryItem.'
-	})
-	public updatedAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The owner ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 1024,
-		comment: 'The key of the RegistryItem.'
-	})
-	public key: string;
-
-	@Column('jsonb', {
-		default: {}, nullable: true,
-		comment: 'The value of the RegistryItem.'
-	})
-	public value: any | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 1024, array: true, default: '{}'
-	})
-	public scope: string[];
-
-	// サードパーティアプリに開放するときのためのカラム
-	@Index()
-	@Column('varchar', {
-		length: 512, nullable: true
-	})
-	public domain: string | null;
-}
diff --git a/src/models/entities/relay.ts b/src/models/entities/relay.ts
deleted file mode 100644
index 4c82ccb125..0000000000
--- a/src/models/entities/relay.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
-import { id } from '../id';
-
-@Entity()
-export class Relay {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 512, nullable: false,
-	})
-	public inbox: string;
-
-	@Column('enum', {
-		enum: ['requesting', 'accepted', 'rejected'],
-	})
-	public status: 'requesting' | 'accepted' | 'rejected';
-}
diff --git a/src/models/entities/signin.ts b/src/models/entities/signin.ts
deleted file mode 100644
index 7e047084b1..0000000000
--- a/src/models/entities/signin.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class Signin {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the Signin.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 128,
-	})
-	public ip: string;
-
-	@Column('jsonb')
-	public headers: Record<string, any>;
-
-	@Column('boolean')
-	public success: boolean;
-}
diff --git a/src/models/entities/sw-subscription.ts b/src/models/entities/sw-subscription.ts
deleted file mode 100644
index 7c3f6f0a6c..0000000000
--- a/src/models/entities/sw-subscription.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class SwSubscription {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone')
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 512,
-	})
-	public endpoint: string;
-
-	@Column('varchar', {
-		length: 256,
-	})
-	public auth: string;
-
-	@Column('varchar', {
-		length: 128,
-	})
-	public publickey: string;
-}
diff --git a/src/models/entities/used-username.ts b/src/models/entities/used-username.ts
deleted file mode 100644
index eb90bef6ca..0000000000
--- a/src/models/entities/used-username.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { PrimaryColumn, Entity, Column } from 'typeorm';
-
-@Entity()
-export class UsedUsername {
-	@PrimaryColumn('varchar', {
-		length: 128,
-	})
-	public username: string;
-
-	@Column('timestamp with time zone')
-	public createdAt: Date;
-
-	constructor(data: Partial<UsedUsername>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/user-group-invitation.ts b/src/models/entities/user-group-invitation.ts
deleted file mode 100644
index 6fe8f20134..0000000000
--- a/src/models/entities/user-group-invitation.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { UserGroup } from './user-group';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'userGroupId'], { unique: true })
-export class UserGroupInvitation {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the UserGroupInvitation.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The user ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The group ID.'
-	})
-	public userGroupId: UserGroup['id'];
-
-	@ManyToOne(type => UserGroup, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public userGroup: UserGroup | null;
-}
diff --git a/src/models/entities/user-group-joining.ts b/src/models/entities/user-group-joining.ts
deleted file mode 100644
index e09c3230f1..0000000000
--- a/src/models/entities/user-group-joining.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { UserGroup } from './user-group';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'userGroupId'], { unique: true })
-export class UserGroupJoining {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the UserGroupJoining.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The user ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The group ID.'
-	})
-	public userGroupId: UserGroup['id'];
-
-	@ManyToOne(type => UserGroup, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public userGroup: UserGroup | null;
-}
diff --git a/src/models/entities/user-group.ts b/src/models/entities/user-group.ts
deleted file mode 100644
index f4bac03223..0000000000
--- a/src/models/entities/user-group.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class UserGroup {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the UserGroup.'
-	})
-	public createdAt: Date;
-
-	@Column('varchar', {
-		length: 256,
-	})
-	public name: string;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The ID of owner.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public isPrivate: boolean;
-
-	constructor(data: Partial<UserGroup>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/user-keypair.ts b/src/models/entities/user-keypair.ts
deleted file mode 100644
index 603321d758..0000000000
--- a/src/models/entities/user-keypair.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class UserKeypair {
-	@PrimaryColumn(id())
-	public userId: User['id'];
-
-	@OneToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 4096,
-	})
-	public publicKey: string;
-
-	@Column('varchar', {
-		length: 4096,
-	})
-	public privateKey: string;
-
-	constructor(data: Partial<UserKeypair>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/user-list-joining.ts b/src/models/entities/user-list-joining.ts
deleted file mode 100644
index bb7dc40b95..0000000000
--- a/src/models/entities/user-list-joining.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { UserList } from './user-list';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'userListId'], { unique: true })
-export class UserListJoining {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the UserListJoining.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The user ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The list ID.'
-	})
-	public userListId: UserList['id'];
-
-	@ManyToOne(type => UserList, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public userList: UserList | null;
-}
diff --git a/src/models/entities/user-list.ts b/src/models/entities/user-list.ts
deleted file mode 100644
index 35a83ef8c3..0000000000
--- a/src/models/entities/user-list.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class UserList {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the UserList.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The owner ID.'
-	})
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 128,
-		comment: 'The name of the UserList.'
-	})
-	public name: string;
-}
diff --git a/src/models/entities/user-note-pining.ts b/src/models/entities/user-note-pining.ts
deleted file mode 100644
index 04a6f8f645..0000000000
--- a/src/models/entities/user-note-pining.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
-import { Note } from './note';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-@Index(['userId', 'noteId'], { unique: true })
-export class UserNotePining {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the UserNotePinings.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column(id())
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public note: Note | null;
-}
diff --git a/src/models/entities/user-pending.ts b/src/models/entities/user-pending.ts
deleted file mode 100644
index 40482af333..0000000000
--- a/src/models/entities/user-pending.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
-import { id } from '../id';
-
-@Entity()
-export class UserPending {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Column('timestamp with time zone')
-	public createdAt: Date;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 128,
-	})
-	public code: string;
-
-	@Column('varchar', {
-		length: 128,
-	})
-	public username: string;
-
-	@Column('varchar', {
-		length: 128,
-	})
-	public email: string;
-
-	@Column('varchar', {
-		length: 128,
-	})
-	public password: string;
-}
diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts
deleted file mode 100644
index 8a8cacfd52..0000000000
--- a/src/models/entities/user-profile.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
-import { id } from '../id';
-import { User } from './user';
-import { Page } from './page';
-import { ffVisibility, notificationTypes } from '@/types';
-
-// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
-//       ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
-@Entity()
-export class UserProfile {
-	@PrimaryColumn(id())
-	public userId: User['id'];
-
-	@OneToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: 'The location of the User.'
-	})
-	public location: string | null;
-
-	@Column('char', {
-		length: 10, nullable: true,
-		comment: 'The birthday (YYYY-MM-DD) of the User.'
-	})
-	public birthday: string | null;
-
-	@Column('varchar', {
-		length: 2048, nullable: true,
-		comment: 'The description (bio) of the User.'
-	})
-	public description: string | null;
-
-	@Column('jsonb', {
-		default: [],
-	})
-	public fields: {
-		name: string;
-		value: string;
-	}[];
-
-	@Column('varchar', {
-		length: 32, nullable: true,
-	})
-	public lang: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'Remote URL of the user.'
-	})
-	public url: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: 'The email address of the User.'
-	})
-	public email: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-	})
-	public emailVerifyCode: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public emailVerified: boolean;
-
-	@Column('jsonb', {
-		default: ['follow', 'receiveFollowRequest', 'groupInvited']
-	})
-	public emailNotificationTypes: string[];
-
-	@Column('boolean', {
-		default: false,
-	})
-	public publicReactions: boolean;
-
-	@Column('enum', {
-		enum: ffVisibility,
-		default: 'public',
-	})
-	public ffVisibility: typeof ffVisibility[number];
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-	})
-	public twoFactorTempSecret: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-	})
-	public twoFactorSecret: string | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public twoFactorEnabled: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public securityKeysAvailable: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public usePasswordLessLogin: boolean;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: 'The password hash of the User. It will be null if the origin of the user is local.'
-	})
-	public password: string | null;
-
-	// TODO: そのうち消す
-	@Column('jsonb', {
-		default: {},
-		comment: 'The client-specific data of the User.'
-	})
-	public clientData: Record<string, any>;
-
-	@Column('jsonb', {
-		default: {},
-		comment: 'The room data of the User.'
-	})
-	public room: Record<string, any>;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public autoAcceptFollowed: boolean;
-
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether reject index by crawler.'
-	})
-	public noCrawle: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public alwaysMarkNsfw: boolean;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public carefulBot: boolean;
-
-	@Column('boolean', {
-		default: true,
-	})
-	public injectFeaturedNote: boolean;
-
-	@Column('boolean', {
-		default: true,
-	})
-	public receiveAnnouncementEmail: boolean;
-
-	@Column({
-		...id(),
-		nullable: true
-	})
-	public pinnedPageId: Page['id'] | null;
-
-	@OneToOne(type => Page, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public pinnedPage: Page | null;
-
-	@Column('jsonb', {
-		default: {}
-	})
-	public integrations: Record<string, any>;
-
-	@Index()
-	@Column('boolean', {
-		default: false, select: false,
-	})
-	public enableWordMute: boolean;
-
-	@Column('jsonb', {
-		default: []
-	})
-	public mutedWords: string[][];
-
-	@Column('enum', {
-		enum: notificationTypes,
-		array: true,
-		default: [],
-	})
-	public mutingNotificationTypes: typeof notificationTypes[number][];
-
-	//#region Denormalized fields
-	@Index()
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: '[Denormalized]'
-	})
-	public userHost: string | null;
-	//#endregion
-
-	constructor(data: Partial<UserProfile>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/user-publickey.ts b/src/models/entities/user-publickey.ts
deleted file mode 100644
index 21edc3e9e2..0000000000
--- a/src/models/entities/user-publickey.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class UserPublickey {
-	@PrimaryColumn(id())
-	public userId: User['id'];
-
-	@OneToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index({ unique: true })
-	@Column('varchar', {
-		length: 256,
-	})
-	public keyId: string;
-
-	@Column('varchar', {
-		length: 4096,
-	})
-	public keyPem: string;
-
-	constructor(data: Partial<UserPublickey>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/user-security-key.ts b/src/models/entities/user-security-key.ts
deleted file mode 100644
index d54c728e53..0000000000
--- a/src/models/entities/user-security-key.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
-import { User } from './user';
-import { id } from '../id';
-
-@Entity()
-export class UserSecurityKey {
-	@PrimaryColumn('varchar', {
-		comment: 'Variable-length id given to navigator.credentials.get()'
-	})
-	public id: string;
-
-	@Index()
-	@Column(id())
-	public userId: User['id'];
-
-	@ManyToOne(type => User, {
-		onDelete: 'CASCADE'
-	})
-	@JoinColumn()
-	public user: User | null;
-
-	@Index()
-	@Column('varchar', {
-		comment:
-			'Variable-length public key used to verify attestations (hex-encoded).'
-	})
-	public publicKey: string;
-
-	@Column('timestamp with time zone', {
-		comment:
-			'The date of the last time the UserSecurityKey was successfully validated.'
-	})
-	public lastUsed: Date;
-
-	@Column('varchar', {
-		comment: 'User-defined name for this key',
-		length: 30
-	})
-	public name: string;
-
-	constructor(data: Partial<UserSecurityKey>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts
deleted file mode 100644
index 65aebd2d1a..0000000000
--- a/src/models/entities/user.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
-import { DriveFile } from './drive-file';
-import { id } from '../id';
-
-@Entity()
-@Index(['usernameLower', 'host'], { unique: true })
-export class User {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		comment: 'The created date of the User.'
-	})
-	public createdAt: Date;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		nullable: true,
-		comment: 'The updated date of the User.'
-	})
-	public updatedAt: Date | null;
-
-	@Column('timestamp with time zone', {
-		nullable: true
-	})
-	public lastFetchedAt: Date | null;
-
-	@Index()
-	@Column('timestamp with time zone', {
-		nullable: true
-	})
-	public lastActiveDate: Date | null;
-
-	@Column('boolean', {
-		default: false,
-	})
-	public hideOnlineStatus: boolean;
-
-	@Column('varchar', {
-		length: 128,
-		comment: 'The username of the User.'
-	})
-	public username: string;
-
-	@Index()
-	@Column('varchar', {
-		length: 128, select: false,
-		comment: 'The username (lowercased) of the User.'
-	})
-	public usernameLower: string;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: 'The name of the User.'
-	})
-	public name: string | null;
-
-	@Column('integer', {
-		default: 0,
-		comment: 'The count of followers.'
-	})
-	public followersCount: number;
-
-	@Column('integer', {
-		default: 0,
-		comment: 'The count of following.'
-	})
-	public followingCount: number;
-
-	@Column('integer', {
-		default: 0,
-		comment: 'The count of notes.'
-	})
-	public notesCount: number;
-
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The ID of avatar DriveFile.'
-	})
-	public avatarId: DriveFile['id'] | null;
-
-	@OneToOne(type => DriveFile, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public avatar: DriveFile | null;
-
-	@Column({
-		...id(),
-		nullable: true,
-		comment: 'The ID of banner DriveFile.'
-	})
-	public bannerId: DriveFile['id'] | null;
-
-	@OneToOne(type => DriveFile, {
-		onDelete: 'SET NULL'
-	})
-	@JoinColumn()
-	public banner: DriveFile | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 128, array: true, default: '{}'
-	})
-	public tags: string[];
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-	})
-	public avatarUrl: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-	})
-	public bannerUrl: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-	})
-	public avatarBlurhash: string | null;
-
-	@Column('varchar', {
-		length: 128, nullable: true,
-	})
-	public bannerBlurhash: string | null;
-
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the User is suspended.'
-	})
-	public isSuspended: boolean;
-
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the User is silenced.'
-	})
-	public isSilenced: boolean;
-
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the User is locked.'
-	})
-	public isLocked: boolean;
-
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the User is a bot.'
-	})
-	public isBot: boolean;
-
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the User is a cat.'
-	})
-	public isCat: boolean;
-
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the User is the admin.'
-	})
-	public isAdmin: boolean;
-
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the User is a moderator.'
-	})
-	public isModerator: boolean;
-
-	@Index()
-	@Column('boolean', {
-		default: true,
-		comment: 'Whether the User is explorable.'
-	})
-	public isExplorable: boolean;
-
-	// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
-	@Column('boolean', {
-		default: false,
-		comment: 'Whether the User is deleted.'
-	})
-	public isDeleted: boolean;
-
-	@Column('varchar', {
-		length: 128, array: true, default: '{}'
-	})
-	public emojis: string[];
-
-	@Index()
-	@Column('varchar', {
-		length: 128, nullable: true,
-		comment: 'The host of the User. It will be null if the origin of the user is local.'
-	})
-	public host: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The inbox URL of the User. It will be null if the origin of the user is local.'
-	})
-	public inbox: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'
-	})
-	public sharedInbox: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The featured URL of the User. It will be null if the origin of the user is local.'
-	})
-	public featured: string | null;
-
-	@Index()
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The URI of the User. It will be null if the origin of the user is local.'
-	})
-	public uri: string | null;
-
-	@Column('varchar', {
-		length: 512, nullable: true,
-		comment: 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'
-	})
-	public followersUri: string | null;
-
-	@Index({ unique: true })
-	@Column('char', {
-		length: 16, nullable: true, unique: true,
-		comment: 'The native access token of the User. It will be null if the origin of the user is local.'
-	})
-	public token: string | null;
-
-	constructor(data: Partial<User>) {
-		if (data == null) return;
-
-		for (const [k, v] of Object.entries(data)) {
-			(this as any)[k] = v;
-		}
-	}
-}
-
-export interface ILocalUser extends User {
-	host: null;
-}
-
-export interface IRemoteUser extends User {
-	host: string;
-}
diff --git a/src/models/id.ts b/src/models/id.ts
deleted file mode 100644
index cdb8259073..0000000000
--- a/src/models/id.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const id = () => ({
-	type: 'varchar' as const,
-	length: 32
-});
diff --git a/src/models/index.ts b/src/models/index.ts
deleted file mode 100644
index 7154cca550..0000000000
--- a/src/models/index.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import { getRepository, getCustomRepository } from 'typeorm';
-import { Announcement } from './entities/announcement';
-import { AnnouncementRead } from './entities/announcement-read';
-import { Instance } from './entities/instance';
-import { Poll } from './entities/poll';
-import { PollVote } from './entities/poll-vote';
-import { Meta } from './entities/meta';
-import { SwSubscription } from './entities/sw-subscription';
-import { NoteWatching } from './entities/note-watching';
-import { NoteThreadMuting } from './entities/note-thread-muting';
-import { NoteUnread } from './entities/note-unread';
-import { RegistrationTicket } from './entities/registration-tickets';
-import { UserRepository } from './repositories/user';
-import { NoteRepository } from './repositories/note';
-import { DriveFileRepository } from './repositories/drive-file';
-import { DriveFolderRepository } from './repositories/drive-folder';
-import { AccessToken } from './entities/access-token';
-import { UserNotePining } from './entities/user-note-pining';
-import { SigninRepository } from './repositories/signin';
-import { MessagingMessageRepository } from './repositories/messaging-message';
-import { ReversiGameRepository } from './repositories/games/reversi/game';
-import { UserListRepository } from './repositories/user-list';
-import { UserListJoining } from './entities/user-list-joining';
-import { UserGroupRepository } from './repositories/user-group';
-import { UserGroupJoining } from './entities/user-group-joining';
-import { UserGroupInvitationRepository } from './repositories/user-group-invitation';
-import { FollowRequestRepository } from './repositories/follow-request';
-import { MutingRepository } from './repositories/muting';
-import { BlockingRepository } from './repositories/blocking';
-import { NoteReactionRepository } from './repositories/note-reaction';
-import { NotificationRepository } from './repositories/notification';
-import { NoteFavoriteRepository } from './repositories/note-favorite';
-import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
-import { UserPublickey } from './entities/user-publickey';
-import { UserKeypair } from './entities/user-keypair';
-import { AppRepository } from './repositories/app';
-import { FollowingRepository } from './repositories/following';
-import { AbuseUserReportRepository } from './repositories/abuse-user-report';
-import { AuthSessionRepository } from './repositories/auth-session';
-import { UserProfile } from './entities/user-profile';
-import { AttestationChallenge } from './entities/attestation-challenge';
-import { UserSecurityKey } from './entities/user-security-key';
-import { HashtagRepository } from './repositories/hashtag';
-import { PageRepository } from './repositories/page';
-import { PageLikeRepository } from './repositories/page-like';
-import { GalleryPostRepository } from './repositories/gallery-post';
-import { GalleryLikeRepository } from './repositories/gallery-like';
-import { ModerationLogRepository } from './repositories/moderation-logs';
-import { UsedUsername } from './entities/used-username';
-import { ClipRepository } from './repositories/clip';
-import { ClipNote } from './entities/clip-note';
-import { AntennaRepository } from './repositories/antenna';
-import { AntennaNote } from './entities/antenna-note';
-import { PromoNote } from './entities/promo-note';
-import { PromoRead } from './entities/promo-read';
-import { EmojiRepository } from './repositories/emoji';
-import { RelayRepository } from './repositories/relay';
-import { ChannelRepository } from './repositories/channel';
-import { MutedNote } from './entities/muted-note';
-import { ChannelFollowing } from './entities/channel-following';
-import { ChannelNotePining } from './entities/channel-note-pining';
-import { RegistryItem } from './entities/registry-item';
-import { Ad } from './entities/ad';
-import { PasswordResetRequest } from './entities/password-reset-request';
-import { UserPending } from './entities/user-pending';
-
-export const Announcements = getRepository(Announcement);
-export const AnnouncementReads = getRepository(AnnouncementRead);
-export const Apps = getCustomRepository(AppRepository);
-export const Notes = getCustomRepository(NoteRepository);
-export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
-export const NoteWatchings = getRepository(NoteWatching);
-export const NoteThreadMutings = getRepository(NoteThreadMuting);
-export const NoteReactions = getCustomRepository(NoteReactionRepository);
-export const NoteUnreads = getRepository(NoteUnread);
-export const Polls = getRepository(Poll);
-export const PollVotes = getRepository(PollVote);
-export const Users = getCustomRepository(UserRepository);
-export const UserProfiles = getRepository(UserProfile);
-export const UserKeypairs = getRepository(UserKeypair);
-export const UserPendings = getRepository(UserPending);
-export const AttestationChallenges = getRepository(AttestationChallenge);
-export const UserSecurityKeys = getRepository(UserSecurityKey);
-export const UserPublickeys = getRepository(UserPublickey);
-export const UserLists = getCustomRepository(UserListRepository);
-export const UserListJoinings = getRepository(UserListJoining);
-export const UserGroups = getCustomRepository(UserGroupRepository);
-export const UserGroupJoinings = getRepository(UserGroupJoining);
-export const UserGroupInvitations = getCustomRepository(UserGroupInvitationRepository);
-export const UserNotePinings = getRepository(UserNotePining);
-export const UsedUsernames = getRepository(UsedUsername);
-export const Followings = getCustomRepository(FollowingRepository);
-export const FollowRequests = getCustomRepository(FollowRequestRepository);
-export const Instances = getRepository(Instance);
-export const Emojis = getCustomRepository(EmojiRepository);
-export const DriveFiles = getCustomRepository(DriveFileRepository);
-export const DriveFolders = getCustomRepository(DriveFolderRepository);
-export const Notifications = getCustomRepository(NotificationRepository);
-export const Metas = getRepository(Meta);
-export const Mutings = getCustomRepository(MutingRepository);
-export const Blockings = getCustomRepository(BlockingRepository);
-export const SwSubscriptions = getRepository(SwSubscription);
-export const Hashtags = getCustomRepository(HashtagRepository);
-export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
-export const RegistrationTickets = getRepository(RegistrationTicket);
-export const AuthSessions = getCustomRepository(AuthSessionRepository);
-export const AccessTokens = getRepository(AccessToken);
-export const Signins = getCustomRepository(SigninRepository);
-export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
-export const ReversiGames = getCustomRepository(ReversiGameRepository);
-export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
-export const Pages = getCustomRepository(PageRepository);
-export const PageLikes = getCustomRepository(PageLikeRepository);
-export const GalleryPosts = getCustomRepository(GalleryPostRepository);
-export const GalleryLikes = getCustomRepository(GalleryLikeRepository);
-export const ModerationLogs = getCustomRepository(ModerationLogRepository);
-export const Clips = getCustomRepository(ClipRepository);
-export const ClipNotes = getRepository(ClipNote);
-export const Antennas = getCustomRepository(AntennaRepository);
-export const AntennaNotes = getRepository(AntennaNote);
-export const PromoNotes = getRepository(PromoNote);
-export const PromoReads = getRepository(PromoRead);
-export const Relays = getCustomRepository(RelayRepository);
-export const MutedNotes = getRepository(MutedNote);
-export const Channels = getCustomRepository(ChannelRepository);
-export const ChannelFollowings = getRepository(ChannelFollowing);
-export const ChannelNotePinings = getRepository(ChannelNotePining);
-export const RegistryItems = getRepository(RegistryItem);
-export const Ads = getRepository(Ad);
-export const PasswordResetRequests = getRepository(PasswordResetRequest);
diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts
deleted file mode 100644
index 039a9924d2..0000000000
--- a/src/models/repositories/abuse-user-report.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Users } from '../index';
-import { AbuseUserReport } from '@/models/entities/abuse-user-report';
-import { awaitAll } from '@/prelude/await-all';
-
-@EntityRepository(AbuseUserReport)
-export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
-	public async pack(
-		src: AbuseUserReport['id'] | AbuseUserReport,
-	) {
-		const report = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: report.id,
-			createdAt: report.createdAt,
-			comment: report.comment,
-			resolved: report.resolved,
-			reporterId: report.reporterId,
-			targetUserId: report.targetUserId,
-			assigneeId: report.assigneeId,
-			reporter: Users.pack(report.reporter || report.reporterId, null, {
-				detail: true
-			}),
-			targetUser: Users.pack(report.targetUser || report.targetUserId, null, {
-				detail: true
-			}),
-			assignee: report.assigneeId ? Users.pack(report.assignee || report.assigneeId, null, {
-				detail: true
-			}) : null,
-		});
-	}
-
-	public packMany(
-		reports: any[],
-	) {
-		return Promise.all(reports.map(x => this.pack(x)));
-	}
-}
diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts
deleted file mode 100644
index 657de55581..0000000000
--- a/src/models/repositories/antenna.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Antenna } from '@/models/entities/antenna';
-import { Packed } from '@/misc/schema';
-import { AntennaNotes, UserGroupJoinings } from '../index';
-
-@EntityRepository(Antenna)
-export class AntennaRepository extends Repository<Antenna> {
-	public async pack(
-		src: Antenna['id'] | Antenna,
-	): Promise<Packed<'Antenna'>> {
-		const antenna = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null;
-		const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOne(antenna.userGroupJoiningId) : null;
-
-		return {
-			id: antenna.id,
-			createdAt: antenna.createdAt.toISOString(),
-			name: antenna.name,
-			keywords: antenna.keywords,
-			excludeKeywords: antenna.excludeKeywords,
-			src: antenna.src,
-			userListId: antenna.userListId,
-			userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
-			users: antenna.users,
-			caseSensitive: antenna.caseSensitive,
-			notify: antenna.notify,
-			withReplies: antenna.withReplies,
-			withFile: antenna.withFile,
-			hasUnreadNote
-		};
-	}
-}
-
-export const packedAntennaSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id'
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time'
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const
-		},
-		keywords: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				}
-			}
-		},
-		excludeKeywords: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				}
-			}
-		},
-		src: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			enum: ['home', 'all', 'users', 'list', 'group']
-		},
-		userListId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id'
-		},
-		userGroupId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id'
-		},
-		users: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const
-			}
-		},
-		caseSensitive: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-			default: false
-		},
-		notify: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const
-		},
-		withReplies: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-			default: false
-		},
-		withFile: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const
-		},
-		hasUnreadNote: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-			default: false
-		}
-	},
-};
diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts
deleted file mode 100644
index 0226edad11..0000000000
--- a/src/models/repositories/app.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { App } from '@/models/entities/app';
-import { AccessTokens } from '../index';
-import { Packed } from '@/misc/schema';
-import { User } from '../entities/user';
-
-@EntityRepository(App)
-export class AppRepository extends Repository<App> {
-	public async pack(
-		src: App['id'] | App,
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			detail?: boolean,
-			includeSecret?: boolean,
-			includeProfileImageIds?: boolean
-		}
-	): Promise<Packed<'App'>> {
-		const opts = Object.assign({
-			detail: false,
-			includeSecret: false,
-			includeProfileImageIds: false
-		}, options);
-
-		const app = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: app.id,
-			name: app.name,
-			callbackUrl: app.callbackUrl,
-			permission: app.permission,
-			...(opts.includeSecret ? { secret: app.secret } : {}),
-			...(me ? {
-				isAuthorized: await AccessTokens.count({
-					appId: app.id,
-					userId: me,
-				}).then(count => count > 0)
-			} : {})
-		};
-	}
-}
-
-export const packedAppSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const
-		},
-		callbackUrl: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const
-		},
-		permission: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const
-			}
-		},
-		secret: {
-			type: 'string' as const,
-			optional: true as const, nullable: false as const
-		},
-		isAuthorized: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const
-		}
-	}
-};
diff --git a/src/models/repositories/auth-session.ts b/src/models/repositories/auth-session.ts
deleted file mode 100644
index c8f4c10f2a..0000000000
--- a/src/models/repositories/auth-session.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Apps } from '../index';
-import { AuthSession } from '@/models/entities/auth-session';
-import { awaitAll } from '@/prelude/await-all';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(AuthSession)
-export class AuthSessionRepository extends Repository<AuthSession> {
-	public async pack(
-		src: AuthSession['id'] | AuthSession,
-		me?: { id: User['id'] } | null | undefined
-	) {
-		const session = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: session.id,
-			app: Apps.pack(session.appId, me),
-			token: session.token
-		});
-	}
-}
diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts
deleted file mode 100644
index ac60c9a4ce..0000000000
--- a/src/models/repositories/blocking.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Users } from '../index';
-import { Blocking } from '@/models/entities/blocking';
-import { awaitAll } from '@/prelude/await-all';
-import { Packed } from '@/misc/schema';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(Blocking)
-export class BlockingRepository extends Repository<Blocking> {
-	public async pack(
-		src: Blocking['id'] | Blocking,
-		me?: { id: User['id'] } | null | undefined
-	): Promise<Packed<'Blocking'>> {
-		const blocking = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: blocking.id,
-			createdAt: blocking.createdAt.toISOString(),
-			blockeeId: blocking.blockeeId,
-			blockee: Users.pack(blocking.blockeeId, me, {
-				detail: true
-			})
-		});
-	}
-
-	public packMany(
-		blockings: any[],
-		me: { id: User['id'] }
-	) {
-		return Promise.all(blockings.map(x => this.pack(x, me)));
-	}
-}
-
-export const packedBlockingSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		blockeeId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		blockee: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-	}
-};
diff --git a/src/models/repositories/channel.ts b/src/models/repositories/channel.ts
deleted file mode 100644
index 5c7d095473..0000000000
--- a/src/models/repositories/channel.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Channel } from '@/models/entities/channel';
-import { Packed } from '@/misc/schema';
-import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(Channel)
-export class ChannelRepository extends Repository<Channel> {
-	public async pack(
-		src: Channel['id'] | Channel,
-		me?: { id: User['id'] } | null | undefined,
-	): Promise<Packed<'Channel'>> {
-		const channel = typeof src === 'object' ? src : await this.findOneOrFail(src);
-		const meId = me ? me.id : null;
-
-		const banner = channel.bannerId ? await DriveFiles.findOne(channel.bannerId) : null;
-
-		const hasUnreadNote = meId ? (await NoteUnreads.findOne({ noteChannelId: channel.id, userId: meId })) != null : undefined;
-
-		const following = meId ? await ChannelFollowings.findOne({
-			followerId: meId,
-			followeeId: channel.id,
-		}) : null;
-
-		return {
-			id: channel.id,
-			createdAt: channel.createdAt.toISOString(),
-			lastNotedAt: channel.lastNotedAt ? channel.lastNotedAt.toISOString() : null,
-			name: channel.name,
-			description: channel.description,
-			userId: channel.userId,
-			bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
-			usersCount: channel.usersCount,
-			notesCount: channel.notesCount,
-
-			...(me ? {
-				isFollowing: following != null,
-				hasUnreadNote,
-			} : {})
-		};
-	}
-}
-
-export const packedChannelSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		lastNotedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		description: {
-			type: 'string' as const,
-			nullable: true as const, optional: false as const,
-		},
-		bannerUrl: {
-			type: 'string' as const,
-			format: 'url',
-			nullable: true as const, optional: false as const,
-		},
-		notesCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: false as const,
-		},
-		usersCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: false as const,
-		},
-		isFollowing: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		userId: {
-			type: 'string' as const,
-			nullable: true as const, optional: false as const,
-			format: 'id',
-		},
-	},
-};
diff --git a/src/models/repositories/clip.ts b/src/models/repositories/clip.ts
deleted file mode 100644
index 7892811d48..0000000000
--- a/src/models/repositories/clip.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Clip } from '@/models/entities/clip';
-import { Packed } from '@/misc/schema';
-import { Users } from '../index';
-import { awaitAll } from '@/prelude/await-all';
-
-@EntityRepository(Clip)
-export class ClipRepository extends Repository<Clip> {
-	public async pack(
-		src: Clip['id'] | Clip,
-	): Promise<Packed<'Clip'>> {
-		const clip = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: clip.id,
-			createdAt: clip.createdAt.toISOString(),
-			userId: clip.userId,
-			user: Users.pack(clip.user || clip.userId),
-			name: clip.name,
-			description: clip.description,
-			isPublic: clip.isPublic,
-		});
-	}
-
-	public packMany(
-		clips: Clip[],
-	) {
-		return Promise.all(clips.map(x => this.pack(x)));
-	}
-}
-
-export const packedClipSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: false as const, nullable: false as const,
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		description: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		isPublic: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts
deleted file mode 100644
index ddf9a46afd..0000000000
--- a/src/models/repositories/drive-file.ts
+++ /dev/null
@@ -1,249 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { DriveFile } from '@/models/entities/drive-file';
-import { Users, DriveFolders } from '../index';
-import { User } from '@/models/entities/user';
-import { toPuny } from '@/misc/convert-host';
-import { awaitAll } from '@/prelude/await-all';
-import { Packed } from '@/misc/schema';
-import config from '@/config/index';
-import { query, appendQuery } from '@/prelude/url';
-import { Meta } from '@/models/entities/meta';
-import { fetchMeta } from '@/misc/fetch-meta';
-
-type PackOptions = {
-	detail?: boolean,
-	self?: boolean,
-	withUser?: boolean,
-};
-
-@EntityRepository(DriveFile)
-export class DriveFileRepository extends Repository<DriveFile> {
-	public validateFileName(name: string): boolean {
-		return (
-			(name.trim().length > 0) &&
-			(name.length <= 200) &&
-			(name.indexOf('\\') === -1) &&
-			(name.indexOf('/') === -1) &&
-			(name.indexOf('..') === -1)
-		);
-	}
-
-	public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
-		// リモートかつメディアプロキシ
-		if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
-			return appendQuery(config.mediaProxy, query({
-				url: file.uri,
-				thumbnail: thumbnail ? '1' : undefined
-			}));
-		}
-
-		// リモートかつ期限切れはローカルプロキシを試みる
-		if (file.uri != null && file.isLink && meta && meta.proxyRemoteFiles) {
-			const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey;
-
-			if (key && !key.match('/')) {	// 古いものはここにオブジェクトストレージキーが入ってるので除外
-				return `${config.url}/files/${key}`;
-			}
-		}
-
-		const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type);
-
-		return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
-	}
-
-	public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> {
-		const id = typeof user === 'object' ? user.id : user;
-
-		const { sum } = await this
-			.createQueryBuilder('file')
-			.where('file.userId = :id', { id: id })
-			.andWhere('file.isLink = FALSE')
-			.select('SUM(file.size)', 'sum')
-			.getRawOne();
-
-		return parseInt(sum, 10) || 0;
-	}
-
-	public async calcDriveUsageOfHost(host: string): Promise<number> {
-		const { sum } = await this
-			.createQueryBuilder('file')
-			.where('file.userHost = :host', { host: toPuny(host) })
-			.andWhere('file.isLink = FALSE')
-			.select('SUM(file.size)', 'sum')
-			.getRawOne();
-
-		return parseInt(sum, 10) || 0;
-	}
-
-	public async calcDriveUsageOfLocal(): Promise<number> {
-		const { sum } = await this
-			.createQueryBuilder('file')
-			.where('file.userHost IS NULL')
-			.andWhere('file.isLink = FALSE')
-			.select('SUM(file.size)', 'sum')
-			.getRawOne();
-
-		return parseInt(sum, 10) || 0;
-	}
-
-	public async calcDriveUsageOfRemote(): Promise<number> {
-		const { sum } = await this
-			.createQueryBuilder('file')
-			.where('file.userHost IS NOT NULL')
-			.andWhere('file.isLink = FALSE')
-			.select('SUM(file.size)', 'sum')
-			.getRawOne();
-
-		return parseInt(sum, 10) || 0;
-	}
-
-	public async pack(src: DriveFile['id'], options?: PackOptions): Promise<Packed<'DriveFile'> | null>;
-	public async pack(src: DriveFile, options?: PackOptions): Promise<Packed<'DriveFile'>>;
-	public async pack(
-		src: DriveFile['id'] | DriveFile,
-		options?: PackOptions
-	): Promise<Packed<'DriveFile'> | null> {
-		const opts = Object.assign({
-			detail: false,
-			self: false
-		}, options);
-
-		const file = typeof src === 'object' ? src : await this.findOne(src);
-		if (file == null) return null;
-
-		const meta = await fetchMeta();
-
-		return await awaitAll({
-			id: file.id,
-			createdAt: file.createdAt.toISOString(),
-			name: file.name,
-			type: file.type,
-			md5: file.md5,
-			size: file.size,
-			isSensitive: file.isSensitive,
-			blurhash: file.blurhash,
-			properties: file.properties,
-			url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
-			thumbnailUrl: this.getPublicUrl(file, true, meta),
-			comment: file.comment,
-			folderId: file.folderId,
-			folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
-				detail: true
-			}) : null,
-			userId: opts.withUser ? file.userId : null,
-			user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null
-		});
-	}
-
-	public async packMany(
-		files: (DriveFile['id'] | DriveFile)[],
-		options?: PackOptions
-	) {
-		const items = await Promise.all(files.map(f => this.pack(f, options)));
-		return items.filter(x => x != null);
-	}
-}
-
-export const packedDriveFileSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			example: 'lenna.jpg'
-		},
-		type: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			example: 'image/jpeg'
-		},
-		md5: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'md5',
-			example: '15eca7fba0480996e2245f5185bf39f2'
-		},
-		size: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-			example: 51469
-		},
-		isSensitive: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		blurhash: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const
-		},
-		properties: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				width: {
-					type: 'number' as const,
-					optional: true as const, nullable: false as const,
-					example: 1280
-				},
-				height: {
-					type: 'number' as const,
-					optional: true as const, nullable: false as const,
-					example: 720
-				},
-				avgColor: {
-					type: 'string' as const,
-					optional: true as const, nullable: false as const,
-					example: 'rgb(40,65,87)'
-				}
-			}
-		},
-		url: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'url',
-		},
-		thumbnailUrl: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'url',
-		},
-		comment: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const
-		},
-		folderId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		folder: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'DriveFolder' as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		user: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'User' as const,
-		}
-	},
-};
diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts
deleted file mode 100644
index 8ef6f01b5d..0000000000
--- a/src/models/repositories/drive-folder.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { DriveFolders, DriveFiles } from '../index';
-import { DriveFolder } from '@/models/entities/drive-folder';
-import { awaitAll } from '@/prelude/await-all';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(DriveFolder)
-export class DriveFolderRepository extends Repository<DriveFolder> {
-	public validateFolderName(name: string): boolean {
-		return (
-			(name.trim().length > 0) &&
-			(name.length <= 200)
-		);
-	}
-
-	public async pack(
-		src: DriveFolder['id'] | DriveFolder,
-		options?: {
-			detail: boolean
-		}
-	): Promise<Packed<'DriveFolder'>> {
-		const opts = Object.assign({
-			detail: false
-		}, options);
-
-		const folder = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: folder.id,
-			createdAt: folder.createdAt.toISOString(),
-			name: folder.name,
-			parentId: folder.parentId,
-
-			...(opts.detail ? {
-				foldersCount: DriveFolders.count({
-					parentId: folder.id
-				}),
-				filesCount: DriveFiles.count({
-					folderId: folder.id
-				}),
-
-				...(folder.parentId ? {
-					parent: this.pack(folder.parentId, {
-						detail: true
-					})
-				} : {})
-			} : {})
-		});
-	}
-}
-
-export const packedDriveFolderSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		foldersCount: {
-			type: 'number' as const,
-			optional: true as const, nullable: false as const,
-		},
-		filesCount: {
-			type: 'number' as const,
-			optional: true as const, nullable: false as const,
-		},
-		parentId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		parent: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'DriveFolder' as const,
-		},
-	},
-};
diff --git a/src/models/repositories/emoji.ts b/src/models/repositories/emoji.ts
deleted file mode 100644
index 7985c27aba..0000000000
--- a/src/models/repositories/emoji.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Emoji } from '@/models/entities/emoji';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(Emoji)
-export class EmojiRepository extends Repository<Emoji> {
-	public async pack(
-		src: Emoji['id'] | Emoji,
-	): Promise<Packed<'Emoji'>> {
-		const emoji = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: emoji.id,
-			aliases: emoji.aliases,
-			name: emoji.name,
-			category: emoji.category,
-			host: emoji.host,
-			url: emoji.url,
-		};
-	}
-
-	public packMany(
-		emojis: any[],
-	) {
-		return Promise.all(emojis.map(x => this.pack(x)));
-	}
-}
-
-export const packedEmojiSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		aliases: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-			},
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		category: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		host: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		url: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-	}
-};
diff --git a/src/models/repositories/federation-instance.ts b/src/models/repositories/federation-instance.ts
deleted file mode 100644
index 4b70971ecf..0000000000
--- a/src/models/repositories/federation-instance.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import config from '@/config/index';
-
-export const packedFederationInstanceSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id'
-		},
-		caughtAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time'
-		},
-		host: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			example: 'misskey.example.com'
-		},
-		usersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		notesCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		followingCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		followersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		driveUsage: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		driveFiles: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		latestRequestSentAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'date-time'
-		},
-		lastCommunicatedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time'
-		},
-		isNotResponding: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const
-		},
-		isSuspended: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const
-		},
-		softwareName: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			example: 'misskey'
-		},
-		softwareVersion: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			example: config.version
-		},
-		openRegistrations: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: true as const,
-			example: true
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const
-		},
-		description: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const
-		},
-		maintainerName: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const
-		},
-		maintainerEmail: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const
-		},
-		iconUrl: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'url'
-		},
-		infoUpdatedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'date-time'
-		}
-	}
-};
diff --git a/src/models/repositories/follow-request.ts b/src/models/repositories/follow-request.ts
deleted file mode 100644
index d6ee58e235..0000000000
--- a/src/models/repositories/follow-request.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { FollowRequest } from '@/models/entities/follow-request';
-import { Users } from '../index';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(FollowRequest)
-export class FollowRequestRepository extends Repository<FollowRequest> {
-	public async pack(
-		src: FollowRequest['id'] | FollowRequest,
-		me?: { id: User['id'] } | null | undefined
-	) {
-		const request = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: request.id,
-			follower: await Users.pack(request.followerId, me),
-			followee: await Users.pack(request.followeeId, me),
-		};
-	}
-}
diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts
deleted file mode 100644
index b1f716069f..0000000000
--- a/src/models/repositories/following.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Users } from '../index';
-import { Following } from '@/models/entities/following';
-import { awaitAll } from '@/prelude/await-all';
-import { Packed } from '@/misc/schema';
-import { User } from '@/models/entities/user';
-
-type LocalFollowerFollowing = Following & {
-	followerHost: null;
-	followerInbox: null;
-	followerSharedInbox: null;
-};
-
-type RemoteFollowerFollowing = Following & {
-	followerHost: string;
-	followerInbox: string;
-	followerSharedInbox: string;
-};
-
-type LocalFolloweeFollowing = Following & {
-	followeeHost: null;
-	followeeInbox: null;
-	followeeSharedInbox: null;
-};
-
-type RemoteFolloweeFollowing = Following & {
-	followeeHost: string;
-	followeeInbox: string;
-	followeeSharedInbox: string;
-};
-
-@EntityRepository(Following)
-export class FollowingRepository extends Repository<Following> {
-	public isLocalFollower(following: Following): following is LocalFollowerFollowing {
-		return following.followerHost == null;
-	}
-
-	public isRemoteFollower(following: Following): following is RemoteFollowerFollowing {
-		return following.followerHost != null;
-	}
-
-	public isLocalFollowee(following: Following): following is LocalFolloweeFollowing {
-		return following.followeeHost == null;
-	}
-
-	public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing {
-		return following.followeeHost != null;
-	}
-
-	public async pack(
-		src: Following['id'] | Following,
-		me?: { id: User['id'] } | null | undefined,
-		opts?: {
-			populateFollowee?: boolean;
-			populateFollower?: boolean;
-		}
-	): Promise<Packed<'Following'>> {
-		const following = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		if (opts == null) opts = {};
-
-		return await awaitAll({
-			id: following.id,
-			createdAt: following.createdAt.toISOString(),
-			followeeId: following.followeeId,
-			followerId: following.followerId,
-			followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
-				detail: true
-			}) : undefined,
-			follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
-				detail: true
-			}) : undefined,
-		});
-	}
-
-	public packMany(
-		followings: any[],
-		me?: { id: User['id'] } | null | undefined,
-		opts?: {
-			populateFollowee?: boolean;
-			populateFollower?: boolean;
-		}
-	) {
-		return Promise.all(followings.map(x => this.pack(x, me, opts)));
-	}
-}
-
-export const packedFollowingSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		followeeId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		followee: {
-			type: 'object' as const,
-			optional: true as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-		followerId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		follower: {
-			type: 'object' as const,
-			optional: true as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-	}
-};
diff --git a/src/models/repositories/gallery-like.ts b/src/models/repositories/gallery-like.ts
deleted file mode 100644
index 79123e5eec..0000000000
--- a/src/models/repositories/gallery-like.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { GalleryLike } from '@/models/entities/gallery-like';
-import { GalleryPosts } from '../index';
-
-@EntityRepository(GalleryLike)
-export class GalleryLikeRepository extends Repository<GalleryLike> {
-	public async pack(
-		src: GalleryLike['id'] | GalleryLike,
-		me?: any
-	) {
-		const like = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: like.id,
-			post: await GalleryPosts.pack(like.post || like.postId, me),
-		};
-	}
-
-	public packMany(
-		likes: any[],
-		me: any
-	) {
-		return Promise.all(likes.map(x => this.pack(x, me)));
-	}
-}
diff --git a/src/models/repositories/gallery-post.ts b/src/models/repositories/gallery-post.ts
deleted file mode 100644
index 4f666ff252..0000000000
--- a/src/models/repositories/gallery-post.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { GalleryPost } from '@/models/entities/gallery-post';
-import { Packed } from '@/misc/schema';
-import { Users, DriveFiles, GalleryLikes } from '../index';
-import { awaitAll } from '@/prelude/await-all';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(GalleryPost)
-export class GalleryPostRepository extends Repository<GalleryPost> {
-	public async pack(
-		src: GalleryPost['id'] | GalleryPost,
-		me?: { id: User['id'] } | null | undefined,
-	): Promise<Packed<'GalleryPost'>> {
-		const meId = me ? me.id : null;
-		const post = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: post.id,
-			createdAt: post.createdAt.toISOString(),
-			updatedAt: post.updatedAt.toISOString(),
-			userId: post.userId,
-			user: Users.pack(post.user || post.userId, me),
-			title: post.title,
-			description: post.description,
-			fileIds: post.fileIds,
-			files: DriveFiles.packMany(post.fileIds),
-			tags: post.tags.length > 0 ? post.tags : undefined,
-			isSensitive: post.isSensitive,
-			likedCount: post.likedCount,
-			isLiked: meId ? await GalleryLikes.findOne({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
-		});
-	}
-
-	public packMany(
-		posts: GalleryPost[],
-		me?: { id: User['id'] } | null | undefined,
-	) {
-		return Promise.all(posts.map(x => this.pack(x, me)));
-	}
-}
-
-export const packedGalleryPostSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		updatedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		title: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		description: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: false as const, nullable: false as const,
-		},
-		fileIds: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			}
-		},
-		files: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'DriveFile' as const,
-			}
-		},
-		tags: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			}
-		},
-		isSensitive: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-	}
-};
diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts
deleted file mode 100644
index 9adb386fa9..0000000000
--- a/src/models/repositories/games/reversi/game.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { User } from '@/models/entities/user';
-import { EntityRepository, Repository } from 'typeorm';
-import { Users } from '../../../index';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(ReversiGame)
-export class ReversiGameRepository extends Repository<ReversiGame> {
-	public async pack(
-		src: ReversiGame['id'] | ReversiGame,
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			detail?: boolean
-		}
-	): Promise<Packed<'ReversiGame'>> {
-		const opts = Object.assign({
-			detail: true
-		}, options);
-
-		const game = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: game.id,
-			createdAt: game.createdAt.toISOString(),
-			startedAt: game.startedAt && game.startedAt.toISOString(),
-			isStarted: game.isStarted,
-			isEnded: game.isEnded,
-			form1: game.form1,
-			form2: game.form2,
-			user1Accepted: game.user1Accepted,
-			user2Accepted: game.user2Accepted,
-			user1Id: game.user1Id,
-			user2Id: game.user2Id,
-			user1: await Users.pack(game.user1Id, me),
-			user2: await Users.pack(game.user2Id, me),
-			winnerId: game.winnerId,
-			winner: game.winnerId ? await Users.pack(game.winnerId, me) : null,
-			surrendered: game.surrendered,
-			black: game.black,
-			bw: game.bw,
-			isLlotheo: game.isLlotheo,
-			canPutEverywhere: game.canPutEverywhere,
-			loopedBoard: game.loopedBoard,
-			...(opts.detail ? {
-				logs: game.logs.map(log => ({
-					at: log.at.toISOString(),
-					color: log.color,
-					pos: log.pos
-				})),
-				map: game.map,
-			} : {})
-		};
-	}
-}
-
-export const packedReversiGameSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		startedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'date-time',
-		},
-		isStarted: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		isEnded: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		form1: {
-			type: 'any' as const,
-			optional: false as const, nullable: true as const,
-		},
-		form2: {
-			type: 'any' as const,
-			optional: false as const, nullable: true as const,
-		},
-		user1Accepted: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		user2Accepted: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		user1Id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		user2Id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		user1: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-		user2: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-		winnerId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		winner: {
-			type: 'object' as const,
-			optional: false as const, nullable: true as const,
-			ref: 'User' as const,
-		},
-		surrendered: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		black: {
-			type: 'number' as const,
-			optional: false as const, nullable: true as const,
-		},
-		bw: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		isLlotheo: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		canPutEverywhere: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		loopedBoard: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		logs: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'object' as const,
-				optional: true as const, nullable: false as const,
-				properties: {
-					at: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
-						format: 'date-time',
-					},
-					color: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const,
-					},
-					pos: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-					},
-				}
-			}
-		},
-		map: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			}
-		}
-	}
-};
diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts
deleted file mode 100644
index b4515800df..0000000000
--- a/src/models/repositories/games/reversi/matching.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { ReversiMatching } from '@/models/entities/games/reversi/matching';
-import { Users } from '../../../index';
-import { awaitAll } from '@/prelude/await-all';
-import { User } from '@/models/entities/user';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(ReversiMatching)
-export class ReversiMatchingRepository extends Repository<ReversiMatching> {
-	public async pack(
-		src: ReversiMatching['id'] | ReversiMatching,
-		me: { id: User['id'] }
-	): Promise<Packed<'ReversiMatching'>> {
-		const matching = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: matching.id,
-			createdAt: matching.createdAt.toISOString(),
-			parentId: matching.parentId,
-			parent: Users.pack(matching.parentId, me, {
-				detail: true
-			}),
-			childId: matching.childId,
-			child: Users.pack(matching.childId, me, {
-				detail: true
-			})
-		});
-	}
-}
-
-export const packedReversiMatchingSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		parentId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		parent: {
-			type: 'object' as const,
-			optional: false as const, nullable: true as const,
-			ref: 'User' as const,
-		},
-		childId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		child: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-	}
-};
diff --git a/src/models/repositories/hashtag.ts b/src/models/repositories/hashtag.ts
deleted file mode 100644
index d52f6ba7c6..0000000000
--- a/src/models/repositories/hashtag.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Hashtag } from '@/models/entities/hashtag';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(Hashtag)
-export class HashtagRepository extends Repository<Hashtag> {
-	public async pack(
-		src: Hashtag,
-	): Promise<Packed<'Hashtag'>> {
-		return {
-			tag: src.name,
-			mentionedUsersCount: src.mentionedUsersCount,
-			mentionedLocalUsersCount: src.mentionedLocalUsersCount,
-			mentionedRemoteUsersCount: src.mentionedRemoteUsersCount,
-			attachedUsersCount: src.attachedUsersCount,
-			attachedLocalUsersCount: src.attachedLocalUsersCount,
-			attachedRemoteUsersCount: src.attachedRemoteUsersCount,
-		};
-	}
-
-	public packMany(
-		hashtags: Hashtag[],
-	) {
-		return Promise.all(hashtags.map(x => this.pack(x)));
-	}
-}
-
-export const packedHashtagSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		tag: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			example: 'misskey',
-		},
-		mentionedUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		mentionedLocalUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		mentionedRemoteUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		attachedUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		attachedLocalUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		attachedRemoteUsersCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-	}
-};
diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts
deleted file mode 100644
index abdff63689..0000000000
--- a/src/models/repositories/messaging-message.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-import { Users, DriveFiles, UserGroups } from '../index';
-import { Packed } from '@/misc/schema';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(MessagingMessage)
-export class MessagingMessageRepository extends Repository<MessagingMessage> {
-	public validateText(text: string): boolean {
-		return text.trim().length <= 1000 && text.trim() != '';
-	}
-
-	public async pack(
-		src: MessagingMessage['id'] | MessagingMessage,
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			populateRecipient?: boolean,
-			populateGroup?: boolean,
-		}
-	): Promise<Packed<'MessagingMessage'>> {
-		const opts = options || {
-			populateRecipient: true,
-			populateGroup: true,
-		};
-
-		const message = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: message.id,
-			createdAt: message.createdAt.toISOString(),
-			text: message.text,
-			userId: message.userId,
-			user: await Users.pack(message.user || message.userId, me),
-			recipientId: message.recipientId,
-			recipient: message.recipientId && opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : undefined,
-			groupId: message.groupId,
-			group: message.groupId && opts.populateGroup ? await UserGroups.pack(message.group || message.groupId) : undefined,
-			fileId: message.fileId,
-			file: message.fileId ? await DriveFiles.pack(message.fileId) : null,
-			isRead: message.isRead,
-			reads: message.reads,
-		};
-	}
-}
-
-export const packedMessagingMessageSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: true as const, nullable: false as const,
-		},
-		text: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		fileId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-		},
-		file: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'DriveFile' as const,
-		},
-		recipientId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-		},
-		recipient: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'User' as const,
-		},
-		groupId: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-			format: 'id',
-		},
-		group: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'UserGroup' as const,
-		},
-		isRead: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		reads: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			}
-		},
-	},
-};
diff --git a/src/models/repositories/moderation-logs.ts b/src/models/repositories/moderation-logs.ts
deleted file mode 100644
index c7df3afdc9..0000000000
--- a/src/models/repositories/moderation-logs.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Users } from '../index';
-import { ModerationLog } from '@/models/entities/moderation-log';
-import { awaitAll } from '@/prelude/await-all';
-
-@EntityRepository(ModerationLog)
-export class ModerationLogRepository extends Repository<ModerationLog> {
-	public async pack(
-		src: ModerationLog['id'] | ModerationLog,
-	) {
-		const log = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: log.id,
-			createdAt: log.createdAt,
-			type: log.type,
-			info: log.info,
-			userId: log.userId,
-			user: Users.pack(log.user || log.userId, null, {
-				detail: true
-			}),
-		});
-	}
-
-	public packMany(
-		reports: any[],
-	) {
-		return Promise.all(reports.map(x => this.pack(x)));
-	}
-}
diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts
deleted file mode 100644
index 869afd3c4e..0000000000
--- a/src/models/repositories/muting.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Users } from '../index';
-import { Muting } from '@/models/entities/muting';
-import { awaitAll } from '@/prelude/await-all';
-import { Packed } from '@/misc/schema';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(Muting)
-export class MutingRepository extends Repository<Muting> {
-	public async pack(
-		src: Muting['id'] | Muting,
-		me?: { id: User['id'] } | null | undefined
-	): Promise<Packed<'Muting'>> {
-		const muting = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return await awaitAll({
-			id: muting.id,
-			createdAt: muting.createdAt.toISOString(),
-			muteeId: muting.muteeId,
-			mutee: Users.pack(muting.muteeId, me, {
-				detail: true
-			})
-		});
-	}
-
-	public packMany(
-		mutings: any[],
-		me: { id: User['id'] }
-	) {
-		return Promise.all(mutings.map(x => this.pack(x, me)));
-	}
-}
-
-export const packedMutingSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		muteeId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		mutee: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-	}
-};
diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts
deleted file mode 100644
index 47586a9116..0000000000
--- a/src/models/repositories/note-favorite.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { NoteFavorite } from '@/models/entities/note-favorite';
-import { Notes } from '../index';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(NoteFavorite)
-export class NoteFavoriteRepository extends Repository<NoteFavorite> {
-	public async pack(
-		src: NoteFavorite['id'] | NoteFavorite,
-		me?: { id: User['id'] } | null | undefined
-	) {
-		const favorite = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: favorite.id,
-			createdAt: favorite.createdAt,
-			noteId: favorite.noteId,
-			note: await Notes.pack(favorite.note || favorite.noteId, me),
-		};
-	}
-
-	public packMany(
-		favorites: any[],
-		me: { id: User['id'] }
-	) {
-		return Promise.all(favorites.map(x => this.pack(x, me)));
-	}
-}
-
-export const packedNoteFavoriteSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		note: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note' as const,
-		},
-		noteId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-	},
-};
diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts
deleted file mode 100644
index 5d86065526..0000000000
--- a/src/models/repositories/note-reaction.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { NoteReaction } from '@/models/entities/note-reaction';
-import { Notes, Users } from '../index';
-import { Packed } from '@/misc/schema';
-import { convertLegacyReaction } from '@/misc/reaction-lib';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(NoteReaction)
-export class NoteReactionRepository extends Repository<NoteReaction> {
-	public async pack(
-		src: NoteReaction['id'] | NoteReaction,
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			withNote: boolean;
-		},
-	): Promise<Packed<'NoteReaction'>> {
-		const opts = Object.assign({
-			withNote: false,
-		}, options);
-
-		const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: reaction.id,
-			createdAt: reaction.createdAt.toISOString(),
-			user: await Users.pack(reaction.userId, me),
-			type: convertLegacyReaction(reaction.reaction),
-			...(opts.withNote ? {
-				note: await Notes.pack(reaction.noteId, me),
-			} : {})
-		};
-	}
-}
-
-export const packedNoteReactionSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		user: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User' as const,
-		},
-		type: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-	},
-};
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
deleted file mode 100644
index 0f00c34c9c..0000000000
--- a/src/models/repositories/note.ts
+++ /dev/null
@@ -1,512 +0,0 @@
-import { EntityRepository, Repository, In } from 'typeorm';
-import * as mfm from 'mfm-js';
-import { Note } from '@/models/entities/note';
-import { User } from '@/models/entities/user';
-import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '../index';
-import { Packed } from '@/misc/schema';
-import { nyaize } from '@/misc/nyaize';
-import { awaitAll } from '@/prelude/await-all';
-import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib';
-import { NoteReaction } from '@/models/entities/note-reaction';
-import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis';
-
-@EntityRepository(Note)
-export class NoteRepository extends Repository<Note> {
-	public validateCw(x: string) {
-		return x.trim().length <= 100;
-	}
-
-	public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
-		// visibility が specified かつ自分が指定されていなかったら非表示
-		if (note.visibility === 'specified') {
-			if (meId == null) {
-				return false;
-			} else if (meId === note.userId) {
-				return true;
-			} else {
-				// 指定されているかどうか
-				const specified = note.visibleUserIds.some((id: any) => meId === id);
-
-				if (specified) {
-					return true;
-				} else {
-					return false;
-				}
-			}
-		}
-
-		// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
-		if (note.visibility === 'followers') {
-			if (meId == null) {
-				return false;
-			} else if (meId === note.userId) {
-				return true;
-			} else if (note.reply && (meId === note.reply.userId)) {
-				// 自分の投稿に対するリプライ
-				return true;
-			} else if (note.mentions && note.mentions.some(id => meId === id)) {
-				// 自分へのメンション
-				return true;
-			} else {
-				// フォロワーかどうか
-				const following = await Followings.findOne({
-					followeeId: note.userId,
-					followerId: meId
-				});
-
-				if (following == null) {
-					return false;
-				} else {
-					return true;
-				}
-			}
-		}
-
-		return true;
-	}
-
-	private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
-		// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
-		let hide = false;
-
-		// visibility が specified かつ自分が指定されていなかったら非表示
-		if (packedNote.visibility === 'specified') {
-			if (meId == null) {
-				hide = true;
-			} else if (meId === packedNote.userId) {
-				hide = false;
-			} else {
-				// 指定されているかどうか
-				const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
-
-				if (specified) {
-					hide = false;
-				} else {
-					hide = true;
-				}
-			}
-		}
-
-		// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
-		if (packedNote.visibility === 'followers') {
-			if (meId == null) {
-				hide = true;
-			} else if (meId === packedNote.userId) {
-				hide = false;
-			} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
-				// 自分の投稿に対するリプライ
-				hide = false;
-			} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
-				// 自分へのメンション
-				hide = false;
-			} else {
-				// フォロワーかどうか
-				const following = await Followings.findOne({
-					followeeId: packedNote.userId,
-					followerId: meId
-				});
-
-				if (following == null) {
-					hide = true;
-				} else {
-					hide = false;
-				}
-			}
-		}
-
-		if (hide) {
-			packedNote.visibleUserIds = undefined;
-			packedNote.fileIds = [];
-			packedNote.files = [];
-			packedNote.text = null;
-			packedNote.poll = undefined;
-			packedNote.cw = null;
-			packedNote.isHidden = true;
-		}
-	}
-
-	public async pack(
-		src: Note['id'] | Note,
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			detail?: boolean;
-			skipHide?: boolean;
-			_hint_?: {
-				myReactions: Map<Note['id'], NoteReaction | null>;
-			};
-		}
-	): Promise<Packed<'Note'>> {
-		const opts = Object.assign({
-			detail: true,
-			skipHide: false
-		}, options);
-
-		const meId = me ? me.id : null;
-		const note = typeof src === 'object' ? src : await this.findOneOrFail(src);
-		const host = note.userHost;
-
-		async function populatePoll() {
-			const poll = await Polls.findOneOrFail(note.id);
-			const choices = poll.choices.map(c => ({
-				text: c,
-				votes: poll.votes[poll.choices.indexOf(c)],
-				isVoted: false
-			}));
-
-			if (poll.multiple) {
-				const votes = await PollVotes.find({
-					userId: meId!,
-					noteId: note.id
-				});
-
-				const myChoices = votes.map(v => v.choice);
-				for (const myChoice of myChoices) {
-					choices[myChoice].isVoted = true;
-				}
-			} else {
-				const vote = await PollVotes.findOne({
-					userId: meId!,
-					noteId: note.id
-				});
-
-				if (vote) {
-					choices[vote.choice].isVoted = true;
-				}
-			}
-
-			return {
-				multiple: poll.multiple,
-				expiresAt: poll.expiresAt,
-				choices
-			};
-		}
-
-		async function populateMyReaction() {
-			if (options?._hint_?.myReactions) {
-				const reaction = options._hint_.myReactions.get(note.id);
-				if (reaction) {
-					return convertLegacyReaction(reaction.reaction);
-				} else if (reaction === null) {
-					return undefined;
-				}
-				// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
-			}
-
-			const reaction = await NoteReactions.findOne({
-				userId: meId!,
-				noteId: note.id,
-			});
-
-			if (reaction) {
-				return convertLegacyReaction(reaction.reaction);
-			}
-
-			return undefined;
-		}
-
-		let text = note.text;
-
-		if (note.name && (note.url || note.uri)) {
-			text = `【${note.name}】\n${(note.text || '').trim()}\n\n${note.url || note.uri}`;
-		}
-
-		const channel = note.channelId
-			? note.channel
-				? note.channel
-				: await Channels.findOne(note.channelId)
-			: null;
-
-		const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
-
-		const packed = await awaitAll({
-			id: note.id,
-			createdAt: note.createdAt.toISOString(),
-			userId: note.userId,
-			user: Users.pack(note.user || note.userId, me, {
-				detail: false,
-			}),
-			text: text,
-			cw: note.cw,
-			visibility: note.visibility,
-			localOnly: note.localOnly || undefined,
-			visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
-			viaMobile: note.viaMobile || undefined,
-			renoteCount: note.renoteCount,
-			repliesCount: note.repliesCount,
-			reactions: convertLegacyReactions(note.reactions),
-			tags: note.tags.length > 0 ? note.tags : undefined,
-			emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host),
-			fileIds: note.fileIds,
-			files: DriveFiles.packMany(note.fileIds),
-			replyId: note.replyId,
-			renoteId: note.renoteId,
-			channelId: note.channelId || undefined,
-			channel: channel ? {
-				id: channel.id,
-				name: channel.name,
-			} : undefined,
-			mentions: note.mentions.length > 0 ? note.mentions : undefined,
-			uri: note.uri || undefined,
-			url: note.url || undefined,
-
-			...(opts.detail ? {
-				reply: note.replyId ? this.pack(note.reply || note.replyId, me, {
-					detail: false,
-					_hint_: options?._hint_
-				}) : undefined,
-
-				renote: note.renoteId ? this.pack(note.renote || note.renoteId, me, {
-					detail: true,
-					_hint_: options?._hint_
-				}) : undefined,
-
-				poll: note.hasPoll ? populatePoll() : undefined,
-
-				...(meId ? {
-					myReaction: populateMyReaction()
-				} : {})
-			} : {})
-		});
-
-		if (packed.user.isCat && packed.text) {
-			const tokens = packed.text ? mfm.parse(packed.text) : [];
-			mfm.inspect(tokens, node => {
-				if (node.type === 'text') {
-					// TODO: quoteなtextはskip
-					node.props.text = nyaize(node.props.text);
-				}
-			});
-			packed.text = mfm.toString(tokens);
-		}
-
-		if (!opts.skipHide) {
-			await this.hideNote(packed, meId);
-		}
-
-		return packed;
-	}
-
-	public async packMany(
-		notes: Note[],
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			detail?: boolean;
-			skipHide?: boolean;
-		}
-	) {
-		if (notes.length === 0) return [];
-
-		const meId = me ? me.id : null;
-		const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
-		if (meId) {
-			const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
-			const targets = [...notes.map(n => n.id), ...renoteIds];
-			const myReactions = await NoteReactions.find({
-				userId: meId,
-				noteId: In(targets),
-			});
-
-			for (const target of targets) {
-				myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
-			}
-		}
-
-		await prefetchEmojis(aggregateNoteEmojis(notes));
-
-		return await Promise.all(notes.map(n => this.pack(n, me, {
-			...options,
-			_hint_: {
-				myReactions: myReactionsMap
-			}
-		})));
-	}
-}
-
-export const packedNoteSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		text: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		cw: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: false as const, nullable: false as const,
-		},
-		replyId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		renoteId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		reply: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'Note' as const,
-		},
-		renote: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			ref: 'Note' as const,
-		},
-		viaMobile: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		isHidden: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		visibility: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		mentions: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			}
-		},
-		visibleUserIds: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			}
-		},
-		fileIds: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			}
-		},
-		files: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'DriveFile' as const,
-			}
-		},
-		tags: {
-			type: 'array' as const,
-			optional: true as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			}
-		},
-		poll: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-		},
-		channelId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		channel: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-			items: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				properties: {
-					id: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
-					},
-					name: {
-						type: 'string' as const,
-						optional: false as const, nullable: true as const,
-					},
-				},
-			},
-		},
-		localOnly: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const,
-		},
-		emojis: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				properties: {
-					name: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
-					},
-					url: {
-						type: 'string' as const,
-						optional: false as const, nullable: true as const,
-					},
-				},
-			},
-		},
-		reactions: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-		},
-		renoteCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		repliesCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		uri: {
-			type: 'string' as const,
-			optional: true as const, nullable: false as const,
-		},
-		url: {
-			type: 'string' as const,
-			optional: true as const, nullable: false as const,
-		},
-
-		myReaction: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-		},
-	},
-};
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
deleted file mode 100644
index d1cf9b087e..0000000000
--- a/src/models/repositories/notification.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import { EntityRepository, In, Repository } from 'typeorm';
-import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index';
-import { Notification } from '@/models/entities/notification';
-import { awaitAll } from '@/prelude/await-all';
-import { Packed } from '@/misc/schema';
-import { Note } from '@/models/entities/note';
-import { NoteReaction } from '@/models/entities/note-reaction';
-import { User } from '@/models/entities/user';
-import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis';
-import { notificationTypes } from '@/types';
-
-@EntityRepository(Notification)
-export class NotificationRepository extends Repository<Notification> {
-	public async pack(
-		src: Notification['id'] | Notification,
-		options: {
-			_hintForEachNotes_?: {
-				myReactions: Map<Note['id'], NoteReaction | null>;
-			};
-		}
-	): Promise<Packed<'Notification'>> {
-		const notification = typeof src === 'object' ? src : await this.findOneOrFail(src);
-		const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null;
-
-		return await awaitAll({
-			id: notification.id,
-			createdAt: notification.createdAt.toISOString(),
-			type: notification.type,
-			isRead: notification.isRead,
-			userId: notification.notifierId,
-			user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null,
-			...(notification.type === 'mention' ? {
-				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
-					detail: true,
-					_hint_: options._hintForEachNotes_
-				}),
-			} : {}),
-			...(notification.type === 'reply' ? {
-				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
-					detail: true,
-					_hint_: options._hintForEachNotes_
-				}),
-			} : {}),
-			...(notification.type === 'renote' ? {
-				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
-					detail: true,
-					_hint_: options._hintForEachNotes_
-				}),
-			} : {}),
-			...(notification.type === 'quote' ? {
-				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
-					detail: true,
-					_hint_: options._hintForEachNotes_
-				}),
-			} : {}),
-			...(notification.type === 'reaction' ? {
-				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
-					detail: true,
-					_hint_: options._hintForEachNotes_
-				}),
-				reaction: notification.reaction
-			} : {}),
-			...(notification.type === 'pollVote' ? {
-				note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, {
-					detail: true,
-					_hint_: options._hintForEachNotes_
-				}),
-				choice: notification.choice
-			} : {}),
-			...(notification.type === 'groupInvited' ? {
-				invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!),
-			} : {}),
-			...(notification.type === 'app' ? {
-				body: notification.customBody,
-				header: notification.customHeader || token?.name,
-				icon: notification.customIcon || token?.iconUrl,
-			} : {}),
-		});
-	}
-
-	public async packMany(
-		notifications: Notification[],
-		meId: User['id']
-	) {
-		if (notifications.length === 0) return [];
-
-		const notes = notifications.filter(x => x.note != null).map(x => x.note!);
-		const noteIds = notes.map(n => n.id);
-		const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
-		const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
-		const targets = [...noteIds, ...renoteIds];
-		const myReactions = await NoteReactions.find({
-			userId: meId,
-			noteId: In(targets),
-		});
-
-		for (const target of targets) {
-			myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
-		}
-
-		await prefetchEmojis(aggregateNoteEmojis(notes));
-
-		return await Promise.all(notifications.map(x => this.pack(x, {
-			_hintForEachNotes_: {
-				myReactions: myReactionsMap
-			}
-		})));
-	}
-}
-
-export const packedNotificationSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		isRead: {
-			type: 'boolean' as const,
-			optional: false as const, nullable: false as const,
-		},
-		type: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			enum: [...notificationTypes],
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: true as const, nullable: true as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-			format: 'id',
-		},
-		note: {
-			type: 'object' as const,
-			ref: 'Note' as const,
-			optional: true as const, nullable: true as const,
-		},
-		reaction: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-		choice: {
-			type: 'number' as const,
-			optional: true as const, nullable: true as const,
-		},
-		invitation: {
-			type: 'object' as const,
-			optional: true as const, nullable: true as const,
-		},
-		body: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-		header: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-		icon: {
-			type: 'string' as const,
-			optional: true as const, nullable: true as const,
-		},
-	}
-};
diff --git a/src/models/repositories/page-like.ts b/src/models/repositories/page-like.ts
deleted file mode 100644
index 28f34254d9..0000000000
--- a/src/models/repositories/page-like.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { PageLike } from '@/models/entities/page-like';
-import { Pages } from '../index';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(PageLike)
-export class PageLikeRepository extends Repository<PageLike> {
-	public async pack(
-		src: PageLike['id'] | PageLike,
-		me?: { id: User['id'] } | null | undefined
-	) {
-		const like = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: like.id,
-			page: await Pages.pack(like.page || like.pageId, me),
-		};
-	}
-
-	public packMany(
-		likes: any[],
-		me: { id: User['id'] }
-	) {
-		return Promise.all(likes.map(x => this.pack(x, me)));
-	}
-}
diff --git a/src/models/repositories/page.ts b/src/models/repositories/page.ts
deleted file mode 100644
index 3a3642d7ec..0000000000
--- a/src/models/repositories/page.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Page } from '@/models/entities/page';
-import { Packed } from '@/misc/schema';
-import { Users, DriveFiles, PageLikes } from '../index';
-import { awaitAll } from '@/prelude/await-all';
-import { DriveFile } from '@/models/entities/drive-file';
-import { User } from '@/models/entities/user';
-
-@EntityRepository(Page)
-export class PageRepository extends Repository<Page> {
-	public async pack(
-		src: Page['id'] | Page,
-		me?: { id: User['id'] } | null | undefined,
-	): Promise<Packed<'Page'>> {
-		const meId = me ? me.id : null;
-		const page = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		const attachedFiles: Promise<DriveFile | undefined>[] = [];
-		const collectFile = (xs: any[]) => {
-			for (const x of xs) {
-				if (x.type === 'image') {
-					attachedFiles.push(DriveFiles.findOne({
-						id: x.fileId,
-						userId: page.userId
-					}));
-				}
-				if (x.children) {
-					collectFile(x.children);
-				}
-			}
-		};
-		collectFile(page.content);
-
-		// 後方互換性のため
-		let migrated = false;
-		const migrate = (xs: any[]) => {
-			for (const x of xs) {
-				if (x.type === 'input') {
-					if (x.inputType === 'text') {
-						x.type = 'textInput';
-					}
-					if (x.inputType === 'number') {
-						x.type = 'numberInput';
-						if (x.default) x.default = parseInt(x.default, 10);
-					}
-					migrated = true;
-				}
-				if (x.children) {
-					migrate(x.children);
-				}
-			}
-		};
-		migrate(page.content);
-		if (migrated) {
-			this.update(page.id, {
-				content: page.content
-			});
-		}
-
-		return await awaitAll({
-			id: page.id,
-			createdAt: page.createdAt.toISOString(),
-			updatedAt: page.updatedAt.toISOString(),
-			userId: page.userId,
-			user: Users.pack(page.user || page.userId, me), // { detail: true } すると無限ループするので注意
-			content: page.content,
-			variables: page.variables,
-			title: page.title,
-			name: page.name,
-			summary: page.summary,
-			hideTitleWhenPinned: page.hideTitleWhenPinned,
-			alignCenter: page.alignCenter,
-			font: page.font,
-			script: page.script,
-			eyeCatchingImageId: page.eyeCatchingImageId,
-			eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
-			attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
-			likedCount: page.likedCount,
-			isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
-		});
-	}
-
-	public packMany(
-		pages: Page[],
-		me?: { id: User['id'] } | null | undefined,
-	) {
-		return Promise.all(pages.map(x => this.pack(x, me)));
-	}
-}
-
-export const packedPageSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		updatedAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		title: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		summary: {
-			type: 'string' as const,
-			optional: false as const, nullable: true as const,
-		},
-		content: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-		},
-		variables: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-		},
-		userId: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-		},
-		user: {
-			type: 'object' as const,
-			ref: 'User' as const,
-			optional: false as const, nullable: false as const,
-		},
-	}
-};
diff --git a/src/models/repositories/queue.ts b/src/models/repositories/queue.ts
deleted file mode 100644
index 161751ddc8..0000000000
--- a/src/models/repositories/queue.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-export const packedQueueCountSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		waiting: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		active: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		completed: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		failed: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		delayed: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		},
-		paused: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const
-		}
-	}
-};
diff --git a/src/models/repositories/relay.ts b/src/models/repositories/relay.ts
deleted file mode 100644
index 72ead899f1..0000000000
--- a/src/models/repositories/relay.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Relay } from '@/models/entities/relay';
-
-@EntityRepository(Relay)
-export class RelayRepository extends Repository<Relay> {
-}
diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts
deleted file mode 100644
index f375f9b5c0..0000000000
--- a/src/models/repositories/signin.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Signin } from '@/models/entities/signin';
-
-@EntityRepository(Signin)
-export class SigninRepository extends Repository<Signin> {
-	public async pack(
-		src: Signin,
-	) {
-		return src;
-	}
-}
diff --git a/src/models/repositories/user-group-invitation.ts b/src/models/repositories/user-group-invitation.ts
deleted file mode 100644
index 638603d6ea..0000000000
--- a/src/models/repositories/user-group-invitation.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { UserGroupInvitation } from '@/models/entities/user-group-invitation';
-import { UserGroups } from '../index';
-
-@EntityRepository(UserGroupInvitation)
-export class UserGroupInvitationRepository extends Repository<UserGroupInvitation> {
-	public async pack(
-		src: UserGroupInvitation['id'] | UserGroupInvitation,
-	) {
-		const invitation = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		return {
-			id: invitation.id,
-			group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId),
-		};
-	}
-
-	public packMany(
-		invitations: any[],
-	) {
-		return Promise.all(invitations.map(x => this.pack(x)));
-	}
-}
diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts
deleted file mode 100644
index b38a2fb50d..0000000000
--- a/src/models/repositories/user-group.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { UserGroup } from '@/models/entities/user-group';
-import { UserGroupJoinings } from '../index';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(UserGroup)
-export class UserGroupRepository extends Repository<UserGroup> {
-	public async pack(
-		src: UserGroup['id'] | UserGroup,
-	): Promise<Packed<'UserGroup'>> {
-		const userGroup = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		const users = await UserGroupJoinings.find({
-			userGroupId: userGroup.id
-		});
-
-		return {
-			id: userGroup.id,
-			createdAt: userGroup.createdAt.toISOString(),
-			name: userGroup.name,
-			ownerId: userGroup.userId,
-			userIds: users.map(x => x.userId)
-		};
-	}
-}
-
-export const packedUserGroupSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		ownerId: {
-			type: 'string' as const,
-			nullable: false as const, optional: false as const,
-			format: 'id',
-		},
-		userIds: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				format: 'id',
-			}
-		},
-	},
-};
diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts
deleted file mode 100644
index 331c278e6f..0000000000
--- a/src/models/repositories/user-list.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { UserList } from '@/models/entities/user-list';
-import { UserListJoinings } from '../index';
-import { Packed } from '@/misc/schema';
-
-@EntityRepository(UserList)
-export class UserListRepository extends Repository<UserList> {
-	public async pack(
-		src: UserList['id'] | UserList,
-	): Promise<Packed<'UserList'>> {
-		const userList = typeof src === 'object' ? src : await this.findOneOrFail(src);
-
-		const users = await UserListJoinings.find({
-			userListId: userList.id
-		});
-
-		return {
-			id: userList.id,
-			createdAt: userList.createdAt.toISOString(),
-			name: userList.name,
-			userIds: users.map(x => x.userId)
-		};
-	}
-}
-
-export const packedUserListSchema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		createdAt: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-			format: 'date-time',
-		},
-		name: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		},
-		userIds: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				format: 'id',
-			}
-		},
-	},
-};
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
deleted file mode 100644
index fc0860970c..0000000000
--- a/src/models/repositories/user.ts
+++ /dev/null
@@ -1,659 +0,0 @@
-import $ from 'cafy';
-import { EntityRepository, Repository, In, Not } from 'typeorm';
-import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
-import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '../index';
-import config from '@/config/index';
-import { Packed } from '@/misc/schema';
-import { awaitAll } from '@/prelude/await-all';
-import { populateEmojis } from '@/misc/populate-emojis';
-import { getAntennas } from '@/misc/antenna-cache';
-import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const';
-
-@EntityRepository(User)
-export class UserRepository extends Repository<User> {
-	public async getRelation(me: User['id'], target: User['id']) {
-		const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
-			Followings.findOne({
-				followerId: me,
-				followeeId: target
-			}),
-			Followings.findOne({
-				followerId: target,
-				followeeId: me
-			}),
-			FollowRequests.findOne({
-				followerId: me,
-				followeeId: target
-			}),
-			FollowRequests.findOne({
-				followerId: target,
-				followeeId: me
-			}),
-			Blockings.findOne({
-				blockerId: me,
-				blockeeId: target
-			}),
-			Blockings.findOne({
-				blockerId: target,
-				blockeeId: me
-			}),
-			Mutings.findOne({
-				muterId: me,
-				muteeId: target
-			})
-		]);
-
-		return {
-			id: target,
-			isFollowing: following1 != null,
-			hasPendingFollowRequestFromYou: followReq1 != null,
-			hasPendingFollowRequestToYou: followReq2 != null,
-			isFollowed: following2 != null,
-			isBlocking: toBlocking != null,
-			isBlocked: fromBlocked != null,
-			isMuted: mute != null
-		};
-	}
-
-	public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
-		const mute = await Mutings.find({
-			muterId: userId
-		});
-
-		const joinings = await UserGroupJoinings.find({ userId: userId });
-
-		const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
-			.where(`message.groupId = :groupId`, { groupId: j.userGroupId })
-			.andWhere('message.userId != :userId', { userId: userId })
-			.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
-			.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
-			.getOne().then(x => x != null)));
-
-		const [withUser, withGroups] = await Promise.all([
-			MessagingMessages.count({
-				where: {
-					recipientId: userId,
-					isRead: false,
-					...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}),
-				},
-				take: 1
-			}).then(count => count > 0),
-			groupQs
-		]);
-
-		return withUser || withGroups.some(x => x);
-	}
-
-	public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
-		const reads = await AnnouncementReads.find({
-			userId: userId
-		});
-
-		const count = await Announcements.count(reads.length > 0 ? {
-			id: Not(In(reads.map(read => read.announcementId)))
-		} : {});
-
-		return count > 0;
-	}
-
-	public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
-		const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
-
-		const unread = myAntennas.length > 0 ? await AntennaNotes.findOne({
-			antennaId: In(myAntennas.map(x => x.id)),
-			read: false
-		}) : null;
-
-		return unread != null;
-	}
-
-	public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
-		const channels = await ChannelFollowings.find({ followerId: userId });
-
-		const unread = channels.length > 0 ? await NoteUnreads.findOne({
-			userId: userId,
-			noteChannelId: In(channels.map(x => x.followeeId)),
-		}) : null;
-
-		return unread != null;
-	}
-
-	public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
-		const mute = await Mutings.find({
-			muterId: userId
-		});
-		const mutedUserIds = mute.map(m => m.muteeId);
-
-		const count = await Notifications.count({
-			where: {
-				notifieeId: userId,
-				...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
-				isRead: false
-			},
-			take: 1
-		});
-
-		return count > 0;
-	}
-
-	public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
-		const count = await FollowRequests.count({
-			followeeId: userId
-		});
-
-		return count > 0;
-	}
-
-	public getOnlineStatus(user: User): string {
-		if (user.hideOnlineStatus) return 'unknown';
-		if (user.lastActiveDate == null) return 'unknown';
-		const elapsed = Date.now() - user.lastActiveDate.getTime();
-		return (
-			elapsed < USER_ONLINE_THRESHOLD ? 'online' :
-			elapsed < USER_ACTIVE_THRESHOLD ? 'active' :
-			'offline'
-		);
-	}
-
-	public getAvatarUrl(user: User): string {
-		if (user.avatarUrl) {
-			return user.avatarUrl;
-		} else {
-			return `${config.url}/random-avatar/${user.id}`;
-		}
-	}
-
-	public async pack(
-		src: User['id'] | User,
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			detail?: boolean,
-			includeSecrets?: boolean,
-		}
-	): Promise<Packed<'User'>> {
-		const opts = Object.assign({
-			detail: false,
-			includeSecrets: false
-		}, options);
-
-		const user = typeof src === 'object' ? src : await this.findOneOrFail(src);
-		const meId = me ? me.id : null;
-
-		const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
-		const pins = opts.detail ? await UserNotePinings.createQueryBuilder('pin')
-			.where('pin.userId = :userId', { userId: user.id })
-			.innerJoinAndSelect('pin.note', 'note')
-			.orderBy('pin.id', 'DESC')
-			.getMany() : [];
-		const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null;
-
-		const followingCount = profile == null ? null :
-			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount :
-			(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followingCount :
-			null;
-
-		const followersCount = profile == null ? null :
-			(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount :
-			(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followersCount :
-			null;
-
-		const falsy = opts.detail ? false : undefined;
-
-		const packed = {
-			id: user.id,
-			name: user.name,
-			username: user.username,
-			host: user.host,
-			avatarUrl: this.getAvatarUrl(user),
-			avatarBlurhash: user.avatarBlurhash,
-			avatarColor: null, // 後方互換性のため
-			isAdmin: user.isAdmin || falsy,
-			isModerator: user.isModerator || falsy,
-			isBot: user.isBot || falsy,
-			isCat: user.isCat || falsy,
-			instance: user.host ? Instances.findOne({ host: user.host }).then(instance => instance ? {
-				name: instance.name,
-				softwareName: instance.softwareName,
-				softwareVersion: instance.softwareVersion,
-				iconUrl: instance.iconUrl,
-				faviconUrl: instance.faviconUrl,
-				themeColor: instance.themeColor,
-			} : undefined) : undefined,
-			emojis: populateEmojis(user.emojis, user.host),
-			onlineStatus: this.getOnlineStatus(user),
-
-			...(opts.detail ? {
-				url: profile!.url,
-				uri: user.uri,
-				createdAt: user.createdAt.toISOString(),
-				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
-				lastFetchedAt: user.lastFetchedAt?.toISOString(),
-				bannerUrl: user.bannerUrl,
-				bannerBlurhash: user.bannerBlurhash,
-				bannerColor: null, // 後方互換性のため
-				isLocked: user.isLocked,
-				isModerator: user.isModerator || falsy,
-				isSilenced: user.isSilenced || falsy,
-				isSuspended: user.isSuspended || falsy,
-				description: profile!.description,
-				location: profile!.location,
-				birthday: profile!.birthday,
-				lang: profile!.lang,
-				fields: profile!.fields,
-				followersCount: followersCount || 0,
-				followingCount: followingCount || 0,
-				notesCount: user.notesCount,
-				pinnedNoteIds: pins.map(pin => pin.noteId),
-				pinnedNotes: Notes.packMany(pins.map(pin => pin.note!), me, {
-					detail: true
-				}),
-				pinnedPageId: profile!.pinnedPageId,
-				pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, me) : null,
-				publicReactions: profile!.publicReactions,
-				ffVisibility: profile!.ffVisibility,
-				twoFactorEnabled: profile!.twoFactorEnabled,
-				usePasswordLessLogin: profile!.usePasswordLessLogin,
-				securityKeys: profile!.twoFactorEnabled
-					? UserSecurityKeys.count({
-						userId: user.id
-					}).then(result => result >= 1)
-					: false,
-			} : {}),
-
-			...(opts.detail && meId === user.id ? {
-				avatarId: user.avatarId,
-				bannerId: user.bannerId,
-				injectFeaturedNote: profile!.injectFeaturedNote,
-				receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
-				alwaysMarkNsfw: profile!.alwaysMarkNsfw,
-				carefulBot: profile!.carefulBot,
-				autoAcceptFollowed: profile!.autoAcceptFollowed,
-				noCrawle: profile!.noCrawle,
-				isExplorable: user.isExplorable,
-				isDeleted: user.isDeleted,
-				hideOnlineStatus: user.hideOnlineStatus,
-				hasUnreadSpecifiedNotes: NoteUnreads.count({
-					where: { userId: user.id, isSpecified: true },
-					take: 1
-				}).then(count => count > 0),
-				hasUnreadMentions: NoteUnreads.count({
-					where: { userId: user.id, isMentioned: true },
-					take: 1
-				}).then(count => count > 0),
-				hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
-				hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
-				hasUnreadChannel: this.getHasUnreadChannel(user.id),
-				hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
-				hasUnreadNotification: this.getHasUnreadNotification(user.id),
-				hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
-				integrations: profile!.integrations,
-				mutedWords: profile!.mutedWords,
-				mutingNotificationTypes: profile!.mutingNotificationTypes,
-				emailNotificationTypes: profile!.emailNotificationTypes,
-			} : {}),
-
-			...(opts.includeSecrets ? {
-				email: profile!.email,
-				emailVerified: profile!.emailVerified,
-				securityKeysList: profile!.twoFactorEnabled
-					? UserSecurityKeys.find({
-						where: {
-							userId: user.id
-						},
-						select: ['id', 'name', 'lastUsed']
-					})
-					: []
-			} : {}),
-
-			...(relation ? {
-				isFollowing: relation.isFollowing,
-				isFollowed: relation.isFollowed,
-				hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
-				hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
-				isBlocking: relation.isBlocking,
-				isBlocked: relation.isBlocked,
-				isMuted: relation.isMuted,
-			} : {})
-		};
-
-		return await awaitAll(packed);
-	}
-
-	public packMany(
-		users: (User['id'] | User)[],
-		me?: { id: User['id'] } | null | undefined,
-		options?: {
-			detail?: boolean,
-			includeSecrets?: boolean,
-		}
-	) {
-		return Promise.all(users.map(u => this.pack(u, me, options)));
-	}
-
-	public isLocalUser(user: User): user is ILocalUser;
-	public isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
-	public isLocalUser(user: User | { host: User['host'] }): boolean {
-		return user.host == null;
-	}
-
-	public isRemoteUser(user: User): user is IRemoteUser;
-	public isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
-	public isRemoteUser(user: User | { host: User['host'] }): boolean {
-		return !this.isLocalUser(user);
-	}
-
-	//#region Validators
-	public validateLocalUsername = $.str.match(/^\w{1,20}$/);
-	public validatePassword = $.str.min(1);
-	public validateName = $.str.min(1).max(50);
-	public validateDescription = $.str.min(1).max(500);
-	public validateLocation = $.str.min(1).max(50);
-	public validateBirthday = $.str.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/);
-	//#endregion
-}
-
-export const packedUserSchema = {
-	type: 'object' as const,
-	nullable: false as const, optional: false as const,
-	properties: {
-		id: {
-			type: 'string' as const,
-			nullable: false as const, optional: false as const,
-			format: 'id',
-			example: 'xxxxxxxxxx',
-		},
-		name: {
-			type: 'string' as const,
-			nullable: true as const, optional: false as const,
-			example: '藍'
-		},
-		username: {
-			type: 'string' as const,
-			nullable: false as const, optional: false as const,
-			example: 'ai'
-		},
-		host: {
-			type: 'string' as const,
-			nullable: true as const, optional: false as const,
-			example: 'misskey.example.com'
-		},
-		avatarUrl: {
-			type: 'string' as const,
-			format: 'url',
-			nullable: true as const, optional: false as const,
-		},
-		avatarBlurhash: {
-			type: 'any' as const,
-			nullable: true as const, optional: false as const,
-		},
-		avatarColor: {
-			type: 'any' as const,
-			nullable: true as const, optional: false as const,
-			default: null
-		},
-		isAdmin: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false
-		},
-		isModerator: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false
-		},
-		isBot: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		isCat: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		emojis: {
-			type: 'array' as const,
-			nullable: false as const, optional: false as const,
-			items: {
-				type: 'object' as const,
-				nullable: false as const, optional: false as const,
-				properties: {
-					name: {
-						type: 'string' as const,
-						nullable: false as const, optional: false as const
-					},
-					url: {
-						type: 'string' as const,
-						nullable: false as const, optional: false as const,
-						format: 'url'
-					},
-				}
-			}
-		},
-		url: {
-			type: 'string' as const,
-			format: 'url',
-			nullable: true as const, optional: true as const,
-		},
-		createdAt: {
-			type: 'string' as const,
-			nullable: false as const, optional: true as const,
-			format: 'date-time',
-		},
-		updatedAt: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			format: 'date-time',
-		},
-		bannerUrl: {
-			type: 'string' as const,
-			format: 'url',
-			nullable: true as const, optional: true as const,
-		},
-		bannerBlurhash: {
-			type: 'any' as const,
-			nullable: true as const, optional: true as const,
-		},
-		bannerColor: {
-			type: 'any' as const,
-			nullable: true as const, optional: true as const,
-			default: null
-		},
-		isLocked: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		isSuspended: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			example: false
-		},
-		description: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			example: 'Hi masters, I am Ai!'
-		},
-		location: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-		},
-		birthday: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			example: '2018-03-12'
-		},
-		fields: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'object' as const,
-				nullable: false as const, optional: false as const,
-				properties: {
-					name: {
-						type: 'string' as const,
-						nullable: false as const, optional: false as const
-					},
-					value: {
-						type: 'string' as const,
-						nullable: false as const, optional: false as const
-					}
-				},
-				maxLength: 4
-			}
-		},
-		followersCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: true as const,
-		},
-		followingCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: true as const,
-		},
-		notesCount: {
-			type: 'number' as const,
-			nullable: false as const, optional: true as const,
-		},
-		pinnedNoteIds: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				format: 'id',
-			}
-		},
-		pinnedNotes: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const,
-			items: {
-				type: 'object' as const,
-				nullable: false as const, optional: false as const,
-				ref: 'Note' as const,
-			}
-		},
-		pinnedPageId: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const
-		},
-		pinnedPage: {
-			type: 'object' as const,
-			nullable: true as const, optional: true as const,
-			ref: 'Page' as const,
-		},
-		twoFactorEnabled: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false
-		},
-		usePasswordLessLogin: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false
-		},
-		securityKeys: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-			default: false
-		},
-		avatarId: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			format: 'id'
-		},
-		bannerId: {
-			type: 'string' as const,
-			nullable: true as const, optional: true as const,
-			format: 'id'
-		},
-		autoWatch: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const
-		},
-		injectFeaturedNote: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const
-		},
-		alwaysMarkNsfw: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const
-		},
-		carefulBot: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const
-		},
-		autoAcceptFollowed: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const
-		},
-		hasUnreadSpecifiedNotes: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadMentions: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadAnnouncement: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadAntenna: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadChannel: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadMessagingMessage: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasUnreadNotification: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		hasPendingReceivedFollowRequest: {
-			type: 'boolean' as const,
-			nullable: false as const, optional: true as const,
-		},
-		integrations: {
-			type: 'object' as const,
-			nullable: false as const, optional: true as const
-		},
-		mutedWords: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const
-		},
-		mutingNotificationTypes: {
-			type: 'array' as const,
-			nullable: false as const, optional: true as const
-		},
-		isFollowing: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const
-		},
-		hasPendingFollowRequestFromYou: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const
-		},
-		hasPendingFollowRequestToYou: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const
-		},
-		isFollowed: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const
-		},
-		isBlocking: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const
-		},
-		isBlocked: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const
-		},
-		isMuted: {
-			type: 'boolean' as const,
-			optional: true as const, nullable: false as const
-		}
-	},
-};
diff --git a/src/prelude/README.md b/src/prelude/README.md
deleted file mode 100644
index bb728cfb1b..0000000000
--- a/src/prelude/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Prelude
-このディレクトリのコードはJavaScriptの表現能力を補うためのコードです。
-Misskey固有の処理とは独立したコードの集まりですが、Misskeyのコードを読みやすくすることを目的としています。
diff --git a/src/prelude/array.ts b/src/prelude/array.ts
deleted file mode 100644
index d63f0475d0..0000000000
--- a/src/prelude/array.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { EndoRelation, Predicate } from './relation';
-
-/**
- * Count the number of elements that satisfy the predicate
- */
-
-export function countIf<T>(f: Predicate<T>, xs: T[]): number {
-	return xs.filter(f).length;
-}
-
-/**
- * Count the number of elements that is equal to the element
- */
-export function count<T>(a: T, xs: T[]): number {
-	return countIf(x => x === a, xs);
-}
-
-/**
- * Concatenate an array of arrays
- */
-export function concat<T>(xss: T[][]): T[] {
-	return ([] as T[]).concat(...xss);
-}
-
-/**
- * Intersperse the element between the elements of the array
- * @param sep The element to be interspersed
- */
-export function intersperse<T>(sep: T, xs: T[]): T[] {
-	return concat(xs.map(x => [sep, x])).slice(1);
-}
-
-/**
- * Returns the array of elements that is not equal to the element
- */
-export function erase<T>(a: T, xs: T[]): T[] {
-	return xs.filter(x => x !== a);
-}
-
-/**
- * Finds the array of all elements in the first array not contained in the second array.
- * The order of result values are determined by the first array.
- */
-export function difference<T>(xs: T[], ys: T[]): T[] {
-	return xs.filter(x => !ys.includes(x));
-}
-
-/**
- * Remove all but the first element from every group of equivalent elements
- */
-export function unique<T>(xs: T[]): T[] {
-	return [...new Set(xs)];
-}
-
-export function sum(xs: number[]): number {
-	return xs.reduce((a, b) => a + b, 0);
-}
-
-export function maximum(xs: number[]): number {
-	return Math.max(...xs);
-}
-
-/**
- * Splits an array based on the equivalence relation.
- * The concatenation of the result is equal to the argument.
- */
-export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
-	const groups = [] as T[][];
-	for (const x of xs) {
-		if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
-			groups[groups.length - 1].push(x);
-		} else {
-			groups.push([x]);
-		}
-	}
-	return groups;
-}
-
-/**
- * Splits an array based on the equivalence relation induced by the function.
- * The concatenation of the result is equal to the argument.
- */
-export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
-	return groupBy((a, b) => f(a) === f(b), xs);
-}
-
-export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
-	return collections.reduce((obj: Record<string, T[]>, item: T) => {
-		const key = keySelector(item);
-		if (!obj.hasOwnProperty(key)) {
-			obj[key] = [];
-		}
-
-		obj[key].push(item);
-
-		return obj;
-	}, {});
-}
-
-/**
- * Compare two arrays by lexicographical order
- */
-export function lessThan(xs: number[], ys: number[]): boolean {
-	for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
-		if (xs[i] < ys[i]) return true;
-		if (xs[i] > ys[i]) return false;
-	}
-	return xs.length < ys.length;
-}
-
-/**
- * Returns the longest prefix of elements that satisfy the predicate
- */
-export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
-	const ys = [];
-	for (const x of xs) {
-		if (f(x)) {
-			ys.push(x);
-		} else {
-			break;
-		}
-	}
-	return ys;
-}
-
-export function cumulativeSum(xs: number[]): number[] {
-	const ys = Array.from(xs); // deep copy
-	for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
-	return ys;
-}
-
-export function toArray<T>(x: T | T[] | undefined): T[] {
-	return Array.isArray(x) ? x : x != null ? [x] : [];
-}
-
-export function toSingle<T>(x: T | T[] | undefined): T | undefined {
-	return Array.isArray(x) ? x[0] : x;
-}
diff --git a/src/prelude/await-all.ts b/src/prelude/await-all.ts
deleted file mode 100644
index 24795f3ae5..0000000000
--- a/src/prelude/await-all.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-type Await<T> = T extends Promise<infer U> ? U : T;
-
-type AwaitAll<T> = {
-	[P in keyof T]: Await<T[P]>;
-};
-
-export async function awaitAll<T>(obj: T): Promise<AwaitAll<T>> {
-	const target = {} as any;
-	const keys = Object.keys(obj);
-	const values = Object.values(obj);
-
-	const resolvedValues = await Promise.all(values.map(value =>
-		(!value || !value.constructor || value.constructor.name !== 'Object')
-			? value
-			: awaitAll(value)
-	));
-
-	for (let i = 0; i < keys.length; i++) {
-		target[keys[i]] = resolvedValues[i];
-	}
-
-	return target;
-}
diff --git a/src/prelude/math.ts b/src/prelude/math.ts
deleted file mode 100644
index 07b94bec30..0000000000
--- a/src/prelude/math.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function gcd(a: number, b: number): number {
-	return b === 0 ? a : gcd(b, a % b);
-}
diff --git a/src/prelude/maybe.ts b/src/prelude/maybe.ts
deleted file mode 100644
index 0b4b543ca5..0000000000
--- a/src/prelude/maybe.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-export interface IMaybe<T> {
-	isJust(): this is IJust<T>;
-}
-
-export interface IJust<T> extends IMaybe<T> {
-	get(): T;
-}
-
-export function just<T>(value: T): IJust<T> {
-	return {
-		isJust: () => true,
-		get: () => value
-	};
-}
-
-export function nothing<T>(): IMaybe<T> {
-	return {
-		isJust: () => false,
-	};
-}
diff --git a/src/prelude/relation.ts b/src/prelude/relation.ts
deleted file mode 100644
index 1f4703f52f..0000000000
--- a/src/prelude/relation.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export type Predicate<T> = (a: T) => boolean;
-
-export type Relation<T, U> = (a: T, b: U) => boolean;
-
-export type EndoRelation<T> = Relation<T, T>;
diff --git a/src/prelude/string.ts b/src/prelude/string.ts
deleted file mode 100644
index b907e0a2e1..0000000000
--- a/src/prelude/string.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export function concat(xs: string[]): string {
-	return xs.join('');
-}
-
-export function capitalize(s: string): string {
-	return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
-}
-
-export function toUpperCase(s: string): string {
-	return s.toUpperCase();
-}
-
-export function toLowerCase(s: string): string {
-	return s.toLowerCase();
-}
diff --git a/src/prelude/symbol.ts b/src/prelude/symbol.ts
deleted file mode 100644
index 51e12f7450..0000000000
--- a/src/prelude/symbol.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const fallback = Symbol('fallback');
diff --git a/src/prelude/time.ts b/src/prelude/time.ts
deleted file mode 100644
index 34e8b6b17c..0000000000
--- a/src/prelude/time.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-const dateTimeIntervals = {
-	'day': 86400000,
-	'hour': 3600000,
-	'ms': 1,
-};
-
-export function dateUTC(time: number[]): Date {
-	const d = time.length === 2 ? Date.UTC(time[0], time[1])
-					: time.length === 3 ? Date.UTC(time[0], time[1], time[2])
-					: time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3])
-					: time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4])
-					: time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5])
-					: time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6])
-					: null;
-
-	if (!d) throw 'wrong number of arguments';
-
-	return new Date(d);
-}
-
-export function isTimeSame(a: Date, b: Date): boolean {
-	return a.getTime() === b.getTime();
-}
-
-export function isTimeBefore(a: Date, b: Date): boolean {
-	return (a.getTime() - b.getTime()) < 0;
-}
-
-export function isTimeAfter(a: Date, b: Date): boolean {
-	return (a.getTime() - b.getTime()) > 0;
-}
-
-export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
-	return new Date(x.getTime() + (value * dateTimeIntervals[span]));
-}
-
-export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date {
-	return new Date(x.getTime() - (value * dateTimeIntervals[span]));
-}
diff --git a/src/prelude/url.ts b/src/prelude/url.ts
deleted file mode 100644
index c7f2b7c1e7..0000000000
--- a/src/prelude/url.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export function query(obj: {}): string {
-	const params = Object.entries(obj)
-		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
-		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
-
-	return Object.entries(params)
-		.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
-		.join('&');
-}
-
-export function appendQuery(url: string, query: string): string {
-	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
-}
diff --git a/src/prelude/xml.ts b/src/prelude/xml.ts
deleted file mode 100644
index 0773f75d47..0000000000
--- a/src/prelude/xml.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-const map: Record<string, string> = {
-	'&': '&amp;',
-	'<': '&lt;',
-	'>': '&gt;',
-	'"': '&quot;',
-	'\'': '&apos;'
-};
-
-const beginingOfCDATA = '<![CDATA[';
-const endOfCDATA = ']]>';
-
-export function escapeValue(x: string): string {
-	let insideOfCDATA = false;
-	let builder = '';
-	for (
-		let i = 0;
-		i < x.length;
-	) {
-		if (insideOfCDATA) {
-			if (x.slice(i, i + beginingOfCDATA.length) === beginingOfCDATA) {
-				insideOfCDATA = true;
-				i += beginingOfCDATA.length;
-			} else {
-				builder += x[i++];
-			}
-		} else {
-			if (x.slice(i, i + endOfCDATA.length) === endOfCDATA) {
-				insideOfCDATA = false;
-				i += endOfCDATA.length;
-			} else {
-				const b = x[i++];
-				builder += map[b] || b;
-			}
-		}
-	}
-	return builder;
-}
-
-export function escapeAttribute(x: string): string {
-	return Object.entries(map).reduce((a, [k, v]) => a.replace(k, v), x);
-}
diff --git a/src/queue/get-job-info.ts b/src/queue/get-job-info.ts
deleted file mode 100644
index f601ae62d0..0000000000
--- a/src/queue/get-job-info.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as Bull from 'bull';
-
-export function getJobInfo(job: Bull.Job, increment = false) {
-	const age = Date.now() - job.timestamp;
-
-	const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m`
-		: age > 10000 ? `${Math.floor(age / 1000)}s`
-		: `${age}ms`;
-
-	// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
-	const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
-	const maxAttempts = job.opts ? job.opts.attempts : 0;
-
-	return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
-}
diff --git a/src/queue/index.ts b/src/queue/index.ts
deleted file mode 100644
index 37eb809604..0000000000
--- a/src/queue/index.ts
+++ /dev/null
@@ -1,255 +0,0 @@
-import * as httpSignature from 'http-signature';
-
-import config from '@/config/index';
-import { envOption } from '../env';
-
-import processDeliver from './processors/deliver';
-import processInbox from './processors/inbox';
-import processDb from './processors/db/index';
-import procesObjectStorage from './processors/object-storage/index';
-import { queueLogger } from './logger';
-import { DriveFile } from '@/models/entities/drive-file';
-import { getJobInfo } from './get-job-info';
-import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
-import { ThinUser } from './types';
-import { IActivity } from '@/remote/activitypub/type';
-
-function renderError(e: Error): any {
-	return {
-		stack: e?.stack,
-		message: e?.message,
-		name: e?.name
-	};
-}
-
-const systemLogger = queueLogger.createSubLogger('system');
-const deliverLogger = queueLogger.createSubLogger('deliver');
-const inboxLogger = queueLogger.createSubLogger('inbox');
-const dbLogger = queueLogger.createSubLogger('db');
-const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
-
-systemQueue
-	.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
-	.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
-	.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
-	.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
-	.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
-	.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
-
-deliverQueue
-	.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
-	.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
-	.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
-	.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
-	.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) }))
-	.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
-
-inboxQueue
-	.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
-	.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
-	.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
-	.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) }))
-	.on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) }))
-	.on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`));
-
-dbQueue
-	.on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`))
-	.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
-	.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
-	.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
-	.on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) }))
-	.on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`));
-
-objectStorageQueue
-	.on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`))
-	.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
-	.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
-	.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
-	.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
-	.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
-
-export function deliver(user: ThinUser, content: unknown, to: string | null) {
-	if (content == null) return null;
-	if (to == null) return null;
-
-	const data = {
-		user: {
-			id: user.id
-		},
-		content,
-		to
-	};
-
-	return deliverQueue.add(data, {
-		attempts: config.deliverJobMaxAttempts || 12,
-		timeout: 1 * 60 * 1000,	// 1min
-		backoff: {
-			type: 'apBackoff'
-		},
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
-	const data = {
-		activity: activity,
-		signature
-	};
-
-	return inboxQueue.add(data, {
-		attempts: config.inboxJobMaxAttempts || 8,
-		timeout: 5 * 60 * 1000,	// 5min
-		backoff: {
-			type: 'apBackoff'
-		},
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createDeleteDriveFilesJob(user: ThinUser) {
-	return dbQueue.add('deleteDriveFiles', {
-		user: user
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createExportNotesJob(user: ThinUser) {
-	return dbQueue.add('exportNotes', {
-		user: user
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createExportFollowingJob(user: ThinUser) {
-	return dbQueue.add('exportFollowing', {
-		user: user
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createExportMuteJob(user: ThinUser) {
-	return dbQueue.add('exportMute', {
-		user: user
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createExportBlockingJob(user: ThinUser) {
-	return dbQueue.add('exportBlocking', {
-		user: user
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createExportUserListsJob(user: ThinUser) {
-	return dbQueue.add('exportUserLists', {
-		user: user
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) {
-	return dbQueue.add('importFollowing', {
-		user: user,
-		fileId: fileId
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) {
-	return dbQueue.add('importMuting', {
-		user: user,
-		fileId: fileId
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) {
-	return dbQueue.add('importBlocking', {
-		user: user,
-		fileId: fileId
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
-	return dbQueue.add('importUserLists', {
-		user: user,
-		fileId: fileId
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
-	return dbQueue.add('deleteAccount', {
-		user: user,
-		soft: opts.soft
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createDeleteObjectStorageFileJob(key: string) {
-	return objectStorageQueue.add('deleteFile', {
-		key: key
-	}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export function createCleanRemoteFilesJob() {
-	return objectStorageQueue.add('cleanRemoteFiles', {}, {
-		removeOnComplete: true,
-		removeOnFail: true
-	});
-}
-
-export default function() {
-	if (envOption.onlyServer) return;
-
-	deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
-	inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
-	processDb(dbQueue);
-	procesObjectStorage(objectStorageQueue);
-
-	systemQueue.add('resyncCharts', {
-	}, {
-		repeat: { cron: '0 0 * * *' }
-	});
-}
-
-export function destroy() {
-	deliverQueue.once('cleaned', (jobs, status) => {
-		deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
-	});
-	deliverQueue.clean(0, 'delayed');
-
-	inboxQueue.once('cleaned', (jobs, status) => {
-		inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
-	});
-	inboxQueue.clean(0, 'delayed');
-}
diff --git a/src/queue/initialize.ts b/src/queue/initialize.ts
deleted file mode 100644
index 31102a3ed2..0000000000
--- a/src/queue/initialize.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import * as Bull from 'bull';
-import config from '@/config/index';
-
-export function initialize<T>(name: string, limitPerSec = -1) {
-	return new Bull<T>(name, {
-		redis: {
-			port: config.redis.port,
-			host: config.redis.host,
-			password: config.redis.pass,
-			db: config.redis.db || 0,
-		},
-		prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
-		limiter: limitPerSec > 0 ? {
-			max: limitPerSec,
-			duration: 1000
-		} : undefined,
-		settings: {
-			backoffStrategies: {
-				apBackoff
-			}
-		}
-	});
-}
-
-// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019
-function apBackoff(attemptsMade: number, err: Error) {
-	const baseDelay = 60 * 1000;	// 1min
-	const maxBackoff = 8 * 60 * 60 * 1000;	// 8hours
-	let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
-	backoff = Math.min(backoff, maxBackoff);
-	backoff += Math.round(backoff * Math.random() * 0.2);
-	return backoff;
-}
diff --git a/src/queue/logger.ts b/src/queue/logger.ts
deleted file mode 100644
index f789b9d079..0000000000
--- a/src/queue/logger.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Logger from '@/services/logger';
-
-export const queueLogger = new Logger('queue', 'orange');
diff --git a/src/queue/processors/db/delete-account.ts b/src/queue/processors/db/delete-account.ts
deleted file mode 100644
index e54f38e35e..0000000000
--- a/src/queue/processors/db/delete-account.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import * as Bull from 'bull';
-import { queueLogger } from '../../logger';
-import { DriveFiles, Notes, UserProfiles, Users } from '@/models/index';
-import { DbUserDeleteJobData } from '@/queue/types';
-import { Note } from '@/models/entities/note';
-import { DriveFile } from '@/models/entities/drive-file';
-import { MoreThan } from 'typeorm';
-import { deleteFileSync } from '@/services/drive/delete-file';
-import { sendEmail } from '@/services/send-email';
-
-const logger = queueLogger.createSubLogger('delete-account');
-
-export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
-	logger.info(`Deleting account of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		return;
-	}
-
-	{ // Delete notes
-		let cursor: Note['id'] | null = null;
-
-		while (true) {
-			const notes = await Notes.find({
-				where: {
-					userId: user.id,
-					...(cursor ? { id: MoreThan(cursor) } : {})
-				},
-				take: 100,
-				order: {
-					id: 1
-				}
-			});
-
-			if (notes.length === 0) {
-				break;
-			}
-
-			cursor = notes[notes.length - 1].id;
-
-			await Notes.delete(notes.map(note => note.id));
-		}
-
-		logger.succ(`All of notes deleted`);
-	}
-
-	{ // Delete files
-		let cursor: DriveFile['id'] | null = null;
-
-		while (true) {
-			const files = await DriveFiles.find({
-				where: {
-					userId: user.id,
-					...(cursor ? { id: MoreThan(cursor) } : {})
-				},
-				take: 10,
-				order: {
-					id: 1
-				}
-			});
-
-			if (files.length === 0) {
-				break;
-			}
-
-			cursor = files[files.length - 1].id;
-
-			for (const file of files) {
-				await deleteFileSync(file);
-			}
-		}
-
-		logger.succ(`All of files deleted`);
-	}
-
-	{ // Send email notification
-		const profile = await UserProfiles.findOneOrFail(user.id);
-		if (profile.email && profile.emailVerified) {
-			sendEmail(profile.email, 'Account deleted',
-				`Your account has been deleted.`,
-				`Your account has been deleted.`);
-		}
-	}
-
-	// soft指定されている場合は物理削除しない
-	if (job.data.soft) {
-		// nop
-	} else {
-		await Users.delete(job.data.user.id);
-	}
-
-	return 'Account deleted';
-}
diff --git a/src/queue/processors/db/delete-drive-files.ts b/src/queue/processors/db/delete-drive-files.ts
deleted file mode 100644
index 8a28468b0d..0000000000
--- a/src/queue/processors/db/delete-drive-files.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as Bull from 'bull';
-
-import { queueLogger } from '../../logger';
-import { deleteFileSync } from '@/services/drive/delete-file';
-import { Users, DriveFiles } from '@/models/index';
-import { MoreThan } from 'typeorm';
-import { DbUserJobData } from '@/queue/types';
-
-const logger = queueLogger.createSubLogger('delete-drive-files');
-
-export async function deleteDriveFiles(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
-	logger.info(`Deleting drive files of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	let deletedCount = 0;
-	let cursor: any = null;
-
-	while (true) {
-		const files = await DriveFiles.find({
-			where: {
-				userId: user.id,
-				...(cursor ? { id: MoreThan(cursor) } : {})
-			},
-			take: 100,
-			order: {
-				id: 1
-			}
-		});
-
-		if (files.length === 0) {
-			job.progress(100);
-			break;
-		}
-
-		cursor = files[files.length - 1].id;
-
-		for (const file of files) {
-			await deleteFileSync(file);
-			deletedCount++;
-		}
-
-		const total = await DriveFiles.count({
-			userId: user.id,
-		});
-
-		job.progress(deletedCount / total);
-	}
-
-	logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
-	done();
-}
diff --git a/src/queue/processors/db/export-blocking.ts b/src/queue/processors/db/export-blocking.ts
deleted file mode 100644
index 8b8aa259d4..0000000000
--- a/src/queue/processors/db/export-blocking.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import * as Bull from 'bull';
-import * as tmp from 'tmp';
-import * as fs from 'fs';
-
-import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
-import * as dateFormat from 'dateformat';
-import { getFullApAccount } from '@/misc/convert-host';
-import { Users, Blockings } from '@/models/index';
-import { MoreThan } from 'typeorm';
-import { DbUserJobData } from '@/queue/types';
-
-const logger = queueLogger.createSubLogger('export-blocking');
-
-export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
-	logger.info(`Exporting blocking of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	// Create temp file
-	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
-		tmp.file((e, path, fd, cleanup) => {
-			if (e) return rej(e);
-			res([path, cleanup]);
-		});
-	});
-
-	logger.info(`Temp file is ${path}`);
-
-	const stream = fs.createWriteStream(path, { flags: 'a' });
-
-	let exportedCount = 0;
-	let cursor: any = null;
-
-	while (true) {
-		const blockings = await Blockings.find({
-			where: {
-				blockerId: user.id,
-				...(cursor ? { id: MoreThan(cursor) } : {})
-			},
-			take: 100,
-			order: {
-				id: 1
-			}
-		});
-
-		if (blockings.length === 0) {
-			job.progress(100);
-			break;
-		}
-
-		cursor = blockings[blockings.length - 1].id;
-
-		for (const block of blockings) {
-			const u = await Users.findOne({ id: block.blockeeId });
-			if (u == null) {
-				exportedCount++; continue;
-			}
-
-			const content = getFullApAccount(u.username, u.host);
-			await new Promise<void>((res, rej) => {
-				stream.write(content + '\n', err => {
-					if (err) {
-						logger.error(err);
-						rej(err);
-					} else {
-						res();
-					}
-				});
-			});
-			exportedCount++;
-		}
-
-		const total = await Blockings.count({
-			blockerId: user.id,
-		});
-
-		job.progress(exportedCount / total);
-	}
-
-	stream.end();
-	logger.succ(`Exported to: ${path}`);
-
-	const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
-
-	logger.succ(`Exported to: ${driveFile.id}`);
-	cleanup();
-	done();
-}
diff --git a/src/queue/processors/db/export-following.ts b/src/queue/processors/db/export-following.ts
deleted file mode 100644
index a0ecf5f560..0000000000
--- a/src/queue/processors/db/export-following.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import * as Bull from 'bull';
-import * as tmp from 'tmp';
-import * as fs from 'fs';
-
-import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
-import * as dateFormat from 'dateformat';
-import { getFullApAccount } from '@/misc/convert-host';
-import { Users, Followings } from '@/models/index';
-import { MoreThan } from 'typeorm';
-import { DbUserJobData } from '@/queue/types';
-
-const logger = queueLogger.createSubLogger('export-following');
-
-export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
-	logger.info(`Exporting following of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	// Create temp file
-	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
-		tmp.file((e, path, fd, cleanup) => {
-			if (e) return rej(e);
-			res([path, cleanup]);
-		});
-	});
-
-	logger.info(`Temp file is ${path}`);
-
-	const stream = fs.createWriteStream(path, { flags: 'a' });
-
-	let exportedCount = 0;
-	let cursor: any = null;
-
-	while (true) {
-		const followings = await Followings.find({
-			where: {
-				followerId: user.id,
-				...(cursor ? { id: MoreThan(cursor) } : {})
-			},
-			take: 100,
-			order: {
-				id: 1
-			}
-		});
-
-		if (followings.length === 0) {
-			job.progress(100);
-			break;
-		}
-
-		cursor = followings[followings.length - 1].id;
-
-		for (const following of followings) {
-			const u = await Users.findOne({ id: following.followeeId });
-			if (u == null) {
-				exportedCount++; continue;
-			}
-
-			const content = getFullApAccount(u.username, u.host);
-			await new Promise<void>((res, rej) => {
-				stream.write(content + '\n', err => {
-					if (err) {
-						logger.error(err);
-						rej(err);
-					} else {
-						res();
-					}
-				});
-			});
-			exportedCount++;
-		}
-
-		const total = await Followings.count({
-			followerId: user.id,
-		});
-
-		job.progress(exportedCount / total);
-	}
-
-	stream.end();
-	logger.succ(`Exported to: ${path}`);
-
-	const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
-
-	logger.succ(`Exported to: ${driveFile.id}`);
-	cleanup();
-	done();
-}
diff --git a/src/queue/processors/db/export-mute.ts b/src/queue/processors/db/export-mute.ts
deleted file mode 100644
index d5976f7d56..0000000000
--- a/src/queue/processors/db/export-mute.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import * as Bull from 'bull';
-import * as tmp from 'tmp';
-import * as fs from 'fs';
-
-import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
-import * as dateFormat from 'dateformat';
-import { getFullApAccount } from '@/misc/convert-host';
-import { Users, Mutings } from '@/models/index';
-import { MoreThan } from 'typeorm';
-import { DbUserJobData } from '@/queue/types';
-
-const logger = queueLogger.createSubLogger('export-mute');
-
-export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
-	logger.info(`Exporting mute of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	// Create temp file
-	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
-		tmp.file((e, path, fd, cleanup) => {
-			if (e) return rej(e);
-			res([path, cleanup]);
-		});
-	});
-
-	logger.info(`Temp file is ${path}`);
-
-	const stream = fs.createWriteStream(path, { flags: 'a' });
-
-	let exportedCount = 0;
-	let cursor: any = null;
-
-	while (true) {
-		const mutes = await Mutings.find({
-			where: {
-				muterId: user.id,
-				...(cursor ? { id: MoreThan(cursor) } : {})
-			},
-			take: 100,
-			order: {
-				id: 1
-			}
-		});
-
-		if (mutes.length === 0) {
-			job.progress(100);
-			break;
-		}
-
-		cursor = mutes[mutes.length - 1].id;
-
-		for (const mute of mutes) {
-			const u = await Users.findOne({ id: mute.muteeId });
-			if (u == null) {
-				exportedCount++; continue;
-			}
-
-			const content = getFullApAccount(u.username, u.host);
-			await new Promise<void>((res, rej) => {
-				stream.write(content + '\n', err => {
-					if (err) {
-						logger.error(err);
-						rej(err);
-					} else {
-						res();
-					}
-				});
-			});
-			exportedCount++;
-		}
-
-		const total = await Mutings.count({
-			muterId: user.id,
-		});
-
-		job.progress(exportedCount / total);
-	}
-
-	stream.end();
-	logger.succ(`Exported to: ${path}`);
-
-	const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
-
-	logger.succ(`Exported to: ${driveFile.id}`);
-	cleanup();
-	done();
-}
diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts
deleted file mode 100644
index 49850aa706..0000000000
--- a/src/queue/processors/db/export-notes.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import * as Bull from 'bull';
-import * as tmp from 'tmp';
-import * as fs from 'fs';
-
-import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
-import * as dateFormat from 'dateformat';
-import { Users, Notes, Polls } from '@/models/index';
-import { MoreThan } from 'typeorm';
-import { Note } from '@/models/entities/note';
-import { Poll } from '@/models/entities/poll';
-import { DbUserJobData } from '@/queue/types';
-
-const logger = queueLogger.createSubLogger('export-notes');
-
-export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
-	logger.info(`Exporting notes of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	// Create temp file
-	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
-		tmp.file((e, path, fd, cleanup) => {
-			if (e) return rej(e);
-			res([path, cleanup]);
-		});
-	});
-
-	logger.info(`Temp file is ${path}`);
-
-	const stream = fs.createWriteStream(path, { flags: 'a' });
-
-	await new Promise<void>((res, rej) => {
-		stream.write('[', err => {
-			if (err) {
-				logger.error(err);
-				rej(err);
-			} else {
-				res();
-			}
-		});
-	});
-
-	let exportedNotesCount = 0;
-	let cursor: any = null;
-
-	while (true) {
-		const notes = await Notes.find({
-			where: {
-				userId: user.id,
-				...(cursor ? { id: MoreThan(cursor) } : {})
-			},
-			take: 100,
-			order: {
-				id: 1
-			}
-		});
-
-		if (notes.length === 0) {
-			job.progress(100);
-			break;
-		}
-
-		cursor = notes[notes.length - 1].id;
-
-		for (const note of notes) {
-			let poll: Poll | undefined;
-			if (note.hasPoll) {
-				poll = await Polls.findOneOrFail({ noteId: note.id });
-			}
-			const content = JSON.stringify(serialize(note, poll));
-			await new Promise<void>((res, rej) => {
-				stream.write(exportedNotesCount === 0 ? content : ',\n' + content, err => {
-					if (err) {
-						logger.error(err);
-						rej(err);
-					} else {
-						res();
-					}
-				});
-			});
-			exportedNotesCount++;
-		}
-
-		const total = await Notes.count({
-			userId: user.id,
-		});
-
-		job.progress(exportedNotesCount / total);
-	}
-
-	await new Promise<void>((res, rej) => {
-		stream.write(']', err => {
-			if (err) {
-				logger.error(err);
-				rej(err);
-			} else {
-				res();
-			}
-		});
-	});
-
-	stream.end();
-	logger.succ(`Exported to: ${path}`);
-
-	const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
-
-	logger.succ(`Exported to: ${driveFile.id}`);
-	cleanup();
-	done();
-}
-
-function serialize(note: Note, poll: Poll | null = null): any {
-	return {
-		id: note.id,
-		text: note.text,
-		createdAt: note.createdAt,
-		fileIds: note.fileIds,
-		replyId: note.replyId,
-		renoteId: note.renoteId,
-		poll: poll,
-		cw: note.cw,
-		viaMobile: note.viaMobile,
-		visibility: note.visibility,
-		visibleUserIds: note.visibleUserIds,
-		localOnly: note.localOnly
-	};
-}
diff --git a/src/queue/processors/db/export-user-lists.ts b/src/queue/processors/db/export-user-lists.ts
deleted file mode 100644
index 8a86c4df5d..0000000000
--- a/src/queue/processors/db/export-user-lists.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import * as Bull from 'bull';
-import * as tmp from 'tmp';
-import * as fs from 'fs';
-
-import { queueLogger } from '../../logger';
-import addFile from '@/services/drive/add-file';
-import * as dateFormat from 'dateformat';
-import { getFullApAccount } from '@/misc/convert-host';
-import { Users, UserLists, UserListJoinings } from '@/models/index';
-import { In } from 'typeorm';
-import { DbUserJobData } from '@/queue/types';
-
-const logger = queueLogger.createSubLogger('export-user-lists');
-
-export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
-	logger.info(`Exporting user lists of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	const lists = await UserLists.find({
-		userId: user.id
-	});
-
-	// Create temp file
-	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
-		tmp.file((e, path, fd, cleanup) => {
-			if (e) return rej(e);
-			res([path, cleanup]);
-		});
-	});
-
-	logger.info(`Temp file is ${path}`);
-
-	const stream = fs.createWriteStream(path, { flags: 'a' });
-
-	for (const list of lists) {
-		const joinings = await UserListJoinings.find({ userListId: list.id });
-		const users = await Users.find({
-			id: In(joinings.map(j => j.userId))
-		});
-
-		for (const u of users) {
-			const acct = getFullApAccount(u.username, u.host);
-			const content = `${list.name},${acct}`;
-			await new Promise<void>((res, rej) => {
-				stream.write(content + '\n', err => {
-					if (err) {
-						logger.error(err);
-						rej(err);
-					} else {
-						res();
-					}
-				});
-			});
-		}
-	}
-
-	stream.end();
-	logger.succ(`Exported to: ${path}`);
-
-	const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
-
-	logger.succ(`Exported to: ${driveFile.id}`);
-	cleanup();
-	done();
-}
diff --git a/src/queue/processors/db/import-blocking.ts b/src/queue/processors/db/import-blocking.ts
deleted file mode 100644
index 9951da669d..0000000000
--- a/src/queue/processors/db/import-blocking.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import * as Bull from 'bull';
-
-import { queueLogger } from '../../logger';
-import { parseAcct } from '@/misc/acct';
-import { resolveUser } from '@/remote/resolve-user';
-import { downloadTextFile } from '@/misc/download-text-file';
-import { isSelfHost, toPuny } from '@/misc/convert-host';
-import { Users, DriveFiles, Blockings } from '@/models/index';
-import { DbUserImportJobData } from '@/queue/types';
-import block from '@/services/blocking/create';
-
-const logger = queueLogger.createSubLogger('import-blocking');
-
-export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
-	logger.info(`Importing blocking of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	const file = await DriveFiles.findOne({
-		id: job.data.fileId
-	});
-	if (file == null) {
-		done();
-		return;
-	}
-
-	const csv = await downloadTextFile(file.url);
-
-	let linenum = 0;
-
-	for (const line of csv.trim().split('\n')) {
-		linenum++;
-
-		try {
-			const acct = line.split(',')[0].trim();
-			const { username, host } = parseAcct(acct);
-
-			let target = isSelfHost(host!) ? await Users.findOne({
-				host: null,
-				usernameLower: username.toLowerCase()
-			}) : await Users.findOne({
-				host: toPuny(host!),
-				usernameLower: username.toLowerCase()
-			});
-
-			if (host == null && target == null) continue;
-
-			if (target == null) {
-				target = await resolveUser(username, host);
-			}
-
-			if (target == null) {
-				throw `cannot resolve user: @${username}@${host}`;
-			}
-
-			// skip myself
-			if (target.id === job.data.user.id) continue;
-
-			logger.info(`Block[${linenum}] ${target.id} ...`);
-
-			await block(user, target);
-		} catch (e) {
-			logger.warn(`Error in line:${linenum} ${e}`);
-		}
-	}
-
-	logger.succ('Imported');
-	done();
-}
-
diff --git a/src/queue/processors/db/import-following.ts b/src/queue/processors/db/import-following.ts
deleted file mode 100644
index 3d7b7ea404..0000000000
--- a/src/queue/processors/db/import-following.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import * as Bull from 'bull';
-
-import { queueLogger } from '../../logger';
-import follow from '@/services/following/create';
-import { parseAcct } from '@/misc/acct';
-import { resolveUser } from '@/remote/resolve-user';
-import { downloadTextFile } from '@/misc/download-text-file';
-import { isSelfHost, toPuny } from '@/misc/convert-host';
-import { Users, DriveFiles } from '@/models/index';
-import { DbUserImportJobData } from '@/queue/types';
-
-const logger = queueLogger.createSubLogger('import-following');
-
-export async function importFollowing(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
-	logger.info(`Importing following of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	const file = await DriveFiles.findOne({
-		id: job.data.fileId
-	});
-	if (file == null) {
-		done();
-		return;
-	}
-
-	const csv = await downloadTextFile(file.url);
-
-	let linenum = 0;
-
-	for (const line of csv.trim().split('\n')) {
-		linenum++;
-
-		try {
-			const acct = line.split(',')[0].trim();
-			const { username, host } = parseAcct(acct);
-
-			let target = isSelfHost(host!) ? await Users.findOne({
-				host: null,
-				usernameLower: username.toLowerCase()
-			}) : await Users.findOne({
-				host: toPuny(host!),
-				usernameLower: username.toLowerCase()
-			});
-
-			if (host == null && target == null) continue;
-
-			if (target == null) {
-				target = await resolveUser(username, host);
-			}
-
-			if (target == null) {
-				throw `cannot resolve user: @${username}@${host}`;
-			}
-
-			// skip myself
-			if (target.id === job.data.user.id) continue;
-
-			logger.info(`Follow[${linenum}] ${target.id} ...`);
-
-			follow(user, target);
-		} catch (e) {
-			logger.warn(`Error in line:${linenum} ${e}`);
-		}
-	}
-
-	logger.succ('Imported');
-	done();
-}
diff --git a/src/queue/processors/db/import-muting.ts b/src/queue/processors/db/import-muting.ts
deleted file mode 100644
index 798f03a627..0000000000
--- a/src/queue/processors/db/import-muting.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import * as Bull from 'bull';
-
-import { queueLogger } from '../../logger';
-import { parseAcct } from '@/misc/acct';
-import { resolveUser } from '@/remote/resolve-user';
-import { downloadTextFile } from '@/misc/download-text-file';
-import { isSelfHost, toPuny } from '@/misc/convert-host';
-import { Users, DriveFiles, Mutings } from '@/models/index';
-import { DbUserImportJobData } from '@/queue/types';
-import { User } from '@/models/entities/user';
-import { genId } from '@/misc/gen-id';
-
-const logger = queueLogger.createSubLogger('import-muting');
-
-export async function importMuting(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
-	logger.info(`Importing muting of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	const file = await DriveFiles.findOne({
-		id: job.data.fileId
-	});
-	if (file == null) {
-		done();
-		return;
-	}
-
-	const csv = await downloadTextFile(file.url);
-
-	let linenum = 0;
-
-	for (const line of csv.trim().split('\n')) {
-		linenum++;
-
-		try {
-			const acct = line.split(',')[0].trim();
-			const { username, host } = parseAcct(acct);
-
-			let target = isSelfHost(host!) ? await Users.findOne({
-				host: null,
-				usernameLower: username.toLowerCase()
-			}) : await Users.findOne({
-				host: toPuny(host!),
-				usernameLower: username.toLowerCase()
-			});
-
-			if (host == null && target == null) continue;
-
-			if (target == null) {
-				target = await resolveUser(username, host);
-			}
-
-			if (target == null) {
-				throw `cannot resolve user: @${username}@${host}`;
-			}
-
-			// skip myself
-			if (target.id === job.data.user.id) continue;
-
-			logger.info(`Mute[${linenum}] ${target.id} ...`);
-
-			await mute(user, target);
-		} catch (e) {
-			logger.warn(`Error in line:${linenum} ${e}`);
-		}
-	}
-
-	logger.succ('Imported');
-	done();
-}
-
-async function mute(user: User, target: User) {
-	await Mutings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		muterId: user.id,
-		muteeId: target.id,
-	});
-}
diff --git a/src/queue/processors/db/import-user-lists.ts b/src/queue/processors/db/import-user-lists.ts
deleted file mode 100644
index 3b8c13262a..0000000000
--- a/src/queue/processors/db/import-user-lists.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import * as Bull from 'bull';
-
-import { queueLogger } from '../../logger';
-import { parseAcct } from '@/misc/acct';
-import { resolveUser } from '@/remote/resolve-user';
-import { pushUserToUserList } from '@/services/user-list/push';
-import { downloadTextFile } from '@/misc/download-text-file';
-import { isSelfHost, toPuny } from '@/misc/convert-host';
-import { DriveFiles, Users, UserLists, UserListJoinings } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { DbUserImportJobData } from '@/queue/types';
-
-const logger = queueLogger.createSubLogger('import-user-lists');
-
-export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
-	logger.info(`Importing user lists of ${job.data.user.id} ...`);
-
-	const user = await Users.findOne(job.data.user.id);
-	if (user == null) {
-		done();
-		return;
-	}
-
-	const file = await DriveFiles.findOne({
-		id: job.data.fileId
-	});
-	if (file == null) {
-		done();
-		return;
-	}
-
-	const csv = await downloadTextFile(file.url);
-
-	let linenum = 0;
-
-	for (const line of csv.trim().split('\n')) {
-		linenum++;
-
-		try {
-			const listName = line.split(',')[0].trim();
-			const { username, host } = parseAcct(line.split(',')[1].trim());
-
-			let list = await UserLists.findOne({
-				userId: user.id,
-				name: listName
-			});
-
-			if (list == null) {
-				list = await UserLists.save({
-					id: genId(),
-					createdAt: new Date(),
-					userId: user.id,
-					name: listName,
-					userIds: []
-				});
-			}
-
-			let target = isSelfHost(host!) ? await Users.findOne({
-				host: null,
-				usernameLower: username.toLowerCase()
-			}) : await Users.findOne({
-				host: toPuny(host!),
-				usernameLower: username.toLowerCase()
-			});
-
-			if (target == null) {
-				target = await resolveUser(username, host);
-			}
-
-			if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
-
-			pushUserToUserList(target, list);
-		} catch (e) {
-			logger.warn(`Error in line:${linenum} ${e}`);
-		}
-	}
-
-	logger.succ('Imported');
-	done();
-}
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
deleted file mode 100644
index 97087642b7..0000000000
--- a/src/queue/processors/db/index.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import * as Bull from 'bull';
-import { DbJobData } from '@/queue/types';
-import { deleteDriveFiles } from './delete-drive-files';
-import { exportNotes } from './export-notes';
-import { exportFollowing } from './export-following';
-import { exportMute } from './export-mute';
-import { exportBlocking } from './export-blocking';
-import { exportUserLists } from './export-user-lists';
-import { importFollowing } from './import-following';
-import { importUserLists } from './import-user-lists';
-import { deleteAccount } from './delete-account';
-import { importMuting } from './import-muting';
-import { importBlocking } from './import-blocking';
-
-const jobs = {
-	deleteDriveFiles,
-	exportNotes,
-	exportFollowing,
-	exportMute,
-	exportBlocking,
-	exportUserLists,
-	importFollowing,
-	importMuting,
-	importBlocking,
-	importUserLists,
-	deleteAccount,
-} as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>;
-
-export default function(dbQueue: Bull.Queue<DbJobData>) {
-	for (const [k, v] of Object.entries(jobs)) {
-		dbQueue.process(k, v);
-	}
-}
diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts
deleted file mode 100644
index 3c61896a2f..0000000000
--- a/src/queue/processors/deliver.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { URL } from 'url';
-import * as Bull from 'bull';
-import request from '@/remote/activitypub/request';
-import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc';
-import Logger from '@/services/logger';
-import { Instances } from '@/models/index';
-import { instanceChart } from '@/services/chart/index';
-import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { toPuny } from '@/misc/convert-host';
-import { Cache } from '@/misc/cache';
-import { Instance } from '@/models/entities/instance';
-import { DeliverJobData } from '../types';
-import { StatusError } from '@/misc/fetch';
-
-const logger = new Logger('deliver');
-
-let latest: string | null = null;
-
-const suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
-
-export default async (job: Bull.Job<DeliverJobData>) => {
-	const { host } = new URL(job.data.to);
-
-	// ブロックしてたら中断
-	const meta = await fetchMeta();
-	if (meta.blockedHosts.includes(toPuny(host))) {
-		return 'skip (blocked)';
-	}
-
-	// isSuspendedなら中断
-	let suspendedHosts = suspendedHostsCache.get(null);
-	if (suspendedHosts == null) {
-		suspendedHosts = await Instances.find({
-			where: {
-				isSuspended: true
-			},
-		});
-		suspendedHostsCache.set(null, suspendedHosts);
-	}
-	if (suspendedHosts.map(x => x.host).includes(toPuny(host))) {
-		return 'skip (suspended)';
-	}
-
-	try {
-		if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
-			logger.debug(`delivering ${latest}`);
-		}
-
-		await request(job.data.user, job.data.to, job.data.content);
-
-		// Update stats
-		registerOrFetchInstanceDoc(host).then(i => {
-			Instances.update(i.id, {
-				latestRequestSentAt: new Date(),
-				latestStatus: 200,
-				lastCommunicatedAt: new Date(),
-				isNotResponding: false
-			});
-
-			fetchInstanceMetadata(i);
-
-			instanceChart.requestSent(i.host, true);
-		});
-
-		return 'Success';
-	} catch (res) {
-		// Update stats
-		registerOrFetchInstanceDoc(host).then(i => {
-			Instances.update(i.id, {
-				latestRequestSentAt: new Date(),
-				latestStatus: res instanceof StatusError ? res.statusCode : null,
-				isNotResponding: true
-			});
-
-			instanceChart.requestSent(i.host, false);
-		});
-
-		if (res instanceof StatusError) {
-			// 4xx
-			if (res.isClientError) {
-				// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
-				// 何回再送しても成功することはないということなのでエラーにはしないでおく
-				return `${res.statusCode} ${res.statusMessage}`;
-			}
-
-			// 5xx etc.
-			throw `${res.statusCode} ${res.statusMessage}`;
-		} else {
-			// DNS error, socket error, timeout ...
-			throw res;
-		}
-	}
-};
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
deleted file mode 100644
index 4032ce8653..0000000000
--- a/src/queue/processors/inbox.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { URL } from 'url';
-import * as Bull from 'bull';
-import * as httpSignature from 'http-signature';
-import perform from '@/remote/activitypub/perform';
-import Logger from '@/services/logger';
-import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc';
-import { Instances } from '@/models/index';
-import { instanceChart } from '@/services/chart/index';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { toPuny, extractDbHost } from '@/misc/convert-host';
-import { getApId } from '@/remote/activitypub/type';
-import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
-import { InboxJobData } from '../types';
-import DbResolver from '@/remote/activitypub/db-resolver';
-import { resolvePerson } from '@/remote/activitypub/models/person';
-import { LdSignature } from '@/remote/activitypub/misc/ld-signature';
-import { StatusError } from '@/misc/fetch';
-
-const logger = new Logger('inbox');
-
-// ユーザーのinboxにアクティビティが届いた時の処理
-export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
-	const signature = job.data.signature;	// HTTP-signature
-	const activity = job.data.activity;
-
-	//#region Log
-	const info = Object.assign({}, activity) as any;
-	delete info['@context'];
-	logger.debug(JSON.stringify(info, null, 2));
-	//#endregion
-
-	const host = toPuny(new URL(signature.keyId).hostname);
-
-	// ブロックしてたら中断
-	const meta = await fetchMeta();
-	if (meta.blockedHosts.includes(host)) {
-		return `Blocked request: ${host}`;
-	}
-
-	const keyIdLower = signature.keyId.toLowerCase();
-	if (keyIdLower.startsWith('acct:')) {
-		return `Old keyId is no longer supported. ${keyIdLower}`;
-	}
-
-	// TDOO: キャッシュ
-	const dbResolver = new DbResolver();
-
-	// HTTP-Signature keyIdを元にDBから取得
-	let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
-
-	// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
-	if (authUser == null) {
-		try {
-			authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
-		} catch (e) {
-			// 対象が4xxならスキップ
-			if (e instanceof StatusError && e.isClientError) {
-				return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
-			}
-			throw `Error in actor ${activity.actor} - ${e.statusCode || e}`;
-		}
-	}
-
-	// それでもわからなければ終了
-	if (authUser == null) {
-		return `skip: failed to resolve user`;
-	}
-
-	// publicKey がなくても終了
-	if (authUser.key == null) {
-		return `skip: failed to resolve user publicKey`;
-	}
-
-	// HTTP-Signatureの検証
-	const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
-
-	// また、signatureのsignerは、activity.actorと一致する必要がある
-	if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
-		// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
-		if (activity.signature) {
-			if (activity.signature.type !== 'RsaSignature2017') {
-				return `skip: unsupported LD-signature type ${activity.signature.type}`;
-			}
-
-			// activity.signature.creator: https://example.oom/users/user#main-key
-			// みたいになっててUserを引っ張れば公開キーも入ることを期待する
-			if (activity.signature.creator) {
-				const candicate = activity.signature.creator.replace(/#.*/, '');
-				await resolvePerson(candicate).catch(() => null);
-			}
-
-			// keyIdからLD-Signatureのユーザーを取得
-			authUser = await dbResolver.getAuthUserFromKeyId(activity.signature.creator);
-			if (authUser == null) {
-				return `skip: LD-Signatureのユーザーが取得できませんでした`;
-			}
-
-			if (authUser.key == null) {
-				return `skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした`;
-			}
-
-			// LD-Signature検証
-			const ldSignature = new LdSignature();
-			const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
-			if (!verified) {
-				return `skip: LD-Signatureの検証に失敗しました`;
-			}
-
-			// もう一度actorチェック
-			if (authUser.user.uri !== activity.actor) {
-				return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
-			}
-
-			// ブロックしてたら中断
-			const ldHost = extractDbHost(authUser.user.uri);
-			if (meta.blockedHosts.includes(ldHost)) {
-				return `Blocked request: ${ldHost}`;
-			}
-		} else {
-			return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
-		}
-	}
-
-	// activity.idがあればホストが署名者のホストであることを確認する
-	if (typeof activity.id === 'string') {
-		const signerHost = extractDbHost(authUser.user.uri!);
-		const activityIdHost = extractDbHost(activity.id);
-		if (signerHost !== activityIdHost) {
-			return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
-		}
-	}
-
-	// Update stats
-	registerOrFetchInstanceDoc(authUser.user.host).then(i => {
-		Instances.update(i.id, {
-			latestRequestReceivedAt: new Date(),
-			lastCommunicatedAt: new Date(),
-			isNotResponding: false
-		});
-
-		fetchInstanceMetadata(i);
-
-		instanceChart.requestReceived(i.host);
-	});
-
-	// アクティビティを処理
-	await perform(authUser.user, activity);
-	return `ok`;
-};
diff --git a/src/queue/processors/object-storage/clean-remote-files.ts b/src/queue/processors/object-storage/clean-remote-files.ts
deleted file mode 100644
index 3b2e4ea939..0000000000
--- a/src/queue/processors/object-storage/clean-remote-files.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import * as Bull from 'bull';
-
-import { queueLogger } from '../../logger';
-import { deleteFileSync } from '@/services/drive/delete-file';
-import { DriveFiles } from '@/models/index';
-import { MoreThan, Not, IsNull } from 'typeorm';
-
-const logger = queueLogger.createSubLogger('clean-remote-files');
-
-export default async function cleanRemoteFiles(job: Bull.Job<{}>, done: any): Promise<void> {
-	logger.info(`Deleting cached remote files...`);
-
-	let deletedCount = 0;
-	let cursor: any = null;
-
-	while (true) {
-		const files = await DriveFiles.find({
-			where: {
-				userHost: Not(IsNull()),
-				isLink: false,
-				...(cursor ? { id: MoreThan(cursor) } : {})
-			},
-			take: 8,
-			order: {
-				id: 1
-			}
-		});
-
-		if (files.length === 0) {
-			job.progress(100);
-			break;
-		}
-
-		cursor = files[files.length - 1].id;
-
-		await Promise.all(files.map(file => deleteFileSync(file, true)));
-
-		deletedCount += 8;
-
-		const total = await DriveFiles.count({
-			userHost: Not(IsNull()),
-			isLink: false,
-		});
-
-		job.progress(deletedCount / total);
-	}
-
-	logger.succ(`All cahced remote files has been deleted.`);
-	done();
-}
diff --git a/src/queue/processors/object-storage/delete-file.ts b/src/queue/processors/object-storage/delete-file.ts
deleted file mode 100644
index ed22968a27..0000000000
--- a/src/queue/processors/object-storage/delete-file.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { ObjectStorageFileJobData } from '@/queue/types';
-import * as Bull from 'bull';
-import { deleteObjectStorageFile } from '@/services/drive/delete-file';
-
-export default async (job: Bull.Job<ObjectStorageFileJobData>) => {
-	const key: string = job.data.key;
-
-	await deleteObjectStorageFile(key);
-
-	return 'Success';
-};
diff --git a/src/queue/processors/object-storage/index.ts b/src/queue/processors/object-storage/index.ts
deleted file mode 100644
index 0d9570e179..0000000000
--- a/src/queue/processors/object-storage/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as Bull from 'bull';
-import { ObjectStorageJobData } from '@/queue/types';
-import deleteFile from './delete-file';
-import cleanRemoteFiles from './clean-remote-files';
-
-const jobs = {
-	deleteFile,
-	cleanRemoteFiles,
-} as Record<string, Bull.ProcessCallbackFunction<ObjectStorageJobData> | Bull.ProcessPromiseFunction<ObjectStorageJobData>>;
-
-export default function(q: Bull.Queue) {
-	for (const [k, v] of Object.entries(jobs)) {
-		q.process(k, 16, v);
-	}
-}
diff --git a/src/queue/processors/system/index.ts b/src/queue/processors/system/index.ts
deleted file mode 100644
index 52b7868105..0000000000
--- a/src/queue/processors/system/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import * as Bull from 'bull';
-import { resyncCharts } from './resync-charts';
-
-const jobs = {
-	resyncCharts,
-} as Record<string, Bull.ProcessCallbackFunction<{}> | Bull.ProcessPromiseFunction<{}>>;
-
-export default function(dbQueue: Bull.Queue<{}>) {
-	for (const [k, v] of Object.entries(jobs)) {
-		dbQueue.process(k, v);
-	}
-}
diff --git a/src/queue/processors/system/resync-charts.ts b/src/queue/processors/system/resync-charts.ts
deleted file mode 100644
index b36b024cfb..0000000000
--- a/src/queue/processors/system/resync-charts.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as Bull from 'bull';
-
-import { queueLogger } from '../../logger';
-import { driveChart, notesChart, usersChart } from '@/services/chart/index';
-
-const logger = queueLogger.createSubLogger('resync-charts');
-
-export default async function resyncCharts(job: Bull.Job<{}>, done: any): Promise<void> {
-	logger.info(`Resync charts...`);
-
-	// TODO: ユーザーごとのチャートも更新する
-	// TODO: インスタンスごとのチャートも更新する
-	await Promise.all([
-		driveChart.resync(),
-		notesChart.resync(),
-		usersChart.resync(),
-	]);
-
-	logger.succ(`All charts successfully resynced.`);
-	done();
-}
diff --git a/src/queue/queues.ts b/src/queue/queues.ts
deleted file mode 100644
index a66a7ca451..0000000000
--- a/src/queue/queues.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import config from '@/config/index';
-import { initialize as initializeQueue } from './initialize';
-import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
-
-export const systemQueue = initializeQueue<{}>('system');
-export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
-export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
-export const dbQueue = initializeQueue<DbJobData>('db');
-export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage');
diff --git a/src/queue/types.ts b/src/queue/types.ts
deleted file mode 100644
index 39cab29966..0000000000
--- a/src/queue/types.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { DriveFile } from '@/models/entities/drive-file';
-import { User } from '@/models/entities/user';
-import { IActivity } from '@/remote/activitypub/type';
-import * as httpSignature from 'http-signature';
-
-export type DeliverJobData = {
-	/** Actor */
-	user: ThinUser;
-	/** Activity */
-	content: unknown;
-	/** inbox URL to deliver */
-	to: string;
-};
-
-export type InboxJobData = {
-	activity: IActivity;
-	signature: httpSignature.IParsedSignature;
-};
-
-export type DbJobData = DbUserJobData | DbUserImportJobData | DbUserDeleteJobData;
-
-export type DbUserJobData = {
-	user: ThinUser;
-};
-
-export type DbUserDeleteJobData = {
-	user: ThinUser;
-	soft?: boolean;
-};
-
-export type DbUserImportJobData = {
-	user: ThinUser;
-	fileId: DriveFile['id'];
-};
-
-export type ObjectStorageJobData = ObjectStorageFileJobData | {};
-
-export type ObjectStorageFileJobData = {
-	key: string;
-};
-
-export type ThinUser = {
-	id: User['id'];
-};
diff --git a/src/remote/activitypub/ap-request.ts b/src/remote/activitypub/ap-request.ts
deleted file mode 100644
index 76a3857140..0000000000
--- a/src/remote/activitypub/ap-request.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import * as crypto from 'crypto';
-import { URL } from 'url';
-
-type Request = {
-	url: string;
-	method: string;
-	headers: Record<string, string>;
-};
-
-type PrivateKey = {
-	privateKeyPem: string;
-	keyId: string;
-};
-
-export function createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }) {
-	const u = new URL(args.url);
-	const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
-
-	const request: Request = {
-		url: u.href,
-		method: 'POST',
-		headers:  objectAssignWithLcKey({
-			'Date': new Date().toUTCString(),
-			'Host': u.hostname,
-			'Content-Type': 'application/activity+json',
-			'Digest': digestHeader,
-		}, args.additionalHeaders),
-	};
-
-	const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
-
-	return {
-		request,
-		signingString: result.signingString,
-		signature: result.signature,
-		signatureHeader: result.signatureHeader,
-	};
-}
-
-export function createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }) {
-	const u = new URL(args.url);
-
-	const request: Request = {
-		url: u.href,
-		method: 'GET',
-		headers:  objectAssignWithLcKey({
-			'Accept': 'application/activity+json, application/ld+json',
-			'Date': new Date().toUTCString(),
-			'Host': new URL(args.url).hostname,
-		}, args.additionalHeaders),
-	};
-
-	const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
-
-	return {
-		request,
-		signingString: result.signingString,
-		signature: result.signature,
-		signatureHeader: result.signatureHeader,
-	};
-}
-
-function signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]) {
-	const signingString = genSigningString(request, includeHeaders);
-	const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
-	const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
-
-	request.headers = objectAssignWithLcKey(request.headers, {
-		Signature: signatureHeader
-	});
-
-	return {
-		request,
-		signingString,
-		signature,
-		signatureHeader,
-	};
-}
-
-function genSigningString(request: Request, includeHeaders: string[]) {
-	request.headers = lcObjectKey(request.headers);
-
-	const results: string[] = [];
-
-	for (const key of includeHeaders.map(x => x.toLowerCase())) {
-		if (key === '(request-target)') {
-			results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`);
-		} else {
-			results.push(`${key}: ${request.headers[key]}`);
-		}
-	}
-
-	return results.join('\n');
-}
-
-function lcObjectKey(src: Record<string, string>) {
-	const dst: Record<string, string> = {};
-	for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
-	return dst;
-}
-
-function objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>) {
-	return Object.assign(lcObjectKey(a), lcObjectKey(b));
-}
diff --git a/src/remote/activitypub/audience.ts b/src/remote/activitypub/audience.ts
deleted file mode 100644
index 3d2dab1459..0000000000
--- a/src/remote/activitypub/audience.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { ApObject, getApIds } from './type';
-import Resolver from './resolver';
-import { resolvePerson } from './models/person';
-import { unique, concat } from '@/prelude/array';
-import * as promiseLimit from 'promise-limit';
-import { User, IRemoteUser } from '@/models/entities/user';
-
-type Visibility = 'public' | 'home' | 'followers' | 'specified';
-
-type AudienceInfo = {
-	visibility: Visibility,
-	mentionedUsers: User[],
-	visibleUsers: User[],
-};
-
-export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
-	const toGroups = groupingAudience(getApIds(to), actor);
-	const ccGroups = groupingAudience(getApIds(cc), actor);
-
-	const others = unique(concat([toGroups.other, ccGroups.other]));
-
-	const limit = promiseLimit<User | null>(2);
-	const mentionedUsers = (await Promise.all(
-		others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null)))
-	)).filter((x): x is User => x != null);
-
-	if (toGroups.public.length > 0) {
-		return {
-			visibility: 'public',
-			mentionedUsers,
-			visibleUsers: []
-		};
-	}
-
-	if (ccGroups.public.length > 0) {
-		return {
-			visibility: 'home',
-			mentionedUsers,
-			visibleUsers: []
-		};
-	}
-
-	if (toGroups.followers.length > 0) {
-		return {
-			visibility: 'followers',
-			mentionedUsers,
-			visibleUsers: []
-		};
-	}
-
-	return {
-		visibility: 'specified',
-		mentionedUsers,
-		visibleUsers: mentionedUsers
-	};
-}
-
-function groupingAudience(ids: string[], actor: IRemoteUser) {
-	const groups = {
-		public: [] as string[],
-		followers: [] as string[],
-		other: [] as string[],
-	};
-
-	for (const id of ids) {
-		if (isPublic(id)) {
-			groups.public.push(id);
-		} else if (isFollowers(id, actor)) {
-			groups.followers.push(id);
-		} else {
-			groups.other.push(id);
-		}
-	}
-
-	groups.other = unique(groups.other);
-
-	return groups;
-}
-
-function isPublic(id: string) {
-	return [
-		'https://www.w3.org/ns/activitystreams#Public',
-		'as#Public',
-		'Public',
-	].includes(id);
-}
-
-function isFollowers(id: string, actor: IRemoteUser) {
-	return (
-		id === (actor.followersUri || `${actor.uri}/followers`)
-	);
-}
diff --git a/src/remote/activitypub/db-resolver.ts b/src/remote/activitypub/db-resolver.ts
deleted file mode 100644
index 289b6f0ee8..0000000000
--- a/src/remote/activitypub/db-resolver.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import config from '@/config/index';
-import { Note } from '@/models/entities/note';
-import { User, IRemoteUser } from '@/models/entities/user';
-import { UserPublickey } from '@/models/entities/user-publickey';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index';
-import { IObject, getApId } from './type';
-import { resolvePerson } from './models/person';
-import escapeRegexp = require('escape-regexp');
-
-export default class DbResolver {
-	constructor() {
-	}
-
-	/**
-	 * AP Note => Misskey Note in DB
-	 */
-	public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
-		const parsed = this.parseUri(value);
-
-		if (parsed.id) {
-			return (await Notes.findOne({
-				id: parsed.id
-			})) || null;
-		}
-
-		if (parsed.uri) {
-			return (await Notes.findOne({
-				uri: parsed.uri
-			})) || null;
-		}
-
-		return null;
-	}
-
-	public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> {
-		const parsed = this.parseUri(value);
-
-		if (parsed.id) {
-			return (await MessagingMessages.findOne({
-				id: parsed.id
-			})) || null;
-		}
-
-		if (parsed.uri) {
-			return (await MessagingMessages.findOne({
-				uri: parsed.uri
-			})) || null;
-		}
-
-		return null;
-	}
-
-	/**
-	 * AP Person => Misskey User in DB
-	 */
-	public async getUserFromApId(value: string | IObject): Promise<User | null> {
-		const parsed = this.parseUri(value);
-
-		if (parsed.id) {
-			return (await Users.findOne({
-				id: parsed.id
-			})) || null;
-		}
-
-		if (parsed.uri) {
-			return (await Users.findOne({
-				uri: parsed.uri
-			})) || null;
-		}
-
-		return null;
-	}
-
-	/**
-	 * AP KeyId => Misskey User and Key
-	 */
-	public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> {
-		const key = await UserPublickeys.findOne({
-			keyId
-		});
-
-		if (key == null) return null;
-
-		const user = await Users.findOne(key.userId) as IRemoteUser;
-
-		return {
-			user,
-			key
-		};
-	}
-
-	/**
-	 * AP Actor id => Misskey User and Key
-	 */
-	public async getAuthUserFromApId(uri: string): Promise<AuthUser | null> {
-		const user = await resolvePerson(uri) as IRemoteUser;
-
-		if (user == null) return null;
-
-		const key = await UserPublickeys.findOne(user.id);
-
-		return {
-			user,
-			key
-		};
-	}
-
-	public parseUri(value: string | IObject): UriParseResult {
-		const uri = getApId(value);
-
-		const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)');
-		const matchLocal = uri.match(localRegex);
-
-		if (matchLocal) {
-			return {
-				type: matchLocal[1],
-				id: matchLocal[2]
-			};
-		} else {
-			return {
-				uri
-			};
-		}
-	}
-}
-
-export type AuthUser = {
-	user: IRemoteUser;
-	key?: UserPublickey;
-};
-
-type UriParseResult = {
-	/** id in DB (local object only) */
-	id?: string;
-	/** uri in DB (remote object only) */
-	uri?: string;
-	/** hint of type (local object only, ex: notes, users) */
-	type?: string
-};
diff --git a/src/remote/activitypub/deliver-manager.ts b/src/remote/activitypub/deliver-manager.ts
deleted file mode 100644
index d37f97a447..0000000000
--- a/src/remote/activitypub/deliver-manager.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import { Users, Followings } from '@/models/index';
-import { ILocalUser, IRemoteUser, User } from '@/models/entities/user';
-import { deliver } from '@/queue/index';
-
-//#region types
-interface IRecipe {
-	type: string;
-}
-
-interface IFollowersRecipe extends IRecipe {
-	type: 'Followers';
-}
-
-interface IDirectRecipe extends IRecipe {
-	type: 'Direct';
-	to: IRemoteUser;
-}
-
-const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
-	recipe.type === 'Followers';
-
-const isDirect = (recipe: any): recipe is IDirectRecipe =>
-	recipe.type === 'Direct';
-//#endregion
-
-export default class DeliverManager {
-	private actor: { id: User['id']; host: null; };
-	private activity: any;
-	private recipes: IRecipe[] = [];
-
-	/**
-	 * Constructor
-	 * @param actor Actor
-	 * @param activity Activity to deliver
-	 */
-	constructor(actor: { id: User['id']; host: null; }, activity: any) {
-		this.actor = actor;
-		this.activity = activity;
-	}
-
-	/**
-	 * Add recipe for followers deliver
-	 */
-	public addFollowersRecipe() {
-		const deliver = {
-			type: 'Followers'
-		} as IFollowersRecipe;
-
-		this.addRecipe(deliver);
-	}
-
-	/**
-	 * Add recipe for direct deliver
-	 * @param to To
-	 */
-	public addDirectRecipe(to: IRemoteUser) {
-		const recipe = {
-			type: 'Direct',
-			to
-		} as IDirectRecipe;
-
-		this.addRecipe(recipe);
-	}
-
-	/**
-	 * Add recipe
-	 * @param recipe Recipe
-	 */
-	public addRecipe(recipe: IRecipe) {
-		this.recipes.push(recipe);
-	}
-
-	/**
-	 * Execute delivers
-	 */
-	public async execute() {
-		if (!Users.isLocalUser(this.actor)) return;
-
-		const inboxes = new Set<string>();
-
-		// build inbox list
-		for (const recipe of this.recipes) {
-			if (isFollowers(recipe)) {
-				// followers deliver
-				const followers = await Followings.find({
-					followeeId: this.actor.id
-				});
-
-				for (const following of followers) {
-					if (Followings.isRemoteFollower(following)) {
-						const inbox = following.followerSharedInbox || following.followerInbox;
-						inboxes.add(inbox);
-					}
-				}
-			} else if (isDirect(recipe)) {
-				// direct deliver
-				const inbox = recipe.to.inbox;
-				if (inbox) inboxes.add(inbox);
-			}
-		}
-
-		// deliver
-		for (const inbox of inboxes) {
-			deliver(this.actor, this.activity, inbox);
-		}
-	}
-}
-
-//#region Utilities
-/**
- * Deliver activity to followers
- * @param activity Activity
- * @param from Followee
- */
-export async function deliverToFollowers(actor: ILocalUser, activity: any) {
-	const manager = new DeliverManager(actor, activity);
-	manager.addFollowersRecipe();
-	await manager.execute();
-}
-
-/**
- * Deliver activity to user
- * @param activity Activity
- * @param to Target user
- */
-export async function deliverToUser(actor: ILocalUser, activity: any, to: IRemoteUser) {
-	const manager = new DeliverManager(actor, activity);
-	manager.addDirectRecipe(to);
-	await manager.execute();
-}
-//#endregion
diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts
deleted file mode 100644
index 1afb733ab5..0000000000
--- a/src/remote/activitypub/kernel/accept/follow.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import accept from '@/services/following/requests/accept';
-import { IFollow } from '../../type';
-import DbResolver from '../../db-resolver';
-import { relayAccepted } from '@/services/relay';
-
-export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
-	// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
-
-	const dbResolver = new DbResolver();
-	const follower = await dbResolver.getUserFromApId(activity.actor);
-
-	if (follower == null) {
-		return `skip: follower not found`;
-	}
-
-	if (follower.host != null) {
-		return `skip: follower is not a local user`;
-	}
-
-	// relay
-	const match = activity.id?.match(/follow-relay\/(\w+)/);
-	if (match) {
-		return await relayAccepted(match[1]);
-	}
-
-	await accept(actor, follower);
-	return `ok`;
-};
diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts
deleted file mode 100644
index 5c6f81b2e3..0000000000
--- a/src/remote/activitypub/kernel/accept/index.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import Resolver from '../../resolver';
-import { IRemoteUser } from '@/models/entities/user';
-import acceptFollow from './follow';
-import { IAccept, isFollow, getApType } from '../../type';
-import { apLogger } from '../../logger';
-
-const logger = apLogger;
-
-export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
-	const uri = activity.id || activity;
-
-	logger.info(`Accept: ${uri}`);
-
-	const resolver = new Resolver();
-
-	const object = await resolver.resolve(activity.object).catch(e => {
-		logger.error(`Resolution failed: ${e}`);
-		throw e;
-	});
-
-	if (isFollow(object)) return await acceptFollow(actor, object);
-
-	return `skip: Unknown Accept type: ${getApType(object)}`;
-};
diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts
deleted file mode 100644
index b33be0cc85..0000000000
--- a/src/remote/activitypub/kernel/add/index.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import { IAdd } from '../../type';
-import { resolveNote } from '../../models/note';
-import { addPinned } from '@/services/i/pin';
-
-export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
-	if ('actor' in activity && actor.uri !== activity.actor) {
-		throw new Error('invalid actor');
-	}
-
-	if (activity.target == null) {
-		throw new Error('target is null');
-	}
-
-	if (activity.target === actor.featured) {
-		const note = await resolveNote(activity.object);
-		if (note == null) throw new Error('note not found');
-		await addPinned(actor, note.id);
-		return;
-	}
-
-	throw new Error(`unknown target: ${activity.target}`);
-};
diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts
deleted file mode 100644
index 581357e577..0000000000
--- a/src/remote/activitypub/kernel/announce/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import Resolver from '../../resolver';
-import { IRemoteUser } from '@/models/entities/user';
-import announceNote from './note';
-import { IAnnounce, getApId } from '../../type';
-import { apLogger } from '../../logger';
-
-const logger = apLogger;
-
-export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
-	const uri = getApId(activity);
-
-	logger.info(`Announce: ${uri}`);
-
-	const resolver = new Resolver();
-
-	const targetUri = getApId(activity.object);
-
-	announceNote(resolver, actor, activity, targetUri);
-};
diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
deleted file mode 100644
index 5230867f24..0000000000
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import Resolver from '../../resolver';
-import post from '@/services/note/create';
-import { IRemoteUser } from '@/models/entities/user';
-import { IAnnounce, getApId } from '../../type';
-import { fetchNote, resolveNote } from '../../models/note';
-import { apLogger } from '../../logger';
-import { extractDbHost } from '@/misc/convert-host';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { getApLock } from '@/misc/app-lock';
-import { parseAudience } from '../../audience';
-import { StatusError } from '@/misc/fetch';
-
-const logger = apLogger;
-
-/**
- * アナウンスアクティビティを捌きます
- */
-export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
-	const uri = getApId(activity);
-
-	// アナウンサーが凍結されていたらスキップ
-	if (actor.isSuspended) {
-		return;
-	}
-
-	// アナウンス先をブロックしてたら中断
-	const meta = await fetchMeta();
-	if (meta.blockedHosts.includes(extractDbHost(uri))) return;
-
-	const unlock = await getApLock(uri);
-
-	try {
-		// 既に同じURIを持つものが登録されていないかチェック
-		const exist = await fetchNote(uri);
-		if (exist) {
-			return;
-		}
-
-		// Announce対象をresolve
-		let renote;
-		try {
-			renote = await resolveNote(targetUri);
-		} catch (e) {
-			// 対象が4xxならスキップ
-			if (e instanceof StatusError && e.isClientError) {
-				logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
-				return;
-			}
-			logger.warn(`Error in announce target ${targetUri} - ${e.statusCode || e}`);
-			throw e;
-		}
-
-		logger.info(`Creating the (Re)Note: ${uri}`);
-
-		const activityAudience = await parseAudience(actor, activity.to, activity.cc);
-
-		await post(actor, {
-			createdAt: activity.published ? new Date(activity.published) : null,
-			renote,
-			visibility: activityAudience.visibility,
-			visibleUsers: activityAudience.visibleUsers,
-			uri
-		});
-	} finally {
-		unlock();
-	}
-}
diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts
deleted file mode 100644
index 4fd1e07b9b..0000000000
--- a/src/remote/activitypub/kernel/block/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { IBlock } from '../../type';
-import block from '@/services/blocking/create';
-import { IRemoteUser } from '@/models/entities/user';
-import DbResolver from '../../db-resolver';
-
-export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
-	// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
-
-	const dbResolver = new DbResolver();
-	const blockee = await dbResolver.getUserFromApId(activity.object);
-
-	if (blockee == null) {
-		return `skip: blockee not found`;
-	}
-
-	if (blockee.host != null) {
-		return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`;
-	}
-
-	await block(actor, blockee);
-	return `ok`;
-};
diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts
deleted file mode 100644
index ce039a363b..0000000000
--- a/src/remote/activitypub/kernel/create/index.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import Resolver from '../../resolver';
-import { IRemoteUser } from '@/models/entities/user';
-import createNote from './note';
-import { ICreate, getApId, isPost, getApType } from '../../type';
-import { apLogger } from '../../logger';
-import { toArray, concat, unique } from '@/prelude/array';
-
-const logger = apLogger;
-
-export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
-	const uri = getApId(activity);
-
-	logger.info(`Create: ${uri}`);
-
-	// copy audiences between activity <=> object.
-	if (typeof activity.object === 'object') {
-		const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
-		const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)]));
-
-		activity.to = to;
-		activity.cc = cc;
-		activity.object.to = to;
-		activity.object.cc = cc;
-	}
-
-	// If there is no attributedTo, use Activity actor.
-	if (typeof activity.object === 'object' && !activity.object.attributedTo) {
-		activity.object.attributedTo = activity.actor;
-	}
-
-	const resolver = new Resolver();
-
-	const object = await resolver.resolve(activity.object).catch(e => {
-		logger.error(`Resolution failed: ${e}`);
-		throw e;
-	});
-
-	if (isPost(object)) {
-		createNote(resolver, actor, object, false, activity);
-	} else {
-		logger.warn(`Unknown type: ${getApType(object)}`);
-	}
-};
diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts
deleted file mode 100644
index 14e311e4cd..0000000000
--- a/src/remote/activitypub/kernel/create/note.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import Resolver from '../../resolver';
-import { IRemoteUser } from '@/models/entities/user';
-import { createNote, fetchNote } from '../../models/note';
-import { getApId, IObject, ICreate } from '../../type';
-import { getApLock } from '@/misc/app-lock';
-import { extractDbHost } from '@/misc/convert-host';
-import { StatusError } from '@/misc/fetch';
-
-/**
- * 投稿作成アクティビティを捌きます
- */
-export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
-	const uri = getApId(note);
-
-	if (typeof note === 'object') {
-		if (actor.uri !== note.attributedTo) {
-			return `skip: actor.uri !== note.attributedTo`;
-		}
-
-		if (typeof note.id === 'string') {
-			if (extractDbHost(actor.uri) !== extractDbHost(note.id)) {
-				return `skip: host in actor.uri !== note.id`;
-			}
-		}
-	}
-
-	const unlock = await getApLock(uri);
-
-	try {
-		const exist = await fetchNote(note);
-		if (exist) return 'skip: note exists';
-
-		await createNote(note, resolver, silent);
-		return 'ok';
-	} catch (e) {
-		if (e instanceof StatusError && e.isClientError) {
-			return `skip ${e.statusCode}`;
-		} else {
-			throw e;
-		}
-	} finally {
-		unlock();
-	}
-}
diff --git a/src/remote/activitypub/kernel/delete/actor.ts b/src/remote/activitypub/kernel/delete/actor.ts
deleted file mode 100644
index 502f8d5ab5..0000000000
--- a/src/remote/activitypub/kernel/delete/actor.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { apLogger } from '../../logger';
-import { createDeleteAccountJob } from '@/queue';
-import { IRemoteUser } from '@/models/entities/user';
-import { Users } from '@/models/index';
-
-const logger = apLogger;
-
-export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> {
-	logger.info(`Deleting the Actor: ${uri}`);
-
-	if (actor.uri !== uri) {
-		return `skip: delete actor ${actor.uri} !== ${uri}`;
-	}
-
-	if (actor.isDeleted) {
-		logger.info(`skip: already deleted`);
-	}
-
-	const job = await createDeleteAccountJob(actor);
-
-	await Users.update(actor.id, {
-		isDeleted: true,
-	});
-
-	return `ok: queued ${job.name} ${job.id}`;
-}
diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts
deleted file mode 100644
index 86a452de76..0000000000
--- a/src/remote/activitypub/kernel/delete/index.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import deleteNote from './note';
-import { IRemoteUser } from '@/models/entities/user';
-import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type';
-import { toSingle } from '@/prelude/array';
-import { deleteActor } from './actor';
-
-/**
- * 削除アクティビティを捌きます
- */
-export default async (actor: IRemoteUser, activity: IDelete): Promise<string> => {
-	if ('actor' in activity && actor.uri !== activity.actor) {
-		throw new Error('invalid actor');
-	}
-
-	// 削除対象objectのtype
-	let formarType: string | undefined;
-
-	if (typeof activity.object === 'string') {
-		// typeが不明だけど、どうせ消えてるのでremote resolveしない
-		formarType = undefined;
-	} else {
-		const object = activity.object as IObject;
-		if (isTombstone(object)) {
-			formarType = toSingle(object.formerType);
-		} else {
-			formarType = toSingle(object.type);
-		}
-	}
-
-	const uri = getApId(activity.object);
-
-	// type不明でもactorとobjectが同じならばそれはPersonに違いない
-	if (!formarType && actor.uri === uri) {
-		formarType = 'Person';
-	}
-
-	// それでもなかったらおそらくNote
-	if (!formarType) {
-		formarType = 'Note';
-	}
-
-	if (validPost.includes(formarType)) {
-		return await deleteNote(actor, uri);
-	} else if (validActor.includes(formarType)) {
-		return await deleteActor(actor, uri);
-	} else {
-		return `Unknown type ${formarType}`;
-	}
-};
diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts
deleted file mode 100644
index 3875a33d13..0000000000
--- a/src/remote/activitypub/kernel/delete/note.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import deleteNode from '@/services/note/delete';
-import { apLogger } from '../../logger';
-import DbResolver from '../../db-resolver';
-import { getApLock } from '@/misc/app-lock';
-import { deleteMessage } from '@/services/messages/delete';
-
-const logger = apLogger;
-
-export default async function(actor: IRemoteUser, uri: string): Promise<string> {
-	logger.info(`Deleting the Note: ${uri}`);
-
-	const unlock = await getApLock(uri);
-
-	try {
-		const dbResolver = new DbResolver();
-		const note = await dbResolver.getNoteFromApId(uri);
-
-		if (note == null) {
-			const message = await dbResolver.getMessageFromApId(uri);
-			if (message == null) return 'message not found';
-
-			if (message.userId !== actor.id) {
-				return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
-			}
-
-			await deleteMessage(message);
-
-			return 'ok: message deleted';
-		}
-
-		if (note.userId !== actor.id) {
-			return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
-		}
-
-		await deleteNode(actor, note);
-		return 'ok: note deleted';
-	} finally {
-		unlock();
-	}
-}
diff --git a/src/remote/activitypub/kernel/flag/index.ts b/src/remote/activitypub/kernel/flag/index.ts
deleted file mode 100644
index 7abfd694cd..0000000000
--- a/src/remote/activitypub/kernel/flag/index.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import config from '@/config/index';
-import { IFlag, getApIds } from '../../type';
-import { AbuseUserReports, Users } from '@/models/index';
-import { In } from 'typeorm';
-import { genId } from '@/misc/gen-id';
-
-export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => {
-	// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
-	// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
-	const uris = getApIds(activity.object);
-
-	const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop());
-	const users = await Users.find({
-		id: In(userIds)
-	});
-	if (users.length < 1) return `skip`;
-
-	await AbuseUserReports.insert({
-		id: genId(),
-		createdAt: new Date(),
-		targetUserId: users[0].id,
-		targetUserHost: users[0].host,
-		reporterId: actor.id,
-		reporterHost: actor.host,
-		comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`
-	});
-
-	return `ok`;
-};
diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts
deleted file mode 100644
index 3183207afa..0000000000
--- a/src/remote/activitypub/kernel/follow.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import follow from '@/services/following/create';
-import { IFollow } from '../type';
-import DbResolver from '../db-resolver';
-
-export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
-	const dbResolver = new DbResolver();
-	const followee = await dbResolver.getUserFromApId(activity.object);
-
-	if (followee == null) {
-		return `skip: followee not found`;
-	}
-
-	if (followee.host != null) {
-		return `skip: フォローしようとしているユーザーはローカルユーザーではありません`;
-	}
-
-	await follow(actor, followee, activity.id);
-	return `ok`;
-};
diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts
deleted file mode 100644
index 20df28eec6..0000000000
--- a/src/remote/activitypub/kernel/index.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type';
-import { IRemoteUser } from '@/models/entities/user';
-import create from './create/index';
-import performDeleteActivity from './delete/index';
-import performUpdateActivity from './update/index';
-import { performReadActivity } from './read';
-import follow from './follow';
-import undo from './undo/index';
-import like from './like';
-import announce from './announce/index';
-import accept from './accept/index';
-import reject from './reject/index';
-import add from './add/index';
-import remove from './remove/index';
-import block from './block/index';
-import flag from './flag/index';
-import { apLogger } from '../logger';
-import Resolver from '../resolver';
-import { toArray } from '@/prelude/array';
-
-export async function performActivity(actor: IRemoteUser, activity: IObject) {
-	if (isCollectionOrOrderedCollection(activity)) {
-		const resolver = new Resolver();
-		for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
-			const act = await resolver.resolve(item);
-			try {
-				await performOneActivity(actor, act);
-			} catch (e) {
-				apLogger.error(e);
-			}
-		}
-	} else {
-		await performOneActivity(actor, activity);
-	}
-}
-
-async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise<void> {
-	if (actor.isSuspended) return;
-
-	if (isCreate(activity)) {
-		await create(actor, activity);
-	} else if (isDelete(activity)) {
-		await performDeleteActivity(actor, activity);
-	} else if (isUpdate(activity)) {
-		await performUpdateActivity(actor, activity);
-	} else if (isRead(activity)) {
-		await performReadActivity(actor, activity);
-	} else if (isFollow(activity)) {
-		await follow(actor, activity);
-	} else if (isAccept(activity)) {
-		await accept(actor, activity);
-	} else if (isReject(activity)) {
-		await reject(actor, activity);
-	} else if (isAdd(activity)) {
-		await add(actor, activity).catch(err => apLogger.error(err));
-	} else if (isRemove(activity)) {
-		await remove(actor, activity).catch(err => apLogger.error(err));
-	} else if (isAnnounce(activity)) {
-		await announce(actor, activity);
-	} else if (isLike(activity)) {
-		await like(actor, activity);
-	} else if (isUndo(activity)) {
-		await undo(actor, activity);
-	} else if (isBlock(activity)) {
-		await block(actor, activity);
-	} else if (isFlag(activity)) {
-		await flag(actor, activity);
-	} else {
-		apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
-	}
-}
diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts
deleted file mode 100644
index 58d5aefefc..0000000000
--- a/src/remote/activitypub/kernel/like.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import { ILike, getApId } from '../type';
-import create from '@/services/note/reaction/create';
-import { fetchNote, extractEmojis } from '../models/note';
-
-export default async (actor: IRemoteUser, activity: ILike) => {
-	const targetUri = getApId(activity.object);
-
-	const note = await fetchNote(targetUri);
-	if (!note) return `skip: target note not found ${targetUri}`;
-
-	await extractEmojis(activity.tag || [], actor.host).catch(() => null);
-
-	return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
-		if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
-			return 'skip: already reacted';
-		} else {
-			throw e;
-		}
-	}).then(() => 'ok');
-};
diff --git a/src/remote/activitypub/kernel/move/index.ts b/src/remote/activitypub/kernel/move/index.ts
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/src/remote/activitypub/kernel/read.ts b/src/remote/activitypub/kernel/read.ts
deleted file mode 100644
index 11a1731869..0000000000
--- a/src/remote/activitypub/kernel/read.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import { IRead, getApId } from '../type';
-import { isSelfHost, extractDbHost } from '@/misc/convert-host';
-import { MessagingMessages } from '@/models/index';
-import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message';
-
-export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
-	const id = await getApId(activity.object);
-
-	if (!isSelfHost(extractDbHost(id))) {
-		return `skip: Read to foreign host (${id})`;
-	}
-
-	const messageId = id.split('/').pop();
-
-	const message = await MessagingMessages.findOne(messageId);
-	if (message == null) {
-		return `skip: message not found`;
-	}
-
-	if (actor.id != message.recipientId) {
-		return `skip: actor is not a message recipient`;
-	}
-
-	await readUserMessagingMessage(message.recipientId!, message.userId, [message.id]);
-	return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`;
-};
diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts
deleted file mode 100644
index 356547440f..0000000000
--- a/src/remote/activitypub/kernel/reject/follow.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import reject from '@/services/following/requests/reject';
-import { IFollow } from '../../type';
-import DbResolver from '../../db-resolver';
-import { relayRejected } from '@/services/relay';
-
-export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
-	// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
-
-	const dbResolver = new DbResolver();
-	const follower = await dbResolver.getUserFromApId(activity.actor);
-
-	if (follower == null) {
-		return `skip: follower not found`;
-	}
-
-	if (follower.host != null) {
-		return `skip: follower is not a local user`;
-	}
-
-	// relay
-	const match = activity.id?.match(/follow-relay\/(\w+)/);
-	if (match) {
-		return await relayRejected(match[1]);
-	}
-
-	await reject(actor, follower);
-	return `ok`;
-};
diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts
deleted file mode 100644
index d0de9c329b..0000000000
--- a/src/remote/activitypub/kernel/reject/index.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import Resolver from '../../resolver';
-import { IRemoteUser } from '@/models/entities/user';
-import rejectFollow from './follow';
-import { IReject, isFollow, getApType } from '../../type';
-import { apLogger } from '../../logger';
-
-const logger = apLogger;
-
-export default async (actor: IRemoteUser, activity: IReject): Promise<string> => {
-	const uri = activity.id || activity;
-
-	logger.info(`Reject: ${uri}`);
-
-	const resolver = new Resolver();
-
-	const object = await resolver.resolve(activity.object).catch(e => {
-		logger.error(`Resolution failed: ${e}`);
-		throw e;
-	});
-
-	if (isFollow(object)) return await rejectFollow(actor, object);
-
-	return `skip: Unknown Reject type: ${getApType(object)}`;
-};
diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts
deleted file mode 100644
index d59953e653..0000000000
--- a/src/remote/activitypub/kernel/remove/index.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import { IRemove } from '../../type';
-import { resolveNote } from '../../models/note';
-import { removePinned } from '@/services/i/pin';
-
-export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
-	if ('actor' in activity && actor.uri !== activity.actor) {
-		throw new Error('invalid actor');
-	}
-
-	if (activity.target == null) {
-		throw new Error('target is null');
-	}
-
-	if (activity.target === actor.featured) {
-		const note = await resolveNote(activity.object);
-		if (note == null) throw new Error('note not found');
-		await removePinned(actor, note.id);
-		return;
-	}
-
-	throw new Error(`unknown target: ${activity.target}`);
-};
diff --git a/src/remote/activitypub/kernel/undo/announce.ts b/src/remote/activitypub/kernel/undo/announce.ts
deleted file mode 100644
index 7bb9d7fcad..0000000000
--- a/src/remote/activitypub/kernel/undo/announce.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Notes } from '@/models/index';
-import { IRemoteUser } from '@/models/entities/user';
-import { IAnnounce, getApId } from '../../type';
-import deleteNote from '@/services/note/delete';
-
-export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<string> => {
-	const uri = getApId(activity);
-
-	const note = await Notes.findOne({
-		uri
-	});
-
-	if (!note) return 'skip: no such Announce';
-
-	await deleteNote(actor, note);
-	return 'ok: deleted';
-};
diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts
deleted file mode 100644
index 61940486be..0000000000
--- a/src/remote/activitypub/kernel/undo/block.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { IBlock } from '../../type';
-import unblock from '@/services/blocking/delete';
-import { IRemoteUser } from '@/models/entities/user';
-import DbResolver from '../../db-resolver';
-
-export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
-	const dbResolver = new DbResolver();
-	const blockee = await dbResolver.getUserFromApId(activity.object);
-
-	if (blockee == null) {
-		return `skip: blockee not found`;
-	}
-
-	if (blockee.host != null) {
-		return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`;
-	}
-
-	await unblock(actor, blockee);
-	return `ok`;
-};
diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts
deleted file mode 100644
index d85c7e4a71..0000000000
--- a/src/remote/activitypub/kernel/undo/follow.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import unfollow from '@/services/following/delete';
-import cancelRequest from '@/services/following/requests/cancel';
-import { IFollow } from '../../type';
-import { IRemoteUser } from '@/models/entities/user';
-import { FollowRequests, Followings } from '@/models/index';
-import DbResolver from '../../db-resolver';
-
-export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
-	const dbResolver = new DbResolver();
-
-	const followee = await dbResolver.getUserFromApId(activity.object);
-	if (followee == null) {
-		return `skip: followee not found`;
-	}
-
-	if (followee.host != null) {
-		return `skip: フォロー解除しようとしているユーザーはローカルユーザーではありません`;
-	}
-
-	const req = await FollowRequests.findOne({
-		followerId: actor.id,
-		followeeId: followee.id
-	});
-
-	const following = await Followings.findOne({
-		followerId: actor.id,
-		followeeId: followee.id
-	});
-
-	if (req) {
-		await cancelRequest(followee, actor);
-		return `ok: follow request canceled`;
-	}
-
-	if (following) {
-		await unfollow(actor, followee);
-		return `ok: unfollowed`;
-	}
-
-	return `skip: リクエストもフォローもされていない`;
-};
diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts
deleted file mode 100644
index 14b1add152..0000000000
--- a/src/remote/activitypub/kernel/undo/index.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type';
-import unfollow from './follow';
-import unblock from './block';
-import undoLike from './like';
-import { undoAnnounce } from './announce';
-import Resolver from '../../resolver';
-import { apLogger } from '../../logger';
-
-const logger = apLogger;
-
-export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
-	if ('actor' in activity && actor.uri !== activity.actor) {
-		throw new Error('invalid actor');
-	}
-
-	const uri = activity.id || activity;
-
-	logger.info(`Undo: ${uri}`);
-
-	const resolver = new Resolver();
-
-	const object = await resolver.resolve(activity.object).catch(e => {
-		logger.error(`Resolution failed: ${e}`);
-		throw e;
-	});
-
-	if (isFollow(object)) return await unfollow(actor, object);
-	if (isBlock(object)) return await unblock(actor, object);
-	if (isLike(object)) return await undoLike(actor, object);
-	if (isAnnounce(object)) return await undoAnnounce(actor, object);
-
-	return `skip: unknown object type ${getApType(object)}`;
-};
diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts
deleted file mode 100644
index 107d3053e3..0000000000
--- a/src/remote/activitypub/kernel/undo/like.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import { ILike, getApId } from '../../type';
-import deleteReaction from '@/services/note/reaction/delete';
-import { fetchNote } from '../../models/note';
-
-/**
- * Process Undo.Like activity
- */
-export default async (actor: IRemoteUser, activity: ILike) => {
-	const targetUri = getApId(activity.object);
-
-	const note = await fetchNote(targetUri);
-	if (!note) return `skip: target note not found ${targetUri}`;
-
-	await deleteReaction(actor, note).catch(e => {
-		if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') return;
-		throw e;
-	});
-
-	return `ok`;
-};
diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts
deleted file mode 100644
index 52bfc5002e..0000000000
--- a/src/remote/activitypub/kernel/update/index.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { IRemoteUser } from '@/models/entities/user';
-import { getApType, IUpdate, isActor } from '../../type';
-import { apLogger } from '../../logger';
-import { updateQuestion } from '../../models/question';
-import Resolver from '../../resolver';
-import { updatePerson } from '../../models/person';
-
-/**
- * Updateアクティビティを捌きます
- */
-export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> => {
-	if ('actor' in activity && actor.uri !== activity.actor) {
-		return `skip: invalid actor`;
-	}
-
-	apLogger.debug('Update');
-
-	const resolver = new Resolver();
-
-	const object = await resolver.resolve(activity.object).catch(e => {
-		apLogger.error(`Resolution failed: ${e}`);
-		throw e;
-	});
-
-	if (isActor(object)) {
-		await updatePerson(actor.uri!, resolver, object);
-		return `ok: Person updated`;
-	} else if (getApType(object) === 'Question') {
-		await updateQuestion(object).catch(e => console.log(e));
-		return `ok: Question updated`;
-	} else {
-		return `skip: Unknown type: ${getApType(object)}`;
-	}
-};
diff --git a/src/remote/activitypub/logger.ts b/src/remote/activitypub/logger.ts
deleted file mode 100644
index e13add01db..0000000000
--- a/src/remote/activitypub/logger.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { remoteLogger } from '../logger';
-
-export const apLogger = remoteLogger.createSubLogger('ap', 'magenta');
diff --git a/src/remote/activitypub/misc/contexts.ts b/src/remote/activitypub/misc/contexts.ts
deleted file mode 100644
index 1426ba15f5..0000000000
--- a/src/remote/activitypub/misc/contexts.ts
+++ /dev/null
@@ -1,526 +0,0 @@
-/* tslint:disable:quotemark indent */
-const id_v1 = {
-  "@context": {
-    "id": "@id",
-    "type": "@type",
-
-    "cred": "https://w3id.org/credentials#",
-    "dc": "http://purl.org/dc/terms/",
-    "identity": "https://w3id.org/identity#",
-    "perm": "https://w3id.org/permissions#",
-    "ps": "https://w3id.org/payswarm#",
-    "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
-    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
-    "sec": "https://w3id.org/security#",
-    "schema": "http://schema.org/",
-    "xsd": "http://www.w3.org/2001/XMLSchema#",
-
-    "Group": "https://www.w3.org/ns/activitystreams#Group",
-
-    "claim": {"@id": "cred:claim", "@type": "@id"},
-    "credential": {"@id": "cred:credential", "@type": "@id"},
-    "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"},
-    "issuer": {"@id": "cred:issuer", "@type": "@id"},
-    "recipient": {"@id": "cred:recipient", "@type": "@id"},
-    "Credential": "cred:Credential",
-    "CryptographicKeyCredential": "cred:CryptographicKeyCredential",
-
-    "about": {"@id": "schema:about", "@type": "@id"},
-    "address": {"@id": "schema:address", "@type": "@id"},
-    "addressCountry": "schema:addressCountry",
-    "addressLocality": "schema:addressLocality",
-    "addressRegion": "schema:addressRegion",
-    "comment": "rdfs:comment",
-    "created": {"@id": "dc:created", "@type": "xsd:dateTime"},
-    "creator": {"@id": "dc:creator", "@type": "@id"},
-    "description": "schema:description",
-    "email": "schema:email",
-    "familyName": "schema:familyName",
-    "givenName": "schema:givenName",
-    "image": {"@id": "schema:image", "@type": "@id"},
-    "label": "rdfs:label",
-    "name": "schema:name",
-    "postalCode": "schema:postalCode",
-    "streetAddress": "schema:streetAddress",
-    "title": "dc:title",
-    "url": {"@id": "schema:url", "@type": "@id"},
-    "Person": "schema:Person",
-    "PostalAddress": "schema:PostalAddress",
-    "Organization": "schema:Organization",
-
-    "identityService": {"@id": "identity:identityService", "@type": "@id"},
-    "idp": {"@id": "identity:idp", "@type": "@id"},
-    "Identity": "identity:Identity",
-
-    "paymentProcessor": "ps:processor",
-    "preferences": {"@id": "ps:preferences", "@type": "@vocab"},
-
-    "cipherAlgorithm": "sec:cipherAlgorithm",
-    "cipherData": "sec:cipherData",
-    "cipherKey": "sec:cipherKey",
-    "digestAlgorithm": "sec:digestAlgorithm",
-    "digestValue": "sec:digestValue",
-    "domain": "sec:domain",
-    "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
-    "initializationVector": "sec:initializationVector",
-    "member": {"@id": "schema:member", "@type": "@id"},
-    "memberOf": {"@id": "schema:memberOf", "@type": "@id"},
-    "nonce": "sec:nonce",
-    "normalizationAlgorithm": "sec:normalizationAlgorithm",
-    "owner": {"@id": "sec:owner", "@type": "@id"},
-    "password": "sec:password",
-    "privateKey": {"@id": "sec:privateKey", "@type": "@id"},
-    "privateKeyPem": "sec:privateKeyPem",
-    "publicKey": {"@id": "sec:publicKey", "@type": "@id"},
-    "publicKeyPem": "sec:publicKeyPem",
-    "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"},
-    "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
-    "signature": "sec:signature",
-    "signatureAlgorithm": "sec:signatureAlgorithm",
-    "signatureValue": "sec:signatureValue",
-    "CryptographicKey": "sec:Key",
-    "EncryptedMessage": "sec:EncryptedMessage",
-    "GraphSignature2012": "sec:GraphSignature2012",
-    "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
-
-    "accessControl": {"@id": "perm:accessControl", "@type": "@id"},
-    "writePermission": {"@id": "perm:writePermission", "@type": "@id"}
-  }
-};
-
-const security_v1 = {
-  "@context": {
-    "id": "@id",
-    "type": "@type",
-
-    "dc": "http://purl.org/dc/terms/",
-    "sec": "https://w3id.org/security#",
-    "xsd": "http://www.w3.org/2001/XMLSchema#",
-
-    "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016",
-    "Ed25519Signature2018": "sec:Ed25519Signature2018",
-    "EncryptedMessage": "sec:EncryptedMessage",
-    "GraphSignature2012": "sec:GraphSignature2012",
-    "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
-    "LinkedDataSignature2016": "sec:LinkedDataSignature2016",
-    "CryptographicKey": "sec:Key",
-
-    "authenticationTag": "sec:authenticationTag",
-    "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm",
-    "cipherAlgorithm": "sec:cipherAlgorithm",
-    "cipherData": "sec:cipherData",
-    "cipherKey": "sec:cipherKey",
-    "created": {"@id": "dc:created", "@type": "xsd:dateTime"},
-    "creator": {"@id": "dc:creator", "@type": "@id"},
-    "digestAlgorithm": "sec:digestAlgorithm",
-    "digestValue": "sec:digestValue",
-    "domain": "sec:domain",
-    "encryptionKey": "sec:encryptionKey",
-    "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
-    "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
-    "initializationVector": "sec:initializationVector",
-    "iterationCount": "sec:iterationCount",
-    "nonce": "sec:nonce",
-    "normalizationAlgorithm": "sec:normalizationAlgorithm",
-    "owner": {"@id": "sec:owner", "@type": "@id"},
-    "password": "sec:password",
-    "privateKey": {"@id": "sec:privateKey", "@type": "@id"},
-    "privateKeyPem": "sec:privateKeyPem",
-    "publicKey": {"@id": "sec:publicKey", "@type": "@id"},
-    "publicKeyBase58": "sec:publicKeyBase58",
-    "publicKeyPem": "sec:publicKeyPem",
-    "publicKeyWif": "sec:publicKeyWif",
-    "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"},
-    "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
-    "salt": "sec:salt",
-    "signature": "sec:signature",
-    "signatureAlgorithm": "sec:signingAlgorithm",
-    "signatureValue": "sec:signatureValue"
-  }
-};
-
-const activitystreams = {
-  "@context": {
-    "@vocab": "_:",
-    "xsd": "http://www.w3.org/2001/XMLSchema#",
-    "as": "https://www.w3.org/ns/activitystreams#",
-    "ldp": "http://www.w3.org/ns/ldp#",
-    "vcard": "http://www.w3.org/2006/vcard/ns#",
-    "id": "@id",
-    "type": "@type",
-    "Accept": "as:Accept",
-    "Activity": "as:Activity",
-    "IntransitiveActivity": "as:IntransitiveActivity",
-    "Add": "as:Add",
-    "Announce": "as:Announce",
-    "Application": "as:Application",
-    "Arrive": "as:Arrive",
-    "Article": "as:Article",
-    "Audio": "as:Audio",
-    "Block": "as:Block",
-    "Collection": "as:Collection",
-    "CollectionPage": "as:CollectionPage",
-    "Relationship": "as:Relationship",
-    "Create": "as:Create",
-    "Delete": "as:Delete",
-    "Dislike": "as:Dislike",
-    "Document": "as:Document",
-    "Event": "as:Event",
-    "Follow": "as:Follow",
-    "Flag": "as:Flag",
-    "Group": "as:Group",
-    "Ignore": "as:Ignore",
-    "Image": "as:Image",
-    "Invite": "as:Invite",
-    "Join": "as:Join",
-    "Leave": "as:Leave",
-    "Like": "as:Like",
-    "Link": "as:Link",
-    "Mention": "as:Mention",
-    "Note": "as:Note",
-    "Object": "as:Object",
-    "Offer": "as:Offer",
-    "OrderedCollection": "as:OrderedCollection",
-    "OrderedCollectionPage": "as:OrderedCollectionPage",
-    "Organization": "as:Organization",
-    "Page": "as:Page",
-    "Person": "as:Person",
-    "Place": "as:Place",
-    "Profile": "as:Profile",
-    "Question": "as:Question",
-    "Reject": "as:Reject",
-    "Remove": "as:Remove",
-    "Service": "as:Service",
-    "TentativeAccept": "as:TentativeAccept",
-    "TentativeReject": "as:TentativeReject",
-    "Tombstone": "as:Tombstone",
-    "Undo": "as:Undo",
-    "Update": "as:Update",
-    "Video": "as:Video",
-    "View": "as:View",
-    "Listen": "as:Listen",
-    "Read": "as:Read",
-    "Move": "as:Move",
-    "Travel": "as:Travel",
-    "IsFollowing": "as:IsFollowing",
-    "IsFollowedBy": "as:IsFollowedBy",
-    "IsContact": "as:IsContact",
-    "IsMember": "as:IsMember",
-    "subject": {
-      "@id": "as:subject",
-      "@type": "@id"
-    },
-    "relationship": {
-      "@id": "as:relationship",
-      "@type": "@id"
-    },
-    "actor": {
-      "@id": "as:actor",
-      "@type": "@id"
-    },
-    "attributedTo": {
-      "@id": "as:attributedTo",
-      "@type": "@id"
-    },
-    "attachment": {
-      "@id": "as:attachment",
-      "@type": "@id"
-    },
-    "bcc": {
-      "@id": "as:bcc",
-      "@type": "@id"
-    },
-    "bto": {
-      "@id": "as:bto",
-      "@type": "@id"
-    },
-    "cc": {
-      "@id": "as:cc",
-      "@type": "@id"
-    },
-    "context": {
-      "@id": "as:context",
-      "@type": "@id"
-    },
-    "current": {
-      "@id": "as:current",
-      "@type": "@id"
-    },
-    "first": {
-      "@id": "as:first",
-      "@type": "@id"
-    },
-    "generator": {
-      "@id": "as:generator",
-      "@type": "@id"
-    },
-    "icon": {
-      "@id": "as:icon",
-      "@type": "@id"
-    },
-    "image": {
-      "@id": "as:image",
-      "@type": "@id"
-    },
-    "inReplyTo": {
-      "@id": "as:inReplyTo",
-      "@type": "@id"
-    },
-    "items": {
-      "@id": "as:items",
-      "@type": "@id"
-    },
-    "instrument": {
-      "@id": "as:instrument",
-      "@type": "@id"
-    },
-    "orderedItems": {
-      "@id": "as:items",
-      "@type": "@id",
-      "@container": "@list"
-    },
-    "last": {
-      "@id": "as:last",
-      "@type": "@id"
-    },
-    "location": {
-      "@id": "as:location",
-      "@type": "@id"
-    },
-    "next": {
-      "@id": "as:next",
-      "@type": "@id"
-    },
-    "object": {
-      "@id": "as:object",
-      "@type": "@id"
-    },
-    "oneOf": {
-      "@id": "as:oneOf",
-      "@type": "@id"
-    },
-    "anyOf": {
-      "@id": "as:anyOf",
-      "@type": "@id"
-    },
-    "closed": {
-      "@id": "as:closed",
-      "@type": "xsd:dateTime"
-    },
-    "origin": {
-      "@id": "as:origin",
-      "@type": "@id"
-    },
-    "accuracy": {
-      "@id": "as:accuracy",
-      "@type": "xsd:float"
-    },
-    "prev": {
-      "@id": "as:prev",
-      "@type": "@id"
-    },
-    "preview": {
-      "@id": "as:preview",
-      "@type": "@id"
-    },
-    "replies": {
-      "@id": "as:replies",
-      "@type": "@id"
-    },
-    "result": {
-      "@id": "as:result",
-      "@type": "@id"
-    },
-    "audience": {
-      "@id": "as:audience",
-      "@type": "@id"
-    },
-    "partOf": {
-      "@id": "as:partOf",
-      "@type": "@id"
-    },
-    "tag": {
-      "@id": "as:tag",
-      "@type": "@id"
-    },
-    "target": {
-      "@id": "as:target",
-      "@type": "@id"
-    },
-    "to": {
-      "@id": "as:to",
-      "@type": "@id"
-    },
-    "url": {
-      "@id": "as:url",
-      "@type": "@id"
-    },
-    "altitude": {
-      "@id": "as:altitude",
-      "@type": "xsd:float"
-    },
-    "content": "as:content",
-    "contentMap": {
-      "@id": "as:content",
-      "@container": "@language"
-    },
-    "name": "as:name",
-    "nameMap": {
-      "@id": "as:name",
-      "@container": "@language"
-    },
-    "duration": {
-      "@id": "as:duration",
-      "@type": "xsd:duration"
-    },
-    "endTime": {
-      "@id": "as:endTime",
-      "@type": "xsd:dateTime"
-    },
-    "height": {
-      "@id": "as:height",
-      "@type": "xsd:nonNegativeInteger"
-    },
-    "href": {
-      "@id": "as:href",
-      "@type": "@id"
-    },
-    "hreflang": "as:hreflang",
-    "latitude": {
-      "@id": "as:latitude",
-      "@type": "xsd:float"
-    },
-    "longitude": {
-      "@id": "as:longitude",
-      "@type": "xsd:float"
-    },
-    "mediaType": "as:mediaType",
-    "published": {
-      "@id": "as:published",
-      "@type": "xsd:dateTime"
-    },
-    "radius": {
-      "@id": "as:radius",
-      "@type": "xsd:float"
-    },
-    "rel": "as:rel",
-    "startIndex": {
-      "@id": "as:startIndex",
-      "@type": "xsd:nonNegativeInteger"
-    },
-    "startTime": {
-      "@id": "as:startTime",
-      "@type": "xsd:dateTime"
-    },
-    "summary": "as:summary",
-    "summaryMap": {
-      "@id": "as:summary",
-      "@container": "@language"
-    },
-    "totalItems": {
-      "@id": "as:totalItems",
-      "@type": "xsd:nonNegativeInteger"
-    },
-    "units": "as:units",
-    "updated": {
-      "@id": "as:updated",
-      "@type": "xsd:dateTime"
-    },
-    "width": {
-      "@id": "as:width",
-      "@type": "xsd:nonNegativeInteger"
-    },
-    "describes": {
-      "@id": "as:describes",
-      "@type": "@id"
-    },
-    "formerType": {
-      "@id": "as:formerType",
-      "@type": "@id"
-    },
-    "deleted": {
-      "@id": "as:deleted",
-      "@type": "xsd:dateTime"
-    },
-    "inbox": {
-      "@id": "ldp:inbox",
-      "@type": "@id"
-    },
-    "outbox": {
-      "@id": "as:outbox",
-      "@type": "@id"
-    },
-    "following": {
-      "@id": "as:following",
-      "@type": "@id"
-    },
-    "followers": {
-      "@id": "as:followers",
-      "@type": "@id"
-    },
-    "streams": {
-      "@id": "as:streams",
-      "@type": "@id"
-    },
-    "preferredUsername": "as:preferredUsername",
-    "endpoints": {
-      "@id": "as:endpoints",
-      "@type": "@id"
-    },
-    "uploadMedia": {
-      "@id": "as:uploadMedia",
-      "@type": "@id"
-    },
-    "proxyUrl": {
-      "@id": "as:proxyUrl",
-      "@type": "@id"
-    },
-    "liked": {
-      "@id": "as:liked",
-      "@type": "@id"
-    },
-    "oauthAuthorizationEndpoint": {
-      "@id": "as:oauthAuthorizationEndpoint",
-      "@type": "@id"
-    },
-    "oauthTokenEndpoint": {
-      "@id": "as:oauthTokenEndpoint",
-      "@type": "@id"
-    },
-    "provideClientKey": {
-      "@id": "as:provideClientKey",
-      "@type": "@id"
-    },
-    "signClientKey": {
-      "@id": "as:signClientKey",
-      "@type": "@id"
-    },
-    "sharedInbox": {
-      "@id": "as:sharedInbox",
-      "@type": "@id"
-    },
-    "Public": {
-      "@id": "as:Public",
-      "@type": "@id"
-    },
-    "source": "as:source",
-    "likes": {
-      "@id": "as:likes",
-      "@type": "@id"
-    },
-    "shares": {
-      "@id": "as:shares",
-      "@type": "@id"
-    },
-    "alsoKnownAs": {
-      "@id": "as:alsoKnownAs",
-      "@type": "@id"
-    }
-  }
-};
-
-export const CONTEXTS: Record<string, any> = {
-  "https://w3id.org/identity/v1": id_v1,
-  "https://w3id.org/security/v1": security_v1,
-  "https://www.w3.org/ns/activitystreams": activitystreams,
-};
diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts
deleted file mode 100644
index a24ec43a69..0000000000
--- a/src/remote/activitypub/misc/get-note-html.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import * as mfm from 'mfm-js';
-import { fnNameList } from '@/mfm/fn-name-list';
-import { Note } from '@/models/entities/note';
-import { toHtml } from '../../../mfm/to-html';
-
-export default function(note: Note) {
-	let html = note.text ? toHtml(mfm.parse(note.text, { fnNameList }), JSON.parse(note.mentionedRemoteUsers)) : null;
-	if (html == null) html = '<p>.</p>';
-
-	return html;
-}
diff --git a/src/remote/activitypub/misc/html-to-mfm.ts b/src/remote/activitypub/misc/html-to-mfm.ts
deleted file mode 100644
index 5cca04df21..0000000000
--- a/src/remote/activitypub/misc/html-to-mfm.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { IObject } from '../type';
-import { extractApHashtagObjects } from '../models/tag';
-import { fromHtml } from '../../../mfm/from-html';
-
-export function htmlToMfm(html: string, tag?: IObject | IObject[]) {
-	const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
-
-	return fromHtml(html, hashtagNames);
-}
diff --git a/src/remote/activitypub/misc/ld-signature.ts b/src/remote/activitypub/misc/ld-signature.ts
deleted file mode 100644
index dec07ea81b..0000000000
--- a/src/remote/activitypub/misc/ld-signature.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import * as crypto from 'crypto';
-import * as jsonld from 'jsonld';
-import { CONTEXTS } from './contexts';
-import fetch from 'node-fetch';
-import { httpAgent, httpsAgent } from '@/misc/fetch';
-
-// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
-
-export class LdSignature {
-	public debug = false;
-	public preLoad = true;
-	public loderTimeout = 10 * 1000;
-
-	constructor() {
-	}
-
-	public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
-		const options = {
-			type: 'RsaSignature2017',
-			creator,
-			domain,
-			nonce: crypto.randomBytes(16).toString('hex'),
-			created: (created || new Date()).toISOString()
-		} as {
-			type: string;
-			creator: string;
-			domain: string;
-			nonce: string;
-			created: string;
-		};
-
-		if (!domain) {
-			delete options.domain;
-		}
-
-		const toBeSigned = await this.createVerifyData(data, options);
-
-		const signer = crypto.createSign('sha256');
-		signer.update(toBeSigned);
-		signer.end();
-
-		const signature = signer.sign(privateKey);
-
-		return {
-			...data,
-			signature: {
-				...options,
-				signatureValue: signature.toString('base64')
-			}
-		};
-	}
-
-	public async verifyRsaSignature2017(data: any, publicKey: string): Promise<boolean> {
-		const toBeSigned = await this.createVerifyData(data, data.signature);
-		const verifier = crypto.createVerify('sha256');
-		verifier.update(toBeSigned);
-		return verifier.verify(publicKey, data.signature.signatureValue, 'base64');
-	}
-
-	public async createVerifyData(data: any, options: any) {
-		const transformedOptions = {
-			...options,
-			'@context': 'https://w3id.org/identity/v1'
-		};
-		delete transformedOptions['type'];
-		delete transformedOptions['id'];
-		delete transformedOptions['signatureValue'];
-		const canonizedOptions = await this.normalize(transformedOptions);
-		const optionsHash = this.sha256(canonizedOptions);
-		const transformedData = { ...data };
-		delete transformedData['signature'];
-		const cannonidedData = await this.normalize(transformedData);
-		if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
-		const documentHash = this.sha256(cannonidedData);
-		const verifyData = `${optionsHash}${documentHash}`;
-		return verifyData;
-	}
-
-	public async normalize(data: any) {
-		const customLoader = this.getLoader();
-		return await jsonld.normalize(data, {
-			documentLoader: customLoader
-		});
-	}
-
-	private getLoader() {
-		return async (url: string): Promise<any> => {
-			if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`;
-
-			if (this.preLoad) {
-				if (url in CONTEXTS) {
-					if (this.debug) console.debug(`HIT: ${url}`);
-					return {
-						contextUrl: null,
-						document: CONTEXTS[url],
-						documentUrl: url
-					};
-				}
-			}
-
-			if (this.debug) console.debug(`MISS: ${url}`);
-			const document = await this.fetchDocument(url);
-			return {
-				contextUrl: null,
-				document: document,
-				documentUrl: url
-			};
-		};
-	}
-
-	private async fetchDocument(url: string) {
-		const json = await fetch(url, {
-			headers: {
-				Accept: 'application/ld+json, application/json',
-			},
-			timeout: this.loderTimeout,
-			agent: u => u.protocol == 'http:' ? httpAgent : httpsAgent,
-		}).then(res => {
-			if (!res.ok) {
-				throw `${res.status} ${res.statusText}`;
-			} else {
-				return res.json();
-			}
-		});
-
-		return json;
-	}
-
-	public sha256(data: string): string {
-		const hash = crypto.createHash('sha256');
-		hash.update(data);
-		return hash.digest('hex');
-	}
-}
diff --git a/src/remote/activitypub/models/icon.ts b/src/remote/activitypub/models/icon.ts
deleted file mode 100644
index 50794a937d..0000000000
--- a/src/remote/activitypub/models/icon.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export type IIcon = {
-	type: string;
-	mediaType?: string;
-	url?: string;
-};
diff --git a/src/remote/activitypub/models/identifier.ts b/src/remote/activitypub/models/identifier.ts
deleted file mode 100644
index f6c3bb8c88..0000000000
--- a/src/remote/activitypub/models/identifier.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export type IIdentifier = {
-	type: string;
-	name: string;
-	value: string;
-};
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
deleted file mode 100644
index d0a96e4313..0000000000
--- a/src/remote/activitypub/models/image.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import uploadFromUrl from '@/services/drive/upload-from-url';
-import { IRemoteUser } from '@/models/entities/user';
-import Resolver from '../resolver';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { apLogger } from '../logger';
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFiles } from '@/models/index';
-import { truncate } from '@/misc/truncate';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
-
-const logger = apLogger;
-
-/**
- * Imageを作成します。
- */
-export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
-	// 投稿者が凍結されていたらスキップ
-	if (actor.isSuspended) {
-		throw new Error('actor has been suspended');
-	}
-
-	const image = await new Resolver().resolve(value) as any;
-
-	if (image.url == null) {
-		throw new Error('invalid image: url not privided');
-	}
-
-	logger.info(`Creating the Image: ${image.url}`);
-
-	const instance = await fetchMeta();
-	const cache = instance.cacheRemoteFiles;
-
-	let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH));
-
-	if (file.isLink) {
-		// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
-		// URLを更新する
-		if (file.url !== image.url) {
-			await DriveFiles.update({ id: file.id }, {
-				url: image.url,
-				uri: image.url
-			});
-
-			file = await DriveFiles.findOneOrFail(file.id);
-		}
-	}
-
-	return file;
-}
-
-/**
- * Imageを解決します。
- *
- * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
- */
-export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
-	// TODO
-
-	// リモートサーバーからフェッチしてきて登録
-	return await createImage(actor, value);
-}
diff --git a/src/remote/activitypub/models/mention.ts b/src/remote/activitypub/models/mention.ts
deleted file mode 100644
index ade9c90806..0000000000
--- a/src/remote/activitypub/models/mention.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { toArray, unique } from '@/prelude/array';
-import { IObject, isMention, IApMention } from '../type';
-import { resolvePerson } from './person';
-import * as promiseLimit from 'promise-limit';
-import Resolver from '../resolver';
-import { User } from '@/models/entities/user';
-
-export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
-	const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
-
-	const resolver = new Resolver();
-
-	const limit = promiseLimit<User | null>(2);
-	const mentionedUsers = (await Promise.all(
-		hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
-	)).filter((x): x is User => x != null);
-
-	return mentionedUsers;
-}
-
-export function extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
-	if (tags == null) return [];
-	return toArray(tags).filter(isMention);
-}
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
deleted file mode 100644
index 492dc05248..0000000000
--- a/src/remote/activitypub/models/note.ts
+++ /dev/null
@@ -1,356 +0,0 @@
-import * as promiseLimit from 'promise-limit';
-
-import config from '@/config/index';
-import Resolver from '../resolver';
-import post from '@/services/note/create';
-import { resolvePerson, updatePerson } from './person';
-import { resolveImage } from './image';
-import { IRemoteUser } from '@/models/entities/user';
-import { htmlToMfm } from '../misc/html-to-mfm';
-import { extractApHashtags } from './tag';
-import { unique, toArray, toSingle } from '@/prelude/array';
-import { extractPollFromQuestion } from './question';
-import vote from '@/services/note/polls/vote';
-import { apLogger } from '../logger';
-import { DriveFile } from '@/models/entities/drive-file';
-import { deliverQuestionUpdate } from '@/services/note/polls/update';
-import { extractDbHost, toPuny } from '@/misc/convert-host';
-import { Emojis, Polls, MessagingMessages } from '@/models/index';
-import { Note } from '@/models/entities/note';
-import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type';
-import { Emoji } from '@/models/entities/emoji';
-import { genId } from '@/misc/gen-id';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { getApLock } from '@/misc/app-lock';
-import { createMessage } from '@/services/messages/create';
-import { parseAudience } from '../audience';
-import { extractApMentions } from './mention';
-import DbResolver from '../db-resolver';
-import { StatusError } from '@/misc/fetch';
-
-const logger = apLogger;
-
-export function validateNote(object: any, uri: string) {
-	const expectHost = extractDbHost(uri);
-
-	if (object == null) {
-		return new Error('invalid Note: object is null');
-	}
-
-	if (!validPost.includes(getApType(object))) {
-		return new Error(`invalid Note: invalid object type ${getApType(object)}`);
-	}
-
-	if (object.id && extractDbHost(object.id) !== expectHost) {
-		return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractDbHost(object.id)}`);
-	}
-
-	if (object.attributedTo && extractDbHost(getOneApId(object.attributedTo)) !== expectHost) {
-		return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractDbHost(object.attributedTo)}`);
-	}
-
-	return null;
-}
-
-/**
- * Noteをフェッチします。
- *
- * Misskeyに対象のNoteが登録されていればそれを返します。
- */
-export async function fetchNote(object: string | IObject): Promise<Note | null> {
-	const dbResolver = new DbResolver();
-	return await dbResolver.getNoteFromApId(object);
-}
-
-/**
- * Noteを作成します。
- */
-export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
-	if (resolver == null) resolver = new Resolver();
-
-	const object: any = await resolver.resolve(value);
-
-	const entryUri = getApId(value);
-	const err = validateNote(object, entryUri);
-	if (err) {
-		logger.error(`${err.message}`, {
-			resolver: {
-				history: resolver.getHistory()
-			},
-			value: value,
-			object: object
-		});
-		throw new Error('invalid note');
-	}
-
-	const note: IPost = object;
-
-	logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
-
-	logger.info(`Creating the Note: ${note.id}`);
-
-	// 投稿者をフェッチ
-	const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser;
-
-	// 投稿者が凍結されていたらスキップ
-	if (actor.isSuspended) {
-		throw new Error('actor has been suspended');
-	}
-
-	const noteAudience = await parseAudience(actor, note.to, note.cc);
-	let visibility = noteAudience.visibility;
-	const visibleUsers = noteAudience.visibleUsers;
-
-	// Audience (to, cc) が指定されてなかった場合
-	if (visibility === 'specified' && visibleUsers.length === 0) {
-		if (typeof value === 'string') {	// 入力がstringならばresolverでGETが発生している
-			// こちらから匿名GET出来たものならばpublic
-			visibility = 'public';
-		}
-	}
-
-	let isTalk = note._misskey_talk && visibility === 'specified';
-
-	const apMentions = await extractApMentions(note.tag);
-	const apHashtags = await extractApHashtags(note.tag);
-
-	// 添付ファイル
-	// TODO: attachmentは必ずしもImageではない
-	// TODO: attachmentは必ずしも配列ではない
-	// Noteがsensitiveなら添付もsensitiveにする
-	const limit = promiseLimit(2);
-
-	note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
-	const files = note.attachment
-		.map(attach => attach.sensitive = note.sensitive)
-		? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>)))
-			.filter(image => image != null)
-		: [];
-
-	// リプライ
-	const reply: Note | null = note.inReplyTo
-		? await resolveNote(note.inReplyTo, resolver).then(x => {
-			if (x == null) {
-				logger.warn(`Specified inReplyTo, but nout found`);
-				throw new Error('inReplyTo not found');
-			} else {
-				return x;
-			}
-		}).catch(async e => {
-			// トークだったらinReplyToのエラーは無視
-			const uri = getApId(note.inReplyTo);
-			if (uri.startsWith(config.url + '/')) {
-				const id = uri.split('/').pop();
-				const talk = await MessagingMessages.findOne(id);
-				if (talk) {
-					isTalk = true;
-					return null;
-				}
-			}
-
-			logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
-			throw e;
-		})
-		: null;
-
-	// 引用
-	let quote: Note | undefined | null;
-
-	if (note._misskey_quote || note.quoteUrl) {
-		const tryResolveNote = async (uri: string): Promise<{
-			status: 'ok';
-			res: Note | null;
-		} | {
-			status: 'permerror' | 'temperror';
-		}> => {
-			if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
-			try {
-				const res = await resolveNote(uri);
-				if (res) {
-					return {
-						status: 'ok',
-						res
-					};
-				} else {
-					return {
-						status: 'permerror'
-					};
-				}
-			} catch (e) {
-				return {
-					status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror'
-				};
-			}
-		};
-
-		const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
-		const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
-
-		quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
-		if (!quote) {
-			if (results.some(x => x.status === 'temperror')) {
-				throw 'quote resolve failed';
-			}
-		}
-	}
-
-	const cw = note.summary === '' ? null : note.summary;
-
-	// テキストのパース
-	const text = note._misskey_content || (note.content ? htmlToMfm(note.content, note.tag) : null);
-
-	// vote
-	if (reply && reply.hasPoll) {
-		const poll = await Polls.findOneOrFail(reply.id);
-
-		const tryCreateVote = async (name: string, index: number): Promise<null> => {
-			if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
-				logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
-			} else if (index >= 0) {
-				logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
-				await vote(actor, reply, index);
-
-				// リモートフォロワーにUpdate配信
-				deliverQuestionUpdate(reply.id);
-			}
-			return null;
-		};
-
-		if (note.name) {
-			return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
-		}
-	}
-
-	const emojis = await extractEmojis(note.tag || [], actor.host).catch(e => {
-		logger.info(`extractEmojis: ${e}`);
-		return [] as Emoji[];
-	});
-
-	const apEmojis = emojis.map(emoji => emoji.name);
-
-	const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined);
-
-	// ユーザーの情報が古かったらついでに更新しておく
-	if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
-		if (actor.uri) updatePerson(actor.uri);
-	}
-
-	if (isTalk) {
-		for (const recipient of visibleUsers) {
-			await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id);
-			return null;
-		}
-	}
-
-	return await post(actor, {
-		createdAt: note.published ? new Date(note.published) : null,
-		files,
-		reply,
-		renote: quote,
-		name: note.name,
-		cw,
-		text,
-		viaMobile: false,
-		localOnly: false,
-		visibility,
-		visibleUsers,
-		apMentions,
-		apHashtags,
-		apEmojis,
-		poll,
-		uri: note.id,
-		url: getOneApHrefNullable(note.url),
-	}, silent);
-}
-
-/**
- * Noteを解決します。
- *
- * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
- */
-export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
-	const uri = typeof value === 'string' ? value : value.id;
-	if (uri == null) throw new Error('missing uri');
-
-	// ブロックしてたら中断
-	const meta = await fetchMeta();
-	if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 };
-
-	const unlock = await getApLock(uri);
-
-	try {
-		//#region このサーバーに既に登録されていたらそれを返す
-		const exist = await fetchNote(uri);
-
-		if (exist) {
-			return exist;
-		}
-		//#endregion
-
-		if (uri.startsWith(config.url)) {
-			throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
-		}
-
-		// リモートサーバーからフェッチしてきて登録
-		// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
-		// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
-		return await createNote(uri, resolver, true);
-	} finally {
-		unlock();
-	}
-}
-
-export async function extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
-	host = toPuny(host);
-
-	if (!tags) return [];
-
-	const eomjiTags = toArray(tags).filter(isEmoji);
-
-	return await Promise.all(eomjiTags.map(async tag => {
-		const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
-		tag.icon = toSingle(tag.icon);
-
-		const exists = await Emojis.findOne({
-			host,
-			name
-		});
-
-		if (exists) {
-			if ((tag.updated != null && exists.updatedAt == null)
-				|| (tag.id != null && exists.uri == null)
-				|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
-				|| (tag.icon!.url !== exists.url)
-			) {
-				await Emojis.update({
-					host,
-					name,
-				}, {
-					uri: tag.id,
-					url: tag.icon!.url,
-					updatedAt: new Date(),
-				});
-
-				return await Emojis.findOne({
-					host,
-					name
-				}) as Emoji;
-			}
-
-			return exists;
-		}
-
-		logger.info(`register emoji host=${host}, name=${name}`);
-
-		return await Emojis.save({
-			id: genId(),
-			host,
-			name,
-			uri: tag.id,
-			url: tag.icon!.url,
-			updatedAt: new Date(),
-			aliases: []
-		} as Partial<Emoji>);
-	}));
-}
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
deleted file mode 100644
index eb8c00a10b..0000000000
--- a/src/remote/activitypub/models/person.ts
+++ /dev/null
@@ -1,494 +0,0 @@
-import { URL } from 'url';
-import * as promiseLimit from 'promise-limit';
-
-import $, { Context } from 'cafy';
-import config from '@/config/index';
-import Resolver from '../resolver';
-import { resolveImage } from './image';
-import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type';
-import { fromHtml } from '../../../mfm/from-html';
-import { htmlToMfm } from '../misc/html-to-mfm';
-import { resolveNote, extractEmojis } from './note';
-import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc';
-import { extractApHashtags } from './tag';
-import { apLogger } from '../logger';
-import { Note } from '@/models/entities/note';
-import { updateUsertags } from '@/services/update-hashtag';
-import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index';
-import { User, IRemoteUser } from '@/models/entities/user';
-import { Emoji } from '@/models/entities/emoji';
-import { UserNotePining } from '@/models/entities/user-note-pining';
-import { genId } from '@/misc/gen-id';
-import { instanceChart, usersChart } from '@/services/chart/index';
-import { UserPublickey } from '@/models/entities/user-publickey';
-import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
-import { toPuny } from '@/misc/convert-host';
-import { UserProfile } from '@/models/entities/user-profile';
-import { getConnection } from 'typeorm';
-import { toArray } from '@/prelude/array';
-import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-import { truncate } from '@/misc/truncate';
-import { StatusError } from '@/misc/fetch';
-
-const logger = apLogger;
-
-const nameLength = 128;
-const summaryLength = 2048;
-
-/**
- * Validate and convert to actor object
- * @param x Fetched object
- * @param uri Fetch target URI
- */
-function validateActor(x: IObject, uri: string): IActor {
-	const expectHost = toPuny(new URL(uri).hostname);
-
-	if (x == null) {
-		throw new Error('invalid Actor: object is null');
-	}
-
-	if (!isActor(x)) {
-		throw new Error(`invalid Actor type '${x.type}'`);
-	}
-
-	const validate = (name: string, value: any, validater: Context) => {
-		const e = validater.test(value);
-		if (e) throw new Error(`invalid Actor: ${name} ${e.message}`);
-	};
-
-	validate('id', x.id, $.str.min(1));
-	validate('inbox', x.inbox, $.str.min(1));
-	validate('preferredUsername', x.preferredUsername, $.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/));
-
-	// These fields are only informational, and some AP software allows these
-	// fields to be very long. If they are too long, we cut them off. This way
-	// we can at least see these users and their activities.
-	validate('name', truncate(x.name, nameLength), $.optional.nullable.str);
-	validate('summary', truncate(x.summary, summaryLength), $.optional.nullable.str);
-
-	const idHost = toPuny(new URL(x.id!).hostname);
-	if (idHost !== expectHost) {
-		throw new Error('invalid Actor: id has different host');
-	}
-
-	if (x.publicKey) {
-		if (typeof x.publicKey.id !== 'string') {
-			throw new Error('invalid Actor: publicKey.id is not a string');
-		}
-
-		const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname);
-		if (publicKeyIdHost !== expectHost) {
-			throw new Error('invalid Actor: publicKey.id has different host');
-		}
-	}
-
-	return x;
-}
-
-/**
- * Personをフェッチします。
- *
- * Misskeyに対象のPersonが登録されていればそれを返します。
- */
-export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
-	if (typeof uri !== 'string') throw new Error('uri is not string');
-
-	// URIがこのサーバーを指しているならデータベースからフェッチ
-	if (uri.startsWith(config.url + '/')) {
-		const id = uri.split('/').pop();
-		return await Users.findOne(id).then(x => x || null);
-	}
-
-	//#region このサーバーに既に登録されていたらそれを返す
-	const exist = await Users.findOne({ uri });
-
-	if (exist) {
-		return exist;
-	}
-	//#endregion
-
-	return null;
-}
-
-/**
- * Personを作成します。
- */
-export async function createPerson(uri: string, resolver?: Resolver): Promise<User> {
-	if (typeof uri !== 'string') throw new Error('uri is not string');
-
-	if (uri.startsWith(config.url)) {
-		throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
-	}
-
-	if (resolver == null) resolver = new Resolver();
-
-	const object = await resolver.resolve(uri) as any;
-
-	const person = validateActor(object, uri);
-
-	logger.info(`Creating the Person: ${person.id}`);
-
-	const host = toPuny(new URL(object.id).hostname);
-
-	const { fields } = analyzeAttachments(person.attachment || []);
-
-	const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
-
-	const isBot = getApType(object) === 'Service';
-
-	const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
-
-	// Create user
-	let user: IRemoteUser;
-	try {
-		// Start transaction
-		await getConnection().transaction(async transactionalEntityManager => {
-			user = await transactionalEntityManager.save(new User({
-				id: genId(),
-				avatarId: null,
-				bannerId: null,
-				createdAt: new Date(),
-				lastFetchedAt: new Date(),
-				name: truncate(person.name, nameLength),
-				isLocked: !!person.manuallyApprovesFollowers,
-				isExplorable: !!person.discoverable,
-				username: person.preferredUsername,
-				usernameLower: person.preferredUsername!.toLowerCase(),
-				host,
-				inbox: person.inbox,
-				sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
-				followersUri: person.followers ? getApId(person.followers) : undefined,
-				featured: person.featured ? getApId(person.featured) : undefined,
-				uri: person.id,
-				tags,
-				isBot,
-				isCat: (person as any).isCat === true
-			})) as IRemoteUser;
-
-			await transactionalEntityManager.save(new UserProfile({
-				userId: user.id,
-				description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
-				url: getOneApHrefNullable(person.url),
-				fields,
-				birthday: bday ? bday[0] : null,
-				location: person['vcard:Address'] || null,
-				userHost: host
-			}));
-
-			if (person.publicKey) {
-				await transactionalEntityManager.save(new UserPublickey({
-					userId: user.id,
-					keyId: person.publicKey.id,
-					keyPem: person.publicKey.publicKeyPem
-				}));
-			}
-		});
-	} catch (e) {
-		// duplicate key error
-		if (isDuplicateKeyValueError(e)) {
-			// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
-			const u = await Users.findOne({
-				uri: person.id
-			});
-
-			if (u) {
-				user = u as IRemoteUser;
-			} else {
-				throw new Error('already registered');
-			}
-		} else {
-			logger.error(e);
-			throw e;
-		}
-	}
-
-	// Register host
-	registerOrFetchInstanceDoc(host).then(i => {
-		Instances.increment({ id: i.id }, 'usersCount', 1);
-		instanceChart.newUser(i.host);
-		fetchInstanceMetadata(i);
-	});
-
-	usersChart.update(user!, true);
-
-	// ハッシュタグ更新
-	updateUsertags(user!, tags);
-
-	//#region アバターとヘッダー画像をフェッチ
-	const [avatar, banner] = await Promise.all([
-		person.icon,
-		person.image
-	].map(img =>
-		img == null
-			? Promise.resolve(null)
-			: resolveImage(user!, img).catch(() => null)
-	));
-
-	const avatarId = avatar ? avatar.id : null;
-	const bannerId = banner ? banner.id : null;
-	const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar, true) : null;
-	const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
-	const avatarBlurhash = avatar ? avatar.blurhash : null;
-	const bannerBlurhash = banner ? banner.blurhash : null;
-
-	await Users.update(user!.id, {
-		avatarId,
-		bannerId,
-		avatarUrl,
-		bannerUrl,
-		avatarBlurhash,
-		bannerBlurhash
-	});
-
-	user!.avatarId = avatarId;
-	user!.bannerId = bannerId;
-	user!.avatarUrl = avatarUrl;
-	user!.bannerUrl = bannerUrl;
-	user!.avatarBlurhash = avatarBlurhash;
-	user!.bannerBlurhash = bannerBlurhash;
-	//#endregion
-
-	//#region カスタム絵文字取得
-	const emojis = await extractEmojis(person.tag || [], host).catch(e => {
-		logger.info(`extractEmojis: ${e}`);
-		return [] as Emoji[];
-	});
-
-	const emojiNames = emojis.map(emoji => emoji.name);
-
-	await Users.update(user!.id, {
-		emojis: emojiNames
-	});
-	//#endregion
-
-	await updateFeatured(user!.id).catch(err => logger.error(err));
-
-	return user!;
-}
-
-/**
- * Personの情報を更新します。
- * Misskeyに対象のPersonが登録されていなければ無視します。
- * @param uri URI of Person
- * @param resolver Resolver
- * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
- */
-export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: object): Promise<void> {
-	if (typeof uri !== 'string') throw new Error('uri is not string');
-
-	// URIがこのサーバーを指しているならスキップ
-	if (uri.startsWith(config.url + '/')) {
-		return;
-	}
-
-	//#region このサーバーに既に登録されているか
-	const exist = await Users.findOne({ uri }) as IRemoteUser;
-
-	if (exist == null) {
-		return;
-	}
-	//#endregion
-
-	if (resolver == null) resolver = new Resolver();
-
-	const object = hint || await resolver.resolve(uri) as any;
-
-	const person = validateActor(object, uri);
-
-	logger.info(`Updating the Person: ${person.id}`);
-
-	// アバターとヘッダー画像をフェッチ
-	const [avatar, banner] = await Promise.all([
-		person.icon,
-		person.image
-	].map(img =>
-		img == null
-			? Promise.resolve(null)
-			: resolveImage(exist, img).catch(() => null)
-	));
-
-	// カスタム絵文字取得
-	const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => {
-		logger.info(`extractEmojis: ${e}`);
-		return [] as Emoji[];
-	});
-
-	const emojiNames = emojis.map(emoji => emoji.name);
-
-	const { fields } = analyzeAttachments(person.attachment || []);
-
-	const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
-
-	const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
-
-	const updates = {
-		lastFetchedAt: new Date(),
-		inbox: person.inbox,
-		sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
-		followersUri: person.followers ? getApId(person.followers) : undefined,
-		featured: person.featured,
-		emojis: emojiNames,
-		name: truncate(person.name, nameLength),
-		tags,
-		isBot: getApType(object) === 'Service',
-		isCat: (person as any).isCat === true,
-		isLocked: !!person.manuallyApprovesFollowers,
-		isExplorable: !!person.discoverable,
-	} as Partial<User>;
-
-	if (avatar) {
-		updates.avatarId = avatar.id;
-		updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
-		updates.avatarBlurhash = avatar.blurhash;
-	}
-
-	if (banner) {
-		updates.bannerId = banner.id;
-		updates.bannerUrl = DriveFiles.getPublicUrl(banner);
-		updates.bannerBlurhash = banner.blurhash;
-	}
-
-	// Update user
-	await Users.update(exist.id, updates);
-
-	if (person.publicKey) {
-		await UserPublickeys.update({ userId: exist.id }, {
-			keyId: person.publicKey.id,
-			keyPem: person.publicKey.publicKeyPem
-		});
-	}
-
-	await UserProfiles.update({ userId: exist.id }, {
-		url: getOneApHrefNullable(person.url),
-		fields,
-		description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
-		birthday: bday ? bday[0] : null,
-		location: person['vcard:Address'] || null,
-	});
-
-	// ハッシュタグ更新
-	updateUsertags(exist, tags);
-
-	// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
-	await Followings.update({
-		followerId: exist.id
-	}, {
-		followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
-	});
-
-	await updateFeatured(exist.id).catch(err => logger.error(err));
-}
-
-/**
- * Personを解決します。
- *
- * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
- */
-export async function resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
-	if (typeof uri !== 'string') throw new Error('uri is not string');
-
-	//#region このサーバーに既に登録されていたらそれを返す
-	const exist = await fetchPerson(uri);
-
-	if (exist) {
-		return exist;
-	}
-	//#endregion
-
-	// リモートサーバーからフェッチしてきて登録
-	if (resolver == null) resolver = new Resolver();
-	return await createPerson(uri, resolver);
-}
-
-const services: {
-		[x: string]: (id: string, username: string) => any
-	} = {
-	'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
-	'misskey:authentication:github': (id, login) => ({ id, login }),
-	'misskey:authentication:discord': (id, name) => $discord(id, name)
-};
-
-const $discord = (id: string, name: string) => {
-	if (typeof name !== 'string')
-		name = 'unknown#0000';
-	const [username, discriminator] = name.split('#');
-	return { id, username, discriminator };
-};
-
-function addService(target: { [x: string]: any }, source: IApPropertyValue) {
-	const service = services[source.name];
-
-	if (typeof source.value !== 'string')
-		source.value = 'unknown';
-
-	const [id, username] = source.value.split('@');
-
-	if (service)
-		target[source.name.split(':')[2]] = service(id, username);
-}
-
-export function analyzeAttachments(attachments: IObject | IObject[] | undefined) {
-	const fields: {
-		name: string,
-		value: string
-	}[] = [];
-	const services: { [x: string]: any } = {};
-
-	if (Array.isArray(attachments)) {
-		for (const attachment of attachments.filter(isPropertyValue)) {
-			if (isPropertyValue(attachment.identifier)) {
-				addService(services, attachment.identifier);
-			} else {
-				fields.push({
-					name: attachment.name,
-					value: fromHtml(attachment.value)
-				});
-			}
-		}
-	}
-
-	return { fields, services };
-}
-
-export async function updateFeatured(userId: User['id']) {
-	const user = await Users.findOneOrFail(userId);
-	if (!Users.isRemoteUser(user)) return;
-	if (!user.featured) return;
-
-	logger.info(`Updating the featured: ${user.uri}`);
-
-	const resolver = new Resolver();
-
-	// Resolve to (Ordered)Collection Object
-	const collection = await resolver.resolveCollection(user.featured);
-	if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`);
-
-	// Resolve to Object(may be Note) arrays
-	const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
-	const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x)));
-
-	// Resolve and regist Notes
-	const limit = promiseLimit<Note | null>(2);
-	const featuredNotes = await Promise.all(items
-		.filter(item => getApType(item) === 'Note')	// TODO: Noteでなくてもいいかも
-		.slice(0, 5)
-		.map(item => limit(() => resolveNote(item, resolver))));
-
-	await getConnection().transaction(async transactionalEntityManager => {
-		await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
-
-		// とりあえずidを別の時間で生成して順番を維持
-		let td = 0;
-		for (const note of featuredNotes.filter(note => note != null)) {
-			td -= 1000;
-			transactionalEntityManager.insert(UserNotePining, {
-				id: genId(new Date(Date.now() + td)),
-				createdAt: new Date(),
-				userId: user.id,
-				noteId: note!.id
-			});
-		}
-	});
-}
diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts
deleted file mode 100644
index 79f93c3a30..0000000000
--- a/src/remote/activitypub/models/question.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import config from '@/config/index';
-import Resolver from '../resolver';
-import { IObject, IQuestion, isQuestion,  } from '../type';
-import { apLogger } from '../logger';
-import { Notes, Polls } from '@/models/index';
-import { IPoll } from '@/models/entities/poll';
-
-export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
-	if (resolver == null) resolver = new Resolver();
-
-	const question = await resolver.resolve(source);
-
-	if (!isQuestion(question)) {
-		throw new Error('invalid type');
-	}
-
-	const multiple = !question.oneOf;
-	const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
-
-	if (multiple && !question.anyOf) {
-		throw new Error('invalid question');
-	}
-
-	const choices = question[multiple ? 'anyOf' : 'oneOf']!
-		.map((x, i) => x.name!);
-
-	const votes = question[multiple ? 'anyOf' : 'oneOf']!
-		.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
-
-	return {
-		choices,
-		votes,
-		multiple,
-		expiresAt
-	};
-}
-
-/**
- * Update votes of Question
- * @param uri URI of AP Question object
- * @returns true if updated
- */
-export async function updateQuestion(value: any) {
-	const uri = typeof value === 'string' ? value : value.id;
-
-	// URIがこのサーバーを指しているならスキップ
-	if (uri.startsWith(config.url + '/')) throw new Error('uri points local');
-
-	//#region このサーバーに既に登録されているか
-	const note = await Notes.findOne({ uri });
-	if (note == null) throw new Error('Question is not registed');
-
-	const poll = await Polls.findOne({ noteId: note.id });
-	if (poll == null) throw new Error('Question is not registed');
-	//#endregion
-
-	// resolve new Question object
-	const resolver = new Resolver();
-	const question = await resolver.resolve(value) as IQuestion;
-	apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
-
-	if (question.type !== 'Question') throw new Error('object is not a Question');
-
-	const apChoices = question.oneOf || question.anyOf;
-
-	let changed = false;
-
-	for (const choice of poll.choices) {
-		const oldCount = poll.votes[poll.choices.indexOf(choice)];
-		const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
-
-		if (oldCount != newCount) {
-			changed = true;
-			poll.votes[poll.choices.indexOf(choice)] = newCount;
-		}
-	}
-
-	await Polls.update({ noteId: note.id }, {
-		votes: poll.votes
-	});
-
-	return changed;
-}
diff --git a/src/remote/activitypub/models/tag.ts b/src/remote/activitypub/models/tag.ts
deleted file mode 100644
index fbc6b9b428..0000000000
--- a/src/remote/activitypub/models/tag.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { toArray } from '@/prelude/array';
-import { IObject, isHashtag, IApHashtag } from '../type';
-
-export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
-	if (tags == null) return [];
-
-	const hashtags = extractApHashtagObjects(tags);
-
-	return hashtags.map(tag => {
-		const m = tag.name.match(/^#(.+)/);
-		return m ? m[1] : null;
-	}).filter((x): x is string => x != null);
-}
-
-export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
-	if (tags == null) return [];
-	return toArray(tags).filter(isHashtag);
-}
diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts
deleted file mode 100644
index 01f0e3676e..0000000000
--- a/src/remote/activitypub/perform.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { IObject } from './type';
-import { IRemoteUser } from '@/models/entities/user';
-import { performActivity } from './kernel/index';
-
-export default async (actor: IRemoteUser, activity: IObject): Promise<void> => {
-	await performActivity(actor, activity);
-};
diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts
deleted file mode 100644
index f1e61f4c6a..0000000000
--- a/src/remote/activitypub/renderer/accept.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-
-export default (object: any, user: { id: User['id']; host: null }) => ({
-	type: 'Accept',
-	actor: `${config.url}/users/${user.id}`,
-	object
-});
diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts
deleted file mode 100644
index 21414a9380..0000000000
--- a/src/remote/activitypub/renderer/add.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import config from '@/config/index';
-import { ILocalUser } from '@/models/entities/user';
-
-export default (user: ILocalUser, target: any, object: any) => ({
-	type: 'Add',
-	actor: `${config.url}/users/${user.id}`,
-	target,
-	object
-});
diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts
deleted file mode 100644
index 7bf90922be..0000000000
--- a/src/remote/activitypub/renderer/announce.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import config from '@/config/index';
-import { Note } from '@/models/entities/note';
-
-export default (object: any, note: Note) => {
-	const attributedTo = `${config.url}/users/${note.userId}`;
-
-	let to: string[] = [];
-	let cc: string[] = [];
-
-	if (note.visibility === 'public') {
-		to = ['https://www.w3.org/ns/activitystreams#Public'];
-		cc = [`${attributedTo}/followers`];
-	} else if (note.visibility === 'home') {
-		to = [`${attributedTo}/followers`];
-		cc = ['https://www.w3.org/ns/activitystreams#Public'];
-	} else {
-		return null;
-	}
-
-	return {
-		id: `${config.url}/notes/${note.id}/activity`,
-		actor: `${config.url}/users/${note.userId}`,
-		type: 'Announce',
-		published: note.createdAt.toISOString(),
-		to,
-		cc,
-		object
-	};
-};
diff --git a/src/remote/activitypub/renderer/block.ts b/src/remote/activitypub/renderer/block.ts
deleted file mode 100644
index bb3d74295a..0000000000
--- a/src/remote/activitypub/renderer/block.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import config from '@/config/index';
-import { ILocalUser, IRemoteUser } from '@/models/entities/user';
-
-export default (blocker: ILocalUser, blockee: IRemoteUser) => ({
-	type: 'Block',
-	actor: `${config.url}/users/${blocker.id}`,
-	object: blockee.uri
-});
diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts
deleted file mode 100644
index 04aa993a91..0000000000
--- a/src/remote/activitypub/renderer/create.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import config from '@/config/index';
-import { Note } from '@/models/entities/note';
-
-export default (object: any, note: Note) => {
-	const activity = {
-		id: `${config.url}/notes/${note.id}/activity`,
-		actor: `${config.url}/users/${note.userId}`,
-		type: 'Create',
-		published: note.createdAt.toISOString(),
-		object
-	} as any;
-
-	if (object.to) activity.to = object.to;
-	if (object.cc) activity.cc = object.cc;
-
-	return activity;
-};
diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts
deleted file mode 100644
index 176a6f7e27..0000000000
--- a/src/remote/activitypub/renderer/delete.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-
-export default (object: any, user: { id: User['id']; host: null }) => ({
-	type: 'Delete',
-	actor: `${config.url}/users/${user.id}`,
-	object,
-	published: new Date().toISOString(),
-});
diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts
deleted file mode 100644
index a9d86dea15..0000000000
--- a/src/remote/activitypub/renderer/document.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFiles } from '@/models/index';
-
-export default (file: DriveFile) => ({
-	type: 'Document',
-	mediaType: file.type,
-	url: DriveFiles.getPublicUrl(file),
-	name: file.comment,
-});
diff --git a/src/remote/activitypub/renderer/emoji.ts b/src/remote/activitypub/renderer/emoji.ts
deleted file mode 100644
index ca514c56b5..0000000000
--- a/src/remote/activitypub/renderer/emoji.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import config from '@/config/index';
-import { Emoji } from '@/models/entities/emoji';
-
-export default (emoji: Emoji) => ({
-	id: `${config.url}/emojis/${emoji.name}`,
-	type: 'Emoji',
-	name: `:${emoji.name}:`,
-	updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString,
-	icon: {
-		type: 'Image',
-		mediaType: emoji.type || 'image/png',
-		url: emoji.url
-	}
-});
diff --git a/src/remote/activitypub/renderer/follow-relay.ts b/src/remote/activitypub/renderer/follow-relay.ts
deleted file mode 100644
index 984c3c7639..0000000000
--- a/src/remote/activitypub/renderer/follow-relay.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import config from '@/config/index';
-import { Relay } from '@/models/entities/relay';
-import { ILocalUser } from '@/models/entities/user';
-
-export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) {
-	const follow = {
-		id: `${config.url}/activities/follow-relay/${relay.id}`,
-		type: 'Follow',
-		actor: `${config.url}/users/${relayActor.id}`,
-		object: 'https://www.w3.org/ns/activitystreams#Public'
-	};
-
-	return follow;
-}
diff --git a/src/remote/activitypub/renderer/follow-user.ts b/src/remote/activitypub/renderer/follow-user.ts
deleted file mode 100644
index e3dde7f7fe..0000000000
--- a/src/remote/activitypub/renderer/follow-user.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import config from '@/config/index';
-import { Users } from '@/models/index';
-import { User } from '@/models/entities/user';
-
-/**
- * Convert (local|remote)(Follower|Followee)ID to URL
- * @param id Follower|Followee ID
- */
-export default async function renderFollowUser(id: User['id']): Promise<any> {
-	const user = await Users.findOneOrFail(id);
-	return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri;
-}
diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
deleted file mode 100644
index c8a7946799..0000000000
--- a/src/remote/activitypub/renderer/follow.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-import { Users } from '@/models/index';
-
-export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => {
-	const follow = {
-		type: 'Follow',
-		actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
-		object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri
-	} as any;
-
-	if (requestId) follow.id = requestId;
-
-	return follow;
-};
diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts
deleted file mode 100644
index 290c74c7fe..0000000000
--- a/src/remote/activitypub/renderer/hashtag.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import config from '@/config/index';
-
-export default (tag: string) => ({
-	type: 'Hashtag',
-	href: `${config.url}/tags/${encodeURIComponent(tag)}`,
-	name: `#${tag}`
-});
diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts
deleted file mode 100644
index 0cb3d6ed65..0000000000
--- a/src/remote/activitypub/renderer/image.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFiles } from '@/models/index';
-
-export default (file: DriveFile) => ({
-	type: 'Image',
-	url: DriveFiles.getPublicUrl(file),
-	sensitive: file.isSensitive,
-	name: file.comment
-});
diff --git a/src/remote/activitypub/renderer/index.ts b/src/remote/activitypub/renderer/index.ts
deleted file mode 100644
index f6ec6583d0..0000000000
--- a/src/remote/activitypub/renderer/index.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import config from '@/config/index';
-import { v4 as uuid } from 'uuid';
-import { IActivity } from '../type';
-import { LdSignature } from '../misc/ld-signature';
-import { getUserKeypair } from '@/misc/keypair-store';
-import { User } from '@/models/entities/user';
-
-export const renderActivity = (x: any): IActivity | null => {
-	if (x == null) return null;
-
-	if (x !== null && typeof x === 'object' && x.id == null) {
-		x.id = `${config.url}/${uuid()}`;
-	}
-
-	return Object.assign({
-		'@context': [
-			'https://www.w3.org/ns/activitystreams',
-			'https://w3id.org/security/v1',
-			{
-				// as non-standards
-				manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
-				sensitive: 'as:sensitive',
-				Hashtag: 'as:Hashtag',
-				quoteUrl: 'as:quoteUrl',
-				// Mastodon
-				toot: 'http://joinmastodon.org/ns#',
-				Emoji: 'toot:Emoji',
-				featured: 'toot:featured',
-				discoverable: 'toot:discoverable',
-				// schema
-				schema: 'http://schema.org#',
-				PropertyValue: 'schema:PropertyValue',
-				value: 'schema:value',
-				// Misskey
-				misskey: `${config.url}/ns#`,
-				'_misskey_content': 'misskey:_misskey_content',
-				'_misskey_quote': 'misskey:_misskey_quote',
-				'_misskey_reaction': 'misskey:_misskey_reaction',
-				'_misskey_votes': 'misskey:_misskey_votes',
-				'_misskey_talk': 'misskey:_misskey_talk',
-				'isCat': 'misskey:isCat',
-				// vcard
-				vcard: 'http://www.w3.org/2006/vcard/ns#',
-			}
-		]
-	}, x);
-};
-
-export const attachLdSignature = async (activity: any, user: { id: User['id']; host: null; }): Promise<IActivity | null> => {
-	if (activity == null) return null;
-
-	const keypair = await getUserKeypair(user.id);
-
-	const ldSignature = new LdSignature();
-	ldSignature.debug = false;
-	activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`);
-
-	return activity;
-};
diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts
deleted file mode 100644
index 992f98d79a..0000000000
--- a/src/remote/activitypub/renderer/key.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import config from '@/config/index';
-import { ILocalUser } from '@/models/entities/user';
-import { UserKeypair } from '@/models/entities/user-keypair';
-import { createPublicKey } from 'crypto';
-
-export default (user: ILocalUser, key: UserKeypair, postfix?: string) => ({
-	id: `${config.url}/users/${user.id}${postfix || '/publickey'}`,
-	type: 'Key',
-	owner: `${config.url}/users/${user.id}`,
-	publicKeyPem: createPublicKey(key.publicKey).export({
-		type: 'spki',
-		format: 'pem'
-	})
-});
diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts
deleted file mode 100644
index a7e79a176f..0000000000
--- a/src/remote/activitypub/renderer/like.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import config from '@/config/index';
-import { NoteReaction } from '@/models/entities/note-reaction';
-import { Note } from '@/models/entities/note';
-import { Emojis } from '@/models/index';
-import renderEmoji from './emoji';
-
-export const renderLike = async (noteReaction: NoteReaction, note: Note) => {
-	const reaction = noteReaction.reaction;
-
-	const object =  {
-		type: 'Like',
-		id: `${config.url}/likes/${noteReaction.id}`,
-		actor: `${config.url}/users/${noteReaction.userId}`,
-		object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
-		content: reaction,
-		_misskey_reaction: reaction
-	} as any;
-
-	if (reaction.startsWith(':')) {
-		const name = reaction.replace(/:/g, '');
-		const emoji = await Emojis.findOne({
-			name,
-			host: null
-		});
-
-		if (emoji) object.tag = [ renderEmoji(emoji) ];
-	}
-
-	return object;
-};
diff --git a/src/remote/activitypub/renderer/mention.ts b/src/remote/activitypub/renderer/mention.ts
deleted file mode 100644
index 06d2d33e59..0000000000
--- a/src/remote/activitypub/renderer/mention.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import config from '@/config/index';
-import { User, ILocalUser } from '@/models/entities/user';
-import { Users } from '@/models/index';
-
-export default (mention: User) => ({
-	type: 'Mention',
-	href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${(mention as ILocalUser).id}`,
-	name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
-});
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
deleted file mode 100644
index 84a1786784..0000000000
--- a/src/remote/activitypub/renderer/note.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import renderDocument from './document';
-import renderHashtag from './hashtag';
-import renderMention from './mention';
-import renderEmoji from './emoji';
-import config from '@/config/index';
-import toHtml from '../misc/get-note-html';
-import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index';
-import { In } from 'typeorm';
-import { Emoji } from '@/models/entities/emoji';
-import { Poll } from '@/models/entities/poll';
-
-export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<any> {
-	const getPromisedFiles = async (ids: string[]) => {
-		if (!ids || ids.length === 0) return [];
-		const items = await DriveFiles.find({ id: In(ids) });
-		return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
-	};
-
-	let inReplyTo;
-	let inReplyToNote: Note | undefined;
-
-	if (note.replyId) {
-		inReplyToNote = await Notes.findOne(note.replyId);
-
-		if (inReplyToNote != null) {
-			const inReplyToUser = await Users.findOne(inReplyToNote.userId);
-
-			if (inReplyToUser != null) {
-				if (inReplyToNote.uri) {
-					inReplyTo = inReplyToNote.uri;
-				} else {
-					if (dive) {
-						inReplyTo = await renderNote(inReplyToNote, false);
-					} else {
-						inReplyTo = `${config.url}/notes/${inReplyToNote.id}`;
-					}
-				}
-			}
-		}
-	} else {
-		inReplyTo = null;
-	}
-
-	let quote;
-
-	if (note.renoteId) {
-		const renote = await Notes.findOne(note.renoteId);
-
-		if (renote) {
-			quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`;
-		}
-	}
-
-	const user = await Users.findOneOrFail(note.userId);
-
-	const attributedTo = `${config.url}/users/${user.id}`;
-
-	const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
-
-	let to: string[] = [];
-	let cc: string[] = [];
-
-	if (note.visibility === 'public') {
-		to = ['https://www.w3.org/ns/activitystreams#Public'];
-		cc = [`${attributedTo}/followers`].concat(mentions);
-	} else if (note.visibility === 'home') {
-		to = [`${attributedTo}/followers`];
-		cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions);
-	} else if (note.visibility === 'followers') {
-		to = [`${attributedTo}/followers`];
-		cc = mentions;
-	} else {
-		to = mentions;
-	}
-
-	const mentionedUsers = note.mentions.length > 0 ? await Users.find({
-		id: In(note.mentions)
-	}) : [];
-
-	const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
-	const mentionTags = mentionedUsers.map(u => renderMention(u));
-
-	const files = await getPromisedFiles(note.fileIds);
-
-	const text = note.text;
-	let poll: Poll | undefined;
-
-	if (note.hasPoll) {
-		poll = await Polls.findOne({ noteId: note.id });
-	}
-
-	let apText = text;
-	if (apText == null) apText = '';
-
-	if (quote) {
-		apText += `\n\nRE: ${quote}`;
-	}
-
-	const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
-
-	const content = toHtml(Object.assign({}, note, {
-		text: apText
-	}));
-
-	const emojis = await getEmojis(note.emojis);
-	const apemojis = emojis.map(emoji => renderEmoji(emoji));
-
-	const tag = [
-		...hashtagTags,
-		...mentionTags,
-		...apemojis,
-	];
-
-	const asPoll = poll ? {
-		type: 'Question',
-		content: toHtml(Object.assign({}, note, {
-			text: text
-		})),
-		[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
-		[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
-			type: 'Note',
-			name: text,
-			replies: {
-				type: 'Collection',
-				totalItems: poll!.votes[i]
-			}
-		}))
-	} : {};
-
-	const asTalk = isTalk ? {
-		_misskey_talk: true
-	} : {};
-
-	return {
-		id: `${config.url}/notes/${note.id}`,
-		type: 'Note',
-		attributedTo,
-		summary,
-		content,
-		_misskey_content: text,
-		_misskey_quote: quote,
-		quoteUrl: quote,
-		published: note.createdAt.toISOString(),
-		to,
-		cc,
-		inReplyTo,
-		attachment: files.map(renderDocument),
-		sensitive: note.cw != null || files.some(file => file.isSensitive),
-		tag,
-		...asPoll,
-		...asTalk
-	};
-}
-
-export async function getEmojis(names: string[]): Promise<Emoji[]> {
-	if (names == null || names.length === 0) return [];
-
-	const emojis = await Promise.all(
-		names.map(name => Emojis.findOne({
-			name,
-			host: null
-		}))
-	);
-
-	return emojis.filter(emoji => emoji != null) as Emoji[];
-}
diff --git a/src/remote/activitypub/renderer/ordered-collection-page.ts b/src/remote/activitypub/renderer/ordered-collection-page.ts
deleted file mode 100644
index 2433358646..0000000000
--- a/src/remote/activitypub/renderer/ordered-collection-page.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Render OrderedCollectionPage
- * @param id URL of self
- * @param totalItems Number of total items
- * @param orderedItems Items
- * @param partOf URL of base
- * @param prev URL of prev page (optional)
- * @param next URL of next page (optional)
- */
-export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
-	const page = {
-		id,
-		partOf,
-		type: 'OrderedCollectionPage',
-		totalItems,
-		orderedItems
-	} as any;
-
-	if (prev) page.prev = prev;
-	if (next) page.next = next;
-
-	return page;
-}
diff --git a/src/remote/activitypub/renderer/ordered-collection.ts b/src/remote/activitypub/renderer/ordered-collection.ts
deleted file mode 100644
index 68870a0ecd..0000000000
--- a/src/remote/activitypub/renderer/ordered-collection.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Render OrderedCollection
- * @param id URL of self
- * @param totalItems Total number of items
- * @param first URL of first page (optional)
- * @param last URL of last page (optional)
- * @param orderedItems attached objects (optional)
- */
-export default function(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: object) {
-	const page: any = {
-		id,
-		type: 'OrderedCollection',
-		totalItems,
-	};
-
-	if (first) page.first = first;
-	if (last) page.last = last;
-	if (orderedItems) page.orderedItems = orderedItems;
-
-	return page;
-}
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
deleted file mode 100644
index 7e94abddfc..0000000000
--- a/src/remote/activitypub/renderer/person.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { URL } from 'url';
-import * as mfm from 'mfm-js';
-import renderImage from './image';
-import renderKey from './key';
-import config from '@/config/index';
-import { ILocalUser } from '@/models/entities/user';
-import { toHtml } from '../../../mfm/to-html';
-import { getEmojis } from './note';
-import renderEmoji from './emoji';
-import { IIdentifier } from '../models/identifier';
-import renderHashtag from './hashtag';
-import { DriveFiles, UserProfiles } from '@/models/index';
-import { getUserKeypair } from '@/misc/keypair-store';
-import { fnNameList } from '@/mfm/fn-name-list';
-
-export async function renderPerson(user: ILocalUser) {
-	const id = `${config.url}/users/${user.id}`;
-	const isSystem = !!user.username.match(/\./);
-
-	const [avatar, banner, profile] = await Promise.all([
-		user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined),
-		user.bannerId ? DriveFiles.findOne(user.bannerId) : Promise.resolve(undefined),
-		UserProfiles.findOneOrFail(user.id)
-	]);
-
-	const attachment: {
-		type: 'PropertyValue',
-		name: string,
-		value: string,
-		identifier?: IIdentifier
-	}[] = [];
-
-	if (profile.fields) {
-		for (const field of profile.fields) {
-			attachment.push({
-				type: 'PropertyValue',
-				name: field.name,
-				value: (field.value != null && field.value.match(/^https?:/))
-					? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
-					: field.value
-			});
-		}
-	}
-
-	const emojis = await getEmojis(user.emojis);
-	const apemojis = emojis.map(emoji => renderEmoji(emoji));
-
-	const hashtagTags = (user.tags || []).map(tag => renderHashtag(tag));
-
-	const tag = [
-		...apemojis,
-		...hashtagTags,
-	];
-
-	const keypair = await getUserKeypair(user.id);
-
-	const person = {
-		type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
-		id,
-		inbox: `${id}/inbox`,
-		outbox: `${id}/outbox`,
-		followers: `${id}/followers`,
-		following: `${id}/following`,
-		featured: `${id}/collections/featured`,
-		sharedInbox: `${config.url}/inbox`,
-		endpoints: { sharedInbox: `${config.url}/inbox` },
-		url: `${config.url}/@${user.username}`,
-		preferredUsername: user.username,
-		name: user.name,
-		summary: profile.description ? toHtml(mfm.parse(profile.description, { fnNameList })) : null,
-		icon: avatar ? renderImage(avatar) : null,
-		image: banner ? renderImage(banner) : null,
-		tag,
-		manuallyApprovesFollowers: user.isLocked,
-		discoverable: !!user.isExplorable,
-		publicKey: renderKey(user, keypair, `#main-key`),
-		isCat: user.isCat,
-		attachment: attachment.length ? attachment : undefined
-	} as any;
-
-	if (profile?.birthday) {
-		person['vcard:bday'] = profile.birthday;
-	}
-
-	if (profile?.location) {
-		person['vcard:Address'] = profile.location;
-	}
-
-	return person;
-}
diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts
deleted file mode 100644
index 246d599bab..0000000000
--- a/src/remote/activitypub/renderer/question.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { Poll } from '@/models/entities/poll';
-
-export default async function renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) {
-	const question = {
-		type: 'Question',
-		id: `${config.url}/questions/${note.id}`,
-		actor: `${config.url}/users/${user.id}`,
-		content:  note.text || '',
-		[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
-			name: text,
-			_misskey_votes: poll.votes[i],
-			replies: {
-				type: 'Collection',
-				totalItems: poll.votes[i]
-			}
-		}))
-	};
-
-	return question;
-}
diff --git a/src/remote/activitypub/renderer/read.ts b/src/remote/activitypub/renderer/read.ts
deleted file mode 100644
index 95357f64d3..0000000000
--- a/src/remote/activitypub/renderer/read.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-
-export const renderReadActivity = (user: { id: User['id'] }, message: MessagingMessage) => ({
-	type: 'Read',
-	actor: `${config.url}/users/${user.id}`,
-	object: message.uri
-});
diff --git a/src/remote/activitypub/renderer/reject.ts b/src/remote/activitypub/renderer/reject.ts
deleted file mode 100644
index 42beffecf2..0000000000
--- a/src/remote/activitypub/renderer/reject.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-
-export default (object: any, user: { id: User['id'] }) => ({
-	type: 'Reject',
-	actor: `${config.url}/users/${user.id}`,
-	object
-});
diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts
deleted file mode 100644
index 79d60edbaa..0000000000
--- a/src/remote/activitypub/renderer/remove.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-
-export default (user: { id: User['id'] }, target: any, object: any) => ({
-	type: 'Remove',
-	actor: `${config.url}/users/${user.id}`,
-	target,
-	object
-});
diff --git a/src/remote/activitypub/renderer/tombstone.ts b/src/remote/activitypub/renderer/tombstone.ts
deleted file mode 100644
index 553406b93b..0000000000
--- a/src/remote/activitypub/renderer/tombstone.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export default (id: string) => ({
-	id,
-	type: 'Tombstone'
-});
diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts
deleted file mode 100644
index 14115b788d..0000000000
--- a/src/remote/activitypub/renderer/undo.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import config from '@/config/index';
-import { ILocalUser, User } from '@/models/entities/user';
-
-export default (object: any, user: { id: User['id'] }) => {
-	if (object == null) return null;
-
-	return {
-		type: 'Undo',
-		actor: `${config.url}/users/${user.id}`,
-		object,
-		published: new Date().toISOString(),
-	};
-};
diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts
deleted file mode 100644
index 8bb415d117..0000000000
--- a/src/remote/activitypub/renderer/update.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-
-export default (object: any, user: { id: User['id'] }) => {
-	const activity = {
-		id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`,
-		actor: `${config.url}/users/${user.id}`,
-		type: 'Update',
-		to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
-		object,
-		published: new Date().toISOString(),
-	} as any;
-
-	return activity;
-};
diff --git a/src/remote/activitypub/renderer/vote.ts b/src/remote/activitypub/renderer/vote.ts
deleted file mode 100644
index ff038070f7..0000000000
--- a/src/remote/activitypub/renderer/vote.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import config from '@/config/index';
-import { Note } from '@/models/entities/note';
-import { IRemoteUser, User } from '@/models/entities/user';
-import { PollVote } from '@/models/entities/poll-vote';
-import { Poll } from '@/models/entities/poll';
-
-export default async function renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> {
-	return {
-		id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`,
-		actor: `${config.url}/users/${user.id}`,
-		type: 'Create',
-		to: [pollOwner.uri],
-		published: new Date().toISOString(),
-		object: {
-			id: `${config.url}/users/${user.id}#votes/${vote.id}`,
-			type: 'Note',
-			attributedTo: `${config.url}/users/${user.id}`,
-			to: [pollOwner.uri],
-			inReplyTo: note.uri,
-			name: poll.choices[vote.choice]
-		}
-	};
-}
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
deleted file mode 100644
index d6ced630c1..0000000000
--- a/src/remote/activitypub/request.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import config from '@/config/index';
-import { getUserKeypair } from '@/misc/keypair-store';
-import { User } from '@/models/entities/user';
-import { getResponse } from '../../misc/fetch';
-import { createSignedPost, createSignedGet } from './ap-request';
-
-export default async (user: { id: User['id'] }, url: string, object: any) => {
-	const body = JSON.stringify(object);
-
-	const keypair = await getUserKeypair(user.id);
-
-	const req = createSignedPost({
-		key: {
-			privateKeyPem: keypair.privateKey,
-			keyId: `${config.url}/users/${user.id}#main-key`
-		},
-		url,
-		body,
-		additionalHeaders: {
-			'User-Agent': config.userAgent,
-		}
-	});
-
-	await getResponse({
-		url,
-		method: req.request.method,
-		headers: req.request.headers,
-		body,
-	});
-};
-
-/**
- * Get AP object with http-signature
- * @param user http-signature user
- * @param url URL to fetch
- */
-export async function signedGet(url: string, user: { id: User['id'] }) {
-	const keypair = await getUserKeypair(user.id);
-
-	const req = createSignedGet({
-		key: {
-			privateKeyPem: keypair.privateKey,
-			keyId: `${config.url}/users/${user.id}#main-key`
-		},
-		url,
-		additionalHeaders: {
-			'User-Agent': config.userAgent,
-		}
-	});
-
-	const res = await getResponse({
-		url,
-		method: req.request.method,
-		headers: req.request.headers
-	});
-
-	return await res.json();
-}
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
deleted file mode 100644
index f392a65e3a..0000000000
--- a/src/remote/activitypub/resolver.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import config from '@/config/index';
-import { getJson } from '@/misc/fetch';
-import { ILocalUser } from '@/models/entities/user';
-import { getInstanceActor } from '@/services/instance-actor';
-import { signedGet } from './request';
-import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { extractDbHost } from '@/misc/convert-host';
-
-export default class Resolver {
-	private history: Set<string>;
-	private user?: ILocalUser;
-
-	constructor() {
-		this.history = new Set();
-	}
-
-	public getHistory(): string[] {
-		return Array.from(this.history);
-	}
-
-	public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
-		const collection = typeof value === 'string'
-			? await this.resolve(value)
-			: value;
-
-		if (isCollectionOrOrderedCollection(collection)) {
-			return collection;
-		} else {
-			throw new Error(`unrecognized collection type: ${collection.type}`);
-		}
-	}
-
-	public async resolve(value: string | IObject): Promise<IObject> {
-		if (value == null) {
-			throw new Error('resolvee is null (or undefined)');
-		}
-
-		if (typeof value !== 'string') {
-			return value;
-		}
-
-		if (this.history.has(value)) {
-			throw new Error('cannot resolve already resolved one');
-		}
-
-		this.history.add(value);
-
-		const meta = await fetchMeta();
-		const host = extractDbHost(value);
-		if (meta.blockedHosts.includes(host)) {
-			throw new Error('Instance is blocked');
-		}
-
-		if (config.signToActivityPubGet && !this.user) {
-			this.user = await getInstanceActor();
-		}
-
-		const object = this.user
-			? await signedGet(value, this.user)
-			: await getJson(value, 'application/activity+json, application/ld+json');
-
-		if (object == null || (
-			Array.isArray(object['@context']) ?
-				!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
-				object['@context'] !== 'https://www.w3.org/ns/activitystreams'
-		)) {
-			throw new Error('invalid response');
-		}
-
-		return object;
-	}
-}
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
deleted file mode 100644
index 2051d2624d..0000000000
--- a/src/remote/activitypub/type.ts
+++ /dev/null
@@ -1,289 +0,0 @@
-export type obj = { [x: string]: any };
-export type ApObject = IObject | string | (IObject | string)[];
-
-export interface IObject {
-	'@context': string | obj | obj[];
-	type: string | string[];
-	id?: string;
-	summary?: string;
-	published?: string;
-	cc?: ApObject;
-	to?: ApObject;
-	attributedTo: ApObject;
-	attachment?: any[];
-	inReplyTo?: any;
-	replies?: ICollection;
-	content?: string;
-	name?: string;
-	startTime?: Date;
-	endTime?: Date;
-	icon?: any;
-	image?: any;
-	url?: ApObject;
-	href?: string;
-	tag?: IObject | IObject[];
-	sensitive?: boolean;
-}
-
-/**
- * Get array of ActivityStreams Objects id
- */
-export function getApIds(value: ApObject | undefined): string[] {
-	if (value == null) return [];
-	const array = Array.isArray(value) ? value : [value];
-	return array.map(x => getApId(x));
-}
-
-/**
- * Get first ActivityStreams Object id
- */
-export function getOneApId(value: ApObject): string {
-	const firstOne = Array.isArray(value) ? value[0] : value;
-	return getApId(firstOne);
-}
-
-/**
- * Get ActivityStreams Object id
- */
-export function getApId(value: string | IObject): string {
-	if (typeof value === 'string') return value;
-	if (typeof value.id === 'string') return value.id;
-	throw new Error(`cannot detemine id`);
-}
-
-/**
- * Get ActivityStreams Object type
- */
-export function getApType(value: IObject): string {
-	if (typeof value.type === 'string') return value.type;
-	if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
-	throw new Error(`cannot detect type`);
-}
-
-export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
-	const firstOne = Array.isArray(value) ? value[0] : value;
-	return getApHrefNullable(firstOne);
-}
-
-export function getApHrefNullable(value: string | IObject | undefined): string | undefined {
-	if (typeof value === 'string') return value;
-	if (typeof value?.href === 'string') return value.href;
-	return undefined;
-}
-
-export interface IActivity extends IObject {
-	//type: 'Activity';
-	actor: IObject | string;
-	object: IObject | string;
-	target?: IObject | string;
-	/** LD-Signature */
-	signature?: {
-		type: string;
-		created: Date;
-		creator: string;
-		domain?: string;
-		nonce?: string;
-		signatureValue: string;
-	};
-}
-
-export interface ICollection extends IObject {
-	type: 'Collection';
-	totalItems: number;
-	items: ApObject;
-}
-
-export interface IOrderedCollection extends IObject {
-	type: 'OrderedCollection';
-	totalItems: number;
-	orderedItems: ApObject;
-}
-
-export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
-
-export const isPost = (object: IObject): object is IPost =>
-	validPost.includes(getApType(object));
-
-export interface IPost extends IObject {
-	type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
-	_misskey_content?: string;
-	_misskey_quote?: string;
-	quoteUrl?: string;
-	_misskey_talk: boolean;
-}
-
-export interface IQuestion extends IObject {
-	type: 'Note' | 'Question';
-	_misskey_content?: string;
-	_misskey_quote?: string;
-	quoteUrl?: string;
-	oneOf?: IQuestionChoice[];
-	anyOf?: IQuestionChoice[];
-	endTime?: Date;
-	closed?: Date;
-}
-
-export const isQuestion = (object: IObject): object is IQuestion =>
-	getApType(object) === 'Note' || getApType(object) === 'Question';
-
-interface IQuestionChoice {
-	name?: string;
-	replies?: ICollection;
-	_misskey_votes?: number;
-}
-export interface ITombstone extends IObject {
-	type: 'Tombstone';
-	formerType?: string;
-	deleted?: Date;
-}
-
-export const isTombstone = (object: IObject): object is ITombstone =>
-	getApType(object) === 'Tombstone';
-
-export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application'];
-
-export const isActor = (object: IObject): object is IActor =>
-	validActor.includes(getApType(object));
-
-export interface IActor extends IObject {
-	type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application';
-	name?: string;
-	preferredUsername?: string;
-	manuallyApprovesFollowers?: boolean;
-	discoverable?: boolean;
-	inbox: string;
-	sharedInbox?: string;	// 後方互換性のため
-	publicKey?: {
-		id: string;
-		publicKeyPem: string;
-	};
-	followers?: string | ICollection | IOrderedCollection;
-	following?: string | ICollection | IOrderedCollection;
-	featured?: string | IOrderedCollection;
-	outbox: string | IOrderedCollection;
-	endpoints?: {
-		sharedInbox?: string;
-	};
-	'vcard:bday'?: string;
-	'vcard:Address'?: string;
-}
-
-export const isCollection = (object: IObject): object is ICollection =>
-	getApType(object) === 'Collection';
-
-export const isOrderedCollection = (object: IObject): object is IOrderedCollection =>
-	getApType(object) === 'OrderedCollection';
-
-export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
-	isCollection(object) || isOrderedCollection(object);
-
-export interface IApPropertyValue extends IObject {
-	type: 'PropertyValue';
-	identifier: IApPropertyValue;
-	name: string;
-	value: string;
-}
-
-export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
-	object &&
-	getApType(object) === 'PropertyValue' &&
-	typeof object.name === 'string' &&
-	typeof (object as any).value === 'string';
-
-export interface IApMention extends IObject {
-	type: 'Mention';
-	href: string;
-}
-
-export const isMention = (object: IObject): object is IApMention=>
-	getApType(object) === 'Mention' &&
-	typeof object.href === 'string';
-
-export interface IApHashtag extends IObject {
-	type: 'Hashtag';
-	name: string;
-}
-
-export const isHashtag = (object: IObject): object is IApHashtag =>
-	getApType(object) === 'Hashtag' &&
-	typeof object.name === 'string';
-
-export interface IApEmoji extends IObject {
-	type: 'Emoji';
-	updated: Date;
-}
-
-export const isEmoji = (object: IObject): object is IApEmoji =>
-	getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
-
-export interface ICreate extends IActivity {
-	type: 'Create';
-}
-
-export interface IDelete extends IActivity {
-	type: 'Delete';
-}
-
-export interface IUpdate extends IActivity {
-	type: 'Update';
-}
-
-export interface IRead extends IActivity {
-	type: 'Read';
-}
-
-export interface IUndo extends IActivity {
-	type: 'Undo';
-}
-
-export interface IFollow extends IActivity {
-	type: 'Follow';
-}
-
-export interface IAccept extends IActivity {
-	type: 'Accept';
-}
-
-export interface IReject extends IActivity {
-	type: 'Reject';
-}
-
-export interface IAdd extends IActivity {
-	type: 'Add';
-}
-
-export interface IRemove extends IActivity {
-	type: 'Remove';
-}
-
-export interface ILike extends IActivity {
-	type: 'Like' | 'EmojiReaction' | 'EmojiReact';
-	_misskey_reaction?: string;
-}
-
-export interface IAnnounce extends IActivity {
-	type: 'Announce';
-}
-
-export interface IBlock extends IActivity {
-	type: 'Block';
-}
-
-export interface IFlag extends IActivity {
-	type: 'Flag';
-}
-
-export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
-export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
-export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
-export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read';
-export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo';
-export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow';
-export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept';
-export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
-export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
-export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
-export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact';
-export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
-export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
-export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
diff --git a/src/remote/logger.ts b/src/remote/logger.ts
deleted file mode 100644
index 9ffad4d716..0000000000
--- a/src/remote/logger.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Logger from '@/services/logger';
-
-export const remoteLogger = new Logger('remote', 'cyan');
diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts
deleted file mode 100644
index a12396abc8..0000000000
--- a/src/remote/resolve-user.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import { URL } from 'url';
-import webFinger from './webfinger';
-import config from '@/config/index';
-import { createPerson, updatePerson } from './activitypub/models/person';
-import { remoteLogger } from './logger';
-import * as chalk from 'chalk';
-import { User, IRemoteUser } from '@/models/entities/user';
-import { Users } from '@/models/index';
-import { toPuny } from '@/misc/convert-host';
-
-const logger = remoteLogger.createSubLogger('resolve-user');
-
-export async function resolveUser(username: string, host: string | null, option?: any, resync = false): Promise<User> {
-	const usernameLower = username.toLowerCase();
-
-	if (host == null) {
-		logger.info(`return local user: ${usernameLower}`);
-		return await Users.findOne({ usernameLower, host: null }).then(u => {
-			if (u == null) {
-				throw new Error('user not found');
-			} else {
-				return u;
-			}
-		});
-	}
-
-	host = toPuny(host);
-
-	if (config.host == host) {
-		logger.info(`return local user: ${usernameLower}`);
-		return await Users.findOne({ usernameLower, host: null }).then(u => {
-			if (u == null) {
-				throw new Error('user not found');
-			} else {
-				return u;
-			}
-		});
-	}
-
-	const user = await Users.findOne({ usernameLower, host }, option) as IRemoteUser;
-
-	const acctLower = `${usernameLower}@${host}`;
-
-	if (user == null) {
-		const self = await resolveSelf(acctLower);
-
-		logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
-		return await createPerson(self.href);
-	}
-
-	// resyncオプション OR ユーザー情報が古い場合は、WebFilgerからやりなおして返す
-	if (resync || user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
-		// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
-		await Users.update(user.id, {
-			lastFetchedAt: new Date(),
-		});
-
-		logger.info(`try resync: ${acctLower}`);
-		const self = await resolveSelf(acctLower);
-
-		if (user.uri !== self.href) {
-			// if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping.
-			logger.info(`uri missmatch: ${acctLower}`);
-			logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
-
-			// validate uri
-			const uri = new URL(self.href);
-			if (uri.hostname !== host) {
-				throw new Error(`Invalid uri`);
-			}
-
-			await Users.update({
-				usernameLower,
-				host: host
-			}, {
-				uri: self.href
-			});
-		} else {
-			logger.info(`uri is fine: ${acctLower}`);
-		}
-
-		await updatePerson(self.href);
-
-		logger.info(`return resynced remote user: ${acctLower}`);
-		return await Users.findOne({ uri: self.href }).then(u => {
-			if (u == null) {
-				throw new Error('user not found');
-			} else {
-				return u;
-			}
-		});
-	}
-
-	logger.info(`return existing remote user: ${acctLower}`);
-	return user;
-}
-
-async function resolveSelf(acctLower: string) {
-	logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
-	const finger = await webFinger(acctLower).catch(e => {
-		logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`);
-		throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
-	});
-	const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self');
-	if (!self) {
-		logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`);
-		throw new Error('self link not found');
-	}
-	return self;
-}
diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts
deleted file mode 100644
index f63fd03628..0000000000
--- a/src/remote/webfinger.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { URL } from 'url';
-import { getJson } from '@/misc/fetch';
-import { query as urlQuery } from '@/prelude/url';
-
-type ILink = {
-	href: string;
-	rel?: string;
-};
-
-type IWebFinger = {
-	links: ILink[];
-	subject: string;
-};
-
-export default async function(query: string): Promise<IWebFinger> {
-	const url = genUrl(query);
-
-	return await getJson(url, 'application/jrd+json, application/json');
-}
-
-function genUrl(query: string) {
-	if (query.match(/^https?:\/\//)) {
-		const u = new URL(query);
-		return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
-	}
-
-	const m = query.match(/^([^@]+)@(.*)/);
-	if (m) {
-		const hostname = m[2];
-		return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` });
-	}
-
-	throw new Error(`Invalid query (${query})`);
-}
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
deleted file mode 100644
index eabe681136..0000000000
--- a/src/server/activitypub.ts
+++ /dev/null
@@ -1,227 +0,0 @@
-import * as Router from '@koa/router';
-import * as json from 'koa-json-body';
-import * as httpSignature from 'http-signature';
-
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderNote from '@/remote/activitypub/renderer/note';
-import renderKey from '@/remote/activitypub/renderer/key';
-import { renderPerson } from '@/remote/activitypub/renderer/person';
-import renderEmoji from '@/remote/activitypub/renderer/emoji';
-import Outbox, { packActivity } from './activitypub/outbox';
-import Followers from './activitypub/followers';
-import Following from './activitypub/following';
-import Featured from './activitypub/featured';
-import { inbox as processInbox } from '@/queue/index';
-import { isSelfHost } from '@/misc/convert-host';
-import { Notes, Users, Emojis, NoteReactions } from '@/models/index';
-import { ILocalUser, User } from '@/models/entities/user';
-import { In } from 'typeorm';
-import { renderLike } from '@/remote/activitypub/renderer/like';
-import { getUserKeypair } from '@/misc/keypair-store';
-
-// Init router
-const router = new Router();
-
-//#region Routing
-
-function inbox(ctx: Router.RouterContext) {
-	let signature;
-
-	try {
-		signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
-	} catch (e) {
-		ctx.status = 401;
-		return;
-	}
-
-	processInbox(ctx.request.body, signature);
-
-	ctx.status = 202;
-}
-
-const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
-const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
-
-function isActivityPubReq(ctx: Router.RouterContext) {
-	ctx.response.vary('Accept');
-	const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON);
-	return typeof accepted === 'string' && !accepted.match(/html/);
-}
-
-export function setResponseType(ctx: Router.RouterContext) {
-	const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON);
-	if (accept === LD_JSON) {
-		ctx.response.type = LD_JSON;
-	} else {
-		ctx.response.type = ACTIVITY_JSON;
-	}
-}
-
-// inbox
-router.post('/inbox', json(), inbox);
-router.post('/users/:user/inbox', json(), inbox);
-
-// note
-router.get('/notes/:note', async (ctx, next) => {
-	if (!isActivityPubReq(ctx)) return await next();
-
-	const note = await Notes.findOne({
-		id: ctx.params.note,
-		visibility: In(['public', 'home']),
-		localOnly: false
-	});
-
-	if (note == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	// リモートだったらリダイレクト
-	if (note.userHost != null) {
-		if (note.uri == null || isSelfHost(note.userHost)) {
-			ctx.status = 500;
-			return;
-		}
-		ctx.redirect(note.uri);
-		return;
-	}
-
-	ctx.body = renderActivity(await renderNote(note, false));
-	ctx.set('Cache-Control', 'public, max-age=180');
-	setResponseType(ctx);
-});
-
-// note activity
-router.get('/notes/:note/activity', async ctx => {
-	const note = await Notes.findOne({
-		id: ctx.params.note,
-		userHost: null,
-		visibility: In(['public', 'home']),
-		localOnly: false
-	});
-
-	if (note == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	ctx.body = renderActivity(await packActivity(note));
-	ctx.set('Cache-Control', 'public, max-age=180');
-	setResponseType(ctx);
-});
-
-// outbox
-router.get('/users/:user/outbox', Outbox);
-
-// followers
-router.get('/users/:user/followers', Followers);
-
-// following
-router.get('/users/:user/following', Following);
-
-// featured
-router.get('/users/:user/collections/featured', Featured);
-
-// publickey
-router.get('/users/:user/publickey', async ctx => {
-	const userId = ctx.params.user;
-
-	const user = await Users.findOne({
-		id: userId,
-		host: null
-	});
-
-	if (user == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	const keypair = await getUserKeypair(user.id);
-
-	if (Users.isLocalUser(user)) {
-		ctx.body = renderActivity(renderKey(user, keypair));
-		ctx.set('Cache-Control', 'public, max-age=180');
-		setResponseType(ctx);
-	} else {
-		ctx.status = 400;
-	}
-});
-
-// user
-async function userInfo(ctx: Router.RouterContext, user: User | undefined) {
-	if (user == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	ctx.body = renderActivity(await renderPerson(user as ILocalUser));
-	ctx.set('Cache-Control', 'public, max-age=180');
-	setResponseType(ctx);
-}
-
-router.get('/users/:user', async (ctx, next) => {
-	if (!isActivityPubReq(ctx)) return await next();
-
-	const userId = ctx.params.user;
-
-	const user = await Users.findOne({
-		id: userId,
-		host: null,
-		isSuspended: false
-	});
-
-	await userInfo(ctx, user);
-});
-
-router.get('/@:user', async (ctx, next) => {
-	if (!isActivityPubReq(ctx)) return await next();
-
-	const user = await Users.findOne({
-		usernameLower: ctx.params.user.toLowerCase(),
-		host: null,
-		isSuspended: false
-	});
-
-	await userInfo(ctx, user);
-});
-//#endregion
-
-// emoji
-router.get('/emojis/:emoji', async ctx => {
-	const emoji = await Emojis.findOne({
-		host: null,
-		name: ctx.params.emoji
-	});
-
-	if (emoji == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	ctx.body = renderActivity(await renderEmoji(emoji));
-	ctx.set('Cache-Control', 'public, max-age=180');
-	setResponseType(ctx);
-});
-
-// like
-router.get('/likes/:like', async ctx => {
-	const reaction = await NoteReactions.findOne(ctx.params.like);
-
-	if (reaction == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	const note = await Notes.findOne(reaction.noteId);
-
-	if (note == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	ctx.body = renderActivity(await renderLike(reaction, note));
-	ctx.set('Cache-Control', 'public, max-age=180');
-	setResponseType(ctx);
-});
-
-export default router;
diff --git a/src/server/activitypub/featured.ts b/src/server/activitypub/featured.ts
deleted file mode 100644
index 1598cc680f..0000000000
--- a/src/server/activitypub/featured.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as Router from '@koa/router';
-import config from '@/config/index';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection';
-import { setResponseType } from '../activitypub';
-import renderNote from '@/remote/activitypub/renderer/note';
-import { Users, Notes, UserNotePinings } from '@/models/index';
-
-export default async (ctx: Router.RouterContext) => {
-	const userId = ctx.params.user;
-
-	// Verify user
-	const user = await Users.findOne({
-		id: userId,
-		host: null
-	});
-
-	if (user == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	const pinings = await UserNotePinings.find({
-		where: { userId: user.id },
-		order: { id: 'DESC' }
-	});
-
-	const pinnedNotes = await Promise.all(pinings.map(pining =>
-		Notes.findOneOrFail(pining.noteId)));
-
-	const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
-
-	const rendered = renderOrderedCollection(
-		`${config.url}/users/${userId}/collections/featured`,
-		renderedNotes.length, undefined, undefined, renderedNotes
-	);
-
-	ctx.body = renderActivity(rendered);
-	ctx.set('Cache-Control', 'public, max-age=180');
-	setResponseType(ctx);
-};
diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts
deleted file mode 100644
index baf2d23460..0000000000
--- a/src/server/activitypub/followers.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import * as Router from '@koa/router';
-import config from '@/config/index';
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import * as url from '@/prelude/url';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection';
-import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page';
-import renderFollowUser from '@/remote/activitypub/renderer/follow-user';
-import { setResponseType } from '../activitypub';
-import { Users, Followings, UserProfiles } from '@/models/index';
-import { LessThan } from 'typeorm';
-
-export default async (ctx: Router.RouterContext) => {
-	const userId = ctx.params.user;
-
-	// Get 'cursor' parameter
-	const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);
-
-	// Get 'page' parameter
-	const pageErr = !$.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
-	const page: boolean = ctx.request.query.page === 'true';
-
-	// Validate parameters
-	if (cursorErr || pageErr) {
-		ctx.status = 400;
-		return;
-	}
-
-	// Verify user
-	const user = await Users.findOne({
-		id: userId,
-		host: null
-	});
-
-	if (user == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	//#region Check ff visibility
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	if (profile.ffVisibility === 'private') {
-		ctx.status = 403;
-		ctx.set('Cache-Control', 'public, max-age=30');
-		return;
-	} else if (profile.ffVisibility === 'followers') {
-		ctx.status = 403;
-		ctx.set('Cache-Control', 'public, max-age=30');
-		return;
-	}
-	//#endregion
-
-	const limit = 10;
-	const partOf = `${config.url}/users/${userId}/followers`;
-
-	if (page) {
-		const query = {
-			followeeId: user.id
-		} as any;
-
-		// カーソルが指定されている場合
-		if (cursor) {
-			query.id = LessThan(cursor);
-		}
-
-		// Get followers
-		const followings = await Followings.find({
-			where: query,
-			take: limit + 1,
-			order: { id: -1 }
-		});
-
-		// 「次のページ」があるかどうか
-		const inStock = followings.length === limit + 1;
-		if (inStock) followings.pop();
-
-		const renderedFollowers = await Promise.all(followings.map(following => renderFollowUser(following.followerId)));
-		const rendered = renderOrderedCollectionPage(
-			`${partOf}?${url.query({
-				page: 'true',
-				cursor
-			})}`,
-			user.followersCount, renderedFollowers, partOf,
-			undefined,
-			inStock ? `${partOf}?${url.query({
-				page: 'true',
-				cursor: followings[followings.length - 1].id
-			})}` : undefined
-		);
-
-		ctx.body = renderActivity(rendered);
-		setResponseType(ctx);
-	} else {
-		// index page
-		const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
-		ctx.body = renderActivity(rendered);
-		ctx.set('Cache-Control', 'public, max-age=180');
-		setResponseType(ctx);
-	}
-};
diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts
deleted file mode 100644
index b9eb806c3c..0000000000
--- a/src/server/activitypub/following.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import * as Router from '@koa/router';
-import config from '@/config/index';
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import * as url from '@/prelude/url';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection';
-import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page';
-import renderFollowUser from '@/remote/activitypub/renderer/follow-user';
-import { setResponseType } from '../activitypub';
-import { Users, Followings, UserProfiles } from '@/models/index';
-import { LessThan, FindConditions } from 'typeorm';
-import { Following } from '@/models/entities/following';
-
-export default async (ctx: Router.RouterContext) => {
-	const userId = ctx.params.user;
-
-	// Get 'cursor' parameter
-	const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);
-
-	// Get 'page' parameter
-	const pageErr = !$.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
-	const page: boolean = ctx.request.query.page === 'true';
-
-	// Validate parameters
-	if (cursorErr || pageErr) {
-		ctx.status = 400;
-		return;
-	}
-
-	// Verify user
-	const user = await Users.findOne({
-		id: userId,
-		host: null
-	});
-
-	if (user == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	//#region Check ff visibility
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	if (profile.ffVisibility === 'private') {
-		ctx.status = 403;
-		ctx.set('Cache-Control', 'public, max-age=30');
-		return;
-	} else if (profile.ffVisibility === 'followers') {
-		ctx.status = 403;
-		ctx.set('Cache-Control', 'public, max-age=30');
-		return;
-	}
-	//#endregion
-
-	const limit = 10;
-	const partOf = `${config.url}/users/${userId}/following`;
-
-	if (page) {
-		const query = {
-			followerId: user.id
-		} as FindConditions<Following>;
-
-		// カーソルが指定されている場合
-		if (cursor) {
-			query.id = LessThan(cursor);
-		}
-
-		// Get followings
-		const followings = await Followings.find({
-			where: query,
-			take: limit + 1,
-			order: { id: -1 }
-		});
-
-		// 「次のページ」があるかどうか
-		const inStock = followings.length === limit + 1;
-		if (inStock) followings.pop();
-
-		const renderedFollowees = await Promise.all(followings.map(following => renderFollowUser(following.followeeId)));
-		const rendered = renderOrderedCollectionPage(
-			`${partOf}?${url.query({
-				page: 'true',
-				cursor
-			})}`,
-			user.followingCount, renderedFollowees, partOf,
-			undefined,
-			inStock ? `${partOf}?${url.query({
-				page: 'true',
-				cursor: followings[followings.length - 1].id
-			})}` : undefined
-		);
-
-		ctx.body = renderActivity(rendered);
-		setResponseType(ctx);
-	} else {
-		// index page
-		const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
-		ctx.body = renderActivity(rendered);
-		ctx.set('Cache-Control', 'public, max-age=180');
-		setResponseType(ctx);
-	}
-};
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
deleted file mode 100644
index df528e8b5a..0000000000
--- a/src/server/activitypub/outbox.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import * as Router from '@koa/router';
-import config from '@/config/index';
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection';
-import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page';
-import { setResponseType } from '../activitypub';
-import renderNote from '@/remote/activitypub/renderer/note';
-import renderCreate from '@/remote/activitypub/renderer/create';
-import renderAnnounce from '@/remote/activitypub/renderer/announce';
-import { countIf } from '@/prelude/array';
-import * as url from '@/prelude/url';
-import { Users, Notes } from '@/models/index';
-import { makePaginationQuery } from '../api/common/make-pagination-query';
-import { Brackets } from 'typeorm';
-import { Note } from '@/models/entities/note';
-
-export default async (ctx: Router.RouterContext) => {
-	const userId = ctx.params.user;
-
-	// Get 'sinceId' parameter
-	const [sinceId, sinceIdErr] = $.optional.type(ID).get(ctx.request.query.since_id);
-
-	// Get 'untilId' parameter
-	const [untilId, untilIdErr] = $.optional.type(ID).get(ctx.request.query.until_id);
-
-	// Get 'page' parameter
-	const pageErr = !$.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
-	const page: boolean = ctx.request.query.page === 'true';
-
-	// Validate parameters
-	if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) {
-		ctx.status = 400;
-		return;
-	}
-
-	// Verify user
-	const user = await Users.findOne({
-		id: userId,
-		host: null
-	});
-
-	if (user == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	const limit = 20;
-	const partOf = `${config.url}/users/${userId}/outbox`;
-
-	if (page) {
-		const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId)
-			.andWhere('note.userId = :userId', { userId: user.id })
-			.andWhere(new Brackets(qb => { qb
-				.where(`note.visibility = 'public'`)
-				.orWhere(`note.visibility = 'home'`);
-			}))
-			.andWhere('note.localOnly = FALSE');
-
-		const notes = await query.take(limit).getMany();
-
-		if (sinceId) notes.reverse();
-
-		const activities = await Promise.all(notes.map(note => packActivity(note)));
-		const rendered = renderOrderedCollectionPage(
-			`${partOf}?${url.query({
-				page: 'true',
-				since_id: sinceId,
-				until_id: untilId
-			})}`,
-			user.notesCount, activities, partOf,
-			notes.length ? `${partOf}?${url.query({
-				page: 'true',
-				since_id: notes[0].id
-			})}` : undefined,
-			notes.length ? `${partOf}?${url.query({
-				page: 'true',
-				until_id: notes[notes.length - 1].id
-			})}` : undefined
-		);
-
-		ctx.body = renderActivity(rendered);
-		setResponseType(ctx);
-	} else {
-		// index page
-		const rendered = renderOrderedCollection(partOf, user.notesCount,
-			`${partOf}?page=true`,
-			`${partOf}?page=true&since_id=000000000000000000000000`
-		);
-		ctx.body = renderActivity(rendered);
-		ctx.set('Cache-Control', 'public, max-age=180');
-		setResponseType(ctx);
-	}
-};
-
-/**
- * Pack Create<Note> or Announce Activity
- * @param note Note
- */
-export async function packActivity(note: Note): Promise<any> {
-	if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
-		const renote = await Notes.findOneOrFail(note.renoteId);
-		return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note);
-	}
-
-	return renderCreate(await renderNote(note, false), note);
-}
diff --git a/src/server/api/2fa.ts b/src/server/api/2fa.ts
deleted file mode 100644
index 117446383d..0000000000
--- a/src/server/api/2fa.ts
+++ /dev/null
@@ -1,422 +0,0 @@
-import * as crypto from 'crypto';
-import config from '@/config/index';
-import * as jsrsasign from 'jsrsasign';
-
-const ECC_PRELUDE = Buffer.from([0x04]);
-const NULL_BYTE = Buffer.from([0]);
-const PEM_PRELUDE = Buffer.from(
-	'3059301306072a8648ce3d020106082a8648ce3d030107034200',
-	'hex'
-);
-
-// Android Safetynet attestations are signed with this cert:
-const GSR2 = `-----BEGIN CERTIFICATE-----
-MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
-A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
-Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
-MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
-A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
-v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
-eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
-tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
-C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
-zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
-mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
-V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
-bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
-3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
-J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
-291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
-ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
-AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
-TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
------END CERTIFICATE-----\n`;
-
-function base64URLDecode(source: string) {
-	return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64');
-}
-
-function getCertSubject(certificate: string) {
-	const subjectCert = new jsrsasign.X509();
-	subjectCert.readCertPEM(certificate);
-
-	const subjectString = subjectCert.getSubjectString();
-	const subjectFields = subjectString.slice(1).split('/');
-
-	const fields = {} as Record<string, string>;
-	for (const field of subjectFields) {
-		const eqIndex = field.indexOf('=');
-		fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1);
-	}
-
-	return fields;
-}
-
-function verifyCertificateChain(certificates: string[]) {
-	let valid = true;
-
-	for (let i = 0; i < certificates.length; i++) {
-		const Cert = certificates[i];
-		const certificate = new jsrsasign.X509();
-		certificate.readCertPEM(Cert);
-
-		const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1];
-
-		const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
-		const algorithm = certificate.getSignatureAlgorithmField();
-		const signatureHex = certificate.getSignatureValueHex();
-
-		// Verify against CA
-		const Signature = new jsrsasign.KJUR.crypto.Signature({alg: algorithm});
-		Signature.init(CACert);
-		Signature.updateHex(certStruct);
-		valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate
-	}
-
-	return valid;
-}
-
-function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') {
-	if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) {
-		pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91);
-		type = 'PUBLIC KEY';
-	}
-	const cert = pemBuffer.toString('base64');
-
-	const keyParts = [];
-	const max = Math.ceil(cert.length / 64);
-	let start = 0;
-	for (let i = 0; i < max; i++) {
-		keyParts.push(cert.substring(start, start + 64));
-		start += 64;
-	}
-
-	return (
-		`-----BEGIN ${type}-----\n` +
-		keyParts.join('\n') +
-		`\n-----END ${type}-----\n`
-	);
-}
-
-export function hash(data: Buffer) {
-	return crypto
-		.createHash('sha256')
-		.update(data)
-		.digest();
-}
-
-export function verifyLogin({
-	publicKey,
-	authenticatorData,
-	clientDataJSON,
-	clientData,
-	signature,
-	challenge
-}: {
-	publicKey: Buffer,
-	authenticatorData: Buffer,
-	clientDataJSON: Buffer,
-	clientData: any,
-	signature: Buffer,
-	challenge: string
-}) {
-	if (clientData.type != 'webauthn.get') {
-		throw new Error('type is not webauthn.get');
-	}
-
-	if (hash(clientData.challenge).toString('hex') != challenge) {
-		throw new Error('challenge mismatch');
-	}
-	if (clientData.origin != config.scheme + '://' + config.host) {
-		throw new Error('origin mismatch');
-	}
-
-	const verificationData = Buffer.concat(
-		[authenticatorData, hash(clientDataJSON)],
-		32 + authenticatorData.length
-	);
-
-	return crypto
-		.createVerify('SHA256')
-		.update(verificationData)
-		.verify(PEMString(publicKey), signature);
-}
-
-export const procedures = {
-	none: {
-		verify({publicKey}: {publicKey: Map<number, Buffer>}) {
-			const negTwo = publicKey.get(-2);
-
-			if (!negTwo || negTwo.length != 32) {
-				throw new Error('invalid or no -2 key given');
-			}
-			const negThree = publicKey.get(-3);
-			if (!negThree || negThree.length != 32) {
-				throw new Error('invalid or no -3 key given');
-			}
-
-			const publicKeyU2F = Buffer.concat(
-				[ECC_PRELUDE, negTwo, negThree],
-				1 + 32 + 32
-			);
-
-			return {
-				publicKey: publicKeyU2F,
-				valid: true
-			};
-		}
-	},
-	'android-key': {
-		verify({
-			attStmt,
-			authenticatorData,
-			clientDataHash,
-			publicKey,
-			rpIdHash,
-			credentialId
-		}: {
-			attStmt: any,
-			authenticatorData: Buffer,
-			clientDataHash: Buffer,
-			publicKey: Map<number, any>;
-			rpIdHash: Buffer,
-			credentialId: Buffer,
-		}) {
-			if (attStmt.alg != -7) {
-				throw new Error('alg mismatch');
-			}
-
-			const verificationData = Buffer.concat([
-				authenticatorData,
-				clientDataHash
-			]);
-
-			const attCert: Buffer = attStmt.x5c[0];
-
-			const negTwo = publicKey.get(-2);
-
-			if (!negTwo || negTwo.length != 32) {
-				throw new Error('invalid or no -2 key given');
-			}
-			const negThree = publicKey.get(-3);
-			if (!negThree || negThree.length != 32) {
-				throw new Error('invalid or no -3 key given');
-			}
-
-			const publicKeyData = Buffer.concat(
-				[ECC_PRELUDE, negTwo, negThree],
-				1 + 32 + 32
-			);
-
-			if (!attCert.equals(publicKeyData)) {
-				throw new Error('public key mismatch');
-			}
-
-			const isValid = crypto
-				.createVerify('SHA256')
-				.update(verificationData)
-				.verify(PEMString(attCert), attStmt.sig);
-
-			// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
-
-			return {
-				valid: isValid,
-				publicKey: publicKeyData
-			};
-		}
-	},
-	// what a stupid attestation
-	'android-safetynet': {
-		verify({
-			attStmt,
-			authenticatorData,
-			clientDataHash,
-			publicKey,
-			rpIdHash,
-			credentialId
-		}: {
-			attStmt: any,
-			authenticatorData: Buffer,
-			clientDataHash: Buffer,
-			publicKey: Map<number, any>;
-			rpIdHash: Buffer,
-			credentialId: Buffer,
-		}) {
-			const verificationData = hash(
-				Buffer.concat([authenticatorData, clientDataHash])
-			);
-
-			const jwsParts = attStmt.response.toString('utf-8').split('.');
-
-			const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
-			const response = JSON.parse(
-				base64URLDecode(jwsParts[1]).toString('utf-8')
-			);
-			const signature = jwsParts[2];
-
-			if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
-				throw new Error('invalid nonce');
-			}
-
-			const certificateChain = header.x5c
-				.map((key: any) => PEMString(key))
-				.concat([GSR2]);
-
-			if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') {
-				throw new Error('invalid common name');
-			}
-
-			if (!verifyCertificateChain(certificateChain)) {
-				throw new Error('Invalid certificate chain!');
-			}
-
-			const signatureBase = Buffer.from(
-				jwsParts[0] + '.' + jwsParts[1],
-				'utf-8'
-			);
-
-			const valid = crypto
-				.createVerify('sha256')
-				.update(signatureBase)
-				.verify(certificateChain[0], base64URLDecode(signature));
-
-			const negTwo = publicKey.get(-2);
-
-			if (!negTwo || negTwo.length != 32) {
-				throw new Error('invalid or no -2 key given');
-			}
-			const negThree = publicKey.get(-3);
-			if (!negThree || negThree.length != 32) {
-				throw new Error('invalid or no -3 key given');
-			}
-
-			const publicKeyData = Buffer.concat(
-				[ECC_PRELUDE, negTwo, negThree],
-				1 + 32 + 32
-			);
-			return {
-				valid,
-				publicKey: publicKeyData
-			};
-		}
-	},
-	packed: {
-		verify({
-			attStmt,
-			authenticatorData,
-			clientDataHash,
-			publicKey,
-			rpIdHash,
-			credentialId
-		}: {
-			attStmt: any,
-			authenticatorData: Buffer,
-			clientDataHash: Buffer,
-			publicKey: Map<number, any>;
-			rpIdHash: Buffer,
-			credentialId: Buffer,
-		}) {
-			const verificationData = Buffer.concat([
-				authenticatorData,
-				clientDataHash
-			]);
-
-			if (attStmt.x5c) {
-				const attCert = attStmt.x5c[0];
-
-				const validSignature = crypto
-					.createVerify('SHA256')
-					.update(verificationData)
-					.verify(PEMString(attCert), attStmt.sig);
-
-				const negTwo = publicKey.get(-2);
-
-				if (!negTwo || negTwo.length != 32) {
-					throw new Error('invalid or no -2 key given');
-				}
-				const negThree = publicKey.get(-3);
-				if (!negThree || negThree.length != 32) {
-					throw new Error('invalid or no -3 key given');
-				}
-
-				const publicKeyData = Buffer.concat(
-					[ECC_PRELUDE, negTwo, negThree],
-					1 + 32 + 32
-				);
-
-				return {
-					valid: validSignature,
-					publicKey: publicKeyData
-				};
-			} else if (attStmt.ecdaaKeyId) {
-				// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
-				throw new Error('ECDAA-Verify is not supported');
-			} else {
-				if (attStmt.alg != -7) throw new Error('alg mismatch');
-
-				throw new Error('self attestation is not supported');
-			}
-		}
-	},
-
-	'fido-u2f': {
-		verify({
-			attStmt,
-			authenticatorData,
-			clientDataHash,
-			publicKey,
-			rpIdHash,
-			credentialId
-		}: {
-			attStmt: any,
-			authenticatorData: Buffer,
-			clientDataHash: Buffer,
-			publicKey: Map<number, any>,
-			rpIdHash: Buffer,
-			credentialId: Buffer
-		}) {
-			const x5c: Buffer[] = attStmt.x5c;
-			if (x5c.length != 1) {
-				throw new Error('x5c length does not match expectation');
-			}
-
-			const attCert = x5c[0];
-
-			// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
-
-			const negTwo: Buffer = publicKey.get(-2);
-
-			if (!negTwo || negTwo.length != 32) {
-				throw new Error('invalid or no -2 key given');
-			}
-			const negThree: Buffer = publicKey.get(-3);
-			if (!negThree || negThree.length != 32) {
-				throw new Error('invalid or no -3 key given');
-			}
-
-			const publicKeyU2F = Buffer.concat(
-				[ECC_PRELUDE, negTwo, negThree],
-				1 + 32 + 32
-			);
-
-			const verificationData = Buffer.concat([
-				NULL_BYTE,
-				rpIdHash,
-				clientDataHash,
-				credentialId,
-				publicKeyU2F
-			]);
-
-			const validSignature = crypto
-				.createVerify('SHA256')
-				.update(verificationData)
-				.verify(PEMString(attCert), attStmt.sig);
-
-			return {
-				valid: validSignature,
-				publicKey: publicKeyU2F
-			};
-		}
-	}
-};
diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts
deleted file mode 100644
index cbace8917e..0000000000
--- a/src/server/api/api-handler.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as Koa from 'koa';
-
-import { IEndpoint } from './endpoints';
-import authenticate, { AuthenticationError } from './authenticate';
-import call from './call';
-import { ApiError } from './error';
-
-export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
-	const body = ctx.request.body;
-
-	const reply = (x?: any, y?: ApiError) => {
-		if (x == null) {
-			ctx.status = 204;
-		} else if (typeof x === 'number' && y) {
-			ctx.status = x;
-			ctx.body = {
-				error: {
-					message: y!.message,
-					code: y!.code,
-					id: y!.id,
-					kind: y!.kind,
-					...(y!.info ? { info: y!.info } : {})
-				}
-			};
-		} else {
-			// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
-			ctx.body = typeof x === 'string' ? JSON.stringify(x) : x;
-		}
-		res();
-	};
-
-	// Authentication
-	authenticate(body['i']).then(([user, app]) => {
-		// API invoking
-		call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => {
-			reply(res);
-		}).catch((e: ApiError) => {
-			reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
-		});
-	}).catch(e => {
-		if (e instanceof AuthenticationError) {
-			reply(403, new ApiError({
-				message: 'Authentication failed. Please ensure your token is correct.',
-				code: 'AUTHENTICATION_FAILED',
-				id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14'
-			}));
-		} else {
-			reply(500, new ApiError());
-		}
-	});
-});
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
deleted file mode 100644
index b8e216edc4..0000000000
--- a/src/server/api/authenticate.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import isNativeToken from './common/is-native-token';
-import { User } from '@/models/entities/user';
-import { Users, AccessTokens, Apps } from '@/models/index';
-import { AccessToken } from '@/models/entities/access-token';
-
-export class AuthenticationError extends Error {
-	constructor(message: string) {
-		super(message);
-		this.name = 'AuthenticationError';
-	}
-}
-
-export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => {
-	if (token == null) {
-		return [null, null];
-	}
-
-	if (isNativeToken(token)) {
-		// Fetch user
-		const user = await Users
-			.findOne({ token });
-
-		if (user == null) {
-			throw new AuthenticationError('user not found');
-		}
-
-		return [user, null];
-	} else {
-		const accessToken = await AccessTokens.findOne({
-			where: [{
-				hash: token.toLowerCase() // app
-			}, {
-				token: token // miauth
-			}],
-		});
-
-		if (accessToken == null) {
-			throw new AuthenticationError('invalid signature');
-		}
-
-		AccessTokens.update(accessToken.id, {
-			lastUsedAt: new Date(),
-		});
-
-		const user = await Users
-			.findOne({
-				id: accessToken.userId // findOne(accessToken.userId) のように書かないのは後方互換性のため
-			});
-
-		if (accessToken.appId) {
-			const app = await Apps
-				.findOneOrFail(accessToken.appId);
-
-			return [user, {
-				id: accessToken.id,
-				permission: app.permission
-			} as AccessToken];
-		} else {
-			return [user, accessToken];
-		}
-	}
-};
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
deleted file mode 100644
index bd86ffdc35..0000000000
--- a/src/server/api/call.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { performance } from 'perf_hooks';
-import limiter from './limiter';
-import { User } from '@/models/entities/user';
-import endpoints from './endpoints';
-import { ApiError } from './error';
-import { apiLogger } from './logger';
-import { AccessToken } from '@/models/entities/access-token';
-
-const accessDenied = {
-	message: 'Access denied.',
-	code: 'ACCESS_DENIED',
-	id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e'
-};
-
-export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => {
-	const isSecure = user != null && token == null;
-
-	const ep = endpoints.find(e => e.name === endpoint);
-
-	if (ep == null) {
-		throw new ApiError({
-			message: 'No such endpoint.',
-			code: 'NO_SUCH_ENDPOINT',
-			id: 'f8080b67-5f9c-4eb7-8c18-7f1eeae8f709',
-			httpStatusCode: 404
-		});
-	}
-
-	if (ep.meta.secure && !isSecure) {
-		throw new ApiError(accessDenied);
-	}
-
-	if (ep.meta.requireCredential && user == null) {
-		throw new ApiError({
-			message: 'Credential required.',
-			code: 'CREDENTIAL_REQUIRED',
-			id: '1384574d-a912-4b81-8601-c7b1c4085df1',
-			httpStatusCode: 401
-		});
-	}
-
-	if (ep.meta.requireCredential && user!.isSuspended) {
-		throw new ApiError({
-			message: 'Your account has been suspended.',
-			code: 'YOUR_ACCOUNT_SUSPENDED',
-			id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
-			httpStatusCode: 403
-		});
-	}
-
-	if (ep.meta.requireAdmin && !user!.isAdmin) {
-		throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
-	}
-
-	if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
-		throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
-	}
-
-	if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
-		throw new ApiError({
-			message: 'Your app does not have the necessary permissions to use this endpoint.',
-			code: 'PERMISSION_DENIED',
-			id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
-		});
-	}
-
-	if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
-		// Rate limit
-		await limiter(ep, user!).catch(e => {
-			throw new ApiError({
-				message: 'Rate limit exceeded. Please try again later.',
-				code: 'RATE_LIMIT_EXCEEDED',
-				id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
-				httpStatusCode: 429
-			});
-		});
-	}
-
-	// API invoking
-	const before = performance.now();
-	return await ep.exec(data, user, token, file).catch((e: Error) => {
-		if (e instanceof ApiError) {
-			throw e;
-		} else {
-			apiLogger.error(`Internal error occurred in ${ep.name}: ${e?.message}`, {
-				ep: ep.name,
-				ps: data,
-				e: {
-					message: e?.message,
-					code: e?.name,
-					stack: e?.stack
-				}
-			});
-			throw new ApiError(null, {
-				e: {
-					message: e?.message,
-					code: e?.name,
-					stack: e?.stack
-				}
-			});
-		}
-	}).finally(() => {
-		const after = performance.now();
-		const time = after - before;
-		if (time > 1000) {
-			apiLogger.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`);
-		}
-	});
-};
diff --git a/src/server/api/common/generate-block-query.ts b/src/server/api/common/generate-block-query.ts
deleted file mode 100644
index 4fd6184738..0000000000
--- a/src/server/api/common/generate-block-query.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { User } from '@/models/entities/user';
-import { Blockings } from '@/models/index';
-import { Brackets, SelectQueryBuilder } from 'typeorm';
-
-// ここでいうBlockedは被Blockedの意
-export function generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
-	const blockingQuery = Blockings.createQueryBuilder('blocking')
-		.select('blocking.blockerId')
-		.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
-
-	// 投稿の作者にブロックされていない かつ
-	// 投稿の返信先の作者にブロックされていない かつ
-	// 投稿の引用元の作者にブロックされていない
-	q
-		.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
-		.andWhere(new Brackets(qb => { qb
-			.where(`note.replyUserId IS NULL`)
-			.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
-		}))
-		.andWhere(new Brackets(qb => { qb
-			.where(`note.renoteUserId IS NULL`)
-			.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
-		}));
-
-	q.setParameters(blockingQuery.getParameters());
-}
-
-export function generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
-	const blockingQuery = Blockings.createQueryBuilder('blocking')
-		.select('blocking.blockeeId')
-		.where('blocking.blockerId = :blockerId', { blockerId: me.id });
-
-	const blockedQuery = Blockings.createQueryBuilder('blocking')
-		.select('blocking.blockerId')
-		.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
-
-	q.andWhere(`user.id NOT IN (${ blockingQuery.getQuery() })`);
-	q.setParameters(blockingQuery.getParameters());
-
-	q.andWhere(`user.id NOT IN (${ blockedQuery.getQuery() })`);
-	q.setParameters(blockedQuery.getParameters());
-}
diff --git a/src/server/api/common/generate-channel-query.ts b/src/server/api/common/generate-channel-query.ts
deleted file mode 100644
index 80a0acf7f9..0000000000
--- a/src/server/api/common/generate-channel-query.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { User } from '@/models/entities/user';
-import { ChannelFollowings } from '@/models/index';
-import { Brackets, SelectQueryBuilder } from 'typeorm';
-
-export function generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
-	if (me == null) {
-		q.andWhere('note.channelId IS NULL');
-	} else {
-		q.leftJoinAndSelect('note.channel', 'channel');
-
-		const channelFollowingQuery = ChannelFollowings.createQueryBuilder('channelFollowing')
-			.select('channelFollowing.followeeId')
-			.where('channelFollowing.followerId = :followerId', { followerId: me.id });
-
-		q.andWhere(new Brackets(qb => { qb
-			// チャンネルのノートではない
-			.where('note.channelId IS NULL')
-			// または自分がフォローしているチャンネルのノート
-			.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
-		}));
-
-		q.setParameters(channelFollowingQuery.getParameters());
-	}
-}
diff --git a/src/server/api/common/generate-muted-note-query.ts b/src/server/api/common/generate-muted-note-query.ts
deleted file mode 100644
index 0737842613..0000000000
--- a/src/server/api/common/generate-muted-note-query.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { User } from '@/models/entities/user';
-import { MutedNotes } from '@/models/index';
-import { SelectQueryBuilder } from 'typeorm';
-
-export function generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
-	const mutedQuery = MutedNotes.createQueryBuilder('muted')
-		.select('muted.noteId')
-		.where('muted.userId = :userId', { userId: me.id });
-
-	q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
-
-	q.setParameters(mutedQuery.getParameters());
-}
diff --git a/src/server/api/common/generate-muted-note-thread-query.ts b/src/server/api/common/generate-muted-note-thread-query.ts
deleted file mode 100644
index 7e2cbd498b..0000000000
--- a/src/server/api/common/generate-muted-note-thread-query.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { User } from '@/models/entities/user';
-import { NoteThreadMutings } from '@/models/index';
-import { Brackets, SelectQueryBuilder } from 'typeorm';
-
-export function generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
-	const mutedQuery = NoteThreadMutings.createQueryBuilder('threadMuted')
-		.select('threadMuted.threadId')
-		.where('threadMuted.userId = :userId', { userId: me.id });
-
-	q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
-	q.andWhere(new Brackets(qb => { qb
-		.where(`note.threadId IS NULL`)
-		.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
-	}));
-
-	q.setParameters(mutedQuery.getParameters());
-}
diff --git a/src/server/api/common/generate-muted-user-query.ts b/src/server/api/common/generate-muted-user-query.ts
deleted file mode 100644
index 7e200b87ef..0000000000
--- a/src/server/api/common/generate-muted-user-query.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { User } from '@/models/entities/user';
-import { Mutings } from '@/models/index';
-import { SelectQueryBuilder, Brackets } from 'typeorm';
-
-export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) {
-	const mutingQuery = Mutings.createQueryBuilder('muting')
-		.select('muting.muteeId')
-		.where('muting.muterId = :muterId', { muterId: me.id });
-
-	if (exclude) {
-		mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
-	}
-
-	// 投稿の作者をミュートしていない かつ
-	// 投稿の返信先の作者をミュートしていない かつ
-	// 投稿の引用元の作者をミュートしていない
-	q
-		.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
-		.andWhere(new Brackets(qb => { qb
-			.where(`note.replyUserId IS NULL`)
-			.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
-		}))
-		.andWhere(new Brackets(qb => { qb
-			.where(`note.renoteUserId IS NULL`)
-			.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
-		}));
-
-	q.setParameters(mutingQuery.getParameters());
-}
-
-export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
-	const mutingQuery = Mutings.createQueryBuilder('muting')
-		.select('muting.muteeId')
-		.where('muting.muterId = :muterId', { muterId: me.id });
-
-	q
-		.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
-
-	q.setParameters(mutingQuery.getParameters());
-}
diff --git a/src/server/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts
deleted file mode 100644
index 1f791c57ce..0000000000
--- a/src/server/api/common/generate-native-user-token.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { secureRndstr } from '@/misc/secure-rndstr';
-
-export default () => secureRndstr(16, true);
diff --git a/src/server/api/common/generate-replies-query.ts b/src/server/api/common/generate-replies-query.ts
deleted file mode 100644
index fbc41b2c25..0000000000
--- a/src/server/api/common/generate-replies-query.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { User } from '@/models/entities/user';
-import { Brackets, SelectQueryBuilder } from 'typeorm';
-
-export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
-	if (me == null) {
-		q.andWhere(new Brackets(qb => { qb
-			.where(`note.replyId IS NULL`) // 返信ではない
-			.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
-				.where(`note.replyId IS NOT NULL`)
-				.andWhere('note.replyUserId = note.userId');
-			}));
-		}));
-	} else {
-		q.andWhere(new Brackets(qb => { qb
-			.where(`note.replyId IS NULL`) // 返信ではない
-			.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
-			.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
-				.where(`note.replyId IS NOT NULL`)
-				.andWhere('note.userId = :meId', { meId: me.id });
-			}))
-			.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
-				.where(`note.replyId IS NOT NULL`)
-				.andWhere('note.replyUserId = note.userId');
-			}));
-		}));
-	}
-}
diff --git a/src/server/api/common/generate-visibility-query.ts b/src/server/api/common/generate-visibility-query.ts
deleted file mode 100644
index 813e8b6c09..0000000000
--- a/src/server/api/common/generate-visibility-query.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { User } from '@/models/entities/user';
-import { Followings } from '@/models/index';
-import { Brackets, SelectQueryBuilder } from 'typeorm';
-
-export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
-	if (me == null) {
-		q.andWhere(new Brackets(qb => { qb
-			.where(`note.visibility = 'public'`)
-			.orWhere(`note.visibility = 'home'`);
-		}));
-	} else {
-		const followingQuery = Followings.createQueryBuilder('following')
-			.select('following.followeeId')
-			.where('following.followerId = :followerId', { followerId: me.id });
-
-		q.andWhere(new Brackets(qb => { qb
-			// 公開投稿である
-			.where(new Brackets(qb => { qb
-				.where(`note.visibility = 'public'`)
-				.orWhere(`note.visibility = 'home'`);
-			}))
-			// または 自分自身
-			.orWhere('note.userId = :userId1', { userId1: me.id })
-			// または 自分宛て
-			.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`)
-			.orWhere(new Brackets(qb => { qb
-				// または フォロワー宛ての投稿であり、
-				.where('note.visibility = \'followers\'')
-				.andWhere(new Brackets(qb => { qb
-					// 自分がフォロワーである
-					.where(`note.userId IN (${ followingQuery.getQuery() })`)
-					// または 自分の投稿へのリプライ
-					.orWhere('note.replyUserId = :userId3', { userId3: me.id });
-				}));
-			}));
-		}));
-
-		q.setParameters(followingQuery.getParameters());
-	}
-}
diff --git a/src/server/api/common/getters.ts b/src/server/api/common/getters.ts
deleted file mode 100644
index 4b2ee8f1da..0000000000
--- a/src/server/api/common/getters.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { IdentifiableError } from '@/misc/identifiable-error';
-import { User } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { Notes, Users } from '@/models/index';
-
-/**
- * Get note for API processing
- */
-export async function getNote(noteId: Note['id']) {
-	const note = await Notes.findOne(noteId);
-
-	if (note == null) {
-		throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
-	}
-
-	return note;
-}
-
-/**
- * Get user for API processing
- */
-export async function getUser(userId: User['id']) {
-	const user = await Users.findOne(userId);
-
-	if (user == null) {
-		throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.');
-	}
-
-	return user;
-}
-
-/**
- * Get remote user for API processing
- */
-export async function getRemoteUser(userId: User['id']) {
-	const user = await getUser(userId);
-
-	if (!Users.isRemoteUser(user)) {
-		throw new Error('user is not a remote user');
-	}
-
-	return user;
-}
-
-/**
- * Get local user for API processing
- */
-export async function getLocalUser(userId: User['id']) {
-	const user = await getUser(userId);
-
-	if (!Users.isLocalUser(user)) {
-		throw new Error('user is not a local user');
-	}
-
-	return user;
-}
diff --git a/src/server/api/common/inject-featured.ts b/src/server/api/common/inject-featured.ts
deleted file mode 100644
index 1dc13c83ef..0000000000
--- a/src/server/api/common/inject-featured.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import rndstr from 'rndstr';
-import { Note } from '@/models/entities/note';
-import { User } from '@/models/entities/user';
-import { Notes, UserProfiles, NoteReactions } from '@/models/index';
-import { generateMutedUserQuery } from './generate-muted-user-query';
-import { generateBlockedUserQuery } from './generate-block-query';
-
-// TODO: リアクション、Renote、返信などをしたノートは除外する
-
-export async function injectFeatured(timeline: Note[], user?: User | null) {
-	if (timeline.length < 5) return;
-
-	if (user) {
-		const profile = await UserProfiles.findOneOrFail(user.id);
-		if (!profile.injectFeaturedNote) return;
-	}
-
-	const max = 30;
-	const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
-
-	const query = Notes.createQueryBuilder('note')
-		.addSelect('note.score')
-		.where('note.userHost IS NULL')
-		.andWhere(`note.score > 0`)
-		.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
-		.andWhere(`note.visibility = 'public'`)
-		.innerJoinAndSelect('note.user', 'user');
-
-	if (user) {
-		query.andWhere('note.userId != :userId', { userId: user.id });
-
-		generateMutedUserQuery(query, user);
-		generateBlockedUserQuery(query, user);
-
-		const reactionQuery = NoteReactions.createQueryBuilder('reaction')
-			.select('reaction.noteId')
-			.where('reaction.userId = :userId', { userId: user.id });
-
-		query.andWhere(`note.id NOT IN (${ reactionQuery.getQuery() })`);
-	}
-
-	const notes = await query
-		.orderBy('note.score', 'DESC')
-		.take(max)
-		.getMany();
-
-	if (notes.length === 0) return;
-
-	// Pick random one
-	const featured = notes[Math.floor(Math.random() * notes.length)];
-
-	(featured as any)._featuredId_ = rndstr('a-z0-9', 8);
-
-	// Inject featured
-	timeline.splice(3, 0, featured);
-}
diff --git a/src/server/api/common/inject-promo.ts b/src/server/api/common/inject-promo.ts
deleted file mode 100644
index 87767a65bf..0000000000
--- a/src/server/api/common/inject-promo.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import rndstr from 'rndstr';
-import { Note } from '@/models/entities/note';
-import { User } from '@/models/entities/user';
-import { PromoReads, PromoNotes, Notes, Users } from '@/models/index';
-
-export async function injectPromo(timeline: Note[], user?: User | null) {
-	if (timeline.length < 5) return;
-
-	// TODO: readやexpireフィルタはクエリ側でやる
-
-	const reads = user ? await PromoReads.find({
-		userId: user.id
-	}) : [];
-
-	let promos = await PromoNotes.find();
-
-	promos = promos.filter(n => n.expiresAt.getTime() > Date.now());
-	promos = promos.filter(n => !reads.map(r => r.noteId).includes(n.noteId));
-
-	if (promos.length === 0) return;
-
-	// Pick random promo
-	const promo = promos[Math.floor(Math.random() * promos.length)];
-
-	const note = await Notes.findOneOrFail(promo.noteId);
-
-	// Join
-	note.user = await Users.findOneOrFail(note.userId);
-
-	(note as any)._prId_ = rndstr('a-z0-9', 8);
-
-	// Inject promo
-	timeline.splice(3, 0, note);
-}
diff --git a/src/server/api/common/is-native-token.ts b/src/server/api/common/is-native-token.ts
deleted file mode 100644
index 2833c570c8..0000000000
--- a/src/server/api/common/is-native-token.ts
+++ /dev/null
@@ -1 +0,0 @@
-export default (token: string) => token.length === 16;
diff --git a/src/server/api/common/make-pagination-query.ts b/src/server/api/common/make-pagination-query.ts
deleted file mode 100644
index 51c11e5dff..0000000000
--- a/src/server/api/common/make-pagination-query.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { SelectQueryBuilder } from 'typeorm';
-
-export function makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number) {
-	if (sinceId && untilId) {
-		q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
-		q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
-		q.orderBy(`${q.alias}.id`, 'DESC');
-	} else if (sinceId) {
-		q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
-		q.orderBy(`${q.alias}.id`, 'ASC');
-	} else if (untilId) {
-		q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
-		q.orderBy(`${q.alias}.id`, 'DESC');
-	} else if (sinceDate && untilDate) {
-		q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
-		q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
-		q.orderBy(`${q.alias}.createdAt`, 'DESC');
-	} else if (sinceDate) {
-		q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) });
-		q.orderBy(`${q.alias}.createdAt`, 'ASC');
-	} else if (untilDate) {
-		q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) });
-		q.orderBy(`${q.alias}.createdAt`, 'DESC');
-	} else {
-		q.orderBy(`${q.alias}.id`, 'DESC');
-	}
-	return q;
-}
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
deleted file mode 100644
index 33f41b2770..0000000000
--- a/src/server/api/common/read-messaging-message.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { publishMainStream, publishGroupMessagingStream } from '@/services/stream';
-import { publishMessagingStream } from '@/services/stream';
-import { publishMessagingIndexStream } from '@/services/stream';
-import { User, IRemoteUser } from '@/models/entities/user';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index';
-import { In } from 'typeorm';
-import { IdentifiableError } from '@/misc/identifiable-error';
-import { UserGroup } from '@/models/entities/user-group';
-import { toArray } from '@/prelude/array';
-import { renderReadActivity } from '@/remote/activitypub/renderer/read';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import { deliver } from '@/queue/index';
-import orderedCollection from '@/remote/activitypub/renderer/ordered-collection';
-
-/**
- * Mark messages as read
- */
-export async function readUserMessagingMessage(
-	userId: User['id'],
-	otherpartyId: User['id'],
-	messageIds: MessagingMessage['id'][]
-) {
-	if (messageIds.length === 0) return;
-
-	const messages = await MessagingMessages.find({
-		id: In(messageIds)
-	});
-
-	for (const message of messages) {
-		if (message.recipientId !== userId) {
-			throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).');
-		}
-	}
-
-	// Update documents
-	await MessagingMessages.update({
-		id: In(messageIds),
-		userId: otherpartyId,
-		recipientId: userId,
-		isRead: false
-	}, {
-		isRead: true
-	});
-
-	// Publish event
-	publishMessagingStream(otherpartyId, userId, 'read', messageIds);
-	publishMessagingIndexStream(userId, 'read', messageIds);
-
-	if (!await Users.getHasUnreadMessagingMessage(userId)) {
-		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
-		publishMainStream(userId, 'readAllMessagingMessages');
-	}
-}
-
-/**
- * Mark messages as read
- */
-export async function readGroupMessagingMessage(
-	userId: User['id'],
-	groupId: UserGroup['id'],
-	messageIds: MessagingMessage['id'][]
-) {
-	if (messageIds.length === 0) return;
-
-	// check joined
-	const joining = await UserGroupJoinings.findOne({
-		userId: userId,
-		userGroupId: groupId
-	});
-
-	if (joining == null) {
-		throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).');
-	}
-
-	const messages = await MessagingMessages.find({
-		id: In(messageIds)
-	});
-
-	const reads: MessagingMessage['id'][] = [];
-
-	for (const message of messages) {
-		if (message.userId === userId) continue;
-		if (message.reads.includes(userId)) continue;
-
-		// Update document
-		await MessagingMessages.createQueryBuilder().update()
-			.set({
-				reads: (() => `array_append("reads", '${joining.userId}')`) as any
-			})
-			.where('id = :id', { id: message.id })
-			.execute();
-
-		reads.push(message.id);
-	}
-
-	// Publish event
-	publishGroupMessagingStream(groupId, 'read', {
-		ids: reads,
-		userId: userId
-	});
-	publishMessagingIndexStream(userId, 'read', reads);
-
-	if (!await Users.getHasUnreadMessagingMessage(userId)) {
-		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
-		publishMainStream(userId, 'readAllMessagingMessages');
-	}
-}
-
-export async function deliverReadActivity(user: { id: User['id']; host: null; }, recipient: IRemoteUser, messages: MessagingMessage | MessagingMessage[]) {
-	messages = toArray(messages).filter(x => x.uri);
-	const contents = messages.map(x => renderReadActivity(user, x));
-
-	if (contents.length > 1) {
-		const collection = orderedCollection(null, contents.length, undefined, undefined, contents);
-		deliver(user, renderActivity(collection), recipient.inbox);
-	} else {
-		for (const content of contents) {
-			deliver(user, renderActivity(content), recipient.inbox);
-		}
-	}
-}
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
deleted file mode 100644
index a4406c9eeb..0000000000
--- a/src/server/api/common/read-notification.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { publishMainStream } from '@/services/stream';
-import { User } from '@/models/entities/user';
-import { Notification } from '@/models/entities/notification';
-import { Notifications, Users } from '@/models/index';
-import { In } from 'typeorm';
-
-export async function readNotification(
-	userId: User['id'],
-	notificationIds: Notification['id'][]
-) {
-	// Update documents
-	await Notifications.update({
-		id: In(notificationIds),
-		isRead: false
-	}, {
-		isRead: true
-	});
-
-	post(userId);
-}
-
-export async function readNotificationByQuery(
-	userId: User['id'],
-	query: Record<string, any>
-) {
-	// Update documents
-	await Notifications.update({
-		...query,
-		notifieeId: userId,
-		isRead: false
-	}, {
-		isRead: true
-	});
-
-	post(userId);
-}
-
-async function post(userId: User['id']) {
-	if (!await Users.getHasUnreadNotification(userId)) {
-		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
-		publishMainStream(userId, 'readAllNotifications');
-	}
-}
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
deleted file mode 100644
index 4c7aacf1cd..0000000000
--- a/src/server/api/common/signin.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as Koa from 'koa';
-
-import config from '@/config/index';
-import { ILocalUser } from '@/models/entities/user';
-import { Signins } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { publishMainStream } from '@/services/stream';
-
-export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
-	if (redirect) {
-		//#region Cookie
-		ctx.cookies.set('igi', user.token, {
-			path: '/',
-			// SEE: https://github.com/koajs/koa/issues/974
-			// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
-			secure: config.url.startsWith('https'),
-			httpOnly: false
-		});
-		//#endregion
-
-		ctx.redirect(config.url);
-	} else {
-		ctx.body = {
-			id: user.id,
-			i: user.token
-		};
-		ctx.status = 200;
-	}
-
-	(async () => {
-		// Append signin history
-		const record = await Signins.save({
-			id: genId(),
-			createdAt: new Date(),
-			userId: user.id,
-			ip: ctx.ip,
-			headers: ctx.headers,
-			success: true
-		});
-
-		// Publish signin event
-		publishMainStream(user.id, 'signin', await Signins.pack(record));
-	})();
-}
diff --git a/src/server/api/common/signup.ts b/src/server/api/common/signup.ts
deleted file mode 100644
index 2ba0d8e479..0000000000
--- a/src/server/api/common/signup.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import * as bcrypt from 'bcryptjs';
-import { generateKeyPair } from 'crypto';
-import generateUserToken from './generate-native-user-token';
-import { User } from '@/models/entities/user';
-import { Users, UsedUsernames } from '@/models/index';
-import { UserProfile } from '@/models/entities/user-profile';
-import { getConnection } from 'typeorm';
-import { genId } from '@/misc/gen-id';
-import { toPunyNullable } from '@/misc/convert-host';
-import { UserKeypair } from '@/models/entities/user-keypair';
-import { usersChart } from '@/services/chart/index';
-import { UsedUsername } from '@/models/entities/used-username';
-
-export async function signup(opts: {
-	username: User['username'];
-	password?: string | null;
-	passwordHash?: UserProfile['password'] | null;
-	host?: string | null;
-}) {
-	const { username, password, passwordHash, host } = opts;
-	let hash = passwordHash;
-
-	// Validate username
-	if (!Users.validateLocalUsername.ok(username)) {
-		throw new Error('INVALID_USERNAME');
-	}
-
-	if (password != null && passwordHash == null) {
-		// Validate password
-		if (!Users.validatePassword.ok(password)) {
-			throw new Error('INVALID_PASSWORD');
-		}
-
-		// Generate hash of password
-		const salt = await bcrypt.genSalt(8);
-		hash = await bcrypt.hash(password, salt);
-	}
-
-	// Generate secret
-	const secret = generateUserToken();
-
-	// Check username duplication
-	if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) {
-		throw new Error('DUPLICATED_USERNAME');
-	}
-
-	// Check deleted username duplication
-	if (await UsedUsernames.findOne({ username: username.toLowerCase() })) {
-		throw new Error('USED_USERNAME');
-	}
-
-	const keyPair = await new Promise<string[]>((res, rej) =>
-		generateKeyPair('rsa', {
-			modulusLength: 4096,
-			publicKeyEncoding: {
-				type: 'spki',
-				format: 'pem'
-			},
-			privateKeyEncoding: {
-				type: 'pkcs8',
-				format: 'pem',
-				cipher: undefined,
-				passphrase: undefined
-			}
-		} as any, (err, publicKey, privateKey) =>
-			err ? rej(err) : res([publicKey, privateKey])
-		));
-
-	let account!: User;
-
-	// Start transaction
-	await getConnection().transaction(async transactionalEntityManager => {
-		const exist = await transactionalEntityManager.findOne(User, {
-			usernameLower: username.toLowerCase(),
-			host: null
-		});
-
-		if (exist) throw new Error(' the username is already used');
-
-		account = await transactionalEntityManager.save(new User({
-			id: genId(),
-			createdAt: new Date(),
-			username: username,
-			usernameLower: username.toLowerCase(),
-			host: toPunyNullable(host),
-			token: secret,
-			isAdmin: (await Users.count({
-				host: null,
-			})) === 0,
-		}));
-
-		await transactionalEntityManager.save(new UserKeypair({
-			publicKey: keyPair[0],
-			privateKey: keyPair[1],
-			userId: account.id
-		}));
-
-		await transactionalEntityManager.save(new UserProfile({
-			userId: account.id,
-			autoAcceptFollowed: true,
-			password: hash,
-		}));
-
-		await transactionalEntityManager.save(new UsedUsername({
-			createdAt: new Date(),
-			username: username.toLowerCase(),
-		}));
-	});
-
-	usersChart.update(account, true);
-
-	return { account, secret };
-}
diff --git a/src/server/api/define.ts b/src/server/api/define.ts
deleted file mode 100644
index 4bd8f95e31..0000000000
--- a/src/server/api/define.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import * as fs from 'fs';
-import { ILocalUser } from '@/models/entities/user';
-import { IEndpointMeta } from './endpoints';
-import { ApiError } from './error';
-import { SchemaType } from '@/misc/schema';
-import { AccessToken } from '@/models/entities/access-token';
-
-type NonOptional<T> = T extends undefined ? never : T;
-
-type SimpleUserInfo = {
-	id: ILocalUser['id'];
-	host: ILocalUser['host'];
-	username: ILocalUser['username'];
-	uri: ILocalUser['uri'];
-	inbox: ILocalUser['inbox'];
-	sharedInbox: ILocalUser['sharedInbox'];
-	isAdmin: ILocalUser['isAdmin'];
-	isModerator: ILocalUser['isModerator'];
-	isSilenced: ILocalUser['isSilenced'];
-};
-
-type Params<T extends IEndpointMeta> = {
-	[P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function
-		? ReturnType<NonNullable<T['params']>[P]['transform']>
-		: NonNullable<T['params']>[P]['default'] extends null | number | string
-			? NonOptional<ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0]>
-			: ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0];
-};
-
-export type Response = Record<string, any> | void;
-
-type executor<T extends IEndpointMeta> =
-	(params: Params<T>, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any, cleanup?: Function) =>
-		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
-
-export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>)
-		: (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => Promise<any> {
-	return (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => {
-		function cleanup() {
-			fs.unlink(file.path, () => {});
-		}
-
-		if (meta.requireFile && file == null) return Promise.reject(new ApiError({
-			message: 'File required.',
-			code: 'FILE_REQUIRED',
-			id: '4267801e-70d1-416a-b011-4ee502885d8b',
-		}));
-
-		const [ps, pserr] = getParams(meta, params);
-		if (pserr) {
-			if (file) cleanup();
-			return Promise.reject(pserr);
-		}
-
-		return cb(ps, user, token, file, cleanup);
-	};
-}
-
-function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, ApiError | null] {
-	if (defs.params == null) return [params, null];
-
-	const x: any = {};
-	let err: ApiError | null = null;
-	Object.entries(defs.params).some(([k, def]) => {
-		const [v, e] = def.validator.get(params[k]);
-		if (e) {
-			err = new ApiError({
-				message: 'Invalid param.',
-				code: 'INVALID_PARAM',
-				id: '3d81ceae-475f-4600-b2a8-2bc116157532',
-			}, {
-				param: k,
-				reason: e.message
-			});
-			return true;
-		} else {
-			if (v === undefined && def.hasOwnProperty('default')) {
-				x[k] = def.default;
-			} else {
-				x[k] = v;
-			}
-			if (def.transform) x[k] = def.transform(x[k]);
-			return false;
-		}
-	});
-	return [x, err];
-}
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
deleted file mode 100644
index 6d9d2b0782..0000000000
--- a/src/server/api/endpoints.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { fileURLToPath } from 'url';
-import { dirname } from 'path';
-import { Context } from 'cafy';
-import * as path from 'path';
-import * as glob from 'glob';
-import { SimpleSchema } from '@/misc/simple-schema';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-export type Param = {
-	validator: Context<any>;
-	transform?: any;
-	default?: any;
-	deprecated?: boolean;
-	ref?: string;
-};
-
-export interface IEndpointMeta {
-	stability?: string; //'deprecated' | 'experimental' | 'stable';
-
-	tags?: string[];
-
-	params?: {
-		[key: string]: Param;
-	};
-
-	errors?: {
-		[key: string]: {
-			message: string;
-			code: string;
-			id: string;
-		};
-	};
-
-	res?: SimpleSchema;
-
-	/**
-	 * このエンドポイントにリクエストするのにユーザー情報が必須か否か
-	 * 省略した場合は false として解釈されます。
-	 */
-	requireCredential?: boolean;
-
-	/**
-	 * 管理者のみ使えるエンドポイントか否か
-	 */
-	requireAdmin?: boolean;
-
-	/**
-	 * 管理者またはモデレーターのみ使えるエンドポイントか否か
-	 */
-	requireModerator?: boolean;
-
-	/**
-	 * エンドポイントのリミテーションに関するやつ
-	 * 省略した場合はリミテーションは無いものとして解釈されます。
-	 * また、withCredential が false の場合はリミテーションを行うことはできません。
-	 */
-	limit?: {
-
-		/**
-		 * 複数のエンドポイントでリミットを共有したい場合に指定するキー
-		 */
-		key?: string;
-
-		/**
-		 * リミットを適用する期間(ms)
-		 * このプロパティを設定する場合、max プロパティも設定する必要があります。
-		 */
-		duration?: number;
-
-		/**
-		 * durationで指定した期間内にいくつまでリクエストできるのか
-		 * このプロパティを設定する場合、duration プロパティも設定する必要があります。
-		 */
-		max?: number;
-
-		/**
-		 * 最低でもどれくらいの間隔を開けてリクエストしなければならないか(ms)
-		 */
-		minInterval?: number;
-	};
-
-	/**
-	 * ファイルの添付を必要とするか否か
-	 * 省略した場合は false として解釈されます。
-	 */
-	requireFile?: boolean;
-
-	/**
-	 * サードパーティアプリからはリクエストすることができないか否か
-	 * 省略した場合は false として解釈されます。
-	 */
-	secure?: boolean;
-
-	/**
-	 * エンドポイントの種類
-	 * パーミッションの実現に利用されます。
-	 */
-	kind?: string;
-}
-
-export interface IEndpoint {
-	name: string;
-	exec: any;
-	meta: IEndpointMeta;
-}
-
-const files = glob.sync('**/*.js', {
-	cwd: path.resolve(_dirname + '/endpoints/')
-});
-
-const endpoints: IEndpoint[] = files.map(f => {
-	const ep = require(`./endpoints/${f}`);
-
-	return {
-		name: f.replace('.js', ''),
-		exec: ep.default,
-		meta: ep.meta || {}
-	};
-});
-
-export default endpoints;
diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts
deleted file mode 100644
index 403eb24191..0000000000
--- a/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { AbuseUserReports } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		state: {
-			validator: $.optional.nullable.str,
-			default: null,
-		},
-
-		reporterOrigin: {
-			validator: $.optional.str.or([
-				'combined',
-				'local',
-				'remote',
-			]),
-			default: 'combined'
-		},
-
-		targetUserOrigin: {
-			validator: $.optional.str.or([
-				'combined',
-				'local',
-				'remote',
-			]),
-			default: 'combined'
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
-					format: 'id',
-					example: 'xxxxxxxxxx',
-				},
-				createdAt: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
-					format: 'date-time',
-				},
-				comment: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
-				},
-				resolved: {
-					type: 'boolean' as const,
-					nullable: false as const, optional: false as const,
-					example: false
-				},
-				reporterId: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
-					format: 'id',
-				},
-				targetUserId: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const,
-					format: 'id',
-				},
-				assigneeId: {
-					type: 'string' as const,
-					nullable: true as const, optional: false as const,
-					format: 'id',
-				},
-				reporter: {
-					type: 'object' as const,
-					nullable: false as const, optional: false as const,
-					ref: 'User'
-				},
-				targetUser: {
-					type: 'object' as const,
-					nullable: false as const, optional: false as const,
-					ref: 'User'
-				},
-				assignee: {
-					type: 'object' as const,
-					nullable: true as const, optional: true as const,
-					ref: 'User'
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId);
-
-	switch (ps.state) {
-		case 'resolved': query.andWhere('report.resolved = TRUE'); break;
-		case 'unresolved': query.andWhere('report.resolved = FALSE'); break;
-	}
-
-	switch (ps.reporterOrigin) {
-		case 'local': query.andWhere('report.reporterHost IS NULL'); break;
-		case 'remote': query.andWhere('report.reporterHost IS NOT NULL'); break;
-	}
-
-	switch (ps.targetUserOrigin) {
-		case 'local': query.andWhere('report.targetUserHost IS NULL'); break;
-		case 'remote': query.andWhere('report.targetUserHost IS NOT NULL'); break;
-	}
-
-	const reports = await query.take(ps.limit!).getMany();
-
-	return await AbuseUserReports.packMany(reports);
-});
diff --git a/src/server/api/endpoints/admin/accounts/create.ts b/src/server/api/endpoints/admin/accounts/create.ts
deleted file mode 100644
index fa15e84f77..0000000000
--- a/src/server/api/endpoints/admin/accounts/create.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import define from '../../../define';
-import { Users } from '@/models/index';
-import { signup } from '../../../common/signup';
-
-export const meta = {
-	tags: ['admin'],
-
-	params: {
-		username: {
-			validator: Users.validateLocalUsername,
-		},
-
-		password: {
-			validator: Users.validatePassword,
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
-		properties: {
-			token: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, _me) => {
-	const me = _me ? await Users.findOneOrFail(_me.id) : null;
-	const noUsers = (await Users.count({
-		host: null,
-	})) === 0;
-	if (!noUsers && !me?.isAdmin) throw new Error('access denied');
-
-	const { account, secret } = await signup({
-		username: ps.username,
-		password: ps.password,
-	});
-
-	const res = await Users.pack(account, account, {
-		detail: true,
-		includeSecrets: true
-	});
-
-	(res as any).token = secret;
-
-	return res;
-});
diff --git a/src/server/api/endpoints/admin/accounts/delete.ts b/src/server/api/endpoints/admin/accounts/delete.ts
deleted file mode 100644
index 4e8a559805..0000000000
--- a/src/server/api/endpoints/admin/accounts/delete.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Users } from '@/models/index';
-import { doPostSuspend } from '@/services/suspend-user';
-import { publishUserEvent } from '@/services/stream';
-import { createDeleteAccountJob } from '@/queue';
-import { ID } from '@/misc/cafy-id';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	if (user.isAdmin) {
-		throw new Error('cannot suspend admin');
-	}
-
-	if (user.isModerator) {
-		throw new Error('cannot suspend moderator');
-	}
-
-	if (Users.isLocalUser(user)) {
-		// 物理削除する前にDelete activityを送信する
-		await doPostSuspend(user).catch(e => {});
-
-		createDeleteAccountJob(user, {
-			soft: false
-		});
-	} else {
-		createDeleteAccountJob(user, {
-			soft: true // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
-		});
-	}
-
-	await Users.update(user.id, {
-		isDeleted: true,
-	});
-
-	if (Users.isLocalUser(user)) {
-		// Terminate streaming
-		publishUserEvent(user.id, 'terminate', {});
-	}
-});
diff --git a/src/server/api/endpoints/admin/ad/create.ts b/src/server/api/endpoints/admin/ad/create.ts
deleted file mode 100644
index 27c7b5d318..0000000000
--- a/src/server/api/endpoints/admin/ad/create.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Ads } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		url: {
-			validator: $.str.min(1)
-		},
-		memo: {
-			validator: $.str
-		},
-		place: {
-			validator: $.str
-		},
-		priority: {
-			validator: $.str
-		},
-		ratio: {
-			validator: $.num.int().min(0)
-		},
-		expiresAt: {
-			validator: $.num.int()
-		},
-		imageUrl: {
-			validator: $.str.min(1)
-		}
-	},
-};
-
-export default define(meta, async (ps) => {
-	await Ads.insert({
-		id: genId(),
-		createdAt: new Date(),
-		expiresAt: new Date(ps.expiresAt),
-		url: ps.url,
-		imageUrl: ps.imageUrl,
-		priority: ps.priority,
-		ratio: ps.ratio,
-		place: ps.place,
-		memo: ps.memo,
-	});
-});
diff --git a/src/server/api/endpoints/admin/ad/delete.ts b/src/server/api/endpoints/admin/ad/delete.ts
deleted file mode 100644
index 91934e1aab..0000000000
--- a/src/server/api/endpoints/admin/ad/delete.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { Ads } from '@/models/index';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		id: {
-			validator: $.type(ID)
-		}
-	},
-
-	errors: {
-		noSuchAd: {
-			message: 'No such ad.',
-			code: 'NO_SUCH_AD',
-			id: 'ccac9863-3a03-416e-b899-8a64041118b1'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const ad = await Ads.findOne(ps.id);
-
-	if (ad == null) throw new ApiError(meta.errors.noSuchAd);
-
-	await Ads.delete(ad.id);
-});
diff --git a/src/server/api/endpoints/admin/ad/list.ts b/src/server/api/endpoints/admin/ad/list.ts
deleted file mode 100644
index 000aaaba9d..0000000000
--- a/src/server/api/endpoints/admin/ad/list.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { Ads } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-};
-
-export default define(meta, async (ps) => {
-	const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId)
-		.andWhere('ad.expiresAt > :now', { now: new Date() });
-
-	const ads = await query.take(ps.limit!).getMany();
-
-	return ads;
-});
diff --git a/src/server/api/endpoints/admin/ad/update.ts b/src/server/api/endpoints/admin/ad/update.ts
deleted file mode 100644
index 36c87895c2..0000000000
--- a/src/server/api/endpoints/admin/ad/update.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { Ads } from '@/models/index';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		id: {
-			validator: $.type(ID)
-		},
-		memo: {
-			validator: $.str
-		},
-		url: {
-			validator: $.str.min(1)
-		},
-		imageUrl: {
-			validator: $.str.min(1)
-		},
-		place: {
-			validator: $.str
-		},
-		priority: {
-			validator: $.str
-		},
-		ratio: {
-			validator: $.num.int().min(0)
-		},
-		expiresAt: {
-			validator: $.num.int()
-		},
-	},
-
-	errors: {
-		noSuchAd: {
-			message: 'No such ad.',
-			code: 'NO_SUCH_AD',
-			id: 'b7aa1727-1354-47bc-a182-3a9c3973d300'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const ad = await Ads.findOne(ps.id);
-
-	if (ad == null) throw new ApiError(meta.errors.noSuchAd);
-
-	await Ads.update(ad.id, {
-		url: ps.url,
-		place: ps.place,
-		priority: ps.priority,
-		ratio: ps.ratio,
-		memo: ps.memo,
-		imageUrl: ps.imageUrl,
-		expiresAt: new Date(ps.expiresAt),
-	});
-});
diff --git a/src/server/api/endpoints/admin/announcements/create.ts b/src/server/api/endpoints/admin/announcements/create.ts
deleted file mode 100644
index f1c07745f9..0000000000
--- a/src/server/api/endpoints/admin/announcements/create.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Announcements } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		title: {
-			validator: $.str.min(1)
-		},
-		text: {
-			validator: $.str.min(1)
-		},
-		imageUrl: {
-			validator: $.nullable.str.min(1)
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-				example: 'xxxxxxxxxx',
-			},
-			createdAt: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'date-time',
-			},
-			updatedAt: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-				format: 'date-time',
-			},
-			title: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			text: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			imageUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const announcement = await Announcements.save({
-		id: genId(),
-		createdAt: new Date(),
-		updatedAt: null,
-		title: ps.title,
-		text: ps.text,
-		imageUrl: ps.imageUrl,
-	});
-
-	return announcement;
-});
diff --git a/src/server/api/endpoints/admin/announcements/delete.ts b/src/server/api/endpoints/admin/announcements/delete.ts
deleted file mode 100644
index 7dbc05b4c9..0000000000
--- a/src/server/api/endpoints/admin/announcements/delete.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { Announcements } from '@/models/index';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		id: {
-			validator: $.type(ID)
-		}
-	},
-
-	errors: {
-		noSuchAnnouncement: {
-			message: 'No such announcement.',
-			code: 'NO_SUCH_ANNOUNCEMENT',
-			id: 'ecad8040-a276-4e85-bda9-015a708d291e'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const announcement = await Announcements.findOne(ps.id);
-
-	if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
-
-	await Announcements.delete(announcement.id);
-});
diff --git a/src/server/api/endpoints/admin/announcements/list.ts b/src/server/api/endpoints/admin/announcements/list.ts
deleted file mode 100644
index 4039bcd88f..0000000000
--- a/src/server/api/endpoints/admin/announcements/list.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { Announcements, AnnouncementReads } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-					example: 'xxxxxxxxxx',
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time',
-				},
-				updatedAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'date-time',
-				},
-				text: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				title: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				imageUrl: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-				},
-				reads: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
-
-	const announcements = await query.take(ps.limit!).getMany();
-
-	for (const announcement of announcements) {
-		(announcement as any).reads = await AnnouncementReads.count({
-			announcementId: announcement.id
-		});
-	}
-
-	return announcements;
-});
diff --git a/src/server/api/endpoints/admin/announcements/update.ts b/src/server/api/endpoints/admin/announcements/update.ts
deleted file mode 100644
index 343f37d626..0000000000
--- a/src/server/api/endpoints/admin/announcements/update.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { Announcements } from '@/models/index';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		id: {
-			validator: $.type(ID)
-		},
-		title: {
-			validator: $.str.min(1)
-		},
-		text: {
-			validator: $.str.min(1)
-		},
-		imageUrl: {
-			validator: $.nullable.str.min(1)
-		}
-	},
-
-	errors: {
-		noSuchAnnouncement: {
-			message: 'No such announcement.',
-			code: 'NO_SUCH_ANNOUNCEMENT',
-			id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const announcement = await Announcements.findOne(ps.id);
-
-	if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
-
-	await Announcements.update(announcement.id, {
-		updatedAt: new Date(),
-		title: ps.title,
-		text: ps.text,
-		imageUrl: ps.imageUrl,
-	});
-});
diff --git a/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
deleted file mode 100644
index 988ab29558..0000000000
--- a/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { deleteFile } from '@/services/drive/delete-file';
-import { DriveFiles } from '@/models/index';
-import { ID } from '@/misc/cafy-id';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const files = await DriveFiles.find({
-		userId: ps.userId
-	});
-
-	for (const file of files) {
-		deleteFile(file);
-	}
-});
diff --git a/src/server/api/endpoints/admin/delete-logs.ts b/src/server/api/endpoints/admin/delete-logs.ts
deleted file mode 100644
index 9d37ceb434..0000000000
--- a/src/server/api/endpoints/admin/delete-logs.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import define from '../../define';
-import { Logs } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-};
-
-export default define(meta, async (ps) => {
-	await Logs.clear();	// TRUNCATE
-});
diff --git a/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/src/server/api/endpoints/admin/drive/clean-remote-files.ts
deleted file mode 100644
index 76a6acff59..0000000000
--- a/src/server/api/endpoints/admin/drive/clean-remote-files.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import define from '../../../define';
-import { createCleanRemoteFilesJob } from '@/queue/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-};
-
-export default define(meta, async (ps, me) => {
-	createCleanRemoteFilesJob();
-});
diff --git a/src/server/api/endpoints/admin/drive/cleanup.ts b/src/server/api/endpoints/admin/drive/cleanup.ts
deleted file mode 100644
index 8497478da9..0000000000
--- a/src/server/api/endpoints/admin/drive/cleanup.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { IsNull } from 'typeorm';
-import define from '../../../define';
-import { deleteFile } from '@/services/drive/delete-file';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-};
-
-export default define(meta, async (ps, me) => {
-	const files = await DriveFiles.find({
-		userId: IsNull()
-	});
-
-	for (const file of files) {
-		deleteFile(file);
-	}
-});
diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts
deleted file mode 100644
index fe1c799805..0000000000
--- a/src/server/api/endpoints/admin/drive/files.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { DriveFiles } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-import { ID } from '@/misc/cafy-id';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: false as const,
-	requireModerator: true,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		type: {
-			validator: $.optional.nullable.str.match(/^[a-zA-Z0-9\/\-*]+$/)
-		},
-
-		origin: {
-			validator: $.optional.str.or([
-				'combined',
-				'local',
-				'remote',
-			]),
-			default: 'local'
-		},
-
-		hostname: {
-			validator: $.optional.nullable.str,
-			default: null
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'DriveFile'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
-
-	if (ps.origin === 'local') {
-		query.andWhere('file.userHost IS NULL');
-	} else if (ps.origin === 'remote') {
-		query.andWhere('file.userHost IS NOT NULL');
-	}
-
-	if (ps.hostname) {
-		query.andWhere('file.userHost = :hostname', { hostname: ps.hostname });
-	}
-
-	if (ps.type) {
-		if (ps.type.endsWith('/*')) {
-			query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
-		} else {
-			query.andWhere('file.type = :type', { type: ps.type });
-		}
-	}
-
-	const files = await query.take(ps.limit!).getMany();
-
-	return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true });
-});
diff --git a/src/server/api/endpoints/admin/drive/show-file.ts b/src/server/api/endpoints/admin/drive/show-file.ts
deleted file mode 100644
index 270b89c4fa..0000000000
--- a/src/server/api/endpoints/admin/drive/show-file.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		fileId: {
-			validator: $.optional.type(ID),
-		},
-
-		url: {
-			validator: $.optional.str,
-		},
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'caf3ca38-c6e5-472e-a30c-b05377dcc240'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-				example: 'xxxxxxxxxx',
-			},
-			createdAt: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'date-time',
-			},
-			userId: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-				format: 'id',
-				example: 'xxxxxxxxxx',
-			},
-			userHost: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			md5: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'md5',
-				example: '15eca7fba0480996e2245f5185bf39f2'
-			},
-			name: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				example: 'lenna.jpg'
-			},
-			type: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				example: 'image/jpeg'
-			},
-			size: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-				example: 51469
-			},
-			comment: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			blurhash: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			properties: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				properties: {
-					width: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-						example: 1280
-					},
-					height: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-						example: 720
-					},
-					avgColor: {
-						type: 'string' as const,
-						optional: true as const, nullable: false as const,
-						example: 'rgb(40,65,87)'
-					}
-				}
-			},
-			storedInternal: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: true as const,
-				example: true
-			},
-			url: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-				format: 'url',
-			},
-			thumbnailUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-				format: 'url',
-			},
-			webpublicUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-				format: 'url',
-			},
-			accessKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			thumbnailAccessKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			webpublicAccessKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			uri: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			src: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			folderId: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-				format: 'id',
-				example: 'xxxxxxxxxx',
-			},
-			isSensitive: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-			},
-			isLink: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const file = ps.fileId ? await DriveFiles.findOne(ps.fileId) : await DriveFiles.findOne({
-		where: [{
-			url: ps.url
-		}, {
-			thumbnailUrl: ps.url
-		}, {
-			webpublicUrl: ps.url
-		}]
-	});
-
-	if (file == null) {
-		throw new ApiError(meta.errors.noSuchFile);
-	}
-
-	return file;
-});
diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts
deleted file mode 100644
index 1af81fe46d..0000000000
--- a/src/server/api/endpoints/admin/emoji/add.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Emojis, DriveFiles } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { getConnection } from 'typeorm';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-import { ApiError } from '../../../error';
-import { ID } from '@/misc/cafy-id';
-import rndstr from 'rndstr';
-import { publishBroadcastStream } from '@/services/stream';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		fileId: {
-			validator: $.type(ID)
-		},
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'MO_SUCH_FILE',
-			id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const file = await DriveFiles.findOne(ps.fileId);
-
-	if (file == null) throw new ApiError(meta.errors.noSuchFile);
-
-	const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
-
-	const emoji = await Emojis.save({
-		id: genId(),
-		updatedAt: new Date(),
-		name: name,
-		category: null,
-		host: null,
-		aliases: [],
-		url: file.url,
-		type: file.type,
-	});
-
-	await getConnection().queryResultCache!.remove(['meta_emojis']);
-
-	publishBroadcastStream('emojiAdded', {
-		emoji: await Emojis.pack(emoji.id)
-	});
-
-	insertModerationLog(me, 'addEmoji', {
-		emojiId: emoji.id
-	});
-
-	return {
-		id: emoji.id
-	};
-});
diff --git a/src/server/api/endpoints/admin/emoji/copy.ts b/src/server/api/endpoints/admin/emoji/copy.ts
deleted file mode 100644
index 4c8ab99f7c..0000000000
--- a/src/server/api/endpoints/admin/emoji/copy.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Emojis } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { getConnection } from 'typeorm';
-import { ApiError } from '../../../error';
-import { DriveFile } from '@/models/entities/drive-file';
-import { ID } from '@/misc/cafy-id';
-import uploadFromUrl from '@/services/drive/upload-from-url';
-import { publishBroadcastStream } from '@/services/stream';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		emojiId: {
-			validator: $.type(ID)
-		},
-	},
-
-	errors: {
-		noSuchEmoji: {
-			message: 'No such emoji.',
-			code: 'NO_SUCH_EMOJI',
-			id: 'e2785b66-dca3-4087-9cac-b93c541cc425'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id',
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const emoji = await Emojis.findOne(ps.emojiId);
-
-	if (emoji == null) {
-		throw new ApiError(meta.errors.noSuchEmoji);
-	}
-
-	let driveFile: DriveFile;
-
-	try {
-		// Create file
-		driveFile = await uploadFromUrl(emoji.url, null, null, null, false, true);
-	} catch (e) {
-		throw new ApiError();
-	}
-
-	const copied = await Emojis.insert({
-		id: genId(),
-		updatedAt: new Date(),
-		name: emoji.name,
-		host: null,
-		aliases: [],
-		url: driveFile.url,
-		type: driveFile.type,
-		fileId: driveFile.id,
-	}).then(x => Emojis.findOneOrFail(x.identifiers[0]));
-
-	await getConnection().queryResultCache!.remove(['meta_emojis']);
-
-	publishBroadcastStream('emojiAdded', {
-		emoji: await Emojis.pack(copied.id)
-	});
-
-	return {
-		id: copied.id
-	};
-});
diff --git a/src/server/api/endpoints/admin/emoji/list-remote.ts b/src/server/api/endpoints/admin/emoji/list-remote.ts
deleted file mode 100644
index 3c8ca22170..0000000000
--- a/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Emojis } from '@/models/index';
-import { toPuny } from '@/misc/convert-host';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-import { ID } from '@/misc/cafy-id';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		query: {
-			validator: $.optional.nullable.str,
-			default: null
-		},
-
-		host: {
-			validator: $.optional.nullable.str,
-			default: null
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				aliases: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
-					items: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const
-					}
-				},
-				name: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				category: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-				},
-				host: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-				},
-				url: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId);
-
-	if (ps.host == null) {
-		q.andWhere(`emoji.host IS NOT NULL`);
-	} else {
-		q.andWhere(`emoji.host = :host`, { host: toPuny(ps.host) });
-	}
-
-	if (ps.query) {
-		q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' });
-	}
-
-	const emojis = await q
-		.orderBy('emoji.id', 'DESC')
-		.take(ps.limit!)
-		.getMany();
-
-	return Emojis.packMany(emojis);
-});
diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts
deleted file mode 100644
index cb1e79e0fe..0000000000
--- a/src/server/api/endpoints/admin/emoji/list.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Emojis } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-import { ID } from '@/misc/cafy-id';
-import { Emoji } from '@/models/entities/emoji';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		query: {
-			validator: $.optional.nullable.str,
-			default: null
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-				},
-				aliases: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
-					items: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const
-					}
-				},
-				name: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				category: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-				},
-				host: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-				},
-				url: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId)
-		.andWhere(`emoji.host IS NULL`);
-
-	let emojis: Emoji[];
-
-	if (ps.query) {
-		//q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` });
-		//const emojis = await q.take(ps.limit!).getMany();
-
-		emojis = await q.getMany();
-
-		emojis = emojis.filter(emoji =>
-			emoji.name.includes(ps.query!) ||
-			emoji.aliases.some(a => a.includes(ps.query!)) ||
-			emoji.category?.includes(ps.query!));
-
-		emojis.splice(ps.limit! + 1);
-	} else {
-		emojis = await q.take(ps.limit!).getMany();
-	}
-
-	return Emojis.packMany(emojis);
-});
diff --git a/src/server/api/endpoints/admin/emoji/remove.ts b/src/server/api/endpoints/admin/emoji/remove.ts
deleted file mode 100644
index 259950e362..0000000000
--- a/src/server/api/endpoints/admin/emoji/remove.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { Emojis } from '@/models/index';
-import { getConnection } from 'typeorm';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		id: {
-			validator: $.type(ID)
-		}
-	},
-
-	errors: {
-		noSuchEmoji: {
-			message: 'No such emoji.',
-			code: 'NO_SUCH_EMOJI',
-			id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const emoji = await Emojis.findOne(ps.id);
-
-	if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
-
-	await Emojis.delete(emoji.id);
-
-	await getConnection().queryResultCache!.remove(['meta_emojis']);
-
-	insertModerationLog(me, 'removeEmoji', {
-		emoji: emoji
-	});
-});
diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts
deleted file mode 100644
index 3fd547d7e5..0000000000
--- a/src/server/api/endpoints/admin/emoji/update.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { Emojis } from '@/models/index';
-import { getConnection } from 'typeorm';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		id: {
-			validator: $.type(ID)
-		},
-
-		name: {
-			validator: $.str
-		},
-
-		category: {
-			validator: $.optional.nullable.str
-		},
-
-		aliases: {
-			validator: $.arr($.str)
-		}
-	},
-
-	errors: {
-		noSuchEmoji: {
-			message: 'No such emoji.',
-			code: 'NO_SUCH_EMOJI',
-			id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8'
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const emoji = await Emojis.findOne(ps.id);
-
-	if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
-
-	await Emojis.update(emoji.id, {
-		updatedAt: new Date(),
-		name: ps.name,
-		category: ps.category,
-		aliases: ps.aliases,
-	});
-
-	await getConnection().queryResultCache!.remove(['meta_emojis']);
-});
diff --git a/src/server/api/endpoints/admin/federation/delete-all-files.ts b/src/server/api/endpoints/admin/federation/delete-all-files.ts
deleted file mode 100644
index 82540c5447..0000000000
--- a/src/server/api/endpoints/admin/federation/delete-all-files.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { deleteFile } from '@/services/drive/delete-file';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		host: {
-			validator: $.str
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const files = await DriveFiles.find({
-		userHost: ps.host
-	});
-
-	for (const file of files) {
-		deleteFile(file);
-	}
-});
diff --git a/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
deleted file mode 100644
index 65a6947ba0..0000000000
--- a/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Instances } from '@/models/index';
-import { toPuny } from '@/misc/convert-host';
-import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		host: {
-			validator: $.str
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const instance = await Instances.findOne({ host: toPuny(ps.host) });
-
-	if (instance == null) {
-		throw new Error('instance not found');
-	}
-
-	fetchInstanceMetadata(instance, true);
-});
diff --git a/src/server/api/endpoints/admin/federation/remove-all-following.ts b/src/server/api/endpoints/admin/federation/remove-all-following.ts
deleted file mode 100644
index 7935eaa631..0000000000
--- a/src/server/api/endpoints/admin/federation/remove-all-following.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import deleteFollowing from '@/services/following/delete';
-import { Followings, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		host: {
-			validator: $.str
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const followings = await Followings.find({
-		followerHost: ps.host
-	});
-
-	const pairs = await Promise.all(followings.map(f => Promise.all([
-		Users.findOneOrFail(f.followerId),
-		Users.findOneOrFail(f.followeeId)
-	])));
-
-	for (const pair of pairs) {
-		deleteFollowing(pair[0], pair[1]);
-	}
-});
diff --git a/src/server/api/endpoints/admin/federation/update-instance.ts b/src/server/api/endpoints/admin/federation/update-instance.ts
deleted file mode 100644
index 34eab27c78..0000000000
--- a/src/server/api/endpoints/admin/federation/update-instance.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Instances } from '@/models/index';
-import { toPuny } from '@/misc/convert-host';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		host: {
-			validator: $.str
-		},
-
-		isSuspended: {
-			validator: $.bool
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const instance = await Instances.findOne({ host: toPuny(ps.host) });
-
-	if (instance == null) {
-		throw new Error('instance not found');
-	}
-
-	Instances.update({ host: toPuny(ps.host) }, {
-		isSuspended: ps.isSuspended
-	});
-});
diff --git a/src/server/api/endpoints/admin/get-index-stats.ts b/src/server/api/endpoints/admin/get-index-stats.ts
deleted file mode 100644
index f2b06d0ef2..0000000000
--- a/src/server/api/endpoints/admin/get-index-stats.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import define from '../../define';
-import { getConnection } from 'typeorm';
-
-export const meta = {
-	requireCredential: true as const,
-	requireModerator: true,
-
-	tags: ['admin'],
-
-	params: {
-	},
-};
-
-export default define(meta, async () => {
-	const stats = await
-		getConnection().query(`SELECT * FROM pg_indexes;`)
-		.then(recs => {
-			const res = [] as { tablename: string; indexname: string; }[];
-			for (const rec of recs) {
-				res.push(rec);
-			}
-			return res;
-		});
-
-	return stats;
-});
diff --git a/src/server/api/endpoints/admin/get-table-stats.ts b/src/server/api/endpoints/admin/get-table-stats.ts
deleted file mode 100644
index bce813232b..0000000000
--- a/src/server/api/endpoints/admin/get-table-stats.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import define from '../../define';
-import { getConnection } from 'typeorm';
-
-export const meta = {
-	requireCredential: true as const,
-	requireModerator: true,
-
-	tags: ['admin'],
-
-	params: {
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		example: {
-			migrations: {
-				count: 66,
-				size: 32768
-			},
-		}
-	}
-};
-
-export default define(meta, async () => {
-	const sizes = await
-		getConnection().query(`
-			SELECT relname AS "table", reltuples as "count", pg_total_relation_size(C.oid) AS "size"
-			FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
-			WHERE nspname NOT IN ('pg_catalog', 'information_schema')
-				AND C.relkind <> 'i'
-				AND nspname !~ '^pg_toast';`)
-		.then(recs => {
-			const res = {} as Record<string, { count: number; size: number; }>;
-			for (const rec of recs) {
-				res[rec.table] = {
-					count: parseInt(rec.count, 10),
-					size: parseInt(rec.size, 10),
-				};
-			}
-			return res;
-		});
-
-	return sizes;
-});
diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts
deleted file mode 100644
index 2c69eec535..0000000000
--- a/src/server/api/endpoints/admin/invite.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import rndstr from 'rndstr';
-import define from '../../define';
-import { RegistrationTickets } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			code: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				example: '2ERUA5VR',
-				maxLength: 8,
-				minLength: 8
-			}
-		}
-	}
-};
-
-export default define(meta, async () => {
-	const code = rndstr({
-		length: 8,
-		chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns)
-	});
-
-	await RegistrationTickets.insert({
-		id: genId(),
-		createdAt: new Date(),
-		code,
-	});
-
-	return {
-		code,
-	};
-});
diff --git a/src/server/api/endpoints/admin/moderators/add.ts b/src/server/api/endpoints/admin/moderators/add.ts
deleted file mode 100644
index 2b87fc217f..0000000000
--- a/src/server/api/endpoints/admin/moderators/add.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireAdmin: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps) => {
-	const user = await Users.findOne(ps.userId as string);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	if (user.isAdmin) {
-		throw new Error('cannot mark as moderator if admin user');
-	}
-
-	await Users.update(user.id, {
-		isModerator: true
-	});
-});
diff --git a/src/server/api/endpoints/admin/moderators/remove.ts b/src/server/api/endpoints/admin/moderators/remove.ts
deleted file mode 100644
index cbb0625224..0000000000
--- a/src/server/api/endpoints/admin/moderators/remove.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireAdmin: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps) => {
-	const user = await Users.findOne(ps.userId as string);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	await Users.update(user.id, {
-		isModerator: false
-	});
-});
diff --git a/src/server/api/endpoints/admin/promo/create.ts b/src/server/api/endpoints/admin/promo/create.ts
deleted file mode 100644
index 3bdaaad4d9..0000000000
--- a/src/server/api/endpoints/admin/promo/create.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getNote } from '../../../common/getters';
-import { PromoNotes } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-
-		expiresAt: {
-			validator: $.num.int()
-		},
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'ee449fbe-af2a-453b-9cae-cf2fe7c895fc'
-		},
-
-		alreadyPromoted: {
-			message: 'The note has already promoted.',
-			code: 'ALREADY_PROMOTED',
-			id: 'ae427aa2-7a41-484f-a18c-2c1104051604'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const exist = await PromoNotes.findOne(note.id);
-
-	if (exist != null) {
-		throw new ApiError(meta.errors.alreadyPromoted);
-	}
-
-	await PromoNotes.insert({
-		noteId: note.id,
-		createdAt: new Date(),
-		expiresAt: new Date(ps.expiresAt),
-		userId: note.userId,
-	});
-});
diff --git a/src/server/api/endpoints/admin/queue/clear.ts b/src/server/api/endpoints/admin/queue/clear.ts
deleted file mode 100644
index fedb7065ab..0000000000
--- a/src/server/api/endpoints/admin/queue/clear.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import define from '../../../define';
-import { destroy } from '@/queue/index';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {}
-};
-
-export default define(meta, async (ps, me) => {
-	destroy();
-
-	insertModerationLog(me, 'clearQueue');
-});
diff --git a/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/src/server/api/endpoints/admin/queue/deliver-delayed.ts
deleted file mode 100644
index cd7b640983..0000000000
--- a/src/server/api/endpoints/admin/queue/deliver-delayed.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { deliverQueue } from '@/queue/queues';
-import { URL } from 'url';
-import define from '../../../define';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				anyOf: [
-					{
-						type: 'string' as const,
-					},
-					{
-						type: 'number' as const,
-					}
-				]
-			}
-		},
-		example: [[
-			'example.com',
-			12
-		]]
-	}
-};
-
-export default define(meta, async (ps) => {
-	const jobs = await deliverQueue.getJobs(['delayed']);
-
-	const res = [] as [string, number][];
-
-	for (const job of jobs) {
-		const host = new URL(job.data.to).host;
-		if (res.find(x => x[0] === host)) {
-			res.find(x => x[0] === host)![1]++;
-		} else {
-			res.push([host, 1]);
-		}
-	}
-
-	res.sort((a, b) => b[1] - a[1]);
-
-	return res;
-});
diff --git a/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/src/server/api/endpoints/admin/queue/inbox-delayed.ts
deleted file mode 100644
index 1925906c28..0000000000
--- a/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { URL } from 'url';
-import define from '../../../define';
-import { inboxQueue } from '@/queue/queues';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				anyOf: [
-					{
-						type: 'string' as const,
-					},
-					{
-						type: 'number' as const,
-					}
-				]
-			}
-		},
-		example: [[
-			'example.com',
-			12
-		]]
-	}
-};
-
-export default define(meta, async (ps) => {
-	const jobs = await inboxQueue.getJobs(['delayed']);
-
-	const res = [] as [string, number][];
-
-	for (const job of jobs) {
-		const host = new URL(job.data.signature.keyId).host;
-		if (res.find(x => x[0] === host)) {
-			res.find(x => x[0] === host)![1]++;
-		} else {
-			res.push([host, 1]);
-		}
-	}
-
-	res.sort((a, b) => b[1] - a[1]);
-
-	return res;
-});
diff --git a/src/server/api/endpoints/admin/queue/jobs.ts b/src/server/api/endpoints/admin/queue/jobs.ts
deleted file mode 100644
index c426e5f39b..0000000000
--- a/src/server/api/endpoints/admin/queue/jobs.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues';
-import $ from 'cafy';
-import define from '../../../define';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		domain: {
-			validator: $.str.or(['deliver', 'inbox', 'db', 'objectStorage']),
-		},
-
-		state: {
-			validator: $.str.or(['active', 'waiting', 'delayed']),
-		},
-
-		limit: {
-			validator: $.optional.num,
-			default: 50
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				data: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const
-				},
-				attempts: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const
-				},
-				maxAttempts: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const
-				},
-				timestamp: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const queue =
-		ps.domain === 'deliver' ? deliverQueue :
-		ps.domain === 'inbox' ? inboxQueue :
-		ps.domain === 'db' ? dbQueue :
-		ps.domain === 'objectStorage' ? objectStorageQueue :
-		null as never;
-
-	const jobs = await queue.getJobs([ps.state], 0, ps.limit!);
-
-	return jobs.map(job => {
-		const data = job.data;
-		delete data.content;
-		delete data.user;
-		return {
-			id: job.id,
-			data,
-			attempts: job.attemptsMade,
-			maxAttempts: job.opts ? job.opts.attempts : 0,
-			timestamp: job.timestamp,
-		};
-	});
-});
diff --git a/src/server/api/endpoints/admin/queue/stats.ts b/src/server/api/endpoints/admin/queue/stats.ts
deleted file mode 100644
index 38f18459dd..0000000000
--- a/src/server/api/endpoints/admin/queue/stats.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues';
-import define from '../../../define';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			deliver: {
-				ref: 'QueueCount'
-			},
-			inbox: {
-				ref: 'QueueCount'
-			},
-			db: {
-				ref: 'QueueCount'
-			},
-			objectStorage: {
-				ref: 'QueueCount'
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const deliverJobCounts = await deliverQueue.getJobCounts();
-	const inboxJobCounts = await inboxQueue.getJobCounts();
-	const dbJobCounts = await dbQueue.getJobCounts();
-	const objectStorageJobCounts = await objectStorageQueue.getJobCounts();
-
-	return {
-		deliver: deliverJobCounts,
-		inbox: inboxJobCounts,
-		db: dbJobCounts,
-		objectStorage: objectStorageJobCounts,
-	};
-});
diff --git a/src/server/api/endpoints/admin/relays/add.ts b/src/server/api/endpoints/admin/relays/add.ts
deleted file mode 100644
index 567035fd3a..0000000000
--- a/src/server/api/endpoints/admin/relays/add.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { URL } from 'url';
-import $ from 'cafy';
-import define from '../../../define';
-import { addRelay } from '@/services/relay';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true as const,
-
-	params: {
-		inbox: {
-			validator: $.str
-		},
-	},
-
-	errors: {
-		invalidUrl: {
-			message: 'Invalid URL',
-			code: 'INVALID_URL',
-			id: 'fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			},
-			inbox: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'url'
-			},
-			status: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				default: 'requesting',
-				enum: [
-					'requesting',
-					'accepted',
-					'rejected'
-				]
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	try {
-		if (new URL(ps.inbox).protocol !== 'https:') throw 'https only';
-	} catch {
-		throw new ApiError(meta.errors.invalidUrl);
-	}
-
-	return await addRelay(ps.inbox);
-});
diff --git a/src/server/api/endpoints/admin/relays/list.ts b/src/server/api/endpoints/admin/relays/list.ts
deleted file mode 100644
index 031ebe85d0..0000000000
--- a/src/server/api/endpoints/admin/relays/list.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import define from '../../../define';
-import { listRelay } from '@/services/relay';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true as const,
-
-	params: {
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				inbox: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'url'
-				},
-				status: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					default: 'requesting',
-					enum: [
-						'requesting',
-						'accepted',
-						'rejected'
-					]
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	return await listRelay();
-});
diff --git a/src/server/api/endpoints/admin/relays/remove.ts b/src/server/api/endpoints/admin/relays/remove.ts
deleted file mode 100644
index c1c50f5dc0..0000000000
--- a/src/server/api/endpoints/admin/relays/remove.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { removeRelay } from '@/services/relay';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true as const,
-
-	params: {
-		inbox: {
-			validator: $.str
-		},
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	return await removeRelay(ps.inbox);
-});
diff --git a/src/server/api/endpoints/admin/reset-password.ts b/src/server/api/endpoints/admin/reset-password.ts
deleted file mode 100644
index 0fc2c6a868..0000000000
--- a/src/server/api/endpoints/admin/reset-password.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import * as bcrypt from 'bcryptjs';
-import rndstr from 'rndstr';
-import { Users, UserProfiles } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			password: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				minLength: 8,
-				maxLength: 8
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const user = await Users.findOne(ps.userId as string);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	if (user.isAdmin) {
-		throw new Error('cannot reset password of admin');
-	}
-
-	const passwd = rndstr('a-zA-Z0-9', 8);
-
-	// Generate hash of password
-	const hash = bcrypt.hashSync(passwd);
-
-	await UserProfiles.update({
-		userId: user.id
-	}, {
-		password: hash
-	});
-
-	return {
-		password: passwd
-	};
-});
diff --git a/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
deleted file mode 100644
index 7b71f8e000..0000000000
--- a/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { AbuseUserReports } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		reportId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const report = await AbuseUserReports.findOne(ps.reportId);
-
-	if (report == null) {
-		throw new Error('report not found');
-	}
-
-	await AbuseUserReports.update(report.id, {
-		resolved: true,
-		assigneeId: me.id,
-	});
-});
diff --git a/src/server/api/endpoints/admin/resync-chart.ts b/src/server/api/endpoints/admin/resync-chart.ts
deleted file mode 100644
index e01dfce1b6..0000000000
--- a/src/server/api/endpoints/admin/resync-chart.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import define from '../../define';
-import { driveChart, notesChart, usersChart } from '@/services/chart/index';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-};
-
-export default define(meta, async (ps, me) => {
-	insertModerationLog(me, 'chartResync');
-
-	driveChart.resync();
-	notesChart.resync();
-	usersChart.resync();
-
-	// TODO: ユーザーごとのチャートもキューに入れて更新する
-	// TODO: インスタンスごとのチャートもキューに入れて更新する
-});
diff --git a/src/server/api/endpoints/admin/send-email.ts b/src/server/api/endpoints/admin/send-email.ts
deleted file mode 100644
index 6f67b78542..0000000000
--- a/src/server/api/endpoints/admin/send-email.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { sendEmail } from '@/services/send-email';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		to: {
-			validator: $.str,
-		},
-		subject: {
-			validator: $.str,
-		},
-		text: {
-			validator: $.str,
-		},
-	}
-};
-
-export default define(meta, async (ps) => {
-	await sendEmail(ps.to, ps.subject, ps.text, ps.text);
-});
diff --git a/src/server/api/endpoints/admin/server-info.ts b/src/server/api/endpoints/admin/server-info.ts
deleted file mode 100644
index bb2d35e397..0000000000
--- a/src/server/api/endpoints/admin/server-info.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import * as os from 'os';
-import * as si from 'systeminformation';
-import { getConnection } from 'typeorm';
-import define from '../../define';
-import { redisClient } from '../../../../db/redis';
-
-export const meta = {
-	requireCredential: true as const,
-	requireModerator: true,
-
-	tags: ['admin', 'meta'],
-
-	params: {
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			machine: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			os: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				example: 'linux'
-			},
-			node: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			psql: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			cpu: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				properties: {
-					model: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
-					},
-					cores: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-					}
-				}
-			},
-			mem: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				properties: {
-					total: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-						format: 'bytes',
-					}
-				}
-			},
-			fs: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				properties: {
-					total: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-						format: 'bytes',
-					},
-					used: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-						format: 'bytes',
-					}
-				}
-			},
-			net: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				properties: {
-					interface: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
-						example: 'eth0'
-					}
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async () => {
-	const memStats = await si.mem();
-	const fsStats = await si.fsSize();
-	const netInterface = await si.networkInterfaceDefault();
-
-	return {
-		machine: os.hostname(),
-		os: os.platform(),
-		node: process.version,
-		psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version),
-		redis: redisClient.server_info.redis_version,
-		cpu: {
-			model: os.cpus()[0].model,
-			cores: os.cpus().length
-		},
-		mem: {
-			total: memStats.total
-		},
-		fs: {
-			total: fsStats[0].size,
-			used: fsStats[0].used,
-		},
-		net: {
-			interface: netInterface
-		}
-	};
-});
diff --git a/src/server/api/endpoints/admin/show-moderation-logs.ts b/src/server/api/endpoints/admin/show-moderation-logs.ts
deleted file mode 100644
index e9509568d0..0000000000
--- a/src/server/api/endpoints/admin/show-moderation-logs.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ModerationLogs } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time'
-				},
-				type: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				},
-				info: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const
-				},
-				userId: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				user: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const query = makePaginationQuery(ModerationLogs.createQueryBuilder('report'), ps.sinceId, ps.untilId);
-
-	const reports = await query.take(ps.limit!).getMany();
-
-	return await ModerationLogs.packMany(reports);
-});
diff --git a/src/server/api/endpoints/admin/show-user.ts b/src/server/api/endpoints/admin/show-user.ts
deleted file mode 100644
index 963c123255..0000000000
--- a/src/server/api/endpoints/admin/show-user.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		nullable: false as const, optional: false as const,
-		properties: {
-			id: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				format: 'id'
-			},
-			createdAt: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				format: 'date-time'
-			},
-			updatedAt: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
-				format: 'date-time'
-			},
-			lastFetchedAt: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const
-			},
-			username: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const
-			},
-			name: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const
-			},
-			folowersCount: {
-				type: 'number' as const,
-				nullable: false as const, optional: false as const
-			},
-			followingCount: {
-				type: 'number' as const,
-				nullable: false as const, optional: false as const
-			},
-			notesCount: {
-				type: 'number' as const,
-				nullable: false as const, optional: false as const
-			},
-			avatarId: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const
-			},
-			bannerId: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const
-			},
-			tags: {
-				type: 'array' as const,
-				nullable: false as const, optional: false as const,
-				items: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const
-				}
-			},
-			avatarUrl: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
-				format: 'url'
-			},
-			bannerUrl: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const,
-				format: 'url'
-			},
-			avatarBlurhash: {
-				type: 'any' as const,
-				nullable: true as const, optional: false as const,
-				default: null
-			},
-			bannerBlurhash: {
-				type: 'any' as const,
-				nullable: true as const, optional: false as const,
-				default: null
-			},
-			isSuspended: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const
-			},
-			isSilenced: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const
-			},
-			isLocked: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const,
-			},
-			isBot: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const
-			},
-			isCat: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const
-			},
-			isAdmin: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const
-			},
-			isModerator: {
-				type: 'boolean' as const,
-				nullable: false as const, optional: false as const
-			},
-			emojis: {
-				type: 'array' as const,
-				nullable: false as const, optional: false as const,
-				items: {
-					type: 'string' as const,
-					nullable: false as const, optional: false as const
-				}
-			},
-			host: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const
-			},
-			inbox: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const
-			},
-			sharedInbox: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const
-			},
-			featured: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const
-			},
-			uri: {
-				type: 'string' as const,
-				nullable: true as const, optional: false as const
-			},
-			token: {
-				type: 'string' as const,
-				nullable: false as const, optional: false as const,
-				default: '<MASKED>'
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId as string);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	if ((me.isModerator && !me.isAdmin) && user.isAdmin) {
-		throw new Error('cannot show info of admin');
-	}
-
-	return {
-		...user,
-		token: user.token != null ? '<MASKED>' : user.token,
-	};
-});
diff --git a/src/server/api/endpoints/admin/show-users.ts b/src/server/api/endpoints/admin/show-users.ts
deleted file mode 100644
index 20b63e7be6..0000000000
--- a/src/server/api/endpoints/admin/show-users.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		},
-
-		sort: {
-			validator: $.optional.str.or([
-				'+follower',
-				'-follower',
-				'+createdAt',
-				'-createdAt',
-				'+updatedAt',
-				'-updatedAt',
-			]),
-		},
-
-		state: {
-			validator: $.optional.str.or([
-				'all',
-				'available',
-				'admin',
-				'moderator',
-				'adminOrModerator',
-				'silenced',
-				'suspended',
-			]),
-			default: 'all'
-		},
-
-		origin: {
-			validator: $.optional.str.or([
-				'combined',
-				'local',
-				'remote',
-			]),
-			default: 'local'
-		},
-
-		username: {
-			validator: $.optional.str,
-			default: null
-		},
-
-		hostname: {
-			validator: $.optional.str,
-			default: null
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		nullable: false as const, optional: false as const,
-		items: {
-			type: 'object' as const,
-			nullable: false as const, optional: false as const,
-			ref: 'User'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const query = Users.createQueryBuilder('user');
-
-	switch (ps.state) {
-		case 'available': query.where('user.isSuspended = FALSE'); break;
-		case 'admin': query.where('user.isAdmin = TRUE'); break;
-		case 'moderator': query.where('user.isModerator = TRUE'); break;
-		case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
-		case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
-		case 'silenced': query.where('user.isSilenced = TRUE'); break;
-		case 'suspended': query.where('user.isSuspended = TRUE'); break;
-	}
-
-	switch (ps.origin) {
-		case 'local': query.andWhere('user.host IS NULL'); break;
-		case 'remote': query.andWhere('user.host IS NOT NULL'); break;
-	}
-
-	if (ps.username) {
-		query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' });
-	}
-
-	if (ps.hostname) {
-		query.andWhere('user.host like :hostname', { hostname: '%' + ps.hostname.toLowerCase() + '%' });
-	}
-
-	switch (ps.sort) {
-		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
-		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
-		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
-		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
-		case '+updatedAt': query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); break;
-		case '-updatedAt': query.orderBy('user.updatedAt', 'ASC', 'NULLS FIRST'); break;
-		default: query.orderBy('user.id', 'ASC'); break;
-	}
-
-	query.take(ps.limit!);
-	query.skip(ps.offset);
-
-	const users = await query.getMany();
-
-	return await Users.packMany(users, me, { detail: true });
-});
diff --git a/src/server/api/endpoints/admin/silence-user.ts b/src/server/api/endpoints/admin/silence-user.ts
deleted file mode 100644
index 9bfed2310a..0000000000
--- a/src/server/api/endpoints/admin/silence-user.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Users } from '@/models/index';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId as string);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	if (user.isAdmin) {
-		throw new Error('cannot silence admin');
-	}
-
-	await Users.update(user.id, {
-		isSilenced: true
-	});
-
-	insertModerationLog(me, 'silence', {
-		targetId: user.id,
-	});
-});
diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts
deleted file mode 100644
index 364f258ce8..0000000000
--- a/src/server/api/endpoints/admin/suspend-user.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import deleteFollowing from '@/services/following/delete';
-import { Users, Followings, Notifications } from '@/models/index';
-import { User } from '@/models/entities/user';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-import { doPostSuspend } from '@/services/suspend-user';
-import { publishUserEvent } from '@/services/stream';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId as string);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	if (user.isAdmin) {
-		throw new Error('cannot suspend admin');
-	}
-
-	if (user.isModerator) {
-		throw new Error('cannot suspend moderator');
-	}
-
-	await Users.update(user.id, {
-		isSuspended: true
-	});
-
-	insertModerationLog(me, 'suspend', {
-		targetId: user.id,
-	});
-
-	// Terminate streaming
-	if (Users.isLocalUser(user)) {
-		publishUserEvent(user.id, 'terminate', {});
-	}
-
-	(async () => {
-		await doPostSuspend(user).catch(e => {});
-		await unFollowAll(user).catch(e => {});
-		await readAllNotify(user).catch(e => {});
-	})();
-});
-
-async function unFollowAll(follower: User) {
-	const followings = await Followings.find({
-		followerId: follower.id
-	});
-
-	for (const following of followings) {
-		const followee = await Users.findOne({
-			id: following.followeeId
-		});
-
-		if (followee == null) {
-			throw `Cant find followee ${following.followeeId}`;
-		}
-
-		await deleteFollowing(follower, followee, true);
-	}
-}
-
-async function readAllNotify(notifier: User) {
-	await Notifications.update({
-		notifierId: notifier.id,
-		isRead: false,
-	}, {
-		isRead: true
-	});
-}
diff --git a/src/server/api/endpoints/admin/unsilence-user.ts b/src/server/api/endpoints/admin/unsilence-user.ts
deleted file mode 100644
index 9994fbf462..0000000000
--- a/src/server/api/endpoints/admin/unsilence-user.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Users } from '@/models/index';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId as string);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	await Users.update(user.id, {
-		isSilenced: false
-	});
-
-	insertModerationLog(me, 'unsilence', {
-		targetId: user.id,
-	});
-});
diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts
deleted file mode 100644
index ab4c2d3dfe..0000000000
--- a/src/server/api/endpoints/admin/unsuspend-user.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Users } from '@/models/index';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-import { doPostUnsuspend } from '@/services/unsuspend-user';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId as string);
-
-	if (user == null) {
-		throw new Error('user not found');
-	}
-
-	await Users.update(user.id, {
-		isSuspended: false
-	});
-
-	insertModerationLog(me, 'unsuspend', {
-		targetId: user.id,
-	});
-
-	doPostUnsuspend(user);
-});
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
deleted file mode 100644
index 55447098dc..0000000000
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ /dev/null
@@ -1,608 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { getConnection } from 'typeorm';
-import { Meta } from '@/models/entities/meta';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
-import { ID } from '@/misc/cafy-id';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireAdmin: true,
-
-	params: {
-		disableRegistration: {
-			validator: $.optional.nullable.bool,
-		},
-
-		disableLocalTimeline: {
-			validator: $.optional.nullable.bool,
-		},
-
-		disableGlobalTimeline: {
-			validator: $.optional.nullable.bool,
-		},
-
-		useStarForReactionFallback: {
-			validator: $.optional.nullable.bool,
-		},
-
-		pinnedUsers: {
-			validator: $.optional.nullable.arr($.str),
-		},
-
-		hiddenTags: {
-			validator: $.optional.nullable.arr($.str),
-		},
-
-		blockedHosts: {
-			validator: $.optional.nullable.arr($.str),
-		},
-
-		mascotImageUrl: {
-			validator: $.optional.nullable.str,
-		},
-
-		bannerUrl: {
-			validator: $.optional.nullable.str,
-		},
-
-		errorImageUrl: {
-			validator: $.optional.nullable.str,
-		},
-
-		iconUrl: {
-			validator: $.optional.nullable.str,
-		},
-
-		backgroundImageUrl: {
-			validator: $.optional.nullable.str,
-		},
-
-		logoImageUrl: {
-			validator: $.optional.nullable.str,
-		},
-
-		name: {
-			validator: $.optional.nullable.str,
-		},
-
-		description: {
-			validator: $.optional.nullable.str,
-		},
-
-		maxNoteTextLength: {
-			validator: $.optional.num.min(0).max(DB_MAX_NOTE_TEXT_LENGTH),
-		},
-
-		localDriveCapacityMb: {
-			validator: $.optional.num.min(0),
-		},
-
-		remoteDriveCapacityMb: {
-			validator: $.optional.num.min(0),
-		},
-
-		cacheRemoteFiles: {
-			validator: $.optional.bool,
-		},
-
-		proxyRemoteFiles: {
-			validator: $.optional.bool,
-		},
-
-		emailRequiredForSignup: {
-			validator: $.optional.bool,
-		},
-
-		enableHcaptcha: {
-			validator: $.optional.bool,
-		},
-
-		hcaptchaSiteKey: {
-			validator: $.optional.nullable.str,
-		},
-
-		hcaptchaSecretKey: {
-			validator: $.optional.nullable.str,
-		},
-
-		enableRecaptcha: {
-			validator: $.optional.bool,
-		},
-
-		recaptchaSiteKey: {
-			validator: $.optional.nullable.str,
-		},
-
-		recaptchaSecretKey: {
-			validator: $.optional.nullable.str,
-		},
-
-		proxyAccountId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		maintainerName: {
-			validator: $.optional.nullable.str,
-		},
-
-		maintainerEmail: {
-			validator: $.optional.nullable.str,
-		},
-
-		pinnedPages: {
-			validator: $.optional.arr($.str),
-		},
-
-		pinnedClipId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		langs: {
-			validator: $.optional.arr($.str),
-		},
-
-		summalyProxy: {
-			validator: $.optional.nullable.str,
-		},
-
-		deeplAuthKey: {
-			validator: $.optional.nullable.str,
-		},
-
-		deeplIsPro: {
-			validator: $.optional.bool,
-		},
-
-		enableTwitterIntegration: {
-			validator: $.optional.bool,
-		},
-
-		twitterConsumerKey: {
-			validator: $.optional.nullable.str,
-		},
-
-		twitterConsumerSecret: {
-			validator: $.optional.nullable.str,
-		},
-
-		enableGithubIntegration: {
-			validator: $.optional.bool,
-		},
-
-		githubClientId: {
-			validator: $.optional.nullable.str,
-		},
-
-		githubClientSecret: {
-			validator: $.optional.nullable.str,
-		},
-
-		enableDiscordIntegration: {
-			validator: $.optional.bool,
-		},
-
-		discordClientId: {
-			validator: $.optional.nullable.str,
-		},
-
-		discordClientSecret: {
-			validator: $.optional.nullable.str,
-		},
-
-		enableEmail: {
-			validator: $.optional.bool,
-		},
-
-		email: {
-			validator: $.optional.nullable.str,
-		},
-
-		smtpSecure: {
-			validator: $.optional.bool,
-		},
-
-		smtpHost: {
-			validator: $.optional.nullable.str,
-		},
-
-		smtpPort: {
-			validator: $.optional.nullable.num,
-		},
-
-		smtpUser: {
-			validator: $.optional.nullable.str,
-		},
-
-		smtpPass: {
-			validator: $.optional.nullable.str,
-		},
-
-		enableServiceWorker: {
-			validator: $.optional.bool,
-		},
-
-		swPublicKey: {
-			validator: $.optional.nullable.str,
-		},
-
-		swPrivateKey: {
-			validator: $.optional.nullable.str,
-		},
-
-		tosUrl: {
-			validator: $.optional.nullable.str,
-		},
-
-		repositoryUrl: {
-			validator: $.optional.str,
-		},
-
-		feedbackUrl: {
-			validator: $.optional.str,
-		},
-
-		useObjectStorage: {
-			validator: $.optional.bool
-		},
-
-		objectStorageBaseUrl: {
-			validator: $.optional.nullable.str
-		},
-
-		objectStorageBucket: {
-			validator: $.optional.nullable.str
-		},
-
-		objectStoragePrefix: {
-			validator: $.optional.nullable.str
-		},
-
-		objectStorageEndpoint: {
-			validator: $.optional.nullable.str
-		},
-
-		objectStorageRegion: {
-			validator: $.optional.nullable.str
-		},
-
-		objectStoragePort: {
-			validator: $.optional.nullable.num
-		},
-
-		objectStorageAccessKey: {
-			validator: $.optional.nullable.str
-		},
-
-		objectStorageSecretKey: {
-			validator: $.optional.nullable.str
-		},
-
-		objectStorageUseSSL: {
-			validator: $.optional.bool
-		},
-
-		objectStorageUseProxy: {
-			validator: $.optional.bool
-		},
-
-		objectStorageSetPublicRead: {
-			validator: $.optional.bool
-		},
-
-		objectStorageS3ForcePathStyle: {
-			validator: $.optional.bool
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const set = {} as Partial<Meta>;
-
-	if (typeof ps.disableRegistration === 'boolean') {
-		set.disableRegistration = ps.disableRegistration;
-	}
-
-	if (typeof ps.disableLocalTimeline === 'boolean') {
-		set.disableLocalTimeline = ps.disableLocalTimeline;
-	}
-
-	if (typeof ps.disableGlobalTimeline === 'boolean') {
-		set.disableGlobalTimeline = ps.disableGlobalTimeline;
-	}
-
-	if (typeof ps.useStarForReactionFallback === 'boolean') {
-		set.useStarForReactionFallback = ps.useStarForReactionFallback;
-	}
-
-	if (Array.isArray(ps.pinnedUsers)) {
-		set.pinnedUsers = ps.pinnedUsers.filter(Boolean);
-	}
-
-	if (Array.isArray(ps.hiddenTags)) {
-		set.hiddenTags = ps.hiddenTags.filter(Boolean);
-	}
-
-	if (Array.isArray(ps.blockedHosts)) {
-		set.blockedHosts = ps.blockedHosts.filter(Boolean);
-	}
-
-	if (ps.mascotImageUrl !== undefined) {
-		set.mascotImageUrl = ps.mascotImageUrl;
-	}
-
-	if (ps.bannerUrl !== undefined) {
-		set.bannerUrl = ps.bannerUrl;
-	}
-
-	if (ps.iconUrl !== undefined) {
-		set.iconUrl = ps.iconUrl;
-	}
-
-	if (ps.backgroundImageUrl !== undefined) {
-		set.backgroundImageUrl = ps.backgroundImageUrl;
-	}
-
-	if (ps.logoImageUrl !== undefined) {
-		set.logoImageUrl = ps.logoImageUrl;
-	}
-
-	if (ps.name !== undefined) {
-		set.name = ps.name;
-	}
-
-	if (ps.description !== undefined) {
-		set.description = ps.description;
-	}
-
-	if (ps.maxNoteTextLength) {
-		set.maxNoteTextLength = ps.maxNoteTextLength;
-	}
-
-	if (ps.localDriveCapacityMb !== undefined) {
-		set.localDriveCapacityMb = ps.localDriveCapacityMb;
-	}
-
-	if (ps.remoteDriveCapacityMb !== undefined) {
-		set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
-	}
-
-	if (ps.cacheRemoteFiles !== undefined) {
-		set.cacheRemoteFiles = ps.cacheRemoteFiles;
-	}
-
-	if (ps.proxyRemoteFiles !== undefined) {
-		set.proxyRemoteFiles = ps.proxyRemoteFiles;
-	}
-
-	if (ps.emailRequiredForSignup !== undefined) {
-		set.emailRequiredForSignup = ps.emailRequiredForSignup;
-	}
-
-	if (ps.enableHcaptcha !== undefined) {
-		set.enableHcaptcha = ps.enableHcaptcha;
-	}
-
-	if (ps.hcaptchaSiteKey !== undefined) {
-		set.hcaptchaSiteKey = ps.hcaptchaSiteKey;
-	}
-
-	if (ps.hcaptchaSecretKey !== undefined) {
-		set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
-	}
-
-	if (ps.enableRecaptcha !== undefined) {
-		set.enableRecaptcha = ps.enableRecaptcha;
-	}
-
-	if (ps.recaptchaSiteKey !== undefined) {
-		set.recaptchaSiteKey = ps.recaptchaSiteKey;
-	}
-
-	if (ps.recaptchaSecretKey !== undefined) {
-		set.recaptchaSecretKey = ps.recaptchaSecretKey;
-	}
-
-	if (ps.proxyAccountId !== undefined) {
-		set.proxyAccountId = ps.proxyAccountId;
-	}
-
-	if (ps.maintainerName !== undefined) {
-		set.maintainerName = ps.maintainerName;
-	}
-
-	if (ps.maintainerEmail !== undefined) {
-		set.maintainerEmail = ps.maintainerEmail;
-	}
-
-	if (Array.isArray(ps.langs)) {
-		set.langs = ps.langs.filter(Boolean);
-	}
-
-	if (Array.isArray(ps.pinnedPages)) {
-		set.pinnedPages = ps.pinnedPages.filter(Boolean);
-	}
-
-	if (ps.pinnedClipId !== undefined) {
-		set.pinnedClipId = ps.pinnedClipId;
-	}
-
-	if (ps.summalyProxy !== undefined) {
-		set.summalyProxy = ps.summalyProxy;
-	}
-
-	if (ps.enableTwitterIntegration !== undefined) {
-		set.enableTwitterIntegration = ps.enableTwitterIntegration;
-	}
-
-	if (ps.twitterConsumerKey !== undefined) {
-		set.twitterConsumerKey = ps.twitterConsumerKey;
-	}
-
-	if (ps.twitterConsumerSecret !== undefined) {
-		set.twitterConsumerSecret = ps.twitterConsumerSecret;
-	}
-
-	if (ps.enableGithubIntegration !== undefined) {
-		set.enableGithubIntegration = ps.enableGithubIntegration;
-	}
-
-	if (ps.githubClientId !== undefined) {
-		set.githubClientId = ps.githubClientId;
-	}
-
-	if (ps.githubClientSecret !== undefined) {
-		set.githubClientSecret = ps.githubClientSecret;
-	}
-
-	if (ps.enableDiscordIntegration !== undefined) {
-		set.enableDiscordIntegration = ps.enableDiscordIntegration;
-	}
-
-	if (ps.discordClientId !== undefined) {
-		set.discordClientId = ps.discordClientId;
-	}
-
-	if (ps.discordClientSecret !== undefined) {
-		set.discordClientSecret = ps.discordClientSecret;
-	}
-
-	if (ps.enableEmail !== undefined) {
-		set.enableEmail = ps.enableEmail;
-	}
-
-	if (ps.email !== undefined) {
-		set.email = ps.email;
-	}
-
-	if (ps.smtpSecure !== undefined) {
-		set.smtpSecure = ps.smtpSecure;
-	}
-
-	if (ps.smtpHost !== undefined) {
-		set.smtpHost = ps.smtpHost;
-	}
-
-	if (ps.smtpPort !== undefined) {
-		set.smtpPort = ps.smtpPort;
-	}
-
-	if (ps.smtpUser !== undefined) {
-		set.smtpUser = ps.smtpUser;
-	}
-
-	if (ps.smtpPass !== undefined) {
-		set.smtpPass = ps.smtpPass;
-	}
-
-	if (ps.errorImageUrl !== undefined) {
-		set.errorImageUrl = ps.errorImageUrl;
-	}
-
-	if (ps.enableServiceWorker !== undefined) {
-		set.enableServiceWorker = ps.enableServiceWorker;
-	}
-
-	if (ps.swPublicKey !== undefined) {
-		set.swPublicKey = ps.swPublicKey;
-	}
-
-	if (ps.swPrivateKey !== undefined) {
-		set.swPrivateKey = ps.swPrivateKey;
-	}
-
-	if (ps.tosUrl !== undefined) {
-		set.ToSUrl = ps.tosUrl;
-	}
-
-	if (ps.repositoryUrl !== undefined) {
-		set.repositoryUrl = ps.repositoryUrl;
-	}
-
-	if (ps.feedbackUrl !== undefined) {
-		set.feedbackUrl = ps.feedbackUrl;
-	}
-
-	if (ps.useObjectStorage !== undefined) {
-		set.useObjectStorage = ps.useObjectStorage;
-	}
-
-	if (ps.objectStorageBaseUrl !== undefined) {
-		set.objectStorageBaseUrl = ps.objectStorageBaseUrl;
-	}
-
-	if (ps.objectStorageBucket !== undefined) {
-		set.objectStorageBucket = ps.objectStorageBucket;
-	}
-
-	if (ps.objectStoragePrefix !== undefined) {
-		set.objectStoragePrefix = ps.objectStoragePrefix;
-	}
-
-	if (ps.objectStorageEndpoint !== undefined) {
-		set.objectStorageEndpoint = ps.objectStorageEndpoint;
-	}
-
-	if (ps.objectStorageRegion !== undefined) {
-		set.objectStorageRegion = ps.objectStorageRegion;
-	}
-
-	if (ps.objectStoragePort !== undefined) {
-		set.objectStoragePort = ps.objectStoragePort;
-	}
-
-	if (ps.objectStorageAccessKey !== undefined) {
-		set.objectStorageAccessKey = ps.objectStorageAccessKey;
-	}
-
-	if (ps.objectStorageSecretKey !== undefined) {
-		set.objectStorageSecretKey = ps.objectStorageSecretKey;
-	}
-
-	if (ps.objectStorageUseSSL !== undefined) {
-		set.objectStorageUseSSL = ps.objectStorageUseSSL;
-	}
-
-	if (ps.objectStorageUseProxy !== undefined) {
-		set.objectStorageUseProxy = ps.objectStorageUseProxy;
-	}
-
-	if (ps.objectStorageSetPublicRead !== undefined) {
-		set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead;
-	}
-
-	if (ps.objectStorageS3ForcePathStyle !== undefined) {
-		set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
-	}
-
-	if (ps.deeplAuthKey !== undefined) {
-		if (ps.deeplAuthKey === '') {
-			set.deeplAuthKey = null;
-		} else {
-			set.deeplAuthKey = ps.deeplAuthKey;
-		}
-	}
-
-	if (ps.deeplIsPro !== undefined) {
-		set.deeplIsPro = ps.deeplIsPro;
-	}
-
-	await getConnection().transaction(async transactionalEntityManager => {
-		const meta = await transactionalEntityManager.findOne(Meta, {
-			order: {
-				id: 'DESC'
-			}
-		});
-
-		if (meta) {
-			await transactionalEntityManager.update(Meta, meta.id, set);
-		} else {
-			await transactionalEntityManager.save(Meta, set);
-		}
-	});
-
-	insertModerationLog(me, 'updateMeta');
-});
diff --git a/src/server/api/endpoints/admin/vacuum.ts b/src/server/api/endpoints/admin/vacuum.ts
deleted file mode 100644
index 9a80d88c44..0000000000
--- a/src/server/api/endpoints/admin/vacuum.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { getConnection } from 'typeorm';
-import { insertModerationLog } from '@/services/insert-moderation-log';
-
-export const meta = {
-	tags: ['admin'],
-
-	requireCredential: true as const,
-	requireModerator: true,
-
-	params: {
-		full: {
-			validator: $.bool,
-		},
-		analyze: {
-			validator: $.bool,
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const params: string[] = [];
-
-	if (ps.full) {
-		params.push('FULL');
-	}
-
-	if (ps.analyze) {
-		params.push('ANALYZE');
-	}
-
-	getConnection().query('VACUUM ' + params.join(' '));
-
-	insertModerationLog(me, 'vacuum', ps);
-});
diff --git a/src/server/api/endpoints/announcements.ts b/src/server/api/endpoints/announcements.ts
deleted file mode 100644
index a67737b2ff..0000000000
--- a/src/server/api/endpoints/announcements.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../define';
-import { Announcements, AnnouncementReads } from '@/models/index';
-import { makePaginationQuery } from '../common/make-pagination-query';
-
-export const meta = {
-	tags: ['meta'],
-
-	requireCredential: false as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		withUnreads: {
-			validator: $.optional.boolean,
-			default: false
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id',
-					example: 'xxxxxxxxxx',
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time',
-				},
-				updatedAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'date-time',
-				},
-				text: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				title: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				imageUrl: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-				},
-				isRead: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
-
-	const announcements = await query.take(ps.limit!).getMany();
-
-	if (user) {
-		const reads = (await AnnouncementReads.find({
-			userId: user.id
-		})).map(x => x.announcementId);
-
-		for (const announcement of announcements) {
-			(announcement as any).isRead = reads.includes(announcement.id);
-		}
-	}
-
-	return ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements;
-});
diff --git a/src/server/api/endpoints/antennas/create.ts b/src/server/api/endpoints/antennas/create.ts
deleted file mode 100644
index 4bdae8cc33..0000000000
--- a/src/server/api/endpoints/antennas/create.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { genId } from '@/misc/gen-id';
-import { Antennas, UserLists, UserGroupJoinings } from '@/models/index';
-import { ID } from '@/misc/cafy-id';
-import { ApiError } from '../../error';
-import { publishInternalEvent } from '@/services/stream';
-
-export const meta = {
-	tags: ['antennas'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		name: {
-			validator: $.str.range(1, 100)
-		},
-
-		src: {
-			validator: $.str.or(['home', 'all', 'users', 'list', 'group'])
-		},
-
-		userListId: {
-			validator: $.nullable.optional.type(ID),
-		},
-
-		userGroupId: {
-			validator: $.nullable.optional.type(ID),
-		},
-
-		keywords: {
-			validator: $.arr($.arr($.str))
-		},
-
-		excludeKeywords: {
-			validator: $.arr($.arr($.str))
-		},
-
-		users: {
-			validator: $.arr($.str)
-		},
-
-		caseSensitive: {
-			validator: $.bool
-		},
-
-		withReplies: {
-			validator: $.bool
-		},
-
-		withFile: {
-			validator: $.bool
-		},
-
-		notify: {
-			validator: $.bool
-		}
-	},
-
-	errors: {
-		noSuchUserList: {
-			message: 'No such user list.',
-			code: 'NO_SUCH_USER_LIST',
-			id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f'
-		},
-
-		noSuchUserGroup: {
-			message: 'No such user group.',
-			code: 'NO_SUCH_USER_GROUP',
-			id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Antenna'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	let userList;
-	let userGroupJoining;
-
-	if (ps.src === 'list' && ps.userListId) {
-		userList = await UserLists.findOne({
-			id: ps.userListId,
-			userId: user.id,
-		});
-
-		if (userList == null) {
-			throw new ApiError(meta.errors.noSuchUserList);
-		}
-	} else if (ps.src === 'group' && ps.userGroupId) {
-		userGroupJoining = await UserGroupJoinings.findOne({
-			userGroupId: ps.userGroupId,
-			userId: user.id,
-		});
-
-		if (userGroupJoining == null) {
-			throw new ApiError(meta.errors.noSuchUserGroup);
-		}
-	}
-
-	const antenna = await Antennas.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		name: ps.name,
-		src: ps.src,
-		userListId: userList ? userList.id : null,
-		userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
-		keywords: ps.keywords,
-		excludeKeywords: ps.excludeKeywords,
-		users: ps.users,
-		caseSensitive: ps.caseSensitive,
-		withReplies: ps.withReplies,
-		withFile: ps.withFile,
-		notify: ps.notify,
-	}).then(x => Antennas.findOneOrFail(x.identifiers[0]));
-
-	publishInternalEvent('antennaCreated', antenna);
-
-	return await Antennas.pack(antenna);
-});
diff --git a/src/server/api/endpoints/antennas/delete.ts b/src/server/api/endpoints/antennas/delete.ts
deleted file mode 100644
index 1cd136183e..0000000000
--- a/src/server/api/endpoints/antennas/delete.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Antennas } from '@/models/index';
-import { publishInternalEvent } from '@/services/stream';
-
-export const meta = {
-	tags: ['antennas'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		antennaId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchAntenna: {
-			message: 'No such antenna.',
-			code: 'NO_SUCH_ANTENNA',
-			id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const antenna = await Antennas.findOne({
-		id: ps.antennaId,
-		userId: user.id
-	});
-
-	if (antenna == null) {
-		throw new ApiError(meta.errors.noSuchAntenna);
-	}
-
-	await Antennas.delete(antenna.id);
-
-	publishInternalEvent('antennaDeleted', antenna);
-});
diff --git a/src/server/api/endpoints/antennas/list.ts b/src/server/api/endpoints/antennas/list.ts
deleted file mode 100644
index 8baae8435b..0000000000
--- a/src/server/api/endpoints/antennas/list.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import define from '../../define';
-import { Antennas } from '@/models/index';
-
-export const meta = {
-	tags: ['antennas', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:account',
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Antenna'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const antennas = await Antennas.find({
-		userId: me.id,
-	});
-
-	return await Promise.all(antennas.map(x => Antennas.pack(x)));
-});
diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts
deleted file mode 100644
index 1759e95b4c..0000000000
--- a/src/server/api/endpoints/antennas/notes.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import readNote from '@/services/note/read';
-import { Antennas, Notes, AntennaNotes } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { ApiError } from '../../error';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['antennas', 'account', 'notes'],
-
-	requireCredential: true as const,
-
-	kind: 'read:account',
-
-	params: {
-		antennaId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchAntenna: {
-			message: 'No such antenna.',
-			code: 'NO_SUCH_ANTENNA',
-			id: '850926e0-fd3b-49b6-b69a-b28a5dbd82fe'
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const antenna = await Antennas.findOne({
-		id: ps.antennaId,
-		userId: user.id
-	});
-
-	if (antenna == null) {
-		throw new ApiError(meta.errors.noSuchAntenna);
-	}
-
-	const antennaQuery = AntennaNotes.createQueryBuilder('joining')
-		.select('joining.noteId')
-		.where('joining.antennaId = :antennaId', { antennaId: antenna.id });
-
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.andWhere(`note.id IN (${ antennaQuery.getQuery() })`)
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser')
-		.setParameters(antennaQuery.getParameters());
-
-	generateVisibilityQuery(query, user);
-	generateMutedUserQuery(query, user);
-	generateBlockedUserQuery(query, user);
-
-	const notes = await query
-		.take(ps.limit!)
-		.getMany();
-
-	if (notes.length > 0) {
-		readNote(user.id, notes);
-	}
-
-	return await Notes.packMany(notes, user);
-});
diff --git a/src/server/api/endpoints/antennas/show.ts b/src/server/api/endpoints/antennas/show.ts
deleted file mode 100644
index 3cdf4dcb61..0000000000
--- a/src/server/api/endpoints/antennas/show.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Antennas } from '@/models/index';
-
-export const meta = {
-	tags: ['antennas', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:account',
-
-	params: {
-		antennaId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchAntenna: {
-			message: 'No such antenna.',
-			code: 'NO_SUCH_ANTENNA',
-			id: 'c06569fb-b025-4f23-b22d-1fcd20d2816b'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Antenna'
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the antenna
-	const antenna = await Antennas.findOne({
-		id: ps.antennaId,
-		userId: me.id,
-	});
-
-	if (antenna == null) {
-		throw new ApiError(meta.errors.noSuchAntenna);
-	}
-
-	return await Antennas.pack(antenna);
-});
diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts
deleted file mode 100644
index d69b4feee6..0000000000
--- a/src/server/api/endpoints/antennas/update.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Antennas, UserLists, UserGroupJoinings } from '@/models/index';
-import { publishInternalEvent } from '@/services/stream';
-
-export const meta = {
-	tags: ['antennas'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		antennaId: {
-			validator: $.type(ID),
-		},
-
-		name: {
-			validator: $.str.range(1, 100)
-		},
-
-		src: {
-			validator: $.str.or(['home', 'all', 'users', 'list', 'group'])
-		},
-
-		userListId: {
-			validator: $.nullable.optional.type(ID),
-		},
-
-		userGroupId: {
-			validator: $.nullable.optional.type(ID),
-		},
-
-		keywords: {
-			validator: $.arr($.arr($.str))
-		},
-
-		excludeKeywords: {
-			validator: $.arr($.arr($.str))
-		},
-
-		users: {
-			validator: $.arr($.str)
-		},
-
-		caseSensitive: {
-			validator: $.bool
-		},
-
-		withReplies: {
-			validator: $.bool
-		},
-
-		withFile: {
-			validator: $.bool
-		},
-
-		notify: {
-			validator: $.bool
-		}
-	},
-
-	errors: {
-		noSuchAntenna: {
-			message: 'No such antenna.',
-			code: 'NO_SUCH_ANTENNA',
-			id: '10c673ac-8852-48eb-aa1f-f5b67f069290'
-		},
-
-		noSuchUserList: {
-			message: 'No such user list.',
-			code: 'NO_SUCH_USER_LIST',
-			id: '1c6b35c9-943e-48c2-81e4-2844989407f7'
-		},
-
-		noSuchUserGroup: {
-			message: 'No such user group.',
-			code: 'NO_SUCH_USER_GROUP',
-			id: '109ed789-b6eb-456e-b8a9-6059d567d385'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Antenna'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch the antenna
-	const antenna = await Antennas.findOne({
-		id: ps.antennaId,
-		userId: user.id
-	});
-
-	if (antenna == null) {
-		throw new ApiError(meta.errors.noSuchAntenna);
-	}
-
-	let userList;
-	let userGroupJoining;
-
-	if (ps.src === 'list' && ps.userListId) {
-		userList = await UserLists.findOne({
-			id: ps.userListId,
-			userId: user.id,
-		});
-
-		if (userList == null) {
-			throw new ApiError(meta.errors.noSuchUserList);
-		}
-	} else if (ps.src === 'group' && ps.userGroupId) {
-		userGroupJoining = await UserGroupJoinings.findOne({
-			userGroupId: ps.userGroupId,
-			userId: user.id,
-		});
-
-		if (userGroupJoining == null) {
-			throw new ApiError(meta.errors.noSuchUserGroup);
-		}
-	}
-
-	await Antennas.update(antenna.id, {
-		name: ps.name,
-		src: ps.src,
-		userListId: userList ? userList.id : null,
-		userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
-		keywords: ps.keywords,
-		excludeKeywords: ps.excludeKeywords,
-		users: ps.users,
-		caseSensitive: ps.caseSensitive,
-		withReplies: ps.withReplies,
-		withFile: ps.withFile,
-		notify: ps.notify,
-	});
-
-	publishInternalEvent('antennaUpdated', await Antennas.findOneOrFail(antenna.id));
-
-	return await Antennas.pack(antenna.id);
-});
diff --git a/src/server/api/endpoints/ap/get.ts b/src/server/api/endpoints/ap/get.ts
deleted file mode 100644
index 78919f43b0..0000000000
--- a/src/server/api/endpoints/ap/get.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import Resolver from '@/remote/activitypub/resolver';
-import { ApiError } from '../../error';
-import * as ms from 'ms';
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: true as const,
-
-	limit: {
-		duration: ms('1hour'),
-		max: 30
-	},
-
-	params: {
-		uri: {
-			validator: $.str,
-		},
-	},
-
-	errors: {
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-	}
-};
-
-export default define(meta, async (ps) => {
-	const resolver = new Resolver();
-	const object = await resolver.resolve(ps.uri);
-	return object;
-});
diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts
deleted file mode 100644
index 2280d93724..0000000000
--- a/src/server/api/endpoints/ap/show.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import config from '@/config/index';
-import { createPerson } from '@/remote/activitypub/models/person';
-import { createNote } from '@/remote/activitypub/models/note';
-import Resolver from '@/remote/activitypub/resolver';
-import { ApiError } from '../../error';
-import { extractDbHost } from '@/misc/convert-host';
-import { Users, Notes } from '@/models/index';
-import { Note } from '@/models/entities/note';
-import { User } from '@/models/entities/user';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { isActor, isPost, getApId } from '@/remote/activitypub/type';
-import * as ms from 'ms';
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: true as const,
-
-	limit: {
-		duration: ms('1hour'),
-		max: 30
-	},
-
-	params: {
-		uri: {
-			validator: $.str,
-		},
-	},
-
-	errors: {
-		noSuchObject: {
-			message: 'No such object.',
-			code: 'NO_SUCH_OBJECT',
-			id: 'dc94d745-1262-4e63-a17d-fecaa57efc82'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			type: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				enum: ['User', 'Note']
-			},
-			object: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	const object = await fetchAny(ps.uri);
-	if (object) {
-		return object;
-	} else {
-		throw new ApiError(meta.errors.noSuchObject);
-	}
-});
-
-/***
- * URIからUserかNoteを解決する
- */
-async function fetchAny(uri: string) {
-	// URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ
-	if (uri.startsWith(config.url + '/')) {
-		const parts = uri.split('/');
-		const id = parts.pop();
-		const type = parts.pop();
-
-		if (type === 'notes') {
-			const note = await Notes.findOne(id);
-
-			if (note) {
-				return {
-					type: 'Note',
-					object: await Notes.pack(note, null, { detail: true })
-				};
-			}
-		} else if (type === 'users') {
-			const user = await Users.findOne(id);
-
-			if (user) {
-				return {
-					type: 'User',
-					object: await Users.pack(user, null, { detail: true })
-				};
-			}
-		}
-	}
-
-	// ブロックしてたら中断
-	const meta = await fetchMeta();
-	if (meta.blockedHosts.includes(extractDbHost(uri))) return null;
-
-	// URI(AP Object id)としてDB検索
-	{
-		const [user, note] = await Promise.all([
-			Users.findOne({ uri: uri }),
-			Notes.findOne({ uri: uri })
-		]);
-
-		const packed = await mergePack(user, note);
-		if (packed !== null) return packed;
-	}
-
-	// リモートから一旦オブジェクトフェッチ
-	const resolver = new Resolver();
-	const object = await resolver.resolve(uri) as any;
-
-	// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
-	// これはDBに存在する可能性があるため再度DB検索
-	if (uri !== object.id) {
-		if (object.id.startsWith(config.url + '/')) {
-			const parts = object.id.split('/');
-			const id = parts.pop();
-			const type = parts.pop();
-
-			if (type === 'notes') {
-				const note = await Notes.findOne(id);
-
-				if (note) {
-					return {
-						type: 'Note',
-						object: await Notes.pack(note, null, { detail: true })
-					};
-				}
-			} else if (type === 'users') {
-				const user = await Users.findOne(id);
-
-				if (user) {
-					return {
-						type: 'User',
-						object: await Users.pack(user, null, { detail: true })
-					};
-				}
-			}
-		}
-
-		const [user, note] = await Promise.all([
-			Users.findOne({ uri: object.id }),
-			Notes.findOne({ uri: object.id })
-		]);
-
-		const packed = await mergePack(user, note);
-		if (packed !== null) return packed;
-	}
-
-	// それでもみつからなければ新規であるため登録
-	if (isActor(object)) {
-		const user = await createPerson(getApId(object));
-		return {
-			type: 'User',
-			object: await Users.pack(user, null, { detail: true })
-		};
-	}
-
-	if (isPost(object)) {
-		const note = await createNote(getApId(object), undefined, true);
-		return {
-			type: 'Note',
-			object: await Notes.pack(note!, null, { detail: true })
-		};
-	}
-
-	return null;
-}
-
-async function mergePack(user: User | null | undefined, note: Note | null | undefined) {
-	if (user != null) {
-		return {
-			type: 'User',
-			object: await Users.pack(user, null, { detail: true })
-		};
-	}
-
-	if (note != null) {
-		return {
-			type: 'Note',
-			object: await Notes.pack(note, null, { detail: true })
-		};
-	}
-
-	return null;
-}
diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts
deleted file mode 100644
index c2ce943dcc..0000000000
--- a/src/server/api/endpoints/app/create.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Apps } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { unique } from '@/prelude/array';
-import { secureRndstr } from '@/misc/secure-rndstr';
-
-export const meta = {
-	tags: ['app'],
-
-	requireCredential: false as const,
-
-	params: {
-		name: {
-			validator: $.str,
-		},
-
-		description: {
-			validator: $.str,
-		},
-
-		permission: {
-			validator: $.arr($.str).unique(),
-		},
-
-		// TODO: Check it is valid url
-		callbackUrl: {
-			validator: $.optional.nullable.str,
-			default: null,
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'App',
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	// Generate secret
-	const secret = secureRndstr(32, true);
-
-	// for backward compatibility
-	const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
-
-	// Create account
-	const app = await Apps.save({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user ? user.id : null,
-		name: ps.name,
-		description: ps.description,
-		permission,
-		callbackUrl: ps.callbackUrl,
-		secret: secret
-	});
-
-	return await Apps.pack(app, null, {
-		detail: true,
-		includeSecret: true
-	});
-});
diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts
deleted file mode 100644
index 27f12eb44f..0000000000
--- a/src/server/api/endpoints/app/show.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Apps } from '@/models/index';
-
-export const meta = {
-	tags: ['app'],
-
-	params: {
-		appId: {
-			validator: $.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'App',
-	},
-
-	errors: {
-		noSuchApp: {
-			message: 'No such app.',
-			code: 'NO_SUCH_APP',
-			id: 'dce83913-2dc6-4093-8a7b-71dbb11718a3'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'App'
-	}
-};
-
-export default define(meta, async (ps, user, token) => {
-	const isSecure = user != null && token == null;
-
-	// Lookup app
-	const ap = await Apps.findOne(ps.appId);
-
-	if (ap == null) {
-		throw new ApiError(meta.errors.noSuchApp);
-	}
-
-	return await Apps.pack(ap, user, {
-		detail: true,
-		includeSecret: isSecure && (ap.userId === user!.id)
-	});
-});
diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts
deleted file mode 100644
index 1d1d8ac227..0000000000
--- a/src/server/api/endpoints/auth/accept.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import * as crypto from 'crypto';
-import $ from 'cafy';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { AuthSessions, AccessTokens, Apps } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { secureRndstr } from '@/misc/secure-rndstr';
-
-export const meta = {
-	tags: ['auth'],
-
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		token: {
-			validator: $.str
-		}
-	},
-
-	errors: {
-		noSuchSession: {
-			message: 'No such session.',
-			code: 'NO_SUCH_SESSION',
-			id: '9c72d8de-391a-43c1-9d06-08d29efde8df'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch token
-	const session = await AuthSessions
-		.findOne({ token: ps.token });
-
-	if (session == null) {
-		throw new ApiError(meta.errors.noSuchSession);
-	}
-
-	// Generate access token
-	const accessToken = secureRndstr(32, true);
-
-	// Fetch exist access token
-	const exist = await AccessTokens.findOne({
-		appId: session.appId,
-		userId: user.id,
-	});
-
-	if (exist == null) {
-		// Lookup app
-		const app = await Apps.findOneOrFail(session.appId);
-
-		// Generate Hash
-		const sha256 = crypto.createHash('sha256');
-		sha256.update(accessToken + app.secret);
-		const hash = sha256.digest('hex');
-
-		const now = new Date();
-
-		// Insert access token doc
-		await AccessTokens.insert({
-			id: genId(),
-			createdAt: now,
-			lastUsedAt: now,
-			appId: session.appId,
-			userId: user.id,
-			token: accessToken,
-			hash: hash
-		});
-	}
-
-	// Update session
-	await AuthSessions.update(session.id, {
-		userId: user.id
-	});
-});
diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts
deleted file mode 100644
index 859cf52ed3..0000000000
--- a/src/server/api/endpoints/auth/session/generate.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { v4 as uuid } from 'uuid';
-import $ from 'cafy';
-import config from '@/config/index';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { Apps, AuthSessions } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['auth'],
-
-	requireCredential: false as const,
-
-	params: {
-		appSecret: {
-			validator: $.str,
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			token: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			url: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'url',
-			},
-		}
-	},
-
-	errors: {
-		noSuchApp: {
-			message: 'No such app.',
-			code: 'NO_SUCH_APP',
-			id: '92f93e63-428e-4f2f-a5a4-39e1407fe998'
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	// Lookup app
-	const app = await Apps.findOne({
-		secret: ps.appSecret
-	});
-
-	if (app == null) {
-		throw new ApiError(meta.errors.noSuchApp);
-	}
-
-	// Generate token
-	const token = uuid();
-
-	// Create session token document
-	const doc = await AuthSessions.save({
-		id: genId(),
-		createdAt: new Date(),
-		appId: app.id,
-		token: token
-	});
-
-	return {
-		token: doc.token,
-		url: `${config.authUrl}/${doc.token}`
-	};
-});
diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts
deleted file mode 100644
index 23f1a56a37..0000000000
--- a/src/server/api/endpoints/auth/session/show.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { AuthSessions } from '@/models/index';
-
-export const meta = {
-	tags: ['auth'],
-
-	requireCredential: false as const,
-
-	params: {
-		token: {
-			validator: $.str,
-		}
-	},
-
-	errors: {
-		noSuchSession: {
-			message: 'No such session.',
-			code: 'NO_SUCH_SESSION',
-			id: 'bd72c97d-eba7-4adb-a467-f171b8847250'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			},
-			app: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'App'
-			},
-			token: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Lookup session
-	const session = await AuthSessions.findOne({
-		token: ps.token
-	});
-
-	if (session == null) {
-		throw new ApiError(meta.errors.noSuchSession);
-	}
-
-	return await AuthSessions.pack(session, user);
-});
diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts
deleted file mode 100644
index 72201cb207..0000000000
--- a/src/server/api/endpoints/auth/session/userkey.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { Apps, AuthSessions, AccessTokens, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['auth'],
-
-	requireCredential: false as const,
-
-	params: {
-		appSecret: {
-			validator: $.str,
-		},
-
-		token: {
-			validator: $.str,
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			accessToken: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-
-			user: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'User',
-			},
-		}
-	},
-
-	errors: {
-		noSuchApp: {
-			message: 'No such app.',
-			code: 'NO_SUCH_APP',
-			id: 'fcab192a-2c5a-43b7-8ad8-9b7054d8d40d'
-		},
-
-		noSuchSession: {
-			message: 'No such session.',
-			code: 'NO_SUCH_SESSION',
-			id: '5b5a1503-8bc8-4bd0-8054-dc189e8cdcb3'
-		},
-
-		pendingSession: {
-			message: 'This session is not completed yet.',
-			code: 'PENDING_SESSION',
-			id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e'
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	// Lookup app
-	const app = await Apps.findOne({
-		secret: ps.appSecret
-	});
-
-	if (app == null) {
-		throw new ApiError(meta.errors.noSuchApp);
-	}
-
-	// Fetch token
-	const session = await AuthSessions.findOne({
-		token: ps.token,
-		appId: app.id
-	});
-
-	if (session == null) {
-		throw new ApiError(meta.errors.noSuchSession);
-	}
-
-	if (session.userId == null) {
-		throw new ApiError(meta.errors.pendingSession);
-	}
-
-	// Lookup access token
-	const accessToken = await AccessTokens.findOneOrFail({
-		appId: app.id,
-		userId: session.userId
-	});
-
-	// Delete session
-	AuthSessions.delete(session.id);
-
-	return {
-		accessToken: accessToken.token,
-		user: await Users.pack(session.userId, null, {
-			detail: true
-		})
-	};
-});
diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts
deleted file mode 100644
index 2953252394..0000000000
--- a/src/server/api/endpoints/blocking/create.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import * as ms from 'ms';
-import create from '@/services/blocking/create';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { Blockings, NoteWatchings, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['account'],
-
-	limit: {
-		duration: ms('1hour'),
-		max: 100
-	},
-
-	requireCredential: true as const,
-
-	kind: 'write:blocks',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e'
-		},
-
-		blockeeIsYourself: {
-			message: 'Blockee is yourself.',
-			code: 'BLOCKEE_IS_YOURSELF',
-			id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6'
-		},
-
-		alreadyBlocking: {
-			message: 'You are already blocking that user.',
-			code: 'ALREADY_BLOCKING',
-			id: '787fed64-acb9-464a-82eb-afbd745b9614'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const blocker = await Users.findOneOrFail(user.id);
-
-	// 自分自身
-	if (user.id === ps.userId) {
-		throw new ApiError(meta.errors.blockeeIsYourself);
-	}
-
-	// Get blockee
-	const blockee = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Check if already blocking
-	const exist = await Blockings.findOne({
-		blockerId: blocker.id,
-		blockeeId: blockee.id
-	});
-
-	if (exist != null) {
-		throw new ApiError(meta.errors.alreadyBlocking);
-	}
-
-	await create(blocker, blockee);
-
-	NoteWatchings.delete({
-		userId: blocker.id,
-		noteUserId: blockee.id
-	});
-
-	return await Users.pack(blockee.id, blocker, {
-		detail: true
-	});
-});
diff --git a/src/server/api/endpoints/blocking/delete.ts b/src/server/api/endpoints/blocking/delete.ts
deleted file mode 100644
index a66e46fdf0..0000000000
--- a/src/server/api/endpoints/blocking/delete.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import * as ms from 'ms';
-import deleteBlocking from '@/services/blocking/delete';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { Blockings, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['account'],
-
-	limit: {
-		duration: ms('1hour'),
-		max: 100
-	},
-
-	requireCredential: true as const,
-
-	kind: 'write:blocks',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '8621d8bf-c358-4303-a066-5ea78610eb3f'
-		},
-
-		blockeeIsYourself: {
-			message: 'Blockee is yourself.',
-			code: 'BLOCKEE_IS_YOURSELF',
-			id: '06f6fac6-524b-473c-a354-e97a40ae6eac'
-		},
-
-		notBlocking: {
-			message: 'You are not blocking that user.',
-			code: 'NOT_BLOCKING',
-			id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const blocker = await Users.findOneOrFail(user.id);
-
-	// Check if the blockee is yourself
-	if (user.id === ps.userId) {
-		throw new ApiError(meta.errors.blockeeIsYourself);
-	}
-
-	// Get blockee
-	const blockee = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Check not blocking
-	const exist = await Blockings.findOne({
-		blockerId: blocker.id,
-		blockeeId: blockee.id
-	});
-
-	if (exist == null) {
-		throw new ApiError(meta.errors.notBlocking);
-	}
-
-	// Delete blocking
-	await deleteBlocking(blocker, blockee);
-
-	return await Users.pack(blockee.id, blocker, {
-		detail: true
-	});
-});
diff --git a/src/server/api/endpoints/blocking/list.ts b/src/server/api/endpoints/blocking/list.ts
deleted file mode 100644
index fe25fdaba1..0000000000
--- a/src/server/api/endpoints/blocking/list.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Blockings } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:blocks',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 30
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Blocking',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(Blockings.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
-		.andWhere(`blocking.blockerId = :meId`, { meId: me.id });
-
-	const blockings = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Blockings.packMany(blockings, me);
-});
diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts
deleted file mode 100644
index 0cedfd6c6a..0000000000
--- a/src/server/api/endpoints/channels/create.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Channels, DriveFiles } from '@/models/index';
-import { Channel } from '@/models/entities/channel';
-import { genId } from '@/misc/gen-id';
-import { ID } from '@/misc/cafy-id';
-
-export const meta = {
-	tags: ['channels'],
-
-	requireCredential: true as const,
-
-	kind: 'write:channels',
-
-	params: {
-		name: {
-			validator: $.str.range(1, 128)
-		},
-
-		description: {
-			validator: $.nullable.optional.str.range(1, 2048)
-		},
-
-		bannerId: {
-			validator: $.nullable.optional.type(ID),
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Channel',
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	let banner = null;
-	if (ps.bannerId != null) {
-		banner = await DriveFiles.findOne({
-			id: ps.bannerId,
-			userId: user.id
-		});
-
-		if (banner == null) {
-			throw new ApiError(meta.errors.noSuchFile);
-		}
-	}
-
-	const channel = await Channels.save({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		name: ps.name,
-		description: ps.description || null,
-		bannerId: banner ? banner.id : null,
-	} as Channel);
-
-	return await Channels.pack(channel, user);
-});
diff --git a/src/server/api/endpoints/channels/featured.ts b/src/server/api/endpoints/channels/featured.ts
deleted file mode 100644
index dc1f49f960..0000000000
--- a/src/server/api/endpoints/channels/featured.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import define from '../../define';
-import { Channels } from '@/models/index';
-
-export const meta = {
-	tags: ['channels'],
-
-	requireCredential: false as const,
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Channel',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = Channels.createQueryBuilder('channel')
-		.where('channel.lastNotedAt IS NOT NULL')
-		.orderBy('channel.lastNotedAt', 'DESC');
-
-	const channels = await query.take(10).getMany();
-
-	return await Promise.all(channels.map(x => Channels.pack(x, me)));
-});
diff --git a/src/server/api/endpoints/channels/follow.ts b/src/server/api/endpoints/channels/follow.ts
deleted file mode 100644
index d4664e6996..0000000000
--- a/src/server/api/endpoints/channels/follow.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Channels, ChannelFollowings } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { publishUserEvent } from '@/services/stream';
-
-export const meta = {
-	tags: ['channels'],
-
-	requireCredential: true as const,
-
-	kind: 'write:channels',
-
-	params: {
-		channelId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchChannel: {
-			message: 'No such channel.',
-			code: 'NO_SUCH_CHANNEL',
-			id: 'c0031718-d573-4e85-928e-10039f1fbb68'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const channel = await Channels.findOne({
-		id: ps.channelId,
-	});
-
-	if (channel == null) {
-		throw new ApiError(meta.errors.noSuchChannel);
-	}
-
-	await ChannelFollowings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		followerId: user.id,
-		followeeId: channel.id,
-	});
-
-	publishUserEvent(user.id, 'followChannel', channel);
-});
diff --git a/src/server/api/endpoints/channels/followed.ts b/src/server/api/endpoints/channels/followed.ts
deleted file mode 100644
index be239a01d6..0000000000
--- a/src/server/api/endpoints/channels/followed.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Channels, ChannelFollowings } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['channels', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:channels',
-
-	params: {
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 5
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Channel',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(ChannelFollowings.createQueryBuilder(), ps.sinceId, ps.untilId)
-		.andWhere({ followerId: me.id });
-
-	const followings = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Promise.all(followings.map(x => Channels.pack(x.followeeId, me)));
-});
diff --git a/src/server/api/endpoints/channels/owned.ts b/src/server/api/endpoints/channels/owned.ts
deleted file mode 100644
index 4a2e9db17b..0000000000
--- a/src/server/api/endpoints/channels/owned.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Channels } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['channels', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:channels',
-
-	params: {
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 5
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Channel',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(Channels.createQueryBuilder(), ps.sinceId, ps.untilId)
-		.andWhere({ userId: me.id });
-
-	const channels = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Promise.all(channels.map(x => Channels.pack(x, me)));
-});
diff --git a/src/server/api/endpoints/channels/pin-note.ts b/src/server/api/endpoints/channels/pin-note.ts
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts
deleted file mode 100644
index 803ce6363d..0000000000
--- a/src/server/api/endpoints/channels/show.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Channels } from '@/models/index';
-
-export const meta = {
-	tags: ['channels'],
-
-	requireCredential: false as const,
-
-	params: {
-		channelId: {
-			validator: $.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Channel',
-	},
-
-	errors: {
-		noSuchChannel: {
-			message: 'No such channel.',
-			code: 'NO_SUCH_CHANNEL',
-			id: '6f6c314b-7486-4897-8966-c04a66a02923'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const channel = await Channels.findOne({
-		id: ps.channelId,
-	});
-
-	if (channel == null) {
-		throw new ApiError(meta.errors.noSuchChannel);
-	}
-
-	return await Channels.pack(channel, me);
-});
diff --git a/src/server/api/endpoints/channels/timeline.ts b/src/server/api/endpoints/channels/timeline.ts
deleted file mode 100644
index 0ed057a11e..0000000000
--- a/src/server/api/endpoints/channels/timeline.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Notes, Channels } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { activeUsersChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['notes', 'channels'],
-
-	requireCredential: false as const,
-
-	params: {
-		channelId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		sinceDate: {
-			validator: $.optional.num,
-		},
-
-		untilDate: {
-			validator: $.optional.num,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		noSuchChannel: {
-			message: 'No such channel.',
-			code: 'NO_SUCH_CHANNEL',
-			id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const channel = await Channels.findOne({
-		id: ps.channelId,
-	});
-
-	if (channel == null) {
-		throw new ApiError(meta.errors.noSuchChannel);
-	}
-
-	//#region Construct query
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-		.andWhere('note.channelId = :channelId', { channelId: channel.id })
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser')
-		.leftJoinAndSelect('note.channel', 'channel');
-	//#endregion
-
-	const timeline = await query.take(ps.limit!).getMany();
-
-	if (user) activeUsersChart.update(user);
-
-	return await Notes.packMany(timeline, user);
-});
diff --git a/src/server/api/endpoints/channels/unfollow.ts b/src/server/api/endpoints/channels/unfollow.ts
deleted file mode 100644
index 700f8e93ba..0000000000
--- a/src/server/api/endpoints/channels/unfollow.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Channels, ChannelFollowings } from '@/models/index';
-import { publishUserEvent } from '@/services/stream';
-
-export const meta = {
-	tags: ['channels'],
-
-	requireCredential: true as const,
-
-	kind: 'write:channels',
-
-	params: {
-		channelId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchChannel: {
-			message: 'No such channel.',
-			code: 'NO_SUCH_CHANNEL',
-			id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const channel = await Channels.findOne({
-		id: ps.channelId,
-	});
-
-	if (channel == null) {
-		throw new ApiError(meta.errors.noSuchChannel);
-	}
-
-	await ChannelFollowings.delete({
-		followerId: user.id,
-		followeeId: channel.id,
-	});
-
-	publishUserEvent(user.id, 'unfollowChannel', channel);
-});
diff --git a/src/server/api/endpoints/channels/update.ts b/src/server/api/endpoints/channels/update.ts
deleted file mode 100644
index 9b447bd04b..0000000000
--- a/src/server/api/endpoints/channels/update.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Channels, DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['channels'],
-
-	requireCredential: true as const,
-
-	kind: 'write:channels',
-
-	params: {
-		channelId: {
-			validator: $.type(ID),
-		},
-
-		name: {
-			validator: $.optional.str.range(1, 128)
-		},
-
-		description: {
-			validator: $.nullable.optional.str.range(1, 2048)
-		},
-
-		bannerId: {
-			validator: $.nullable.optional.type(ID),
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Channel',
-	},
-
-	errors: {
-		noSuchChannel: {
-			message: 'No such channel.',
-			code: 'NO_SUCH_CHANNEL',
-			id: 'f9c5467f-d492-4c3c-9a8d-a70dacc86512'
-		},
-
-		accessDenied: {
-			message: 'You do not have edit privilege of the channel.',
-			code: 'ACCESS_DENIED',
-			id: '1fb7cb09-d46a-4fdf-b8df-057788cce513'
-		},
-
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const channel = await Channels.findOne({
-		id: ps.channelId,
-	});
-
-	if (channel == null) {
-		throw new ApiError(meta.errors.noSuchChannel);
-	}
-
-	if (channel.userId !== me.id) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	// tslint:disable-next-line:no-unnecessary-initializer
-	let banner = undefined;
-	if (ps.bannerId != null) {
-		banner = await DriveFiles.findOne({
-			id: ps.bannerId,
-			userId: me.id
-		});
-
-		if (banner == null) {
-			throw new ApiError(meta.errors.noSuchFile);
-		}
-	} else if (ps.bannerId === null) {
-		banner = null;
-	}
-
-	await Channels.update(channel.id, {
-		...(ps.name !== undefined ? { name: ps.name } : {}),
-		...(ps.description !== undefined ? { description: ps.description } : {}),
-		...(banner ? { bannerId: banner.id } : {}),
-	});
-
-	return await Channels.pack(channel.id, me);
-});
diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts
deleted file mode 100644
index c4878f7d61..0000000000
--- a/src/server/api/endpoints/charts/active-users.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { convertLog } from '@/services/chart/core';
-import { activeUsersChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'users'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-	},
-
-	res: convertLog(activeUsersChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await activeUsersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
-});
diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts
deleted file mode 100644
index 07bff82cf4..0000000000
--- a/src/server/api/endpoints/charts/drive.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { convertLog } from '@/services/chart/core';
-import { driveChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'drive'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-	},
-
-	res: convertLog(driveChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await driveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
-});
diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts
deleted file mode 100644
index 9575f9a7b7..0000000000
--- a/src/server/api/endpoints/charts/federation.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { convertLog } from '@/services/chart/core';
-import { federationChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-	},
-
-	res: convertLog(federationChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await federationChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
-});
diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts
deleted file mode 100644
index 53dc61e51e..0000000000
--- a/src/server/api/endpoints/charts/hashtag.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { convertLog } from '@/services/chart/core';
-import { hashtagChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'hashtags'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-
-		tag: {
-			validator: $.str,
-		},
-	},
-
-	res: convertLog(hashtagChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await hashtagChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.tag);
-});
diff --git a/src/server/api/endpoints/charts/instance.ts b/src/server/api/endpoints/charts/instance.ts
deleted file mode 100644
index 1835023188..0000000000
--- a/src/server/api/endpoints/charts/instance.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { convertLog } from '@/services/chart/core';
-import { instanceChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-
-		host: {
-			validator: $.str,
-		}
-	},
-
-	res: convertLog(instanceChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await instanceChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.host);
-});
diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts
deleted file mode 100644
index b524df93be..0000000000
--- a/src/server/api/endpoints/charts/network.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { convertLog } from '@/services/chart/core';
-import { networkChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-	},
-
-	res: convertLog(networkChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await networkChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
-});
diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts
deleted file mode 100644
index 676f302939..0000000000
--- a/src/server/api/endpoints/charts/notes.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { convertLog } from '@/services/chart/core';
-import { notesChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'notes'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-	},
-
-	res: convertLog(notesChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await notesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
-});
diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts
deleted file mode 100644
index f2770e2df8..0000000000
--- a/src/server/api/endpoints/charts/user/drive.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { convertLog } from '@/services/chart/core';
-import { perUserDriveChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'drive', 'users'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	res: convertLog(perUserDriveChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await perUserDriveChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
-});
diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts
deleted file mode 100644
index 8c97b63e89..0000000000
--- a/src/server/api/endpoints/charts/user/following.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { convertLog } from '@/services/chart/core';
-import { perUserFollowingChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'users', 'following'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	res: convertLog(perUserFollowingChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await perUserFollowingChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
-});
diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts
deleted file mode 100644
index 0d5f5a8b6a..0000000000
--- a/src/server/api/endpoints/charts/user/notes.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { convertLog } from '@/services/chart/core';
-import { perUserNotesChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'users', 'notes'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	res: convertLog(perUserNotesChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await perUserNotesChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
-});
diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts
deleted file mode 100644
index 3cabe40d56..0000000000
--- a/src/server/api/endpoints/charts/user/reactions.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ID } from '@/misc/cafy-id';
-import { convertLog } from '@/services/chart/core';
-import { perUserReactionsChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'users', 'reactions'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	res: convertLog(perUserReactionsChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await perUserReactionsChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null, ps.userId);
-});
diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts
deleted file mode 100644
index deac89b59d..0000000000
--- a/src/server/api/endpoints/charts/users.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { convertLog } from '@/services/chart/core';
-import { usersChart } from '@/services/chart/index';
-
-export const meta = {
-	tags: ['charts', 'users'],
-
-	params: {
-		span: {
-			validator: $.str.or(['day', 'hour']),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 500),
-			default: 30,
-		},
-
-		offset: {
-			validator: $.optional.nullable.num,
-			default: null,
-		},
-	},
-
-	res: convertLog(usersChart.schema),
-};
-
-export default define(meta, async (ps) => {
-	return await usersChart.getChart(ps.span as any, ps.limit!, ps.offset ? new Date(ps.offset) : null);
-});
diff --git a/src/server/api/endpoints/clips/add-note.ts b/src/server/api/endpoints/clips/add-note.ts
deleted file mode 100644
index 79d7b8adde..0000000000
--- a/src/server/api/endpoints/clips/add-note.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ClipNotes, Clips } from '@/models/index';
-import { ApiError } from '../../error';
-import { genId } from '@/misc/gen-id';
-import { getNote } from '../../common/getters';
-
-export const meta = {
-	tags: ['account', 'notes', 'clips'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		clipId: {
-			validator: $.type(ID),
-		},
-
-		noteId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchClip: {
-			message: 'No such clip.',
-			code: 'NO_SUCH_CLIP',
-			id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf'
-		},
-
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b'
-		},
-
-		alreadyClipped: {
-			message: 'The note has already been clipped.',
-			code: 'ALREADY_CLIPPED',
-			id: '734806c4-542c-463a-9311-15c512803965'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const clip = await Clips.findOne({
-		id: ps.clipId,
-		userId: user.id
-	});
-
-	if (clip == null) {
-		throw new ApiError(meta.errors.noSuchClip);
-	}
-
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const exist = await ClipNotes.findOne({
-		noteId: note.id,
-		clipId: clip.id
-	});
-
-	if (exist != null) {
-		throw new ApiError(meta.errors.alreadyClipped);
-	}
-
-	await ClipNotes.insert({
-		id: genId(),
-		noteId: note.id,
-		clipId: clip.id
-	});
-});
diff --git a/src/server/api/endpoints/clips/create.ts b/src/server/api/endpoints/clips/create.ts
deleted file mode 100644
index 02d2773709..0000000000
--- a/src/server/api/endpoints/clips/create.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { genId } from '@/misc/gen-id';
-import { Clips } from '@/models/index';
-
-export const meta = {
-	tags: ['clips'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		name: {
-			validator: $.str.range(1, 100)
-		},
-
-		isPublic: {
-			validator: $.optional.bool
-		},
-
-		description: {
-			validator: $.optional.nullable.str.range(1, 2048)
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Clip'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const clip = await Clips.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		name: ps.name,
-		isPublic: ps.isPublic,
-		description: ps.description,
-	}).then(x => Clips.findOneOrFail(x.identifiers[0]));
-
-	return await Clips.pack(clip);
-});
diff --git a/src/server/api/endpoints/clips/delete.ts b/src/server/api/endpoints/clips/delete.ts
deleted file mode 100644
index ca489af3bf..0000000000
--- a/src/server/api/endpoints/clips/delete.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Clips } from '@/models/index';
-
-export const meta = {
-	tags: ['clips'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		clipId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchClip: {
-			message: 'No such clip.',
-			code: 'NO_SUCH_CLIP',
-			id: '70ca08ba-6865-4630-b6fb-8494759aa754'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const clip = await Clips.findOne({
-		id: ps.clipId,
-		userId: user.id
-	});
-
-	if (clip == null) {
-		throw new ApiError(meta.errors.noSuchClip);
-	}
-
-	await Clips.delete(clip.id);
-});
diff --git a/src/server/api/endpoints/clips/list.ts b/src/server/api/endpoints/clips/list.ts
deleted file mode 100644
index 1f6db9b979..0000000000
--- a/src/server/api/endpoints/clips/list.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import define from '../../define';
-import { Clips } from '@/models/index';
-
-export const meta = {
-	tags: ['clips', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:account',
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Clip'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const clips = await Clips.find({
-		userId: me.id,
-	});
-
-	return await Promise.all(clips.map(x => Clips.pack(x)));
-});
diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts
deleted file mode 100644
index 5a9fed52fa..0000000000
--- a/src/server/api/endpoints/clips/notes.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ClipNotes, Clips, Notes } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { ApiError } from '../../error';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['account', 'notes', 'clips'],
-
-	requireCredential: false as const,
-
-	kind: 'read:account',
-
-	params: {
-		clipId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchClip: {
-			message: 'No such clip.',
-			code: 'NO_SUCH_CLIP',
-			id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00'
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const clip = await Clips.findOne({
-		id: ps.clipId,
-	});
-
-	if (clip == null) {
-		throw new ApiError(meta.errors.noSuchClip);
-	}
-
-	if (!clip.isPublic && (user == null || (clip.userId !== user.id))) {
-		throw new ApiError(meta.errors.noSuchClip);
-	}
-
-	const clipQuery = ClipNotes.createQueryBuilder('joining')
-		.select('joining.noteId')
-		.where('joining.clipId = :clipId', { clipId: clip.id });
-
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.andWhere(`note.id IN (${ clipQuery.getQuery() })`)
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser')
-		.setParameters(clipQuery.getParameters());
-
-	if (user) {
-		generateVisibilityQuery(query, user);
-		generateMutedUserQuery(query, user);
-		generateBlockedUserQuery(query, user);
-	}
-
-	const notes = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Notes.packMany(notes, user);
-});
diff --git a/src/server/api/endpoints/clips/show.ts b/src/server/api/endpoints/clips/show.ts
deleted file mode 100644
index 8f245cd18e..0000000000
--- a/src/server/api/endpoints/clips/show.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Clips } from '@/models/index';
-
-export const meta = {
-	tags: ['clips', 'account'],
-
-	requireCredential: false as const,
-
-	kind: 'read:account',
-
-	params: {
-		clipId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchClip: {
-			message: 'No such clip.',
-			code: 'NO_SUCH_CLIP',
-			id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Clip'
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the clip
-	const clip = await Clips.findOne({
-		id: ps.clipId,
-	});
-
-	if (clip == null) {
-		throw new ApiError(meta.errors.noSuchClip);
-	}
-
-	if (!clip.isPublic && (me == null || (clip.userId !== me.id))) {
-		throw new ApiError(meta.errors.noSuchClip);
-	}
-
-	return await Clips.pack(clip);
-});
diff --git a/src/server/api/endpoints/clips/update.ts b/src/server/api/endpoints/clips/update.ts
deleted file mode 100644
index 7f645560bb..0000000000
--- a/src/server/api/endpoints/clips/update.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Clips } from '@/models/index';
-
-export const meta = {
-	tags: ['clips'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		clipId: {
-			validator: $.type(ID),
-		},
-
-		name: {
-			validator: $.str.range(1, 100),
-		},
-
-		isPublic: {
-			validator: $.optional.bool
-		},
-
-		description: {
-			validator: $.optional.nullable.str.range(1, 2048)
-		}
-	},
-
-	errors: {
-		noSuchClip: {
-			message: 'No such clip.',
-			code: 'NO_SUCH_CLIP',
-			id: 'b4d92d70-b216-46fa-9a3f-a8c811699257'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Clip'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch the clip
-	const clip = await Clips.findOne({
-		id: ps.clipId,
-		userId: user.id
-	});
-
-	if (clip == null) {
-		throw new ApiError(meta.errors.noSuchClip);
-	}
-
-	await Clips.update(clip.id, {
-		name: ps.name,
-		description: ps.description,
-		isPublic: ps.isPublic,
-	});
-
-	return await Clips.pack(clip.id);
-});
diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts
deleted file mode 100644
index 2974ccfab9..0000000000
--- a/src/server/api/endpoints/drive.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import define from '../define';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['drive', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			capacity: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-			usage: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const instance = await fetchMeta(true);
-
-	// Calculate drive usage
-	const usage = await DriveFiles.calcDriveUsageOf(user.id);
-
-	return {
-		capacity: 1024 * 1024 * instance.localDriveCapacityMb,
-		usage: usage
-	};
-});
diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts
deleted file mode 100644
index 95435e1e43..0000000000
--- a/src/server/api/endpoints/drive/files.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { DriveFiles } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		folderId: {
-			validator: $.optional.nullable.type(ID),
-			default: null,
-		},
-
-		type: {
-			validator: $.optional.nullable.str.match(/^[a-zA-Z\/\-*]+$/)
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'DriveFile',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
-		.andWhere('file.userId = :userId', { userId: user.id });
-
-	if (ps.folderId) {
-		query.andWhere('file.folderId = :folderId', { folderId: ps.folderId });
-	} else {
-		query.andWhere('file.folderId IS NULL');
-	}
-
-	if (ps.type) {
-		if (ps.type.endsWith('/*')) {
-			query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
-		} else {
-			query.andWhere('file.type = :type', { type: ps.type });
-		}
-	}
-
-	const files = await query.take(ps.limit!).getMany();
-
-	return await DriveFiles.packMany(files, { detail: false, self: true });
-});
diff --git a/src/server/api/endpoints/drive/files/attached-notes.ts b/src/server/api/endpoints/drive/files/attached-notes.ts
deleted file mode 100644
index eec7d7877e..0000000000
--- a/src/server/api/endpoints/drive/files/attached-notes.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { DriveFiles, Notes } from '@/models/index';
-
-export const meta = {
-	tags: ['drive', 'notes'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		fileId: {
-			validator: $.type(ID),
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'c118ece3-2e4b-4296-99d1-51756e32d232',
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch file
-	const file = await DriveFiles.findOne({
-		id: ps.fileId,
-		userId: user.id,
-	});
-
-	if (file == null) {
-		throw new ApiError(meta.errors.noSuchFile);
-	}
-
-	const notes = await Notes.createQueryBuilder('note')
-		.where(':file = ANY(note.fileIds)', { file: file.id })
-		.getMany();
-
-	return await Notes.packMany(notes, user, {
-		detail: true
-	});
-});
diff --git a/src/server/api/endpoints/drive/files/check-existence.ts b/src/server/api/endpoints/drive/files/check-existence.ts
deleted file mode 100644
index 2c36078421..0000000000
--- a/src/server/api/endpoints/drive/files/check-existence.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		md5: {
-			validator: $.str,
-		}
-	},
-
-	res: {
-		type: 'boolean' as const,
-		optional: false as const, nullable: false as const,
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const file = await DriveFiles.findOne({
-		md5: ps.md5,
-		userId: user.id,
-	});
-
-	return file != null;
-});
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
deleted file mode 100644
index 2abc104e6c..0000000000
--- a/src/server/api/endpoints/drive/files/create.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import * as ms from 'ms';
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import create from '@/services/drive/add-file';
-import define from '../../../define';
-import { apiLogger } from '../../../logger';
-import { ApiError } from '../../../error';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	limit: {
-		duration: ms('1hour'),
-		max: 120
-	},
-
-	requireFile: true,
-
-	kind: 'write:drive',
-
-	params: {
-		folderId: {
-			validator: $.optional.nullable.type(ID),
-			default: null,
-		},
-
-		name: {
-			validator: $.optional.nullable.str,
-			default: null,
-		},
-
-		isSensitive: {
-			validator: $.optional.either($.bool, $.str),
-			default: false,
-			transform: (v: any): boolean => v === true || v === 'true',
-		},
-
-		force: {
-			validator: $.optional.either($.bool, $.str),
-			default: false,
-			transform: (v: any): boolean => v === true || v === 'true',
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'DriveFile',
-	},
-
-	errors: {
-		invalidFileName: {
-			message: 'Invalid file name.',
-			code: 'INVALID_FILE_NAME',
-			id: 'f449b209-0c60-4e51-84d5-29486263bfd4'
-		}
-	}
-};
-
-export default define(meta, async (ps, user, _, file, cleanup) => {
-	// Get 'name' parameter
-	let name = ps.name || file.originalname;
-	if (name !== undefined && name !== null) {
-		name = name.trim();
-		if (name.length === 0) {
-			name = null;
-		} else if (name === 'blob') {
-			name = null;
-		} else if (!DriveFiles.validateFileName(name)) {
-			throw new ApiError(meta.errors.invalidFileName);
-		}
-	} else {
-		name = null;
-	}
-
-	try {
-		// Create file
-		const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
-		return await DriveFiles.pack(driveFile, { self: true });
-	} catch (e) {
-		apiLogger.error(e);
-		throw new ApiError();
-	} finally {
-		cleanup!();
-	}
-});
diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts
deleted file mode 100644
index 038325694d..0000000000
--- a/src/server/api/endpoints/drive/files/delete.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { deleteFile } from '@/services/drive/delete-file';
-import { publishDriveStream } from '@/services/stream';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'write:drive',
-
-	params: {
-		fileId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: '908939ec-e52b-4458-b395-1025195cea58'
-		},
-
-		accessDenied: {
-			message: 'Access denied.',
-			code: 'ACCESS_DENIED',
-			id: '5eb8d909-2540-4970-90b8-dd6f86088121'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const file = await DriveFiles.findOne(ps.fileId);
-
-	if (file == null) {
-		throw new ApiError(meta.errors.noSuchFile);
-	}
-
-	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	// Delete
-	await deleteFile(file);
-
-	// Publish fileDeleted event
-	publishDriveStream(user.id, 'fileDeleted', file.id);
-});
diff --git a/src/server/api/endpoints/drive/files/find-by-hash.ts b/src/server/api/endpoints/drive/files/find-by-hash.ts
deleted file mode 100644
index 5fea7bbbb0..0000000000
--- a/src/server/api/endpoints/drive/files/find-by-hash.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		md5: {
-			validator: $.str,
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'DriveFile',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const files = await DriveFiles.find({
-		md5: ps.md5,
-		userId: user.id,
-	});
-
-	return await DriveFiles.packMany(files, { self: true });
-});
diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts
deleted file mode 100644
index dd419f4c04..0000000000
--- a/src/server/api/endpoints/drive/files/find.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	tags: ['drive'],
-
-	kind: 'read:drive',
-
-	params: {
-		name: {
-			validator: $.str
-		},
-
-		folderId: {
-			validator: $.optional.nullable.type(ID),
-			default: null,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'DriveFile',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const files = await DriveFiles.find({
-		name: ps.name,
-		userId: user.id,
-		folderId: ps.folderId
-	});
-
-	return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true })));
-});
diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts
deleted file mode 100644
index a96ebaa123..0000000000
--- a/src/server/api/endpoints/drive/files/show.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		fileId: {
-			validator: $.optional.type(ID),
-		},
-
-		url: {
-			validator: $.optional.str,
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'DriveFile',
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: '067bc436-2718-4795-b0fb-ecbe43949e31'
-		},
-
-		accessDenied: {
-			message: 'Access denied.',
-			code: 'ACCESS_DENIED',
-			id: '25b73c73-68b1-41d0-bad1-381cfdf6579f'
-		},
-
-		fileIdOrUrlRequired: {
-			message: 'fileId or url required.',
-			code: 'INVALID_PARAM',
-			id: '89674805-722c-440c-8d88-5641830dc3e4'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	let file: DriveFile | undefined;
-
-	if (ps.fileId) {
-		file = await DriveFiles.findOne(ps.fileId);
-	} else if (ps.url) {
-		file = await DriveFiles.findOne({
-			where: [{
-				url: ps.url
-			}, {
-				webpublicUrl: ps.url
-			}, {
-				thumbnailUrl: ps.url
-			}],
-		});
-	} else {
-		throw new ApiError(meta.errors.fileIdOrUrlRequired);
-	}
-
-	if (file == null) {
-		throw new ApiError(meta.errors.noSuchFile);
-	}
-
-	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	return await DriveFiles.pack(file, {
-		detail: true,
-		withUser: true,
-		self: true
-	});
-});
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
deleted file mode 100644
index f277a9c3dc..0000000000
--- a/src/server/api/endpoints/drive/files/update.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishDriveStream } from '@/services/stream';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { DriveFiles, DriveFolders } from '@/models/index';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'write:drive',
-
-	params: {
-		fileId: {
-			validator: $.type(ID),
-		},
-
-		folderId: {
-			validator: $.optional.nullable.type(ID),
-			default: undefined as any,
-		},
-
-		name: {
-			validator: $.optional.str.pipe(DriveFiles.validateFileName),
-			default: undefined as any,
-		},
-
-		isSensitive: {
-			validator: $.optional.bool,
-			default: undefined as any,
-		},
-
-		comment: {
-			validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
-			default: undefined as any,
-		}
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'e7778c7e-3af9-49cd-9690-6dbc3e6c972d'
-		},
-
-		accessDenied: {
-			message: 'Access denied.',
-			code: 'ACCESS_DENIED',
-			id: '01a53b27-82fc-445b-a0c1-b558465a8ed2'
-		},
-
-		noSuchFolder: {
-			message: 'No such folder.',
-			code: 'NO_SUCH_FOLDER',
-			id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'DriveFile'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const file = await DriveFiles.findOne(ps.fileId);
-
-	if (file == null) {
-		throw new ApiError(meta.errors.noSuchFile);
-	}
-
-	if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	if (ps.name) file.name = ps.name;
-
-	if (ps.comment !== undefined) file.comment = ps.comment;
-
-	if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
-
-	if (ps.folderId !== undefined) {
-		if (ps.folderId === null) {
-			file.folderId = null;
-		} else {
-			const folder = await DriveFolders.findOne({
-				id: ps.folderId,
-				userId: user.id
-			});
-
-			if (folder == null) {
-				throw new ApiError(meta.errors.noSuchFolder);
-			}
-
-			file.folderId = folder.id;
-		}
-	}
-
-	await DriveFiles.update(file.id, {
-		name: file.name,
-		comment: file.comment,
-		folderId: file.folderId,
-		isSensitive: file.isSensitive
-	});
-
-	const fileObj = await DriveFiles.pack(file, { self: true });
-
-	// Publish fileUpdated event
-	publishDriveStream(user.id, 'fileUpdated', fileObj);
-
-	return fileObj;
-});
diff --git a/src/server/api/endpoints/drive/files/upload-from-url.ts b/src/server/api/endpoints/drive/files/upload-from-url.ts
deleted file mode 100644
index 9f10a42d24..0000000000
--- a/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import * as ms from 'ms';
-import uploadFromUrl from '@/services/drive/upload-from-url';
-import define from '../../../define';
-import { DriveFiles } from '@/models/index';
-import { publishMainStream } from '@/services/stream';
-import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
-
-export const meta = {
-	tags: ['drive'],
-
-	limit: {
-		duration: ms('1hour'),
-		max: 60
-	},
-
-	requireCredential: true as const,
-
-	kind: 'write:drive',
-
-	params: {
-		url: {
-			// TODO: Validate this url
-			validator: $.str,
-		},
-
-		folderId: {
-			validator: $.optional.nullable.type(ID),
-			default: null,
-		},
-
-		isSensitive: {
-			validator: $.optional.bool,
-			default: false,
-		},
-
-		comment: {
-			validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
-			default: null,
-		},
-
-		marker: {
-			validator: $.optional.nullable.str,
-			default: null,
-		},
-
-		force: {
-			validator: $.optional.bool,
-			default: false,
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force, false, ps.comment).then(file => {
-		DriveFiles.pack(file, { self: true }).then(packedFile => {
-			publishMainStream(user.id, 'urlUploadFinished', {
-				marker: ps.marker,
-				file: packedFile
-			});
-		});
-	});
-});
diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts
deleted file mode 100644
index 6f16878b13..0000000000
--- a/src/server/api/endpoints/drive/folders.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { DriveFolders } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		folderId: {
-			validator: $.optional.nullable.type(ID),
-			default: null,
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'DriveFolder',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId)
-		.andWhere('folder.userId = :userId', { userId: user.id });
-
-	if (ps.folderId) {
-		query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId });
-	} else {
-		query.andWhere('folder.parentId IS NULL');
-	}
-
-	const folders = await query.take(ps.limit!).getMany();
-
-	return await Promise.all(folders.map(folder => DriveFolders.pack(folder)));
-});
diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts
deleted file mode 100644
index 80f96bd641..0000000000
--- a/src/server/api/endpoints/drive/folders/create.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishDriveStream } from '@/services/stream';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { DriveFolders } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'write:drive',
-
-	params: {
-		name: {
-			validator: $.optional.str.pipe(DriveFolders.validateFolderName),
-			default: 'Untitled',
-		},
-
-		parentId: {
-			validator: $.optional.nullable.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchFolder: {
-			message: 'No such folder.',
-			code: 'NO_SUCH_FOLDER',
-			id: '53326628-a00d-40a6-a3cd-8975105c0f95'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'DriveFolder'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// If the parent folder is specified
-	let parent = null;
-	if (ps.parentId) {
-		// Fetch parent folder
-		parent = await DriveFolders.findOne({
-			id: ps.parentId,
-			userId: user.id
-		});
-
-		if (parent == null) {
-			throw new ApiError(meta.errors.noSuchFolder);
-		}
-	}
-
-	// Create folder
-	const folder = await DriveFolders.insert({
-		id: genId(),
-		createdAt: new Date(),
-		name: ps.name,
-		parentId: parent !== null ? parent.id : null,
-		userId: user.id
-	}).then(x => DriveFolders.findOneOrFail(x.identifiers[0]));
-
-	const folderObj = await DriveFolders.pack(folder);
-
-	// Publish folderCreated event
-	publishDriveStream(user.id, 'folderCreated', folderObj);
-
-	return folderObj;
-});
diff --git a/src/server/api/endpoints/drive/folders/delete.ts b/src/server/api/endpoints/drive/folders/delete.ts
deleted file mode 100644
index 38b4aef103..0000000000
--- a/src/server/api/endpoints/drive/folders/delete.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { publishDriveStream } from '@/services/stream';
-import { ApiError } from '../../../error';
-import { DriveFolders, DriveFiles } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'write:drive',
-
-	params: {
-		folderId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchFolder: {
-			message: 'No such folder.',
-			code: 'NO_SUCH_FOLDER',
-			id: '1069098f-c281-440f-b085-f9932edbe091'
-		},
-
-		hasChildFilesOrFolders: {
-			message: 'This folder has child files or folders.',
-			code: 'HAS_CHILD_FILES_OR_FOLDERS',
-			id: 'b0fc8a17-963c-405d-bfbc-859a487295e1'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Get folder
-	const folder = await DriveFolders.findOne({
-		id: ps.folderId,
-		userId: user.id
-	});
-
-	if (folder == null) {
-		throw new ApiError(meta.errors.noSuchFolder);
-	}
-
-	const [childFoldersCount, childFilesCount] = await Promise.all([
-		DriveFolders.count({ parentId: folder.id }),
-		DriveFiles.count({ folderId: folder.id })
-	]);
-
-	if (childFoldersCount !== 0 || childFilesCount !== 0) {
-		throw new ApiError(meta.errors.hasChildFilesOrFolders);
-	}
-
-	await DriveFolders.delete(folder.id);
-
-	// Publish folderCreated event
-	publishDriveStream(user.id, 'folderDeleted', folder.id);
-});
diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts
deleted file mode 100644
index a6c5a49988..0000000000
--- a/src/server/api/endpoints/drive/folders/find.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { DriveFolders } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		name: {
-			validator: $.str
-		},
-
-		parentId: {
-			validator: $.optional.nullable.type(ID),
-			default: null,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'DriveFolder',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const folders = await DriveFolders.find({
-		name: ps.name,
-		userId: user.id,
-		parentId: ps.parentId
-	});
-
-	return await Promise.all(folders.map(folder => DriveFolders.pack(folder)));
-});
diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts
deleted file mode 100644
index e907a24f05..0000000000
--- a/src/server/api/endpoints/drive/folders/show.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { DriveFolders } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		folderId: {
-			validator: $.type(ID),
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'DriveFolder',
-	},
-
-	errors: {
-		noSuchFolder: {
-			message: 'No such folder.',
-			code: 'NO_SUCH_FOLDER',
-			id: 'd74ab9eb-bb09-4bba-bf24-fb58f761e1e9'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Get folder
-	const folder = await DriveFolders.findOne({
-		id: ps.folderId,
-		userId: user.id
-	});
-
-	if (folder == null) {
-		throw new ApiError(meta.errors.noSuchFolder);
-	}
-
-	return await DriveFolders.pack(folder, {
-		detail: true
-	});
-});
diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts
deleted file mode 100644
index 612252e6df..0000000000
--- a/src/server/api/endpoints/drive/folders/update.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishDriveStream } from '@/services/stream';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { DriveFolders } from '@/models/index';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'write:drive',
-
-	params: {
-		folderId: {
-			validator: $.type(ID),
-		},
-
-		name: {
-			validator: $.optional.str.pipe(DriveFolders.validateFolderName),
-		},
-
-		parentId: {
-			validator: $.optional.nullable.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchFolder: {
-			message: 'No such folder.',
-			code: 'NO_SUCH_FOLDER',
-			id: 'f7974dac-2c0d-4a27-926e-23583b28e98e'
-		},
-
-		noSuchParentFolder: {
-			message: 'No such parent folder.',
-			code: 'NO_SUCH_PARENT_FOLDER',
-			id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1'
-		},
-
-		recursiveNesting: {
-			message: 'It can not be structured like nesting folders recursively.',
-			code: 'NO_SUCH_PARENT_FOLDER',
-			id: 'ce104e3a-faaf-49d5-b459-10ff0cbbcaa1'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'DriveFolder'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch folder
-	const folder = await DriveFolders.findOne({
-		id: ps.folderId,
-		userId: user.id
-	});
-
-	if (folder == null) {
-		throw new ApiError(meta.errors.noSuchFolder);
-	}
-
-	if (ps.name) folder.name = ps.name;
-
-	if (ps.parentId !== undefined) {
-		if (ps.parentId === folder.id) {
-			throw new ApiError(meta.errors.recursiveNesting);
-		} else if (ps.parentId === null) {
-			folder.parentId = null;
-		} else {
-			// Get parent folder
-			const parent = await DriveFolders.findOne({
-				id: ps.parentId,
-				userId: user.id
-			});
-
-			if (parent == null) {
-				throw new ApiError(meta.errors.noSuchParentFolder);
-			}
-
-			// Check if the circular reference will occur
-			async function checkCircle(folderId: any): Promise<boolean> {
-				// Fetch folder
-				const folder2 = await DriveFolders.findOne({
-					id: folderId
-				});
-
-				if (folder2!.id === folder!.id) {
-					return true;
-				} else if (folder2!.parentId) {
-					return await checkCircle(folder2!.parentId);
-				} else {
-					return false;
-				}
-			}
-
-			if (parent.parentId !== null) {
-				if (await checkCircle(parent.parentId)) {
-					throw new ApiError(meta.errors.recursiveNesting);
-				}
-			}
-
-			folder.parentId = parent.id;
-		}
-	}
-
-	// Update
-	DriveFolders.update(folder.id, {
-		name: folder.name,
-		parentId: folder.parentId
-	});
-
-	const folderObj = await DriveFolders.pack(folder);
-
-	// Publish folderUpdated event
-	publishDriveStream(user.id, 'folderUpdated', folderObj);
-
-	return folderObj;
-});
diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts
deleted file mode 100644
index 141e02f748..0000000000
--- a/src/server/api/endpoints/drive/stream.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { DriveFiles } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['drive'],
-
-	requireCredential: true as const,
-
-	kind: 'read:drive',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		type: {
-			validator: $.optional.str.match(/^[a-zA-Z\/\-*]+$/)
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'DriveFile',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId)
-		.andWhere('file.userId = :userId', { userId: user.id });
-
-	if (ps.type) {
-		if (ps.type.endsWith('/*')) {
-			query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
-		} else {
-			query.andWhere('file.type = :type', { type: ps.type });
-		}
-	}
-
-	const files = await query.take(ps.limit!).getMany();
-
-	return await DriveFiles.packMany(files, { detail: false, self: true });
-});
diff --git a/src/server/api/endpoints/email-address/available.ts b/src/server/api/endpoints/email-address/available.ts
deleted file mode 100644
index f6fccd59b0..0000000000
--- a/src/server/api/endpoints/email-address/available.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { validateEmailForAccount } from '@/services/validate-email-for-account';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		emailAddress: {
-			validator: $.str
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			available: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-			},
-			reason: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-			},
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	return await validateEmailForAccount(ps.emailAddress);
-});
diff --git a/src/server/api/endpoints/endpoint.ts b/src/server/api/endpoints/endpoint.ts
deleted file mode 100644
index 1a04d8bee8..0000000000
--- a/src/server/api/endpoints/endpoint.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import $ from 'cafy';
-import define from '../define';
-import endpoints from '../endpoints';
-
-export const meta = {
-	requireCredential: false as const,
-
-	tags: ['meta'],
-
-	params: {
-		endpoint: {
-			validator: $.str,
-		}
-	},
-};
-
-export default define(meta, async (ps) => {
-	const ep = endpoints.find(x => x.name === ps.endpoint);
-	if (ep == null) return null;
-	return {
-		params: Object.entries(ep.meta.params || {}).map(([k, v]) => ({
-			name: k,
-			type: v.validator.name === 'ID' ? 'String' : v.validator.name
-		}))
-	};
-});
diff --git a/src/server/api/endpoints/endpoints.ts b/src/server/api/endpoints/endpoints.ts
deleted file mode 100644
index f7b9757d8d..0000000000
--- a/src/server/api/endpoints/endpoints.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import define from '../define';
-import endpoints from '../endpoints';
-
-export const meta = {
-	requireCredential: false as const,
-
-	tags: ['meta'],
-
-	params: {
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const
-		},
-		example: [
-			'admin/abuse-user-reports',
-			'admin/accounts/create',
-			'admin/announcements/create',
-			'...'
-		]
-	}
-};
-
-export default define(meta, async () => {
-	return endpoints.map(x => x.name);
-});
diff --git a/src/server/api/endpoints/federation/dns.ts b/src/server/api/endpoints/federation/dns.ts
deleted file mode 100644
index 7ba566301a..0000000000
--- a/src/server/api/endpoints/federation/dns.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { promises as dns } from 'dns';
-import $ from 'cafy';
-import define from '../../define';
-import { Instances } from '@/models/index';
-import { toPuny } from '@/misc/convert-host';
-
-const resolver = new dns.Resolver();
-resolver.setServers(['1.1.1.1']);
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: false as const,
-
-	params: {
-		host: {
-			validator: $.str
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const instance = await Instances.findOneOrFail({ host: toPuny(ps.host) });
-
-	const [
-		resolved4,
-		resolved6,
-		resolvedCname,
-		resolvedTxt,
-	] = await Promise.all([
-		resolver.resolve4(instance.host).catch(() => []),
-		resolver.resolve6(instance.host).catch(() => []),
-		resolver.resolveCname(instance.host).catch(() => []),
-		resolver.resolveTxt(instance.host).catch(() => []),
-	]);
-
-	return {
-		a: resolved4,
-		aaaa: resolved6,
-		cname: resolvedCname,
-		txt: resolvedTxt,
-	};
-});
diff --git a/src/server/api/endpoints/federation/followers.ts b/src/server/api/endpoints/federation/followers.ts
deleted file mode 100644
index 655e7b7b9a..0000000000
--- a/src/server/api/endpoints/federation/followers.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Followings } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: false as const,
-
-	params: {
-		host: {
-			validator: $.str
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Following',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
-		.andWhere(`following.followeeHost = :host`, { host: ps.host });
-
-	const followings = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Followings.packMany(followings, me, { populateFollowee: true });
-});
diff --git a/src/server/api/endpoints/federation/following.ts b/src/server/api/endpoints/federation/following.ts
deleted file mode 100644
index 5b283581a6..0000000000
--- a/src/server/api/endpoints/federation/following.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Followings } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: false as const,
-
-	params: {
-		host: {
-			validator: $.str
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Following',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
-		.andWhere(`following.followerHost = :host`, { host: ps.host });
-
-	const followings = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Followings.packMany(followings, me, { populateFollowee: true });
-});
diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts
deleted file mode 100644
index cf5e44ebd5..0000000000
--- a/src/server/api/endpoints/federation/instances.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import $ from 'cafy';
-import config from '@/config/index';
-import define from '../../define';
-import { Instances } from '@/models/index';
-import { fetchMeta } from '@/misc/fetch-meta';
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: false as const,
-
-	params: {
-		host: {
-			validator: $.optional.nullable.str,
-		},
-
-		blocked: {
-			validator: $.optional.nullable.bool,
-		},
-
-		notResponding: {
-			validator: $.optional.nullable.bool,
-		},
-
-		suspended: {
-			validator: $.optional.nullable.bool,
-		},
-
-		federating: {
-			validator: $.optional.nullable.bool,
-		},
-
-		subscribing: {
-			validator: $.optional.nullable.bool,
-		},
-
-		publishing: {
-			validator: $.optional.nullable.bool,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 30
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		},
-
-		sort: {
-			validator: $.optional.str,
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'FederationInstance'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const query = Instances.createQueryBuilder('instance');
-
-	switch (ps.sort) {
-		case '+pubSub': query.orderBy('instance.followingCount', 'DESC').orderBy('instance.followersCount', 'DESC'); break;
-		case '-pubSub': query.orderBy('instance.followingCount', 'ASC').orderBy('instance.followersCount', 'ASC'); break;
-		case '+notes': query.orderBy('instance.notesCount', 'DESC'); break;
-		case '-notes': query.orderBy('instance.notesCount', 'ASC'); break;
-		case '+users': query.orderBy('instance.usersCount', 'DESC'); break;
-		case '-users': query.orderBy('instance.usersCount', 'ASC'); break;
-		case '+following': query.orderBy('instance.followingCount', 'DESC'); break;
-		case '-following': query.orderBy('instance.followingCount', 'ASC'); break;
-		case '+followers': query.orderBy('instance.followersCount', 'DESC'); break;
-		case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
-		case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
-		case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
-		case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break;
-		case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break;
-		case '+driveUsage': query.orderBy('instance.driveUsage', 'DESC'); break;
-		case '-driveUsage': query.orderBy('instance.driveUsage', 'ASC'); break;
-		case '+driveFiles': query.orderBy('instance.driveFiles', 'DESC'); break;
-		case '-driveFiles': query.orderBy('instance.driveFiles', 'ASC'); break;
-
-		default: query.orderBy('instance.id', 'DESC'); break;
-	}
-
-	if (typeof ps.blocked === 'boolean') {
-		const meta = await fetchMeta(true);
-		if (ps.blocked) {
-			query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts });
-		} else {
-			query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts });
-		}
-	}
-
-	if (typeof ps.notResponding === 'boolean') {
-		if (ps.notResponding) {
-			query.andWhere('instance.isNotResponding = TRUE');
-		} else {
-			query.andWhere('instance.isNotResponding = FALSE');
-		}
-	}
-
-	if (typeof ps.suspended === 'boolean') {
-		if (ps.suspended) {
-			query.andWhere('instance.isSuspended = TRUE');
-		} else {
-			query.andWhere('instance.isSuspended = FALSE');
-		}
-	}
-
-	if (typeof ps.federating === 'boolean') {
-		if (ps.federating) {
-			query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))');
-		} else {
-			query.andWhere('((instance.followingCount = 0) AND (instance.followersCount = 0))');
-		}
-	}
-
-	if (typeof ps.subscribing === 'boolean') {
-		if (ps.subscribing) {
-			query.andWhere('instance.followersCount > 0');
-		} else {
-			query.andWhere('instance.followersCount = 0');
-		}
-	}
-
-	if (typeof ps.publishing === 'boolean') {
-		if (ps.publishing) {
-			query.andWhere('instance.followingCount > 0');
-		} else {
-			query.andWhere('instance.followingCount = 0');
-		}
-	}
-
-	if (ps.host) {
-		query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' });
-	}
-
-	const instances = await query.take(ps.limit!).skip(ps.offset).getMany();
-
-	return instances;
-});
diff --git a/src/server/api/endpoints/federation/show-instance.ts b/src/server/api/endpoints/federation/show-instance.ts
deleted file mode 100644
index f8352aefb3..0000000000
--- a/src/server/api/endpoints/federation/show-instance.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Instances } from '@/models/index';
-import { toPuny } from '@/misc/convert-host';
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: false as const,
-
-	params: {
-		host: {
-			validator: $.str
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'FederationInstance'
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const instance = await Instances
-		.findOne({ host: toPuny(ps.host) });
-
-	return instance;
-});
diff --git a/src/server/api/endpoints/federation/update-remote-user.ts b/src/server/api/endpoints/federation/update-remote-user.ts
deleted file mode 100644
index 580c3cb3d9..0000000000
--- a/src/server/api/endpoints/federation/update-remote-user.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { getRemoteUser } from '../../common/getters';
-import { updatePerson } from '@/remote/activitypub/models/person';
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: true as const,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps) => {
-	const user = await getRemoteUser(ps.userId);
-	await updatePerson(user.uri!);
-});
diff --git a/src/server/api/endpoints/federation/users.ts b/src/server/api/endpoints/federation/users.ts
deleted file mode 100644
index 0e35df3e1c..0000000000
--- a/src/server/api/endpoints/federation/users.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Users } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['federation'],
-
-	requireCredential: false as const,
-
-	params: {
-		host: {
-			validator: $.str
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(Users.createQueryBuilder('user'), ps.sinceId, ps.untilId)
-		.andWhere(`user.host = :host`, { host: ps.host });
-
-	const users = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Users.packMany(users, me, { detail: true });
-});
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
deleted file mode 100644
index ba9ca1092d..0000000000
--- a/src/server/api/endpoints/following/create.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import * as ms from 'ms';
-import create from '@/services/following/create';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { Followings, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['following', 'users'],
-
-	limit: {
-		duration: ms('1hour'),
-		max: 100
-	},
-
-	requireCredential: true as const,
-
-	kind: 'write:following',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'
-		},
-
-		followeeIsYourself: {
-			message: 'Followee is yourself.',
-			code: 'FOLLOWEE_IS_YOURSELF',
-			id: '26fbe7bb-a331-4857-af17-205b426669a9'
-		},
-
-		alreadyFollowing: {
-			message: 'You are already following that user.',
-			code: 'ALREADY_FOLLOWING',
-			id: '35387507-38c7-4cb9-9197-300b93783fa0'
-		},
-
-		blocking: {
-			message: 'You are blocking that user.',
-			code: 'BLOCKING',
-			id: '4e2206ec-aa4f-4960-b865-6c23ac38e2d9'
-		},
-
-		blocked: {
-			message: 'You are blocked by that user.',
-			code: 'BLOCKED',
-			id: 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const follower = user;
-
-	// 自分自身
-	if (user.id === ps.userId) {
-		throw new ApiError(meta.errors.followeeIsYourself);
-	}
-
-	// Get followee
-	const followee = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Check if already following
-	const exist = await Followings.findOne({
-		followerId: follower.id,
-		followeeId: followee.id
-	});
-
-	if (exist != null) {
-		throw new ApiError(meta.errors.alreadyFollowing);
-	}
-
-	try {
-		await create(follower, followee);
-	} catch (e) {
-		if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);
-		if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked);
-		throw e;
-	}
-
-	return await Users.pack(followee.id, user);
-});
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
deleted file mode 100644
index 0b0158b86e..0000000000
--- a/src/server/api/endpoints/following/delete.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import * as ms from 'ms';
-import deleteFollowing from '@/services/following/delete';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { Followings, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['following', 'users'],
-
-	limit: {
-		duration: ms('1hour'),
-		max: 100
-	},
-
-	requireCredential: true as const,
-
-	kind: 'write:following',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8'
-		},
-
-		followeeIsYourself: {
-			message: 'Followee is yourself.',
-			code: 'FOLLOWEE_IS_YOURSELF',
-			id: 'd9e400b9-36b0-4808-b1d8-79e707f1296c'
-		},
-
-		notFollowing: {
-			message: 'You are not following that user.',
-			code: 'NOT_FOLLOWING',
-			id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const follower = user;
-
-	// Check if the followee is yourself
-	if (user.id === ps.userId) {
-		throw new ApiError(meta.errors.followeeIsYourself);
-	}
-
-	// Get followee
-	const followee = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Check not following
-	const exist = await Followings.findOne({
-		followerId: follower.id,
-		followeeId: followee.id
-	});
-
-	if (exist == null) {
-		throw new ApiError(meta.errors.notFollowing);
-	}
-
-	await deleteFollowing(follower, followee);
-
-	return await Users.pack(followee.id, user);
-});
diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts
deleted file mode 100644
index af39ea1d90..0000000000
--- a/src/server/api/endpoints/following/requests/accept.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import acceptFollowRequest from '@/services/following/requests/accept';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-
-export const meta = {
-	tags: ['following', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:following',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '66ce1645-d66c-46bb-8b79-96739af885bd'
-		},
-		noFollowRequest: {
-			message: 'No follow request.',
-			code: 'NO_FOLLOW_REQUEST',
-			id: 'bcde4f8b-0913-4614-8881-614e522fb041'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch follower
-	const follower = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	await acceptFollowRequest(user, follower).catch(e => {
-		if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError(meta.errors.noFollowRequest);
-		throw e;
-	});
-
-	return;
-});
diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts
deleted file mode 100644
index b69c9d2fe1..0000000000
--- a/src/server/api/endpoints/following/requests/cancel.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import cancelFollowRequest from '@/services/following/requests/cancel';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['following', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:following',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '4e68c551-fc4c-4e46-bb41-7d4a37bf9dab'
-		},
-
-		followRequestNotFound: {
-			message: 'Follow request not found.',
-			code: 'FOLLOW_REQUEST_NOT_FOUND',
-			id: '089b125b-d338-482a-9a09-e2622ac9f8d4'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch followee
-	const followee = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	try {
-		await cancelFollowRequest(followee, user);
-	} catch (e) {
-		if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError(meta.errors.followRequestNotFound);
-		throw e;
-	}
-
-	return await Users.pack(followee.id, user);
-});
diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts
deleted file mode 100644
index 84440ccac7..0000000000
--- a/src/server/api/endpoints/following/requests/list.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import define from '../../../define';
-import { FollowRequests } from '@/models/index';
-
-export const meta = {
-	tags: ['following', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:following',
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				follower: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				},
-				followee: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const reqs = await FollowRequests.find({
-		followeeId: user.id
-	});
-
-	return await Promise.all(reqs.map(req => FollowRequests.pack(req)));
-});
diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts
deleted file mode 100644
index 620324361f..0000000000
--- a/src/server/api/endpoints/following/requests/reject.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import rejectFollowRequest from '@/services/following/requests/reject';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-
-export const meta = {
-	tags: ['following', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:following',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: 'abc2ffa6-25b2-4380-ba99-321ff3a94555'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch follower
-	const follower = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	await rejectFollowRequest(user, follower);
-
-	return;
-});
diff --git a/src/server/api/endpoints/gallery/featured.ts b/src/server/api/endpoints/gallery/featured.ts
deleted file mode 100644
index 30ef8cedec..0000000000
--- a/src/server/api/endpoints/gallery/featured.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import define from '../../define';
-import { GalleryPosts } from '@/models/index';
-
-export const meta = {
-	tags: ['gallery'],
-
-	requireCredential: false as const,
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'GalleryPost',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = GalleryPosts.createQueryBuilder('post')
-		.andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) })
-		.andWhere('post.likedCount > 0')
-		.orderBy('post.likedCount', 'DESC');
-
-	const posts = await query.take(10).getMany();
-
-	return await GalleryPosts.packMany(posts, me);
-});
diff --git a/src/server/api/endpoints/gallery/popular.ts b/src/server/api/endpoints/gallery/popular.ts
deleted file mode 100644
index 18449b9654..0000000000
--- a/src/server/api/endpoints/gallery/popular.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import define from '../../define';
-import { GalleryPosts } from '@/models/index';
-
-export const meta = {
-	tags: ['gallery'],
-
-	requireCredential: false as const,
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'GalleryPost',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = GalleryPosts.createQueryBuilder('post')
-		.andWhere('post.likedCount > 0')
-		.orderBy('post.likedCount', 'DESC');
-
-	const posts = await query.take(10).getMany();
-
-	return await GalleryPosts.packMany(posts, me);
-});
diff --git a/src/server/api/endpoints/gallery/posts.ts b/src/server/api/endpoints/gallery/posts.ts
deleted file mode 100644
index 53d3236d2d..0000000000
--- a/src/server/api/endpoints/gallery/posts.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { GalleryPosts } from '@/models/index';
-
-export const meta = {
-	tags: ['gallery'],
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'GalleryPost',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
-		.innerJoinAndSelect('post.user', 'user');
-
-	const posts = await query.take(ps.limit!).getMany();
-
-	return await GalleryPosts.packMany(posts, me);
-});
diff --git a/src/server/api/endpoints/gallery/posts/create.ts b/src/server/api/endpoints/gallery/posts/create.ts
deleted file mode 100644
index 38b487e6ea..0000000000
--- a/src/server/api/endpoints/gallery/posts/create.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import $ from 'cafy';
-import * as ms from 'ms';
-import define from '../../../define';
-import { ID } from '../../../../../misc/cafy-id';
-import { DriveFiles, GalleryPosts } from '@/models/index';
-import { genId } from '../../../../../misc/gen-id';
-import { GalleryPost } from '@/models/entities/gallery-post';
-import { ApiError } from '../../../error';
-import { DriveFile } from '@/models/entities/drive-file';
-
-export const meta = {
-	tags: ['gallery'],
-
-	requireCredential: true as const,
-
-	kind: 'write:gallery',
-
-	limit: {
-		duration: ms('1hour'),
-		max: 300
-	},
-
-	params: {
-		title: {
-			validator: $.str.min(1),
-		},
-
-		description: {
-			validator: $.optional.nullable.str,
-		},
-
-		fileIds: {
-			validator: $.arr($.type(ID)).unique().range(1, 32),
-		},
-
-		isSensitive: {
-			validator: $.optional.bool,
-			default: false,
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'GalleryPost',
-	},
-
-	errors: {
-
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const files = (await Promise.all(ps.fileIds.map(fileId =>
-		DriveFiles.findOne({
-			id: fileId,
-			userId: user.id
-		})
-	))).filter((file): file is DriveFile => file != null);
-
-	if (files.length === 0) {
-		throw new Error();
-	}
-
-	const post = await GalleryPosts.insert(new GalleryPost({
-		id: genId(),
-		createdAt: new Date(),
-		updatedAt: new Date(),
-		title: ps.title,
-		description: ps.description,
-		userId: user.id,
-		isSensitive: ps.isSensitive,
-		fileIds: files.map(file => file.id)
-	})).then(x => GalleryPosts.findOneOrFail(x.identifiers[0]));
-
-	return await GalleryPosts.pack(post, user);
-});
diff --git a/src/server/api/endpoints/gallery/posts/delete.ts b/src/server/api/endpoints/gallery/posts/delete.ts
deleted file mode 100644
index e5b7c07f2f..0000000000
--- a/src/server/api/endpoints/gallery/posts/delete.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { GalleryPosts } from '@/models/index';
-import { ID } from '@/misc/cafy-id';
-
-export const meta = {
-	tags: ['gallery'],
-
-	requireCredential: true as const,
-
-	kind: 'write:gallery',
-
-	params: {
-		postId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchPost: {
-			message: 'No such post.',
-			code: 'NO_SUCH_POST',
-			id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const post = await GalleryPosts.findOne({
-		id: ps.postId,
-		userId: user.id,
-	});
-
-	if (post == null) {
-		throw new ApiError(meta.errors.noSuchPost);
-	}
-
-	await GalleryPosts.delete(post.id);
-});
diff --git a/src/server/api/endpoints/gallery/posts/like.ts b/src/server/api/endpoints/gallery/posts/like.ts
deleted file mode 100644
index 81a25c0ad1..0000000000
--- a/src/server/api/endpoints/gallery/posts/like.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { GalleryPosts, GalleryLikes } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['gallery'],
-
-	requireCredential: true as const,
-
-	kind: 'write:gallery-likes',
-
-	params: {
-		postId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchPost: {
-			message: 'No such post.',
-			code: 'NO_SUCH_POST',
-			id: '56c06af3-1287-442f-9701-c93f7c4a62ff'
-		},
-
-		yourPost: {
-			message: 'You cannot like your post.',
-			code: 'YOUR_POST',
-			id: 'f78f1511-5ebc-4478-a888-1198d752da68'
-		},
-
-		alreadyLiked: {
-			message: 'The post has already been liked.',
-			code: 'ALREADY_LIKED',
-			id: '40e9ed56-a59c-473a-bf3f-f289c54fb5a7'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const post = await GalleryPosts.findOne(ps.postId);
-	if (post == null) {
-		throw new ApiError(meta.errors.noSuchPost);
-	}
-
-	if (post.userId === user.id) {
-		throw new ApiError(meta.errors.yourPost);
-	}
-
-	// if already liked
-	const exist = await GalleryLikes.findOne({
-		postId: post.id,
-		userId: user.id
-	});
-
-	if (exist != null) {
-		throw new ApiError(meta.errors.alreadyLiked);
-	}
-
-	// Create like
-	await GalleryLikes.insert({
-		id: genId(),
-		createdAt: new Date(),
-		postId: post.id,
-		userId: user.id
-	});
-
-	GalleryPosts.increment({ id: post.id }, 'likedCount', 1);
-});
diff --git a/src/server/api/endpoints/gallery/posts/show.ts b/src/server/api/endpoints/gallery/posts/show.ts
deleted file mode 100644
index 93852a5f8d..0000000000
--- a/src/server/api/endpoints/gallery/posts/show.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { GalleryPosts } from '@/models/index';
-
-export const meta = {
-	tags: ['gallery'],
-
-	requireCredential: false as const,
-
-	params: {
-		postId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchPost: {
-			message: 'No such post.',
-			code: 'NO_SUCH_POST',
-			id: '1137bf14-c5b0-4604-85bb-5b5371b1cd45'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'GalleryPost'
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const post = await GalleryPosts.findOne({
-		id: ps.postId,
-	});
-
-	if (post == null) {
-		throw new ApiError(meta.errors.noSuchPost);
-	}
-
-	return await GalleryPosts.pack(post, me);
-});
diff --git a/src/server/api/endpoints/gallery/posts/unlike.ts b/src/server/api/endpoints/gallery/posts/unlike.ts
deleted file mode 100644
index 0347cdf79e..0000000000
--- a/src/server/api/endpoints/gallery/posts/unlike.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { GalleryPosts, GalleryLikes } from '@/models/index';
-
-export const meta = {
-	tags: ['gallery'],
-
-	requireCredential: true as const,
-
-	kind: 'write:gallery-likes',
-
-	params: {
-		postId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchPost: {
-			message: 'No such post.',
-			code: 'NO_SUCH_POST',
-			id: 'c32e6dd0-b555-4413-925e-b3757d19ed84'
-		},
-
-		notLiked: {
-			message: 'You have not liked that post.',
-			code: 'NOT_LIKED',
-			id: 'e3e8e06e-be37-41f7-a5b4-87a8250288f0'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const post = await GalleryPosts.findOne(ps.postId);
-	if (post == null) {
-		throw new ApiError(meta.errors.noSuchPost);
-	}
-
-	const exist = await GalleryLikes.findOne({
-		postId: post.id,
-		userId: user.id
-	});
-
-	if (exist == null) {
-		throw new ApiError(meta.errors.notLiked);
-	}
-
-	// Delete like
-	await GalleryLikes.delete(exist.id);
-
-	GalleryPosts.decrement({ id: post.id }, 'likedCount', 1);
-});
diff --git a/src/server/api/endpoints/gallery/posts/update.ts b/src/server/api/endpoints/gallery/posts/update.ts
deleted file mode 100644
index 54eea130d3..0000000000
--- a/src/server/api/endpoints/gallery/posts/update.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import $ from 'cafy';
-import * as ms from 'ms';
-import define from '../../../define';
-import { ID } from '../../../../../misc/cafy-id';
-import { DriveFiles, GalleryPosts } from '@/models/index';
-import { GalleryPost } from '@/models/entities/gallery-post';
-import { ApiError } from '../../../error';
-import { DriveFile } from '@/models/entities/drive-file';
-
-export const meta = {
-	tags: ['gallery'],
-
-	requireCredential: true as const,
-
-	kind: 'write:gallery',
-
-	limit: {
-		duration: ms('1hour'),
-		max: 300
-	},
-
-	params: {
-		postId: {
-			validator: $.type(ID),
-		},
-
-		title: {
-			validator: $.str.min(1),
-		},
-
-		description: {
-			validator: $.optional.nullable.str,
-		},
-
-		fileIds: {
-			validator: $.arr($.type(ID)).unique().range(1, 32),
-		},
-
-		isSensitive: {
-			validator: $.optional.bool,
-			default: false,
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'GalleryPost',
-	},
-
-	errors: {
-
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const files = (await Promise.all(ps.fileIds.map(fileId =>
-		DriveFiles.findOne({
-			id: fileId,
-			userId: user.id
-		})
-	))).filter((file): file is DriveFile => file != null);
-
-	if (files.length === 0) {
-		throw new Error();
-	}
-
-	await GalleryPosts.update({
-		id: ps.postId,
-		userId: user.id,
-	}, {
-		updatedAt: new Date(),
-		title: ps.title,
-		description: ps.description,
-		isSensitive: ps.isSensitive,
-		fileIds: files.map(file => file.id)
-	});
-
-	const post = await GalleryPosts.findOneOrFail(ps.postId);
-
-	return await GalleryPosts.pack(post, user);
-});
diff --git a/src/server/api/endpoints/games/reversi/games.ts b/src/server/api/endpoints/games/reversi/games.ts
deleted file mode 100644
index 4db9ecb69f..0000000000
--- a/src/server/api/endpoints/games/reversi/games.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ReversiGames } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-import { Brackets } from 'typeorm';
-
-export const meta = {
-	tags: ['games'],
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		my: {
-			validator: $.optional.bool,
-			default: false
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time'
-				},
-				startedAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time'
-				},
-				isStarted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const
-				},
-				isEnded: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const
-				},
-				form1: {
-					type: 'any' as const,
-					optional: false as const, nullable: true as const
-				},
-				form2: {
-					type: 'any' as const,
-					optional: false as const, nullable: true as const
-				},
-				user1Accepted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-					default: false
-				},
-				user2Accepted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-					default: false
-				},
-				user1Id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				user2Id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				user1: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				},
-				user2: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				},
-				winnerId: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'id'
-				},
-				winner: {
-					type: 'object' as const,
-					optional: false as const, nullable: true as const,
-					ref: 'User'
-				},
-				surrendered: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'id'
-				},
-				black: {
-					type: 'number' as const,
-					optional: false as const, nullable: true as const
-				},
-				bw: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				},
-				isLlotheo: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				canPutEverywhere: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const
-				},
-				loopedBoard: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId)
-		.andWhere('game.isStarted = TRUE');
-
-	if (ps.my && user) {
-		query.andWhere(new Brackets(qb => { qb
-			.where('game.user1Id = :userId', { userId: user.id })
-			.orWhere('game.user2Id = :userId', { userId: user.id });
-		}));
-	}
-
-	// Fetch games
-	const games = await query.take(ps.limit!).getMany();
-
-	return await Promise.all(games.map((g) => ReversiGames.pack(g, user, {
-		detail: false
-	})));
-});
diff --git a/src/server/api/endpoints/games/reversi/games/show.ts b/src/server/api/endpoints/games/reversi/games/show.ts
deleted file mode 100644
index 93afffdb1f..0000000000
--- a/src/server/api/endpoints/games/reversi/games/show.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import Reversi from '../../../../../../games/reversi/core';
-import define from '../../../../define';
-import { ApiError } from '../../../../error';
-import { ReversiGames } from '@/models/index';
-
-export const meta = {
-	tags: ['games'],
-
-	params: {
-		gameId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchGame: {
-			message: 'No such game.',
-			code: 'NO_SUCH_GAME',
-			id: 'f13a03db-fae1-46c9-87f3-43c8165419e1'
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time'
-				},
-				startedAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time'
-				},
-				isStarted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const
-				},
-				isEnded: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const
-				},
-				form1: {
-					type: 'any' as const,
-					optional: false as const, nullable: true as const
-				},
-				form2: {
-					type: 'any' as const,
-					optional: false as const, nullable: true as const
-				},
-				user1Accepted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-					default: false
-				},
-				user2Accepted: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-					default: false
-				},
-				user1Id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				user2Id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				user1: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				},
-				user2: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				},
-				winnerId: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'id'
-				},
-				winner: {
-					type: 'object' as const,
-					optional: false as const, nullable: true as const,
-					ref: 'User'
-				},
-				surrendered: {
-					type: 'string' as const,
-					optional: false as const, nullable: true as const,
-					format: 'id'
-				},
-				black: {
-					type: 'number' as const,
-					optional: false as const, nullable: true as const
-				},
-				bw: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				},
-				isLlotheo: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const,
-				},
-				canPutEverywhere: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const
-				},
-				loopedBoard: {
-					type: 'boolean' as const,
-					optional: false as const, nullable: false as const
-				},
-				board: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
-					items: {
-						type: 'any' as const,
-						optional: false as const, nullable: false as const
-					}
-				},
-				turn: {
-					type: 'any' as const,
-					optional: false as const, nullable: false as const
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const game = await ReversiGames.findOne(ps.gameId);
-
-	if (game == null) {
-		throw new ApiError(meta.errors.noSuchGame);
-	}
-
-	const o = new Reversi(game.map, {
-		isLlotheo: game.isLlotheo,
-		canPutEverywhere: game.canPutEverywhere,
-		loopedBoard: game.loopedBoard
-	});
-
-	for (const log of game.logs) {
-		o.put(log.color, log.pos);
-	}
-
-	const packed = await ReversiGames.pack(game, user);
-
-	return Object.assign({
-		board: o.board,
-		turn: o.turn
-	}, packed);
-});
diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts
deleted file mode 100644
index 00d58b19e3..0000000000
--- a/src/server/api/endpoints/games/reversi/games/surrender.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishReversiGameStream } from '@/services/stream';
-import define from '../../../../define';
-import { ApiError } from '../../../../error';
-import { ReversiGames } from '@/models/index';
-
-export const meta = {
-	tags: ['games'],
-
-	requireCredential: true as const,
-
-	params: {
-		gameId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchGame: {
-			message: 'No such game.',
-			code: 'NO_SUCH_GAME',
-			id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df'
-		},
-
-		alreadyEnded: {
-			message: 'That game has already ended.',
-			code: 'ALREADY_ENDED',
-			id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d'
-		},
-
-		accessDenied: {
-			message: 'Access denied.',
-			code: 'ACCESS_DENIED',
-			id: '6e04164b-a992-4c93-8489-2123069973e1'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const game = await ReversiGames.findOne(ps.gameId);
-
-	if (game == null) {
-		throw new ApiError(meta.errors.noSuchGame);
-	}
-
-	if (game.isEnded) {
-		throw new ApiError(meta.errors.alreadyEnded);
-	}
-
-	if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
-
-	await ReversiGames.update(game.id, {
-		surrendered: user.id,
-		isEnded: true,
-		winnerId: winnerId
-	});
-
-	publishReversiGameStream(game.id, 'ended', {
-		winnerId: winnerId,
-		game: await ReversiGames.pack(game.id, user)
-	});
-});
diff --git a/src/server/api/endpoints/games/reversi/invitations.ts b/src/server/api/endpoints/games/reversi/invitations.ts
deleted file mode 100644
index c8629377b2..0000000000
--- a/src/server/api/endpoints/games/reversi/invitations.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import define from '../../../define';
-import { ReversiMatchings } from '@/models/index';
-
-export const meta = {
-	tags: ['games'],
-
-	requireCredential: true as const,
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				createdAt: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'date-time'
-				},
-				parentId: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				parent: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				},
-				childId: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				child: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'User'
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Find session
-	const invitations = await ReversiMatchings.find({
-		childId: user.id
-	});
-
-	return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user)));
-});
diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts
deleted file mode 100644
index 5ceb16c7d7..0000000000
--- a/src/server/api/endpoints/games/reversi/match.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishMainStream, publishReversiStream } from '@/services/stream';
-import { eighteight } from '../../../../../games/reversi/maps';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { genId } from '@/misc/gen-id';
-import { ReversiMatchings, ReversiGames } from '@/models/index';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { ReversiMatching } from '@/models/entities/games/reversi/matching';
-
-export const meta = {
-	tags: ['games'],
-
-	requireCredential: true as const,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '0b4f0559-b484-4e31-9581-3f73cee89b28'
-		},
-
-		isYourself: {
-			message: 'Target user is yourself.',
-			code: 'TARGET_IS_YOURSELF',
-			id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Myself
-	if (ps.userId === user.id) {
-		throw new ApiError(meta.errors.isYourself);
-	}
-
-	// Find session
-	const exist = await ReversiMatchings.findOne({
-		parentId: ps.userId,
-		childId: user.id
-	});
-
-	if (exist) {
-		// Destroy session
-		ReversiMatchings.delete(exist.id);
-
-		// Create game
-		const game = await ReversiGames.save({
-			id: genId(),
-			createdAt: new Date(),
-			user1Id: exist.parentId,
-			user2Id: user.id,
-			user1Accepted: false,
-			user2Accepted: false,
-			isStarted: false,
-			isEnded: false,
-			logs: [],
-			map: eighteight.data,
-			bw: 'random',
-			isLlotheo: false
-		} as Partial<ReversiGame>);
-
-		publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId }));
-
-		const other = await ReversiMatchings.count({
-			childId: user.id
-		});
-
-		if (other == 0) {
-			publishMainStream(user.id, 'reversiNoInvites');
-		}
-
-		return await ReversiGames.pack(game, user);
-	} else {
-		// Fetch child
-		const child = await getUser(ps.userId).catch(e => {
-			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-			throw e;
-		});
-
-		// 以前のセッションはすべて削除しておく
-		await ReversiMatchings.delete({
-			parentId: user.id
-		});
-
-		// セッションを作成
-		const matching = await ReversiMatchings.save({
-			id: genId(),
-			createdAt: new Date(),
-			parentId: user.id,
-			childId: child.id
-		} as ReversiMatching);
-
-		const packed = await ReversiMatchings.pack(matching, child);
-		publishReversiStream(child.id, 'invited', packed);
-		publishMainStream(child.id, 'reversiInvited', packed);
-
-		return;
-	}
-});
diff --git a/src/server/api/endpoints/games/reversi/match/cancel.ts b/src/server/api/endpoints/games/reversi/match/cancel.ts
deleted file mode 100644
index e4a138bb87..0000000000
--- a/src/server/api/endpoints/games/reversi/match/cancel.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import define from '../../../../define';
-import { ReversiMatchings } from '@/models/index';
-
-export const meta = {
-	tags: ['games'],
-
-	requireCredential: true as const
-};
-
-export default define(meta, async (ps, user) => {
-	await ReversiMatchings.delete({
-		parentId: user.id
-	});
-});
diff --git a/src/server/api/endpoints/get-online-users-count.ts b/src/server/api/endpoints/get-online-users-count.ts
deleted file mode 100644
index 5c80d588d3..0000000000
--- a/src/server/api/endpoints/get-online-users-count.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { USER_ONLINE_THRESHOLD } from '@/const';
-import { Users } from '@/models/index';
-import { MoreThan } from 'typeorm';
-import define from '../define';
-
-export const meta = {
-	tags: ['meta'],
-
-	requireCredential: false as const,
-
-	params: {
-	}
-};
-
-export default define(meta, async () => {
-	const count = await Users.count({
-		lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD))
-	});
-
-	return {
-		count
-	};
-});
diff --git a/src/server/api/endpoints/hashtags/list.ts b/src/server/api/endpoints/hashtags/list.ts
deleted file mode 100644
index 821016a50c..0000000000
--- a/src/server/api/endpoints/hashtags/list.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Hashtags } from '@/models/index';
-
-export const meta = {
-	tags: ['hashtags'],
-
-	requireCredential: false as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		attachedToUserOnly: {
-			validator: $.optional.bool,
-			default: false
-		},
-
-		attachedToLocalUserOnly: {
-			validator: $.optional.bool,
-			default: false
-		},
-
-		attachedToRemoteUserOnly: {
-			validator: $.optional.bool,
-			default: false
-		},
-
-		sort: {
-			validator: $.str.or([
-				'+mentionedUsers',
-				'-mentionedUsers',
-				'+mentionedLocalUsers',
-				'-mentionedLocalUsers',
-				'+mentionedRemoteUsers',
-				'-mentionedRemoteUsers',
-				'+attachedUsers',
-				'-attachedUsers',
-				'+attachedLocalUsers',
-				'-attachedLocalUsers',
-				'+attachedRemoteUsers',
-				'-attachedRemoteUsers',
-			]),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Hashtag',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = Hashtags.createQueryBuilder('tag');
-
-	if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0');
-	if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0');
-	if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0');
-
-	switch (ps.sort) {
-		case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break;
-		case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break;
-		case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break;
-		case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break;
-		case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break;
-		case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break;
-		case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break;
-		case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break;
-		case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break;
-		case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break;
-		case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break;
-		case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break;
-	}
-
-	query.select([
-		'tag.name',
-		'tag.mentionedUsersCount',
-		'tag.mentionedLocalUsersCount',
-		'tag.mentionedRemoteUsersCount',
-		'tag.attachedUsersCount',
-		'tag.attachedLocalUsersCount',
-		'tag.attachedRemoteUsersCount',
-	]);
-
-	const tags = await query.take(ps.limit!).getMany();
-
-	return Hashtags.packMany(tags);
-});
diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts
deleted file mode 100644
index fd0cac3983..0000000000
--- a/src/server/api/endpoints/hashtags/search.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Hashtags } from '@/models/index';
-
-export const meta = {
-	tags: ['hashtags'],
-
-	requireCredential: false as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		query: {
-			validator: $.str,
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0,
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		}
-	},
-};
-
-export default define(meta, async (ps) => {
-	const hashtags = await Hashtags.createQueryBuilder('tag')
-		.where('tag.name like :q', { q: ps.query.toLowerCase() + '%' })
-		.orderBy('tag.count', 'DESC')
-		.groupBy('tag.id')
-		.take(ps.limit!)
-		.skip(ps.offset)
-		.getMany();
-
-	return hashtags.map(tag => tag.name);
-});
diff --git a/src/server/api/endpoints/hashtags/show.ts b/src/server/api/endpoints/hashtags/show.ts
deleted file mode 100644
index f22edbfffd..0000000000
--- a/src/server/api/endpoints/hashtags/show.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Hashtags } from '@/models/index';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-
-export const meta = {
-	tags: ['hashtags'],
-
-	requireCredential: false as const,
-
-	params: {
-		tag: {
-			validator: $.str,
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Hashtag',
-	},
-
-	errors: {
-		noSuchHashtag: {
-			message: 'No such hashtag.',
-			code: 'NO_SUCH_HASHTAG',
-			id: '110ee688-193e-4a3a-9ecf-c167b2e6981e'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const hashtag = await Hashtags.findOne({ name: normalizeForSearch(ps.tag) });
-	if (hashtag == null) {
-		throw new ApiError(meta.errors.noSuchHashtag);
-	}
-
-	return await Hashtags.pack(hashtag);
-});
diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts
deleted file mode 100644
index 3d67241ab6..0000000000
--- a/src/server/api/endpoints/hashtags/trend.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { Brackets } from 'typeorm';
-import define from '../../define';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Notes } from '@/models/index';
-import { Note } from '@/models/entities/note';
-import { safeForSql } from '@/misc/safe-for-sql';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-
-/*
-トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前～今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
-ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる
-
-..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する
-*/
-
-const rangeA = 1000 * 60 * 60; // 60分
-//const rangeB = 1000 * 60 * 120; // 2時間
-//const coefficient = 1.25; // 「n倍」の部分
-//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか
-
-const max = 5;
-
-export const meta = {
-	tags: ['hashtags'],
-
-	requireCredential: false as const,
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				tag: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-				},
-				chart: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
-					items: {
-						type: 'number' as const,
-						optional: false as const, nullable: false as const,
-					}
-				},
-				usersCount: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async () => {
-	const instance = await fetchMeta(true);
-	const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
-
-	const now = new Date(); // 5分単位で丸めた現在日時
-	now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0);
-
-	const tagNotes = await Notes.createQueryBuilder('note')
-		.where(`note.createdAt > :date`, { date: new Date(now.getTime() - rangeA) })
-		.andWhere(new Brackets(qb => { qb
-			.where(`note.visibility = 'public'`)
-			.orWhere(`note.visibility = 'home'`);
-		}))
-		.andWhere(`note.tags != '{}'`)
-		.select(['note.tags', 'note.userId'])
-		.cache(60000) // 1 min
-		.getMany();
-
-	if (tagNotes.length === 0) {
-		return [];
-	}
-
-	const tags: {
-		name: string;
-		users: Note['userId'][];
-	}[] = [];
-
-	for (const note of tagNotes) {
-		for (const tag of note.tags) {
-			if (hiddenTags.includes(tag)) continue;
-
-			const x = tags.find(x => x.name === tag);
-			if (x) {
-				if (!x.users.includes(note.userId)) {
-					x.users.push(note.userId);
-				}
-			} else {
-				tags.push({
-					name: tag,
-					users: [note.userId]
-				});
-			}
-		}
-	}
-
-	// タグを人気順に並べ替え
-	const hots = tags
-		.sort((a, b) => b.users.length - a.users.length)
-		.map(tag => tag.name)
-		.slice(0, max);
-
-	//#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する
-	const countPromises: Promise<number[]>[] = [];
-
-	const range = 20;
-
-	// 10分
-	const interval = 1000 * 60 * 10;
-
-	for (let i = 0; i < range; i++) {
-		countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
-			.select('count(distinct note.userId)')
-			.where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`)
-			.andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) })
-			.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) })
-			.cache(60000) // 1 min
-			.getRawOne()
-			.then(x => parseInt(x.count, 10))
-		)));
-	}
-
-	const countsLog = await Promise.all(countPromises);
-	//#endregion
-
-	const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
-		.select('count(distinct note.userId)')
-		.where(`'{"${safeForSql(tag) ? tag : 'aichan_kawaii'}"}' <@ note.tags`)
-		.andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - rangeA) })
-		.cache(60000 * 60) // 60 min
-		.getRawOne()
-		.then(x => parseInt(x.count, 10))
-	));
-
-	const stats = hots.map((tag, i) => ({
-		tag,
-		chart: countsLog.map(counts => counts[i]),
-		usersCount: totalCounts[i]
-	}));
-
-	return stats;
-});
diff --git a/src/server/api/endpoints/hashtags/users.ts b/src/server/api/endpoints/hashtags/users.ts
deleted file mode 100644
index 8c8cd1510b..0000000000
--- a/src/server/api/endpoints/hashtags/users.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Users } from '@/models/index';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-
-export const meta = {
-	requireCredential: false as const,
-
-	tags: ['hashtags', 'users'],
-
-	params: {
-		tag: {
-			validator: $.str,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sort: {
-			validator: $.str.or([
-				'+follower',
-				'-follower',
-				'+createdAt',
-				'-createdAt',
-				'+updatedAt',
-				'-updatedAt',
-			]),
-		},
-
-		state: {
-			validator: $.optional.str.or([
-				'all',
-				'alive'
-			]),
-			default: 'all'
-		},
-
-		origin: {
-			validator: $.optional.str.or([
-				'combined',
-				'local',
-				'remote',
-			]),
-			default: 'local'
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = Users.createQueryBuilder('user')
-		.where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) });
-
-	const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));
-
-	if (ps.state === 'alive') {
-		query.andWhere('user.updatedAt > :date', { date: recent });
-	}
-
-	if (ps.origin === 'local') {
-		query.andWhere('user.host IS NULL');
-	} else if (ps.origin === 'remote') {
-		query.andWhere('user.host IS NOT NULL');
-	}
-
-	switch (ps.sort) {
-		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
-		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
-		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
-		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
-		case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break;
-		case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break;
-	}
-
-	const users = await query.take(ps.limit!).getMany();
-
-	return await Users.packMany(users, me, { detail: true });
-});
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
deleted file mode 100644
index 0568a962d8..0000000000
--- a/src/server/api/endpoints/i.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import define from '../define';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	params: {},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
-	},
-};
-
-export default define(meta, async (ps, user, token) => {
-	const isSecure = token == null;
-
-	// ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す
-	return await Users.pack(user.id, user, {
-		detail: true,
-		includeSecrets: isSecure
-	});
-});
diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts
deleted file mode 100644
index 2bd2128cce..0000000000
--- a/src/server/api/endpoints/i/2fa/done.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import $ from 'cafy';
-import * as speakeasy from 'speakeasy';
-import define from '../../../define';
-import { UserProfiles } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		token: {
-			validator: $.str
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const token = ps.token.replace(/\s/g, '');
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	if (profile.twoFactorTempSecret == null) {
-		throw new Error('二段階認証の設定が開始されていません');
-	}
-
-	const verified = (speakeasy as any).totp.verify({
-		secret: profile.twoFactorTempSecret,
-		encoding: 'base32',
-		token: token
-	});
-
-	if (!verified) {
-		throw new Error('not verified');
-	}
-
-	await UserProfiles.update(user.id, {
-		twoFactorSecret: profile.twoFactorTempSecret,
-		twoFactorEnabled: true
-	});
-});
diff --git a/src/server/api/endpoints/i/2fa/key-done.ts b/src/server/api/endpoints/i/2fa/key-done.ts
deleted file mode 100644
index b4d3af235a..0000000000
--- a/src/server/api/endpoints/i/2fa/key-done.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import { promisify } from 'util';
-import * as cbor from 'cbor';
-import define from '../../../define';
-import {
-	UserProfiles,
-	UserSecurityKeys,
-	AttestationChallenges,
-	Users
-} from '@/models/index';
-import config from '@/config/index';
-import { procedures, hash } from '../../../2fa';
-import { publishMainStream } from '@/services/stream';
-
-const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		clientDataJSON: {
-			validator: $.str
-		},
-		attestationObject: {
-			validator: $.str
-		},
-		password: {
-			validator: $.str
-		},
-		challengeId: {
-			validator: $.str
-		},
-		name: {
-			validator: $.str
-		}
-	}
-};
-
-const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8'));
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(ps.password, profile.password!);
-
-	if (!same) {
-		throw new Error('incorrect password');
-	}
-
-	if (!profile.twoFactorEnabled) {
-		throw new Error('2fa not enabled');
-	}
-
-	const clientData = JSON.parse(ps.clientDataJSON);
-
-	if (clientData.type != 'webauthn.create') {
-		throw new Error('not a creation attestation');
-	}
-	if (clientData.origin != config.scheme + '://' + config.host) {
-		throw new Error('origin mismatch');
-	}
-
-	const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
-
-	const attestation = await cborDecodeFirst(ps.attestationObject);
-
-	const rpIdHash = attestation.authData.slice(0, 32);
-	if (!rpIdHashReal.equals(rpIdHash)) {
-		throw new Error('rpIdHash mismatch');
-	}
-
-	const flags = attestation.authData[32];
-
-	// tslint:disable-next-line:no-bitwise
-	if (!(flags & 1)) {
-		throw new Error('user not present');
-	}
-
-	const authData = Buffer.from(attestation.authData);
-	const credentialIdLength = authData.readUInt16BE(53);
-	const credentialId = authData.slice(55, 55 + credentialIdLength);
-	const publicKeyData = authData.slice(55 + credentialIdLength);
-	const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
-	if (publicKey.get(3) != -7) {
-		throw new Error('alg mismatch');
-	}
-
-	if (!(procedures as any)[attestation.fmt]) {
-		throw new Error('unsupported fmt');
-	}
-
-	const verificationData = (procedures as any)[attestation.fmt].verify({
-		attStmt: attestation.attStmt,
-		authenticatorData: authData,
-		clientDataHash: clientDataJSONHash,
-		credentialId,
-		publicKey,
-		rpIdHash
-	});
-	if (!verificationData.valid) throw new Error('signature invalid');
-
-	const attestationChallenge = await AttestationChallenges.findOne({
-		userId: user.id,
-		id: ps.challengeId,
-		registrationChallenge: true,
-		challenge: hash(clientData.challenge).toString('hex')
-	});
-
-	if (!attestationChallenge) {
-		throw new Error('non-existent challenge');
-	}
-
-	await AttestationChallenges.delete({
-		userId: user.id,
-		id: ps.challengeId
-	});
-
-	// Expired challenge (> 5min old)
-	if (
-		new Date().getTime() - attestationChallenge.createdAt.getTime() >=
-		5 * 60 * 1000
-	) {
-		throw new Error('expired challenge');
-	}
-
-	const credentialIdString = credentialId.toString('hex');
-
-	await UserSecurityKeys.save({
-		userId: user.id,
-		id: credentialIdString,
-		lastUsed: new Date(),
-		name: ps.name,
-		publicKey: verificationData.publicKey.toString('hex')
-	});
-
-	// Publish meUpdated event
-	publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, {
-		detail: true,
-		includeSecrets: true
-	}));
-
-	return {
-		id: credentialIdString,
-		name: ps.name
-	};
-});
diff --git a/src/server/api/endpoints/i/2fa/password-less.ts b/src/server/api/endpoints/i/2fa/password-less.ts
deleted file mode 100644
index 064828b638..0000000000
--- a/src/server/api/endpoints/i/2fa/password-less.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { UserProfiles } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		value: {
-			validator: $.boolean
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	await UserProfiles.update(user.id, {
-		usePasswordLessLogin: ps.value
-	});
-});
diff --git a/src/server/api/endpoints/i/2fa/register-key.ts b/src/server/api/endpoints/i/2fa/register-key.ts
deleted file mode 100644
index 1b385a10ee..0000000000
--- a/src/server/api/endpoints/i/2fa/register-key.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import define from '../../../define';
-import { UserProfiles, AttestationChallenges } from '@/models/index';
-import { promisify } from 'util';
-import * as crypto from 'crypto';
-import { genId } from '@/misc/gen-id';
-import { hash } from '../../../2fa';
-
-const randomBytes = promisify(crypto.randomBytes);
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		password: {
-			validator: $.str
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(ps.password, profile.password!);
-
-	if (!same) {
-		throw new Error('incorrect password');
-	}
-
-	if (!profile.twoFactorEnabled) {
-		throw new Error('2fa not enabled');
-	}
-
-	// 32 byte challenge
-	const entropy = await randomBytes(32);
-	const challenge = entropy.toString('base64')
-		.replace(/=/g, '')
-		.replace(/\+/g, '-')
-		.replace(/\//g, '_');
-
-	const challengeId = genId();
-
-	await AttestationChallenges.save({
-		userId: user.id,
-		id: challengeId,
-		challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
-		createdAt: new Date(),
-		registrationChallenge: true
-	});
-
-	return {
-		challengeId,
-		challenge
-	};
-});
diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts
deleted file mode 100644
index b03b98188a..0000000000
--- a/src/server/api/endpoints/i/2fa/register.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import * as speakeasy from 'speakeasy';
-import * as QRCode from 'qrcode';
-import config from '@/config/index';
-import define from '../../../define';
-import { UserProfiles } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		password: {
-			validator: $.str
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(ps.password, profile.password!);
-
-	if (!same) {
-		throw new Error('incorrect password');
-	}
-
-	// Generate user's secret key
-	const secret = speakeasy.generateSecret({
-		length: 32
-	});
-
-	await UserProfiles.update(user.id, {
-		twoFactorTempSecret: secret.base32
-	});
-
-	// Get the data URL of the authenticator URL
-	const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({
-		secret: secret.base32,
-		encoding: 'base32',
-		label: user.username,
-		issuer: config.host
-	}));
-
-	return {
-		qr: dataUrl,
-		secret: secret.base32,
-		label: user.username,
-		issuer: config.host
-	};
-});
diff --git a/src/server/api/endpoints/i/2fa/remove-key.ts b/src/server/api/endpoints/i/2fa/remove-key.ts
deleted file mode 100644
index dea56301ab..0000000000
--- a/src/server/api/endpoints/i/2fa/remove-key.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import define from '../../../define';
-import { UserProfiles, UserSecurityKeys, Users } from '@/models/index';
-import { publishMainStream } from '@/services/stream';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		password: {
-			validator: $.str
-		},
-		credentialId: {
-			validator: $.str
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(ps.password, profile.password!);
-
-	if (!same) {
-		throw new Error('incorrect password');
-	}
-
-	// Make sure we only delete the user's own creds
-	await UserSecurityKeys.delete({
-		userId: user.id,
-		id: ps.credentialId
-	});
-
-	// Publish meUpdated event
-	publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, {
-		detail: true,
-		includeSecrets: true
-	}));
-
-	return {};
-});
diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts
deleted file mode 100644
index af53033daa..0000000000
--- a/src/server/api/endpoints/i/2fa/unregister.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import define from '../../../define';
-import { UserProfiles } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		password: {
-			validator: $.str
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(ps.password, profile.password!);
-
-	if (!same) {
-		throw new Error('incorrect password');
-	}
-
-	await UserProfiles.update(user.id, {
-		twoFactorSecret: null,
-		twoFactorEnabled: false
-	});
-});
diff --git a/src/server/api/endpoints/i/apps.ts b/src/server/api/endpoints/i/apps.ts
deleted file mode 100644
index 994528e5c9..0000000000
--- a/src/server/api/endpoints/i/apps.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { AccessTokens } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		sort: {
-			validator: $.optional.str.or([
-				'+createdAt',
-				'-createdAt',
-				'+lastUsedAt',
-				'-lastUsedAt',
-			]),
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = AccessTokens.createQueryBuilder('token')
-		.where('token.userId = :userId', { userId: user.id });
-
-	switch (ps.sort) {
-		case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break;
-		case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break;
-		case '+lastUsedAt': query.orderBy('token.lastUsedAt', 'DESC'); break;
-		case '-lastUsedAt': query.orderBy('token.lastUsedAt', 'ASC'); break;
-		default: query.orderBy('token.id', 'ASC'); break;
-	}
-
-	const tokens = await query.getMany();
-
-	return await Promise.all(tokens.map(token => ({
-		id: token.id,
-		name: token.name,
-		createdAt: token.createdAt,
-		lastUsedAt: token.lastUsedAt,
-		permission: token.permission,
-	})));
-});
diff --git a/src/server/api/endpoints/i/authorized-apps.ts b/src/server/api/endpoints/i/authorized-apps.ts
deleted file mode 100644
index 042fcd14e8..0000000000
--- a/src/server/api/endpoints/i/authorized-apps.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { AccessTokens, Apps } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0,
-		},
-
-		sort: {
-			validator: $.optional.str.or('desc|asc'),
-			default: 'desc',
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Get tokens
-	const tokens = await AccessTokens.find({
-		where: {
-			userId: user.id
-		},
-		take: ps.limit!,
-		skip: ps.offset,
-		order: {
-			id: ps.sort == 'asc' ? 1 : -1
-		}
-	});
-
-	return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, {
-		detail: true
-	})));
-});
diff --git a/src/server/api/endpoints/i/change-password.ts b/src/server/api/endpoints/i/change-password.ts
deleted file mode 100644
index 7ea5f8c488..0000000000
--- a/src/server/api/endpoints/i/change-password.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import define from '../../define';
-import { UserProfiles } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		currentPassword: {
-			validator: $.str
-		},
-
-		newPassword: {
-			validator: $.str
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(ps.currentPassword, profile.password!);
-
-	if (!same) {
-		throw new Error('incorrect password');
-	}
-
-	// Generate hash of password
-	const salt = await bcrypt.genSalt(8);
-	const hash = await bcrypt.hash(ps.newPassword, salt);
-
-	await UserProfiles.update(user.id, {
-		password: hash
-	});
-});
diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts
deleted file mode 100644
index 10e5adf64a..0000000000
--- a/src/server/api/endpoints/i/delete-account.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import define from '../../define';
-import { UserProfiles, Users } from '@/models/index';
-import { doPostSuspend } from '@/services/suspend-user';
-import { publishUserEvent } from '@/services/stream';
-import { createDeleteAccountJob } from '@/queue';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		password: {
-			validator: $.str
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-	const userDetailed = await Users.findOneOrFail(user.id);
-	if (userDetailed.isDeleted) {
-		return;
-	}
-
-	// Compare password
-	const same = await bcrypt.compare(ps.password, profile.password!);
-
-	if (!same) {
-		throw new Error('incorrect password');
-	}
-
-	// 物理削除する前にDelete activityを送信する
-	await doPostSuspend(user).catch(e => {});
-
-	createDeleteAccountJob(user, {
-		soft: false
-	});
-
-	await Users.update(user.id, {
-		isDeleted: true,
-	});
-
-	// Terminate streaming
-	publishUserEvent(user.id, 'terminate', {});
-});
diff --git a/src/server/api/endpoints/i/export-blocking.ts b/src/server/api/endpoints/i/export-blocking.ts
deleted file mode 100644
index e4797da0c1..0000000000
--- a/src/server/api/endpoints/i/export-blocking.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import define from '../../define';
-import { createExportBlockingJob } from '@/queue/index';
-import * as ms from 'ms';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-	limit: {
-		duration: ms('1hour'),
-		max: 1,
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	createExportBlockingJob(user);
-});
diff --git a/src/server/api/endpoints/i/export-following.ts b/src/server/api/endpoints/i/export-following.ts
deleted file mode 100644
index b0f154cda8..0000000000
--- a/src/server/api/endpoints/i/export-following.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import define from '../../define';
-import { createExportFollowingJob } from '@/queue/index';
-import * as ms from 'ms';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-	limit: {
-		duration: ms('1hour'),
-		max: 1,
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	createExportFollowingJob(user);
-});
diff --git a/src/server/api/endpoints/i/export-mute.ts b/src/server/api/endpoints/i/export-mute.ts
deleted file mode 100644
index 46d547fa53..0000000000
--- a/src/server/api/endpoints/i/export-mute.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import define from '../../define';
-import { createExportMuteJob } from '@/queue/index';
-import * as ms from 'ms';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-	limit: {
-		duration: ms('1hour'),
-		max: 1,
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	createExportMuteJob(user);
-});
diff --git a/src/server/api/endpoints/i/export-notes.ts b/src/server/api/endpoints/i/export-notes.ts
deleted file mode 100644
index 441bf16896..0000000000
--- a/src/server/api/endpoints/i/export-notes.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import define from '../../define';
-import { createExportNotesJob } from '@/queue/index';
-import * as ms from 'ms';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-	limit: {
-		duration: ms('1day'),
-		max: 1,
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	createExportNotesJob(user);
-});
diff --git a/src/server/api/endpoints/i/export-user-lists.ts b/src/server/api/endpoints/i/export-user-lists.ts
deleted file mode 100644
index 24043a862a..0000000000
--- a/src/server/api/endpoints/i/export-user-lists.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import define from '../../define';
-import { createExportUserListsJob } from '@/queue/index';
-import * as ms from 'ms';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-	limit: {
-		duration: ms('1min'),
-		max: 1,
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	createExportUserListsJob(user);
-});
diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts
deleted file mode 100644
index b79d68ae73..0000000000
--- a/src/server/api/endpoints/i/favorites.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { NoteFavorites } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['account', 'notes', 'favorites'],
-
-	requireCredential: true as const,
-
-	kind: 'read:favorites',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'NoteFavorite',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId)
-		.andWhere(`favorite.userId = :meId`, { meId: user.id })
-		.leftJoinAndSelect('favorite.note', 'note');
-
-	const favorites = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await NoteFavorites.packMany(favorites, user);
-});
diff --git a/src/server/api/endpoints/i/gallery/likes.ts b/src/server/api/endpoints/i/gallery/likes.ts
deleted file mode 100644
index 7a2935a5ec..0000000000
--- a/src/server/api/endpoints/i/gallery/likes.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { GalleryLikes } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['account', 'gallery'],
-
-	requireCredential: true as const,
-
-	kind: 'read:gallery-likes',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			},
-			page: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'GalleryPost'
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(GalleryLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
-		.andWhere(`like.userId = :meId`, { meId: user.id })
-		.leftJoinAndSelect('like.post', 'post');
-
-	const likes = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await GalleryLikes.packMany(likes, user);
-});
diff --git a/src/server/api/endpoints/i/gallery/posts.ts b/src/server/api/endpoints/i/gallery/posts.ts
deleted file mode 100644
index 21bb8759fc..0000000000
--- a/src/server/api/endpoints/i/gallery/posts.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { GalleryPosts } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['account', 'gallery'],
-
-	requireCredential: true as const,
-
-	kind: 'read:gallery',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'GalleryPost'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
-		.andWhere(`post.userId = :meId`, { meId: user.id });
-
-	const posts = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await GalleryPosts.packMany(posts, user);
-});
diff --git a/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/src/server/api/endpoints/i/get-word-muted-notes-count.ts
deleted file mode 100644
index 6b9be98582..0000000000
--- a/src/server/api/endpoints/i/get-word-muted-notes-count.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import define from '../../define';
-import { MutedNotes } from '@/models/index';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:account',
-
-	params: {
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			count: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	return {
-		count: await MutedNotes.count({
-			userId: user.id,
-			reason: 'word'
-		})
-	};
-});
diff --git a/src/server/api/endpoints/i/import-blocking.ts b/src/server/api/endpoints/i/import-blocking.ts
deleted file mode 100644
index d44d0b6077..0000000000
--- a/src/server/api/endpoints/i/import-blocking.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { createImportBlockingJob } from '@/queue/index';
-import * as ms from 'ms';
-import { ApiError } from '../../error';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-
-	limit: {
-		duration: ms('1hour'),
-		max: 1,
-	},
-
-	params: {
-		fileId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'ebb53e5f-6574-9c0c-0b92-7ca6def56d7e'
-		},
-
-		unexpectedFileType: {
-			message: 'We need csv file.',
-			code: 'UNEXPECTED_FILE_TYPE',
-			id: 'b6fab7d6-d945-d67c-dfdb-32da1cd12cfe'
-		},
-
-		tooBigFile: {
-			message: 'That file is too big.',
-			code: 'TOO_BIG_FILE',
-			id: 'b7fbf0b1-aeef-3b21-29ef-fadd4cb72ccf'
-		},
-
-		emptyFile: {
-			message: 'That file is empty.',
-			code: 'EMPTY_FILE',
-			id: '6f3a4dcc-f060-a707-4950-806fbdbe60d6'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const file = await DriveFiles.findOne(ps.fileId);
-
-	if (file == null) throw new ApiError(meta.errors.noSuchFile);
-	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
-	if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
-	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
-
-	createImportBlockingJob(user, file.id);
-});
diff --git a/src/server/api/endpoints/i/import-following.ts b/src/server/api/endpoints/i/import-following.ts
deleted file mode 100644
index b3de397661..0000000000
--- a/src/server/api/endpoints/i/import-following.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { createImportFollowingJob } from '@/queue/index';
-import * as ms from 'ms';
-import { ApiError } from '../../error';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-	limit: {
-		duration: ms('1hour'),
-		max: 1,
-	},
-
-	params: {
-		fileId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'b98644cf-a5ac-4277-a502-0b8054a709a3'
-		},
-
-		unexpectedFileType: {
-			message: 'We need csv file.',
-			code: 'UNEXPECTED_FILE_TYPE',
-			id: '660f3599-bce0-4f95-9dde-311fd841c183'
-		},
-
-		tooBigFile: {
-			message: 'That file is too big.',
-			code: 'TOO_BIG_FILE',
-			id: 'dee9d4ed-ad07-43ed-8b34-b2856398bc60'
-		},
-
-		emptyFile: {
-			message: 'That file is empty.',
-			code: 'EMPTY_FILE',
-			id: '31a1b42c-06f7-42ae-8a38-a661c5c9f691'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const file = await DriveFiles.findOne(ps.fileId);
-
-	if (file == null) throw new ApiError(meta.errors.noSuchFile);
-	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
-	if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
-	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
-
-	createImportFollowingJob(user, file.id);
-});
diff --git a/src/server/api/endpoints/i/import-muting.ts b/src/server/api/endpoints/i/import-muting.ts
deleted file mode 100644
index c17434c587..0000000000
--- a/src/server/api/endpoints/i/import-muting.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { createImportMutingJob } from '@/queue/index';
-import * as ms from 'ms';
-import { ApiError } from '../../error';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-
-	limit: {
-		duration: ms('1hour'),
-		max: 1,
-	},
-
-	params: {
-		fileId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'e674141e-bd2a-ba85-e616-aefb187c9c2a'
-		},
-
-		unexpectedFileType: {
-			message: 'We need csv file.',
-			code: 'UNEXPECTED_FILE_TYPE',
-			id: '568c6e42-c86c-ba09-c004-517f83f9f1a8'
-		},
-
-		tooBigFile: {
-			message: 'That file is too big.',
-			code: 'TOO_BIG_FILE',
-			id: '9b4ada6d-d7f7-0472-0713-4f558bd1ec9c'
-		},
-
-		emptyFile: {
-			message: 'That file is empty.',
-			code: 'EMPTY_FILE',
-			id: 'd2f12af1-e7b4-feac-86a3-519548f2728e'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const file = await DriveFiles.findOne(ps.fileId);
-
-	if (file == null) throw new ApiError(meta.errors.noSuchFile);
-	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
-	if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
-	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
-
-	createImportMutingJob(user, file.id);
-});
diff --git a/src/server/api/endpoints/i/import-user-lists.ts b/src/server/api/endpoints/i/import-user-lists.ts
deleted file mode 100644
index 9069a019a9..0000000000
--- a/src/server/api/endpoints/i/import-user-lists.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { createImportUserListsJob } from '@/queue/index';
-import * as ms from 'ms';
-import { ApiError } from '../../error';
-import { DriveFiles } from '@/models/index';
-
-export const meta = {
-	secure: true,
-	requireCredential: true as const,
-	limit: {
-		duration: ms('1hour'),
-		max: 1,
-	},
-
-	params: {
-		fileId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'ea9cc34f-c415-4bc6-a6fe-28ac40357049'
-		},
-
-		unexpectedFileType: {
-			message: 'We need csv file.',
-			code: 'UNEXPECTED_FILE_TYPE',
-			id: 'a3c9edda-dd9b-4596-be6a-150ef813745c'
-		},
-
-		tooBigFile: {
-			message: 'That file is too big.',
-			code: 'TOO_BIG_FILE',
-			id: 'ae6e7a22-971b-4b52-b2be-fc0b9b121fe9'
-		},
-
-		emptyFile: {
-			message: 'That file is empty.',
-			code: 'EMPTY_FILE',
-			id: '99efe367-ce6e-4d44-93f8-5fae7b040356'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const file = await DriveFiles.findOne(ps.fileId);
-
-	if (file == null) throw new ApiError(meta.errors.noSuchFile);
-	//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
-	if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile);
-	if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
-
-	createImportUserListsJob(user, file.id);
-});
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
deleted file mode 100644
index 56668d03b7..0000000000
--- a/src/server/api/endpoints/i/notifications.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { readNotification } from '../../common/read-notification';
-import define from '../../define';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Notifications, Followings, Mutings, Users } from '@/models/index';
-import { notificationTypes } from '@/types';
-import read from '@/services/note/read';
-import { Brackets } from 'typeorm';
-
-export const meta = {
-	tags: ['account', 'notifications'],
-
-	requireCredential: true as const,
-
-	kind: 'read:notifications',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		following: {
-			validator: $.optional.bool,
-			default: false
-		},
-
-		unreadOnly: {
-			validator: $.optional.bool,
-			default: false
-		},
-
-		markAsRead: {
-			validator: $.optional.bool,
-			default: true
-		},
-
-		includeTypes: {
-			validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])),
-		},
-
-		excludeTypes: {
-			validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])),
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Notification',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	// includeTypes が空の場合はクエリしない
-	if (ps.includeTypes && ps.includeTypes.length === 0) {
-		return [];
-	}
-	// excludeTypes に全指定されている場合はクエリしない
-	if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
-		return [];
-	}
-	const followingQuery = Followings.createQueryBuilder('following')
-		.select('following.followeeId')
-		.where('following.followerId = :followerId', { followerId: user.id });
-
-	const mutingQuery = Mutings.createQueryBuilder('muting')
-		.select('muting.muteeId')
-		.where('muting.muterId = :muterId', { muterId: user.id });
-
-	const suspendedQuery = Users.createQueryBuilder('users')
-		.select('users.id')
-		.where('users.isSuspended = TRUE');
-
-	const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
-		.andWhere(`notification.notifieeId = :meId`, { meId: user.id })
-		.leftJoinAndSelect('notification.notifier', 'notifier')
-		.leftJoinAndSelect('notification.note', 'note')
-		.leftJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	query.andWhere(new Brackets(qb => { qb
-		.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
-		.orWhere('notification.notifierId IS NULL');
-	}));
-	query.setParameters(mutingQuery.getParameters());
-
-	query.andWhere(new Brackets(qb => { qb
-		.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
-		.orWhere('notification.notifierId IS NULL');
-	}));
-
-	if (ps.following) {
-		query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id });
-		query.setParameters(followingQuery.getParameters());
-	}
-
-	if (ps.includeTypes && ps.includeTypes.length > 0) {
-		query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
-	} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
-		query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
-	}
-
-	if (ps.unreadOnly) {
-		query.andWhere(`notification.isRead = false`);
-	}
-
-	const notifications = await query.take(ps.limit!).getMany();
-
-	// Mark all as read
-	if (notifications.length > 0 && ps.markAsRead) {
-		readNotification(user.id, notifications.map(x => x.id));
-	}
-
-	const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!);
-
-	if (notes.length > 0) {
-		read(user.id, notes);
-	}
-
-	return await Notifications.packMany(notifications, user.id);
-});
diff --git a/src/server/api/endpoints/i/page-likes.ts b/src/server/api/endpoints/i/page-likes.ts
deleted file mode 100644
index fa2bc31730..0000000000
--- a/src/server/api/endpoints/i/page-likes.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { PageLikes } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['account', 'pages'],
-
-	requireCredential: true as const,
-
-	kind: 'read:page-likes',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			id: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'id'
-			},
-			page: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'Page'
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
-		.andWhere(`like.userId = :meId`, { meId: user.id })
-		.leftJoinAndSelect('like.page', 'page');
-
-	const likes = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await PageLikes.packMany(likes, user);
-});
diff --git a/src/server/api/endpoints/i/pages.ts b/src/server/api/endpoints/i/pages.ts
deleted file mode 100644
index ee87fffa2d..0000000000
--- a/src/server/api/endpoints/i/pages.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Pages } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['account', 'pages'],
-
-	requireCredential: true as const,
-
-	kind: 'read:pages',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Page'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
-		.andWhere(`page.userId = :meId`, { meId: user.id });
-
-	const pages = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Pages.packMany(pages);
-});
diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts
deleted file mode 100644
index de94220ba9..0000000000
--- a/src/server/api/endpoints/i/pin.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { addPinned } from '@/services/i/pin';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['account', 'notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '56734f8b-3928-431e-bf80-6ff87df40cb3'
-		},
-
-		pinLimitExceeded: {
-			message: 'You can not pin notes any more.',
-			code: 'PIN_LIMIT_EXCEEDED',
-			id: '72dab508-c64d-498f-8740-a8eec1ba385a'
-		},
-
-		alreadyPinned: {
-			message: 'That note has already been pinned.',
-			code: 'ALREADY_PINNED',
-			id: '8b18c2b7-68fe-4edb-9892-c0cbaeb6c913'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	await addPinned(user, ps.noteId).catch(e => {
-		if (e.id === '70c4e51f-5bea-449c-a030-53bee3cce202') throw new ApiError(meta.errors.noSuchNote);
-		if (e.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError(meta.errors.pinLimitExceeded);
-		if (e.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError(meta.errors.alreadyPinned);
-		throw e;
-	});
-
-	return await Users.pack(user.id, user, {
-		detail: true
-	});
-});
diff --git a/src/server/api/endpoints/i/read-all-messaging-messages.ts b/src/server/api/endpoints/i/read-all-messaging-messages.ts
deleted file mode 100644
index 9aca7611c9..0000000000
--- a/src/server/api/endpoints/i/read-all-messaging-messages.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { publishMainStream } from '@/services/stream';
-import define from '../../define';
-import { MessagingMessages, UserGroupJoinings } from '@/models/index';
-
-export const meta = {
-	tags: ['account', 'messaging'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Update documents
-	await MessagingMessages.update({
-		recipientId: user.id,
-		isRead: false
-	}, {
-		isRead: true
-	});
-
-	const joinings = await UserGroupJoinings.find({ userId: user.id });
-
-	await Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder().update()
-		.set({
-			reads: (() => `array_append("reads", '${user.id}')`) as any
-		})
-		.where(`groupId = :groupId`, { groupId: j.userGroupId })
-		.andWhere('userId != :userId', { userId: user.id })
-		.andWhere('NOT (:userId = ANY(reads))', { userId: user.id })
-		.execute()));
-
-	publishMainStream(user.id, 'readAllMessagingMessages');
-});
diff --git a/src/server/api/endpoints/i/read-all-unread-notes.ts b/src/server/api/endpoints/i/read-all-unread-notes.ts
deleted file mode 100644
index 2a7102a590..0000000000
--- a/src/server/api/endpoints/i/read-all-unread-notes.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { publishMainStream } from '@/services/stream';
-import define from '../../define';
-import { NoteUnreads } from '@/models/index';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Remove documents
-	await NoteUnreads.delete({
-		userId: user.id
-	});
-
-	// 全て既読になったイベントを発行
-	publishMainStream(user.id, 'readAllUnreadMentions');
-	publishMainStream(user.id, 'readAllUnreadSpecifiedNotes');
-});
diff --git a/src/server/api/endpoints/i/read-announcement.ts b/src/server/api/endpoints/i/read-announcement.ts
deleted file mode 100644
index 2f5036f953..0000000000
--- a/src/server/api/endpoints/i/read-announcement.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { genId } from '@/misc/gen-id';
-import { AnnouncementReads, Announcements, Users } from '@/models/index';
-import { publishMainStream } from '@/services/stream';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		announcementId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchAnnouncement: {
-			message: 'No such announcement.',
-			code: 'NO_SUCH_ANNOUNCEMENT',
-			id: '184663db-df88-4bc2-8b52-fb85f0681939'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Check if announcement exists
-	const announcement = await Announcements.findOne(ps.announcementId);
-
-	if (announcement == null) {
-		throw new ApiError(meta.errors.noSuchAnnouncement);
-	}
-
-	// Check if already read
-	const read = await AnnouncementReads.findOne({
-		announcementId: ps.announcementId,
-		userId: user.id
-	});
-
-	if (read != null) {
-		return;
-	}
-
-	// Create read
-	await AnnouncementReads.insert({
-		id: genId(),
-		createdAt: new Date(),
-		announcementId: ps.announcementId,
-		userId: user.id,
-	});
-
-	if (!await Users.getHasUnreadAnnouncement(user.id)) {
-		publishMainStream(user.id, 'readAllAnnouncements');
-	}
-});
diff --git a/src/server/api/endpoints/i/regenerate-token.ts b/src/server/api/endpoints/i/regenerate-token.ts
deleted file mode 100644
index 1cce2d37be..0000000000
--- a/src/server/api/endpoints/i/regenerate-token.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import { publishMainStream, publishUserEvent } from '@/services/stream';
-import generateUserToken from '../../common/generate-native-user-token';
-import define from '../../define';
-import { Users, UserProfiles } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		password: {
-			validator: $.str
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(ps.password, profile.password!);
-
-	if (!same) {
-		throw new Error('incorrect password');
-	}
-
-	// Generate secret
-	const secret = generateUserToken();
-
-	await Users.update(user.id, {
-		token: secret
-	});
-
-	// Publish event
-	publishMainStream(user.id, 'myTokenRegenerated');
-
-	// Terminate streaming
-	setTimeout(() => {
-		publishUserEvent(user.id, 'terminate', {});
-	}, 5000);
-});
diff --git a/src/server/api/endpoints/i/registry/get-all.ts b/src/server/api/endpoints/i/registry/get-all.ts
deleted file mode 100644
index c8eaf83a25..0000000000
--- a/src/server/api/endpoints/i/registry/get-all.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { RegistryItems } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		scope: {
-			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
-			default: [],
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = RegistryItems.createQueryBuilder('item')
-		.where('item.domain IS NULL')
-		.andWhere('item.userId = :userId', { userId: user.id })
-		.andWhere('item.scope = :scope', { scope: ps.scope });
-
-	const items = await query.getMany();
-
-	const res = {} as Record<string, any>;
-
-	for (const item of items) {
-		res[item.key] = item.value;
-	}
-
-	return res;
-});
diff --git a/src/server/api/endpoints/i/registry/get-detail.ts b/src/server/api/endpoints/i/registry/get-detail.ts
deleted file mode 100644
index 992800c44c..0000000000
--- a/src/server/api/endpoints/i/registry/get-detail.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { RegistryItems } from '@/models/index';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		key: {
-			validator: $.str
-		},
-
-		scope: {
-			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
-			default: [],
-		},
-	},
-
-	errors: {
-		noSuchKey: {
-			message: 'No such key.',
-			code: 'NO_SUCH_KEY',
-			id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a'
-		},
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = RegistryItems.createQueryBuilder('item')
-		.where('item.domain IS NULL')
-		.andWhere('item.userId = :userId', { userId: user.id })
-		.andWhere('item.key = :key', { key: ps.key })
-		.andWhere('item.scope = :scope', { scope: ps.scope });
-
-	const item = await query.getOne();
-
-	if (item == null) {
-		throw new ApiError(meta.errors.noSuchKey);
-	}
-
-	return {
-		updatedAt: item.updatedAt,
-		value: item.value,
-	};
-});
diff --git a/src/server/api/endpoints/i/registry/get.ts b/src/server/api/endpoints/i/registry/get.ts
deleted file mode 100644
index 569c3a9280..0000000000
--- a/src/server/api/endpoints/i/registry/get.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { RegistryItems } from '@/models/index';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		key: {
-			validator: $.str
-		},
-
-		scope: {
-			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
-			default: [],
-		},
-	},
-
-	errors: {
-		noSuchKey: {
-			message: 'No such key.',
-			code: 'NO_SUCH_KEY',
-			id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a'
-		},
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = RegistryItems.createQueryBuilder('item')
-		.where('item.domain IS NULL')
-		.andWhere('item.userId = :userId', { userId: user.id })
-		.andWhere('item.key = :key', { key: ps.key })
-		.andWhere('item.scope = :scope', { scope: ps.scope });
-
-	const item = await query.getOne();
-
-	if (item == null) {
-		throw new ApiError(meta.errors.noSuchKey);
-	}
-
-	return item.value;
-});
diff --git a/src/server/api/endpoints/i/registry/keys-with-type.ts b/src/server/api/endpoints/i/registry/keys-with-type.ts
deleted file mode 100644
index 16a4fee374..0000000000
--- a/src/server/api/endpoints/i/registry/keys-with-type.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { RegistryItems } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		scope: {
-			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
-			default: [],
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = RegistryItems.createQueryBuilder('item')
-		.where('item.domain IS NULL')
-		.andWhere('item.userId = :userId', { userId: user.id })
-		.andWhere('item.scope = :scope', { scope: ps.scope });
-
-	const items = await query.getMany();
-
-	const res = {} as Record<string, string>;
-
-	for (const item of items) {
-		const type = typeof item.value;
-		res[item.key] =
-			item.value === null ? 'null' :
-			Array.isArray(item.value) ? 'array' :
-			type === 'number' ? 'number' :
-			type === 'string' ? 'string' :
-			type === 'boolean' ? 'boolean' :
-			type === 'object' ? 'object' :
-			null as never;
-	}
-
-	return res;
-});
diff --git a/src/server/api/endpoints/i/registry/keys.ts b/src/server/api/endpoints/i/registry/keys.ts
deleted file mode 100644
index 3a8aeaa195..0000000000
--- a/src/server/api/endpoints/i/registry/keys.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { RegistryItems } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		scope: {
-			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
-			default: [],
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = RegistryItems.createQueryBuilder('item')
-		.select('item.key')
-		.where('item.domain IS NULL')
-		.andWhere('item.userId = :userId', { userId: user.id })
-		.andWhere('item.scope = :scope', { scope: ps.scope });
-
-	const items = await query.getMany();
-
-	return items.map(x => x.key);
-});
diff --git a/src/server/api/endpoints/i/registry/remove.ts b/src/server/api/endpoints/i/registry/remove.ts
deleted file mode 100644
index 07bc23d4a6..0000000000
--- a/src/server/api/endpoints/i/registry/remove.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { RegistryItems } from '@/models/index';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		key: {
-			validator: $.str
-		},
-
-		scope: {
-			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
-			default: [],
-		},
-	},
-
-	errors: {
-		noSuchKey: {
-			message: 'No such key.',
-			code: 'NO_SUCH_KEY',
-			id: '1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019'
-		},
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = RegistryItems.createQueryBuilder('item')
-		.where('item.domain IS NULL')
-		.andWhere('item.userId = :userId', { userId: user.id })
-		.andWhere('item.key = :key', { key: ps.key })
-		.andWhere('item.scope = :scope', { scope: ps.scope });
-
-	const item = await query.getOne();
-
-	if (item == null) {
-		throw new ApiError(meta.errors.noSuchKey);
-	}
-
-	await RegistryItems.remove(item);
-});
diff --git a/src/server/api/endpoints/i/registry/scopes.ts b/src/server/api/endpoints/i/registry/scopes.ts
deleted file mode 100644
index ecbdb05a8e..0000000000
--- a/src/server/api/endpoints/i/registry/scopes.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import define from '../../../define';
-import { RegistryItems } from '@/models/index';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = RegistryItems.createQueryBuilder('item')
-		.select('item.scope')
-		.where('item.domain IS NULL')
-		.andWhere('item.userId = :userId', { userId: user.id });
-
-	const items = await query.getMany();
-
-	const res = [] as string[][];
-
-	for (const item of items) {
-		if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue;
-		res.push(item.scope);
-	}
-
-	return res;
-});
diff --git a/src/server/api/endpoints/i/registry/set.ts b/src/server/api/endpoints/i/registry/set.ts
deleted file mode 100644
index f129ee1b70..0000000000
--- a/src/server/api/endpoints/i/registry/set.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import $ from 'cafy';
-import { publishMainStream } from '@/services/stream';
-import define from '../../../define';
-import { RegistryItems } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		key: {
-			validator: $.str.min(1)
-		},
-
-		value: {
-			validator: $.nullable.any
-		},
-
-		scope: {
-			validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
-			default: [],
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = RegistryItems.createQueryBuilder('item')
-		.where('item.domain IS NULL')
-		.andWhere('item.userId = :userId', { userId: user.id })
-		.andWhere('item.key = :key', { key: ps.key })
-		.andWhere('item.scope = :scope', { scope: ps.scope });
-
-	const existingItem = await query.getOne();
-
-	if (existingItem) {
-		await RegistryItems.update(existingItem.id, {
-			updatedAt: new Date(),
-			value: ps.value
-		});
-	} else {
-		await RegistryItems.insert({
-			id: genId(),
-			createdAt: new Date(),
-			updatedAt: new Date(),
-			userId: user.id,
-			domain: null,
-			scope: ps.scope,
-			key: ps.key,
-			value: ps.value
-		});
-	}
-
-	// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
-	publishMainStream(user.id, 'registryUpdated', {
-		scope: ps.scope,
-		key: ps.key,
-		value: ps.value
-	});
-});
diff --git a/src/server/api/endpoints/i/revoke-token.ts b/src/server/api/endpoints/i/revoke-token.ts
deleted file mode 100644
index bed868def4..0000000000
--- a/src/server/api/endpoints/i/revoke-token.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { AccessTokens } from '@/models/index';
-import { ID } from '@/misc/cafy-id';
-import { publishUserEvent } from '@/services/stream';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		tokenId: {
-			validator: $.type(ID)
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const token = await AccessTokens.findOne(ps.tokenId);
-
-	if (token) {
-		await AccessTokens.delete({
-			id: ps.tokenId,
-			userId: user.id,
-		});
-
-		// Terminate streaming
-		publishUserEvent(user.id, 'terminate');
-	}
-});
diff --git a/src/server/api/endpoints/i/signin-history.ts b/src/server/api/endpoints/i/signin-history.ts
deleted file mode 100644
index a2c10148c6..0000000000
--- a/src/server/api/endpoints/i/signin-history.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Signins } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId)
-		.andWhere(`signin.userId = :meId`, { meId: user.id });
-
-	const history = await query.take(ps.limit!).getMany();
-
-	return await Promise.all(history.map(record => Signins.pack(record)));
-});
diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts
deleted file mode 100644
index dc79e255ab..0000000000
--- a/src/server/api/endpoints/i/unpin.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { removePinned } from '@/services/i/pin';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['account', 'notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '454170ce-9d63-4a43-9da1-ea10afe81e21'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User'
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	await removePinned(user, ps.noteId).catch(e => {
-		if (e.id === 'b302d4cf-c050-400a-bbb3-be208681f40c') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	return await Users.pack(user.id, user, {
-		detail: true
-	});
-});
diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts
deleted file mode 100644
index 9b6fb9c410..0000000000
--- a/src/server/api/endpoints/i/update-email.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import $ from 'cafy';
-import { publishMainStream } from '@/services/stream';
-import define from '../../define';
-import rndstr from 'rndstr';
-import config from '@/config/index';
-import * as ms from 'ms';
-import * as bcrypt from 'bcryptjs';
-import { Users, UserProfiles } from '@/models/index';
-import { sendEmail } from '@/services/send-email';
-import { ApiError } from '../../error';
-import { validateEmailForAccount } from '@/services/validate-email-for-account';
-
-export const meta = {
-	requireCredential: true as const,
-
-	secure: true,
-
-	limit: {
-		duration: ms('1hour'),
-		max: 3
-	},
-
-	params: {
-		password: {
-			validator: $.str
-		},
-
-		email: {
-			validator: $.optional.nullable.str
-		},
-	},
-
-	errors: {
-		incorrectPassword: {
-			message: 'Incorrect password.',
-			code: 'INCORRECT_PASSWORD',
-			id: 'e54c1d7e-e7d6-4103-86b6-0a95069b4ad3'
-		},
-
-		unavailable: {
-			message: 'Unavailable email address.',
-			code: 'UNAVAILABLE',
-			id: 'a2defefb-f220-8849-0af6-17f816099323'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(ps.password, profile.password!);
-
-	if (!same) {
-		throw new ApiError(meta.errors.incorrectPassword);
-	}
-
-	if (ps.email != null) {
-		const available = await validateEmailForAccount(ps.email);
-		if (!available) {
-			throw new ApiError(meta.errors.unavailable);
-		}
-	}
-
-	await UserProfiles.update(user.id, {
-		email: ps.email,
-		emailVerified: false,
-		emailVerifyCode: null
-	});
-
-	const iObj = await Users.pack(user.id, user, {
-		detail: true,
-		includeSecrets: true
-	});
-
-	// Publish meUpdated event
-	publishMainStream(user.id, 'meUpdated', iObj);
-
-	if (ps.email != null) {
-		const code = rndstr('a-z0-9', 16);
-
-		await UserProfiles.update(user.id, {
-			emailVerifyCode: code
-		});
-
-		const link = `${config.url}/verify-email/${code}`;
-
-		sendEmail(ps.email, 'Email verification',
-			`To verify email, please click this link:<br><a href="${link}">${link}</a>`,
-			`To verify email, please click this link: ${link}`);
-	}
-
-	return iObj;
-});
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
deleted file mode 100644
index d0f201ab60..0000000000
--- a/src/server/api/endpoints/i/update.ts
+++ /dev/null
@@ -1,294 +0,0 @@
-import $ from 'cafy';
-import * as mfm from 'mfm-js';
-import { ID } from '@/misc/cafy-id';
-import { publishMainStream, publishUserEvent } from '@/services/stream';
-import acceptAllFollowRequests from '@/services/following/requests/accept-all';
-import { publishToFollowers } from '@/services/i/update';
-import define from '../../define';
-import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm';
-import { extractHashtags } from '@/misc/extract-hashtags';
-import * as langmap from 'langmap';
-import { updateUsertags } from '@/services/update-hashtag';
-import { ApiError } from '../../error';
-import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index';
-import { User } from '@/models/entities/user';
-import { UserProfile } from '@/models/entities/user-profile';
-import { notificationTypes } from '@/types';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		name: {
-			validator: $.optional.nullable.use(Users.validateName),
-		},
-
-		description: {
-			validator: $.optional.nullable.use(Users.validateDescription),
-		},
-
-		lang: {
-			validator: $.optional.nullable.str.or(Object.keys(langmap)),
-		},
-
-		location: {
-			validator: $.optional.nullable.use(Users.validateLocation),
-		},
-
-		birthday: {
-			validator: $.optional.nullable.use(Users.validateBirthday),
-		},
-
-		avatarId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		bannerId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		fields: {
-			validator: $.optional.arr($.object()).range(1, 4),
-		},
-
-		isLocked: {
-			validator: $.optional.bool,
-		},
-
-		isExplorable: {
-			validator: $.optional.bool,
-		},
-
-		hideOnlineStatus: {
-			validator: $.optional.bool,
-		},
-
-		publicReactions: {
-			validator: $.optional.bool,
-		},
-
-		ffVisibility: {
-			validator: $.optional.str,
-		},
-
-		carefulBot: {
-			validator: $.optional.bool,
-		},
-
-		autoAcceptFollowed: {
-			validator: $.optional.bool,
-		},
-
-		noCrawle: {
-			validator: $.optional.bool,
-		},
-
-		isBot: {
-			validator: $.optional.bool,
-		},
-
-		isCat: {
-			validator: $.optional.bool,
-		},
-
-		injectFeaturedNote: {
-			validator: $.optional.bool,
-		},
-
-		receiveAnnouncementEmail: {
-			validator: $.optional.bool,
-		},
-
-		alwaysMarkNsfw: {
-			validator: $.optional.bool,
-		},
-
-		pinnedPageId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		mutedWords: {
-			validator: $.optional.arr($.arr($.str))
-		},
-
-		mutingNotificationTypes: {
-			validator: $.optional.arr($.str.or(notificationTypes as unknown as string[]))
-		},
-
-		emailNotificationTypes: {
-			validator: $.optional.arr($.str)
-		},
-	},
-
-	errors: {
-		noSuchAvatar: {
-			message: 'No such avatar file.',
-			code: 'NO_SUCH_AVATAR',
-			id: '539f3a45-f215-4f81-a9a8-31293640207f'
-		},
-
-		noSuchBanner: {
-			message: 'No such banner file.',
-			code: 'NO_SUCH_BANNER',
-			id: '0d8f5629-f210-41c2-9433-735831a58595'
-		},
-
-		avatarNotAnImage: {
-			message: 'The file specified as an avatar is not an image.',
-			code: 'AVATAR_NOT_AN_IMAGE',
-			id: 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191'
-		},
-
-		bannerNotAnImage: {
-			message: 'The file specified as a banner is not an image.',
-			code: 'BANNER_NOT_AN_IMAGE',
-			id: '75aedb19-2afd-4e6d-87fc-67941256fa60'
-		},
-
-		noSuchPage: {
-			message: 'No such page.',
-			code: 'NO_SUCH_PAGE',
-			id: '8e01b590-7eb9-431b-a239-860e086c408e'
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User'
-	}
-};
-
-export default define(meta, async (ps, _user, token) => {
-	const user = await Users.findOneOrFail(_user.id);
-	const isSecure = token == null;
-
-	const updates = {} as Partial<User>;
-	const profileUpdates = {} as Partial<UserProfile>;
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	if (ps.name !== undefined) updates.name = ps.name;
-	if (ps.description !== undefined) profileUpdates.description = ps.description;
-	if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
-	if (ps.location !== undefined) profileUpdates.location = ps.location;
-	if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
-	if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
-	if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
-	if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
-	if (ps.mutedWords !== undefined) {
-		profileUpdates.mutedWords = ps.mutedWords;
-		profileUpdates.enableWordMute = ps.mutedWords.length > 0;
-	}
-	if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][];
-	if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
-	if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
-	if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
-	if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
-	if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
-	if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
-	if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
-	if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
-	if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
-	if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
-	if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
-	if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
-	if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
-
-	if (ps.avatarId) {
-		const avatar = await DriveFiles.findOne(ps.avatarId);
-
-		if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
-		if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
-
-		updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
-
-		if (avatar.blurhash) {
-			updates.avatarBlurhash = avatar.blurhash;
-		}
-	}
-
-	if (ps.bannerId) {
-		const banner = await DriveFiles.findOne(ps.bannerId);
-
-		if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
-		if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
-
-		updates.bannerUrl = DriveFiles.getPublicUrl(banner, false);
-
-		if (banner.blurhash) {
-			updates.bannerBlurhash = banner.blurhash;
-		}
-	}
-
-	if (ps.pinnedPageId) {
-		const page = await Pages.findOne(ps.pinnedPageId);
-
-		if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
-
-		profileUpdates.pinnedPageId = page.id;
-	} else if (ps.pinnedPageId === null) {
-		profileUpdates.pinnedPageId = null;
-	}
-
-	if (ps.fields) {
-		profileUpdates.fields = ps.fields
-			.filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
-			.map(x => {
-				return { name: x.name, value: x.value };
-			});
-	}
-
-	//#region emojis/tags
-
-	let emojis = [] as string[];
-	let tags = [] as string[];
-
-	const newName = updates.name === undefined ? user.name : updates.name;
-	const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
-
-	if (newName != null) {
-		const tokens = mfm.parsePlain(newName);
-		emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
-	}
-
-	if (newDescription != null) {
-		const tokens = mfm.parse(newDescription);
-		emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
-		tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
-	}
-
-	updates.emojis = emojis;
-	updates.tags = tags;
-
-	// ハッシュタグ更新
-	updateUsertags(user, tags);
-	//#endregion
-
-	if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
-	if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates);
-
-	const iObj = await Users.pack(user.id, user, {
-		detail: true,
-		includeSecrets: isSecure
-	});
-
-	// Publish meUpdated event
-	publishMainStream(user.id, 'meUpdated', iObj);
-	publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.findOne(user.id));
-
-	// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
-	if (user.isLocked && ps.isLocked === false) {
-		acceptAllFollowRequests(user);
-	}
-
-	// フォロワーにUpdateを配信
-	publishToFollowers(user.id);
-
-	return iObj;
-});
diff --git a/src/server/api/endpoints/i/user-group-invites.ts b/src/server/api/endpoints/i/user-group-invites.ts
deleted file mode 100644
index 1ebde243ca..0000000000
--- a/src/server/api/endpoints/i/user-group-invites.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { UserGroupInvitations } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['account', 'groups'],
-
-	requireCredential: true as const,
-
-	kind: 'read:user-groups',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const,
-					format: 'id'
-				},
-				group: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					ref: 'UserGroup'
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(UserGroupInvitations.createQueryBuilder('invitation'), ps.sinceId, ps.untilId)
-		.andWhere(`invitation.userId = :meId`, { meId: user.id })
-		.leftJoinAndSelect('invitation.userGroup', 'user_group');
-
-	const invitations = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await UserGroupInvitations.packMany(invitations);
-});
diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts
deleted file mode 100644
index e447703546..0000000000
--- a/src/server/api/endpoints/messaging/history.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-import { MessagingMessages, Mutings, UserGroupJoinings } from '@/models/index';
-import { Brackets } from 'typeorm';
-
-export const meta = {
-	tags: ['messaging'],
-
-	requireCredential: true as const,
-
-	kind: 'read:messaging',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		group: {
-			validator: $.optional.bool,
-			default: false
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'MessagingMessage',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const mute = await Mutings.find({
-		muterId: user.id,
-	});
-
-	const groups = ps.group ? await UserGroupJoinings.find({
-		userId: user.id,
-	}).then(xs => xs.map(x => x.userGroupId)) : [];
-
-	if (ps.group && groups.length === 0) {
-		return [];
-	}
-
-	const history: MessagingMessage[] = [];
-
-	for (let i = 0; i < ps.limit!; i++) {
-		const found = ps.group
-			? history.map(m => m.groupId!)
-			: history.map(m => (m.userId === user.id) ? m.recipientId! : m.userId!);
-
-		const query = MessagingMessages.createQueryBuilder('message')
-			.orderBy('message.createdAt', 'DESC');
-
-		if (ps.group) {
-			query.where(`message.groupId IN (:...groups)`, { groups: groups });
-
-			if (found.length > 0) {
-				query.andWhere(`message.groupId NOT IN (:...found)`, { found: found });
-			}
-		} else {
-			query.where(new Brackets(qb => { qb
-				.where(`message.userId = :userId`, { userId: user.id })
-				.orWhere(`message.recipientId = :userId`, { userId: user.id });
-			}));
-			query.andWhere(`message.groupId IS NULL`);
-
-			if (found.length > 0) {
-				query.andWhere(`message.userId NOT IN (:...found)`, { found: found });
-				query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found });
-			}
-
-			if (mute.length > 0) {
-				query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
-				query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) });
-			}
-		}
-
-		const message = await query.getOne();
-
-		if (message) {
-			history.push(message);
-		} else {
-			break;
-		}
-	}
-
-	return await Promise.all(history.map(h => MessagingMessages.pack(h.id, user)));
-});
diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts
deleted file mode 100644
index 6baa24609e..0000000000
--- a/src/server/api/endpoints/messaging/messages.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { MessagingMessages, UserGroups, UserGroupJoinings, Users } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Brackets } from 'typeorm';
-import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message';
-
-export const meta = {
-	tags: ['messaging'],
-
-	requireCredential: true as const,
-
-	kind: 'read:messaging',
-
-	params: {
-		userId: {
-			validator: $.optional.type(ID),
-		},
-
-		groupId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		markAsRead: {
-			validator: $.optional.bool,
-			default: true
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'MessagingMessage',
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '11795c64-40ea-4198-b06e-3c873ed9039d'
-		},
-
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f'
-		},
-
-		groupAccessDenied: {
-			message: 'You can not read messages of groups that you have not joined.',
-			code: 'GROUP_ACCESS_DENIED',
-			id: 'a053a8dd-a491-4718-8f87-50775aad9284'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	if (ps.userId != null) {
-		// Fetch recipient (user)
-		const recipient = await getUser(ps.userId).catch(e => {
-			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-			throw e;
-		});
-
-		const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
-			.andWhere(new Brackets(qb => { qb
-				.where(new Brackets(qb => { qb
-					.where('message.userId = :meId')
-					.andWhere('message.recipientId = :recipientId');
-				}))
-				.orWhere(new Brackets(qb => { qb
-					.where('message.userId = :recipientId')
-					.andWhere('message.recipientId = :meId');
-				}));
-			}))
-			.setParameter('meId', user.id)
-			.setParameter('recipientId', recipient.id);
-
-		const messages = await query.take(ps.limit!).getMany();
-
-		// Mark all as read
-		if (ps.markAsRead) {
-			readUserMessagingMessage(user.id, recipient.id, messages.filter(m => m.recipientId === user.id).map(x => x.id));
-
-			// リモートユーザーとのメッセージだったら既読配信
-			if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) {
-				deliverReadActivity(user, recipient, messages);
-			}
-		}
-
-		return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
-			populateRecipient: false
-		})));
-	} else if (ps.groupId != null) {
-		// Fetch recipient (group)
-		const recipientGroup = await UserGroups.findOne(ps.groupId);
-
-		if (recipientGroup == null) {
-			throw new ApiError(meta.errors.noSuchGroup);
-		}
-
-		// check joined
-		const joining = await UserGroupJoinings.findOne({
-			userId: user.id,
-			userGroupId: recipientGroup.id
-		});
-
-		if (joining == null) {
-			throw new ApiError(meta.errors.groupAccessDenied);
-		}
-
-		const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId)
-			.andWhere(`message.groupId = :groupId`, { groupId: recipientGroup.id });
-
-		const messages = await query.take(ps.limit!).getMany();
-
-		// Mark all as read
-		if (ps.markAsRead) {
-			readGroupMessagingMessage(user.id, recipientGroup.id, messages.map(x => x.id));
-		}
-
-		return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
-			populateGroup: false
-		})));
-	} else {
-		throw new Error();
-	}
-});
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
deleted file mode 100644
index df0b455cbe..0000000000
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { MessagingMessages, DriveFiles, UserGroups, UserGroupJoinings, Blockings } from '@/models/index';
-import { User } from '@/models/entities/user';
-import { UserGroup } from '@/models/entities/user-group';
-import { createMessage } from '@/services/messages/create';
-
-export const meta = {
-	tags: ['messaging'],
-
-	requireCredential: true as const,
-
-	kind: 'write:messaging',
-
-	params: {
-		userId: {
-			validator: $.optional.type(ID),
-		},
-
-		groupId: {
-			validator: $.optional.type(ID),
-		},
-
-		text: {
-			validator: $.optional.str.pipe(MessagingMessages.validateText)
-		},
-
-		fileId: {
-			validator: $.optional.type(ID),
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'MessagingMessage',
-	},
-
-	errors: {
-		recipientIsYourself: {
-			message: 'You can not send a message to yourself.',
-			code: 'RECIPIENT_IS_YOURSELF',
-			id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e'
-		},
-
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '11795c64-40ea-4198-b06e-3c873ed9039d'
-		},
-
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537'
-		},
-
-		groupAccessDenied: {
-			message: 'You can not send messages to groups that you have not joined.',
-			code: 'GROUP_ACCESS_DENIED',
-			id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd'
-		},
-
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: '4372b8e2-185d-4146-8749-2f68864a3e5f'
-		},
-
-		contentRequired: {
-			message: 'Content required. You need to set text or fileId.',
-			code: 'CONTENT_REQUIRED',
-			id: '25587321-b0e6-449c-9239-f8925092942c'
-		},
-
-		youHaveBeenBlocked: {
-			message: 'You cannot send a message because you have been blocked by this user.',
-			code: 'YOU_HAVE_BEEN_BLOCKED',
-			id: 'c15a5199-7422-4968-941a-2a462c478f7d'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	let recipientUser: User | undefined;
-	let recipientGroup: UserGroup | undefined;
-
-	if (ps.userId != null) {
-		// Myself
-		if (ps.userId === user.id) {
-			throw new ApiError(meta.errors.recipientIsYourself);
-		}
-
-		// Fetch recipient (user)
-		recipientUser = await getUser(ps.userId).catch(e => {
-			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-			throw e;
-		});
-
-		// Check blocking
-		const block = await Blockings.findOne({
-			blockerId: recipientUser.id,
-			blockeeId: user.id,
-		});
-		if (block) {
-			throw new ApiError(meta.errors.youHaveBeenBlocked);
-		}
-	} else if (ps.groupId != null) {
-		// Fetch recipient (group)
-		recipientGroup = await UserGroups.findOne(ps.groupId);
-
-		if (recipientGroup == null) {
-			throw new ApiError(meta.errors.noSuchGroup);
-		}
-
-		// check joined
-		const joining = await UserGroupJoinings.findOne({
-			userId: user.id,
-			userGroupId: recipientGroup.id
-		});
-
-		if (joining == null) {
-			throw new ApiError(meta.errors.groupAccessDenied);
-		}
-	}
-
-	let file = null;
-	if (ps.fileId != null) {
-		file = await DriveFiles.findOne({
-			id: ps.fileId,
-			userId: user.id
-		});
-
-		if (file == null) {
-			throw new ApiError(meta.errors.noSuchFile);
-		}
-	}
-
-	// テキストが無いかつ添付ファイルも無かったらエラー
-	if (ps.text == null && file == null) {
-		throw new ApiError(meta.errors.contentRequired);
-	}
-
-	return await createMessage(user, recipientUser, recipientGroup, ps.text, file);
-});
diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts
deleted file mode 100644
index bd4890fc8a..0000000000
--- a/src/server/api/endpoints/messaging/messages/delete.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import * as ms from 'ms';
-import { ApiError } from '../../../error';
-import { MessagingMessages } from '@/models/index';
-import { deleteMessage } from '@/services/messages/delete';
-
-export const meta = {
-	tags: ['messaging'],
-
-	requireCredential: true as const,
-
-	kind: 'write:messaging',
-
-	limit: {
-		duration: ms('1hour'),
-		max: 300,
-		minInterval: ms('1sec')
-	},
-
-	params: {
-		messageId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchMessage: {
-			message: 'No such message.',
-			code: 'NO_SUCH_MESSAGE',
-			id: '54b5b326-7925-42cf-8019-130fda8b56af'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const message = await MessagingMessages.findOne({
-		id: ps.messageId,
-		userId: user.id
-	});
-
-	if (message == null) {
-		throw new ApiError(meta.errors.noSuchMessage);
-	}
-
-	await deleteMessage(message);
-});
diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts
deleted file mode 100644
index a1747310d3..0000000000
--- a/src/server/api/endpoints/messaging/messages/read.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { MessagingMessages } from '@/models/index';
-import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message';
-
-export const meta = {
-	tags: ['messaging'],
-
-	requireCredential: true as const,
-
-	kind: 'write:messaging',
-
-	params: {
-		messageId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchMessage: {
-			message: 'No such message.',
-			code: 'NO_SUCH_MESSAGE',
-			id: '86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const message = await MessagingMessages.findOne(ps.messageId);
-
-	if (message == null) {
-		throw new ApiError(meta.errors.noSuchMessage);
-	}
-
-	if (message.recipientId) {
-		await readUserMessagingMessage(user.id, message.userId, [message.id]).catch(e => {
-			if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage);
-			throw e;
-		});
-	} else if (message.groupId) {
-		await readGroupMessagingMessage(user.id, message.groupId, [message.id]).catch(e => {
-			if (e.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage);
-			throw e;
-		});
-	}
-});
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
deleted file mode 100644
index ce21556243..0000000000
--- a/src/server/api/endpoints/meta.ts
+++ /dev/null
@@ -1,598 +0,0 @@
-import $ from 'cafy';
-import config from '@/config/index';
-import define from '../define';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Ads, Emojis, Users } from '@/models/index';
-import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
-import { MoreThan } from 'typeorm';
-
-export const meta = {
-	tags: ['meta'],
-
-	requireCredential: false as const,
-
-	params: {
-		detail: {
-			validator: $.optional.bool,
-			default: true
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			maintainerName: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			maintainerEmail: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			version: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				example: config.version
-			},
-			name: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			},
-			uri: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'url',
-				example: 'https://misskey.example.com'
-			},
-			description: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const,
-			},
-			langs: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				}
-			},
-			tosUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			repositoryUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				default: 'https://github.com/misskey-dev/misskey'
-			},
-			feedbackUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				default: 'https://github.com/misskey-dev/misskey/issues/new'
-			},
-			secure: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-				default: false
-			},
-			disableRegistration: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-			},
-			disableLocalTimeline: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-			},
-			disableGlobalTimeline: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-			},
-			driveCapacityPerLocalUserMb: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const
-			},
-			driveCapacityPerRemoteUserMb: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const
-			},
-			cacheRemoteFiles: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			proxyRemoteFiles: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			emailRequiredForSignup: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			enableHcaptcha: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			hcaptchaSiteKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			enableRecaptcha: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			recaptchaSiteKey: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			swPublickey: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			mascotImageUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				default: '/assets/ai.png'
-			},
-			bannerUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const
-			},
-			errorImageUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				default: 'https://xn--931a.moe/aiart/yubitun.png'
-			},
-			iconUrl: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			maxNoteTextLength: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-				default: 500
-			},
-			emojis: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					properties: {
-						id: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
-							format: 'id'
-						},
-						aliases: {
-							type: 'array' as const,
-							optional: false as const, nullable: false as const,
-							items: {
-								type: 'string' as const,
-								optional: false as const, nullable: false as const
-							}
-						},
-						category: {
-							type: 'string' as const,
-							optional: false as const, nullable: true as const
-						},
-						host: {
-							type: 'string' as const,
-							optional: false as const, nullable: true as const
-						},
-						url: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
-							format: 'url'
-						}
-					}
-				}
-			},
-			ads: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					properties: {
-						place: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const
-						},
-						url: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
-							format: 'url'
-						},
-						imageUrl: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
-							format: 'url'
-						},
-					}
-				}
-			},
-			requireSetup: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-				example: false
-			},
-			enableEmail: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			enableTwitterIntegration: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			enableGithubIntegration: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			enableDiscordIntegration: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			enableServiceWorker: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			translatorAvailable: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			proxyAccountName: {
-				type: 'string' as const,
-				optional: false as const, nullable: true as const
-			},
-			features: {
-				type: 'object' as const,
-				optional: true as const, nullable: false as const,
-				properties: {
-					registration: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					localTimeLine: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					globalTimeLine: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					elasticsearch: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					hcaptcha: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					recaptcha: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					objectStorage: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					twitter: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					github: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					discord: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					serviceWorker: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					miauth: {
-						type: 'boolean' as const,
-						optional: true as const, nullable: false as const,
-						default: true
-					},
-				}
-			},
-			userStarForReactionFallback: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const,
-			},
-			pinnedUsers: {
-				type: 'array' as const,
-				optional: true as const, nullable: false as const,
-				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				}
-			},
-			hiddenTags: {
-				type: 'array' as const,
-				optional: true as const, nullable: false as const,
-				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				}
-			},
-			blockedHosts: {
-				type: 'array' as const,
-				optional: true as const, nullable: false as const,
-				items: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				}
-			},
-			hcaptchaSecretKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			recaptchaSecretKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			proxyAccountId: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const,
-				format: 'id'
-			},
-			twitterConsumerKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			twitterConsumerSecret: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			githubClientId: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			githubClientSecret: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			discordClientId: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			discordClientSecret: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			summaryProxy: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			email: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			smtpSecure: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const
-			},
-			smtpHost: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			smtpPort: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			smtpUser: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			smtpPass: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			swPrivateKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			useObjectStorage: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const
-			},
-			objectStorageBaseUrl: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			objectStorageBucket: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			objectStoragePrefix: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			objectStorageEndpoint: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			objectStorageRegion: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			objectStoragePort: {
-				type: 'number' as const,
-				optional: true as const, nullable: true as const
-			},
-			objectStorageAccessKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			objectStorageSecretKey: {
-				type: 'string' as const,
-				optional: true as const, nullable: true as const
-			},
-			objectStorageUseSSL: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const
-			},
-			objectStorageUseProxy: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const
-			},
-			objectStorageSetPublicRead: {
-				type: 'boolean' as const,
-				optional: true as const, nullable: false as const
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const instance = await fetchMeta(true);
-
-	const emojis = await Emojis.find({
-		where: {
-			host: null
-		},
-		order: {
-			category: 'ASC',
-			name: 'ASC'
-		},
-		cache: {
-			id: 'meta_emojis',
-			milliseconds: 3600000	// 1 hour
-		}
-	});
-
-	const ads = await Ads.find({
-		where: {
-			expiresAt: MoreThan(new Date())
-		},
-	});
-
-	const response: any = {
-		maintainerName: instance.maintainerName,
-		maintainerEmail: instance.maintainerEmail,
-
-		version: config.version,
-
-		name: instance.name,
-		uri: config.url,
-		description: instance.description,
-		langs: instance.langs,
-		tosUrl: instance.ToSUrl,
-		repositoryUrl: instance.repositoryUrl,
-		feedbackUrl: instance.feedbackUrl,
-
-		secure: config.https != null,
-
-		disableRegistration: instance.disableRegistration,
-		disableLocalTimeline: instance.disableLocalTimeline,
-		disableGlobalTimeline: instance.disableGlobalTimeline,
-		driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
-		driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
-		emailRequiredForSignup: instance.emailRequiredForSignup,
-		enableHcaptcha: instance.enableHcaptcha,
-		hcaptchaSiteKey: instance.hcaptchaSiteKey,
-		enableRecaptcha: instance.enableRecaptcha,
-		recaptchaSiteKey: instance.recaptchaSiteKey,
-		swPublickey: instance.swPublicKey,
-		mascotImageUrl: instance.mascotImageUrl,
-		bannerUrl: instance.bannerUrl,
-		errorImageUrl: instance.errorImageUrl,
-		iconUrl: instance.iconUrl,
-		backgroundImageUrl: instance.backgroundImageUrl,
-		logoImageUrl: instance.logoImageUrl,
-		maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
-		emojis: await Emojis.packMany(emojis),
-		ads: ads.map(ad => ({
-			id: ad.id,
-			url: ad.url,
-			place: ad.place,
-			ratio: ad.ratio,
-			imageUrl: ad.imageUrl,
-		})),
-		enableEmail: instance.enableEmail,
-
-		enableTwitterIntegration: instance.enableTwitterIntegration,
-		enableGithubIntegration: instance.enableGithubIntegration,
-		enableDiscordIntegration: instance.enableDiscordIntegration,
-
-		enableServiceWorker: instance.enableServiceWorker,
-
-		translatorAvailable: instance.deeplAuthKey != null,
-
-		...(ps.detail ? {
-			pinnedPages: instance.pinnedPages,
-			pinnedClipId: instance.pinnedClipId,
-			cacheRemoteFiles: instance.cacheRemoteFiles,
-			proxyRemoteFiles: instance.proxyRemoteFiles,
-			requireSetup: (await Users.count({
-				host: null,
-			})) === 0,
-		} : {})
-	};
-
-	if (ps.detail) {
-		const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
-
-		response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
-		response.features = {
-			registration: !instance.disableRegistration,
-			localTimeLine: !instance.disableLocalTimeline,
-			globalTimeLine: !instance.disableGlobalTimeline,
-			emailRequiredForSignup: instance.emailRequiredForSignup,
-			elasticsearch: config.elasticsearch ? true : false,
-			hcaptcha: instance.enableHcaptcha,
-			recaptcha: instance.enableRecaptcha,
-			objectStorage: instance.useObjectStorage,
-			twitter: instance.enableTwitterIntegration,
-			github: instance.enableGithubIntegration,
-			discord: instance.enableDiscordIntegration,
-			serviceWorker: instance.enableServiceWorker,
-			miauth: true,
-		};
-
-		if (me && me.isAdmin) {
-			response.useStarForReactionFallback = instance.useStarForReactionFallback;
-			response.pinnedUsers = instance.pinnedUsers;
-			response.hiddenTags = instance.hiddenTags;
-			response.blockedHosts = instance.blockedHosts;
-			response.hcaptchaSecretKey = instance.hcaptchaSecretKey;
-			response.recaptchaSecretKey = instance.recaptchaSecretKey;
-			response.proxyAccountId = instance.proxyAccountId;
-			response.twitterConsumerKey = instance.twitterConsumerKey;
-			response.twitterConsumerSecret = instance.twitterConsumerSecret;
-			response.githubClientId = instance.githubClientId;
-			response.githubClientSecret = instance.githubClientSecret;
-			response.discordClientId = instance.discordClientId;
-			response.discordClientSecret = instance.discordClientSecret;
-			response.summalyProxy = instance.summalyProxy;
-			response.email = instance.email;
-			response.smtpSecure = instance.smtpSecure;
-			response.smtpHost = instance.smtpHost;
-			response.smtpPort = instance.smtpPort;
-			response.smtpUser = instance.smtpUser;
-			response.smtpPass = instance.smtpPass;
-			response.swPrivateKey = instance.swPrivateKey;
-			response.useObjectStorage = instance.useObjectStorage;
-			response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
-			response.objectStorageBucket = instance.objectStorageBucket;
-			response.objectStoragePrefix = instance.objectStoragePrefix;
-			response.objectStorageEndpoint = instance.objectStorageEndpoint;
-			response.objectStorageRegion = instance.objectStorageRegion;
-			response.objectStoragePort = instance.objectStoragePort;
-			response.objectStorageAccessKey = instance.objectStorageAccessKey;
-			response.objectStorageSecretKey = instance.objectStorageSecretKey;
-			response.objectStorageUseSSL = instance.objectStorageUseSSL;
-			response.objectStorageUseProxy = instance.objectStorageUseProxy;
-			response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
-			response.objectStorageS3ForcePathStyle = instance.objectStorageS3ForcePathStyle;
-			response.deeplAuthKey = instance.deeplAuthKey;
-			response.deeplIsPro = instance.deeplIsPro;
-		}
-	}
-
-	return response;
-});
diff --git a/src/server/api/endpoints/miauth/gen-token.ts b/src/server/api/endpoints/miauth/gen-token.ts
deleted file mode 100644
index 321fa42fc9..0000000000
--- a/src/server/api/endpoints/miauth/gen-token.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { AccessTokens } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { secureRndstr } from '@/misc/secure-rndstr';
-
-export const meta = {
-	tags: ['auth'],
-
-	requireCredential: true as const,
-
-	secure: true,
-
-	params: {
-		session: {
-			validator: $.nullable.str
-		},
-
-		name: {
-			validator: $.nullable.optional.str
-		},
-
-		description: {
-			validator: $.nullable.optional.str,
-		},
-
-		iconUrl: {
-			validator: $.nullable.optional.str,
-		},
-
-		permission: {
-			validator: $.arr($.str).unique(),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			token: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Generate access token
-	const accessToken = secureRndstr(32, true);
-
-	const now = new Date();
-
-	// Insert access token doc
-	await AccessTokens.insert({
-		id: genId(),
-		createdAt: now,
-		lastUsedAt: now,
-		session: ps.session,
-		userId: user.id,
-		token: accessToken,
-		hash: accessToken,
-		name: ps.name,
-		description: ps.description,
-		iconUrl: ps.iconUrl,
-		permission: ps.permission,
-	});
-
-	return {
-		token: accessToken
-	};
-});
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
deleted file mode 100644
index 3fc64d3eba..0000000000
--- a/src/server/api/endpoints/mute/create.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { genId } from '@/misc/gen-id';
-import { Mutings, NoteWatchings } from '@/models/index';
-import { Muting } from '@/models/entities/muting';
-import { publishUserEvent } from '@/services/stream';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:mutes',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '6fef56f3-e765-4957-88e5-c6f65329b8a5'
-		},
-
-		muteeIsYourself: {
-			message: 'Mutee is yourself.',
-			code: 'MUTEE_IS_YOURSELF',
-			id: 'a4619cb2-5f23-484b-9301-94c903074e10'
-		},
-
-		alreadyMuting: {
-			message: 'You are already muting that user.',
-			code: 'ALREADY_MUTING',
-			id: '7e7359cb-160c-4956-b08f-4d1c653cd007'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const muter = user;
-
-	// 自分自身
-	if (user.id === ps.userId) {
-		throw new ApiError(meta.errors.muteeIsYourself);
-	}
-
-	// Get mutee
-	const mutee = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Check if already muting
-	const exist = await Mutings.findOne({
-		muterId: muter.id,
-		muteeId: mutee.id
-	});
-
-	if (exist != null) {
-		throw new ApiError(meta.errors.alreadyMuting);
-	}
-
-	// Create mute
-	await Mutings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		muterId: muter.id,
-		muteeId: mutee.id,
-	} as Muting);
-
-	publishUserEvent(user.id, 'mute', mutee);
-
-	NoteWatchings.delete({
-		userId: muter.id,
-		noteUserId: mutee.id
-	});
-});
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
deleted file mode 100644
index 3ffd1f4562..0000000000
--- a/src/server/api/endpoints/mute/delete.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { Mutings } from '@/models/index';
-import { publishUserEvent } from '@/services/stream';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:mutes',
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: 'b851d00b-8ab1-4a56-8b1b-e24187cb48ef'
-		},
-
-		muteeIsYourself: {
-			message: 'Mutee is yourself.',
-			code: 'MUTEE_IS_YOURSELF',
-			id: 'f428b029-6b39-4d48-a1d2-cc1ae6dd5cf9'
-		},
-
-		notMuting: {
-			message: 'You are not muting that user.',
-			code: 'NOT_MUTING',
-			id: '5467d020-daa9-4553-81e1-135c0c35a96d'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const muter = user;
-
-	// Check if the mutee is yourself
-	if (user.id === ps.userId) {
-		throw new ApiError(meta.errors.muteeIsYourself);
-	}
-
-	// Get mutee
-	const mutee = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Check not muting
-	const exist = await Mutings.findOne({
-		muterId: muter.id,
-		muteeId: mutee.id
-	});
-
-	if (exist == null) {
-		throw new ApiError(meta.errors.notMuting);
-	}
-
-	// Delete mute
-	await Mutings.delete({
-		id: exist.id
-	});
-
-	publishUserEvent(user.id, 'unmute', mutee);
-});
diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts
deleted file mode 100644
index ae4c3a719d..0000000000
--- a/src/server/api/endpoints/mute/list.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Mutings } from '@/models/index';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:mutes',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 30
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Muting',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(Mutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
-		.andWhere(`muting.muterId = :meId`, { meId: me.id });
-
-	const mutings = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Mutings.packMany(mutings, me);
-});
diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts
deleted file mode 100644
index d91562b62f..0000000000
--- a/src/server/api/endpoints/my/apps.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Apps } from '@/models/index';
-
-export const meta = {
-	tags: ['account', 'app'],
-
-	requireCredential: true as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				id: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				},
-				name: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				},
-				callbackUrl: {
-					type: 'string' as const,
-					optional: false as const, nullable: false as const
-				},
-				permission: {
-					type: 'array' as const,
-					optional: false as const, nullable: false as const,
-					items: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const
-					}
-				},
-				secret: {
-					type: 'string' as const,
-					optional: true as const, nullable: false as const
-				},
-				isAuthorized: {
-					type: 'object' as const,
-					optional: true as const, nullable: false as const,
-					properties: {
-						appId: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const
-						},
-						userId: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const
-						}
-					}
-				}
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = {
-		userId: user.id
-	};
-
-	const apps = await Apps.find({
-		where: query,
-		take: ps.limit!,
-		skip: ps.offset,
-	});
-
-	return await Promise.all(apps.map(app => Apps.pack(app, user, {
-		detail: true
-	})));
-});
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
deleted file mode 100644
index a3f6e187f2..0000000000
--- a/src/server/api/endpoints/notes.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../define';
-import { makePaginationQuery } from '../common/make-pagination-query';
-import { Notes } from '@/models/index';
-
-export const meta = {
-	tags: ['notes'],
-
-	params: {
-		local: {
-			validator: $.optional.bool,
-		},
-
-		reply: {
-			validator: $.optional.bool,
-		},
-
-		renote: {
-			validator: $.optional.bool,
-		},
-
-		withFiles: {
-			validator: $.optional.bool,
-		},
-
-		poll: {
-			validator: $.optional.bool,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-};
-
-export default define(meta, async (ps) => {
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.andWhere(`note.visibility = 'public'`)
-		.andWhere(`note.localOnly = FALSE`)
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	if (ps.local) {
-		query.andWhere('note.userHost IS NULL');
-	}
-
-	if (ps.reply != undefined) {
-		query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL');
-	}
-
-	if (ps.renote != undefined) {
-		query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL');
-	}
-
-	if (ps.withFiles != undefined) {
-		query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`);
-	}
-
-	if (ps.poll != undefined) {
-		query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
-	}
-
-	// TODO
-	//if (bot != undefined) {
-	//	query.isBot = bot;
-	//}
-
-	const notes = await query.take(ps.limit!).getMany();
-
-	return await Notes.packMany(notes);
-});
diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts
deleted file mode 100644
index 68881fda9e..0000000000
--- a/src/server/api/endpoints/notes/children.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { Brackets } from 'typeorm';
-import { Notes } from '@/models/index';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.andWhere(new Brackets(qb => { qb
-			.where(`note.replyId = :noteId`, { noteId: ps.noteId })
-			.orWhere(new Brackets(qb => { qb
-				.where(`note.renoteId = :noteId`, { noteId: ps.noteId })
-				.andWhere(new Brackets(qb => { qb
-					.where(`note.text IS NOT NULL`)
-					.orWhere(`note.fileIds != '{}'`)
-					.orWhere(`note.hasPoll = TRUE`);
-				}));
-			}));
-		}))
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	generateVisibilityQuery(query, user);
-	if (user) generateMutedUserQuery(query, user);
-	if (user) generateBlockedUserQuery(query, user);
-
-	const notes = await query.take(ps.limit!).getMany();
-
-	return await Notes.packMany(notes, user);
-});
diff --git a/src/server/api/endpoints/notes/clips.ts b/src/server/api/endpoints/notes/clips.ts
deleted file mode 100644
index 6b303d87ec..0000000000
--- a/src/server/api/endpoints/notes/clips.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ClipNotes, Clips } from '@/models/index';
-import { getNote } from '../../common/getters';
-import { ApiError } from '../../error';
-import { In } from 'typeorm';
-
-export const meta = {
-	tags: ['clips', 'notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const clipNotes = await ClipNotes.find({
-		noteId: note.id,
-	});
-
-	const clips = await Clips.find({
-		id: In(clipNotes.map(x => x.clipId)),
-		isPublic: true
-	});
-
-	return await Promise.all(clips.map(x => Clips.pack(x)));
-});
diff --git a/src/server/api/endpoints/notes/conversation.ts b/src/server/api/endpoints/notes/conversation.ts
deleted file mode 100644
index 0fe323ea00..0000000000
--- a/src/server/api/endpoints/notes/conversation.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getNote } from '../../common/getters';
-import { Note } from '@/models/entities/note';
-import { Notes } from '@/models/index';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'e1035875-9551-45ec-afa8-1ded1fcb53c8'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const conversation: Note[] = [];
-	let i = 0;
-
-	async function get(id: any) {
-		i++;
-		const p = await Notes.findOne(id);
-		if (p == null) return;
-
-		if (i > ps.offset!) {
-			conversation.push(p);
-		}
-
-		if (conversation.length == ps.limit!) {
-			return;
-		}
-
-		if (p.replyId) {
-			await get(p.replyId);
-		}
-	}
-
-	if (note.replyId) {
-		await get(note.replyId);
-	}
-
-	return await Notes.packMany(conversation, user);
-});
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
deleted file mode 100644
index 751673f955..0000000000
--- a/src/server/api/endpoints/notes/create.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-import $ from 'cafy';
-import * as ms from 'ms';
-import { length } from 'stringz';
-import create from '@/services/note/create';
-import define from '../../define';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { ApiError } from '../../error';
-import { ID } from '@/misc/cafy-id';
-import { User } from '@/models/entities/user';
-import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index';
-import { DriveFile } from '@/models/entities/drive-file';
-import { Note } from '@/models/entities/note';
-import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
-import { noteVisibilities } from '../../../../types';
-import { Channel } from '@/models/entities/channel';
-
-let maxNoteTextLength = 500;
-
-setInterval(() => {
-	fetchMeta().then(m => {
-		maxNoteTextLength = m.maxNoteTextLength;
-	});
-}, 3000);
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	limit: {
-		duration: ms('1hour'),
-		max: 300
-	},
-
-	kind: 'write:notes',
-
-	params: {
-		visibility: {
-			validator: $.optional.str.or(noteVisibilities as unknown as string[]),
-			default: 'public',
-		},
-
-		visibleUserIds: {
-			validator: $.optional.arr($.type(ID)).unique().min(0),
-		},
-
-		text: {
-			validator: $.optional.nullable.str.pipe(text =>
-				text.trim() != ''
-					&& length(text.trim()) <= maxNoteTextLength
-					&& Array.from(text.trim()).length <= DB_MAX_NOTE_TEXT_LENGTH	// DB limit
-			),
-			default: null,
-		},
-
-		cw: {
-			validator: $.optional.nullable.str.pipe(Notes.validateCw),
-		},
-
-		viaMobile: {
-			validator: $.optional.bool,
-			default: false,
-		},
-
-		localOnly: {
-			validator: $.optional.bool,
-			default: false,
-		},
-
-		noExtractMentions: {
-			validator: $.optional.bool,
-			default: false,
-		},
-
-		noExtractHashtags: {
-			validator: $.optional.bool,
-			default: false,
-		},
-
-		noExtractEmojis: {
-			validator: $.optional.bool,
-			default: false,
-		},
-
-		fileIds: {
-			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
-		},
-
-		mediaIds: {
-			validator: $.optional.arr($.type(ID)).unique().range(1, 4),
-			deprecated: true,
-		},
-
-		replyId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		renoteId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		channelId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		poll: {
-			validator: $.optional.nullable.obj({
-				choices: $.arr($.str)
-					.unique()
-					.range(2, 10)
-					.each(c => c.length > 0 && c.length < 50),
-				multiple: $.optional.bool,
-				expiresAt: $.optional.nullable.num.int(),
-				expiredAfter: $.optional.nullable.num.int().min(1)
-			}).strict(),
-			ref: 'poll'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			createdNote: {
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				ref: 'Note',
-			}
-		}
-	},
-
-	errors: {
-		noSuchRenoteTarget: {
-			message: 'No such renote target.',
-			code: 'NO_SUCH_RENOTE_TARGET',
-			id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4'
-		},
-
-		cannotReRenote: {
-			message: 'You can not Renote a pure Renote.',
-			code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE',
-			id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a'
-		},
-
-		noSuchReplyTarget: {
-			message: 'No such reply target.',
-			code: 'NO_SUCH_REPLY_TARGET',
-			id: '749ee0f6-d3da-459a-bf02-282e2da4292c'
-		},
-
-		cannotReplyToPureRenote: {
-			message: 'You can not reply to a pure Renote.',
-			code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
-			id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15'
-		},
-
-		contentRequired: {
-			message: 'Content required. You need to set text, fileIds, renoteId or poll.',
-			code: 'CONTENT_REQUIRED',
-			id: '6f57e42b-c348-439b-bc45-993995cc515a'
-		},
-
-		cannotCreateAlreadyExpiredPoll: {
-			message: 'Poll is already expired.',
-			code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
-			id: '04da457d-b083-4055-9082-955525eda5a5'
-		},
-
-		noSuchChannel: {
-			message: 'No such channel.',
-			code: 'NO_SUCH_CHANNEL',
-			id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb'
-		},
-
-		youHaveBeenBlocked: {
-			message: 'You have been blocked by this user.',
-			code: 'YOU_HAVE_BEEN_BLOCKED',
-			id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	let visibleUsers: User[] = [];
-	if (ps.visibleUserIds) {
-		visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id))))
-			.filter(x => x != null) as User[];
-	}
-
-	let files: DriveFile[] = [];
-	const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
-	if (fileIds != null) {
-		files = (await Promise.all(fileIds.map(fileId =>
-			DriveFiles.findOne({
-				id: fileId,
-				userId: user.id
-			})
-		))).filter(file => file != null) as DriveFile[];
-	}
-
-	let renote: Note | undefined;
-	if (ps.renoteId != null) {
-		// Fetch renote to note
-		renote = await Notes.findOne(ps.renoteId);
-
-		if (renote == null) {
-			throw new ApiError(meta.errors.noSuchRenoteTarget);
-		} else if (renote.renoteId && !renote.text && !renote.fileIds) {
-			throw new ApiError(meta.errors.cannotReRenote);
-		}
-
-		// Check blocking
-		if (renote.userId !== user.id) {
-			const block = await Blockings.findOne({
-				blockerId: renote.userId,
-				blockeeId: user.id,
-			});
-			if (block) {
-				throw new ApiError(meta.errors.youHaveBeenBlocked);
-			}
-		}
-	}
-
-	let reply: Note | undefined;
-	if (ps.replyId != null) {
-		// Fetch reply
-		reply = await Notes.findOne(ps.replyId);
-
-		if (reply == null) {
-			throw new ApiError(meta.errors.noSuchReplyTarget);
-		}
-
-		// 返信対象が引用でないRenoteだったらエラー
-		if (reply.renoteId && !reply.text && !reply.fileIds) {
-			throw new ApiError(meta.errors.cannotReplyToPureRenote);
-		}
-
-		// Check blocking
-		if (reply.userId !== user.id) {
-			const block = await Blockings.findOne({
-				blockerId: reply.userId,
-				blockeeId: user.id,
-			});
-			if (block) {
-				throw new ApiError(meta.errors.youHaveBeenBlocked);
-			}
-		}
-	}
-
-	if (ps.poll) {
-		if (typeof ps.poll.expiresAt === 'number') {
-			if (ps.poll.expiresAt < Date.now())
-				throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
-		} else if (typeof ps.poll.expiredAfter === 'number') {
-			ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
-		}
-	}
-
-	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
-	if (!(ps.text || files.length || renote || ps.poll)) {
-		throw new ApiError(meta.errors.contentRequired);
-	}
-
-	let channel: Channel | undefined;
-	if (ps.channelId != null) {
-		channel = await Channels.findOne(ps.channelId);
-
-		if (channel == null) {
-			throw new ApiError(meta.errors.noSuchChannel);
-		}
-	}
-
-	// 投稿を作成
-	const note = await create(user, {
-		createdAt: new Date(),
-		files: files,
-		poll: ps.poll ? {
-			choices: ps.poll.choices,
-			multiple: ps.poll.multiple || false,
-			expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null
-		} : undefined,
-		text: ps.text || undefined,
-		reply,
-		renote,
-		cw: ps.cw,
-		viaMobile: ps.viaMobile,
-		localOnly: ps.localOnly,
-		visibility: ps.visibility,
-		visibleUsers,
-		channel,
-		apMentions: ps.noExtractMentions ? [] : undefined,
-		apHashtags: ps.noExtractHashtags ? [] : undefined,
-		apEmojis: ps.noExtractEmojis ? [] : undefined,
-	});
-
-	return {
-		createdNote: await Notes.pack(note, user)
-	};
-});
diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts
deleted file mode 100644
index 7163a2b9d2..0000000000
--- a/src/server/api/endpoints/notes/delete.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import deleteNote from '@/services/note/delete';
-import define from '../../define';
-import * as ms from 'ms';
-import { getNote } from '../../common/getters';
-import { ApiError } from '../../error';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:notes',
-
-	limit: {
-		duration: ms('1hour'),
-		max: 300,
-		minInterval: ms('1sec')
-	},
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '490be23f-8c1f-4796-819f-94cb4f9d1630'
-		},
-
-		accessDenied: {
-			message: 'Access denied.',
-			code: 'ACCESS_DENIED',
-			id: 'fe8d7103-0ea8-4ec3-814d-f8b401dc69e9'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	// この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
-	await deleteNote(await Users.findOneOrFail(note.userId), note);
-});
diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
deleted file mode 100644
index 1bb25edd7f..0000000000
--- a/src/server/api/endpoints/notes/favorites/create.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getNote } from '../../../common/getters';
-import { NoteFavorites } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['notes', 'favorites'],
-
-	requireCredential: true as const,
-
-	kind: 'write:favorites',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '6dd26674-e060-4816-909a-45ba3f4da458'
-		},
-
-		alreadyFavorited: {
-			message: 'The note has already been marked as a favorite.',
-			code: 'ALREADY_FAVORITED',
-			id: 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Get favoritee
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	// if already favorited
-	const exist = await NoteFavorites.findOne({
-		noteId: note.id,
-		userId: user.id
-	});
-
-	if (exist != null) {
-		throw new ApiError(meta.errors.alreadyFavorited);
-	}
-
-	// Create favorite
-	await NoteFavorites.insert({
-		id: genId(),
-		createdAt: new Date(),
-		noteId: note.id,
-		userId: user.id
-	});
-});
diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts
deleted file mode 100644
index 75eb9a359a..0000000000
--- a/src/server/api/endpoints/notes/favorites/delete.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getNote } from '../../../common/getters';
-import { NoteFavorites } from '@/models/index';
-
-export const meta = {
-	tags: ['notes', 'favorites'],
-
-	requireCredential: true as const,
-
-	kind: 'write:favorites',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '80848a2c-398f-4343-baa9-df1d57696c56'
-		},
-
-		notFavorited: {
-			message: 'You have not marked that note a favorite.',
-			code: 'NOT_FAVORITED',
-			id: 'b625fc69-635e-45e9-86f4-dbefbef35af5'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Get favoritee
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	// if already favorited
-	const exist = await NoteFavorites.findOne({
-		noteId: note.id,
-		userId: user.id
-	});
-
-	if (exist == null) {
-		throw new ApiError(meta.errors.notFavorited);
-	}
-
-	// Delete favorite
-	await NoteFavorites.delete(exist.id);
-});
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
deleted file mode 100644
index 8d33c0e73d..0000000000
--- a/src/server/api/endpoints/notes/featured.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { Notes } from '@/models/index';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const max = 30;
-	const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
-
-	const query = Notes.createQueryBuilder('note')
-		.addSelect('note.score')
-		.where('note.userHost IS NULL')
-		.andWhere(`note.score > 0`)
-		.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
-		.andWhere(`note.visibility = 'public'`)
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	if (user) generateMutedUserQuery(query, user);
-	if (user) generateBlockedUserQuery(query, user);
-
-	let notes = await query
-		.orderBy('note.score', 'DESC')
-		.take(max)
-		.getMany();
-
-	notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
-
-	notes = notes.slice(ps.offset, ps.offset + ps.limit);
-
-	return await Notes.packMany(notes, user);
-});
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
deleted file mode 100644
index 5902c0415c..0000000000
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { ApiError } from '../../error';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Notes } from '@/models/index';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { activeUsersChart } from '@/services/chart/index';
-import { generateRepliesQuery } from '../../common/generate-replies-query';
-import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	params: {
-		withFiles: {
-			validator: $.optional.bool,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		sinceDate: {
-			validator: $.optional.num
-		},
-
-		untilDate: {
-			validator: $.optional.num
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		gtlDisabled: {
-			message: 'Global timeline has been disabled.',
-			code: 'GTL_DISABLED',
-			id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const m = await fetchMeta();
-	if (m.disableGlobalTimeline) {
-		if (user == null || (!user.isAdmin && !user.isModerator)) {
-			throw new ApiError(meta.errors.gtlDisabled);
-		}
-	}
-
-	//#region Construct query
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
-			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-		.andWhere('note.visibility = \'public\'')
-		.andWhere('note.channelId IS NULL')
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	generateRepliesQuery(query, user);
-	if (user) generateMutedUserQuery(query, user);
-	if (user) generateMutedNoteQuery(query, user);
-	if (user) generateBlockedUserQuery(query, user);
-
-	if (ps.withFiles) {
-		query.andWhere('note.fileIds != \'{}\'');
-	}
-	//#endregion
-
-	const timeline = await query.take(ps.limit!).getMany();
-
-	process.nextTick(() => {
-		if (user) {
-			activeUsersChart.update(user);
-		}
-	});
-
-	return await Notes.packMany(timeline, user);
-});
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
deleted file mode 100644
index 47f08f208b..0000000000
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { ApiError } from '../../error';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Followings, Notes } from '@/models/index';
-import { Brackets } from 'typeorm';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { activeUsersChart } from '@/services/chart/index';
-import { generateRepliesQuery } from '../../common/generate-replies-query';
-import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
-import { generateChannelQuery } from '../../common/generate-channel-query';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		sinceDate: {
-			validator: $.optional.num,
-		},
-
-		untilDate: {
-			validator: $.optional.num,
-		},
-
-		includeMyRenotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		includeRenotedMyNotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		includeLocalRenotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		withFiles: {
-			validator: $.optional.bool,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		stlDisabled: {
-			message: 'Hybrid timeline has been disabled.',
-			code: 'STL_DISABLED',
-			id: '620763f4-f621-4533-ab33-0577a1a3c342'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const m = await fetchMeta();
-	if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
-		throw new ApiError(meta.errors.stlDisabled);
-	}
-
-	//#region Construct query
-	const followingQuery = Followings.createQueryBuilder('following')
-		.select('following.followeeId')
-		.where('following.followerId = :followerId', { followerId: user.id });
-
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
-			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-		.andWhere(new Brackets(qb => {
-			qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id })
-				.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
-		}))
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser')
-		.setParameters(followingQuery.getParameters());
-
-	generateChannelQuery(query, user);
-	generateRepliesQuery(query, user);
-	generateVisibilityQuery(query, user);
-	generateMutedUserQuery(query, user);
-	generateMutedNoteQuery(query, user);
-	generateBlockedUserQuery(query, user);
-
-	if (ps.includeMyRenotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.userId != :meId', { meId: user.id });
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.includeRenotedMyNotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.includeLocalRenotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.renoteUserHost IS NOT NULL');
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.withFiles) {
-		query.andWhere('note.fileIds != \'{}\'');
-	}
-	//#endregion
-
-	const timeline = await query.take(ps.limit!).getMany();
-
-	process.nextTick(() => {
-		if (user) {
-			activeUsersChart.update(user);
-		}
-	});
-
-	return await Notes.packMany(timeline, user);
-});
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
deleted file mode 100644
index f670d478bf..0000000000
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { ApiError } from '../../error';
-import { Notes } from '@/models/index';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { activeUsersChart } from '@/services/chart/index';
-import { Brackets } from 'typeorm';
-import { generateRepliesQuery } from '../../common/generate-replies-query';
-import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
-import { generateChannelQuery } from '../../common/generate-channel-query';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	params: {
-		withFiles: {
-			validator: $.optional.bool,
-		},
-
-		fileType: {
-			validator: $.optional.arr($.str),
-		},
-
-		excludeNsfw: {
-			validator: $.optional.bool,
-			default: false,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		sinceDate: {
-			validator: $.optional.num,
-		},
-
-		untilDate: {
-			validator: $.optional.num,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		ltlDisabled: {
-			message: 'Local timeline has been disabled.',
-			code: 'LTL_DISABLED',
-			id: '45a6eb02-7695-4393-b023-dd3be9aaaefd'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const m = await fetchMeta();
-	if (m.disableLocalTimeline) {
-		if (user == null || (!user.isAdmin && !user.isModerator)) {
-			throw new ApiError(meta.errors.ltlDisabled);
-		}
-	}
-
-	//#region Construct query
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
-			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-		.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	generateChannelQuery(query, user);
-	generateRepliesQuery(query, user);
-	generateVisibilityQuery(query, user);
-	if (user) generateMutedUserQuery(query, user);
-	if (user) generateMutedNoteQuery(query, user);
-	if (user) generateBlockedUserQuery(query, user);
-
-	if (ps.withFiles) {
-		query.andWhere('note.fileIds != \'{}\'');
-	}
-
-	if (ps.fileType != null) {
-		query.andWhere('note.fileIds != \'{}\'');
-		query.andWhere(new Brackets(qb => {
-			for (const type of ps.fileType!) {
-				const i = ps.fileType!.indexOf(type);
-				qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
-			}
-		}));
-
-		if (ps.excludeNsfw) {
-			query.andWhere('note.cw IS NULL');
-			query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
-		}
-	}
-	//#endregion
-
-	const timeline = await query.take(ps.limit!).getMany();
-
-	process.nextTick(() => {
-		if (user) {
-			activeUsersChart.update(user);
-		}
-	});
-
-	return await Notes.packMany(timeline, user);
-});
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
deleted file mode 100644
index ffaebd6c95..0000000000
--- a/src/server/api/endpoints/notes/mentions.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import read from '@/services/note/read';
-import { Notes, Followings } from '@/models/index';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Brackets } from 'typeorm';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	params: {
-		following: {
-			validator: $.optional.bool,
-			default: false
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		visibility: {
-			validator: $.optional.str,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const followingQuery = Followings.createQueryBuilder('following')
-		.select('following.followeeId')
-		.where('following.followerId = :followerId', { followerId: user.id });
-
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.andWhere(new Brackets(qb => { qb
-			.where(`'{"${user.id}"}' <@ note.mentions`)
-			.orWhere(`'{"${user.id}"}' <@ note.visibleUserIds`);
-		}))
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	generateVisibilityQuery(query, user);
-	generateMutedUserQuery(query, user);
-	generateMutedNoteThreadQuery(query, user);
-	generateBlockedUserQuery(query, user);
-
-	if (ps.visibility) {
-		query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
-	}
-
-	if (ps.following) {
-		query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id });
-		query.setParameters(followingQuery.getParameters());
-	}
-
-	const mentions = await query.take(ps.limit!).getMany();
-
-	read(user.id, mentions);
-
-	return await Notes.packMany(mentions, user);
-});
diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts
deleted file mode 100644
index 0763f0c8fd..0000000000
--- a/src/server/api/endpoints/notes/polls/recommendation.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { Polls, Mutings, Notes, PollVotes } from '@/models/index';
-import { Brackets, In } from 'typeorm';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = Polls.createQueryBuilder('poll')
-		.where('poll.userHost IS NULL')
-		.andWhere(`poll.userId != :meId`, { meId: user.id })
-		.andWhere(`poll.noteVisibility = 'public'`)
-		.andWhere(new Brackets(qb => { qb
-			.where('poll.expiresAt IS NULL')
-			.orWhere('poll.expiresAt > :now', { now: new Date() });
-		}));
-
-	//#region exclude arleady voted polls
-	const votedQuery = PollVotes.createQueryBuilder('vote')
-		.select('vote.noteId')
-		.where('vote.userId = :meId', { meId: user.id });
-
-	query
-		.andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`);
-
-	query.setParameters(votedQuery.getParameters());
-	//#endregion
-
-	//#region mute
-	const mutingQuery = Mutings.createQueryBuilder('muting')
-		.select('muting.muteeId')
-		.where('muting.muterId = :muterId', { muterId: user.id });
-
-	query
-		.andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`);
-
-	query.setParameters(mutingQuery.getParameters());
-	//#endregion
-
-	const polls = await query.take(ps.limit!).skip(ps.offset).getMany();
-
-	if (polls.length === 0) return [];
-
-	const notes = await Notes.find({
-		id: In(polls.map(poll => poll.noteId))
-	});
-
-	return await Notes.packMany(notes, user, {
-		detail: true
-	});
-});
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
deleted file mode 100644
index f670501385..0000000000
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishNoteStream } from '@/services/stream';
-import { createNotification } from '@/services/create-notification';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getNote } from '../../../common/getters';
-import { deliver } from '@/queue/index';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderVote from '@/remote/activitypub/renderer/vote';
-import { deliverQuestionUpdate } from '@/services/note/polls/update';
-import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index';
-import { Not } from 'typeorm';
-import { IRemoteUser } from '@/models/entities/user';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:votes',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-
-		choice: {
-			validator: $.num
-		},
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'ecafbd2e-c283-4d6d-aecb-1a0a33b75396'
-		},
-
-		noPoll: {
-			message: 'The note does not attach a poll.',
-			code: 'NO_POLL',
-			id: '5f979967-52d9-4314-a911-1c673727f92f'
-		},
-
-		invalidChoice: {
-			message: 'Choice ID is invalid.',
-			code: 'INVALID_CHOICE',
-			id: 'e0cc9a04-f2e8-41e4-a5f1-4127293260cc'
-		},
-
-		alreadyVoted: {
-			message: 'You have already voted.',
-			code: 'ALREADY_VOTED',
-			id: '0963fc77-efac-419b-9424-b391608dc6d8'
-		},
-
-		alreadyExpired: {
-			message: 'The poll is already expired.',
-			code: 'ALREADY_EXPIRED',
-			id: '1022a357-b085-4054-9083-8f8de358337e'
-		},
-
-		youHaveBeenBlocked: {
-			message: 'You cannot vote this poll because you have been blocked by this user.',
-			code: 'YOU_HAVE_BEEN_BLOCKED',
-			id: '85a5377e-b1e9-4617-b0b9-5bea73331e49'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const createdAt = new Date();
-
-	// Get votee
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	if (!note.hasPoll) {
-		throw new ApiError(meta.errors.noPoll);
-	}
-
-	// Check blocking
-	if (note.userId !== user.id) {
-		const block = await Blockings.findOne({
-			blockerId: note.userId,
-			blockeeId: user.id,
-		});
-		if (block) {
-			throw new ApiError(meta.errors.youHaveBeenBlocked);
-		}
-	}
-
-	const poll = await Polls.findOneOrFail({ noteId: note.id });
-
-	if (poll.expiresAt && poll.expiresAt < createdAt) {
-		throw new ApiError(meta.errors.alreadyExpired);
-	}
-
-	if (poll.choices[ps.choice] == null) {
-		throw new ApiError(meta.errors.invalidChoice);
-	}
-
-	// if already voted
-	const exist = await PollVotes.find({
-		noteId: note.id,
-		userId: user.id
-	});
-
-	if (exist.length) {
-		if (poll.multiple) {
-			if (exist.some(x => x.choice == ps.choice))
-				throw new ApiError(meta.errors.alreadyVoted);
-		} else {
-			throw new ApiError(meta.errors.alreadyVoted);
-		}
-	}
-
-	// Create vote
-	const vote = await PollVotes.insert({
-		id: genId(),
-		createdAt,
-		noteId: note.id,
-		userId: user.id,
-		choice: ps.choice
-	}).then(x => PollVotes.findOneOrFail(x.identifiers[0]));
-
-	// Increment votes count
-	const index = ps.choice + 1; // In SQL, array index is 1 based
-	await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
-
-	publishNoteStream(note.id, 'pollVoted', {
-		choice: ps.choice,
-		userId: user.id
-	});
-
-	// Notify
-	createNotification(note.userId, 'pollVote', {
-		notifierId: user.id,
-		noteId: note.id,
-		choice: ps.choice
-	});
-
-	// Fetch watchers
-	NoteWatchings.find({
-		noteId: note.id,
-		userId: Not(user.id),
-	}).then(watchers => {
-		for (const watcher of watchers) {
-			createNotification(watcher.userId, 'pollVote', {
-				notifierId: user.id,
-				noteId: note.id,
-				choice: ps.choice
-			});
-		}
-	});
-
-	// リモート投票の場合リプライ送信
-	if (note.userHost != null) {
-		const pollOwner = await Users.findOneOrFail(note.userId) as IRemoteUser;
-
-		deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
-	}
-
-	// リモートフォロワーにUpdate配信
-	deliverQuestionUpdate(note.id);
-});
diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts
deleted file mode 100644
index 09dd6b600b..0000000000
--- a/src/server/api/endpoints/notes/reactions.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { getNote } from '../../common/getters';
-import { ApiError } from '../../error';
-import { NoteReactions } from '@/models/index';
-import { DeepPartial } from 'typeorm';
-import { NoteReaction } from '@/models/entities/note-reaction';
-
-export const meta = {
-	tags: ['notes', 'reactions'],
-
-	requireCredential: false as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-
-		type: {
-			validator: $.optional.nullable.str,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		offset: {
-			validator: $.optional.num,
-			default: 0
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'NoteReaction',
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '263fff3d-d0e1-4af4-bea7-8408059b451a'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const query = {
-		noteId: note.id
-	} as DeepPartial<NoteReaction>;
-
-	if (ps.type) {
-		// ローカルリアクションはホスト名が . とされているが
-		// DB 上ではそうではないので、必要に応じて変換
-		const suffix = '@.:';
-		const type = ps.type.endsWith(suffix) ? ps.type.slice(0, ps.type.length - suffix.length) + ':' : ps.type;
-		query.reaction = type;
-	}
-
-	const reactions = await NoteReactions.find({
-		where: query,
-		take: ps.limit!,
-		skip: ps.offset,
-		order: {
-			id: -1
-		}
-	});
-
-	return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user)));
-});
diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
deleted file mode 100644
index 24a73a8d4f..0000000000
--- a/src/server/api/endpoints/notes/reactions/create.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import createReaction from '@/services/note/reaction/create';
-import define from '../../../define';
-import { getNote } from '../../../common/getters';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['reactions', 'notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:reactions',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-
-		reaction: {
-			validator: $.str,
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '033d0620-5bfe-4027-965d-980b0c85a3ea'
-		},
-
-		alreadyReacted: {
-			message: 'You are already reacting to that note.',
-			code: 'ALREADY_REACTED',
-			id: '71efcf98-86d6-4e2b-b2ad-9d032369366b'
-		},
-
-		youHaveBeenBlocked: {
-			message: 'You cannot react this note because you have been blocked by this user.',
-			code: 'YOU_HAVE_BEEN_BLOCKED',
-			id: '20ef5475-9f38-4e4c-bd33-de6d979498ec'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-	await createReaction(user, note, ps.reaction).catch(e => {
-		if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
-		if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
-		throw e;
-	});
-	return;
-});
diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts
deleted file mode 100644
index 69550f96de..0000000000
--- a/src/server/api/endpoints/notes/reactions/delete.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import * as ms from 'ms';
-import deleteReaction from '@/services/note/reaction/delete';
-import { getNote } from '../../../common/getters';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['reactions', 'notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:reactions',
-
-	limit: {
-		duration: ms('1hour'),
-		max: 60,
-		minInterval: ms('3sec')
-	},
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '764d9fce-f9f2-4a0e-92b1-6ceac9a7ad37'
-		},
-
-		notReacted: {
-			message: 'You are not reacting to that note.',
-			code: 'NOT_REACTED',
-			id: '92f4426d-4196-4125-aa5b-02943e2ec8fc'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-	await deleteReaction(user, note).catch(e => {
-		if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted);
-		throw e;
-	});
-});
diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts
deleted file mode 100644
index 26bfc1657d..0000000000
--- a/src/server/api/endpoints/notes/renotes.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { getNote } from '../../common/getters';
-import { ApiError } from '../../error';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Notes } from '@/models/index';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '12908022-2e21-46cd-ba6a-3edaf6093f46'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	generateVisibilityQuery(query, user);
-	if (user) generateMutedUserQuery(query, user);
-	if (user) generateBlockedUserQuery(query, user);
-
-	const renotes = await query.take(ps.limit!).getMany();
-
-	return await Notes.packMany(renotes, user);
-});
diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts
deleted file mode 100644
index 0bb62413ae..0000000000
--- a/src/server/api/endpoints/notes/replies.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Notes } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	generateVisibilityQuery(query, user);
-	if (user) generateMutedUserQuery(query, user);
-	if (user) generateBlockedUserQuery(query, user);
-
-	const timeline = await query.take(ps.limit!).getMany();
-
-	return await Notes.packMany(timeline, user);
-});
diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts
deleted file mode 100644
index 40e1499736..0000000000
--- a/src/server/api/endpoints/notes/search-by-tag.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Notes } from '@/models/index';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { Brackets } from 'typeorm';
-import { safeForSql } from '@/misc/safe-for-sql';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes', 'hashtags'],
-
-	params: {
-		tag: {
-			validator: $.optional.str,
-		},
-
-		query: {
-			validator: $.optional.arr($.arr($.str)),
-		},
-
-		reply: {
-			validator: $.optional.nullable.bool,
-			default: null,
-		},
-
-		renote: {
-			validator: $.optional.nullable.bool,
-			default: null,
-		},
-
-		withFiles: {
-			validator: $.optional.bool,
-		},
-
-		poll: {
-			validator: $.optional.nullable.bool,
-			default: null,
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	generateVisibilityQuery(query, me);
-	if (me) generateMutedUserQuery(query, me);
-	if (me) generateBlockedUserQuery(query, me);
-
-	try {
-		if (ps.tag) {
-			if (!safeForSql(ps.tag)) throw 'Injection';
-			query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
-		} else {
-			query.andWhere(new Brackets(qb => {
-				for (const tags of ps.query!) {
-					qb.orWhere(new Brackets(qb => {
-						for (const tag of tags) {
-							if (!safeForSql(tag)) throw 'Injection';
-							qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
-						}
-					}));
-				}
-			}));
-		}
-	} catch (e) {
-		if (e === 'Injection') return [];
-		throw e;
-	}
-
-	if (ps.reply != null) {
-		if (ps.reply) {
-			query.andWhere('note.replyId IS NOT NULL');
-		} else {
-			query.andWhere('note.replyId IS NULL');
-		}
-	}
-
-	if (ps.renote != null) {
-		if (ps.renote) {
-			query.andWhere('note.renoteId IS NOT NULL');
-		} else {
-			query.andWhere('note.renoteId IS NULL');
-		}
-	}
-
-	if (ps.withFiles) {
-		query.andWhere('note.fileIds != \'{}\'');
-	}
-
-	if (ps.poll != null) {
-		if (ps.poll) {
-			query.andWhere('note.hasPoll = TRUE');
-		} else {
-			query.andWhere('note.hasPoll = FALSE');
-		}
-	}
-
-	// Search notes
-	const notes = await query.take(ps.limit!).getMany();
-
-	return await Notes.packMany(notes, me);
-});
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
deleted file mode 100644
index eb832a6b31..0000000000
--- a/src/server/api/endpoints/notes/search.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import $ from 'cafy';
-import es from '../../../../db/elasticsearch';
-import define from '../../define';
-import { Notes } from '@/models/index';
-import { In } from 'typeorm';
-import { ID } from '@/misc/cafy-id';
-import config from '@/config/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		query: {
-			validator: $.str
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		host: {
-			validator: $.optional.nullable.str,
-			default: undefined
-		},
-
-		userId: {
-			validator: $.optional.nullable.type(ID),
-			default: null
-		},
-
-		channelId: {
-			validator: $.optional.nullable.type(ID),
-			default: null
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	if (es == null) {
-		const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId);
-
-		if (ps.userId) {
-			query.andWhere('note.userId = :userId', { userId: ps.userId });
-		} else if (ps.channelId) {
-			query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
-		}
-
-		query
-			.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
-			.innerJoinAndSelect('note.user', 'user')
-			.leftJoinAndSelect('note.reply', 'reply')
-			.leftJoinAndSelect('note.renote', 'renote')
-			.leftJoinAndSelect('reply.user', 'replyUser')
-			.leftJoinAndSelect('renote.user', 'renoteUser');
-
-		generateVisibilityQuery(query, me);
-		if (me) generateMutedUserQuery(query, me);
-		if (me) generateBlockedUserQuery(query, me);
-
-		const notes = await query.take(ps.limit!).getMany();
-
-		return await Notes.packMany(notes, me);
-	} else {
-		const userQuery = ps.userId != null ? [{
-			term: {
-				userId: ps.userId
-			}
-		}] : [];
-
-		const hostQuery = ps.userId == null ?
-			ps.host === null ? [{
-				bool: {
-					must_not: {
-						exists: {
-							field: 'userHost'
-						}
-					}
-				}
-			}] : ps.host !== undefined ? [{
-				term: {
-					userHost: ps.host
-				}
-			}] : []
-		: [];
-
-		const result = await es.search({
-			index: config.elasticsearch.index || 'misskey_note',
-			body: {
-				size: ps.limit!,
-				from: ps.offset,
-				query: {
-					bool: {
-						must: [{
-							simple_query_string: {
-								fields: ['text'],
-								query: ps.query.toLowerCase(),
-								default_operator: 'and'
-							},
-						}, ...hostQuery, ...userQuery]
-					}
-				},
-				sort: [{
-					_doc: 'desc'
-				}]
-			}
-		});
-
-		const hits = result.body.hits.hits.map((hit: any) => hit._id);
-
-		if (hits.length === 0) return [];
-
-		// Fetch found notes
-		const notes = await Notes.find({
-			where: {
-				id: In(hits)
-			},
-			order: {
-				id: -1
-			}
-		});
-
-		return await Notes.packMany(notes, me);
-	}
-});
diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts
deleted file mode 100644
index fad63d6483..0000000000
--- a/src/server/api/endpoints/notes/show.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { getNote } from '../../common/getters';
-import { ApiError } from '../../error';
-import { Notes } from '@/models/index';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Note',
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	return await Notes.pack(note, user, {
-		detail: true
-	});
-});
diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts
deleted file mode 100644
index b3913a5e79..0000000000
--- a/src/server/api/endpoints/notes/state.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			isFavorited: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			isWatching: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-			isMutedThread: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const
-			},
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await Notes.findOneOrFail(ps.noteId);
-
-	const [favorite, watching, threadMuting] = await Promise.all([
-		NoteFavorites.count({
-			where: {
-				userId: user.id,
-				noteId: note.id,
-			},
-			take: 1
-		}),
-		NoteWatchings.count({
-			where: {
-				userId: user.id,
-				noteId: note.id,
-			},
-			take: 1
-		}),
-		NoteThreadMutings.count({
-			where: {
-				userId: user.id,
-				threadId: note.threadId || note.id,
-			},
-			take: 1
-		}),
-	]);
-
-	return {
-		isFavorited: favorite !== 0,
-		isWatching: watching !== 0,
-		isMutedThread: threadMuting !== 0,
-	};
-});
diff --git a/src/server/api/endpoints/notes/thread-muting/create.ts b/src/server/api/endpoints/notes/thread-muting/create.ts
deleted file mode 100644
index 2010d54331..0000000000
--- a/src/server/api/endpoints/notes/thread-muting/create.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { getNote } from '../../../common/getters';
-import { ApiError } from '../../../error';
-import { Notes, NoteThreadMutings } from '@/models';
-import { genId } from '@/misc/gen-id';
-import readNote from '@/services/note/read';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const mutedNotes = await Notes.find({
-		where: [{
-			id: note.threadId || note.id,
-		}, {
-			threadId: note.threadId || note.id,
-		}],
-	});
-
-	await readNote(user.id, mutedNotes);
-
-	await NoteThreadMutings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		threadId: note.threadId || note.id,
-		userId: user.id,
-	});
-});
diff --git a/src/server/api/endpoints/notes/thread-muting/delete.ts b/src/server/api/endpoints/notes/thread-muting/delete.ts
deleted file mode 100644
index 05d5691870..0000000000
--- a/src/server/api/endpoints/notes/thread-muting/delete.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { getNote } from '../../../common/getters';
-import { ApiError } from '../../../error';
-import { NoteThreadMutings } from '@/models';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	await NoteThreadMutings.delete({
-		threadId: note.threadId || note.id,
-		userId: user.id,
-	});
-});
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
deleted file mode 100644
index 1bd0e57d34..0000000000
--- a/src/server/api/endpoints/notes/timeline.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { Notes, Followings } from '@/models/index';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { activeUsersChart } from '@/services/chart/index';
-import { Brackets } from 'typeorm';
-import { generateRepliesQuery } from '../../common/generate-replies-query';
-import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
-import { generateChannelQuery } from '../../common/generate-channel-query';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		sinceDate: {
-			validator: $.optional.num,
-		},
-
-		untilDate: {
-			validator: $.optional.num,
-		},
-
-		includeMyRenotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		includeRenotedMyNotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		includeLocalRenotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		withFiles: {
-			validator: $.optional.bool,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const hasFollowing = (await Followings.count({
-		where: {
-			followerId: user.id,
-		},
-		take: 1
-	})) !== 0;
-
-	//#region Construct query
-	const followingQuery = Followings.createQueryBuilder('following')
-		.select('following.followeeId')
-		.where('following.followerId = :followerId', { followerId: user.id });
-
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
-			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-		.andWhere(new Brackets(qb => { qb
-			.where('note.userId = :meId', { meId: user.id });
-			if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
-		}))
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser')
-		.setParameters(followingQuery.getParameters());
-
-	generateChannelQuery(query, user);
-	generateRepliesQuery(query, user);
-	generateVisibilityQuery(query, user);
-	generateMutedUserQuery(query, user);
-	generateMutedNoteQuery(query, user);
-	generateBlockedUserQuery(query, user);
-
-	if (ps.includeMyRenotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.userId != :meId', { meId: user.id });
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.includeRenotedMyNotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.includeLocalRenotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.renoteUserHost IS NOT NULL');
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.withFiles) {
-		query.andWhere('note.fileIds != \'{}\'');
-	}
-	//#endregion
-
-	const timeline = await query.take(ps.limit!).getMany();
-
-	process.nextTick(() => {
-		if (user) {
-			activeUsersChart.update(user);
-		}
-	});
-
-	return await Notes.packMany(timeline, user);
-});
diff --git a/src/server/api/endpoints/notes/translate.ts b/src/server/api/endpoints/notes/translate.ts
deleted file mode 100644
index b56b1debdd..0000000000
--- a/src/server/api/endpoints/notes/translate.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { getNote } from '../../common/getters';
-import { ApiError } from '../../error';
-import fetch from 'node-fetch';
-import config from '@/config/index';
-import { getAgentByUrl } from '@/misc/fetch';
-import { URLSearchParams } from 'url';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Notes } from '@/models';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: false as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		},
-		targetLang: {
-			validator: $.str,
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'bea9b03f-36e0-49c5-a4db-627a029f8971'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) {
-		return 204; // TODO: 良い感じのエラー返す
-	}
-
-	if (note.text == null) {
-		return 204;
-	}
-
-	const instance = await fetchMeta();
-
-	if (instance.deeplAuthKey == null) {
-		return 204; // TODO: 良い感じのエラー返す
-	}
-
-	let targetLang = ps.targetLang;
-	if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
-
-	const params = new URLSearchParams();
-	params.append('auth_key', instance.deeplAuthKey);
-	params.append('text', note.text);
-	params.append('target_lang', targetLang);
-
-	const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
-
-	const res = await fetch(endpoint, {
-		method: 'POST',
-		headers: {
-			'Content-Type': 'application/x-www-form-urlencoded',
-			'User-Agent': config.userAgent,
-			Accept: 'application/json, */*'
-		},
-		body: params,
-		timeout: 10000,
-		agent: getAgentByUrl,
-	});
-
-	const json = await res.json();
-
-	return {
-		sourceLang: json.translations[0].detected_source_language,
-		text: json.translations[0].text
-	};
-});
diff --git a/src/server/api/endpoints/notes/unrenote.ts b/src/server/api/endpoints/notes/unrenote.ts
deleted file mode 100644
index dce43d9d9c..0000000000
--- a/src/server/api/endpoints/notes/unrenote.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import deleteNote from '@/services/note/delete';
-import define from '../../define';
-import * as ms from 'ms';
-import { getNote } from '../../common/getters';
-import { ApiError } from '../../error';
-import { Notes, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:notes',
-
-	limit: {
-		duration: ms('1hour'),
-		max: 300,
-		minInterval: ms('1sec')
-	},
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'efd4a259-2442-496b-8dd7-b255aa1a160f'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const renotes = await Notes.find({
-		userId: user.id,
-		renoteId: note.id
-	});
-
-	for (const note of renotes) {
-		deleteNote(await Users.findOneOrFail(user.id), note);
-	}
-});
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
deleted file mode 100644
index 32c370004c..0000000000
--- a/src/server/api/endpoints/notes/user-list-timeline.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { UserLists, UserListJoinings, Notes } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { activeUsersChart } from '@/services/chart/index';
-import { Brackets } from 'typeorm';
-
-export const meta = {
-	tags: ['notes', 'lists'],
-
-	requireCredential: true as const,
-
-	params: {
-		listId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		sinceDate: {
-			validator: $.optional.num,
-		},
-
-		untilDate: {
-			validator: $.optional.num,
-		},
-
-		includeMyRenotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		includeRenotedMyNotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		includeLocalRenotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		withFiles: {
-			validator: $.optional.bool,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		noSuchList: {
-			message: 'No such list.',
-			code: 'NO_SUCH_LIST',
-			id: '8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const list = await UserLists.findOne({
-		id: ps.listId,
-		userId: user.id
-	});
-
-	if (list == null) {
-		throw new ApiError(meta.errors.noSuchList);
-	}
-
-	//#region Construct query
-	const listQuery = UserListJoinings.createQueryBuilder('joining')
-		.select('joining.userId')
-		.where('joining.userListId = :userListId', { userListId: list.id });
-
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser')
-		.setParameters(listQuery.getParameters());
-
-	generateVisibilityQuery(query, user);
-
-	if (ps.includeMyRenotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.userId != :meId', { meId: user.id });
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.includeRenotedMyNotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.renoteUserId != :meId', { meId: user.id });
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.includeLocalRenotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.renoteUserHost IS NOT NULL');
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	if (ps.withFiles) {
-		query.andWhere('note.fileIds != \'{}\'');
-	}
-	//#endregion
-
-	const timeline = await query.take(ps.limit!).getMany();
-
-	activeUsersChart.update(user);
-
-	return await Notes.packMany(timeline, user);
-});
diff --git a/src/server/api/endpoints/notes/watching/create.ts b/src/server/api/endpoints/notes/watching/create.ts
deleted file mode 100644
index 4d182d3715..0000000000
--- a/src/server/api/endpoints/notes/watching/create.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import watch from '@/services/note/watch';
-import { getNote } from '../../../common/getters';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'ea0e37a6-90a3-4f58-ba6b-c328ca206fc7'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	await watch(user.id, note);
-});
diff --git a/src/server/api/endpoints/notes/watching/delete.ts b/src/server/api/endpoints/notes/watching/delete.ts
deleted file mode 100644
index dd58c52b57..0000000000
--- a/src/server/api/endpoints/notes/watching/delete.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import unwatch from '@/services/note/unwatch';
-import { getNote } from '../../../common/getters';
-import { ApiError } from '../../../error';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: '09b3695c-f72c-4731-a428-7cff825fc82e'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	await unwatch(user.id, note);
-});
diff --git a/src/server/api/endpoints/notifications/create.ts b/src/server/api/endpoints/notifications/create.ts
deleted file mode 100644
index 8003c497ee..0000000000
--- a/src/server/api/endpoints/notifications/create.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { createNotification } from '@/services/create-notification';
-
-export const meta = {
-	tags: ['notifications'],
-
-	requireCredential: true as const,
-
-	kind: 'write:notifications',
-
-	params: {
-		body: {
-			validator: $.str
-		},
-
-		header: {
-			validator: $.optional.nullable.str
-		},
-
-		icon: {
-			validator: $.optional.nullable.str
-		},
-	},
-
-	errors: {
-	}
-};
-
-export default define(meta, async (ps, user, token) => {
-	createNotification(user.id, 'app', {
-		appAccessTokenId: token ? token.id : null,
-		customBody: ps.body,
-		customHeader: ps.header,
-		customIcon: ps.icon,
-	});
-});
diff --git a/src/server/api/endpoints/notifications/mark-all-as-read.ts b/src/server/api/endpoints/notifications/mark-all-as-read.ts
deleted file mode 100644
index 8d4e512750..0000000000
--- a/src/server/api/endpoints/notifications/mark-all-as-read.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { publishMainStream } from '@/services/stream';
-import define from '../../define';
-import { Notifications } from '@/models/index';
-
-export const meta = {
-	tags: ['notifications', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:notifications'
-};
-
-export default define(meta, async (ps, user) => {
-	// Update documents
-	await Notifications.update({
-		notifieeId: user.id,
-		isRead: false,
-	}, {
-		isRead: true
-	});
-
-	// 全ての通知を読みましたよというイベントを発行
-	publishMainStream(user.id, 'readAllNotifications');
-});
diff --git a/src/server/api/endpoints/notifications/read.ts b/src/server/api/endpoints/notifications/read.ts
deleted file mode 100644
index 66bbc4efd7..0000000000
--- a/src/server/api/endpoints/notifications/read.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishMainStream } from '@/services/stream';
-import define from '../../define';
-import { Notifications } from '@/models/index';
-import { readNotification } from '../../common/read-notification';
-import { ApiError } from '../../error';
-
-export const meta = {
-	tags: ['notifications', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'write:notifications',
-
-	params: {
-		notificationId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchNotification: {
-			message: 'No such notification.',
-			code: 'NO_SUCH_NOTIFICATION',
-			id: 'efa929d5-05b5-47d1-beec-e6a4dbed011e'
-		},
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const notification = await Notifications.findOne({
-		notifieeId: user.id,
-		id: ps.notificationId,
-	});
-
-	if (notification == null) {
-		throw new ApiError(meta.errors.noSuchNotification);
-	}
-
-	readNotification(user.id, [notification.id]);
-});
diff --git a/src/server/api/endpoints/page-push.ts b/src/server/api/endpoints/page-push.ts
deleted file mode 100644
index a0412e89f1..0000000000
--- a/src/server/api/endpoints/page-push.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import $ from 'cafy';
-import define from '../define';
-import { ID } from '@/misc/cafy-id';
-import { publishMainStream } from '@/services/stream';
-import { Users, Pages } from '@/models/index';
-import { ApiError } from '../error';
-
-export const meta = {
-	requireCredential: true as const,
-	secure: true,
-
-	params: {
-		pageId: {
-			validator: $.type(ID)
-		},
-
-		event: {
-			validator: $.str
-		},
-
-		var: {
-			validator: $.optional.nullable.any
-		}
-	},
-
-	errors: {
-		noSuchPage: {
-			message: 'No such page.',
-			code: 'NO_SUCH_PAGE',
-			id: '4a13ad31-6729-46b4-b9af-e86b265c2e74'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const page = await Pages.findOne(ps.pageId);
-	if (page == null) {
-		throw new ApiError(meta.errors.noSuchPage);
-	}
-
-	publishMainStream(page.userId, 'pageEvent', {
-		pageId: ps.pageId,
-		event: ps.event,
-		var: ps.var,
-		userId: user.id,
-		user: await Users.pack(user.id, { id: page.userId }, {
-			detail: true
-		})
-	});
-});
diff --git a/src/server/api/endpoints/pages/create.ts b/src/server/api/endpoints/pages/create.ts
deleted file mode 100644
index c23978f093..0000000000
--- a/src/server/api/endpoints/pages/create.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import $ from 'cafy';
-import * as ms from 'ms';
-import define from '../../define';
-import { ID } from '@/misc/cafy-id';
-import { Pages, DriveFiles } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { Page } from '@/models/entities/page';
-import { ApiError } from '../../error';
-
-export const meta = {
-	tags: ['pages'],
-
-	requireCredential: true as const,
-
-	kind: 'write:pages',
-
-	limit: {
-		duration: ms('1hour'),
-		max: 300
-	},
-
-	params: {
-		title: {
-			validator: $.str,
-		},
-
-		name: {
-			validator: $.str.min(1),
-		},
-
-		summary: {
-			validator: $.optional.nullable.str,
-		},
-
-		content: {
-			validator: $.arr($.obj())
-		},
-
-		variables: {
-			validator: $.arr($.obj())
-		},
-
-		script: {
-			validator: $.str,
-		},
-
-		eyeCatchingImageId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		font: {
-			validator: $.optional.str.or(['serif', 'sans-serif']),
-			default: 'sans-serif'
-		},
-
-		alignCenter: {
-			validator: $.optional.bool,
-			default: false
-		},
-
-		hideTitleWhenPinned: {
-			validator: $.optional.bool,
-			default: false
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Page',
-	},
-
-	errors: {
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c'
-		},
-		nameAlreadyExists: {
-			message: 'Specified name already exists.',
-			code: 'NAME_ALREADY_EXISTS',
-			id: '4650348e-301c-499a-83c9-6aa988c66bc1'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	let eyeCatchingImage = null;
-	if (ps.eyeCatchingImageId != null) {
-		eyeCatchingImage = await DriveFiles.findOne({
-			id: ps.eyeCatchingImageId,
-			userId: user.id
-		});
-
-		if (eyeCatchingImage == null) {
-			throw new ApiError(meta.errors.noSuchFile);
-		}
-	}
-
-	await Pages.find({
-		userId: user.id,
-		name: ps.name
-	}).then(result => {
-		if (result.length > 0) {
-			throw new ApiError(meta.errors.nameAlreadyExists);
-		}
-	});
-
-	const page = await Pages.save(new Page({
-		id: genId(),
-		createdAt: new Date(),
-		updatedAt: new Date(),
-		title: ps.title,
-		name: ps.name,
-		summary: ps.summary,
-		content: ps.content,
-		variables: ps.variables,
-		script: ps.script,
-		eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null,
-		userId: user.id,
-		visibility: 'public',
-		alignCenter: ps.alignCenter,
-		hideTitleWhenPinned: ps.hideTitleWhenPinned,
-		font: ps.font
-	}));
-
-	return await Pages.pack(page);
-});
diff --git a/src/server/api/endpoints/pages/delete.ts b/src/server/api/endpoints/pages/delete.ts
deleted file mode 100644
index b1f8c8a709..0000000000
--- a/src/server/api/endpoints/pages/delete.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Pages } from '@/models/index';
-import { ID } from '@/misc/cafy-id';
-
-export const meta = {
-	tags: ['pages'],
-
-	requireCredential: true as const,
-
-	kind: 'write:pages',
-
-	params: {
-		pageId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchPage: {
-			message: 'No such page.',
-			code: 'NO_SUCH_PAGE',
-			id: 'eb0c6e1d-d519-4764-9486-52a7e1c6392a'
-		},
-
-		accessDenied: {
-			message: 'Access denied.',
-			code: 'ACCESS_DENIED',
-			id: '8b741b3e-2c22-44b3-a15f-29949aa1601e'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const page = await Pages.findOne(ps.pageId);
-	if (page == null) {
-		throw new ApiError(meta.errors.noSuchPage);
-	}
-	if (page.userId !== user.id) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	await Pages.delete(page.id);
-});
diff --git a/src/server/api/endpoints/pages/featured.ts b/src/server/api/endpoints/pages/featured.ts
deleted file mode 100644
index f891c45f05..0000000000
--- a/src/server/api/endpoints/pages/featured.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import define from '../../define';
-import { Pages } from '@/models/index';
-
-export const meta = {
-	tags: ['pages'],
-
-	requireCredential: false as const,
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Page',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = Pages.createQueryBuilder('page')
-		.where('page.visibility = \'public\'')
-		.andWhere('page.likedCount > 0')
-		.orderBy('page.likedCount', 'DESC');
-
-	const pages = await query.take(10).getMany();
-
-	return await Pages.packMany(pages, me);
-});
diff --git a/src/server/api/endpoints/pages/like.ts b/src/server/api/endpoints/pages/like.ts
deleted file mode 100644
index a95a377802..0000000000
--- a/src/server/api/endpoints/pages/like.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Pages, PageLikes } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['pages'],
-
-	requireCredential: true as const,
-
-	kind: 'write:page-likes',
-
-	params: {
-		pageId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchPage: {
-			message: 'No such page.',
-			code: 'NO_SUCH_PAGE',
-			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3'
-		},
-
-		yourPage: {
-			message: 'You cannot like your page.',
-			code: 'YOUR_PAGE',
-			id: '28800466-e6db-40f2-8fae-bf9e82aa92b8'
-		},
-
-		alreadyLiked: {
-			message: 'The page has already been liked.',
-			code: 'ALREADY_LIKED',
-			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const page = await Pages.findOne(ps.pageId);
-	if (page == null) {
-		throw new ApiError(meta.errors.noSuchPage);
-	}
-
-	if (page.userId === user.id) {
-		throw new ApiError(meta.errors.yourPage);
-	}
-
-	// if already liked
-	const exist = await PageLikes.findOne({
-		pageId: page.id,
-		userId: user.id
-	});
-
-	if (exist != null) {
-		throw new ApiError(meta.errors.alreadyLiked);
-	}
-
-	// Create like
-	await PageLikes.insert({
-		id: genId(),
-		createdAt: new Date(),
-		pageId: page.id,
-		userId: user.id
-	});
-
-	Pages.increment({ id: page.id }, 'likedCount', 1);
-});
diff --git a/src/server/api/endpoints/pages/show.ts b/src/server/api/endpoints/pages/show.ts
deleted file mode 100644
index 7c55d4a9e6..0000000000
--- a/src/server/api/endpoints/pages/show.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Pages, Users } from '@/models/index';
-import { ID } from '@/misc/cafy-id';
-import { Page } from '@/models/entities/page';
-
-export const meta = {
-	tags: ['pages'],
-
-	requireCredential: false as const,
-
-	params: {
-		pageId: {
-			validator: $.optional.type(ID),
-		},
-
-		name: {
-			validator: $.optional.str,
-		},
-
-		username: {
-			validator: $.optional.str,
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'Page',
-	},
-
-	errors: {
-		noSuchPage: {
-			message: 'No such page.',
-			code: 'NO_SUCH_PAGE',
-			id: '222120c0-3ead-4528-811b-b96f233388d7'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	let page: Page | undefined;
-
-	if (ps.pageId) {
-		page = await Pages.findOne(ps.pageId);
-	} else if (ps.name && ps.username) {
-		const author = await Users.findOne({
-			host: null,
-			usernameLower: ps.username.toLowerCase()
-		});
-		if (author) {
-			page = await Pages.findOne({
-				name: ps.name,
-				userId: author.id
-			});
-		}
-	}
-
-	if (page == null) {
-		throw new ApiError(meta.errors.noSuchPage);
-	}
-
-	return await Pages.pack(page, user);
-});
diff --git a/src/server/api/endpoints/pages/unlike.ts b/src/server/api/endpoints/pages/unlike.ts
deleted file mode 100644
index facf2d6d5f..0000000000
--- a/src/server/api/endpoints/pages/unlike.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Pages, PageLikes } from '@/models/index';
-
-export const meta = {
-	tags: ['pages'],
-
-	requireCredential: true as const,
-
-	kind: 'write:page-likes',
-
-	params: {
-		pageId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchPage: {
-			message: 'No such page.',
-			code: 'NO_SUCH_PAGE',
-			id: 'a0d41e20-1993-40bd-890e-f6e560ae648e'
-		},
-
-		notLiked: {
-			message: 'You have not liked that page.',
-			code: 'NOT_LIKED',
-			id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const page = await Pages.findOne(ps.pageId);
-	if (page == null) {
-		throw new ApiError(meta.errors.noSuchPage);
-	}
-
-	const exist = await PageLikes.findOne({
-		pageId: page.id,
-		userId: user.id
-	});
-
-	if (exist == null) {
-		throw new ApiError(meta.errors.notLiked);
-	}
-
-	// Delete like
-	await PageLikes.delete(exist.id);
-
-	Pages.decrement({ id: page.id }, 'likedCount', 1);
-});
diff --git a/src/server/api/endpoints/pages/update.ts b/src/server/api/endpoints/pages/update.ts
deleted file mode 100644
index b3a7f26963..0000000000
--- a/src/server/api/endpoints/pages/update.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import $ from 'cafy';
-import * as ms from 'ms';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Pages, DriveFiles } from '@/models/index';
-import { ID } from '@/misc/cafy-id';
-import { Not } from 'typeorm';
-
-export const meta = {
-	tags: ['pages'],
-
-	requireCredential: true as const,
-
-	kind: 'write:pages',
-
-	limit: {
-		duration: ms('1hour'),
-		max: 300
-	},
-
-	params: {
-		pageId: {
-			validator: $.type(ID),
-		},
-
-		title: {
-			validator: $.str,
-		},
-
-		name: {
-			validator: $.str.min(1),
-		},
-
-		summary: {
-			validator: $.optional.nullable.str,
-		},
-
-		content: {
-			validator: $.arr($.obj())
-		},
-
-		variables: {
-			validator: $.arr($.obj())
-		},
-
-		script: {
-			validator: $.str,
-		},
-
-		eyeCatchingImageId: {
-			validator: $.optional.nullable.type(ID),
-		},
-
-		font: {
-			validator: $.optional.str.or(['serif', 'sans-serif']),
-		},
-
-		alignCenter: {
-			validator: $.optional.bool,
-		},
-
-		hideTitleWhenPinned: {
-			validator: $.optional.bool,
-		},
-	},
-
-	errors: {
-		noSuchPage: {
-			message: 'No such page.',
-			code: 'NO_SUCH_PAGE',
-			id: '21149b9e-3616-4778-9592-c4ce89f5a864'
-		},
-
-		accessDenied: {
-			message: 'Access denied.',
-			code: 'ACCESS_DENIED',
-			id: '3c15cd52-3b4b-4274-967d-6456fc4f792b'
-		},
-
-		noSuchFile: {
-			message: 'No such file.',
-			code: 'NO_SUCH_FILE',
-			id: 'cfc23c7c-3887-490e-af30-0ed576703c82'
-		},
-		nameAlreadyExists: {
-			message: 'Specified name already exists.',
-			code: 'NAME_ALREADY_EXISTS',
-			id: '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const page = await Pages.findOne(ps.pageId);
-	if (page == null) {
-		throw new ApiError(meta.errors.noSuchPage);
-	}
-	if (page.userId !== user.id) {
-		throw new ApiError(meta.errors.accessDenied);
-	}
-
-	let eyeCatchingImage = null;
-	if (ps.eyeCatchingImageId != null) {
-		eyeCatchingImage = await DriveFiles.findOne({
-			id: ps.eyeCatchingImageId,
-			userId: user.id
-		});
-
-		if (eyeCatchingImage == null) {
-			throw new ApiError(meta.errors.noSuchFile);
-		}
-	}
-
-	await Pages.find({
-		id: Not(ps.pageId),
-		userId: user.id,
-		name: ps.name
-	}).then(result => {
-		if (result.length > 0) {
-			throw new ApiError(meta.errors.nameAlreadyExists);
-		}
-	});
-
-	await Pages.update(page.id, {
-		updatedAt: new Date(),
-		title: ps.title,
-		name: ps.name === undefined ? page.name : ps.name,
-		summary: ps.name === undefined ? page.summary : ps.summary,
-		content: ps.content,
-		variables: ps.variables,
-		script: ps.script,
-		alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
-		hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
-		font: ps.font === undefined ? page.font : ps.font,
-		eyeCatchingImageId: ps.eyeCatchingImageId === null
-			? null
-			: ps.eyeCatchingImageId === undefined
-				? page.eyeCatchingImageId
-				: eyeCatchingImage!.id,
-	});
-});
diff --git a/src/server/api/endpoints/ping.ts b/src/server/api/endpoints/ping.ts
deleted file mode 100644
index 0b1bb6e164..0000000000
--- a/src/server/api/endpoints/ping.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import define from '../define';
-
-export const meta = {
-	requireCredential: false as const,
-
-	tags: ['meta'],
-
-	params: {
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			pong: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-		}
-	}
-};
-
-export default define(meta, async () => {
-	return {
-		pong: Date.now(),
-	};
-});
diff --git a/src/server/api/endpoints/pinned-users.ts b/src/server/api/endpoints/pinned-users.ts
deleted file mode 100644
index e88dfbd535..0000000000
--- a/src/server/api/endpoints/pinned-users.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import define from '../define';
-import { Users } from '@/models/index';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { parseAcct } from '@/misc/acct';
-import { User } from '@/models/entities/user';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const meta = await fetchMeta();
-
-	const users = await Promise.all(meta.pinnedUsers.map(acct => Users.findOne(parseAcct(acct))));
-
-	return await Users.packMany(users.filter(x => x !== undefined) as User[], me, { detail: true });
-});
diff --git a/src/server/api/endpoints/promo/read.ts b/src/server/api/endpoints/promo/read.ts
deleted file mode 100644
index ae57bf9cf1..0000000000
--- a/src/server/api/endpoints/promo/read.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getNote } from '../../common/getters';
-import { PromoReads } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['notes'],
-
-	requireCredential: true as const,
-
-	params: {
-		noteId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchNote: {
-			message: 'No such note.',
-			code: 'NO_SUCH_NOTE',
-			id: 'd785b897-fcd3-4fe9-8fc3-b85c26e6c932'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const note = await getNote(ps.noteId).catch(e => {
-		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
-		throw e;
-	});
-
-	const exist = await PromoReads.findOne({
-		noteId: note.id,
-		userId: user.id
-	});
-
-	if (exist != null) {
-		return;
-	}
-
-	await PromoReads.insert({
-		id: genId(),
-		createdAt: new Date(),
-		noteId: note.id,
-		userId: user.id
-	});
-});
diff --git a/src/server/api/endpoints/request-reset-password.ts b/src/server/api/endpoints/request-reset-password.ts
deleted file mode 100644
index f9928c2ee6..0000000000
--- a/src/server/api/endpoints/request-reset-password.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import $ from 'cafy';
-import { publishMainStream } from '@/services/stream';
-import define from '../define';
-import rndstr from 'rndstr';
-import config from '@/config/index';
-import * as ms from 'ms';
-import { Users, UserProfiles, PasswordResetRequests } from '@/models/index';
-import { sendEmail } from '@/services/send-email';
-import { ApiError } from '../error';
-import { genId } from '@/misc/gen-id';
-import { IsNull } from 'typeorm';
-
-export const meta = {
-	requireCredential: false as const,
-
-	limit: {
-		duration: ms('1hour'),
-		max: 3
-	},
-
-	params: {
-		username: {
-			validator: $.str
-		},
-
-		email: {
-			validator: $.str
-		},
-	},
-
-	errors: {
-
-	}
-};
-
-export default define(meta, async (ps) => {
-	const user = await Users.findOne({
-		usernameLower: ps.username.toLowerCase(),
-		host: IsNull()
-	});
-
-	// 合致するユーザーが登録されていなかったら無視
-	if (user == null) {
-		return;
-	}
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// 合致するメアドが登録されていなかったら無視
-	if (profile.email !== ps.email) {
-		return;
-	}
-
-	// メアドが認証されていなかったら無視
-	if (!profile.emailVerified) {
-		return;
-	}
-
-	const token = rndstr('a-z0-9', 64);
-
-	await PasswordResetRequests.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: profile.userId,
-		token
-	});
-
-	const link = `${config.url}/reset-password/${token}`;
-
-	sendEmail(ps.email, 'Password reset requested',
-		`To reset password, please click this link:<br><a href="${link}">${link}</a>`,
-		`To reset password, please click this link: ${link}`);
-});
diff --git a/src/server/api/endpoints/reset-db.ts b/src/server/api/endpoints/reset-db.ts
deleted file mode 100644
index f0a9dae4ff..0000000000
--- a/src/server/api/endpoints/reset-db.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import $ from 'cafy';
-import define from '../define';
-import { ApiError } from '../error';
-import { resetDb } from '@/db/postgre';
-
-export const meta = {
-	requireCredential: false as const,
-
-	params: {
-	},
-
-	errors: {
-
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
-
-	await resetDb();
-
-	await new Promise(resolve => setTimeout(resolve, 1000));
-});
diff --git a/src/server/api/endpoints/reset-password.ts b/src/server/api/endpoints/reset-password.ts
deleted file mode 100644
index 53b0bfde0b..0000000000
--- a/src/server/api/endpoints/reset-password.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import * as bcrypt from 'bcryptjs';
-import { publishMainStream } from '@/services/stream';
-import define from '../define';
-import { Users, UserProfiles, PasswordResetRequests } from '@/models/index';
-import { ApiError } from '../error';
-
-export const meta = {
-	requireCredential: false as const,
-
-	params: {
-		token: {
-			validator: $.str
-		},
-
-		password: {
-			validator: $.str
-		}
-	},
-
-	errors: {
-
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const req = await PasswordResetRequests.findOneOrFail({
-		token: ps.token,
-	});
-
-	// 発行してから30分以上経過していたら無効
-	if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) {
-		throw new Error(); // TODO
-	}
-
-	// Generate hash of password
-	const salt = await bcrypt.genSalt(8);
-	const hash = await bcrypt.hash(ps.password, salt);
-
-	await UserProfiles.update(req.userId, {
-		password: hash
-	});
-
-	PasswordResetRequests.delete(req.id);
-});
diff --git a/src/server/api/endpoints/room/show.ts b/src/server/api/endpoints/room/show.ts
deleted file mode 100644
index a6461d4a6e..0000000000
--- a/src/server/api/endpoints/room/show.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Users, UserProfiles } from '@/models/index';
-import { ID } from '@/misc/cafy-id';
-import { toPunyNullable } from '@/misc/convert-host';
-
-export const meta = {
-	tags: ['room'],
-
-	requireCredential: false as const,
-
-	params: {
-		userId: {
-			validator: $.optional.type(ID),
-		},
-
-		username: {
-			validator: $.optional.str
-		},
-
-		host: {
-			validator: $.optional.nullable.str
-		},
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '7ad3fa3e-5e12-42f0-b23a-f3d13f10ee4b'
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			roomType: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				enum: ['default', 'washitsu']
-			},
-			furnitures: {
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					properties: {
-						id: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const
-						},
-						type: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const
-						},
-						props: {
-							type: 'object' as const,
-							optional: true as const, nullable: false as const,
-						},
-						position: {
-							type: 'object' as const,
-							optional: false as const, nullable: false as const,
-							properties: {
-								x: {
-									type: 'number' as const,
-									optional: false as const, nullable: false as const
-								},
-								y: {
-									type: 'number' as const,
-									optional: false as const, nullable: false as const
-								},
-								z: {
-									type: 'number' as const,
-									optional: false as const, nullable: false as const
-								}
-							}
-						},
-						rotation: {
-							type: 'object' as const,
-							optional: false as const, nullable: false as const,
-							properties: {
-								x: {
-									type: 'number' as const,
-									optional: false as const, nullable: false as const
-								},
-								y: {
-									type: 'number' as const,
-									optional: false as const, nullable: false as const
-								},
-								z: {
-									type: 'number' as const,
-									optional: false as const, nullable: false as const
-								}
-							}
-						}
-					}
-				}
-			},
-			carpetColor: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				format: 'hex',
-				example: '#85CAF0'
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId != null
-		? { id: ps.userId }
-		: { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) });
-
-	if (user == null) {
-		throw new ApiError(meta.errors.noSuchUser);
-	}
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	if (profile.room.furnitures == null) {
-		await UserProfiles.update(user.id, {
-			room: {
-				furnitures: [],
-				...profile.room
-			}
-		});
-
-		profile.room.furnitures = [];
-	}
-
-	if (profile.room.roomType == null) {
-		const initialType = 'default';
-		await UserProfiles.update(user.id, {
-			room: {
-				roomType: initialType as any,
-				...profile.room
-			}
-		});
-
-		profile.room.roomType = initialType;
-	}
-
-	if (profile.room.carpetColor == null) {
-		const initialColor = '#85CAF0';
-		await UserProfiles.update(user.id, {
-			room: {
-				carpetColor: initialColor as any,
-				...profile.room
-			}
-		});
-
-		profile.room.carpetColor = initialColor;
-	}
-
-	return profile.room;
-});
diff --git a/src/server/api/endpoints/room/update.ts b/src/server/api/endpoints/room/update.ts
deleted file mode 100644
index 8c4cfbdea6..0000000000
--- a/src/server/api/endpoints/room/update.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import $ from 'cafy';
-import { publishMainStream } from '@/services/stream';
-import define from '../../define';
-import { Users, UserProfiles } from '@/models/index';
-
-export const meta = {
-	tags: ['room'],
-
-	requireCredential: true as const,
-
-	params: {
-		room: {
-			validator: $.obj({
-				furnitures: $.arr($.obj({
-					id: $.str,
-					type: $.str,
-					position: $.obj({
-						x: $.num,
-						y: $.num,
-						z: $.num,
-					}),
-					rotation: $.obj({
-						x: $.num,
-						y: $.num,
-						z: $.num,
-					}),
-					props: $.optional.nullable.obj(),
-				})),
-				roomType: $.str,
-				carpetColor: $.str
-			})
-		},
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	await UserProfiles.update(user.id, {
-		room: ps.room as any
-	});
-
-	const iObj = await Users.pack(user.id, user, {
-		detail: true,
-		includeSecrets: true
-	});
-
-	// Publish meUpdated event
-	publishMainStream(user.id, 'meUpdated', iObj);
-
-	// TODO: レスポンスがおかしいと思う by YuzuRyo61
-	return iObj;
-});
diff --git a/src/server/api/endpoints/server-info.ts b/src/server/api/endpoints/server-info.ts
deleted file mode 100644
index 4e636d331c..0000000000
--- a/src/server/api/endpoints/server-info.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as os from 'os';
-import * as si from 'systeminformation';
-import define from '../define';
-
-export const meta = {
-	requireCredential: false as const,
-
-	desc: {
-	},
-
-	tags: ['meta'],
-
-	params: {
-	},
-};
-
-export default define(meta, async () => {
-	const memStats = await si.mem();
-	const fsStats = await si.fsSize();
-
-	return {
-		machine: os.hostname(),
-		cpu: {
-			model: os.cpus()[0].model,
-			cores: os.cpus().length
-		},
-		mem: {
-			total: memStats.total
-		},
-		fs: {
-			total: fsStats[0].size,
-			used: fsStats[0].used,
-		},
-	};
-});
diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts
deleted file mode 100644
index 15c8001742..0000000000
--- a/src/server/api/endpoints/stats.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import define from '../define';
-import { NoteReactions, Notes, Users } from '@/models/index';
-import { federationChart, driveChart } from '@/services/chart/index';
-
-export const meta = {
-	requireCredential: false as const,
-
-	tags: ['meta'],
-
-	params: {
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			notesCount: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-			originalNotesCount: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-			usersCount: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-			originalUsersCount: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-			instances: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-			driveUsageLocal: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const
-			},
-			driveUsageRemote: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const
-			}
-		}
-	}
-};
-
-export default define(meta, async () => {
-	const [
-		notesCount,
-		originalNotesCount,
-		usersCount,
-		originalUsersCount,
-		reactionsCount,
-		//originalReactionsCount,
-		instances,
-		driveUsageLocal,
-		driveUsageRemote
-	] = await Promise.all([
-		Notes.count({ cache: 3600000 }), // 1 hour
-		Notes.count({ where: { userHost: null }, cache: 3600000 }),
-		Users.count({ cache: 3600000 }),
-		Users.count({ where: { host: null }, cache: 3600000 }),
-		NoteReactions.count({ cache: 3600000 }), // 1 hour
-		//NoteReactions.count({ where: { userHost: null }, cache: 3600000 }),
-		federationChart.getChart('hour', 1, null).then(chart => chart.instance.total[0]),
-		driveChart.getChart('hour', 1, null).then(chart => chart.local.totalSize[0]),
-		driveChart.getChart('hour', 1, null).then(chart => chart.remote.totalSize[0]),
-	]);
-
-	return {
-		notesCount,
-		originalNotesCount,
-		usersCount,
-		originalUsersCount,
-		reactionsCount,
-		//originalReactionsCount,
-		instances,
-		driveUsageLocal,
-		driveUsageRemote
-	};
-});
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
deleted file mode 100644
index 6e14ba2669..0000000000
--- a/src/server/api/endpoints/sw/register.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { genId } from '@/misc/gen-id';
-import { SwSubscriptions } from '@/models/index';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	params: {
-		endpoint: {
-			validator: $.str
-		},
-
-		auth: {
-			validator: $.str
-		},
-
-		publickey: {
-			validator: $.str
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			state: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-				enum: ['already-subscribed', 'subscribed']
-			},
-			key: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// if already subscribed
-	const exist = await SwSubscriptions.findOne({
-		userId: user.id,
-		endpoint: ps.endpoint,
-		auth: ps.auth,
-		publickey: ps.publickey,
-	});
-
-	const instance = await fetchMeta(true);
-
-	if (exist != null) {
-		return {
-			state: 'already-subscribed',
-			key: instance.swPublicKey
-		};
-	}
-
-	await SwSubscriptions.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		endpoint: ps.endpoint,
-		auth: ps.auth,
-		publickey: ps.publickey
-	});
-
-	return {
-		state: 'subscribed',
-		key: instance.swPublicKey
-	};
-});
diff --git a/src/server/api/endpoints/sw/unregister.ts b/src/server/api/endpoints/sw/unregister.ts
deleted file mode 100644
index 817ad1f517..0000000000
--- a/src/server/api/endpoints/sw/unregister.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { SwSubscriptions } from '../../../../models';
-
-export const meta = {
-	tags: ['account'],
-
-	requireCredential: true as const,
-
-	params: {
-		endpoint: {
-			validator: $.str
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	await SwSubscriptions.delete({
-		userId: user.id,
-		endpoint: ps.endpoint,
-	});
-});
diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts
deleted file mode 100644
index 1ae75448ea..0000000000
--- a/src/server/api/endpoints/username/available.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Users, UsedUsernames } from '@/models/index';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		username: {
-			validator: $.use(Users.validateLocalUsername)
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			available: {
-				type: 'boolean' as const,
-				optional: false as const, nullable: false as const,
-			}
-		}
-	}
-};
-
-export default define(meta, async (ps) => {
-	// Get exist
-	const exist = await Users.count({
-		host: null,
-		usernameLower: ps.username.toLowerCase()
-	});
-
-	const exist2 = await UsedUsernames.count({ username: ps.username.toLowerCase() });
-
-	return {
-		available: exist === 0 && exist2 === 0
-	};
-});
diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts
deleted file mode 100644
index 930dcc7616..0000000000
--- a/src/server/api/endpoints/users.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import $ from 'cafy';
-import define from '../define';
-import { Users } from '@/models/index';
-import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query';
-import { generateBlockQueryForUsers } from '../common/generate-block-query';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		},
-
-		sort: {
-			validator: $.optional.str.or([
-				'+follower',
-				'-follower',
-				'+createdAt',
-				'-createdAt',
-				'+updatedAt',
-				'-updatedAt',
-			]),
-		},
-
-		state: {
-			validator: $.optional.str.or([
-				'all',
-				'admin',
-				'moderator',
-				'adminOrModerator',
-				'alive'
-			]),
-			default: 'all'
-		},
-
-		origin: {
-			validator: $.optional.str.or([
-				'combined',
-				'local',
-				'remote',
-			]),
-			default: 'local'
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = Users.createQueryBuilder('user');
-	query.where('user.isExplorable = TRUE');
-
-	switch (ps.state) {
-		case 'admin': query.andWhere('user.isAdmin = TRUE'); break;
-		case 'moderator': query.andWhere('user.isModerator = TRUE'); break;
-		case 'adminOrModerator': query.andWhere('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
-		case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
-	}
-
-	switch (ps.origin) {
-		case 'local': query.andWhere('user.host IS NULL'); break;
-		case 'remote': query.andWhere('user.host IS NOT NULL'); break;
-	}
-
-	switch (ps.sort) {
-		case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
-		case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
-		case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break;
-		case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break;
-		case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break;
-		case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break;
-		default: query.orderBy('user.id', 'ASC'); break;
-	}
-
-	if (me) generateMutedUserQueryForUsers(query, me);
-	if (me) generateBlockQueryForUsers(query, me);
-
-	query.take(ps.limit!);
-	query.skip(ps.offset);
-
-	const users = await query.getMany();
-
-	return await Users.packMany(users, me, { detail: true });
-});
diff --git a/src/server/api/endpoints/users/clips.ts b/src/server/api/endpoints/users/clips.ts
deleted file mode 100644
index 8feca9422a..0000000000
--- a/src/server/api/endpoints/users/clips.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Clips } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['users', 'clips'],
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId)
-		.andWhere(`clip.userId = :userId`, { userId: ps.userId })
-		.andWhere('clip.isPublic = true');
-
-	const clips = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Clips.packMany(clips);
-});
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
deleted file mode 100644
index 6d042a2861..0000000000
--- a/src/server/api/endpoints/users/followers.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Users, Followings, UserProfiles } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { toPunyNullable } from '@/misc/convert-host';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		userId: {
-			validator: $.optional.type(ID),
-		},
-
-		username: {
-			validator: $.optional.str
-		},
-
-		host: {
-			validator: $.optional.nullable.str
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Following',
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '27fa5435-88ab-43de-9360-387de88727cd'
-		},
-
-		forbidden: {
-			message: 'Forbidden.',
-			code: 'FORBIDDEN',
-			id: '3c6a84db-d619-26af-ca14-06232a21df8a'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId != null
-		? { id: ps.userId }
-		: { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) });
-
-	if (user == null) {
-		throw new ApiError(meta.errors.noSuchUser);
-	}
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	if (profile.ffVisibility === 'private') {
-		if (me == null || (me.id !== user.id)) {
-			throw new ApiError(meta.errors.forbidden);
-		}
-	} else if (profile.ffVisibility === 'followers') {
-		if (me == null) {
-			throw new ApiError(meta.errors.forbidden);
-		} else if (me.id !== user.id) {
-			const following = await Followings.findOne({
-				followeeId: user.id,
-				followerId: me.id,
-			});
-			if (following == null) {
-				throw new ApiError(meta.errors.forbidden);
-			}
-		}
-	}
-
-	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
-		.andWhere(`following.followeeId = :userId`, { userId: user.id })
-		.innerJoinAndSelect('following.follower', 'follower');
-
-	const followings = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Followings.packMany(followings, me, { populateFollower: true });
-});
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
deleted file mode 100644
index 1033117ef8..0000000000
--- a/src/server/api/endpoints/users/following.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { Users, Followings, UserProfiles } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { toPunyNullable } from '@/misc/convert-host';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		userId: {
-			validator: $.optional.type(ID),
-		},
-
-		username: {
-			validator: $.optional.str
-		},
-
-		host: {
-			validator: $.optional.nullable.str
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Following',
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '63e4aba4-4156-4e53-be25-c9559e42d71b'
-		},
-
-		forbidden: {
-			message: 'Forbidden.',
-			code: 'FORBIDDEN',
-			id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId != null
-		? { id: ps.userId }
-		: { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) });
-
-	if (user == null) {
-		throw new ApiError(meta.errors.noSuchUser);
-	}
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	if (profile.ffVisibility === 'private') {
-		if (me == null || (me.id !== user.id)) {
-			throw new ApiError(meta.errors.forbidden);
-		}
-	} else if (profile.ffVisibility === 'followers') {
-		if (me == null) {
-			throw new ApiError(meta.errors.forbidden);
-		} else if (me.id !== user.id) {
-			const following = await Followings.findOne({
-				followeeId: user.id,
-				followerId: me.id,
-			});
-			if (following == null) {
-				throw new ApiError(meta.errors.forbidden);
-			}
-		}
-	}
-
-	const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
-		.andWhere(`following.followerId = :userId`, { userId: user.id })
-		.innerJoinAndSelect('following.followee', 'followee');
-
-	const followings = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Followings.packMany(followings, me, { populateFollowee: true });
-});
diff --git a/src/server/api/endpoints/users/gallery/posts.ts b/src/server/api/endpoints/users/gallery/posts.ts
deleted file mode 100644
index 845de1089c..0000000000
--- a/src/server/api/endpoints/users/gallery/posts.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { GalleryPosts } from '@/models/index';
-import { makePaginationQuery } from '../../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['users', 'gallery'],
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
-		.andWhere(`post.userId = :userId`, { userId: ps.userId });
-
-	const posts = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await GalleryPosts.packMany(posts, user);
-});
diff --git a/src/server/api/endpoints/users/get-frequently-replied-users.ts b/src/server/api/endpoints/users/get-frequently-replied-users.ts
deleted file mode 100644
index 32ebfd683a..0000000000
--- a/src/server/api/endpoints/users/get-frequently-replied-users.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { maximum } from '@/prelude/array';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { Not, In, IsNull } from 'typeorm';
-import { Notes, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: 'e6965129-7b2a-40a4-bae2-cd84cd434822'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Lookup user
-	const user = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Fetch recent notes
-	const recentNotes = await Notes.find({
-		where: {
-			userId: user.id,
-			replyId: Not(IsNull())
-		},
-		order: {
-			id: -1
-		},
-		take: 1000,
-		select: ['replyId']
-	});
-
-	// 投稿が少なかったら中断
-	if (recentNotes.length === 0) {
-		return [];
-	}
-
-	// TODO ミュートを考慮
-	const replyTargetNotes = await Notes.find({
-		where: {
-			id: In(recentNotes.map(p => p.replyId)),
-		},
-		select: ['userId']
-	});
-
-	const repliedUsers: any = {};
-
-	// Extract replies from recent notes
-	for (const userId of replyTargetNotes.map(x => x.userId.toString())) {
-		if (repliedUsers[userId]) {
-			repliedUsers[userId]++;
-		} else {
-			repliedUsers[userId] = 1;
-		}
-	}
-
-	// Calc peak
-	const peak = maximum(Object.values(repliedUsers));
-
-	// Sort replies by frequency
-	const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
-
-	// Extract top replied users
-	const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit!);
-
-	// Make replies object (includes weights)
-	const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
-		user: await Users.pack(user, me, { detail: true }),
-		weight: repliedUsers[user] / peak
-	})));
-
-	return repliesObj;
-});
diff --git a/src/server/api/endpoints/users/groups/create.ts b/src/server/api/endpoints/users/groups/create.ts
deleted file mode 100644
index dc1ee3879e..0000000000
--- a/src/server/api/endpoints/users/groups/create.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { UserGroups, UserGroupJoinings } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { UserGroup } from '@/models/entities/user-group';
-import { UserGroupJoining } from '@/models/entities/user-group-joining';
-
-export const meta = {
-	tags: ['groups'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		name: {
-			validator: $.str.range(1, 100)
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'UserGroup',
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const userGroup = await UserGroups.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		name: ps.name,
-	} as UserGroup).then(x => UserGroups.findOneOrFail(x.identifiers[0]));
-
-	// Push the owner
-	await UserGroupJoinings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		userGroupId: userGroup.id
-	} as UserGroupJoining);
-
-	return await UserGroups.pack(userGroup);
-});
diff --git a/src/server/api/endpoints/users/groups/delete.ts b/src/server/api/endpoints/users/groups/delete.ts
deleted file mode 100644
index 7da1b4a273..0000000000
--- a/src/server/api/endpoints/users/groups/delete.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { UserGroups } from '@/models/index';
-
-export const meta = {
-	tags: ['groups'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		groupId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: '63dbd64c-cd77-413f-8e08-61781e210b38'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const userGroup = await UserGroups.findOne({
-		id: ps.groupId,
-		userId: user.id
-	});
-
-	if (userGroup == null) {
-		throw new ApiError(meta.errors.noSuchGroup);
-	}
-
-	await UserGroups.delete(userGroup.id);
-});
diff --git a/src/server/api/endpoints/users/groups/invitations/accept.ts b/src/server/api/endpoints/users/groups/invitations/accept.ts
deleted file mode 100644
index 09e6ae2647..0000000000
--- a/src/server/api/endpoints/users/groups/invitations/accept.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../../define';
-import { ApiError } from '../../../../error';
-import { UserGroupJoinings, UserGroupInvitations } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { UserGroupJoining } from '@/models/entities/user-group-joining';
-
-export const meta = {
-	tags: ['groups', 'users'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		invitationId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchInvitation: {
-			message: 'No such invitation.',
-			code: 'NO_SUCH_INVITATION',
-			id: '98c11eca-c890-4f42-9806-c8c8303ebb5e'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch the invitation
-	const invitation = await UserGroupInvitations.findOne({
-		id: ps.invitationId,
-	});
-
-	if (invitation == null) {
-		throw new ApiError(meta.errors.noSuchInvitation);
-	}
-
-	if (invitation.userId !== user.id) {
-		throw new ApiError(meta.errors.noSuchInvitation);
-	}
-
-	// Push the user
-	await UserGroupJoinings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		userGroupId: invitation.userGroupId
-	} as UserGroupJoining);
-
-	UserGroupInvitations.delete(invitation.id);
-});
diff --git a/src/server/api/endpoints/users/groups/invitations/reject.ts b/src/server/api/endpoints/users/groups/invitations/reject.ts
deleted file mode 100644
index 741fcefb35..0000000000
--- a/src/server/api/endpoints/users/groups/invitations/reject.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../../define';
-import { ApiError } from '../../../../error';
-import { UserGroupInvitations } from '@/models/index';
-
-export const meta = {
-	tags: ['groups', 'users'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		invitationId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchInvitation: {
-			message: 'No such invitation.',
-			code: 'NO_SUCH_INVITATION',
-			id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch the invitation
-	const invitation = await UserGroupInvitations.findOne({
-		id: ps.invitationId,
-	});
-
-	if (invitation == null) {
-		throw new ApiError(meta.errors.noSuchInvitation);
-	}
-
-	if (invitation.userId !== user.id) {
-		throw new ApiError(meta.errors.noSuchInvitation);
-	}
-
-	await UserGroupInvitations.delete(invitation.id);
-});
diff --git a/src/server/api/endpoints/users/groups/invite.ts b/src/server/api/endpoints/users/groups/invite.ts
deleted file mode 100644
index f1ee8bf8b7..0000000000
--- a/src/server/api/endpoints/users/groups/invite.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { UserGroupInvitation } from '@/models/entities/user-group-invitation';
-import { createNotification } from '@/services/create-notification';
-
-export const meta = {
-	tags: ['groups', 'users'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		groupId: {
-			validator: $.type(ID),
-		},
-
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: '583f8bc0-8eee-4b78-9299-1e14fc91e409'
-		},
-
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: 'da52de61-002c-475b-90e1-ba64f9cf13a8'
-		},
-
-		alreadyAdded: {
-			message: 'That user has already been added to that group.',
-			code: 'ALREADY_ADDED',
-			id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c'
-		},
-
-		alreadyInvited: {
-			message: 'That user has already been invited to that group.',
-			code: 'ALREADY_INVITED',
-			id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the group
-	const userGroup = await UserGroups.findOne({
-		id: ps.groupId,
-		userId: me.id,
-	});
-
-	if (userGroup == null) {
-		throw new ApiError(meta.errors.noSuchGroup);
-	}
-
-	// Fetch the user
-	const user = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	const joining = await UserGroupJoinings.findOne({
-		userGroupId: userGroup.id,
-		userId: user.id
-	});
-
-	if (joining) {
-		throw new ApiError(meta.errors.alreadyAdded);
-	}
-
-	const existInvitation = await UserGroupInvitations.findOne({
-		userGroupId: userGroup.id,
-		userId: user.id
-	});
-
-	if (existInvitation) {
-		throw new ApiError(meta.errors.alreadyInvited);
-	}
-
-	const invitation = await UserGroupInvitations.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		userGroupId: userGroup.id
-	} as UserGroupInvitation).then(x => UserGroupInvitations.findOneOrFail(x.identifiers[0]));
-
-	// 通知を作成
-	createNotification(user.id, 'groupInvited', {
-		notifierId: me.id,
-		userGroupInvitationId: invitation.id
-	});
-});
diff --git a/src/server/api/endpoints/users/groups/joined.ts b/src/server/api/endpoints/users/groups/joined.ts
deleted file mode 100644
index d5e8fe4032..0000000000
--- a/src/server/api/endpoints/users/groups/joined.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import define from '../../../define';
-import { UserGroups, UserGroupJoinings } from '@/models/index';
-import { Not, In } from 'typeorm';
-
-export const meta = {
-	tags: ['groups', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:user-groups',
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'UserGroup',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const ownedGroups = await UserGroups.find({
-		userId: me.id,
-	});
-
-	const joinings = await UserGroupJoinings.find({
-		userId: me.id,
-		...(ownedGroups.length > 0 ? {
-			userGroupId: Not(In(ownedGroups.map(x => x.id)))
-		} : {})
-	});
-
-	return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId)));
-});
diff --git a/src/server/api/endpoints/users/groups/leave.ts b/src/server/api/endpoints/users/groups/leave.ts
deleted file mode 100644
index 0e52f2abdf..0000000000
--- a/src/server/api/endpoints/users/groups/leave.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { UserGroups, UserGroupJoinings } from '@/models/index';
-
-export const meta = {
-	tags: ['groups', 'users'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		groupId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: '62780270-1f67-5dc0-daca-3eb510612e31'
-		},
-
-		youAreOwner: {
-			message: 'Your are the owner.',
-			code: 'YOU_ARE_OWNER',
-			id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the group
-	const userGroup = await UserGroups.findOne({
-		id: ps.groupId,
-	});
-
-	if (userGroup == null) {
-		throw new ApiError(meta.errors.noSuchGroup);
-	}
-
-	if (me.id === userGroup.userId) {
-		throw new ApiError(meta.errors.youAreOwner);
-	}
-
-	await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id });
-});
diff --git a/src/server/api/endpoints/users/groups/owned.ts b/src/server/api/endpoints/users/groups/owned.ts
deleted file mode 100644
index 17de370dbc..0000000000
--- a/src/server/api/endpoints/users/groups/owned.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import define from '../../../define';
-import { UserGroups } from '@/models/index';
-
-export const meta = {
-	tags: ['groups', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:user-groups',
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'UserGroup',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const userGroups = await UserGroups.find({
-		userId: me.id,
-	});
-
-	return await Promise.all(userGroups.map(x => UserGroups.pack(x)));
-});
diff --git a/src/server/api/endpoints/users/groups/pull.ts b/src/server/api/endpoints/users/groups/pull.ts
deleted file mode 100644
index ce4d2e2881..0000000000
--- a/src/server/api/endpoints/users/groups/pull.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { UserGroups, UserGroupJoinings } from '@/models/index';
-
-export const meta = {
-	tags: ['groups', 'users'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		groupId: {
-			validator: $.type(ID),
-		},
-
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: '4662487c-05b1-4b78-86e5-fd46998aba74'
-		},
-
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '0b5cc374-3681-41da-861e-8bc1146f7a55'
-		},
-
-		isOwner: {
-			message: 'The user is the owner.',
-			code: 'IS_OWNER',
-			id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the group
-	const userGroup = await UserGroups.findOne({
-		id: ps.groupId,
-		userId: me.id,
-	});
-
-	if (userGroup == null) {
-		throw new ApiError(meta.errors.noSuchGroup);
-	}
-
-	// Fetch the user
-	const user = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	if (user.id === userGroup.userId) {
-		throw new ApiError(meta.errors.isOwner);
-	}
-
-	// Pull the user
-	await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: user.id });
-});
diff --git a/src/server/api/endpoints/users/groups/show.ts b/src/server/api/endpoints/users/groups/show.ts
deleted file mode 100644
index 3c030bf3a5..0000000000
--- a/src/server/api/endpoints/users/groups/show.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { UserGroups, UserGroupJoinings } from '@/models/index';
-
-export const meta = {
-	tags: ['groups', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:user-groups',
-
-	params: {
-		groupId: {
-			validator: $.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'UserGroup',
-	},
-
-	errors: {
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the group
-	const userGroup = await UserGroups.findOne({
-		id: ps.groupId,
-	});
-
-	if (userGroup == null) {
-		throw new ApiError(meta.errors.noSuchGroup);
-	}
-
-	const joining = await UserGroupJoinings.findOne({
-		userId: me.id,
-		userGroupId: userGroup.id
-	});
-
-	if (joining == null && userGroup.userId !== me.id) {
-		throw new ApiError(meta.errors.noSuchGroup);
-	}
-
-	return await UserGroups.pack(userGroup);
-});
diff --git a/src/server/api/endpoints/users/groups/transfer.ts b/src/server/api/endpoints/users/groups/transfer.ts
deleted file mode 100644
index 17c42e1127..0000000000
--- a/src/server/api/endpoints/users/groups/transfer.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { UserGroups, UserGroupJoinings } from '@/models/index';
-
-export const meta = {
-	tags: ['groups', 'users'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		groupId: {
-			validator: $.type(ID),
-		},
-
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'UserGroup',
-	},
-
-	errors: {
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db'
-		},
-
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9'
-		},
-
-		noSuchGroupMember: {
-			message: 'No such group member.',
-			code: 'NO_SUCH_GROUP_MEMBER',
-			id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the group
-	const userGroup = await UserGroups.findOne({
-		id: ps.groupId,
-		userId: me.id,
-	});
-
-	if (userGroup == null) {
-		throw new ApiError(meta.errors.noSuchGroup);
-	}
-
-	// Fetch the user
-	const user = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	const joining = await UserGroupJoinings.findOne({
-		userGroupId: userGroup.id,
-		userId: user.id
-	});
-
-	if (joining == null) {
-		throw new ApiError(meta.errors.noSuchGroupMember);
-	}
-
-	await UserGroups.update(userGroup.id, {
-		userId: ps.userId
-	});
-
-	return await UserGroups.pack(userGroup.id);
-});
diff --git a/src/server/api/endpoints/users/groups/update.ts b/src/server/api/endpoints/users/groups/update.ts
deleted file mode 100644
index 127bbc47a1..0000000000
--- a/src/server/api/endpoints/users/groups/update.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { UserGroups } from '@/models/index';
-
-export const meta = {
-	tags: ['groups'],
-
-	requireCredential: true as const,
-
-	kind: 'write:user-groups',
-
-	params: {
-		groupId: {
-			validator: $.type(ID),
-		},
-
-		name: {
-			validator: $.str.range(1, 100),
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'UserGroup',
-	},
-
-	errors: {
-		noSuchGroup: {
-			message: 'No such group.',
-			code: 'NO_SUCH_GROUP',
-			id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the group
-	const userGroup = await UserGroups.findOne({
-		id: ps.groupId,
-		userId: me.id
-	});
-
-	if (userGroup == null) {
-		throw new ApiError(meta.errors.noSuchGroup);
-	}
-
-	await UserGroups.update(userGroup.id, {
-		name: ps.name
-	});
-
-	return await UserGroups.pack(userGroup.id);
-});
diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts
deleted file mode 100644
index e0bfe611fc..0000000000
--- a/src/server/api/endpoints/users/lists/create.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import $ from 'cafy';
-import define from '../../../define';
-import { UserLists } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { UserList } from '@/models/entities/user-list';
-
-export const meta = {
-	tags: ['lists'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		name: {
-			validator: $.str.range(1, 100)
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'UserList',
-	},
-};
-
-export default define(meta, async (ps, user) => {
-	const userList = await UserLists.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		name: ps.name,
-	} as UserList).then(x => UserLists.findOneOrFail(x.identifiers[0]));
-
-	return await UserLists.pack(userList);
-});
diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts
deleted file mode 100644
index 5fe3bfb03d..0000000000
--- a/src/server/api/endpoints/users/lists/delete.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { UserLists } from '@/models/index';
-
-export const meta = {
-	tags: ['lists'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		listId: {
-			validator: $.type(ID),
-		}
-	},
-
-	errors: {
-		noSuchList: {
-			message: 'No such list.',
-			code: 'NO_SUCH_LIST',
-			id: '78436795-db79-42f5-b1e2-55ea2cf19166'
-		}
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const userList = await UserLists.findOne({
-		id: ps.listId,
-		userId: user.id
-	});
-
-	if (userList == null) {
-		throw new ApiError(meta.errors.noSuchList);
-	}
-
-	await UserLists.delete(userList.id);
-});
diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts
deleted file mode 100644
index cf0c92bb84..0000000000
--- a/src/server/api/endpoints/users/lists/list.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import define from '../../../define';
-import { UserLists } from '@/models/index';
-
-export const meta = {
-	tags: ['lists', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:account',
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'UserList',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const userLists = await UserLists.find({
-		userId: me.id,
-	});
-
-	return await Promise.all(userLists.map(x => UserLists.pack(x)));
-});
diff --git a/src/server/api/endpoints/users/lists/pull.ts b/src/server/api/endpoints/users/lists/pull.ts
deleted file mode 100644
index d4357fc5e7..0000000000
--- a/src/server/api/endpoints/users/lists/pull.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import { publishUserListStream } from '@/services/stream';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { UserLists, UserListJoinings, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['lists', 'users'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		listId: {
-			validator: $.type(ID),
-		},
-
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchList: {
-			message: 'No such list.',
-			code: 'NO_SUCH_LIST',
-			id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02'
-		},
-
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '588e7f72-c744-4a61-b180-d354e912bda2'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the list
-	const userList = await UserLists.findOne({
-		id: ps.listId,
-		userId: me.id,
-	});
-
-	if (userList == null) {
-		throw new ApiError(meta.errors.noSuchList);
-	}
-
-	// Fetch the user
-	const user = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Pull the user
-	await UserListJoinings.delete({ userListId: userList.id, userId: user.id });
-
-	publishUserListStream(userList.id, 'userRemoved', await Users.pack(user));
-});
diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts
deleted file mode 100644
index 8e21059d3d..0000000000
--- a/src/server/api/endpoints/users/lists/push.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { getUser } from '../../../common/getters';
-import { pushUserToUserList } from '@/services/user-list/push';
-import { UserLists, UserListJoinings, Blockings } from '@/models/index';
-
-export const meta = {
-	tags: ['lists', 'users'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		listId: {
-			validator: $.type(ID),
-		},
-
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchList: {
-			message: 'No such list.',
-			code: 'NO_SUCH_LIST',
-			id: '2214501d-ac96-4049-b717-91e42272a711'
-		},
-
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: 'a89abd3d-f0bc-4cce-beb1-2f446f4f1e6a'
-		},
-
-		alreadyAdded: {
-			message: 'That user has already been added to that list.',
-			code: 'ALREADY_ADDED',
-			id: '1de7c884-1595-49e9-857e-61f12f4d4fc5'
-		},
-
-		youHaveBeenBlocked: {
-			message: 'You cannot push this user because you have been blocked by this user.',
-			code: 'YOU_HAVE_BEEN_BLOCKED',
-			id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the list
-	const userList = await UserLists.findOne({
-		id: ps.listId,
-		userId: me.id,
-	});
-
-	if (userList == null) {
-		throw new ApiError(meta.errors.noSuchList);
-	}
-
-	// Fetch the user
-	const user = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	// Check blocking
-	if (user.id !== me.id) {
-		const block = await Blockings.findOne({
-			blockerId: user.id,
-			blockeeId: me.id,
-		});
-		if (block) {
-			throw new ApiError(meta.errors.youHaveBeenBlocked);
-		}
-	}
-
-	const exist = await UserListJoinings.findOne({
-		userListId: userList.id,
-		userId: user.id
-	});
-
-	if (exist) {
-		throw new ApiError(meta.errors.alreadyAdded);
-	}
-
-	// Push the user
-	await pushUserToUserList(user, userList);
-});
diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts
deleted file mode 100644
index f9a35cdab3..0000000000
--- a/src/server/api/endpoints/users/lists/show.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { UserLists } from '@/models/index';
-
-export const meta = {
-	tags: ['lists', 'account'],
-
-	requireCredential: true as const,
-
-	kind: 'read:account',
-
-	params: {
-		listId: {
-			validator: $.type(ID),
-		},
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'UserList',
-	},
-
-	errors: {
-		noSuchList: {
-			message: 'No such list.',
-			code: 'NO_SUCH_LIST',
-			id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Fetch the list
-	const userList = await UserLists.findOne({
-		id: ps.listId,
-		userId: me.id,
-	});
-
-	if (userList == null) {
-		throw new ApiError(meta.errors.noSuchList);
-	}
-
-	return await UserLists.pack(userList);
-});
diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts
deleted file mode 100644
index 1185af5043..0000000000
--- a/src/server/api/endpoints/users/lists/update.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../../define';
-import { ApiError } from '../../../error';
-import { UserLists } from '@/models/index';
-
-export const meta = {
-	tags: ['lists'],
-
-	requireCredential: true as const,
-
-	kind: 'write:account',
-
-	params: {
-		listId: {
-			validator: $.type(ID),
-		},
-
-		name: {
-			validator: $.str.range(1, 100),
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'UserList',
-	},
-
-	errors: {
-		noSuchList: {
-			message: 'No such list.',
-			code: 'NO_SUCH_LIST',
-			id: '796666fe-3dff-4d39-becb-8a5932c1d5b7'
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	// Fetch the list
-	const userList = await UserLists.findOne({
-		id: ps.listId,
-		userId: user.id
-	});
-
-	if (userList == null) {
-		throw new ApiError(meta.errors.noSuchList);
-	}
-
-	await UserLists.update(userList.id, {
-		name: ps.name
-	});
-
-	return await UserLists.pack(userList.id);
-});
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
deleted file mode 100644
index 0afbad9d04..0000000000
--- a/src/server/api/endpoints/users/notes.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { Notes } from '@/models/index';
-import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
-import { Brackets } from 'typeorm';
-import { generateBlockedUserQuery } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['users', 'notes'],
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-
-		includeReplies: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		sinceDate: {
-			validator: $.optional.num,
-		},
-
-		untilDate: {
-			validator: $.optional.num,
-		},
-
-		includeMyRenotes: {
-			validator: $.optional.bool,
-			default: true,
-		},
-
-		withFiles: {
-			validator: $.optional.bool,
-			default: false,
-		},
-
-		fileType: {
-			validator: $.optional.arr($.str),
-		},
-
-		excludeNsfw: {
-			validator: $.optional.bool,
-			default: false,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'Note',
-		}
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Lookup user
-	const user = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	//#region Construct query
-	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-		.andWhere('note.userId = :userId', { userId: user.id })
-		.innerJoinAndSelect('note.user', 'user')
-		.leftJoinAndSelect('note.reply', 'reply')
-		.leftJoinAndSelect('note.renote', 'renote')
-		.leftJoinAndSelect('reply.user', 'replyUser')
-		.leftJoinAndSelect('renote.user', 'renoteUser');
-
-	generateVisibilityQuery(query, me);
-	if (me) generateMutedUserQuery(query, me, user);
-	if (me) generateBlockedUserQuery(query, me);
-
-	if (ps.withFiles) {
-		query.andWhere('note.fileIds != \'{}\'');
-	}
-
-	if (ps.fileType != null) {
-		query.andWhere('note.fileIds != \'{}\'');
-		query.andWhere(new Brackets(qb => {
-			for (const type of ps.fileType!) {
-				const i = ps.fileType!.indexOf(type);
-				qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
-			}
-		}));
-
-		if (ps.excludeNsfw) {
-			query.andWhere('note.cw IS NULL');
-			query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
-		}
-	}
-
-	if (!ps.includeReplies) {
-		query.andWhere('note.replyId IS NULL');
-	}
-
-	if (ps.includeMyRenotes === false) {
-		query.andWhere(new Brackets(qb => {
-			qb.orWhere('note.userId != :userId', { userId: user.id });
-			qb.orWhere('note.renoteId IS NULL');
-			qb.orWhere('note.text IS NOT NULL');
-			qb.orWhere('note.fileIds != \'{}\'');
-			qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
-		}));
-	}
-
-	//#endregion
-
-	const timeline = await query.take(ps.limit!).getMany();
-
-	return await Notes.packMany(timeline, me);
-});
diff --git a/src/server/api/endpoints/users/pages.ts b/src/server/api/endpoints/users/pages.ts
deleted file mode 100644
index 24e9e207fd..0000000000
--- a/src/server/api/endpoints/users/pages.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { Pages } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-
-export const meta = {
-	tags: ['users', 'pages'],
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-	}
-};
-
-export default define(meta, async (ps, user) => {
-	const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
-		.andWhere(`page.userId = :userId`, { userId: ps.userId })
-		.andWhere('page.visibility = \'public\'');
-
-	const pages = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Pages.packMany(pages);
-});
diff --git a/src/server/api/endpoints/users/reactions.ts b/src/server/api/endpoints/users/reactions.ts
deleted file mode 100644
index fe5e4d84a9..0000000000
--- a/src/server/api/endpoints/users/reactions.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { NoteReactions, UserProfiles } from '@/models/index';
-import { makePaginationQuery } from '../../common/make-pagination-query';
-import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { ApiError } from '../../error';
-
-export const meta = {
-	tags: ['users', 'reactions'],
-
-	requireCredential: false as const,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		sinceId: {
-			validator: $.optional.type(ID),
-		},
-
-		untilId: {
-			validator: $.optional.type(ID),
-		},
-
-		sinceDate: {
-			validator: $.optional.num,
-		},
-
-		untilDate: {
-			validator: $.optional.num,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'NoteReaction',
-		}
-	},
-
-	errors: {
-		reactionsNotPublic: {
-			message: 'Reactions of the user is not public.',
-			code: 'REACTIONS_NOT_PUBLIC',
-			id: '673a7dd2-6924-1093-e0c0-e68456ceae5c'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const profile = await UserProfiles.findOneOrFail(ps.userId);
-
-	if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
-		throw new ApiError(meta.errors.reactionsNotPublic);
-	}
-
-	const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'),
-			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-		.andWhere(`reaction.userId = :userId`, { userId: ps.userId })
-		.leftJoinAndSelect('reaction.note', 'note');
-
-	generateVisibilityQuery(query, me);
-
-	const reactions = await query
-		.take(ps.limit!)
-		.getMany();
-
-	return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true })));
-});
diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
deleted file mode 100644
index dde6bb1037..0000000000
--- a/src/server/api/endpoints/users/recommendation.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import * as ms from 'ms';
-import $ from 'cafy';
-import define from '../../define';
-import { Users, Followings } from '@/models/index';
-import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query';
-import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: true as const,
-
-	kind: 'read:account',
-
-	params: {
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0
-		}
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const query = Users.createQueryBuilder('user')
-		.where('user.isLocked = FALSE')
-		.andWhere('user.isExplorable = TRUE')
-		.andWhere('user.host IS NULL')
-		.andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) })
-		.andWhere('user.id != :meId', { meId: me.id })
-		.orderBy('user.followersCount', 'DESC');
-
-	generateMutedUserQueryForUsers(query, me);
-	generateBlockQueryForUsers(query, me);
-	generateBlockedUserQuery(query, me);
-
-	const followingQuery = Followings.createQueryBuilder('following')
-		.select('following.followeeId')
-		.where('following.followerId = :followerId', { followerId: me.id });
-
-	query
-		.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`);
-
-	query.setParameters(followingQuery.getParameters());
-
-	const users = await query.take(ps.limit!).skip(ps.offset).getMany();
-
-	return await Users.packMany(users, me, { detail: true });
-});
diff --git a/src/server/api/endpoints/users/relation.ts b/src/server/api/endpoints/users/relation.ts
deleted file mode 100644
index 32d76a5322..0000000000
--- a/src/server/api/endpoints/users/relation.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { ID } from '@/misc/cafy-id';
-import { Users } from '@/models/index';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: true as const,
-
-	params: {
-		userId: {
-			validator: $.either($.type(ID), $.arr($.type(ID)).unique()),
-		}
-	},
-
-	res: {
-		oneOf: [
-			{
-				type: 'object' as const,
-				optional: false as const, nullable: false as const,
-				properties: {
-					id: {
-						type: 'string' as const,
-						optional: false as const, nullable: false as const,
-						format: 'id'
-					},
-					isFollowing: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					hasPendingFollowRequestFromYou: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					hasPendingFollowRequestToYou: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					isFollowed: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					isBlocking: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					isBlocked: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					},
-					isMuted: {
-						type: 'boolean' as const,
-						optional: false as const, nullable: false as const
-					}
-				}
-			},
-			{
-				type: 'array' as const,
-				optional: false as const, nullable: false as const,
-				items: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					properties: {
-						id: {
-							type: 'string' as const,
-							optional: false as const, nullable: false as const,
-							format: 'id'
-						},
-						isFollowing: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const
-						},
-						hasPendingFollowRequestFromYou: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const
-						},
-						hasPendingFollowRequestToYou: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const
-						},
-						isFollowed: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const
-						},
-						isBlocking: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const
-						},
-						isBlocked: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const
-						},
-						isMuted: {
-							type: 'boolean' as const,
-							optional: false as const, nullable: false as const
-						}
-					}
-				}
-			}
-		]
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
-
-	const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id)));
-
-	return Array.isArray(ps.userId) ? relations : relations[0];
-});
diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts
deleted file mode 100644
index 2c8672cd47..0000000000
--- a/src/server/api/endpoints/users/report-abuse.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import $ from 'cafy';
-import { ID } from '@/misc/cafy-id';
-import define from '../../define';
-import { publishAdminStream } from '@/services/stream';
-import { ApiError } from '../../error';
-import { getUser } from '../../common/getters';
-import { AbuseUserReports, Users } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: true as const,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-
-		comment: {
-			validator: $.str.range(1, 2048),
-		},
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '1acefcb5-0959-43fd-9685-b48305736cb5'
-		},
-
-		cannotReportYourself: {
-			message: 'Cannot report yourself.',
-			code: 'CANNOT_REPORT_YOURSELF',
-			id: '1e13149e-b1e8-43cf-902e-c01dbfcb202f'
-		},
-
-		cannotReportAdmin: {
-			message: 'Cannot report the admin.',
-			code: 'CANNOT_REPORT_THE_ADMIN',
-			id: '35e166f5-05fb-4f87-a2d5-adb42676d48f'
-		}
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	// Lookup user
-	const user = await getUser(ps.userId).catch(e => {
-		if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
-		throw e;
-	});
-
-	if (user.id === me.id) {
-		throw new ApiError(meta.errors.cannotReportYourself);
-	}
-
-	if (user.isAdmin) {
-		throw new ApiError(meta.errors.cannotReportAdmin);
-	}
-
-	const report = await AbuseUserReports.save({
-		id: genId(),
-		createdAt: new Date(),
-		targetUserId: user.id,
-		targetUserHost: user.host,
-		reporterId: me.id,
-		reporterHost: null,
-		comment: ps.comment,
-	});
-
-	// Publish event to moderators
-	setTimeout(async () => {
-		const moderators = await Users.find({
-			where: [{
-				isAdmin: true
-			}, {
-				isModerator: true
-			}]
-		});
-
-		for (const moderator of moderators) {
-			publishAdminStream(moderator.id, 'newAbuseUserReport', {
-				id: report.id,
-				targetUserId: report.targetUserId,
-				reporterId: report.reporterId,
-				comment: report.comment
-			});
-		}
-	}, 1);
-});
diff --git a/src/server/api/endpoints/users/search-by-username-and-host.ts b/src/server/api/endpoints/users/search-by-username-and-host.ts
deleted file mode 100644
index 1ec5e1a743..0000000000
--- a/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { Followings, Users } from '@/models/index';
-import { Brackets } from 'typeorm';
-import { USER_ACTIVE_THRESHOLD } from '@/const';
-import { User } from '@/models/entities/user';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		username: {
-			validator: $.optional.nullable.str,
-		},
-
-		host: {
-			validator: $.optional.nullable.str,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		detail: {
-			validator: $.optional.bool,
-			default: true,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
-
-	if (ps.host) {
-		const q = Users.createQueryBuilder('user')
-			.where('user.isSuspended = FALSE')
-			.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
-
-		if (ps.username) {
-			q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' });
-		}
-
-		q.andWhere('user.updatedAt IS NOT NULL');
-		q.orderBy('user.updatedAt', 'DESC');
-
-		const users = await q.take(ps.limit!).getMany();
-
-		return await Users.packMany(users, me, { detail: ps.detail });
-	} else if (ps.username) {
-		let users: User[] = [];
-
-		if (me) {
-			const followingQuery = Followings.createQueryBuilder('following')
-				.select('following.followeeId')
-				.where('following.followerId = :followerId', { followerId: me.id });
-
-			const query = Users.createQueryBuilder('user')
-				.where(`user.id IN (${ followingQuery.getQuery() })`)
-				.andWhere(`user.id != :meId`, { meId: me.id })
-				.andWhere('user.isSuspended = FALSE')
-				.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
-				.andWhere(new Brackets(qb => { qb
-					.where('user.updatedAt IS NULL')
-					.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-				}));
-
-			query.setParameters(followingQuery.getParameters());
-
-			users = await query
-				.orderBy('user.usernameLower', 'ASC')
-				.take(ps.limit!)
-				.getMany();
-
-			if (users.length < ps.limit!) {
-				const otherQuery = await Users.createQueryBuilder('user')
-					.where(`user.id NOT IN (${ followingQuery.getQuery() })`)
-					.andWhere(`user.id != :meId`, { meId: me.id })
-					.andWhere('user.isSuspended = FALSE')
-					.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
-					.andWhere('user.updatedAt IS NOT NULL');
-
-				otherQuery.setParameters(followingQuery.getParameters());
-
-				const otherUsers = await otherQuery
-					.orderBy('user.updatedAt', 'DESC')
-					.take(ps.limit! - users.length)
-					.getMany();
-
-				users = users.concat(otherUsers);
-			}
-		} else {
-			users = await Users.createQueryBuilder('user')
-				.where('user.isSuspended = FALSE')
-				.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
-				.andWhere('user.updatedAt IS NOT NULL')
-				.orderBy('user.updatedAt', 'DESC')
-				.take(ps.limit! - users.length)
-				.getMany();
-		}
-
-		return await Users.packMany(users, me, { detail: ps.detail });
-	}
-});
diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts
deleted file mode 100644
index 9aa988d9ed..0000000000
--- a/src/server/api/endpoints/users/search.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { UserProfiles, Users } from '@/models/index';
-import { User } from '@/models/entities/user';
-import { Brackets } from 'typeorm';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		query: {
-			validator: $.str,
-		},
-
-		offset: {
-			validator: $.optional.num.min(0),
-			default: 0,
-		},
-
-		limit: {
-			validator: $.optional.num.range(1, 100),
-			default: 10,
-		},
-
-		origin: {
-			validator: $.optional.str.or(['local', 'remote', 'combined']),
-			default: 'combined',
-		},
-
-		detail: {
-			validator: $.optional.bool,
-			default: true,
-		},
-	},
-
-	res: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			ref: 'User',
-		}
-	},
-};
-
-export default define(meta, async (ps, me) => {
-	const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
-
-	const isUsername = ps.query.startsWith('@');
-
-	let users: User[] = [];
-
-	if (isUsername) {
-		const usernameQuery = Users.createQueryBuilder('user')
-			.where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
-			.andWhere(new Brackets(qb => { qb
-				.where('user.updatedAt IS NULL')
-				.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-			}))
-			.andWhere('user.isSuspended = FALSE');
-
-		if (ps.origin === 'local') {
-			usernameQuery.andWhere('user.host IS NULL');
-		} else if (ps.origin === 'remote') {
-			usernameQuery.andWhere('user.host IS NOT NULL');
-		}
-
-		users = await usernameQuery
-			.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
-			.take(ps.limit!)
-			.skip(ps.offset)
-			.getMany();
-	} else {
-		const nameQuery = Users.createQueryBuilder('user')
-			.where('user.name ILIKE :query', { query: '%' + ps.query + '%' })
-			.andWhere(new Brackets(qb => { qb
-				.where('user.updatedAt IS NULL')
-				.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-			}))
-			.andWhere('user.isSuspended = FALSE');
-
-		if (ps.origin === 'local') {
-			nameQuery.andWhere('user.host IS NULL');
-		} else if (ps.origin === 'remote') {
-			nameQuery.andWhere('user.host IS NOT NULL');
-		}
-
-		users = await nameQuery
-			.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
-			.take(ps.limit!)
-			.skip(ps.offset)
-			.getMany();
-
-		if (users.length < ps.limit!) {
-			const profQuery = UserProfiles.createQueryBuilder('prof')
-				.select('prof.userId')
-				.where('prof.description ILIKE :query', { query: '%' + ps.query + '%' });
-
-			if (ps.origin === 'local') {
-				profQuery.andWhere('prof.userHost IS NULL');
-			} else if (ps.origin === 'remote') {
-				profQuery.andWhere('prof.userHost IS NOT NULL');
-			}
-
-			const query = Users.createQueryBuilder('user')
-				.where(`user.id IN (${ profQuery.getQuery() })`)
-				.andWhere(new Brackets(qb => { qb
-					.where('user.updatedAt IS NULL')
-					.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
-				}))
-				.andWhere('user.isSuspended = FALSE')
-				.setParameters(profQuery.getParameters());
-
-			users = users.concat(await query
-				.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
-				.take(ps.limit!)
-				.skip(ps.offset)
-				.getMany()
-			);
-		}
-	}
-
-	return await Users.packMany(users, me, { detail: ps.detail });
-});
diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts
deleted file mode 100644
index f056983636..0000000000
--- a/src/server/api/endpoints/users/show.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import $ from 'cafy';
-import { resolveUser } from '@/remote/resolve-user';
-import define from '../../define';
-import { apiLogger } from '../../logger';
-import { ApiError } from '../../error';
-import { ID } from '@/misc/cafy-id';
-import { Users } from '@/models/index';
-import { In } from 'typeorm';
-import { User } from '@/models/entities/user';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		userId: {
-			validator: $.optional.type(ID),
-		},
-
-		userIds: {
-			validator: $.optional.arr($.type(ID)).unique(),
-		},
-
-		username: {
-			validator: $.optional.str
-		},
-
-		host: {
-			validator: $.optional.nullable.str
-		}
-	},
-
-	res: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		ref: 'User',
-	},
-
-	errors: {
-		failedToResolveRemoteUser: {
-			message: 'Failed to resolve remote user.',
-			code: 'FAILED_TO_RESOLVE_REMOTE_USER',
-			id: 'ef7b9be4-9cba-4e6f-ab41-90ed171c7d3c',
-			kind: 'server' as const
-		},
-
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '4362f8dc-731f-4ad8-a694-be5a88922a24'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	let user;
-
-	const isAdminOrModerator = me && (me.isAdmin || me.isModerator);
-
-	if (ps.userIds) {
-		if (ps.userIds.length === 0) {
-			return [];
-		}
-
-		const users = await Users.find(isAdminOrModerator ? {
-			id: In(ps.userIds)
-		} : {
-			id: In(ps.userIds),
-			isSuspended: false
-		});
-
-		// リクエストされた通りに並べ替え
-		const _users: User[] = [];
-		for (const id of ps.userIds) {
-			_users.push(users.find(x => x.id === id)!);
-		}
-
-		return await Promise.all(_users.map(u => Users.pack(u, me, {
-			detail: true
-		})));
-	} else {
-		// Lookup user
-		if (typeof ps.host === 'string' && typeof ps.username === 'string') {
-			user = await resolveUser(ps.username, ps.host).catch(e => {
-				apiLogger.warn(`failed to resolve remote user: ${e}`);
-				throw new ApiError(meta.errors.failedToResolveRemoteUser);
-			});
-		} else {
-			const q: any = ps.userId != null
-				? { id: ps.userId }
-				: { usernameLower: ps.username!.toLowerCase(), host: null };
-
-			user = await Users.findOne(q);
-		}
-
-		if (user == null || (!isAdminOrModerator && user.isSuspended)) {
-			throw new ApiError(meta.errors.noSuchUser);
-		}
-
-		return await Users.pack(user, me, {
-			detail: true
-		});
-	}
-});
diff --git a/src/server/api/endpoints/users/stats.ts b/src/server/api/endpoints/users/stats.ts
deleted file mode 100644
index ef8afd5625..0000000000
--- a/src/server/api/endpoints/users/stats.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import $ from 'cafy';
-import define from '../../define';
-import { ApiError } from '../../error';
-import { ID } from '@/misc/cafy-id';
-import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, ReversiGames, Users } from '@/models/index';
-
-export const meta = {
-	tags: ['users'],
-
-	requireCredential: false as const,
-
-	params: {
-		userId: {
-			validator: $.type(ID),
-		},
-	},
-
-	errors: {
-		noSuchUser: {
-			message: 'No such user.',
-			code: 'NO_SUCH_USER',
-			id: '9e638e45-3b25-4ef7-8f95-07e8498f1819'
-		},
-	}
-};
-
-export default define(meta, async (ps, me) => {
-	const user = await Users.findOne(ps.userId);
-	if (user == null) {
-		throw new ApiError(meta.errors.noSuchUser);
-	}
-
-	const [
-		notesCount,
-		repliesCount,
-		renotesCount,
-		repliedCount,
-		renotedCount,
-		pollVotesCount,
-		pollVotedCount,
-		localFollowingCount,
-		remoteFollowingCount,
-		localFollowersCount,
-		remoteFollowersCount,
-		sentReactionsCount,
-		receivedReactionsCount,
-		noteFavoritesCount,
-		pageLikesCount,
-		pageLikedCount,
-		driveFilesCount,
-		driveUsage,
-		reversiCount,
-	] = await Promise.all([
-		Notes.createQueryBuilder('note')
-			.where('note.userId = :userId', { userId: user.id })
-			.getCount(),
-		Notes.createQueryBuilder('note')
-			.where('note.userId = :userId', { userId: user.id })
-			.andWhere('note.replyId IS NOT NULL')
-			.getCount(),
-		Notes.createQueryBuilder('note')
-			.where('note.userId = :userId', { userId: user.id })
-			.andWhere('note.renoteId IS NOT NULL')
-			.getCount(),
-		Notes.createQueryBuilder('note')
-			.where('note.replyUserId = :userId', { userId: user.id })
-			.getCount(),
-		Notes.createQueryBuilder('note')
-			.where('note.renoteUserId = :userId', { userId: user.id })
-			.getCount(),
-		PollVotes.createQueryBuilder('vote')
-			.where('vote.userId = :userId', { userId: user.id })
-			.getCount(),
-		PollVotes.createQueryBuilder('vote')
-			.innerJoin('vote.note', 'note')
-			.where('note.userId = :userId', { userId: user.id })
-			.getCount(),
-		Followings.createQueryBuilder('following')
-			.where('following.followerId = :userId', { userId: user.id })
-			.andWhere('following.followeeHost IS NULL')
-			.getCount(),
-		Followings.createQueryBuilder('following')
-			.where('following.followerId = :userId', { userId: user.id })
-			.andWhere('following.followeeHost IS NOT NULL')
-			.getCount(),
-		Followings.createQueryBuilder('following')
-			.where('following.followeeId = :userId', { userId: user.id })
-			.andWhere('following.followerHost IS NULL')
-			.getCount(),
-		Followings.createQueryBuilder('following')
-			.where('following.followeeId = :userId', { userId: user.id })
-			.andWhere('following.followerHost IS NOT NULL')
-			.getCount(),
-		NoteReactions.createQueryBuilder('reaction')
-			.where('reaction.userId = :userId', { userId: user.id })
-			.getCount(),
-		NoteReactions.createQueryBuilder('reaction')
-			.innerJoin('reaction.note', 'note')
-			.where('note.userId = :userId', { userId: user.id })
-			.getCount(),
-		NoteFavorites.createQueryBuilder('favorite')
-			.where('favorite.userId = :userId', { userId: user.id })
-			.getCount(),
-		PageLikes.createQueryBuilder('like')
-			.where('like.userId = :userId', { userId: user.id })
-			.getCount(),
-		PageLikes.createQueryBuilder('like')
-			.innerJoin('like.page', 'page')
-			.where('page.userId = :userId', { userId: user.id })
-			.getCount(),
-		DriveFiles.createQueryBuilder('file')
-			.where('file.userId = :userId', { userId: user.id })
-			.getCount(),
-		DriveFiles.calcDriveUsageOf(user),
-		ReversiGames.createQueryBuilder('game')
-			.where('game.user1Id = :userId', { userId: user.id })
-			.orWhere('game.user2Id = :userId', { userId: user.id })
-			.getCount(),
-	]);
-
-	return {
-		notesCount,
-		repliesCount,
-		renotesCount,
-		repliedCount,
-		renotedCount,
-		pollVotesCount,
-		pollVotedCount,
-		localFollowingCount,
-		remoteFollowingCount,
-		localFollowersCount,
-		remoteFollowersCount,
-		followingCount: localFollowingCount + remoteFollowingCount,
-		followersCount: localFollowersCount + remoteFollowersCount,
-		sentReactionsCount,
-		receivedReactionsCount,
-		noteFavoritesCount,
-		pageLikesCount,
-		pageLikedCount,
-		driveFilesCount,
-		driveUsage,
-		reversiCount,
-	};
-});
diff --git a/src/server/api/error.ts b/src/server/api/error.ts
deleted file mode 100644
index cb0bdc9f47..0000000000
--- a/src/server/api/error.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-type E = { message: string, code: string, id: string, kind?: 'client' | 'server', httpStatusCode?: number };
-
-export class ApiError extends Error {
-	public message: string;
-	public code: string;
-	public id: string;
-	public kind: string;
-	public httpStatusCode?: number;
-	public info?: any;
-
-	constructor(e?: E | null | undefined, info?: any | null | undefined) {
-		if (e == null) e = {
-			message: 'Internal error occurred. Please contact us if the error persists.',
-			code: 'INTERNAL_ERROR',
-			id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
-			kind: 'server',
-			httpStatusCode: 500
-		};
-
-		super(e.message);
-		this.message = e.message;
-		this.code = e.code;
-		this.id = e.id;
-		this.kind = e.kind || 'client';
-		this.httpStatusCode = e.httpStatusCode;
-		this.info = info;
-	}
-}
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
deleted file mode 100644
index 82579075eb..0000000000
--- a/src/server/api/index.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * API Server
- */
-
-import * as Koa from 'koa';
-import * as Router from '@koa/router';
-import * as multer from '@koa/multer';
-import * as bodyParser from 'koa-bodyparser';
-import * as cors from '@koa/cors';
-
-import endpoints from './endpoints';
-import handler from './api-handler';
-import signup from './private/signup';
-import signin from './private/signin';
-import signupPending from './private/signup-pending';
-import discord from './service/discord';
-import github from './service/github';
-import twitter from './service/twitter';
-import { Instances, AccessTokens, Users } from '@/models/index';
-import config from '@/config';
-
-// Init app
-const app = new Koa();
-
-app.use(cors({
-	origin: '*'
-}));
-
-// No caching
-app.use(async (ctx, next) => {
-	ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
-	await next();
-});
-
-app.use(bodyParser({
-	// リクエストが multipart/form-data でない限りはJSONだと見なす
-	detectJSON: ctx => !ctx.is('multipart/form-data')
-}));
-
-// Init multer instance
-const upload = multer({
-	storage: multer.diskStorage({}),
-	limits: {
-		fileSize: config.maxFileSize || 262144000,
-		files: 1,
-	}
-});
-
-// Init router
-const router = new Router();
-
-/**
- * Register endpoint handlers
- */
-for (const endpoint of endpoints) {
-	if (endpoint.meta.requireFile) {
-		router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint));
-	} else {
-		if (endpoint.name.includes('-')) {
-			// 後方互換性のため
-			router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint));
-		}
-		router.post(`/${endpoint.name}`, handler.bind(null, endpoint));
-	}
-}
-
-router.post('/signup', signup);
-router.post('/signin', signin);
-router.post('/signup-pending', signupPending);
-
-router.use(discord.routes());
-router.use(github.routes());
-router.use(twitter.routes());
-
-router.get('/v1/instance/peers', async ctx => {
-	const instances = await Instances.find({
-		select: ['host']
-	});
-
-	ctx.body = instances.map(instance => instance.host);
-});
-
-router.post('/miauth/:session/check', async ctx => {
-	const token = await AccessTokens.findOne({
-		session: ctx.params.session
-	});
-
-	if (token && token.session != null && !token.fetched) {
-		AccessTokens.update(token.id, {
-			fetched: true
-		});
-
-		ctx.body = {
-			ok: true,
-			token: token.token,
-			user: await Users.pack(token.userId, null, { detail: true })
-		};
-	} else {
-		ctx.body = {
-			ok: false,
-		};
-	}
-});
-
-// Return 404 for unknown API
-router.all('(.*)', async ctx => {
-	ctx.status = 404;
-});
-
-// Register router
-app.use(router.routes());
-
-export default app;
diff --git a/src/server/api/limiter.ts b/src/server/api/limiter.ts
deleted file mode 100644
index e677aad0b6..0000000000
--- a/src/server/api/limiter.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import * as Limiter from 'ratelimiter';
-import { redisClient } from '../../db/redis';
-import { IEndpoint } from './endpoints';
-import { getAcct } from '@/misc/acct';
-import { User } from '@/models/entities/user';
-import Logger from '@/services/logger';
-
-const logger = new Logger('limiter');
-
-export default (endpoint: IEndpoint, user: User) => new Promise<void>((ok, reject) => {
-	const limitation = endpoint.meta.limit!;
-
-	const key = limitation.hasOwnProperty('key')
-		? limitation.key
-		: endpoint.name;
-
-	const hasShortTermLimit =
-		limitation.hasOwnProperty('minInterval');
-
-	const hasLongTermLimit =
-		limitation.hasOwnProperty('duration') &&
-		limitation.hasOwnProperty('max');
-
-	if (hasShortTermLimit) {
-		min();
-	} else if (hasLongTermLimit) {
-		max();
-	} else {
-		ok();
-	}
-
-	// Short-term limit
-	function min() {
-		const minIntervalLimiter = new Limiter({
-			id: `${user.id}:${key}:min`,
-			duration: limitation.minInterval,
-			max: 1,
-			db: redisClient
-		});
-
-		minIntervalLimiter.get((err, info) => {
-			if (err) {
-				return reject('ERR');
-			}
-
-			logger.debug(`@${getAcct(user)} ${endpoint.name} min remaining: ${info.remaining}`);
-
-			if (info.remaining === 0) {
-				reject('BRIEF_REQUEST_INTERVAL');
-			} else {
-				if (hasLongTermLimit) {
-					max();
-				} else {
-					ok();
-				}
-			}
-		});
-	}
-
-	// Long term limit
-	function max() {
-		const limiter = new Limiter({
-			id: `${user.id}:${key}`,
-			duration: limitation.duration,
-			max: limitation.max,
-			db: redisClient
-		});
-
-		limiter.get((err, info) => {
-			if (err) {
-				return reject('ERR');
-			}
-
-			logger.debug(`@${getAcct(user)} ${endpoint.name} max remaining: ${info.remaining}`);
-
-			if (info.remaining === 0) {
-				reject('RATE_LIMIT_EXCEEDED');
-			} else {
-				ok();
-			}
-		});
-	}
-});
diff --git a/src/server/api/logger.ts b/src/server/api/logger.ts
deleted file mode 100644
index 750defe547..0000000000
--- a/src/server/api/logger.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Logger from '@/services/logger';
-
-export const apiLogger = new Logger('api');
diff --git a/src/server/api/openapi/description.ts b/src/server/api/openapi/description.ts
deleted file mode 100644
index e51b312259..0000000000
--- a/src/server/api/openapi/description.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import endpoints from '../endpoints';
-import * as locale from '../../../../locales/index';
-import { kinds as kindsList } from '@/misc/api-permissions';
-
-export interface IKindInfo {
-	endpoints: string[];
-	descs: { [x: string]: string; };
-}
-
-export function kinds() {
-	const kinds = Object.fromEntries(
-		kindsList
-			.map(k => [k, {
-					endpoints: [],
-					descs: Object.fromEntries(
-						Object.keys(locale)
-							.map(l => [l, locale[l]._permissions[k] as string])
-						)
-				} as IKindInfo])
-			);
-
-	const errors = [] as string[][];
-
-	for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
-		if (endpoint.meta.kind) {
-			const kind = endpoint.meta.kind;
-			if (kind in kinds) kinds[kind].endpoints.push(endpoint.name);
-			else errors.push([kind, endpoint.name]);
-		}
-	}
-
-	if (errors.length > 0) throw Error('\n  ' + errors.map((e) => `Unknown kind (permission) "${e[0]}" found at ${e[1]}.`).join('\n  '));
-
-	return kinds;
-}
-
-export function getDescription(lang = 'ja-JP'): string {
-	const permissionTable = Object.entries(kinds())
-		.map(e => `|${e[0]}|${e[1].descs[lang]}|${e[1].endpoints.map(f => `[${f}](#operation/${f})`).join(', ')}|`)
-		.join('\n');
-
-	const descriptions: { [x: string]: string } = {
-		'ja-JP': `
-# Permissions
-|Permisson (kind)|Description|Endpoints|
-|:--|:--|:--|
-${permissionTable}
-`
-	};
-	return lang in descriptions ? descriptions[lang] : descriptions['ja-JP'];
-}
diff --git a/src/server/api/openapi/errors.ts b/src/server/api/openapi/errors.ts
deleted file mode 100644
index 43bcc323ba..0000000000
--- a/src/server/api/openapi/errors.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-
-export const errors = {
-	'400': {
-		'INVALID_PARAM': {
-			value: {
-				error: {
-					message: 'Invalid param.',
-					code: 'INVALID_PARAM',
-					id: '3d81ceae-475f-4600-b2a8-2bc116157532',
-				}
-			}
-		}
-	},
-	'401': {
-		'CREDENTIAL_REQUIRED': {
-			value: {
-				error: {
-					message: 'Credential required.',
-					code: 'CREDENTIAL_REQUIRED',
-					id: '1384574d-a912-4b81-8601-c7b1c4085df1',
-				}
-			}
-		}
-	},
-	'403': {
-		'AUTHENTICATION_FAILED': {
-			value: {
-				error: {
-					message: 'Authentication failed. Please ensure your token is correct.',
-					code: 'AUTHENTICATION_FAILED',
-					id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
-				}
-			}
-		}
-	},
-	'418': {
-		'I_AM_AI': {
-			value: {
-				error: {
-					message: 'You sent a request to Ai-chan, Misskey\'s showgirl, instead of the server.',
-					code: 'I_AM_AI',
-					id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84',
-				}
-			}
-		}
-	},
-	'429': {
-		'RATE_LIMIT_EXCEEDED': {
-			value: {
-				error: {
-					message: 'Rate limit exceeded. Please try again later.',
-					code: 'RATE_LIMIT_EXCEEDED',
-					id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
-				}
-			}
-		}
-	},
-	'500': {
-		'INTERNAL_ERROR': {
-			value: {
-				error: {
-					message: 'Internal error occurred. Please contact us if the error persists.',
-					code: 'INTERNAL_ERROR',
-					id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
-				}
-			}
-		}
-	}
-};
diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts
deleted file mode 100644
index 9db47c6dfc..0000000000
--- a/src/server/api/openapi/gen-spec.ts
+++ /dev/null
@@ -1,237 +0,0 @@
-import endpoints from '../endpoints';
-import { Context } from 'cafy';
-import config from '@/config/index';
-import { errors as basicErrors } from './errors';
-import { schemas, convertSchemaToOpenApiSchema } from './schemas';
-import { getDescription } from './description';
-
-export function genOpenapiSpec(lang = 'ja-JP') {
-	const spec = {
-		openapi: '3.0.0',
-
-		info: {
-			version: 'v1',
-			title: 'Misskey API',
-			description: getDescription(lang),
-			'x-logo': { url: '/static-assets/api-doc.png' }
-		},
-
-		externalDocs: {
-			description: 'Repository',
-			url: 'https://github.com/misskey-dev/misskey'
-		},
-
-		servers: [{
-			url: config.apiUrl
-		}],
-
-		paths: {} as any,
-
-		components: {
-			schemas: schemas,
-
-			securitySchemes: {
-				ApiKeyAuth: {
-					type: 'apiKey',
-					in: 'body',
-					name: 'i'
-				}
-			}
-		}
-	};
-
-	function genProps(props: { [key: string]: Context; }) {
-		const properties = {} as any;
-
-		for (const [k, v] of Object.entries(props)) {
-			properties[k] = genProp(v);
-		}
-
-		return properties;
-	}
-
-	function genProp(param: Context): any {
-		const required = param.name === 'Object' ? (param as any).props ? Object.entries((param as any).props).filter(([k, v]: any) => !v.isOptional).map(([k, v]) => k) : [] : [];
-		return {
-			description: (param.data || {}).desc,
-			default: (param.data || {}).default,
-			deprecated: (param.data || {}).deprecated,
-			...((param.data || {}).default ? { default: (param.data || {}).default } : {}),
-			type: param.name === 'ID' ? 'string' : param.name.toLowerCase(),
-			...(param.name === 'ID' ? { example: 'xxxxxxxxxx', format: 'id' } : {}),
-			nullable: param.isNullable,
-			...(param.name === 'String' ? {
-				...((param as any).enum ? { enum: (param as any).enum } : {}),
-				...((param as any).minLength ? { minLength: (param as any).minLength } : {}),
-				...((param as any).maxLength ? { maxLength: (param as any).maxLength } : {}),
-			} : {}),
-			...(param.name === 'Number' ? {
-				...((param as any).minimum ? { minimum: (param as any).minimum } : {}),
-				...((param as any).maximum ? { maximum: (param as any).maximum } : {}),
-			} : {}),
-			...(param.name === 'Object' ? {
-				...(required.length > 0 ? { required } : {}),
-				properties: (param as any).props ? genProps((param as any).props) : {}
-			} : {}),
-			...(param.name === 'Array' ? {
-				items: (param as any).ctx ? genProp((param as any).ctx) : {}
-			} : {})
-		};
-	}
-
-	for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
-		const porops = {} as any;
-		const errors = {} as any;
-
-		if (endpoint.meta.errors) {
-			for (const e of Object.values(endpoint.meta.errors)) {
-				errors[e.code] = {
-					value: {
-						error: e
-					}
-				};
-			}
-		}
-
-		if (endpoint.meta.params) {
-			for (const [k, v] of Object.entries(endpoint.meta.params)) {
-				if (v.validator.data == null) v.validator.data = {};
-				if (v.desc) v.validator.data.desc = v.desc[lang];
-				if (v.deprecated) v.validator.data.deprecated = v.deprecated;
-				if (v.default) v.validator.data.default = v.default;
-				porops[k] = v.validator;
-			}
-		}
-
-		const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : [];
-
-		const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
-
-		let desc = (endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.') + '\n\n';
-		desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`;
-		if (endpoint.meta.kind) {
-			const kind = endpoint.meta.kind;
-			desc += ` / **Permission**: *${kind}*`;
-		}
-
-		const info = {
-			operationId: endpoint.name,
-			summary: endpoint.name,
-			description: desc,
-			externalDocs: {
-				description: 'Source code',
-				url: `https://github.com/misskey-dev/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts`
-			},
-			...(endpoint.meta.tags ? {
-				tags: [endpoint.meta.tags[0]]
-			} : {}),
-			...(endpoint.meta.requireCredential ? {
-				security: [{
-					ApiKeyAuth: []
-				}]
-			} : {}),
-			requestBody: {
-				required: true,
-				content: {
-					'application/json': {
-						schema: {
-							type: 'object',
-							...(required.length > 0 ? { required } : {}),
-							properties: endpoint.meta.params ? genProps(porops) : {}
-						}
-					}
-				}
-			},
-			responses: {
-				...(endpoint.meta.res ? {
-					'200': {
-						description: 'OK (with results)',
-						content: {
-							'application/json': {
-								schema: resSchema
-							}
-						}
-					}
-				} : {
-					'204': {
-						description: 'OK (without any results)',
-					}
-				}),
-				'400': {
-					description: 'Client error',
-					content: {
-						'application/json': {
-							schema: {
-								$ref: '#/components/schemas/Error'
-							},
-							examples: { ...errors, ...basicErrors['400'] }
-						}
-					}
-				},
-				'401': {
-					description: 'Authentication error',
-					content: {
-						'application/json': {
-							schema: {
-								$ref: '#/components/schemas/Error'
-							},
-							examples: basicErrors['401']
-						}
-					}
-				},
-				'403': {
-					description: 'Forbidden error',
-					content: {
-						'application/json': {
-							schema: {
-								$ref: '#/components/schemas/Error'
-							},
-							examples: basicErrors['403']
-						}
-					}
-				},
-				'418': {
-					description: 'I\'m Ai',
-					content: {
-						'application/json': {
-							schema: {
-								$ref: '#/components/schemas/Error'
-							},
-							examples: basicErrors['418']
-						}
-					}
-				},
-				...(endpoint.meta.limit ? {
-					'429': {
-						description: 'To many requests',
-						content: {
-							'application/json': {
-								schema: {
-									$ref: '#/components/schemas/Error'
-								},
-								examples: basicErrors['429']
-							}
-						}
-					}
-				} : {}),
-				'500': {
-					description: 'Internal server error',
-					content: {
-						'application/json': {
-							schema: {
-								$ref: '#/components/schemas/Error'
-							},
-							examples: basicErrors['500']
-						}
-					}
-				},
-			}
-		};
-
-		spec.paths['/' + endpoint.name] = {
-			post: info
-		};
-	}
-
-	return spec;
-}
diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts
deleted file mode 100644
index 12fc207c47..0000000000
--- a/src/server/api/openapi/schemas.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { refs, Schema } from '@/misc/schema';
-
-export function convertSchemaToOpenApiSchema(schema: Schema) {
-	const res: any = schema;
-
-	if (schema.type === 'object' && schema.properties) {
-		res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
-
-		for (const k of Object.keys(schema.properties)) {
-			res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
-		}
-	}
-
-	if (schema.type === 'array' && schema.items) {
-		res.items = convertSchemaToOpenApiSchema(schema.items);
-	}
-
-	if (schema.ref) {
-		res.$ref = `#/components/schemas/${schema.ref}`;
-	}
-
-	return res;
-}
-
-export const schemas = {
-	Error: {
-		type: 'object',
-		properties: {
-			error: {
-				type: 'object',
-				description: 'An error object.',
-				properties: {
-					code: {
-						type: 'string',
-						description: 'An error code. Unique within the endpoint.',
-					},
-					message: {
-						type: 'string',
-						description: 'An error message.',
-					},
-					id: {
-						type: 'string',
-						format: 'uuid',
-						description: 'An error ID. This ID is static.',
-					}
-				},
-				required: ['code', 'id', 'message']
-			},
-		},
-		required: ['error']
-	},
-
-	...Object.fromEntries(
-		Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)])
-	),
-};
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
deleted file mode 100644
index 83c3dfee94..0000000000
--- a/src/server/api/private/signin.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import * as Koa from 'koa';
-import * as bcrypt from 'bcryptjs';
-import * as speakeasy from 'speakeasy';
-import signin from '../common/signin';
-import config from '@/config/index';
-import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index';
-import { ILocalUser } from '@/models/entities/user';
-import { genId } from '@/misc/gen-id';
-import { verifyLogin, hash } from '../2fa';
-import { randomBytes } from 'crypto';
-
-export default async (ctx: Koa.Context) => {
-	ctx.set('Access-Control-Allow-Origin', config.url);
-	ctx.set('Access-Control-Allow-Credentials', 'true');
-
-	const body = ctx.request.body as any;
-	const username = body['username'];
-	const password = body['password'];
-	const token = body['token'];
-
-	function error(status: number, error: { id: string }) {
-		ctx.status = status;
-		ctx.body = { error };
-	}
-
-	if (typeof username != 'string') {
-		ctx.status = 400;
-		return;
-	}
-
-	if (typeof password != 'string') {
-		ctx.status = 400;
-		return;
-	}
-
-	if (token != null && typeof token != 'string') {
-		ctx.status = 400;
-		return;
-	}
-
-	// Fetch user
-	const user = await Users.findOne({
-		usernameLower: username.toLowerCase(),
-		host: null
-	}) as ILocalUser;
-
-	if (user == null) {
-		error(404, {
-			id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
-		});
-		return;
-	}
-
-	if (user.isSuspended) {
-		error(403, {
-			id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
-		});
-		return;
-	}
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	// Compare password
-	const same = await bcrypt.compare(password, profile.password!);
-
-	async function fail(status?: number, failure?: { id: string }) {
-		// Append signin history
-		await Signins.insert({
-			id: genId(),
-			createdAt: new Date(),
-			userId: user.id,
-			ip: ctx.ip,
-			headers: ctx.headers,
-			success: false
-		});
-
-		error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
-	}
-
-	if (!profile.twoFactorEnabled) {
-		if (same) {
-			signin(ctx, user);
-			return;
-		} else {
-			await fail(403, {
-				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
-			});
-			return;
-		}
-	}
-
-	if (token) {
-		if (!same) {
-			await fail(403, {
-				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
-			});
-			return;
-		}
-
-		const verified = (speakeasy as any).totp.verify({
-			secret: profile.twoFactorSecret,
-			encoding: 'base32',
-			token: token,
-			window: 2
-		});
-
-		if (verified) {
-			signin(ctx, user);
-			return;
-		} else {
-			await fail(403, {
-				id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f'
-			});
-			return;
-		}
-	} else if (body.credentialId) {
-		if (!same && !profile.usePasswordLessLogin) {
-			await fail(403, {
-				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
-			});
-			return;
-		}
-
-		const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
-		const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
-		const challenge = await AttestationChallenges.findOne({
-			userId: user.id,
-			id: body.challengeId,
-			registrationChallenge: false,
-			challenge: hash(clientData.challenge).toString('hex')
-		});
-
-		if (!challenge) {
-			await fail(403, {
-				id: '2715a88a-2125-4013-932f-aa6fe72792da'
-			});
-			return;
-		}
-
-		await AttestationChallenges.delete({
-			userId: user.id,
-			id: body.challengeId
-		});
-
-		if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
-			await fail(403, {
-				id: '2715a88a-2125-4013-932f-aa6fe72792da'
-			});
-			return;
-		}
-
-		const securityKey = await UserSecurityKeys.findOne({
-			id: Buffer.from(
-				body.credentialId
-					.replace(/-/g, '+')
-					.replace(/_/g, '/'),
-					'base64'
-			).toString('hex')
-		});
-
-		if (!securityKey) {
-			await fail(403, {
-				id: '66269679-aeaf-4474-862b-eb761197e046'
-			});
-			return;
-		}
-
-		const isValid = verifyLogin({
-			publicKey: Buffer.from(securityKey.publicKey, 'hex'),
-			authenticatorData: Buffer.from(body.authenticatorData, 'hex'),
-			clientDataJSON,
-			clientData,
-			signature: Buffer.from(body.signature, 'hex'),
-			challenge: challenge.challenge
-		});
-
-		if (isValid) {
-			signin(ctx, user);
-			return;
-		} else {
-			await fail(403, {
-				id: '93b86c4b-72f9-40eb-9815-798928603d1e'
-			});
-			return;
-		}
-	} else {
-		if (!same && !profile.usePasswordLessLogin) {
-			await fail(403, {
-				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
-			});
-			return;
-		}
-
-		const keys = await UserSecurityKeys.find({
-			userId: user.id
-		});
-
-		if (keys.length === 0) {
-			await fail(403, {
-				id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4'
-			});
-			return;
-		}
-
-		// 32 byte challenge
-		const challenge = randomBytes(32).toString('base64')
-			.replace(/=/g, '')
-			.replace(/\+/g, '-')
-			.replace(/\//g, '_');
-
-		const challengeId = genId();
-
-		await AttestationChallenges.insert({
-			userId: user.id,
-			id: challengeId,
-			challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
-			createdAt: new Date(),
-			registrationChallenge: false
-		});
-
-		ctx.body = {
-			challenge,
-			challengeId,
-			securityKeys: keys.map(key => ({
-				id: key.id
-			}))
-		};
-		ctx.status = 200;
-		return;
-	}
-	// never get here
-};
diff --git a/src/server/api/private/signup-pending.ts b/src/server/api/private/signup-pending.ts
deleted file mode 100644
index c0638a1cda..0000000000
--- a/src/server/api/private/signup-pending.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as Koa from 'koa';
-import { Users, UserPendings, UserProfiles } from '@/models/index';
-import { signup } from '../common/signup';
-import signin from '../common/signin';
-
-export default async (ctx: Koa.Context) => {
-	const body = ctx.request.body;
-
-	const code = body['code'];
-
-	try {
-		const pendingUser = await UserPendings.findOneOrFail({ code });
-
-		const { account, secret } = await signup({
-			username: pendingUser.username,
-			passwordHash: pendingUser.password,
-		});
-
-		UserPendings.delete({
-			id: pendingUser.id,
-		});
-
-		const profile = await UserProfiles.findOneOrFail(account.id);
-
-		await UserProfiles.update({ userId: profile.userId }, {
-			email: pendingUser.email,
-			emailVerified: true,
-			emailVerifyCode: null,
-		});
-
-		signin(ctx, account);
-	} catch (e) {
-		ctx.throw(400, e);
-	}
-};
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
deleted file mode 100644
index 2b6a3eb00c..0000000000
--- a/src/server/api/private/signup.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import * as Koa from 'koa';
-import rndstr from 'rndstr';
-import * as bcrypt from 'bcryptjs';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha';
-import { Users, RegistrationTickets, UserPendings } from '@/models/index';
-import { signup } from '../common/signup';
-import config from '@/config';
-import { sendEmail } from '@/services/send-email';
-import { genId } from '@/misc/gen-id';
-import { validateEmailForAccount } from '@/services/validate-email-for-account';
-
-export default async (ctx: Koa.Context) => {
-	const body = ctx.request.body;
-
-	const instance = await fetchMeta(true);
-
-	// Verify *Captcha
-	// ただしテスト時はこの機構は障害となるため無効にする
-	if (process.env.NODE_ENV !== 'test') {
-		if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
-			await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
-				ctx.throw(400, e);
-			});
-		}
-
-		if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
-			await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
-				ctx.throw(400, e);
-			});
-		}
-	}
-
-	const username = body['username'];
-	const password = body['password'];
-	const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null;
-	const invitationCode = body['invitationCode'];
-	const emailAddress = body['emailAddress'];
-
-	if (instance.emailRequiredForSignup) {
-		if (emailAddress == null || typeof emailAddress != 'string') {
-			ctx.status = 400;
-			return;
-		}
-
-		const available = await validateEmailForAccount(emailAddress);
-		if (!available) {
-			ctx.status = 400;
-			return;
-		}
-	}
-
-	if (instance.disableRegistration) {
-		if (invitationCode == null || typeof invitationCode != 'string') {
-			ctx.status = 400;
-			return;
-		}
-
-		const ticket = await RegistrationTickets.findOne({
-			code: invitationCode
-		});
-
-		if (ticket == null) {
-			ctx.status = 400;
-			return;
-		}
-
-		RegistrationTickets.delete(ticket.id);
-	}
-
-	if (instance.emailRequiredForSignup) {
-		const code = rndstr('a-z0-9', 16);
-
-		// Generate hash of password
-		const salt = await bcrypt.genSalt(8);
-		const hash = await bcrypt.hash(password, salt);
-
-		await UserPendings.insert({
-			id: genId(),
-			createdAt: new Date(),
-			code,
-			email: emailAddress,
-			username: username,
-			password: hash,
-		});
-
-		const link = `${config.url}/signup-complete/${code}`;
-
-		sendEmail(emailAddress, 'Signup',
-			`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
-			`To complete signup, please click this link: ${link}`);
-
-		ctx.status = 204;
-	} else {
-		try {
-			const { account, secret } = await signup({
-				username, password, host
-			});
-
-			const res = await Users.pack(account, account, {
-				detail: true,
-				includeSecrets: true
-			});
-
-			(res as any).token = secret;
-
-			ctx.body = res;
-		} catch (e) {
-			ctx.throw(400, e);
-		}
-	}
-};
diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts
deleted file mode 100644
index dd52a23376..0000000000
--- a/src/server/api/service/discord.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-import * as Koa from 'koa';
-import * as Router from '@koa/router';
-import { getJson } from '@/misc/fetch';
-import { OAuth2 } from 'oauth';
-import config from '@/config/index';
-import { publishMainStream } from '@/services/stream';
-import { redisClient } from '../../../db/redis';
-import { v4 as uuid } from 'uuid';
-import signin from '../common/signin';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Users, UserProfiles } from '@/models/index';
-import { ILocalUser } from '@/models/entities/user';
-
-function getUserToken(ctx: Koa.Context) {
-	return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
-}
-
-function compareOrigin(ctx: Koa.Context) {
-	function normalizeUrl(url: string) {
-		return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
-	}
-
-	const referer = ctx.headers['referer'];
-
-	return (normalizeUrl(referer) == normalizeUrl(config.url));
-}
-
-// Init router
-const router = new Router();
-
-router.get('/disconnect/discord', async ctx => {
-	if (!compareOrigin(ctx)) {
-		ctx.throw(400, 'invalid origin');
-		return;
-	}
-
-	const userToken = getUserToken(ctx);
-	if (!userToken) {
-		ctx.throw(400, 'signin required');
-		return;
-	}
-
-	const user = await Users.findOneOrFail({
-		host: null,
-		token: userToken
-	});
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	delete profile.integrations.discord;
-
-	await UserProfiles.update(user.id, {
-		integrations: profile.integrations,
-	});
-
-	ctx.body = `Discordの連携を解除しました :v:`;
-
-	// Publish i updated event
-	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
-		detail: true,
-		includeSecrets: true
-	}));
-});
-
-async function getOAuth2() {
-	const meta = await fetchMeta(true);
-
-	if (meta.enableDiscordIntegration) {
-		return new OAuth2(
-			meta.discordClientId!,
-			meta.discordClientSecret!,
-			'https://discord.com/',
-			'api/oauth2/authorize',
-			'api/oauth2/token');
-	} else {
-		return null;
-	}
-}
-
-router.get('/connect/discord', async ctx => {
-	if (!compareOrigin(ctx)) {
-		ctx.throw(400, 'invalid origin');
-		return;
-	}
-
-	const userToken = getUserToken(ctx);
-	if (!userToken) {
-		ctx.throw(400, 'signin required');
-		return;
-	}
-
-	const params = {
-		redirect_uri: `${config.url}/api/dc/cb`,
-		scope: ['identify'],
-		state: uuid(),
-		response_type: 'code'
-	};
-
-	redisClient.set(userToken, JSON.stringify(params));
-
-	const oauth2 = await getOAuth2();
-	ctx.redirect(oauth2!.getAuthorizeUrl(params));
-});
-
-router.get('/signin/discord', async ctx => {
-	const sessid = uuid();
-
-	const params = {
-		redirect_uri: `${config.url}/api/dc/cb`,
-		scope: ['identify'],
-		state: uuid(),
-		response_type: 'code'
-	};
-
-	ctx.cookies.set('signin_with_discord_sid', sessid, {
-		path: '/',
-		secure: config.url.startsWith('https'),
-		httpOnly: true
-	});
-
-	redisClient.set(sessid, JSON.stringify(params));
-
-	const oauth2 = await getOAuth2();
-	ctx.redirect(oauth2!.getAuthorizeUrl(params));
-});
-
-router.get('/dc/cb', async ctx => {
-	const userToken = getUserToken(ctx);
-
-	const oauth2 = await getOAuth2();
-
-	if (!userToken) {
-		const sessid = ctx.cookies.get('signin_with_discord_sid');
-
-		if (!sessid) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const code = ctx.query.code;
-
-		if (!code) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const { redirect_uri, state } = await new Promise<any>((res, rej) => {
-			redisClient.get(sessid, async (_, state) => {
-				res(JSON.parse(state));
-			});
-		});
-
-		if (ctx.query.state !== state) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
-			oauth2!.getOAuthAccessToken(code, {
-				grant_type: 'authorization_code',
-				redirect_uri
-			}, (err, accessToken, refreshToken, result) => {
-				if (err) {
-					rej(err);
-				} else if (result.error) {
-					rej(result.error);
-				} else {
-					res({
-						accessToken,
-						refreshToken,
-						expiresDate: Date.now() + Number(result.expires_in) * 1000
-					});
-				}
-			}));
-
-		const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
-			'Authorization': `Bearer ${accessToken}`,
-		});
-
-		if (!id || !username || !discriminator) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const profile = await UserProfiles.createQueryBuilder()
-			.where(`"integrations"->'discord'->>'id' = :id`, { id: id })
-			.andWhere('"userHost" IS NULL')
-			.getOne();
-
-		if (profile == null) {
-			ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
-			return;
-		}
-
-		await UserProfiles.update(profile.userId, {
-			integrations: {
-				...profile.integrations,
-				discord: {
-					id: id,
-					accessToken: accessToken,
-					refreshToken: refreshToken,
-					expiresDate: expiresDate,
-					username: username,
-					discriminator: discriminator
-				}
-			},
-		});
-
-		signin(ctx, await Users.findOne(profile.userId) as ILocalUser, true);
-	} else {
-		const code = ctx.query.code;
-
-		if (!code) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const { redirect_uri, state } = await new Promise<any>((res, rej) => {
-			redisClient.get(userToken, async (_, state) => {
-				res(JSON.parse(state));
-			});
-		});
-
-		if (ctx.query.state !== state) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) =>
-			oauth2!.getOAuthAccessToken(code, {
-				grant_type: 'authorization_code',
-				redirect_uri
-			}, (err, accessToken, refreshToken, result) => {
-				if (err) {
-					rej(err);
-				} else if (result.error) {
-					rej(result.error);
-				} else {
-					res({
-						accessToken,
-						refreshToken,
-						expiresDate: Date.now() + Number(result.expires_in) * 1000
-					});
-				}
-			}));
-
-		const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
-			'Authorization': `Bearer ${accessToken}`,
-		});
-		if (!id || !username || !discriminator) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const user = await Users.findOneOrFail({
-			host: null,
-			token: userToken
-		});
-
-		const profile = await UserProfiles.findOneOrFail(user.id);
-
-		await UserProfiles.update(user.id, {
-			integrations: {
-				...profile.integrations,
-				discord: {
-					accessToken: accessToken,
-					refreshToken: refreshToken,
-					expiresDate: expiresDate,
-					id: id,
-					username: username,
-					discriminator: discriminator
-				}
-			}
-		});
-
-		ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました！`;
-
-		// Publish i updated event
-		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
-			detail: true,
-			includeSecrets: true
-		}));
-	}
-});
-
-export default router;
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
deleted file mode 100644
index 0616f3f773..0000000000
--- a/src/server/api/service/github.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-import * as Koa from 'koa';
-import * as Router from '@koa/router';
-import { getJson } from '@/misc/fetch';
-import { OAuth2 } from 'oauth';
-import config from '@/config/index';
-import { publishMainStream } from '@/services/stream';
-import { redisClient } from '../../../db/redis';
-import { v4 as uuid } from 'uuid';
-import signin from '../common/signin';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Users, UserProfiles } from '@/models/index';
-import { ILocalUser } from '@/models/entities/user';
-
-function getUserToken(ctx: Koa.Context) {
-	return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
-}
-
-function compareOrigin(ctx: Koa.Context) {
-	function normalizeUrl(url: string) {
-		return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
-	}
-
-	const referer = ctx.headers['referer'];
-
-	return (normalizeUrl(referer) == normalizeUrl(config.url));
-}
-
-// Init router
-const router = new Router();
-
-router.get('/disconnect/github', async ctx => {
-	if (!compareOrigin(ctx)) {
-		ctx.throw(400, 'invalid origin');
-		return;
-	}
-
-	const userToken = getUserToken(ctx);
-	if (!userToken) {
-		ctx.throw(400, 'signin required');
-		return;
-	}
-
-	const user = await Users.findOneOrFail({
-		host: null,
-		token: userToken
-	});
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	delete profile.integrations.github;
-
-	await UserProfiles.update(user.id, {
-		integrations: profile.integrations,
-	});
-
-	ctx.body = `GitHubの連携を解除しました :v:`;
-
-	// Publish i updated event
-	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
-		detail: true,
-		includeSecrets: true
-	}));
-});
-
-async function getOath2() {
-	const meta = await fetchMeta(true);
-
-	if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) {
-		return new OAuth2(
-			meta.githubClientId,
-			meta.githubClientSecret,
-			'https://github.com/',
-			'login/oauth/authorize',
-			'login/oauth/access_token');
-	} else {
-		return null;
-	}
-}
-
-router.get('/connect/github', async ctx => {
-	if (!compareOrigin(ctx)) {
-		ctx.throw(400, 'invalid origin');
-		return;
-	}
-
-	const userToken = getUserToken(ctx);
-	if (!userToken) {
-		ctx.throw(400, 'signin required');
-		return;
-	}
-
-	const params = {
-		redirect_uri: `${config.url}/api/gh/cb`,
-		scope: ['read:user'],
-		state: uuid()
-	};
-
-	redisClient.set(userToken, JSON.stringify(params));
-
-	const oauth2 = await getOath2();
-	ctx.redirect(oauth2!.getAuthorizeUrl(params));
-});
-
-router.get('/signin/github', async ctx => {
-	const sessid = uuid();
-
-	const params = {
-		redirect_uri: `${config.url}/api/gh/cb`,
-		scope: ['read:user'],
-		state: uuid()
-	};
-
-	ctx.cookies.set('signin_with_github_sid', sessid, {
-		path: '/',
-		secure: config.url.startsWith('https'),
-		httpOnly: true
-	});
-
-	redisClient.set(sessid, JSON.stringify(params));
-
-	const oauth2 = await getOath2();
-	ctx.redirect(oauth2!.getAuthorizeUrl(params));
-});
-
-router.get('/gh/cb', async ctx => {
-	const userToken = getUserToken(ctx);
-
-	const oauth2 = await getOath2();
-
-	if (!userToken) {
-		const sessid = ctx.cookies.get('signin_with_github_sid');
-
-		if (!sessid) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const code = ctx.query.code;
-
-		if (!code) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const { redirect_uri, state } = await new Promise<any>((res, rej) => {
-			redisClient.get(sessid, async (_, state) => {
-				res(JSON.parse(state));
-			});
-		});
-
-		if (ctx.query.state !== state) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const { accessToken } = await new Promise<any>((res, rej) =>
-			oauth2!.getOAuthAccessToken(code, {
-				redirect_uri
-			}, (err, accessToken, refresh, result) => {
-				if (err) {
-					rej(err);
-				} else if (result.error) {
-					rej(result.error);
-				} else {
-					res({ accessToken });
-				}
-			}));
-
-		const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
-			'Authorization': `bearer ${accessToken}`
-		});
-		if (!login || !id) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const link = await UserProfiles.createQueryBuilder()
-			.where(`"integrations"->'github'->>'id' = :id`, { id: id })
-			.andWhere('"userHost" IS NULL')
-			.getOne();
-
-		if (link == null) {
-			ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
-			return;
-		}
-
-		signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
-	} else {
-		const code = ctx.query.code;
-
-		if (!code) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const { redirect_uri, state } = await new Promise<any>((res, rej) => {
-			redisClient.get(userToken, async (_, state) => {
-				res(JSON.parse(state));
-			});
-		});
-
-		if (ctx.query.state !== state) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const { accessToken } = await new Promise<any>((res, rej) =>
-			oauth2!.getOAuthAccessToken(
-				code,
-				{ redirect_uri },
-				(err, accessToken, refresh, result) => {
-					if (err)
-						rej(err);
-					else if (result.error)
-						rej(result.error);
-					else
-						res({ accessToken });
-				}));
-
-		const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
-			'Authorization': `bearer ${accessToken}`
-		});
-
-		if (!login || !id) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const user = await Users.findOneOrFail({
-			host: null,
-			token: userToken
-		});
-
-		const profile = await UserProfiles.findOneOrFail(user.id);
-
-		await UserProfiles.update(user.id, {
-			integrations: {
-				...profile.integrations,
-				github: {
-					accessToken: accessToken,
-					id: id,
-					login: login,
-				}
-			}
-		});
-
-		ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました！`;
-
-		// Publish i updated event
-		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
-			detail: true,
-			includeSecrets: true
-		}));
-	}
-});
-
-export default router;
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
deleted file mode 100644
index 8a6a58aeee..0000000000
--- a/src/server/api/service/twitter.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-import * as Koa from 'koa';
-import * as Router from '@koa/router';
-import { v4 as uuid } from 'uuid';
-import autwh from 'autwh';
-import { redisClient } from '../../../db/redis';
-import { publishMainStream } from '@/services/stream';
-import config from '@/config/index';
-import signin from '../common/signin';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Users, UserProfiles } from '@/models/index';
-import { ILocalUser } from '@/models/entities/user';
-
-function getUserToken(ctx: Koa.Context) {
-	return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
-}
-
-function compareOrigin(ctx: Koa.Context) {
-	function normalizeUrl(url: string) {
-		return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
-	}
-
-	const referer = ctx.headers['referer'];
-
-	return (normalizeUrl(referer) == normalizeUrl(config.url));
-}
-
-// Init router
-const router = new Router();
-
-router.get('/disconnect/twitter', async ctx => {
-	if (!compareOrigin(ctx)) {
-		ctx.throw(400, 'invalid origin');
-		return;
-	}
-
-	const userToken = getUserToken(ctx);
-	if (userToken == null) {
-		ctx.throw(400, 'signin required');
-		return;
-	}
-
-	const user = await Users.findOneOrFail({
-		host: null,
-		token: userToken
-	});
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	delete profile.integrations.twitter;
-
-	await UserProfiles.update(user.id, {
-		integrations: profile.integrations,
-	});
-
-	ctx.body = `Twitterの連携を解除しました :v:`;
-
-	// Publish i updated event
-	publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
-		detail: true,
-		includeSecrets: true
-	}));
-});
-
-async function getTwAuth() {
-	const meta = await fetchMeta(true);
-
-	if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) {
-		return autwh({
-			consumerKey: meta.twitterConsumerKey,
-			consumerSecret: meta.twitterConsumerSecret,
-			callbackUrl: `${config.url}/api/tw/cb`
-		});
-	} else {
-		return null;
-	}
-}
-
-router.get('/connect/twitter', async ctx => {
-	if (!compareOrigin(ctx)) {
-		ctx.throw(400, 'invalid origin');
-		return;
-	}
-
-	const userToken = getUserToken(ctx);
-	if (userToken == null) {
-		ctx.throw(400, 'signin required');
-		return;
-	}
-
-	const twAuth = await getTwAuth();
-	const twCtx = await twAuth!.begin();
-	redisClient.set(userToken, JSON.stringify(twCtx));
-	ctx.redirect(twCtx.url);
-});
-
-router.get('/signin/twitter', async ctx => {
-	const twAuth = await getTwAuth();
-	const twCtx = await twAuth!.begin();
-
-	const sessid = uuid();
-
-	redisClient.set(sessid, JSON.stringify(twCtx));
-
-	ctx.cookies.set('signin_with_twitter_sid', sessid, {
-		path: '/',
-		secure: config.url.startsWith('https'),
-		httpOnly: true
-	});
-
-	ctx.redirect(twCtx.url);
-});
-
-router.get('/tw/cb', async ctx => {
-	const userToken = getUserToken(ctx);
-
-	const twAuth = await getTwAuth();
-
-	if (userToken == null) {
-		const sessid = ctx.cookies.get('signin_with_twitter_sid');
-
-		if (sessid == null) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const get = new Promise<any>((res, rej) => {
-			redisClient.get(sessid, async (_, twCtx) => {
-				res(twCtx);
-			});
-		});
-
-		const twCtx = await get;
-
-		const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
-
-		const link = await UserProfiles.createQueryBuilder()
-			.where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId })
-			.andWhere('"userHost" IS NULL')
-			.getOne();
-
-		if (link == null) {
-			ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
-			return;
-		}
-
-		signin(ctx, await Users.findOne(link.userId) as ILocalUser, true);
-	} else {
-		const verifier = ctx.query.oauth_verifier;
-
-		if (verifier == null) {
-			ctx.throw(400, 'invalid session');
-			return;
-		}
-
-		const get = new Promise<any>((res, rej) => {
-			redisClient.get(userToken, async (_, twCtx) => {
-				res(twCtx);
-			});
-		});
-
-		const twCtx = await get;
-
-		const result = await twAuth!.done(JSON.parse(twCtx), verifier);
-
-		const user = await Users.findOneOrFail({
-			host: null,
-			token: userToken
-		});
-
-		const profile = await UserProfiles.findOneOrFail(user.id);
-
-		await UserProfiles.update(user.id, {
-			integrations: {
-				...profile.integrations,
-				twitter: {
-					accessToken: result.accessToken,
-					accessTokenSecret: result.accessTokenSecret,
-					userId: result.userId,
-					screenName: result.screenName,
-				}
-			},
-		});
-
-		ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました！`;
-
-		// Publish i updated event
-		publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
-			detail: true,
-			includeSecrets: true
-		}));
-	}
-});
-
-export default router;
diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts
deleted file mode 100644
index 2824d7d1b8..0000000000
--- a/src/server/api/stream/channel.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import autobind from 'autobind-decorator';
-import Connection from '.';
-
-/**
- * Stream channel
- */
-export default abstract class Channel {
-	protected connection: Connection;
-	public id: string;
-	public abstract readonly chName: string;
-	public static readonly shouldShare: boolean;
-	public static readonly requireCredential: boolean;
-
-	protected get user() {
-		return this.connection.user;
-	}
-
-	protected get userProfile() {
-		return this.connection.userProfile;
-	}
-
-	protected get following() {
-		return this.connection.following;
-	}
-
-	protected get muting() {
-		return this.connection.muting;
-	}
-
-	protected get blocking() {
-		return this.connection.blocking;
-	}
-
-	protected get followingChannels() {
-		return this.connection.followingChannels;
-	}
-
-	protected get subscriber() {
-		return this.connection.subscriber;
-	}
-
-	constructor(id: string, connection: Connection) {
-		this.id = id;
-		this.connection = connection;
-	}
-
-	@autobind
-	public send(typeOrPayload: any, payload?: any) {
-		const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
-		const body = payload === undefined ? typeOrPayload.body : payload;
-
-		this.connection.sendMessageToWs('channel', {
-			id: this.id,
-			type: type,
-			body: body
-		});
-	}
-
-	public abstract init(params: any): void;
-	public dispose?(): void;
-	public onMessage?(type: string, body: any): void;
-}
diff --git a/src/server/api/stream/channels/admin.ts b/src/server/api/stream/channels/admin.ts
deleted file mode 100644
index 1ff932d1dd..0000000000
--- a/src/server/api/stream/channels/admin.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import autobind from 'autobind-decorator';
-import Channel from '../channel';
-
-export default class extends Channel {
-	public readonly chName = 'admin';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	@autobind
-	public async init(params: any) {
-		// Subscribe admin stream
-		this.subscriber.on(`adminStream:${this.user!.id}`, data => {
-			this.send(data);
-		});
-	}
-}
diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts
deleted file mode 100644
index 3cbdfebb43..0000000000
--- a/src/server/api/stream/channels/antenna.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import autobind from 'autobind-decorator';
-import Channel from '../channel';
-import { Notes } from '@/models/index';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { StreamMessages } from '../types';
-
-export default class extends Channel {
-	public readonly chName = 'antenna';
-	public static shouldShare = false;
-	public static requireCredential = false;
-	private antennaId: string;
-
-	@autobind
-	public async init(params: any) {
-		this.antennaId = params.antennaId as string;
-
-		// Subscribe stream
-		this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);
-	}
-
-	@autobind
-	private async onEvent(data: StreamMessages['antenna']['payload']) {
-		if (data.type === 'note') {
-			const note = await Notes.pack(data.body.id, this.user, { detail: true });
-
-			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-			if (isMutedUserRelated(note, this.muting)) return;
-			// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
-			if (isBlockerUserRelated(note, this.blocking)) return;
-
-			this.connection.cacheNote(note);
-
-			this.send('note', note);
-		} else {
-			this.send(data.type, data.body);
-		}
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent);
-	}
-}
diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts
deleted file mode 100644
index bf7942f522..0000000000
--- a/src/server/api/stream/channels/channel.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import autobind from 'autobind-decorator';
-import Channel from '../channel';
-import { Notes, Users } from '@/models/index';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { User } from '@/models/entities/user';
-import { StreamMessages } from '../types';
-import { Packed } from '@/misc/schema';
-
-export default class extends Channel {
-	public readonly chName = 'channel';
-	public static shouldShare = false;
-	public static requireCredential = false;
-	private channelId: string;
-	private typers: Record<User['id'], Date> = {};
-	private emitTypersIntervalId: ReturnType<typeof setInterval>;
-
-	@autobind
-	public async init(params: any) {
-		this.channelId = params.channelId as string;
-
-		// Subscribe stream
-		this.subscriber.on('notesStream', this.onNote);
-		this.subscriber.on(`channelStream:${this.channelId}`, this.onEvent);
-		this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
-	}
-
-	@autobind
-	private async onNote(note: Packed<'Note'>) {
-		if (note.channelId !== this.channelId) return;
-
-		// リプライなら再pack
-		if (note.replyId != null) {
-			note.reply = await Notes.pack(note.replyId, this.user, {
-				detail: true
-			});
-		}
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await Notes.pack(note.renoteId, this.user, {
-				detail: true
-			});
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (isMutedUserRelated(note, this.muting)) return;
-		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
-		if (isBlockerUserRelated(note, this.blocking)) return;
-
-		this.connection.cacheNote(note);
-
-		this.send('note', note);
-	}
-
-	@autobind
-	private onEvent(data: StreamMessages['channel']['payload']) {
-		if (data.type === 'typing') {
-			const id = data.body;
-			const begin = this.typers[id] == null;
-			this.typers[id] = new Date();
-			if (begin) {
-				this.emitTypers();
-			}
-		}
-	}
-
-	@autobind
-	private async emitTypers() {
-		const now = new Date();
-
-		// Remove not typing users
-		for (const [userId, date] of Object.entries(this.typers)) {
-			if (now.getTime() - date.getTime() > 5000) delete this.typers[userId];
-		}
-
-		const users = await Users.packMany(Object.keys(this.typers), null, { detail: false });
-
-		this.send({
-			type: 'typers',
-			body: users,
-		});
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off('notesStream', this.onNote);
-		this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent);
-
-		clearInterval(this.emitTypersIntervalId);
-	}
-}
diff --git a/src/server/api/stream/channels/drive.ts b/src/server/api/stream/channels/drive.ts
deleted file mode 100644
index 4112dd9b04..0000000000
--- a/src/server/api/stream/channels/drive.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import autobind from 'autobind-decorator';
-import Channel from '../channel';
-
-export default class extends Channel {
-	public readonly chName = 'drive';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	@autobind
-	public async init(params: any) {
-		// Subscribe drive stream
-		this.subscriber.on(`driveStream:${this.user!.id}`, data => {
-			this.send(data);
-		});
-	}
-}
diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts
deleted file mode 100644
index bfdbf1d266..0000000000
--- a/src/server/api/stream/channels/games/reversi-game.ts
+++ /dev/null
@@ -1,372 +0,0 @@
-import autobind from 'autobind-decorator';
-import * as CRC32 from 'crc-32';
-import { publishReversiGameStream } from '@/services/stream';
-import Reversi from '../../../../../games/reversi/core';
-import * as maps from '../../../../../games/reversi/maps';
-import Channel from '../../channel';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { ReversiGames, Users } from '@/models/index';
-import { User } from '@/models/entities/user';
-
-export default class extends Channel {
-	public readonly chName = 'gamesReversiGame';
-	public static shouldShare = false;
-	public static requireCredential = false;
-
-	private gameId: ReversiGame['id'] | null = null;
-	private watchers: Record<User['id'], Date> = {};
-	private emitWatchersIntervalId: ReturnType<typeof setInterval>;
-
-	@autobind
-	public async init(params: any) {
-		this.gameId = params.gameId;
-
-		// Subscribe game stream
-		this.subscriber.on(`reversiGameStream:${this.gameId}`, this.onEvent);
-		this.emitWatchersIntervalId = setInterval(this.emitWatchers, 5000);
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		// 観戦者イベント
-		this.watch(game);
-	}
-
-	@autobind
-	private onEvent(data: any) {
-		if (data.type === 'watching') {
-			const id = data.body;
-			this.watchers[id] = new Date();
-		} else {
-			this.send(data);
-		}
-	}
-
-	@autobind
-	private async emitWatchers() {
-		const now = new Date();
-
-		// Remove not watching users
-		for (const [userId, date] of Object.entries(this.watchers)) {
-			if (now.getTime() - date.getTime() > 5000) delete this.watchers[userId];
-		}
-
-		const users = await Users.packMany(Object.keys(this.watchers), null, { detail: false });
-
-		this.send({
-			type: 'watchers',
-			body: users,
-		});
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off(`reversiGameStream:${this.gameId}`, this.onEvent);
-		clearInterval(this.emitWatchersIntervalId);
-	}
-
-	@autobind
-	public onMessage(type: string, body: any) {
-		switch (type) {
-			case 'accept': this.accept(true); break;
-			case 'cancelAccept': this.accept(false); break;
-			case 'updateSettings': this.updateSettings(body.key, body.value); break;
-			case 'initForm': this.initForm(body); break;
-			case 'updateForm': this.updateForm(body.id, body.value); break;
-			case 'message': this.message(body); break;
-			case 'set': this.set(body.pos); break;
-			case 'check': this.check(body.crc32); break;
-		}
-	}
-
-	@autobind
-	private async updateSettings(key: string, value: any) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (game.isStarted) return;
-		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
-		if ((game.user1Id === this.user.id) && game.user1Accepted) return;
-		if ((game.user2Id === this.user.id) && game.user2Accepted) return;
-
-		if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
-
-		await ReversiGames.update(this.gameId!, {
-			[key]: value
-		});
-
-		publishReversiGameStream(this.gameId!, 'updateSettings', {
-			key: key,
-			value: value
-		});
-	}
-
-	@autobind
-	private async initForm(form: any) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (game.isStarted) return;
-		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
-
-		const set = game.user1Id === this.user.id ? {
-			form1: form
-		} : {
-			form2: form
-		};
-
-		await ReversiGames.update(this.gameId!, set);
-
-		publishReversiGameStream(this.gameId!, 'initForm', {
-			userId: this.user.id,
-			form
-		});
-	}
-
-	@autobind
-	private async updateForm(id: string, value: any) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (game.isStarted) return;
-		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
-
-		const form = game.user1Id === this.user.id ? game.form2 : game.form1;
-
-		const item = form.find((i: any) => i.id == id);
-
-		if (item == null) return;
-
-		item.value = value;
-
-		const set = game.user1Id === this.user.id ? {
-			form2: form
-		} : {
-				form1: form
-			};
-
-		await ReversiGames.update(this.gameId!, set);
-
-		publishReversiGameStream(this.gameId!, 'updateForm', {
-			userId: this.user.id,
-			id,
-			value
-		});
-	}
-
-	@autobind
-	private async message(message: any) {
-		if (this.user == null) return;
-
-		message.id = Math.random();
-		publishReversiGameStream(this.gameId!, 'message', {
-			userId: this.user.id,
-			message
-		});
-	}
-
-	@autobind
-	private async accept(accept: boolean) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (game.isStarted) return;
-
-		let bothAccepted = false;
-
-		if (game.user1Id === this.user.id) {
-			await ReversiGames.update(this.gameId!, {
-				user1Accepted: accept
-			});
-
-			publishReversiGameStream(this.gameId!, 'changeAccepts', {
-				user1: accept,
-				user2: game.user2Accepted
-			});
-
-			if (accept && game.user2Accepted) bothAccepted = true;
-		} else if (game.user2Id === this.user.id) {
-			await ReversiGames.update(this.gameId!, {
-				user2Accepted: accept
-			});
-
-			publishReversiGameStream(this.gameId!, 'changeAccepts', {
-				user1: game.user1Accepted,
-				user2: accept
-			});
-
-			if (accept && game.user1Accepted) bothAccepted = true;
-		} else {
-			return;
-		}
-
-		if (bothAccepted) {
-			// 3秒後、まだacceptされていたらゲーム開始
-			setTimeout(async () => {
-				const freshGame = await ReversiGames.findOne(this.gameId!);
-				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
-				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
-
-				let bw: number;
-				if (freshGame.bw == 'random') {
-					bw = Math.random() > 0.5 ? 1 : 2;
-				} else {
-					bw = parseInt(freshGame.bw, 10);
-				}
-
-				function getRandomMap() {
-					const mapCount = Object.entries(maps).length;
-					const rnd = Math.floor(Math.random() * mapCount);
-					return Object.values(maps)[rnd].data;
-				}
-
-				const map = freshGame.map != null ? freshGame.map : getRandomMap();
-
-				await ReversiGames.update(this.gameId!, {
-					startedAt: new Date(),
-					isStarted: true,
-					black: bw,
-					map: map
-				});
-
-				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
-				const o = new Reversi(map, {
-					isLlotheo: freshGame.isLlotheo,
-					canPutEverywhere: freshGame.canPutEverywhere,
-					loopedBoard: freshGame.loopedBoard
-				});
-
-				if (o.isEnded) {
-					let winner;
-					if (o.winner === true) {
-						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id;
-					} else if (o.winner === false) {
-						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id;
-					} else {
-						winner = null;
-					}
-
-					await ReversiGames.update(this.gameId!, {
-						isEnded: true,
-						winnerId: winner
-					});
-
-					publishReversiGameStream(this.gameId!, 'ended', {
-						winnerId: winner,
-						game: await ReversiGames.pack(this.gameId!, this.user)
-					});
-				}
-				//#endregion
-
-				publishReversiGameStream(this.gameId!, 'started',
-					await ReversiGames.pack(this.gameId!, this.user));
-			}, 3000);
-		}
-	}
-
-	// 石を打つ
-	@autobind
-	private async set(pos: number) {
-		if (this.user == null) return;
-
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (!game.isStarted) return;
-		if (game.isEnded) return;
-		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
-
-		const myColor =
-			((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
-				? true
-				: false;
-
-		const o = new Reversi(game.map, {
-			isLlotheo: game.isLlotheo,
-			canPutEverywhere: game.canPutEverywhere,
-			loopedBoard: game.loopedBoard
-		});
-
-		// 盤面の状態を再生
-		for (const log of game.logs) {
-			o.put(log.color, log.pos);
-		}
-
-		if (o.turn !== myColor) return;
-
-		if (!o.canPut(myColor, pos)) return;
-		o.put(myColor, pos);
-
-		let winner;
-		if (o.isEnded) {
-			if (o.winner === true) {
-				winner = game.black == 1 ? game.user1Id : game.user2Id;
-			} else if (o.winner === false) {
-				winner = game.black == 1 ? game.user2Id : game.user1Id;
-			} else {
-				winner = null;
-			}
-		}
-
-		const log = {
-			at: new Date(),
-			color: myColor,
-			pos
-		};
-
-		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
-
-		game.logs.push(log);
-
-		await ReversiGames.update(this.gameId!, {
-			crc32,
-			isEnded: o.isEnded,
-			winnerId: winner,
-			logs: game.logs
-		});
-
-		publishReversiGameStream(this.gameId!, 'set', Object.assign(log, {
-			next: o.turn
-		}));
-
-		if (o.isEnded) {
-			publishReversiGameStream(this.gameId!, 'ended', {
-				winnerId: winner,
-				game: await ReversiGames.pack(this.gameId!, this.user)
-			});
-		}
-	}
-
-	@autobind
-	private async check(crc32: string | number) {
-		const game = await ReversiGames.findOne(this.gameId!);
-		if (game == null) throw new Error('game not found');
-
-		if (!game.isStarted) return;
-
-		if (crc32.toString() !== game.crc32) {
-			this.send('rescue', await ReversiGames.pack(game, this.user));
-		}
-
-		// ついでに観戦者イベントを発行
-		this.watch(game);
-	}
-
-	@autobind
-	private watch(game: ReversiGame) {
-		if (this.user != null) {
-			if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) {
-				publishReversiGameStream(this.gameId!, 'watching', this.user.id);
-			}
-		}
-	}
-}
diff --git a/src/server/api/stream/channels/games/reversi.ts b/src/server/api/stream/channels/games/reversi.ts
deleted file mode 100644
index 3b89aac35c..0000000000
--- a/src/server/api/stream/channels/games/reversi.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import autobind from 'autobind-decorator';
-import { publishMainStream } from '@/services/stream';
-import Channel from '../../channel';
-import { ReversiMatchings } from '@/models/index';
-
-export default class extends Channel {
-	public readonly chName = 'gamesReversi';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	@autobind
-	public async init(params: any) {
-		// Subscribe reversi stream
-		this.subscriber.on(`reversiStream:${this.user!.id}`, data => {
-			this.send(data);
-		});
-	}
-
-	@autobind
-	public async onMessage(type: string, body: any) {
-		switch (type) {
-			case 'ping':
-				if (body.id == null) return;
-				const matching = await ReversiMatchings.findOne({
-					parentId: this.user!.id,
-					childId: body.id
-				});
-				if (matching == null) return;
-				publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, { id: matching.childId }));
-				break;
-		}
-	}
-}
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
deleted file mode 100644
index f5983ab472..0000000000
--- a/src/server/api/stream/channels/global-timeline.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import autobind from 'autobind-decorator';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import Channel from '../channel';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Notes } from '@/models/index';
-import { checkWordMute } from '@/misc/check-word-mute';
-import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { Packed } from '@/misc/schema';
-
-export default class extends Channel {
-	public readonly chName = 'globalTimeline';
-	public static shouldShare = true;
-	public static requireCredential = false;
-
-	@autobind
-	public async init(params: any) {
-		const meta = await fetchMeta();
-		if (meta.disableGlobalTimeline) {
-			if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return;
-		}
-
-		// Subscribe events
-		this.subscriber.on('notesStream', this.onNote);
-	}
-
-	@autobind
-	private async onNote(note: Packed<'Note'>) {
-		if (note.visibility !== 'public') return;
-		if (note.channelId != null) return;
-
-		// リプライなら再pack
-		if (note.replyId != null) {
-			note.reply = await Notes.pack(note.replyId, this.user, {
-				detail: true
-			});
-		}
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await Notes.pack(note.renoteId, this.user, {
-				detail: true
-			});
-		}
-
-		// 関係ない返信は除外
-		if (note.reply) {
-			const reply = note.reply;
-			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
-			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (isMutedUserRelated(note, this.muting)) return;
-		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
-		if (isBlockerUserRelated(note, this.blocking)) return;
-
-		// 流れてきたNoteがミュートすべきNoteだったら無視する
-		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
-		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
-		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
-		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
-		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
-
-		this.connection.cacheNote(note);
-
-		this.send('note', note);
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off('notesStream', this.onNote);
-	}
-}
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
deleted file mode 100644
index 281be4f2eb..0000000000
--- a/src/server/api/stream/channels/hashtag.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import autobind from 'autobind-decorator';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import Channel from '../channel';
-import { Notes } from '@/models/index';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { Packed } from '@/misc/schema';
-
-export default class extends Channel {
-	public readonly chName = 'hashtag';
-	public static shouldShare = false;
-	public static requireCredential = false;
-	private q: string[][];
-
-	@autobind
-	public async init(params: any) {
-		this.q = params.q;
-
-		if (this.q == null) return;
-
-		// Subscribe stream
-		this.subscriber.on('notesStream', this.onNote);
-	}
-
-	@autobind
-	private async onNote(note: Packed<'Note'>) {
-		const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
-		const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
-		if (!matched) return;
-
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await Notes.pack(note.renoteId, this.user, {
-				detail: true
-			});
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (isMutedUserRelated(note, this.muting)) return;
-		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
-		if (isBlockerUserRelated(note, this.blocking)) return;
-
-		this.connection.cacheNote(note);
-
-		this.send('note', note);
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off('notesStream', this.onNote);
-	}
-}
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
deleted file mode 100644
index 52e9aec250..0000000000
--- a/src/server/api/stream/channels/home-timeline.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import autobind from 'autobind-decorator';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import Channel from '../channel';
-import { Notes } from '@/models/index';
-import { checkWordMute } from '@/misc/check-word-mute';
-import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { Packed } from '@/misc/schema';
-
-export default class extends Channel {
-	public readonly chName = 'homeTimeline';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	@autobind
-	public async init(params: any) {
-		// Subscribe events
-		this.subscriber.on('notesStream', this.onNote);
-	}
-
-	@autobind
-	private async onNote(note: Packed<'Note'>) {
-		if (note.channelId) {
-			if (!this.followingChannels.has(note.channelId)) return;
-		} else {
-			// その投稿のユーザーをフォローしていなかったら弾く
-			if ((this.user!.id !== note.userId) && !this.following.has(note.userId)) return;
-		}
-
-		if (['followers', 'specified'].includes(note.visibility)) {
-			note = await Notes.pack(note.id, this.user!, {
-				detail: true
-			});
-
-			if (note.isHidden) {
-				return;
-			}
-		} else {
-			// リプライなら再pack
-			if (note.replyId != null) {
-				note.reply = await Notes.pack(note.replyId, this.user!, {
-					detail: true
-				});
-			}
-			// Renoteなら再pack
-			if (note.renoteId != null) {
-				note.renote = await Notes.pack(note.renoteId, this.user!, {
-					detail: true
-				});
-			}
-		}
-
-		// 関係ない返信は除外
-		if (note.reply) {
-			const reply = note.reply;
-			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
-			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (isMutedUserRelated(note, this.muting)) return;
-		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
-		if (isBlockerUserRelated(note, this.blocking)) return;
-
-		// 流れてきたNoteがミュートすべきNoteだったら無視する
-		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
-		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
-		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
-		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
-		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
-
-		this.connection.cacheNote(note);
-
-		this.send('note', note);
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off('notesStream', this.onNote);
-	}
-}
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
deleted file mode 100644
index 51f95fc0cd..0000000000
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import autobind from 'autobind-decorator';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import Channel from '../channel';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Notes } from '@/models/index';
-import { checkWordMute } from '@/misc/check-word-mute';
-import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { Packed } from '@/misc/schema';
-
-export default class extends Channel {
-	public readonly chName = 'hybridTimeline';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	@autobind
-	public async init(params: any) {
-		const meta = await fetchMeta();
-		if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return;
-
-		// Subscribe events
-		this.subscriber.on('notesStream', this.onNote);
-	}
-
-	@autobind
-	private async onNote(note: Packed<'Note'>) {
-		// チャンネルの投稿ではなく、自分自身の投稿 または
-		// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
-		// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
-		// フォローしているチャンネルの投稿 の場合だけ
-		if (!(
-			(note.channelId == null && this.user!.id === note.userId) ||
-			(note.channelId == null && this.following.has(note.userId)) ||
-			(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
-			(note.channelId != null && this.followingChannels.has(note.channelId))
-		)) return;
-
-		if (['followers', 'specified'].includes(note.visibility)) {
-			note = await Notes.pack(note.id, this.user!, {
-				detail: true
-			});
-
-			if (note.isHidden) {
-				return;
-			}
-		} else {
-			// リプライなら再pack
-			if (note.replyId != null) {
-				note.reply = await Notes.pack(note.replyId, this.user!, {
-					detail: true
-				});
-			}
-			// Renoteなら再pack
-			if (note.renoteId != null) {
-				note.renote = await Notes.pack(note.renoteId, this.user!, {
-					detail: true
-				});
-			}
-		}
-
-		// 関係ない返信は除外
-		if (note.reply) {
-			const reply = note.reply;
-			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
-			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (isMutedUserRelated(note, this.muting)) return;
-		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
-		if (isBlockerUserRelated(note, this.blocking)) return;
-
-		// 流れてきたNoteがミュートすべきNoteだったら無視する
-		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
-		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
-		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
-		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
-		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
-
-		this.connection.cacheNote(note);
-
-		this.send('note', note);
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off('notesStream', this.onNote);
-	}
-}
diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts
deleted file mode 100644
index 1841573043..0000000000
--- a/src/server/api/stream/channels/index.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import main from './main';
-import homeTimeline from './home-timeline';
-import localTimeline from './local-timeline';
-import hybridTimeline from './hybrid-timeline';
-import globalTimeline from './global-timeline';
-import serverStats from './server-stats';
-import queueStats from './queue-stats';
-import userList from './user-list';
-import antenna from './antenna';
-import messaging from './messaging';
-import messagingIndex from './messaging-index';
-import drive from './drive';
-import hashtag from './hashtag';
-import channel from './channel';
-import admin from './admin';
-import gamesReversi from './games/reversi';
-import gamesReversiGame from './games/reversi-game';
-
-export default {
-	main,
-	homeTimeline,
-	localTimeline,
-	hybridTimeline,
-	globalTimeline,
-	serverStats,
-	queueStats,
-	userList,
-	antenna,
-	messaging,
-	messagingIndex,
-	drive,
-	hashtag,
-	channel,
-	admin,
-	gamesReversi,
-	gamesReversiGame
-};
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
deleted file mode 100644
index a6166c2be2..0000000000
--- a/src/server/api/stream/channels/local-timeline.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import autobind from 'autobind-decorator';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import Channel from '../channel';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Notes } from '@/models/index';
-import { checkWordMute } from '@/misc/check-word-mute';
-import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { Packed } from '@/misc/schema';
-
-export default class extends Channel {
-	public readonly chName = 'localTimeline';
-	public static shouldShare = true;
-	public static requireCredential = false;
-
-	@autobind
-	public async init(params: any) {
-		const meta = await fetchMeta();
-		if (meta.disableLocalTimeline) {
-			if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return;
-		}
-
-		// Subscribe events
-		this.subscriber.on('notesStream', this.onNote);
-	}
-
-	@autobind
-	private async onNote(note: Packed<'Note'>) {
-		if (note.user.host !== null) return;
-		if (note.visibility !== 'public') return;
-		if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
-
-		// リプライなら再pack
-		if (note.replyId != null) {
-			note.reply = await Notes.pack(note.replyId, this.user, {
-				detail: true
-			});
-		}
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await Notes.pack(note.renoteId, this.user, {
-				detail: true
-			});
-		}
-
-		// 関係ない返信は除外
-		if (note.reply) {
-			const reply = note.reply;
-			// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
-			if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (isMutedUserRelated(note, this.muting)) return;
-		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
-		if (isBlockerUserRelated(note, this.blocking)) return;
-
-		// 流れてきたNoteがミュートすべきNoteだったら無視する
-		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
-		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
-		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
-		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
-		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
-
-		this.connection.cacheNote(note);
-
-		this.send('note', note);
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off('notesStream', this.onNote);
-	}
-}
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
deleted file mode 100644
index 131ac30472..0000000000
--- a/src/server/api/stream/channels/main.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import autobind from 'autobind-decorator';
-import Channel from '../channel';
-import { Notes } from '@/models/index';
-
-export default class extends Channel {
-	public readonly chName = 'main';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	@autobind
-	public async init(params: any) {
-		// Subscribe main stream channel
-		this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
-			switch (data.type) {
-				case 'notification': {
-					if (data.body.userId && this.muting.has(data.body.userId)) return;
-
-					if (data.body.note && data.body.note.isHidden) {
-						const note = await Notes.pack(data.body.note.id, this.user, {
-							detail: true
-						});
-						this.connection.cacheNote(note);
-						data.body.note = note;
-					}
-					break;
-				}
-				case 'mention': {
-					if (this.muting.has(data.body.userId)) return;
-					if (data.body.isHidden) {
-						const note = await Notes.pack(data.body.id, this.user, {
-							detail: true
-						});
-						this.connection.cacheNote(note);
-						data.body = note;
-					}
-					break;
-				}
-			}
-
-			this.send(data.type, data.body);
-		});
-	}
-}
diff --git a/src/server/api/stream/channels/messaging-index.ts b/src/server/api/stream/channels/messaging-index.ts
deleted file mode 100644
index 0c495398ab..0000000000
--- a/src/server/api/stream/channels/messaging-index.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import autobind from 'autobind-decorator';
-import Channel from '../channel';
-
-export default class extends Channel {
-	public readonly chName = 'messagingIndex';
-	public static shouldShare = true;
-	public static requireCredential = true;
-
-	@autobind
-	public async init(params: any) {
-		// Subscribe messaging index stream
-		this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => {
-			this.send(data);
-		});
-	}
-}
diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts
deleted file mode 100644
index c049e880b9..0000000000
--- a/src/server/api/stream/channels/messaging.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import autobind from 'autobind-decorator';
-import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message';
-import Channel from '../channel';
-import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index';
-import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
-import { UserGroup } from '@/models/entities/user-group';
-import { StreamMessages } from '../types';
-
-export default class extends Channel {
-	public readonly chName = 'messaging';
-	public static shouldShare = false;
-	public static requireCredential = true;
-
-	private otherpartyId: string | null;
-	private otherparty: User | null;
-	private groupId: string | null;
-	private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`;
-	private typers: Record<User['id'], Date> = {};
-	private emitTypersIntervalId: ReturnType<typeof setInterval>;
-
-	@autobind
-	public async init(params: any) {
-		this.otherpartyId = params.otherparty;
-		this.otherparty = this.otherpartyId ? await Users.findOneOrFail({ id: this.otherpartyId }) : null;
-		this.groupId = params.group;
-
-		// Check joining
-		if (this.groupId) {
-			const joining = await UserGroupJoinings.findOne({
-				userId: this.user!.id,
-				userGroupId: this.groupId
-			});
-
-			if (joining == null) {
-				return;
-			}
-		}
-
-		this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
-
-		this.subCh = this.otherpartyId
-			? `messagingStream:${this.user!.id}-${this.otherpartyId}`
-			: `messagingStream:${this.groupId}`;
-
-		// Subscribe messaging stream
-		this.subscriber.on(this.subCh, this.onEvent);
-	}
-
-	@autobind
-	private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) {
-		if (data.type === 'typing') {
-			const id = data.body;
-			const begin = this.typers[id] == null;
-			this.typers[id] = new Date();
-			if (begin) {
-				this.emitTypers();
-			}
-		} else {
-			this.send(data);
-		}
-	}
-
-	@autobind
-	public onMessage(type: string, body: any) {
-		switch (type) {
-			case 'read':
-				if (this.otherpartyId) {
-					readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]);
-
-					// リモートユーザーからのメッセージだったら既読配信
-					if (Users.isLocalUser(this.user!) && Users.isRemoteUser(this.otherparty!)) {
-						MessagingMessages.findOne(body.id).then(message => {
-							if (message) deliverReadActivity(this.user as ILocalUser, this.otherparty as IRemoteUser, message);
-						});
-					}
-				} else if (this.groupId) {
-					readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]);
-				}
-				break;
-		}
-	}
-
-	@autobind
-	private async emitTypers() {
-		const now = new Date();
-
-		// Remove not typing users
-		for (const [userId, date] of Object.entries(this.typers)) {
-			if (now.getTime() - date.getTime() > 5000) delete this.typers[userId];
-		}
-
-		const users = await Users.packMany(Object.keys(this.typers), null, { detail: false });
-
-		this.send({
-			type: 'typers',
-			body: users,
-		});
-	}
-
-	@autobind
-	public dispose() {
-		this.subscriber.off(this.subCh, this.onEvent);
-
-		clearInterval(this.emitTypersIntervalId);
-	}
-}
diff --git a/src/server/api/stream/channels/queue-stats.ts b/src/server/api/stream/channels/queue-stats.ts
deleted file mode 100644
index 0bda0cfcb9..0000000000
--- a/src/server/api/stream/channels/queue-stats.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import autobind from 'autobind-decorator';
-import Xev from 'xev';
-import Channel from '../channel';
-
-const ev = new Xev();
-
-export default class extends Channel {
-	public readonly chName = 'queueStats';
-	public static shouldShare = true;
-	public static requireCredential = false;
-
-	@autobind
-	public async init(params: any) {
-		ev.addListener('queueStats', this.onStats);
-	}
-
-	@autobind
-	private onStats(stats: any) {
-		this.send('stats', stats);
-	}
-
-	@autobind
-	public onMessage(type: string, body: any) {
-		switch (type) {
-			case 'requestLog':
-				ev.once(`queueStatsLog:${body.id}`, statsLog => {
-					this.send('statsLog', statsLog);
-				});
-				ev.emit('requestQueueStatsLog', {
-					id: body.id,
-					length: body.length
-				});
-				break;
-		}
-	}
-
-	@autobind
-	public dispose() {
-		ev.removeListener('queueStats', this.onStats);
-	}
-}
diff --git a/src/server/api/stream/channels/server-stats.ts b/src/server/api/stream/channels/server-stats.ts
deleted file mode 100644
index d245a7f70c..0000000000
--- a/src/server/api/stream/channels/server-stats.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import autobind from 'autobind-decorator';
-import Xev from 'xev';
-import Channel from '../channel';
-
-const ev = new Xev();
-
-export default class extends Channel {
-	public readonly chName = 'serverStats';
-	public static shouldShare = true;
-	public static requireCredential = false;
-
-	@autobind
-	public async init(params: any) {
-		ev.addListener('serverStats', this.onStats);
-	}
-
-	@autobind
-	private onStats(stats: any) {
-		this.send('stats', stats);
-	}
-
-	@autobind
-	public onMessage(type: string, body: any) {
-		switch (type) {
-			case 'requestLog':
-				ev.once(`serverStatsLog:${body.id}`, statsLog => {
-					this.send('statsLog', statsLog);
-				});
-				ev.emit('requestServerStatsLog', {
-					id: body.id,
-					length: body.length
-				});
-				break;
-		}
-	}
-
-	@autobind
-	public dispose() {
-		ev.removeListener('serverStats', this.onStats);
-	}
-}
diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts
deleted file mode 100644
index 63b254605b..0000000000
--- a/src/server/api/stream/channels/user-list.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import autobind from 'autobind-decorator';
-import Channel from '../channel';
-import { Notes, UserListJoinings, UserLists } from '@/models/index';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import { User } from '@/models/entities/user';
-import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
-import { Packed } from '@/misc/schema';
-
-export default class extends Channel {
-	public readonly chName = 'userList';
-	public static shouldShare = false;
-	public static requireCredential = false;
-	private listId: string;
-	public listUsers: User['id'][] = [];
-	private listUsersClock: NodeJS.Timer;
-
-	@autobind
-	public async init(params: any) {
-		this.listId = params.listId as string;
-
-		// Check existence and owner
-		const list = await UserLists.findOne({
-			id: this.listId,
-			userId: this.user!.id
-		});
-		if (!list) return;
-
-		// Subscribe stream
-		this.subscriber.on(`userListStream:${this.listId}`, this.send);
-
-		this.subscriber.on('notesStream', this.onNote);
-
-		this.updateListUsers();
-		this.listUsersClock = setInterval(this.updateListUsers, 5000);
-	}
-
-	@autobind
-	private async updateListUsers() {
-		const users = await UserListJoinings.find({
-			where: {
-				userListId: this.listId,
-			},
-			select: ['userId']
-		});
-
-		this.listUsers = users.map(x => x.userId);
-	}
-
-	@autobind
-	private async onNote(note: Packed<'Note'>) {
-		if (!this.listUsers.includes(note.userId)) return;
-
-		if (['followers', 'specified'].includes(note.visibility)) {
-			note = await Notes.pack(note.id, this.user, {
-				detail: true
-			});
-
-			if (note.isHidden) {
-				return;
-			}
-		} else {
-			// リプライなら再pack
-			if (note.replyId != null) {
-				note.reply = await Notes.pack(note.replyId, this.user, {
-					detail: true
-				});
-			}
-			// Renoteなら再pack
-			if (note.renoteId != null) {
-				note.renote = await Notes.pack(note.renoteId, this.user, {
-					detail: true
-				});
-			}
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (isMutedUserRelated(note, this.muting)) return;
-		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
-		if (isBlockerUserRelated(note, this.blocking)) return;
-
-		this.send('note', note);
-	}
-
-	@autobind
-	public dispose() {
-		// Unsubscribe events
-		this.subscriber.off(`userListStream:${this.listId}`, this.send);
-		this.subscriber.off('notesStream', this.onNote);
-
-		clearInterval(this.listUsersClock);
-	}
-}
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
deleted file mode 100644
index da4ea5ec99..0000000000
--- a/src/server/api/stream/index.ts
+++ /dev/null
@@ -1,421 +0,0 @@
-import autobind from 'autobind-decorator';
-import * as websocket from 'websocket';
-import { readNotification } from '../common/read-notification';
-import call from '../call';
-import readNote from '@/services/note/read';
-import Channel from './channel';
-import channels from './channels/index';
-import { EventEmitter } from 'events';
-import { User } from '@/models/entities/user';
-import { Channel as ChannelModel } from '@/models/entities/channel';
-import { Users, Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index';
-import { ApiError } from '../error';
-import { AccessToken } from '@/models/entities/access-token';
-import { UserProfile } from '@/models/entities/user-profile';
-import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream';
-import { UserGroup } from '@/models/entities/user-group';
-import { StreamEventEmitter, StreamMessages } from './types';
-import { Packed } from '@/misc/schema';
-
-/**
- * Main stream connection
- */
-export default class Connection {
-	public user?: User;
-	public userProfile?: UserProfile;
-	public following: Set<User['id']> = new Set();
-	public muting: Set<User['id']> = new Set();
-	public blocking: Set<User['id']> = new Set(); // "被"blocking
-	public followingChannels: Set<ChannelModel['id']> = new Set();
-	public token?: AccessToken;
-	private wsConnection: websocket.connection;
-	public subscriber: StreamEventEmitter;
-	private channels: Channel[] = [];
-	private subscribingNotes: any = {};
-	private cachedNotes: Packed<'Note'>[] = [];
-
-	constructor(
-		wsConnection: websocket.connection,
-		subscriber: EventEmitter,
-		user: User | null | undefined,
-		token: AccessToken | null | undefined
-	) {
-		this.wsConnection = wsConnection;
-		this.subscriber = subscriber;
-		if (user) this.user = user;
-		if (token) this.token = token;
-
-		this.wsConnection.on('message', this.onWsConnectionMessage);
-
-		this.subscriber.on('broadcast', data => {
-			this.onBroadcastMessage(data);
-		});
-
-		if (this.user) {
-			this.updateFollowing();
-			this.updateMuting();
-			this.updateBlocking();
-			this.updateFollowingChannels();
-			this.updateUserProfile();
-
-			this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
-		}
-	}
-
-	@autobind
-	private onUserEvent(data: StreamMessages['user']['payload']) { // { type, body }と展開するとそれぞれ型が分離してしまう
-		switch (data.type) {
-			case 'follow':
-				this.following.add(data.body.id);
-				break;
-
-			case 'unfollow':
-				this.following.delete(data.body.id);
-				break;
-
-			case 'mute':
-				this.muting.add(data.body.id);
-				break;
-
-			case 'unmute':
-				this.muting.delete(data.body.id);
-				break;
-
-			// TODO: block events
-
-			case 'followChannel':
-				this.followingChannels.add(data.body.id);
-				break;
-
-			case 'unfollowChannel':
-				this.followingChannels.delete(data.body.id);
-				break;
-
-			case 'updateUserProfile':
-				this.userProfile = data.body;
-				break;
-
-			case 'terminate':
-				this.wsConnection.close();
-				this.dispose();
-				break;
-
-			default:
-				break;
-		}
-	}
-
-	/**
-	 * クライアントからメッセージ受信時
-	 */
-	@autobind
-	private async onWsConnectionMessage(data: websocket.IMessage) {
-		if (data.utf8Data == null) return;
-
-		let obj: Record<string, any>;
-
-		try {
-			obj = JSON.parse(data.utf8Data);
-		} catch (e) {
-			return;
-		}
-
-		const { type, body } = obj;
-
-		switch (type) {
-			case 'api': this.onApiRequest(body); break;
-			case 'readNotification': this.onReadNotification(body); break;
-			case 'subNote': this.onSubscribeNote(body); break;
-			case 's': this.onSubscribeNote(body); break; // alias
-			case 'sr': this.onSubscribeNote(body); this.readNote(body); break;
-			case 'unsubNote': this.onUnsubscribeNote(body); break;
-			case 'un': this.onUnsubscribeNote(body); break; // alias
-			case 'connect': this.onChannelConnectRequested(body); break;
-			case 'disconnect': this.onChannelDisconnectRequested(body); break;
-			case 'channel': this.onChannelMessageRequested(body); break;
-			case 'ch': this.onChannelMessageRequested(body); break; // alias
-
-			// 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、
-			// クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別
-			// なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。
-			case 'typingOnChannel': this.typingOnChannel(body.channel); break;
-			case 'typingOnMessaging': this.typingOnMessaging(body); break;
-		}
-	}
-
-	@autobind
-	private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) {
-		this.sendMessageToWs(data.type, data.body);
-	}
-
-	@autobind
-	public cacheNote(note: Packed<'Note'>) {
-		const add = (note: Packed<'Note'>) => {
-			const existIndex = this.cachedNotes.findIndex(n => n.id === note.id);
-			if (existIndex > -1) {
-				this.cachedNotes[existIndex] = note;
-				return;
-			}
-
-			this.cachedNotes.unshift(note);
-			if (this.cachedNotes.length > 32) {
-				this.cachedNotes.splice(32);
-			}
-		};
-
-		add(note);
-		if (note.reply) add(note.reply);
-		if (note.renote) add(note.renote);
-	}
-
-	@autobind
-	private readNote(body: any) {
-		const id = body.id;
-
-		const note = this.cachedNotes.find(n => n.id === id);
-		if (note == null) return;
-
-		if (this.user && (note.userId !== this.user.id)) {
-			readNote(this.user.id, [note], {
-				following: this.following,
-				followingChannels: this.followingChannels,
-			});
-		}
-	}
-
-	/**
-	 * APIリクエスト要求時
-	 */
-	@autobind
-	private async onApiRequest(payload: any) {
-		// 新鮮なデータを利用するためにユーザーをフェッチ
-		const user = this.user ? await Users.findOne(this.user.id) : null;
-
-		const endpoint = payload.endpoint || payload.ep; // alias
-
-		// 呼び出し
-		call(endpoint, user, this.token, payload.data).then(res => {
-			this.sendMessageToWs(`api:${payload.id}`, { res });
-		}).catch((e: ApiError) => {
-			this.sendMessageToWs(`api:${payload.id}`, {
-				error: {
-					message: e.message,
-					code: e.code,
-					id: e.id,
-					kind: e.kind,
-					...(e.info ? { info: e.info } : {})
-				}
-			});
-		});
-	}
-
-	@autobind
-	private onReadNotification(payload: any) {
-		if (!payload.id) return;
-		readNotification(this.user!.id, [payload.id]);
-	}
-
-	/**
-	 * 投稿購読要求時
-	 */
-	@autobind
-	private onSubscribeNote(payload: any) {
-		if (!payload.id) return;
-
-		if (this.subscribingNotes[payload.id] == null) {
-			this.subscribingNotes[payload.id] = 0;
-		}
-
-		this.subscribingNotes[payload.id]++;
-
-		if (this.subscribingNotes[payload.id] === 1) {
-			this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
-		}
-	}
-
-	/**
-	 * 投稿購読解除要求時
-	 */
-	@autobind
-	private onUnsubscribeNote(payload: any) {
-		if (!payload.id) return;
-
-		this.subscribingNotes[payload.id]--;
-		if (this.subscribingNotes[payload.id] <= 0) {
-			delete this.subscribingNotes[payload.id];
-			this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
-		}
-	}
-
-	@autobind
-	private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
-		this.sendMessageToWs('noteUpdated', {
-			id: data.body.id,
-			type: data.type,
-			body: data.body.body,
-		});
-	}
-
-	/**
-	 * チャンネル接続要求時
-	 */
-	@autobind
-	private onChannelConnectRequested(payload: any) {
-		const { channel, id, params, pong } = payload;
-		this.connectChannel(id, params, channel, pong);
-	}
-
-	/**
-	 * チャンネル切断要求時
-	 */
-	@autobind
-	private onChannelDisconnectRequested(payload: any) {
-		const { id } = payload;
-		this.disconnectChannel(id);
-	}
-
-	/**
-	 * クライアントにメッセージ送信
-	 */
-	@autobind
-	public sendMessageToWs(type: string, payload: any) {
-		this.wsConnection.send(JSON.stringify({
-			type: type,
-			body: payload
-		}));
-	}
-
-	/**
-	 * チャンネルに接続
-	 */
-	@autobind
-	public connectChannel(id: string, params: any, channel: string, pong = false) {
-		if ((channels as any)[channel].requireCredential && this.user == null) {
-			return;
-		}
-
-		// 共有可能チャンネルに接続しようとしていて、かつそのチャンネルに既に接続していたら無意味なので無視
-		if ((channels as any)[channel].shouldShare && this.channels.some(c => c.chName === channel)) {
-			return;
-		}
-
-		const ch: Channel = new (channels as any)[channel](id, this);
-		this.channels.push(ch);
-		ch.init(params);
-
-		if (pong) {
-			this.sendMessageToWs('connected', {
-				id: id
-			});
-		}
-	}
-
-	/**
-	 * チャンネルから切断
-	 * @param id チャンネルコネクションID
-	 */
-	@autobind
-	public disconnectChannel(id: string) {
-		const channel = this.channels.find(c => c.id === id);
-
-		if (channel) {
-			if (channel.dispose) channel.dispose();
-			this.channels = this.channels.filter(c => c.id !== id);
-		}
-	}
-
-	/**
-	 * チャンネルへメッセージ送信要求時
-	 * @param data メッセージ
-	 */
-	@autobind
-	private onChannelMessageRequested(data: any) {
-		const channel = this.channels.find(c => c.id === data.id);
-		if (channel != null && channel.onMessage != null) {
-			channel.onMessage(data.type, data.body);
-		}
-	}
-
-	@autobind
-	private typingOnChannel(channel: ChannelModel['id']) {
-		if (this.user) {
-			publishChannelStream(channel, 'typing', this.user.id);
-		}
-	}
-
-	@autobind
-	private typingOnMessaging(param: { partner?: User['id']; group?: UserGroup['id']; }) {
-		if (this.user) {
-			if (param.partner) {
-				publishMessagingStream(param.partner, this.user.id, 'typing', this.user.id);
-			} else if (param.group) {
-				publishGroupMessagingStream(param.group, 'typing', this.user.id);
-			}
-		}
-	}
-
-	@autobind
-	private async updateFollowing() {
-		const followings = await Followings.find({
-			where: {
-				followerId: this.user!.id
-			},
-			select: ['followeeId']
-		});
-
-		this.following = new Set<string>(followings.map(x => x.followeeId));
-	}
-
-	@autobind
-	private async updateMuting() {
-		const mutings = await Mutings.find({
-			where: {
-				muterId: this.user!.id
-			},
-			select: ['muteeId']
-		});
-
-		this.muting = new Set<string>(mutings.map(x => x.muteeId));
-	}
-
-	@autobind
-	private async updateBlocking() { // ここでいうBlockingは被Blockingの意
-		const blockings = await Blockings.find({
-			where: {
-				blockeeId: this.user!.id
-			},
-			select: ['blockerId']
-		});
-
-		this.blocking = new Set<string>(blockings.map(x => x.blockerId));
-	}
-
-	@autobind
-	private async updateFollowingChannels() {
-		const followings = await ChannelFollowings.find({
-			where: {
-				followerId: this.user!.id
-			},
-			select: ['followeeId']
-		});
-
-		this.followingChannels = new Set<string>(followings.map(x => x.followeeId));
-	}
-
-	@autobind
-	private async updateUserProfile() {
-		this.userProfile = await UserProfiles.findOne({
-			userId: this.user!.id
-		});
-	}
-
-	/**
-	 * ストリームが切れたとき
-	 */
-	@autobind
-	public dispose() {
-		for (const c of this.channels.filter(c => c.dispose)) {
-			if (c.dispose) c.dispose();
-		}
-	}
-}
diff --git a/src/server/api/stream/types.ts b/src/server/api/stream/types.ts
deleted file mode 100644
index 70eb5c5ce5..0000000000
--- a/src/server/api/stream/types.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-import { EventEmitter } from 'events';
-import Emitter from 'strict-event-emitter-types';
-import { Channel } from '@/models/entities/channel';
-import { User } from '@/models/entities/user';
-import { UserProfile } from '@/models/entities/user-profile';
-import { Note } from '@/models/entities/note';
-import { Antenna } from '@/models/entities/antenna';
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFolder } from '@/models/entities/drive-folder';
-import { Emoji } from '@/models/entities/emoji';
-import { UserList } from '@/models/entities/user-list';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-import { UserGroup } from '@/models/entities/user-group';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { AbuseUserReport } from '@/models/entities/abuse-user-report';
-import { Signin } from '@/models/entities/signin';
-import { Page } from '@/models/entities/page';
-import { Packed } from '@/misc/schema';
-
-//#region Stream type-body definitions
-export interface InternalStreamTypes {
-	antennaCreated: Antenna;
-	antennaDeleted: Antenna;
-	antennaUpdated: Antenna;
-}
-
-export interface BroadcastTypes {
-	emojiAdded: {
-		emoji: Packed<'Emoji'>;
-	};
-}
-
-export interface UserStreamTypes {
-	terminate: {};
-	followChannel: Channel;
-	unfollowChannel: Channel;
-	updateUserProfile: UserProfile;
-	mute: User;
-	unmute: User;
-	follow: Packed<'User'>;
-	unfollow: Packed<'User'>;
-	userAdded: Packed<'User'>;
-}
-
-export interface MainStreamTypes {
-	notification: Packed<'Notification'>;
-	mention: Packed<'Note'>;
-	reply: Packed<'Note'>;
-	renote: Packed<'Note'>;
-	follow: Packed<'User'>;
-	followed: Packed<'User'>;
-	unfollow: Packed<'User'>;
-	meUpdated: Packed<'User'>;
-	pageEvent: {
-		pageId: Page['id'];
-		event: string;
-		var: any;
-		userId: User['id'];
-		user: Packed<'User'>;
-	};
-	urlUploadFinished: {
-		marker?: string | null;
-		file: Packed<'DriveFile'>;
-	};
-	readAllNotifications: undefined;
-	unreadNotification: Packed<'Notification'>;
-	unreadMention: Note['id'];
-	readAllUnreadMentions: undefined;
-	unreadSpecifiedNote: Note['id'];
-	readAllUnreadSpecifiedNotes: undefined;
-	readAllMessagingMessages: undefined;
-	messagingMessage: Packed<'MessagingMessage'>;
-	unreadMessagingMessage: Packed<'MessagingMessage'>;
-	readAllAntennas: undefined;
-	unreadAntenna: Antenna;
-	readAllAnnouncements: undefined;
-	readAllChannels: undefined;
-	unreadChannel: Note['id'];
-	myTokenRegenerated: undefined;
-	reversiNoInvites: undefined;
-	reversiInvited: Packed<'ReversiMatching'>;
-	signin: Signin;
-	registryUpdated: {
-		scope?: string[];
-		key: string;
-		value: any | null;
-	};
-	driveFileCreated: Packed<'DriveFile'>;
-	readAntenna: Antenna;
-}
-
-export interface DriveStreamTypes {
-	fileCreated: Packed<'DriveFile'>;
-	fileDeleted: DriveFile['id'];
-	fileUpdated: Packed<'DriveFile'>;
-	folderCreated: Packed<'DriveFolder'>;
-	folderDeleted: DriveFolder['id'];
-	folderUpdated: Packed<'DriveFolder'>;
-}
-
-export interface NoteStreamTypes {
-	pollVoted: {
-		choice: number;
-		userId: User['id'];
-	};
-	deleted: {
-		deletedAt: Date;
-	};
-	reacted: {
-		reaction: string;
-		emoji?: Emoji;
-		userId: User['id'];
-	};
-	unreacted: {
-		reaction: string;
-		userId: User['id'];
-	};
-}
-type NoteStreamEventTypes = {
-	[key in keyof NoteStreamTypes]: {
-		id: Note['id'];
-		body: NoteStreamTypes[key];
-	};
-};
-
-export interface ChannelStreamTypes {
-	typing: User['id'];
-}
-
-export interface UserListStreamTypes {
-	userAdded: Packed<'User'>;
-	userRemoved: Packed<'User'>;
-}
-
-export interface AntennaStreamTypes {
-	note: Note;
-}
-
-export interface MessagingStreamTypes {
-	read: MessagingMessage['id'][];
-	typing: User['id'];
-	message: Packed<'MessagingMessage'>;
-	deleted: MessagingMessage['id'];
-}
-
-export interface GroupMessagingStreamTypes {
-	read: {
-		ids: MessagingMessage['id'][];
-		userId: User['id'];
-	};
-	typing: User['id'];
-	message: Packed<'MessagingMessage'>;
-	deleted: MessagingMessage['id'];
-}
-
-export interface MessagingIndexStreamTypes {
-	read: MessagingMessage['id'][];
-	message: Packed<'MessagingMessage'>;
-}
-
-export interface ReversiStreamTypes {
-	matched: Packed<'ReversiGame'>;
-	invited: Packed<'ReversiMatching'>;
-}
-
-export interface ReversiGameStreamTypes {
-	started: Packed<'ReversiGame'>;
-	ended: {
-		winnerId?: User['id'] | null,
-		game: Packed<'ReversiGame'>;
-	};
-	updateSettings: {
-		key: string;
-		value: FIXME;
-	};
-	initForm: {
-		userId: User['id'];
-		form: FIXME;
-	};
-	updateForm: {
-		userId: User['id'];
-		id: string;
-		value: FIXME;
-	};
-	message: {
-		userId: User['id'];
-		message: FIXME;
-	};
-	changeAccepts: {
-		user1: boolean;
-		user2: boolean;
-	};
-	set: {
-		at: Date;
-		color: boolean;
-		pos: number;
-		next: boolean;
-	};
-	watching: User['id'];
-}
-
-export interface AdminStreamTypes {
-	newAbuseUserReport: {
-		id: AbuseUserReport['id'];
-		targetUserId: User['id'],
-		reporterId: User['id'],
-		comment: string;
-	};
-}
-//#endregion
-
-// 辞書(interface or type)から{ type, body }ユニオンを定義
-// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
-// VS Codeの展開を防止するためにEvents型を定義
-type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
-type EventUnionFromDictionary<
-	T extends object,
-	U = Events<T>
-> = U[keyof U];
-
-// name/messages(spec) pairs dictionary
-export type StreamMessages = {
-	internal: {
-		name: 'internal';
-		payload: EventUnionFromDictionary<InternalStreamTypes>;
-	};
-	broadcast: {
-		name: 'broadcast';
-		payload: EventUnionFromDictionary<BroadcastTypes>;
-	};
-	user: {
-		name: `user:${User['id']}`;
-		payload: EventUnionFromDictionary<UserStreamTypes>;
-	};
-	main: {
-		name: `mainStream:${User['id']}`;
-		payload: EventUnionFromDictionary<MainStreamTypes>;
-	};
-	drive: {
-		name: `driveStream:${User['id']}`;
-		payload: EventUnionFromDictionary<DriveStreamTypes>;
-	};
-	note: {
-		name: `noteStream:${Note['id']}`;
-		payload: EventUnionFromDictionary<NoteStreamEventTypes>;
-	};
-	channel: {
-		name: `channelStream:${Channel['id']}`;
-		payload: EventUnionFromDictionary<ChannelStreamTypes>;
-	};
-	userList: {
-		name: `userListStream:${UserList['id']}`;
-		payload: EventUnionFromDictionary<UserListStreamTypes>;
-	};
-	antenna: {
-		name: `antennaStream:${Antenna['id']}`;
-		payload: EventUnionFromDictionary<AntennaStreamTypes>;
-	};
-	messaging: {
-		name: `messagingStream:${User['id']}-${User['id']}`;
-		payload: EventUnionFromDictionary<MessagingStreamTypes>;
-	};
-	groupMessaging: {
-		name: `messagingStream:${UserGroup['id']}`;
-		payload: EventUnionFromDictionary<GroupMessagingStreamTypes>;
-	};
-	messagingIndex: {
-		name: `messagingIndexStream:${User['id']}`;
-		payload: EventUnionFromDictionary<MessagingIndexStreamTypes>;
-	};
-	reversi: {
-		name: `reversiStream:${User['id']}`;
-		payload: EventUnionFromDictionary<ReversiStreamTypes>;
-	};
-	reversiGame: {
-		name: `reversiGameStream:${ReversiGame['id']}`;
-		payload: EventUnionFromDictionary<ReversiGameStreamTypes>;
-	};
-	admin: {
-		name: `adminStream:${User['id']}`;
-		payload: EventUnionFromDictionary<AdminStreamTypes>;
-	};
-	notes: {
-		name: 'notesStream';
-		payload: Packed<'Note'>;
-	};
-};
-
-// API event definitions
-// ストリームごとのEmitterの辞書を用意
-type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> };
-// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
-type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
-// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
-export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
-// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
-
-// provide stream channels union
-export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
deleted file mode 100644
index 8808bc9860..0000000000
--- a/src/server/api/streaming.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import * as http from 'http';
-import * as websocket from 'websocket';
-
-import MainStreamConnection from './stream/index';
-import { ParsedUrlQuery } from 'querystring';
-import authenticate from './authenticate';
-import { EventEmitter } from 'events';
-import { subsdcriber as redisClient } from '../../db/redis';
-import { Users } from '@/models/index';
-
-module.exports = (server: http.Server) => {
-	// Init websocket server
-	const ws = new websocket.server({
-		httpServer: server
-	});
-
-	ws.on('request', async (request) => {
-		const q = request.resourceURL.query as ParsedUrlQuery;
-
-		// TODO: トークンが間違ってるなどしてauthenticateに失敗したら
-		// コネクション切断するなりエラーメッセージ返すなりする
-		// (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので)
-		const [user, app] = await authenticate(q.i as string);
-
-		if (user?.isSuspended) {
-			request.reject(400);
-			return;
-		}
-
-		const connection = request.accept();
-
-		const ev = new EventEmitter();
-
-		async function onRedisMessage(_: string, data: string) {
-			const parsed = JSON.parse(data);
-			ev.emit(parsed.channel, parsed.message);
-		}
-
-		redisClient.on('message', onRedisMessage);
-
-		const main = new MainStreamConnection(connection, ev, user, app);
-
-		const intervalId = user ? setInterval(() => {
-			Users.update(user.id, {
-				lastActiveDate: new Date(),
-			});
-		}, 1000 * 60 * 5) : null;
-		if (user) {
-			Users.update(user.id, {
-				lastActiveDate: new Date(),
-			});
-		}
-
-		connection.once('close', () => {
-			ev.removeAllListeners();
-			main.dispose();
-			redisClient.off('message', onRedisMessage);
-			if (intervalId) clearInterval(intervalId);
-		});
-
-		connection.on('message', async (data) => {
-			if (data.utf8Data === 'ping') {
-				connection.send('pong');
-			}
-		});
-	});
-};
diff --git a/src/server/file/assets/bad-egg.png b/src/server/file/assets/bad-egg.png
deleted file mode 100644
index e96ba0dcc1..0000000000
Binary files a/src/server/file/assets/bad-egg.png and /dev/null differ
diff --git a/src/server/file/assets/cache-expired.png b/src/server/file/assets/cache-expired.png
deleted file mode 100644
index 5d988c502b..0000000000
Binary files a/src/server/file/assets/cache-expired.png and /dev/null differ
diff --git a/src/server/file/assets/dummy.png b/src/server/file/assets/dummy.png
deleted file mode 100644
index 39332b0c1b..0000000000
Binary files a/src/server/file/assets/dummy.png and /dev/null differ
diff --git a/src/server/file/assets/not-an-image.png b/src/server/file/assets/not-an-image.png
deleted file mode 100644
index 39e4aa0892..0000000000
Binary files a/src/server/file/assets/not-an-image.png and /dev/null differ
diff --git a/src/server/file/assets/thumbnail-not-available.png b/src/server/file/assets/thumbnail-not-available.png
deleted file mode 100644
index 07cad9919c..0000000000
Binary files a/src/server/file/assets/thumbnail-not-available.png and /dev/null differ
diff --git a/src/server/file/assets/tombstone.png b/src/server/file/assets/tombstone.png
deleted file mode 100644
index 83159d6b3c..0000000000
Binary files a/src/server/file/assets/tombstone.png and /dev/null differ
diff --git a/src/server/file/index.ts b/src/server/file/index.ts
deleted file mode 100644
index a455acd1cf..0000000000
--- a/src/server/file/index.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * File Server
- */
-
-import * as fs from 'fs';
-import { fileURLToPath } from 'url';
-import { dirname } from 'path';
-import * as Koa from 'koa';
-import * as cors from '@koa/cors';
-import * as Router from '@koa/router';
-import sendDriveFile from './send-drive-file';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-// Init app
-const app = new Koa();
-app.use(cors());
-app.use(async (ctx, next) => {
-	ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`);
-	await next();
-});
-
-// Init router
-const router = new Router();
-
-router.get('/app-default.jpg', ctx => {
-	const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
-	ctx.body = file;
-	ctx.set('Content-Type', 'image/jpeg');
-	ctx.set('Cache-Control', 'max-age=31536000, immutable');
-});
-
-router.get('/:key', sendDriveFile);
-router.get('/:key/(.*)', sendDriveFile);
-
-// Register router
-app.use(router.routes());
-
-module.exports = app;
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
deleted file mode 100644
index 1908c969a5..0000000000
--- a/src/server/file/send-drive-file.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import * as fs from 'fs';
-import { fileURLToPath } from 'url';
-import { dirname } from 'path';
-import * as Koa from 'koa';
-import * as send from 'koa-send';
-import * as rename from 'rename';
-import * as tmp from 'tmp';
-import { serverLogger } from '../index';
-import { contentDisposition } from '@/misc/content-disposition';
-import { DriveFiles } from '@/models/index';
-import { InternalStorage } from '@/services/drive/internal-storage';
-import { downloadUrl } from '@/misc/download-url';
-import { detectType } from '@/misc/get-file-info';
-import { convertToJpeg, convertToPngOrJpeg } from '@/services/drive/image-processor';
-import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail';
-import { StatusError } from '@/misc/fetch';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-const assets = `${_dirname}/../../server/file/assets/`;
-
-const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => {
-	serverLogger.error(e);
-	ctx.status = 500;
-	ctx.set('Cache-Control', 'max-age=300');
-};
-
-export default async function(ctx: Koa.Context) {
-	const key = ctx.params.key;
-
-	// Fetch drive file
-	const file = await DriveFiles.createQueryBuilder('file')
-		.where('file.accessKey = :accessKey', { accessKey: key })
-		.orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
-		.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
-		.getOne();
-
-	if (file == null) {
-		ctx.status = 404;
-		ctx.set('Cache-Control', 'max-age=86400');
-		await send(ctx as any, '/dummy.png', { root: assets });
-		return;
-	}
-
-	const isThumbnail = file.thumbnailAccessKey === key;
-	const isWebpublic = file.webpublicAccessKey === key;
-
-	if (!file.storedInternal) {
-		if (file.isLink && file.uri) {	// 期限切れリモートファイル
-			const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
-				tmp.file((e, path, fd, cleanup) => {
-					if (e) return rej(e);
-					res([path, cleanup]);
-				});
-			});
-
-			try {
-				await downloadUrl(file.uri, path);
-
-				const { mime, ext } = await detectType(path);
-
-				const convertFile = async () => {
-					if (isThumbnail) {
-						if (['image/jpeg', 'image/webp'].includes(mime)) {
-							return await convertToJpeg(path, 498, 280);
-						} else if (['image/png'].includes(mime)) {
-							return await convertToPngOrJpeg(path, 498, 280);
-						} else if (mime.startsWith('video/')) {
-							return await GenerateVideoThumbnail(path);
-						}
-					}
-
-					return {
-						data: fs.readFileSync(path),
-						ext,
-						type: mime,
-					};
-				};
-
-				const image = await convertFile();
-				ctx.body = image.data;
-				ctx.set('Content-Type', image.type);
-				ctx.set('Cache-Control', 'max-age=31536000, immutable');
-			} catch (e) {
-				serverLogger.error(`${e}`);
-
-				if (e instanceof StatusError && e.isClientError) {
-					ctx.status = e.statusCode;
-					ctx.set('Cache-Control', 'max-age=86400');
-				} else {
-					ctx.status = 500;
-					ctx.set('Cache-Control', 'max-age=300');
-				}
-			} finally {
-				cleanup();
-			}
-			return;
-		}
-
-		ctx.status = 204;
-		ctx.set('Cache-Control', 'max-age=86400');
-		return;
-	}
-
-	if (isThumbnail || isWebpublic) {
-		const { mime, ext } = await detectType(InternalStorage.resolvePath(key));
-		const filename = rename(file.name, {
-			suffix: isThumbnail ? '-thumb' : '-web',
-			extname: ext ? `.${ext}` : undefined
-		}).toString();
-
-		ctx.body = InternalStorage.read(key);
-		ctx.set('Content-Type', mime);
-		ctx.set('Cache-Control', 'max-age=31536000, immutable');
-		ctx.set('Content-Disposition', contentDisposition('inline', filename));
-	} else {
-		const readable = InternalStorage.read(file.accessKey!);
-		readable.on('error', commonReadableHandlerGenerator(ctx));
-		ctx.body = readable;
-		ctx.set('Content-Type', file.type);
-		ctx.set('Cache-Control', 'max-age=31536000, immutable');
-		ctx.set('Content-Disposition', contentDisposition('inline', file.name));
-	}
-}
diff --git a/src/server/index.ts b/src/server/index.ts
deleted file mode 100644
index 5e1a12e4d3..0000000000
--- a/src/server/index.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- * Core Server
- */
-
-import * as fs from 'fs';
-import * as http from 'http';
-import * as http2 from 'http2';
-import * as https from 'https';
-import * as Koa from 'koa';
-import * as Router from '@koa/router';
-import * as mount from 'koa-mount';
-import * as koaLogger from 'koa-logger';
-import * as requestStats from 'request-stats';
-import * as slow from 'koa-slow';
-
-import activityPub from './activitypub';
-import nodeinfo from './nodeinfo';
-import wellKnown from './well-known';
-import config from '@/config/index';
-import apiServer from './api/index';
-import { sum } from '@/prelude/array';
-import Logger from '@/services/logger';
-import { envOption } from '../env';
-import { UserProfiles, Users } from '@/models/index';
-import { networkChart } from '@/services/chart/index';
-import { genAvatar } from '@/misc/gen-avatar';
-import { createTemp } from '@/misc/create-temp';
-import { publishMainStream } from '@/services/stream';
-import { parseAcct } from '@/misc/acct';
-
-export const serverLogger = new Logger('server', 'gray', false);
-
-// Init app
-const app = new Koa();
-app.proxy = true;
-
-if (!['production', 'test'].includes(process.env.NODE_ENV || '')) {
-	// Logger
-	app.use(koaLogger(str => {
-		serverLogger.info(str);
-	}));
-
-	// Delay
-	if (envOption.slow) {
-		app.use(slow({
-			delay: 3000
-		}));
-	}
-}
-
-// HSTS
-// 6months (15552000sec)
-if (config.url.startsWith('https') && !config.disableHsts) {
-	app.use(async (ctx, next) => {
-		ctx.set('strict-transport-security', 'max-age=15552000; preload');
-		await next();
-	});
-}
-
-app.use(mount('/api', apiServer));
-app.use(mount('/files', require('./file')));
-app.use(mount('/proxy', require('./proxy')));
-
-// Init router
-const router = new Router();
-
-// Routing
-router.use(activityPub.routes());
-router.use(nodeinfo.routes());
-router.use(wellKnown.routes());
-
-router.get('/avatar/@:acct', async ctx => {
-	const { username, host } = parseAcct(ctx.params.acct);
-	const user = await Users.findOne({
-		usernameLower: username.toLowerCase(),
-		host: host === config.host ? null : host,
-		isSuspended: false
-	});
-
-	if (user) {
-		ctx.redirect(Users.getAvatarUrl(user));
-	} else {
-		ctx.redirect('/static-assets/user-unknown.png');
-	}
-});
-
-router.get('/random-avatar/:x', async ctx => {
-	const [temp] = await createTemp();
-	await genAvatar(ctx.params.x, fs.createWriteStream(temp));
-	ctx.set('Content-Type', 'image/png');
-	ctx.body = fs.createReadStream(temp);
-});
-
-router.get('/verify-email/:code', async ctx => {
-	const profile = await UserProfiles.findOne({
-		emailVerifyCode: ctx.params.code
-	});
-
-	if (profile != null) {
-		ctx.body = 'Verify succeeded!';
-		ctx.status = 200;
-
-		await UserProfiles.update({ userId: profile.userId }, {
-			emailVerified: true,
-			emailVerifyCode: null
-		});
-
-		publishMainStream(profile.userId, 'meUpdated', await Users.pack(profile.userId, { id: profile.userId }, {
-			detail: true,
-			includeSecrets: true
-		}));
-	} else {
-		ctx.status = 404;
-	}
-});
-
-// Register router
-app.use(router.routes());
-
-app.use(mount(require('./web')));
-
-function createServer() {
-	if (config.https) {
-		const certs: any = {};
-		for (const k of Object.keys(config.https)) {
-			certs[k] = fs.readFileSync(config.https[k]);
-		}
-		certs['allowHTTP1'] = true;
-		return http2.createSecureServer(certs, app.callback()) as https.Server;
-	} else {
-		return http.createServer(app.callback());
-	}
-}
-
-// For testing
-export const startServer = () => {
-	const server = createServer();
-
-	// Init stream server
-	require('./api/streaming')(server);
-
-	// Listen
-	server.listen(config.port);
-
-	return server;
-};
-
-export default () => new Promise(resolve => {
-	const server = createServer();
-
-	// Init stream server
-	require('./api/streaming')(server);
-
-	// Listen
-	server.listen(config.port, resolve);
-
-	//#region Network stats
-	let queue: any[] = [];
-
-	requestStats(server, (stats: any) => {
-		if (stats.ok) {
-			queue.push(stats);
-		}
-	});
-
-	// Bulk write
-	setInterval(() => {
-		if (queue.length === 0) return;
-
-		const requests = queue.length;
-		const time = sum(queue.map(x => x.time));
-		const incomingBytes = sum(queue.map(x => x.req.byets));
-		const outgoingBytes = sum(queue.map(x => x.res.byets));
-		queue = [];
-
-		networkChart.update(requests, time, incomingBytes, outgoingBytes);
-	}, 5000);
-	//#endregion
-});
diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts
deleted file mode 100644
index 6a864fcc52..0000000000
--- a/src/server/nodeinfo.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import * as Router from '@koa/router';
-import config from '@/config/index';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Users } from '@/models/index';
-// import User from '../models/user';
-// import Note from '../models/note';
-
-const router = new Router();
-
-const nodeinfo2_1path = '/nodeinfo/2.1';
-const nodeinfo2_0path = '/nodeinfo/2.0';
-
-export const links = [/* (awaiting release) {
-	rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
-	href: config.url + nodeinfo2_1path
-}, */{
-	rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
-	href: config.url + nodeinfo2_0path
-}];
-
-const nodeinfo2 = async () => {
-	const [
-		meta,
-		// total,
-		// activeHalfyear,
-		// activeMonth,
-		// localPosts,
-		// localComments
-	] = await Promise.all([
-		fetchMeta(true),
-		// User.count({ host: null }),
-		// User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 15552000000) } }),
-		// User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 2592000000) } }),
-		// Note.count({ '_user.host': null, replyId: null }),
-		// Note.count({ '_user.host': null, replyId: { $ne: null } })
-	]);
-
-	const proxyAccount = meta.proxyAccountId ? await Users.pack(meta.proxyAccountId).catch(() => null) : null;
-
-	return {
-		software: {
-			name: 'misskey',
-			version: config.version,
-			repository: meta.repositoryUrl,
-		},
-		protocols: ['activitypub'],
-		services: {
-			inbound: [] as string[],
-			outbound: ['atom1.0', 'rss2.0']
-		},
-		openRegistrations: !meta.disableRegistration,
-		usage: {
-			users: {} // { total, activeHalfyear, activeMonth },
-			// localPosts,
-			// localComments
-		},
-		metadata: {
-			nodeName: meta.name,
-			nodeDescription: meta.description,
-			maintainer: {
-				name: meta.maintainerName,
-				email: meta.maintainerEmail
-			},
-			langs: meta.langs,
-			tosUrl: meta.ToSUrl,
-			repositoryUrl: meta.repositoryUrl,
-			feedbackUrl: meta.feedbackUrl,
-			disableRegistration: meta.disableRegistration,
-			disableLocalTimeline: meta.disableLocalTimeline,
-			disableGlobalTimeline: meta.disableGlobalTimeline,
-			emailRequiredForSignup: meta.emailRequiredForSignup,
-			enableHcaptcha: meta.enableHcaptcha,
-			enableRecaptcha: meta.enableRecaptcha,
-			maxNoteTextLength: meta.maxNoteTextLength,
-			enableTwitterIntegration: meta.enableTwitterIntegration,
-			enableGithubIntegration: meta.enableGithubIntegration,
-			enableDiscordIntegration: meta.enableDiscordIntegration,
-			enableEmail: meta.enableEmail,
-			enableServiceWorker: meta.enableServiceWorker,
-			proxyAccountName: proxyAccount ? proxyAccount.username : null,
-		}
-	};
-};
-
-router.get(nodeinfo2_1path, async ctx => {
-	const base = await nodeinfo2();
-
-	ctx.body = { version: '2.1', ...base };
-	ctx.set('Cache-Control', 'public, max-age=600');
-});
-
-router.get(nodeinfo2_0path, async ctx => {
-	const base = await nodeinfo2();
-
-	delete base.software.repository;
-
-	ctx.body = { version: '2.0', ...base };
-	ctx.set('Cache-Control', 'public, max-age=600');
-});
-
-export default router;
diff --git a/src/server/proxy/index.ts b/src/server/proxy/index.ts
deleted file mode 100644
index b8993f19f8..0000000000
--- a/src/server/proxy/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Media Proxy
- */
-
-import * as Koa from 'koa';
-import * as cors from '@koa/cors';
-import * as Router from '@koa/router';
-import { proxyMedia } from './proxy-media';
-
-// Init app
-const app = new Koa();
-app.use(cors());
-app.use(async (ctx, next) => {
-	ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`);
-	await next();
-});
-
-// Init router
-const router = new Router();
-
-router.get('/:url*', proxyMedia);
-
-// Register router
-app.use(router.routes());
-
-module.exports = app;
diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts
deleted file mode 100644
index 9e13c0877f..0000000000
--- a/src/server/proxy/proxy-media.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as fs from 'fs';
-import * as Koa from 'koa';
-import { serverLogger } from '../index';
-import { IImage, convertToPng, convertToJpeg } from '@/services/drive/image-processor';
-import { createTemp } from '@/misc/create-temp';
-import { downloadUrl } from '@/misc/download-url';
-import { detectType } from '@/misc/get-file-info';
-import { StatusError } from '@/misc/fetch';
-
-export async function proxyMedia(ctx: Koa.Context) {
-	const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
-
-	// Create temp file
-	const [path, cleanup] = await createTemp();
-
-	try {
-		await downloadUrl(url, path);
-
-		const { mime, ext } = await detectType(path);
-
-		if (!mime.startsWith('image/')) throw 403;
-
-		let image: IImage;
-
-		if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp'].includes(mime)) {
-			image = await convertToPng(path, 498, 280);
-		} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) {
-			image = await convertToJpeg(path, 200, 200);
-		} else {
-			image = {
-				data: fs.readFileSync(path),
-				ext,
-				type: mime,
-			};
-		}
-
-		ctx.set('Content-Type', image.type);
-		ctx.set('Cache-Control', 'max-age=31536000, immutable');
-		ctx.body = image.data;
-	} catch (e) {
-		serverLogger.error(`${e}`);
-
-		if (e instanceof StatusError && e.isClientError) {
-			ctx.status = e.statusCode;
-		} else {
-			ctx.status = 500;
-		}
-	} finally {
-		cleanup();
-	}
-}
diff --git a/src/server/web/bios.css b/src/server/web/bios.css
deleted file mode 100644
index b0da3ee39b..0000000000
--- a/src/server/web/bios.css
+++ /dev/null
@@ -1,40 +0,0 @@
-* {
-	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
-}
-
-html {
-	background: #ffb4e1;
-}
-
-main {
-	background: #dedede;
-}
-main > .tabs {
-	padding: 16px;
-	border-bottom: solid 4px #c3c3c3;
-}
-
-#lsEditor > .adder {
-	margin: 16px;
-	padding: 16px;
-	border: solid 2px #c3c3c3;
-}
-#lsEditor > .adder > textarea {
-	display: block;
-	width: 100%;
-	min-height: 5em;
-	box-sizing: border-box;
-}
-#lsEditor > .record {
-	padding: 16px;
-	border-bottom: solid 1px #c3c3c3;
-}
-#lsEditor > .record > header {
-	font-weight: bold;
-}
-#lsEditor > .record > textarea {
-	display: block;
-	width: 100%;
-	min-height: 5em;
-	box-sizing: border-box;
-}
diff --git a/src/server/web/bios.js b/src/server/web/bios.js
deleted file mode 100644
index d06dee801a..0000000000
--- a/src/server/web/bios.js
+++ /dev/null
@@ -1,87 +0,0 @@
-'use strict';
-
-window.onload = async () => {
-	const account = JSON.parse(localStorage.getItem('account'));
-	const i = account.token;
-
-	const api = (endpoint, data = {}) => {
-		const promise = new Promise((resolve, reject) => {
-			// Append a credential
-			if (i) data.i = i;
-	
-			// Send request
-			fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
-				method: 'POST',
-				body: JSON.stringify(data),
-				credentials: 'omit',
-				cache: 'no-cache'
-			}).then(async (res) => {
-				const body = res.status === 204 ? null : await res.json();
-	
-				if (res.status === 200) {
-					resolve(body);
-				} else if (res.status === 204) {
-					resolve();
-				} else {
-					reject(body.error);
-				}
-			}).catch(reject);
-		});
-		
-		return promise;
-	};
-
-	const content = document.getElementById('content');
-
-	document.getElementById('ls').addEventListener('click', () => {
-		content.innerHTML = '';
-
-		const lsEditor = document.createElement('div');
-		lsEditor.id = 'lsEditor';
-
-		const adder = document.createElement('div');
-		adder.classList.add('adder');
-		const addKeyInput = document.createElement('input');
-		const addValueTextarea = document.createElement('textarea');
-		const addButton = document.createElement('button');
-		addButton.textContent = 'add';
-		addButton.addEventListener('click', () => {
-			localStorage.setItem(addKeyInput.value, addValueTextarea.value);
-			location.reload();
-		});
-
-		adder.appendChild(addKeyInput);
-		adder.appendChild(addValueTextarea);
-		adder.appendChild(addButton);
-		lsEditor.appendChild(adder);
-
-		for (let i = 0; i < localStorage.length; i++) {
-			const k = localStorage.key(i);
-			const record = document.createElement('div');
-			record.classList.add('record');
-			const header = document.createElement('header');
-			header.textContent = k;
-			const textarea = document.createElement('textarea');
-			textarea.textContent = localStorage.getItem(k);
-			const saveButton = document.createElement('button');
-			saveButton.textContent = 'save';
-			saveButton.addEventListener('click', () => {
-				localStorage.setItem(k, textarea.value);
-				location.reload();
-			});
-			const removeButton = document.createElement('button');
-			removeButton.textContent = 'remove';
-			removeButton.addEventListener('click', () => {
-				localStorage.removeItem(k);
-				location.reload();
-			});
-			record.appendChild(header);
-			record.appendChild(textarea);
-			record.appendChild(saveButton);
-			record.appendChild(removeButton);
-			lsEditor.appendChild(record);
-		}
-
-		content.appendChild(lsEditor);
-	});
-};
diff --git a/src/server/web/boot.js b/src/server/web/boot.js
deleted file mode 100644
index d4a2529e63..0000000000
--- a/src/server/web/boot.js
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- * BOOT LOADER
- * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
- * - 翻訳ファイルをフェッチする。
- * - バージョンに基づいて適切なメインスクリプトを読み込む。
- * - キャッシュされたコンパイル済みテーマを適用する。
- * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。
- * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
- * 注: webpackは介さないため、このファイルではrequireやimportは使えません。
- */
-
-'use strict';
-
-// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
-(async () => {
-	window.onerror = (e) => {
-		renderError('SOMETHING_HAPPENED', e.toString());
-	};
-	window.onunhandledrejection = (e) => {
-		renderError('SOMETHING_HAPPENED_IN_PROMISE', e.toString());
-	};
-
-	const v = localStorage.getItem('v') || VERSION;
-
-	//#region Detect language & fetch translations
-	const localeVersion = localStorage.getItem('localeVersion');
-	const localeOutdated = (localeVersion == null || localeVersion !== v);
-
-	if (!localStorage.hasOwnProperty('locale') || localeOutdated) {
-		const supportedLangs = LANGS;
-		let lang = localStorage.getItem('lang');
-		if (lang == null || !supportedLangs.includes(lang)) {
-			if (supportedLangs.includes(navigator.language)) {
-				lang = navigator.language;
-			} else {
-				lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
-
-				// Fallback
-				if (lang == null) lang = 'en-US';
-			}
-		}
-
-		const res = await fetch(`/assets/locales/${lang}.${v}.json`);
-		if (res.status === 200) {
-			localStorage.setItem('lang', lang);
-			localStorage.setItem('locale', await res.text());
-			localStorage.setItem('localeVersion', v);
-		} else if (localeOutdated) {
-			// nop
-		} else {
-			await checkUpdate();
-			renderError('LOCALE_FETCH_FAILED');
-			return;
-		}
-	}
-	//#endregion
-
-	//#region Script
-	const salt = localStorage.getItem('salt')
-		? `?salt=${localStorage.getItem('salt')}`
-		: '';
-
-	const script = document.createElement('script');
-	script.setAttribute('src', `/assets/app.${v}.js${salt}`);
-	script.setAttribute('async', 'true');
-	script.setAttribute('defer', 'true');
-	script.addEventListener('error', async () => {
-		await checkUpdate();
-		renderError('APP_FETCH_FAILED');
-	});
-	document.head.appendChild(script);
-	//#endregion
-
-	//#region Theme
-	const theme = localStorage.getItem('theme');
-	if (theme) {
-		for (const [k, v] of Object.entries(JSON.parse(theme))) {
-			document.documentElement.style.setProperty(`--${k}`, v.toString());
-
-			// HTMLの theme-color 適用
-			if (k === 'htmlThemeColor') {
-				for (const tag of document.head.children) {
-					if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
-						tag.setAttribute('content', v);
-						break;
-					}
-				}
-			}
-		}
-	}
-	//#endregion
-
-	const fontSize = localStorage.getItem('fontSize');
-	if (fontSize) {
-		document.documentElement.classList.add('f-' + fontSize);
-	}
-
-	const useSystemFont = localStorage.getItem('useSystemFont');
-	if (useSystemFont) {
-		document.documentElement.classList.add('useSystemFont');
-	}
-
-	const wallpaper = localStorage.getItem('wallpaper');
-	if (wallpaper) {
-		document.documentElement.style.backgroundImage = `url(${wallpaper})`;
-	}
-
-	const customCss = localStorage.getItem('customCss');
-	if (customCss && customCss.length > 0) {
-		const style = document.createElement('style');
-		style.innerHTML = customCss;
-		document.head.appendChild(style);
-	}
-
-	// eslint-disable-next-line no-inner-declarations
-	function renderError(code, details) {
-		document.documentElement.innerHTML = `
-			<h1>⚠エラーが発生しました</h1>
-			<p>問題が解決しない場合は管理者までお問い合わせください。以下のオプションを試すこともできます:</p>
-			<ul>
-				<li><a href="/cli">簡易クライアント</a>を起動</li>
-				<li><a href="/bios">BIOS</a>で修復を試みる</li>
-				<li><a href="/flush">キャッシュをクリア</a>する</li>
-			</ul>
-			<hr>
-			<code>ERROR CODE: ${code}</code>
-			<details>
-				${details}
-			</details>
-		`;
-	}
-
-	// eslint-disable-next-line no-inner-declarations
-	async function checkUpdate() {
-		// TODO: サーバーが落ちている場合などのエラーハンドリング
-		const res = await fetch('/api/meta', {
-			method: 'POST',
-			cache: 'no-cache'
-		});
-
-		const meta = await res.json();
-
-		if (meta.version != v) {
-			localStorage.setItem('v', meta.version);
-			refresh();
-		}
-	}
-
-	// eslint-disable-next-line no-inner-declarations
-	function refresh() {
-		// Random
-		localStorage.setItem('salt', Math.random().toString().substr(2, 8));
-
-		// Clear cache (service worker)
-		try {
-			navigator.serviceWorker.controller.postMessage('clear');
-			navigator.serviceWorker.getRegistrations().then(registrations => {
-				registrations.forEach(registration => registration.unregister());
-			});
-		} catch (e) {
-			console.error(e);
-		}
-
-		location.reload();
-	}
-})();
diff --git a/src/server/web/cli.css b/src/server/web/cli.css
deleted file mode 100644
index 07cd27830b..0000000000
--- a/src/server/web/cli.css
+++ /dev/null
@@ -1,19 +0,0 @@
-* {
-	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
-}
-
-html {
-	background: #ffb4e1;
-}
-
-main {
-	background: #dedede;
-}
-
-#tl > div {
-	padding: 16px;
-	border-bottom: solid 1px #c3c3c3;
-}
-#tl > div > header {
-	font-weight: bold;
-}
diff --git a/src/server/web/cli.js b/src/server/web/cli.js
deleted file mode 100644
index 3dff1d4860..0000000000
--- a/src/server/web/cli.js
+++ /dev/null
@@ -1,55 +0,0 @@
-'use strict';
-
-window.onload = async () => {
-	const account = JSON.parse(localStorage.getItem('account'));
-	const i = account.token;
-
-	const api = (endpoint, data = {}) => {
-		const promise = new Promise((resolve, reject) => {
-			// Append a credential
-			if (i) data.i = i;
-	
-			// Send request
-			fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
-				method: 'POST',
-				body: JSON.stringify(data),
-				credentials: 'omit',
-				cache: 'no-cache'
-			}).then(async (res) => {
-				const body = res.status === 204 ? null : await res.json();
-	
-				if (res.status === 200) {
-					resolve(body);
-				} else if (res.status === 204) {
-					resolve();
-				} else {
-					reject(body.error);
-				}
-			}).catch(reject);
-		});
-		
-		return promise;
-	};
-
-	document.getElementById('submit').addEventListener('click', () => {
-		api('notes/create', {
-			text: document.getElementById('text').value
-		}).then(() => {
-			location.reload();
-		});
-	});
-
-	api('notes/timeline').then(notes => {
-		const tl = document.getElementById('tl');
-		for (const note of notes) {
-			const el = document.createElement('div');
-			const name = document.createElement('header');
-			name.textContent = `${note.user.name} @${note.user.username}`;
-			const text = document.createElement('div');
-			text.textContent = `${note.text}`;
-			el.appendChild(name);
-			el.appendChild(text);
-			tl.appendChild(el);
-		}
-	});
-};
diff --git a/src/server/web/feed.ts b/src/server/web/feed.ts
deleted file mode 100644
index 1d4c47dafb..0000000000
--- a/src/server/web/feed.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Feed } from 'feed';
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-import { Notes, DriveFiles, UserProfiles } from '@/models/index';
-import { In } from 'typeorm';
-
-export default async function(user: User) {
-	const author = {
-		link: `${config.url}/@${user.username}`,
-		name: user.name || user.username
-	};
-
-	const profile = await UserProfiles.findOneOrFail(user.id);
-
-	const notes = await Notes.find({
-		where: {
-			userId: user.id,
-			renoteId: null,
-			visibility: In(['public', 'home'])
-		},
-		order: { createdAt: -1 },
-		take: 20
-	});
-
-	const feed = new Feed({
-		id: author.link,
-		title: `${author.name} (@${user.username}@${config.host})`,
-		updated: notes[0].createdAt,
-		generator: 'Misskey',
-		description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
-		link: author.link,
-		image: user.avatarUrl ? user.avatarUrl : undefined,
-		feedLinks: {
-			json: `${author.link}.json`,
-			atom: `${author.link}.atom`,
-		},
-		author,
-		copyright: user.name || user.username
-	});
-
-	for (const note of notes) {
-		const files = note.fileIds.length > 0 ? await DriveFiles.find({
-			id: In(note.fileIds)
-		}) : [];
-		const file = files.find(file => file.type.startsWith('image/'));
-
-		feed.addItem({
-			title: `New note by ${author.name}`,
-			link: `${config.url}/notes/${note.id}`,
-			date: note.createdAt,
-			description: note.cw || undefined,
-			content: note.text || undefined,
-			image: file ? DriveFiles.getPublicUrl(file) || undefined : undefined
-		});
-	}
-
-	return feed;
-}
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
deleted file mode 100644
index 70d5b696ff..0000000000
--- a/src/server/web/index.ts
+++ /dev/null
@@ -1,419 +0,0 @@
-/**
- * Web Client Server
- */
-
-import * as os from 'os';
-import * as fs from 'fs';
-import { dirname } from 'path';
-import * as ms from 'ms';
-import * as Koa from 'koa';
-import * as Router from '@koa/router';
-import * as send from 'koa-send';
-import * as favicon from 'koa-favicon';
-import * as views from 'koa-views';
-
-import packFeed from './feed';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { genOpenapiSpec } from '../api/openapi/gen-spec';
-import config from '@/config/index';
-import { Users, Notes, Emojis, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index';
-import { parseAcct } from '@/misc/acct';
-import { getNoteSummary } from '@/misc/get-note-summary';
-import { getConnection } from 'typeorm';
-import { redisClient } from '../../db/redis';
-import * as locales from '../../../locales/index';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-const staticAssets = `${_dirname}/../../../assets/`;
-const assets = `${_dirname}/../../assets/`;
-
-// Init app
-const app = new Koa();
-
-// Init renderer
-app.use(views(_dirname + '/views', {
-	extension: 'pug',
-	options: {
-		version: config.version,
-		config
-	}
-}));
-
-// Serve favicon
-app.use(favicon(`${_dirname}/../../../assets/favicon.ico`));
-
-// Common request handler
-app.use(async (ctx, next) => {
-	// IFrameの中に入れられないようにする
-	ctx.set('X-Frame-Options', 'DENY');
-	await next();
-});
-
-// Init router
-const router = new Router();
-
-//#region static assets
-
-router.get('/static-assets/(.*)', async ctx => {
-	await send(ctx as any, ctx.path.replace('/static-assets/', ''), {
-		root: staticAssets,
-		maxage: ms('7 days'),
-	});
-});
-
-router.get('/assets/(.*)', async ctx => {
-	await send(ctx as any, ctx.path.replace('/assets/', ''), {
-		root: assets,
-		maxage: ms('7 days'),
-	});
-});
-
-// Apple touch icon
-router.get('/apple-touch-icon.png', async ctx => {
-	await send(ctx as any, '/apple-touch-icon.png', {
-		root: staticAssets
-	});
-});
-
-router.get('/twemoji/(.*)', async ctx => {
-	const path = ctx.path.replace('/twemoji/', '');
-
-	if (!path.match(/^[0-9a-f-]+\.svg$/)) {
-		ctx.status = 404;
-		return;
-	}
-
-	ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`);
-
-	await send(ctx as any, path, {
-		root: `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`,
-		maxage: ms('30 days'),
-	});
-});
-
-// ServiceWorker
-router.get('/sw.js', async ctx => {
-	await send(ctx as any, `/sw.${config.version}.js`, {
-		root: assets
-	});
-});
-
-// Manifest
-router.get('/manifest.json', require('./manifest'));
-
-router.get('/robots.txt', async ctx => {
-	await send(ctx as any, '/robots.txt', {
-		root: staticAssets
-	});
-});
-
-//#endregion
-
-// Docs
-router.get('/api-doc', async ctx => {
-	await send(ctx as any, '/redoc.html', {
-		root: staticAssets
-	});
-});
-
-// URL preview endpoint
-router.get('/url', require('./url-preview'));
-
-router.get('/api.json', async ctx => {
-	ctx.body = genOpenapiSpec();
-});
-
-const getFeed = async (acct: string) => {
-	const { username, host } = parseAcct(acct);
-	const user = await Users.findOne({
-		usernameLower: username.toLowerCase(),
-		host,
-		isSuspended: false
-	});
-
-	return user && await packFeed(user);
-};
-
-// Atom
-router.get('/@:user.atom', async ctx => {
-	const feed = await getFeed(ctx.params.user);
-
-	if (feed) {
-		ctx.set('Content-Type', 'application/atom+xml; charset=utf-8');
-		ctx.body = feed.atom1();
-	} else {
-		ctx.status = 404;
-	}
-});
-
-// RSS
-router.get('/@:user.rss', async ctx => {
-	const feed = await getFeed(ctx.params.user);
-
-	if (feed) {
-		ctx.set('Content-Type', 'application/rss+xml; charset=utf-8');
-		ctx.body = feed.rss2();
-	} else {
-		ctx.status = 404;
-	}
-});
-
-// JSON
-router.get('/@:user.json', async ctx => {
-	const feed = await getFeed(ctx.params.user);
-
-	if (feed) {
-		ctx.set('Content-Type', 'application/json; charset=utf-8');
-		ctx.body = feed.json1();
-	} else {
-		ctx.status = 404;
-	}
-});
-
-//#region SSR (for crawlers)
-// User
-router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
-	const { username, host } = parseAcct(ctx.params.user);
-	const user = await Users.findOne({
-		usernameLower: username.toLowerCase(),
-		host,
-		isSuspended: false
-	});
-
-	if (user != null) {
-		const profile = await UserProfiles.findOneOrFail(user.id);
-		const meta = await fetchMeta();
-		const me = profile.fields
-			? profile.fields
-				.filter(filed => filed.value != null && filed.value.match(/^https?:/))
-				.map(field => field.value)
-			: [];
-
-		await ctx.render('user', {
-			user, profile, me,
-			sub: ctx.params.sub,
-			instanceName: meta.name || 'Misskey',
-			icon: meta.iconUrl
-		});
-		ctx.set('Cache-Control', 'public, max-age=30');
-	} else {
-		// リモートユーザーなので
-		// モデレータがAPI経由で参照可能にするために404にはしない
-		await next();
-	}
-});
-
-router.get('/users/:user', async ctx => {
-	const user = await Users.findOne({
-		id: ctx.params.user,
-		host: null,
-		isSuspended: false
-	});
-
-	if (user == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	ctx.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
-});
-
-// Note
-router.get('/notes/:note', async (ctx, next) => {
-	const note = await Notes.findOne(ctx.params.note);
-
-	if (note) {
-		const _note = await Notes.pack(note);
-		const profile = await UserProfiles.findOneOrFail(note.userId);
-		const meta = await fetchMeta();
-		await ctx.render('note', {
-			note: _note,
-			profile,
-			// TODO: Let locale changeable by instance setting
-			summary: getNoteSummary(_note, locales['ja-JP']),
-			instanceName: meta.name || 'Misskey',
-			icon: meta.iconUrl
-		});
-
-		if (['public', 'home'].includes(note.visibility)) {
-			ctx.set('Cache-Control', 'public, max-age=180');
-		} else {
-			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
-		}
-
-		return;
-	}
-
-	await next();
-});
-
-// Page
-router.get('/@:user/pages/:page', async (ctx, next) => {
-	const { username, host } = parseAcct(ctx.params.user);
-	const user = await Users.findOne({
-		usernameLower: username.toLowerCase(),
-		host
-	});
-
-	if (user == null) return;
-
-	const page = await Pages.findOne({
-		name: ctx.params.page,
-		userId: user.id
-	});
-
-	if (page) {
-		const _page = await Pages.pack(page);
-		const profile = await UserProfiles.findOneOrFail(page.userId);
-		const meta = await fetchMeta();
-		await ctx.render('page', {
-			page: _page,
-			profile,
-			instanceName: meta.name || 'Misskey'
-		});
-
-		if (['public'].includes(page.visibility)) {
-			ctx.set('Cache-Control', 'public, max-age=180');
-		} else {
-			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
-		}
-
-		return;
-	}
-
-	await next();
-});
-
-// Clip
-// TODO: 非publicなclipのハンドリング
-router.get('/clips/:clip', async (ctx, next) => {
-	const clip = await Clips.findOne({
-		id: ctx.params.clip,
-	});
-
-	if (clip) {
-		const _clip = await Clips.pack(clip);
-		const profile = await UserProfiles.findOneOrFail(clip.userId);
-		const meta = await fetchMeta();
-		await ctx.render('clip', {
-			clip: _clip,
-			profile,
-			instanceName: meta.name || 'Misskey'
-		});
-
-		ctx.set('Cache-Control', 'public, max-age=180');
-
-		return;
-	}
-
-	await next();
-});
-
-// Gallery post
-router.get('/gallery/:post', async (ctx, next) => {
-	const post = await GalleryPosts.findOne(ctx.params.post);
-
-	if (post) {
-		const _post = await GalleryPosts.pack(post);
-		const profile = await UserProfiles.findOneOrFail(post.userId);
-		const meta = await fetchMeta();
-		await ctx.render('gallery-post', {
-			post: _post,
-			profile,
-			instanceName: meta.name || 'Misskey',
-			icon: meta.iconUrl
-		});
-
-		ctx.set('Cache-Control', 'public, max-age=180');
-
-		return;
-	}
-
-	await next();
-});
-
-// Channel
-router.get('/channels/:channel', async (ctx, next) => {
-	const channel = await Channels.findOne({
-		id: ctx.params.channel,
-	});
-
-	if (channel) {
-		const _channel = await Channels.pack(channel);
-		const meta = await fetchMeta();
-		await ctx.render('channel', {
-			channel: _channel,
-			instanceName: meta.name || 'Misskey'
-		});
-
-		ctx.set('Cache-Control', 'public, max-age=180');
-
-		return;
-	}
-
-	await next();
-});
-//#endregion
-
-router.get('/_info_card_', async ctx => {
-	const meta = await fetchMeta(true);
-
-	ctx.remove('X-Frame-Options');
-
-	await ctx.render('info-card', {
-		version: config.version,
-		host: config.host,
-		meta: meta,
-		originalUsersCount: await Users.count({ host: null }),
-		originalNotesCount: await Notes.count({ userHost: null })
-	});
-});
-
-router.get('/bios', async ctx => {
-	await ctx.render('bios', {
-		version: config.version,
-	});
-});
-
-router.get('/cli', async ctx => {
-	await ctx.render('cli', {
-		version: config.version,
-	});
-});
-
-const override = (source: string, target: string, depth: number = 0) =>
-	[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
-
-router.get('/othello', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games/reversi', 1)));
-router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games')));
-
-router.get('/flush', async ctx => {
-	await ctx.render('flush');
-});
-
-// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる
-router.get('/streaming', async ctx => {
-	ctx.status = 503;
-	ctx.set('Cache-Control', 'private, max-age=0');
-});
-
-// Render base html for all requests
-router.get('(.*)', async ctx => {
-	const meta = await fetchMeta();
-	await ctx.render('base', {
-		img: meta.bannerUrl,
-		title: meta.name || 'Misskey',
-		instanceName: meta.name || 'Misskey',
-		desc: meta.description,
-		icon: meta.iconUrl
-	});
-	ctx.set('Cache-Control', 'public, max-age=300');
-});
-
-// Register router
-app.use(router.routes());
-
-module.exports = app;
diff --git a/src/server/web/manifest.json b/src/server/web/manifest.json
deleted file mode 100644
index 48030a2980..0000000000
--- a/src/server/web/manifest.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-	"short_name": "Misskey",
-	"name": "Misskey",
-	"start_url": "/",
-	"display": "standalone",
-	"background_color": "#313a42",
-	"theme_color": "#86b300",
-	"icons": [
-		{
-			"src": "/static-assets/icons/192.png",
-			"sizes": "192x192",
-			"type": "image/png"
-		},
-		{
-			"src": "/static-assets/icons/512.png",
-			"sizes": "512x512",
-			"type": "image/png"
-		}
-	],
-	"share_target": {
-		"action": "/share/",
-		"params": {
-			"title": "title",
-			"text": "text",
-			"url": "url"
-		}
-	}
-}
diff --git a/src/server/web/manifest.ts b/src/server/web/manifest.ts
deleted file mode 100644
index 918fe27c03..0000000000
--- a/src/server/web/manifest.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as Koa from 'koa';
-import * as manifest from './manifest.json';
-import { fetchMeta } from '@/misc/fetch-meta';
-
-module.exports = async (ctx: Koa.Context) => {
-	const json = JSON.parse(JSON.stringify(manifest));
-
-	const instance = await fetchMeta(true);
-
-	json.short_name = instance.name || 'Misskey';
-	json.name = instance.name || 'Misskey';
-
-	ctx.set('Cache-Control', 'max-age=300');
-	ctx.body = json;
-};
diff --git a/src/server/web/style.css b/src/server/web/style.css
deleted file mode 100644
index 43fbe1ab06..0000000000
--- a/src/server/web/style.css
+++ /dev/null
@@ -1,29 +0,0 @@
-html {
-	background-color: var(--bg);
-	color: var(--fg);
-}
-
-#splash {
-	position: fixed;
-	z-index: 10000;
-	top: 0;
-	left: 0;
-	width: 100vw;
-	height: 100vh;
-	cursor: wait;
-	background-color: var(--bg);
-	opacity: 1;
-	transition: opacity 0.5s ease;
-}
-
-#splash > img {
-	position: absolute;
-	top: 0;
-	right: 0;
-	bottom: 0;
-	left: 0;
-	margin: auto;
-	width: 64px;
-	height: 64px;
-	pointer-events: none;
-}
diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts
deleted file mode 100644
index 1375420c0a..0000000000
--- a/src/server/web/url-preview.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as Koa from 'koa';
-import summaly from 'summaly';
-import { fetchMeta } from '@/misc/fetch-meta';
-import Logger from '@/services/logger';
-import config from '@/config/index';
-import { query } from '@/prelude/url';
-import { getJson } from '@/misc/fetch';
-
-const logger = new Logger('url-preview');
-
-module.exports = async (ctx: Koa.Context) => {
-	const meta = await fetchMeta();
-
-	logger.info(meta.summalyProxy
-		? `(Proxy) Getting preview of ${ctx.query.url}@${ctx.query.lang} ...`
-		: `Getting preview of ${ctx.query.url}@${ctx.query.lang} ...`);
-
-	try {
-		const summary = meta.summalyProxy ? await getJson(`${meta.summalyProxy}?${query({
-			url: ctx.query.url,
-			lang: ctx.query.lang || 'ja-JP'
-		})}`) : await summaly(ctx.query.url, {
-			followRedirects: false,
-			lang: ctx.query.lang || 'ja-JP'
-		});
-
-		logger.succ(`Got preview of ${ctx.query.url}: ${summary.title}`);
-
-		summary.icon = wrap(summary.icon);
-		summary.thumbnail = wrap(summary.thumbnail);
-
-		// Cache 7days
-		ctx.set('Cache-Control', 'max-age=604800, immutable');
-
-		ctx.body = summary;
-	} catch (e) {
-		logger.warn(`Failed to get preview of ${ctx.query.url}: ${e}`);
-		ctx.status = 200;
-		ctx.set('Cache-Control', 'max-age=86400, immutable');
-		ctx.body = '{}';
-	}
-};
-
-function wrap(url?: string): string | null {
-	return url != null
-		? url.match(/^https?:\/\//)
-			? `${config.url}/proxy/preview.jpg?${query({
-				url,
-				preview: '1'
-			})}`
-			: url
-		: null;
-}
diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug
deleted file mode 100644
index 42c068c403..0000000000
--- a/src/server/web/views/base.pug
+++ /dev/null
@@ -1,60 +0,0 @@
-block vars
-
-doctype html
-
-!= '<!--\n'
-!= '  _____ _         _           \n'
-!= ' |     |_|___ ___| |_ ___ _ _ \n'
-!= ' | | | | |_ -|_ -| \'_| -_| | |\n'
-!= ' |_|_|_|_|___|___|_,_|___|_  |\n'
-!= '                         |___|\n'
-!= ' Thank you for using Misskey!\n'
-!= ' If you are reading this message... how about joining the development?\n'
-!= ' https://github.com/misskey-dev/misskey'
-!= '\n-->\n'
-
-html
-
-	head
-		meta(charset='utf-8')
-		meta(name='application-name' content='Misskey')
-		meta(name='referrer' content='origin')
-		meta(name='theme-color' content='#86b300')
-		meta(name='theme-color-orig' content='#86b300')
-		meta(property='og:site_name' content= instanceName || 'Misskey')
-		meta(name='viewport' content='width=device-width, initial-scale=1')
-		link(rel='icon' href= icon || '/favicon.ico')
-		link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
-		link(rel='manifest' href='/manifest.json')
-		link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
-		link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
-		link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
-		link(rel='preload' href='https://use.fontawesome.com/releases/v5.15.3/css/all.css' as='style')
-		link(rel='stylesheet' href='https://use.fontawesome.com/releases/v5.15.3/css/all.css')
-
-		title
-			block title
-				= title || 'Misskey'
-
-		block desc
-			meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
-
-		block meta
-
-		block og
-			meta(property='og:image' content=img)
-
-		style
-			include ../style.css
-
-		script
-			include ../boot.js
-
-	body
-		noscript: p
-			| JavaScriptを有効にしてください
-			br
-			| Please turn on your JavaScript
-		div#splash
-			img(src='/favicon.ico')
-		block content
diff --git a/src/server/web/views/bios.pug b/src/server/web/views/bios.pug
deleted file mode 100644
index d81a3ee67f..0000000000
--- a/src/server/web/views/bios.pug
+++ /dev/null
@@ -1,20 +0,0 @@
-doctype html
-
-html
-
-	head
-		meta(charset='utf-8')
-		meta(name='application-name' content='Misskey')
-		title Misskey BIOS
-		style
-			include ../bios.css
-		script
-			include ../bios.js
-
-	body
-		header
-			h1 Misskey BIOS #{version}
-		main
-			div.tabs
-				button#ls edit local storage
-			div#content
diff --git a/src/server/web/views/channel.pug b/src/server/web/views/channel.pug
deleted file mode 100644
index 273632f0e0..0000000000
--- a/src/server/web/views/channel.pug
+++ /dev/null
@@ -1,21 +0,0 @@
-extends ./base
-
-block vars
-	- const title = channel.name;
-	- const url = `${config.url}/channels/${channel.id}`;
-
-block title
-	= `${title} | ${instanceName}`
-
-block desc
-	meta(name='description' content= channel.description)
-
-block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= channel.description)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= channel.bannerUrl)
-
-block meta
-	meta(name='twitter:card' content='summary')
diff --git a/src/server/web/views/cli.pug b/src/server/web/views/cli.pug
deleted file mode 100644
index d2cf7c4335..0000000000
--- a/src/server/web/views/cli.pug
+++ /dev/null
@@ -1,21 +0,0 @@
-doctype html
-
-html
-
-	head
-		meta(charset='utf-8')
-		meta(name='application-name' content='Misskey')
-		title Misskey Cli
-		style
-			include ../cli.css
-		script
-			include ../cli.js
-
-	body
-		header
-			h1 Misskey Cli #{version}
-		main
-			div#form
-				textarea#text
-				button#submit submit
-			div#tl
diff --git a/src/server/web/views/clip.pug b/src/server/web/views/clip.pug
deleted file mode 100644
index 8de53f19d6..0000000000
--- a/src/server/web/views/clip.pug
+++ /dev/null
@@ -1,33 +0,0 @@
-extends ./base
-
-block vars
-	- const user = clip.user;
-	- const title = clip.name;
-	- const url = `${config.url}/clips/${clip.id}`;
-
-block title
-	= `${title} | ${instanceName}`
-
-block desc
-	meta(name='description' content= clip.description)
-
-block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= clip.description)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= user.avatarUrl)
-
-block meta
-	if profile.noCrawle
-		meta(name='robots' content='noindex')
-
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
-	meta(name='misskey:clip-id' content=clip.id)
-
-	meta(name='twitter:card' content='summary')
-
-	// todo
-	if user.twitter
-		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/src/server/web/views/flush.pug b/src/server/web/views/flush.pug
deleted file mode 100644
index ec585a34db..0000000000
--- a/src/server/web/views/flush.pug
+++ /dev/null
@@ -1,47 +0,0 @@
-doctype html
-
-html
-	#msg
-	script.
-		const msg = document.getElementById('msg');
-		const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
-
-		message('Start flushing.');
-
-		(async function() {
-			try {
-				localStorage.clear();
-				message('localStorage cleared.');
-
-				const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
-					const delidb = indexedDB.deleteDatabase(name);
-					delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
-					delidb.onerror = e => rej(e)
-				}));
-
-				await Promise.all(idbPromises);
-
-				if (navigator.serviceWorker.controller) {
-					navigator.serviceWorker.controller.postMessage('clear');
-					await navigator.serviceWorker.getRegistrations()
-						.then(registrations => {
-							return Promise.all(registrations.map(registration => registration.unregister()));
-						})
-						.catch(e => { throw Error(e) });
-				}
-
-				message(successText);
-			} catch (e) {
-				message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
-				message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
-
-				console.error(e);
-				setTimeout(() => {
-					location = '/';
-				}, 10000)
-			}
-		})();
-
-		function message(text) {
-			msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)
-		}
diff --git a/src/server/web/views/gallery-post.pug b/src/server/web/views/gallery-post.pug
deleted file mode 100644
index 95bbb2437c..0000000000
--- a/src/server/web/views/gallery-post.pug
+++ /dev/null
@@ -1,35 +0,0 @@
-extends ./base
-
-block vars
-	- const user = post.user;
-	- const title = post.title;
-	- const url = `${config.url}/gallery/${post.id}`;
-
-block title
-	= `${title} | ${instanceName}`
-
-block desc
-	meta(name='description' content= post.description)
-
-block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= post.description)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= post.files[0].thumbnailUrl)
-
-block meta
-	if user.host || profile.noCrawle
-		meta(name='robots' content='noindex')
-
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
-
-	meta(name='twitter:card' content='summary')
-
-	// todo
-	if user.twitter
-		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
-
-	if !user.host
-		link(rel='alternate' href=url type='application/activity+json')
diff --git a/src/server/web/views/info-card.pug b/src/server/web/views/info-card.pug
deleted file mode 100644
index 1d62778ce1..0000000000
--- a/src/server/web/views/info-card.pug
+++ /dev/null
@@ -1,50 +0,0 @@
-doctype html
-
-html
-
-	head
-		meta(charset='utf-8')
-		meta(name='application-name' content='Misskey')
-		title= meta.name || host
-		style.
-			html, body {
-				margin: 0;
-				padding: 0;
-				min-height: 100vh;
-				background: #fff;
-			}
-
-			#a {
-				display: block;
-			}
-
-			#banner {
-				background-size: cover;
-				background-position: center center;
-			}
-
-			#title {
-				display: inline-block;
-				margin: 24px;
-				padding: 0.5em 0.8em;
-				color: #fff;
-				background: rgba(0, 0, 0, 0.5);
-				font-weight: bold;
-				font-size: 1.3em;
-			}
-
-			#content {
-				overflow: auto;
-				color: #353c3e;
-			}
-
-			#description {
-				margin: 24px;
-			}
-
-	body
-		a#a(href=`https://${host}` target="_blank")
-			header#banner(style=`background-image: url(${meta.bannerUrl})`)
-				div#title= meta.name || host
-		div#content
-			div#description= meta.description
diff --git a/src/server/web/views/note.pug b/src/server/web/views/note.pug
deleted file mode 100644
index 7030936975..0000000000
--- a/src/server/web/views/note.pug
+++ /dev/null
@@ -1,43 +0,0 @@
-extends ./base
-
-block vars
-	- const user = note.user;
-	- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
-	- const url = `${config.url}/notes/${note.id}`;
-
-block title
-	= `${title} | ${instanceName}`
-
-block desc
-	meta(name='description' content= summary)
-
-block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= summary)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= user.avatarUrl)
-
-block meta
-	if user.host || profile.noCrawle
-		meta(name='robots' content='noindex')
-
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
-	meta(name='misskey:note-id' content=note.id)
-
-	meta(name='twitter:card' content='summary')
-
-	// todo
-	if user.twitter
-		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
-
-	if note.prev
-		link(rel='prev' href=`${config.url}/notes/${note.prev}`)
-	if note.next
-		link(rel='next' href=`${config.url}/notes/${note.next}`)
-
-	if !user.host
-		link(rel='alternate' href=url type='application/activity+json')
-	if note.uri
-		link(rel='alternate' href=note.uri type='application/activity+json')
diff --git a/src/server/web/views/page.pug b/src/server/web/views/page.pug
deleted file mode 100644
index cb9e1039e1..0000000000
--- a/src/server/web/views/page.pug
+++ /dev/null
@@ -1,33 +0,0 @@
-extends ./base
-
-block vars
-	- const user = page.user;
-	- const title = page.title;
-	- const url = `${config.url}/@${user.username}/${page.name}`;
-
-block title
-	= `${title} | ${instanceName}`
-
-block desc
-	meta(name='description' content= page.summary)
-
-block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= page.summary)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : user.avatarUrl)
-
-block meta
-	if profile.noCrawle
-		meta(name='robots' content='noindex')
-
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
-	meta(name='misskey:page-id' content=page.id)
-
-	meta(name='twitter:card' content='summary')
-
-	// todo
-	if user.twitter
-		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug
deleted file mode 100644
index 1a8a6b4413..0000000000
--- a/src/server/web/views/user.pug
+++ /dev/null
@@ -1,42 +0,0 @@
-extends ./base
-
-block vars
-	- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
-	- const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
-	- const img = user.avatarUrl || null;
-
-block title
-	= `${title} | ${instanceName}`
-
-block desc
-	meta(name='description' content= profile.description)
-
-block og
-	meta(property='og:type'        content='blog')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= profile.description)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= img)
-
-block meta
-	if user.host || profile.noCrawle
-		meta(name='robots' content='noindex')
-
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
-
-	meta(name='twitter:card' content='summary')
-
-	if profile.twitter
-		meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
-
-	if !sub
-		if !user.host
-			link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
-		if user.uri
-			link(rel='alternate' href=user.uri type='application/activity+json')
-		if profile.url
-			link(rel='alternate' href=profile.url type='text/html')
-
-	each m in me
-		link(rel='me' href=`${m}`)
diff --git a/src/server/well-known.ts b/src/server/well-known.ts
deleted file mode 100644
index a2e6bc0bc4..0000000000
--- a/src/server/well-known.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import * as Router from '@koa/router';
-
-import config from '@/config/index';
-import { parseAcct, Acct } from '@/misc/acct';
-import { links } from './nodeinfo';
-import { escapeAttribute, escapeValue } from '@/prelude/xml';
-import { Users } from '@/models/index';
-import { User } from '@/models/entities/user';
-
-// Init router
-const router = new Router();
-
-const XRD = (...x: { element: string, value?: string, attributes?: Record<string, string> }[]) =>
-	`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">${x.map(({ element, value, attributes }) =>
-	`<${
-		Object.entries(typeof attributes === 'object' && attributes || {}).reduce((a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`, element)
-	}${
-		typeof value === 'string' ? `>${escapeValue(value)}</${element}` : '/'
-	}>`).reduce((a, c) => a + c, '')}</XRD>`;
-
-const allPath = '/.well-known/(.*)';
-const webFingerPath = '/.well-known/webfinger';
-const jrd = 'application/jrd+json';
-const xrd = 'application/xrd+xml';
-
-router.use(allPath, async (ctx, next) => {
-	ctx.set({
-		'Access-Control-Allow-Headers': 'Accept',
-		'Access-Control-Allow-Methods': 'GET, OPTIONS',
-		'Access-Control-Allow-Origin': '*',
-		'Access-Control-Expose-Headers': 'Vary',
-	});
-	await next();
-});
-
-router.options(allPath, async ctx => {
-	ctx.status = 204;
-});
-
-router.get('/.well-known/host-meta', async ctx => {
-	ctx.set('Content-Type', xrd);
-	ctx.body = XRD({ element: 'Link', attributes: {
-		type: xrd,
-		template: `${config.url}${webFingerPath}?resource={uri}`
-	}});
-});
-
-router.get('/.well-known/host-meta.json', async ctx => {
-	ctx.set('Content-Type', jrd);
-	ctx.body = {
-		links: [{
-			rel: 'lrdd',
-			type: jrd,
-			template: `${config.url}${webFingerPath}?resource={uri}`
-		}]
-	};
-});
-
-router.get('/.well-known/nodeinfo', async ctx => {
-	ctx.body = { links };
-});
-
-/* TODO
-router.get('/.well-known/change-password', async ctx => {
-});
-*/
-
-router.get(webFingerPath, async ctx => {
-	const fromId = (id: User['id']): Record<string, any> => ({
-		id,
-		host: null,
-		isSuspended: false
-	});
-
-	const generateQuery = (resource: string) =>
-		resource.startsWith(`${config.url.toLowerCase()}/users/`) ?
-			fromId(resource.split('/').pop()!) :
-			fromAcct(parseAcct(
-				resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop()! :
-				resource.startsWith('acct:') ? resource.slice('acct:'.length) :
-				resource));
-
-	const fromAcct = (acct: Acct): Record<string, any> | number =>
-		!acct.host || acct.host === config.host.toLowerCase() ? {
-			usernameLower: acct.username,
-			host: null,
-			isSuspended: false
-		} : 422;
-
-	if (typeof ctx.query.resource !== 'string') {
-		ctx.status = 400;
-		return;
-	}
-
-	const query = generateQuery(ctx.query.resource.toLowerCase());
-
-	if (typeof query === 'number') {
-		ctx.status = query;
-		return;
-	}
-
-	const user = await Users.findOne(query);
-
-	if (user == null) {
-		ctx.status = 404;
-		return;
-	}
-
-	const subject = `acct:${user.username}@${config.host}`;
-	const self = {
-		rel: 'self',
-		type: 'application/activity+json',
-		href: `${config.url}/users/${user.id}`
-	};
-	const profilePage = {
-		rel: 'http://webfinger.net/rel/profile-page',
-		type: 'text/html',
-		href: `${config.url}/@${user.username}`
-	};
-	const subscribe = {
-		rel: 'http://ostatus.org/schema/1.0/subscribe',
-		template: `${config.url}/authorize-follow?acct={uri}`
-	};
-
-	if (ctx.accepts(jrd, xrd) === xrd) {
-		ctx.body = XRD(
-			{ element: 'Subject', value: subject },
-			{ element: 'Link', attributes: self },
-			{ element: 'Link', attributes: profilePage },
-			{ element: 'Link', attributes: subscribe });
-		ctx.type = xrd;
-	} else {
-		ctx.body = {
-			subject,
-			links: [self, profilePage, subscribe]
-		};
-		ctx.type = jrd;
-	}
-
-	ctx.vary('Accept');
-	ctx.set('Cache-Control', 'public, max-age=180');
-});
-
-// Return 404 for other .well-known
-router.all(allPath, async ctx => {
-	ctx.status = 404;
-});
-
-export default router;
diff --git a/src/services/add-note-to-antenna.ts b/src/services/add-note-to-antenna.ts
deleted file mode 100644
index 3aedbd2c32..0000000000
--- a/src/services/add-note-to-antenna.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Antenna } from '@/models/entities/antenna';
-import { Note } from '@/models/entities/note';
-import { AntennaNotes, Mutings, Notes } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { isMutedUserRelated } from '@/misc/is-muted-user-related';
-import { publishAntennaStream, publishMainStream } from '@/services/stream';
-import { User } from '@/models/entities/user';
-
-export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }) {
-	// 通知しない設定になっているか、自分自身の投稿なら既読にする
-	const read = !antenna.notify || (antenna.userId === noteUser.id);
-
-	AntennaNotes.insert({
-		id: genId(),
-		antennaId: antenna.id,
-		noteId: note.id,
-		read: read,
-	});
-
-	publishAntennaStream(antenna.id, 'note', note);
-
-	if (!read) {
-		const mutings = await Mutings.find({
-			where: {
-				muterId: antenna.userId
-			},
-			select: ['muteeId']
-		});
-
-		// Copy
-		const _note: Note = {
-			...note
-		};
-
-		if (note.replyId != null) {
-			_note.reply = await Notes.findOneOrFail(note.replyId);
-		}
-		if (note.renoteId != null) {
-			_note.renote = await Notes.findOneOrFail(note.renoteId);
-		}
-
-		if (isMutedUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
-			return;
-		}
-
-		// 2秒経っても既読にならなかったら通知
-		setTimeout(async () => {
-			const unread = await AntennaNotes.findOne({ antennaId: antenna.id, read: false });
-			if (unread) {
-				publishMainStream(antenna.userId, 'unreadAntenna', antenna);
-			}
-		}, 2000);
-	}
-}
diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts
deleted file mode 100644
index 6aadc847a9..0000000000
--- a/src/services/blocking/create.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { publishMainStream, publishUserEvent } from '@/services/stream';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderFollow from '@/remote/activitypub/renderer/follow';
-import renderUndo from '@/remote/activitypub/renderer/undo';
-import renderBlock from '@/remote/activitypub/renderer/block';
-import { deliver } from '@/queue/index';
-import renderReject from '@/remote/activitypub/renderer/reject';
-import { User } from '@/models/entities/user';
-import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index';
-import { perUserFollowingChart } from '@/services/chart/index';
-import { genId } from '@/misc/gen-id';
-import { IdentifiableError } from '@/misc/identifiable-error';
-
-export default async function(blocker: User, blockee: User) {
-	await Promise.all([
-		cancelRequest(blocker, blockee),
-		cancelRequest(blockee, blocker),
-		unFollow(blocker, blockee),
-		unFollow(blockee, blocker),
-		removeFromList(blockee, blocker),
-	]);
-
-	await Blockings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		blockerId: blocker.id,
-		blockeeId: blockee.id,
-	});
-
-	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
-		const content = renderActivity(renderBlock(blocker, blockee));
-		deliver(blocker, content, blockee.inbox);
-	}
-}
-
-async function cancelRequest(follower: User, followee: User) {
-	const request = await FollowRequests.findOne({
-		followeeId: followee.id,
-		followerId: follower.id
-	});
-
-	if (request == null) {
-		return;
-	}
-
-	await FollowRequests.delete({
-		followeeId: followee.id,
-		followerId: follower.id
-	});
-
-	if (Users.isLocalUser(followee)) {
-		Users.pack(followee, followee, {
-			detail: true
-		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
-	}
-
-	if (Users.isLocalUser(follower)) {
-		Users.pack(followee, follower, {
-			detail: true
-		}).then(packed => {
-			publishUserEvent(follower.id, 'unfollow', packed);
-			publishMainStream(follower.id, 'unfollow', packed);
-		});
-	}
-
-	// リモートにフォローリクエストをしていたらUndoFollow送信
-	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
-		deliver(follower, content, followee.inbox);
-	}
-
-	// リモートからフォローリクエストを受けていたらReject送信
-	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
-		const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId!), followee));
-		deliver(followee, content, follower.inbox);
-	}
-}
-
-async function unFollow(follower: User, followee: User) {
-	const following = await Followings.findOne({
-		followerId: follower.id,
-		followeeId: followee.id
-	});
-
-	if (following == null) {
-		return;
-	}
-
-	Followings.delete(following.id);
-
-	//#region Decrement following count
-	Users.decrement({ id: follower.id }, 'followingCount', 1);
-	//#endregion
-
-	//#region Decrement followers count
-	Users.decrement({ id: followee.id }, 'followersCount', 1);
-	//#endregion
-
-	perUserFollowingChart.update(follower, followee, false);
-
-	// Publish unfollow event
-	if (Users.isLocalUser(follower)) {
-		Users.pack(followee, follower, {
-			detail: true
-		}).then(packed => {
-			publishUserEvent(follower.id, 'unfollow', packed);
-			publishMainStream(follower.id, 'unfollow', packed);
-		});
-	}
-
-	// リモートにフォローをしていたらUndoFollow送信
-	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
-		deliver(follower, content, followee.inbox);
-	}
-}
-
-async function removeFromList(listOwner: User, user: User) {
-	const userLists = await UserLists.find({
-		userId: listOwner.id,
-	});
-
-	for (const userList of userLists) {
-		await UserListJoinings.delete({
-			userListId: userList.id,
-			userId: user.id,
-		});
-	}
-}
diff --git a/src/services/blocking/delete.ts b/src/services/blocking/delete.ts
deleted file mode 100644
index de7efb1558..0000000000
--- a/src/services/blocking/delete.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderBlock from '@/remote/activitypub/renderer/block';
-import renderUndo from '@/remote/activitypub/renderer/undo';
-import { deliver } from '@/queue/index';
-import Logger from '../logger';
-import { User } from '@/models/entities/user';
-import { Blockings, Users } from '@/models/index';
-
-const logger = new Logger('blocking/delete');
-
-export default async function(blocker: User, blockee: User) {
-	const blocking = await Blockings.findOne({
-		blockerId: blocker.id,
-		blockeeId: blockee.id
-	});
-
-	if (blocking == null) {
-		logger.warn('ブロック解除がリクエストされましたがブロックしていませんでした');
-		return;
-	}
-
-	Blockings.delete(blocking.id);
-
-	// deliver if remote bloking
-	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
-		const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker));
-		deliver(blocker, content, blockee.inbox);
-	}
-}
diff --git a/src/services/chart/charts/classes/active-users.ts b/src/services/chart/charts/classes/active-users.ts
deleted file mode 100644
index f80d8a3322..0000000000
--- a/src/services/chart/charts/classes/active-users.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { User } from '@/models/entities/user';
-import { SchemaType } from '@/misc/schema';
-import { Users } from '@/models/index';
-import { name, schema } from '../schemas/active-users';
-
-type ActiveUsersLog = SchemaType<typeof schema>;
-
-export default class ActiveUsersChart extends Chart<ActiveUsersLog> {
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: ActiveUsersLog): DeepPartial<ActiveUsersLog> {
-		return {};
-	}
-
-	@autobind
-	protected aggregate(logs: ActiveUsersLog[]): ActiveUsersLog {
-		return {
-			local: {
-				users: logs.reduce((a, b) => a.concat(b.local.users), [] as ActiveUsersLog['local']['users']),
-			},
-			remote: {
-				users: logs.reduce((a, b) => a.concat(b.remote.users), [] as ActiveUsersLog['remote']['users']),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<ActiveUsersLog>> {
-		return {};
-	}
-
-	@autobind
-	public async update(user: { id: User['id'], host: User['host'] }) {
-		const update: Obj = {
-			users: [user.id]
-		};
-
-		await this.inc({
-			[Users.isLocalUser(user) ? 'local' : 'remote']: update
-		});
-	}
-}
diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts
deleted file mode 100644
index 93eabf3096..0000000000
--- a/src/services/chart/charts/classes/drive.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { DriveFiles } from '@/models/index';
-import { Not, IsNull } from 'typeorm';
-import { DriveFile } from '@/models/entities/drive-file';
-import { name, schema } from '../schemas/drive';
-
-type DriveLog = SchemaType<typeof schema>;
-
-export default class DriveChart extends Chart<DriveLog> {
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: DriveLog): DeepPartial<DriveLog> {
-		return {
-			local: {
-				totalCount: latest.local.totalCount,
-				totalSize: latest.local.totalSize,
-			},
-			remote: {
-				totalCount: latest.remote.totalCount,
-				totalSize: latest.remote.totalSize,
-			}
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: DriveLog[]): DriveLog {
-		return {
-			local: {
-				totalCount: logs[0].local.totalCount,
-				totalSize: logs[0].local.totalSize,
-				incCount: logs.reduce((a, b) => a + b.local.incCount, 0),
-				incSize: logs.reduce((a, b) => a + b.local.incSize, 0),
-				decCount: logs.reduce((a, b) => a + b.local.decCount, 0),
-				decSize: logs.reduce((a, b) => a + b.local.decSize, 0),
-			},
-			remote: {
-				totalCount: logs[0].remote.totalCount,
-				totalSize: logs[0].remote.totalSize,
-				incCount: logs.reduce((a, b) => a + b.remote.incCount, 0),
-				incSize: logs.reduce((a, b) => a + b.remote.incSize, 0),
-				decCount: logs.reduce((a, b) => a + b.remote.decCount, 0),
-				decSize: logs.reduce((a, b) => a + b.remote.decSize, 0),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<DriveLog>> {
-		const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([
-			DriveFiles.count({ userHost: null }),
-			DriveFiles.count({ userHost: Not(IsNull()) }),
-			DriveFiles.calcDriveUsageOfLocal(),
-			DriveFiles.calcDriveUsageOfRemote()
-		]);
-
-		return {
-			local: {
-				totalCount: localCount,
-				totalSize: localSize,
-			},
-			remote: {
-				totalCount: remoteCount,
-				totalSize: remoteSize,
-			}
-		};
-	}
-
-	@autobind
-	public async update(file: DriveFile, isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.totalCount = isAdditional ? 1 : -1;
-		update.totalSize = isAdditional ? file.size : -file.size;
-		if (isAdditional) {
-			update.incCount = 1;
-			update.incSize = file.size;
-		} else {
-			update.decCount = 1;
-			update.decSize = file.size;
-		}
-
-		await this.inc({
-			[file.userHost === null ? 'local' : 'remote']: update
-		});
-	}
-}
diff --git a/src/services/chart/charts/classes/federation.ts b/src/services/chart/charts/classes/federation.ts
deleted file mode 100644
index 5f918b294f..0000000000
--- a/src/services/chart/charts/classes/federation.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { Instances } from '@/models/index';
-import { name, schema } from '../schemas/federation';
-
-type FederationLog = SchemaType<typeof schema>;
-
-export default class FederationChart extends Chart<FederationLog> {
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: FederationLog): DeepPartial<FederationLog> {
-		return {
-			instance: {
-				total: latest.instance.total,
-			}
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: FederationLog[]): FederationLog {
-		return {
-			instance: {
-				total: logs[0].instance.total,
-				inc: logs.reduce((a, b) => a + b.instance.inc, 0),
-				dec: logs.reduce((a, b) => a + b.instance.dec, 0),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<FederationLog>> {
-		const [total] = await Promise.all([
-			Instances.count({})
-		]);
-
-		return {
-			instance: {
-				total: total,
-			}
-		};
-	}
-
-	@autobind
-	public async update(isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.total = isAdditional ? 1 : -1;
-		if (isAdditional) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		await this.inc({
-			instance: update
-		});
-	}
-}
diff --git a/src/services/chart/charts/classes/hashtag.ts b/src/services/chart/charts/classes/hashtag.ts
deleted file mode 100644
index f7f5e17dec..0000000000
--- a/src/services/chart/charts/classes/hashtag.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { User } from '@/models/entities/user';
-import { SchemaType } from '@/misc/schema';
-import { Users } from '@/models/index';
-import { name, schema } from '../schemas/hashtag';
-
-type HashtagLog = SchemaType<typeof schema>;
-
-export default class HashtagChart extends Chart<HashtagLog> {
-	constructor() {
-		super(name, schema, true);
-	}
-
-	@autobind
-	protected genNewLog(latest: HashtagLog): DeepPartial<HashtagLog> {
-		return {};
-	}
-
-	@autobind
-	protected aggregate(logs: HashtagLog[]): HashtagLog {
-		return {
-			local: {
-				users: logs.reduce((a, b) => a.concat(b.local.users), [] as HashtagLog['local']['users']),
-			},
-			remote: {
-				users: logs.reduce((a, b) => a.concat(b.remote.users), [] as HashtagLog['remote']['users']),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<HashtagLog>> {
-		return {};
-	}
-
-	@autobind
-	public async update(hashtag: string, user: { id: User['id'], host: User['host'] }) {
-		const update: Obj = {
-			users: [user.id]
-		};
-
-		await this.inc({
-			[Users.isLocalUser(user) ? 'local' : 'remote']: update
-		}, hashtag);
-	}
-}
diff --git a/src/services/chart/charts/classes/instance.ts b/src/services/chart/charts/classes/instance.ts
deleted file mode 100644
index 1032de7bc0..0000000000
--- a/src/services/chart/charts/classes/instance.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { DriveFiles, Followings, Users, Notes } from '@/models/index';
-import { DriveFile } from '@/models/entities/drive-file';
-import { name, schema } from '../schemas/instance';
-import { Note } from '@/models/entities/note';
-import { toPuny } from '@/misc/convert-host';
-
-type InstanceLog = SchemaType<typeof schema>;
-
-export default class InstanceChart extends Chart<InstanceLog> {
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: InstanceLog): DeepPartial<InstanceLog> {
-		return {
-			notes: {
-				total: latest.notes.total,
-			},
-			users: {
-				total: latest.users.total,
-			},
-			following: {
-				total: latest.following.total,
-			},
-			followers: {
-				total: latest.followers.total,
-			},
-			drive: {
-				totalFiles: latest.drive.totalFiles,
-				totalUsage: latest.drive.totalUsage,
-			}
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: InstanceLog[]): InstanceLog {
-		return {
-			requests: {
-				failed: logs.reduce((a, b) => a + b.requests.failed, 0),
-				succeeded: logs.reduce((a, b) => a + b.requests.succeeded, 0),
-				received: logs.reduce((a, b) => a + b.requests.received, 0),
-			},
-			notes: {
-				total: logs[0].notes.total,
-				inc: logs.reduce((a, b) => a + b.notes.inc, 0),
-				dec: logs.reduce((a, b) => a + b.notes.dec, 0),
-				diffs: {
-					reply: logs.reduce((a, b) => a + b.notes.diffs.reply, 0),
-					renote: logs.reduce((a, b) => a + b.notes.diffs.renote, 0),
-					normal: logs.reduce((a, b) => a + b.notes.diffs.normal, 0),
-				},
-			},
-			users: {
-				total: logs[0].users.total,
-				inc: logs.reduce((a, b) => a + b.users.inc, 0),
-				dec: logs.reduce((a, b) => a + b.users.dec, 0),
-			},
-			following: {
-				total: logs[0].following.total,
-				inc: logs.reduce((a, b) => a + b.following.inc, 0),
-				dec: logs.reduce((a, b) => a + b.following.dec, 0),
-			},
-			followers: {
-				total: logs[0].followers.total,
-				inc: logs.reduce((a, b) => a + b.followers.inc, 0),
-				dec: logs.reduce((a, b) => a + b.followers.dec, 0),
-			},
-			drive: {
-				totalFiles: logs[0].drive.totalFiles,
-				totalUsage: logs[0].drive.totalUsage,
-				incFiles: logs.reduce((a, b) => a + b.drive.incFiles, 0),
-				incUsage: logs.reduce((a, b) => a + b.drive.incUsage, 0),
-				decFiles: logs.reduce((a, b) => a + b.drive.decFiles, 0),
-				decUsage: logs.reduce((a, b) => a + b.drive.decUsage, 0),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(group: string): Promise<DeepPartial<InstanceLog>> {
-		const [
-			notesCount,
-			usersCount,
-			followingCount,
-			followersCount,
-			driveFiles,
-			driveUsage,
-		] = await Promise.all([
-			Notes.count({ userHost: group }),
-			Users.count({ host: group }),
-			Followings.count({ followerHost: group }),
-			Followings.count({ followeeHost: group }),
-			DriveFiles.count({ userHost: group }),
-			DriveFiles.calcDriveUsageOfHost(group),
-		]);
-
-		return {
-			notes: {
-				total: notesCount,
-			},
-			users: {
-				total: usersCount,
-			},
-			following: {
-				total: followingCount,
-			},
-			followers: {
-				total: followersCount,
-			},
-			drive: {
-				totalFiles: driveFiles,
-				totalUsage: driveUsage,
-			}
-		};
-	}
-
-	@autobind
-	public async requestReceived(host: string) {
-		await this.inc({
-			requests: {
-				received: 1
-			}
-		}, toPuny(host));
-	}
-
-	@autobind
-	public async requestSent(host: string, isSucceeded: boolean) {
-		const update: Obj = {};
-
-		if (isSucceeded) {
-			update.succeeded = 1;
-		} else {
-			update.failed = 1;
-		}
-
-		await this.inc({
-			requests: update
-		}, toPuny(host));
-	}
-
-	@autobind
-	public async newUser(host: string) {
-		await this.inc({
-			users: {
-				total: 1,
-				inc: 1
-			}
-		}, toPuny(host));
-	}
-
-	@autobind
-	public async updateNote(host: string, note: Note, isAdditional: boolean) {
-		const diffs = {} as any;
-
-		if (note.replyId != null) {
-			diffs.reply = isAdditional ? 1 : -1;
-		} else if (note.renoteId != null) {
-			diffs.renote = isAdditional ? 1 : -1;
-		} else {
-			diffs.normal = isAdditional ? 1 : -1;
-		}
-
-		await this.inc({
-			notes: {
-				total: isAdditional ? 1 : -1,
-				inc: isAdditional ? 1 : 0,
-				dec: isAdditional ? 0 : 1,
-				diffs: diffs
-			}
-		}, toPuny(host));
-	}
-
-	@autobind
-	public async updateFollowing(host: string, isAdditional: boolean) {
-		await this.inc({
-			following: {
-				total: isAdditional ? 1 : -1,
-				inc: isAdditional ? 1 : 0,
-				dec: isAdditional ? 0 : 1,
-			}
-		}, toPuny(host));
-	}
-
-	@autobind
-	public async updateFollowers(host: string, isAdditional: boolean) {
-		await this.inc({
-			followers: {
-				total: isAdditional ? 1 : -1,
-				inc: isAdditional ? 1 : 0,
-				dec: isAdditional ? 0 : 1,
-			}
-		}, toPuny(host));
-	}
-
-	@autobind
-	public async updateDrive(file: DriveFile, isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.totalFiles = isAdditional ? 1 : -1;
-		update.totalUsage = isAdditional ? file.size : -file.size;
-		if (isAdditional) {
-			update.incFiles = 1;
-			update.incUsage = file.size;
-		} else {
-			update.decFiles = 1;
-			update.decUsage = file.size;
-		}
-
-		await this.inc({
-			drive: update
-		}, file.userHost);
-	}
-}
diff --git a/src/services/chart/charts/classes/network.ts b/src/services/chart/charts/classes/network.ts
deleted file mode 100644
index 2ce75e0b34..0000000000
--- a/src/services/chart/charts/classes/network.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { name, schema } from '../schemas/network';
-
-type NetworkLog = SchemaType<typeof schema>;
-
-export default class NetworkChart extends Chart<NetworkLog> {
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: NetworkLog): DeepPartial<NetworkLog> {
-		return {};
-	}
-
-	@autobind
-	protected aggregate(logs: NetworkLog[]): NetworkLog {
-		return {
-			incomingRequests: logs.reduce((a, b) => a + b.incomingRequests, 0),
-			outgoingRequests: logs.reduce((a, b) => a + b.outgoingRequests, 0),
-			totalTime: logs.reduce((a, b) => a + b.totalTime, 0),
-			incomingBytes: logs.reduce((a, b) => a + b.incomingBytes, 0),
-			outgoingBytes: logs.reduce((a, b) => a + b.outgoingBytes, 0),
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<NetworkLog>> {
-		return {};
-	}
-
-	@autobind
-	public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
-		const inc: DeepPartial<NetworkLog> = {
-			incomingRequests: incomingRequests,
-			totalTime: time,
-			incomingBytes: incomingBytes,
-			outgoingBytes: outgoingBytes
-		};
-
-		await this.inc(inc);
-	}
-}
diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts
deleted file mode 100644
index 0675d346d1..0000000000
--- a/src/services/chart/charts/classes/notes.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { Notes } from '@/models/index';
-import { Not, IsNull } from 'typeorm';
-import { Note } from '@/models/entities/note';
-import { name, schema } from '../schemas/notes';
-
-type NotesLog = SchemaType<typeof schema>;
-
-export default class NotesChart extends Chart<NotesLog> {
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: NotesLog): DeepPartial<NotesLog> {
-		return {
-			local: {
-				total: latest.local.total,
-			},
-			remote: {
-				total: latest.remote.total,
-			}
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: NotesLog[]): NotesLog {
-		return {
-			local: {
-				total: logs[0].local.total,
-				inc: logs.reduce((a, b) => a + b.local.inc, 0),
-				dec: logs.reduce((a, b) => a + b.local.dec, 0),
-				diffs: {
-					reply: logs.reduce((a, b) => a + b.local.diffs.reply, 0),
-					renote: logs.reduce((a, b) => a + b.local.diffs.renote, 0),
-					normal: logs.reduce((a, b) => a + b.local.diffs.normal, 0),
-				},
-			},
-			remote: {
-				total: logs[0].remote.total,
-				inc: logs.reduce((a, b) => a + b.remote.inc, 0),
-				dec: logs.reduce((a, b) => a + b.remote.dec, 0),
-				diffs: {
-					reply: logs.reduce((a, b) => a + b.remote.diffs.reply, 0),
-					renote: logs.reduce((a, b) => a + b.remote.diffs.renote, 0),
-					normal: logs.reduce((a, b) => a + b.remote.diffs.normal, 0),
-				},
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<NotesLog>> {
-		const [localCount, remoteCount] = await Promise.all([
-			Notes.count({ userHost: null }),
-			Notes.count({ userHost: Not(IsNull()) })
-		]);
-
-		return {
-			local: {
-				total: localCount,
-			},
-			remote: {
-				total: remoteCount,
-			}
-		};
-	}
-
-	@autobind
-	public async update(note: Note, isAdditional: boolean) {
-		const update: Obj = {
-			diffs: {}
-		};
-
-		update.total = isAdditional ? 1 : -1;
-
-		if (isAdditional) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		if (note.replyId != null) {
-			update.diffs.reply = isAdditional ? 1 : -1;
-		} else if (note.renoteId != null) {
-			update.diffs.renote = isAdditional ? 1 : -1;
-		} else {
-			update.diffs.normal = isAdditional ? 1 : -1;
-		}
-
-		await this.inc({
-			[note.userHost === null ? 'local' : 'remote']: update
-		});
-	}
-}
diff --git a/src/services/chart/charts/classes/per-user-drive.ts b/src/services/chart/charts/classes/per-user-drive.ts
deleted file mode 100644
index f28987191b..0000000000
--- a/src/services/chart/charts/classes/per-user-drive.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { DriveFiles } from '@/models/index';
-import { DriveFile } from '@/models/entities/drive-file';
-import { name, schema } from '../schemas/per-user-drive';
-
-type PerUserDriveLog = SchemaType<typeof schema>;
-
-export default class PerUserDriveChart extends Chart<PerUserDriveLog> {
-	constructor() {
-		super(name, schema, true);
-	}
-
-	@autobind
-	protected genNewLog(latest: PerUserDriveLog): DeepPartial<PerUserDriveLog> {
-		return {
-			totalCount: latest.totalCount,
-			totalSize: latest.totalSize,
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: PerUserDriveLog[]): PerUserDriveLog {
-		return {
-			totalCount: logs[0].totalCount,
-			totalSize: logs[0].totalSize,
-			incCount: logs.reduce((a, b) => a + b.incCount, 0),
-			incSize: logs.reduce((a, b) => a + b.incSize, 0),
-			decCount: logs.reduce((a, b) => a + b.decCount, 0),
-			decSize: logs.reduce((a, b) => a + b.decSize, 0),
-		};
-	}
-
-	@autobind
-	protected async fetchActual(group: string): Promise<DeepPartial<PerUserDriveLog>> {
-		const [count, size] = await Promise.all([
-			DriveFiles.count({ userId: group }),
-			DriveFiles.calcDriveUsageOf(group)
-		]);
-
-		return {
-			totalCount: count,
-			totalSize: size,
-		};
-	}
-
-	@autobind
-	public async update(file: DriveFile, isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.totalCount = isAdditional ? 1 : -1;
-		update.totalSize = isAdditional ? file.size : -file.size;
-		if (isAdditional) {
-			update.incCount = 1;
-			update.incSize = file.size;
-		} else {
-			update.decCount = 1;
-			update.decSize = file.size;
-		}
-
-		await this.inc(update, file.userId);
-	}
-}
diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts
deleted file mode 100644
index 08a9ad1d2b..0000000000
--- a/src/services/chart/charts/classes/per-user-following.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { Followings, Users } from '@/models/index';
-import { Not, IsNull } from 'typeorm';
-import { User } from '@/models/entities/user';
-import { name, schema } from '../schemas/per-user-following';
-
-type PerUserFollowingLog = SchemaType<typeof schema>;
-
-export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
-	constructor() {
-		super(name, schema, true);
-	}
-
-	@autobind
-	protected genNewLog(latest: PerUserFollowingLog): DeepPartial<PerUserFollowingLog> {
-		return {
-			local: {
-				followings: {
-					total: latest.local.followings.total,
-				},
-				followers: {
-					total: latest.local.followers.total,
-				}
-			},
-			remote: {
-				followings: {
-					total: latest.remote.followings.total,
-				},
-				followers: {
-					total: latest.remote.followers.total,
-				}
-			}
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: PerUserFollowingLog[]): PerUserFollowingLog {
-		return {
-			local: {
-				followings: {
-					total: logs[0].local.followings.total,
-					inc: logs.reduce((a, b) => a + b.local.followings.inc, 0),
-					dec: logs.reduce((a, b) => a + b.local.followings.dec, 0),
-				},
-				followers: {
-					total: logs[0].local.followers.total,
-					inc: logs.reduce((a, b) => a + b.local.followers.inc, 0),
-					dec: logs.reduce((a, b) => a + b.local.followers.dec, 0),
-				},
-			},
-			remote: {
-				followings: {
-					total: logs[0].remote.followings.total,
-					inc: logs.reduce((a, b) => a + b.remote.followings.inc, 0),
-					dec: logs.reduce((a, b) => a + b.remote.followings.dec, 0),
-				},
-				followers: {
-					total: logs[0].remote.followers.total,
-					inc: logs.reduce((a, b) => a + b.remote.followers.inc, 0),
-					dec: logs.reduce((a, b) => a + b.remote.followers.dec, 0),
-				},
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(group: string): Promise<DeepPartial<PerUserFollowingLog>> {
-		const [
-			localFollowingsCount,
-			localFollowersCount,
-			remoteFollowingsCount,
-			remoteFollowersCount
-		] = await Promise.all([
-			Followings.count({ followerId: group, followeeHost: null }),
-			Followings.count({ followeeId: group, followerHost: null }),
-			Followings.count({ followerId: group, followeeHost: Not(IsNull()) }),
-			Followings.count({ followeeId: group, followerHost: Not(IsNull()) })
-		]);
-
-		return {
-			local: {
-				followings: {
-					total: localFollowingsCount,
-				},
-				followers: {
-					total: localFollowersCount,
-				}
-			},
-			remote: {
-				followings: {
-					total: remoteFollowingsCount,
-				},
-				followers: {
-					total: remoteFollowersCount,
-				}
-			}
-		};
-	}
-
-	@autobind
-	public async update(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, isFollow: boolean) {
-		const update: Obj = {};
-
-		update.total = isFollow ? 1 : -1;
-
-		if (isFollow) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		this.inc({
-			[Users.isLocalUser(follower) ? 'local' : 'remote']: { followings: update }
-		}, follower.id);
-		this.inc({
-			[Users.isLocalUser(followee) ? 'local' : 'remote']: { followers: update }
-		}, followee.id);
-	}
-}
diff --git a/src/services/chart/charts/classes/per-user-notes.ts b/src/services/chart/charts/classes/per-user-notes.ts
deleted file mode 100644
index 0e808766f5..0000000000
--- a/src/services/chart/charts/classes/per-user-notes.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { User } from '@/models/entities/user';
-import { SchemaType } from '@/misc/schema';
-import { Notes } from '@/models/index';
-import { Note } from '@/models/entities/note';
-import { name, schema } from '../schemas/per-user-notes';
-
-type PerUserNotesLog = SchemaType<typeof schema>;
-
-export default class PerUserNotesChart extends Chart<PerUserNotesLog> {
-	constructor() {
-		super(name, schema, true);
-	}
-
-	@autobind
-	protected genNewLog(latest: PerUserNotesLog): DeepPartial<PerUserNotesLog> {
-		return {
-			total: latest.total,
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: PerUserNotesLog[]): PerUserNotesLog {
-		return {
-			total: logs[0].total,
-			inc: logs.reduce((a, b) => a + b.inc, 0),
-			dec: logs.reduce((a, b) => a + b.dec, 0),
-			diffs: {
-				reply: logs.reduce((a, b) => a + b.diffs.reply, 0),
-				renote: logs.reduce((a, b) => a + b.diffs.renote, 0),
-				normal: logs.reduce((a, b) => a + b.diffs.normal, 0),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(group: string): Promise<DeepPartial<PerUserNotesLog>> {
-		const [count] = await Promise.all([
-			Notes.count({ userId: group }),
-		]);
-
-		return {
-			total: count,
-		};
-	}
-
-	@autobind
-	public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean) {
-		const update: Obj = {
-			diffs: {}
-		};
-
-		update.total = isAdditional ? 1 : -1;
-
-		if (isAdditional) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		if (note.replyId != null) {
-			update.diffs.reply = isAdditional ? 1 : -1;
-		} else if (note.renoteId != null) {
-			update.diffs.renote = isAdditional ? 1 : -1;
-		} else {
-			update.diffs.normal = isAdditional ? 1 : -1;
-		}
-
-		await this.inc(update, user.id);
-	}
-}
diff --git a/src/services/chart/charts/classes/per-user-reactions.ts b/src/services/chart/charts/classes/per-user-reactions.ts
deleted file mode 100644
index e71bcb71c4..0000000000
--- a/src/services/chart/charts/classes/per-user-reactions.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { DeepPartial } from '../../core';
-import { User } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { SchemaType } from '@/misc/schema';
-import { Users } from '@/models/index';
-import { name, schema } from '../schemas/per-user-reactions';
-
-type PerUserReactionsLog = SchemaType<typeof schema>;
-
-export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
-	constructor() {
-		super(name, schema, true);
-	}
-
-	@autobind
-	protected genNewLog(latest: PerUserReactionsLog): DeepPartial<PerUserReactionsLog> {
-		return {};
-	}
-
-	@autobind
-	protected aggregate(logs: PerUserReactionsLog[]): PerUserReactionsLog {
-		return {
-			local: {
-				count: logs.reduce((a, b) => a + b.local.count, 0),
-			},
-			remote: {
-				count: logs.reduce((a, b) => a + b.remote.count, 0),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(group: string): Promise<DeepPartial<PerUserReactionsLog>> {
-		return {};
-	}
-
-	@autobind
-	public async update(user: { id: User['id'], host: User['host'] }, note: Note) {
-		this.inc({
-			[Users.isLocalUser(user) ? 'local' : 'remote']: { count: 1 }
-		}, note.userId);
-	}
-}
diff --git a/src/services/chart/charts/classes/test-grouped.ts b/src/services/chart/charts/classes/test-grouped.ts
deleted file mode 100644
index 84e6d5e33f..0000000000
--- a/src/services/chart/charts/classes/test-grouped.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { name, schema } from '../schemas/test-grouped';
-
-type TestGroupedLog = SchemaType<typeof schema>;
-
-export default class TestGroupedChart extends Chart<TestGroupedLog> {
-	private total = {} as Record<string, number>;
-
-	constructor() {
-		super(name, schema, true);
-	}
-
-	@autobind
-	protected genNewLog(latest: TestGroupedLog): DeepPartial<TestGroupedLog> {
-		return {
-			foo: {
-				total: latest.foo.total,
-			},
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: TestGroupedLog[]): TestGroupedLog {
-		return {
-			foo: {
-				total: logs[0].foo.total,
-				inc: logs.reduce((a, b) => a + b.foo.inc, 0),
-				dec: logs.reduce((a, b) => a + b.foo.dec, 0),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(group: string): Promise<DeepPartial<TestGroupedLog>> {
-		return {
-			foo: {
-				total: this.total[group],
-			},
-		};
-	}
-
-	@autobind
-	public async increment(group: string) {
-		if (this.total[group] == null) this.total[group] = 0;
-
-		const update: Obj = {};
-
-		update.total = 1;
-		update.inc = 1;
-		this.total[group]++;
-
-		await this.inc({
-			foo: update
-		}, group);
-	}
-}
diff --git a/src/services/chart/charts/classes/test-unique.ts b/src/services/chart/charts/classes/test-unique.ts
deleted file mode 100644
index 559fda13c9..0000000000
--- a/src/services/chart/charts/classes/test-unique.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { name, schema } from '../schemas/test-unique';
-
-type TestUniqueLog = SchemaType<typeof schema>;
-
-export default class TestUniqueChart extends Chart<TestUniqueLog> {
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: TestUniqueLog): DeepPartial<TestUniqueLog> {
-		return {};
-	}
-
-	@autobind
-	protected aggregate(logs: TestUniqueLog[]): TestUniqueLog {
-		return {
-			foo: logs.reduce((a, b) => a.concat(b.foo), [] as TestUniqueLog['foo']),
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<TestUniqueLog>> {
-		return {};
-	}
-
-	@autobind
-	public async uniqueIncrement(key: string) {
-		await this.inc({
-			foo: [key]
-		});
-	}
-}
diff --git a/src/services/chart/charts/classes/test.ts b/src/services/chart/charts/classes/test.ts
deleted file mode 100644
index a91d5e1895..0000000000
--- a/src/services/chart/charts/classes/test.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { name, schema } from '../schemas/test';
-
-type TestLog = SchemaType<typeof schema>;
-
-export default class TestChart extends Chart<TestLog> {
-	public total = 0; // publicにするのはテストのため
-
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: TestLog): DeepPartial<TestLog> {
-		return {
-			foo: {
-				total: latest.foo.total,
-			},
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: TestLog[]): TestLog {
-		return {
-			foo: {
-				total: logs[0].foo.total,
-				inc: logs.reduce((a, b) => a + b.foo.inc, 0),
-				dec: logs.reduce((a, b) => a + b.foo.dec, 0),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<TestLog>> {
-		return {
-			foo: {
-				total: this.total,
-			},
-		};
-	}
-
-	@autobind
-	public async increment() {
-		const update: Obj = {};
-
-		update.total = 1;
-		update.inc = 1;
-		this.total++;
-
-		await this.inc({
-			foo: update
-		});
-	}
-
-	@autobind
-	public async decrement() {
-		const update: Obj = {};
-
-		update.total = -1;
-		update.dec = 1;
-		this.total--;
-
-		await this.inc({
-			foo: update
-		});
-	}
-}
diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts
deleted file mode 100644
index 89b480ef77..0000000000
--- a/src/services/chart/charts/classes/users.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import autobind from 'autobind-decorator';
-import Chart, { Obj, DeepPartial } from '../../core';
-import { SchemaType } from '@/misc/schema';
-import { Users } from '@/models/index';
-import { Not, IsNull } from 'typeorm';
-import { User } from '@/models/entities/user';
-import { name, schema } from '../schemas/users';
-
-type UsersLog = SchemaType<typeof schema>;
-
-export default class UsersChart extends Chart<UsersLog> {
-	constructor() {
-		super(name, schema);
-	}
-
-	@autobind
-	protected genNewLog(latest: UsersLog): DeepPartial<UsersLog> {
-		return {
-			local: {
-				total: latest.local.total,
-			},
-			remote: {
-				total: latest.remote.total,
-			}
-		};
-	}
-
-	@autobind
-	protected aggregate(logs: UsersLog[]): UsersLog {
-		return {
-			local: {
-				total: logs[0].local.total,
-				inc: logs.reduce((a, b) => a + b.local.inc, 0),
-				dec: logs.reduce((a, b) => a + b.local.dec, 0),
-			},
-			remote: {
-				total: logs[0].remote.total,
-				inc: logs.reduce((a, b) => a + b.remote.inc, 0),
-				dec: logs.reduce((a, b) => a + b.remote.dec, 0),
-			},
-		};
-	}
-
-	@autobind
-	protected async fetchActual(): Promise<DeepPartial<UsersLog>> {
-		const [localCount, remoteCount] = await Promise.all([
-			Users.count({ host: null }),
-			Users.count({ host: Not(IsNull()) })
-		]);
-
-		return {
-			local: {
-				total: localCount,
-			},
-			remote: {
-				total: remoteCount,
-			}
-		};
-	}
-
-	@autobind
-	public async update(user: { id: User['id'], host: User['host'] }, isAdditional: boolean) {
-		const update: Obj = {};
-
-		update.total = isAdditional ? 1 : -1;
-		if (isAdditional) {
-			update.inc = 1;
-		} else {
-			update.dec = 1;
-		}
-
-		await this.inc({
-			[Users.isLocalUser(user) ? 'local' : 'remote']: update
-		});
-	}
-}
diff --git a/src/services/chart/charts/schemas/active-users.ts b/src/services/chart/charts/schemas/active-users.ts
deleted file mode 100644
index 1d65f280b0..0000000000
--- a/src/services/chart/charts/schemas/active-users.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-export const logSchema = {
-	/**
-	 * アクティブユーザー
-	 */
-	users: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		}
-	},
-};
-
-/**
- * アクティブユーザーに関するチャート
- */
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		local: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-	}
-};
-
-export const name = 'activeUsers';
diff --git a/src/services/chart/charts/schemas/drive.ts b/src/services/chart/charts/schemas/drive.ts
deleted file mode 100644
index 133b47846a..0000000000
--- a/src/services/chart/charts/schemas/drive.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-const logSchema = {
-	/**
-	 * 集計期間時点での、全ドライブファイル数
-	 */
-	totalCount: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	/**
-	 * 集計期間時点での、全ドライブファイルの合計サイズ
-	 */
-	totalSize: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	/**
-	 * 増加したドライブファイル数
-	 */
-	incCount: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	/**
-	 * 増加したドライブ使用量
-	 */
-	incSize: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	/**
-	 * 減少したドライブファイル数
-	 */
-	decCount: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	/**
-	 * 減少したドライブ使用量
-	 */
-	decSize: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-};
-
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		local: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-	}
-};
-
-export const name = 'drive';
diff --git a/src/services/chart/charts/schemas/federation.ts b/src/services/chart/charts/schemas/federation.ts
deleted file mode 100644
index dca4587cac..0000000000
--- a/src/services/chart/charts/schemas/federation.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * フェデレーションに関するチャート
- */
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		instance: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				total: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				inc: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				dec: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		}
-	}
-};
-
-export const name = 'federation';
diff --git a/src/services/chart/charts/schemas/hashtag.ts b/src/services/chart/charts/schemas/hashtag.ts
deleted file mode 100644
index 4e7c542bbc..0000000000
--- a/src/services/chart/charts/schemas/hashtag.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-export const logSchema = {
-	/**
-	 * 投稿したユーザー
-	 */
-	users: {
-		type: 'array' as const,
-		optional: false as const, nullable: false as const,
-		items: {
-			type: 'string' as const,
-			optional: false as const, nullable: false as const,
-		}
-	},
-};
-
-/**
- * ハッシュタグに関するチャート
- */
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		local: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-	}
-};
-
-export const name = 'hashtag';
diff --git a/src/services/chart/charts/schemas/instance.ts b/src/services/chart/charts/schemas/instance.ts
deleted file mode 100644
index 785d6ae7ce..0000000000
--- a/src/services/chart/charts/schemas/instance.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/**
- * インスタンスごとのチャート
- */
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		requests: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				failed: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				succeeded: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				received: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		},
-
-		notes: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				total: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				inc: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				dec: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-
-				diffs: {
-					type: 'object' as const,
-					optional: false as const, nullable: false as const,
-					properties: {
-						normal: {
-							type: 'number' as const,
-							optional: false as const, nullable: false as const,
-						},
-
-						reply: {
-							type: 'number' as const,
-							optional: false as const, nullable: false as const,
-						},
-
-						renote: {
-							type: 'number' as const,
-							optional: false as const, nullable: false as const,
-						},
-					}
-				},
-			}
-		},
-
-		users: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				total: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				inc: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				dec: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		},
-
-		following: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				total: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				inc: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				dec: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		},
-
-		followers: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				total: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				inc: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				dec: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		},
-
-		drive: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				totalFiles: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				totalUsage: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				incFiles: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				incUsage: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				decFiles: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-				decUsage: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		},
-	}
-};
-
-export const name = 'instance';
diff --git a/src/services/chart/charts/schemas/network.ts b/src/services/chart/charts/schemas/network.ts
deleted file mode 100644
index 49a364debc..0000000000
--- a/src/services/chart/charts/schemas/network.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * ネットワークに関するチャート
- */
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		incomingRequests: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		outgoingRequests: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		totalTime: { // TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		incomingBytes: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-		outgoingBytes: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-	}
-};
-
-export const name = 'network';
diff --git a/src/services/chart/charts/schemas/notes.ts b/src/services/chart/charts/schemas/notes.ts
deleted file mode 100644
index 2b5105348c..0000000000
--- a/src/services/chart/charts/schemas/notes.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-const logSchema = {
-	total: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	inc: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	dec: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	diffs: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			normal: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-
-			reply: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-
-			renote: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-		}
-	},
-};
-
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		local: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-	}
-};
-
-export const name = 'notes';
diff --git a/src/services/chart/charts/schemas/per-user-drive.ts b/src/services/chart/charts/schemas/per-user-drive.ts
deleted file mode 100644
index 856f1e0439..0000000000
--- a/src/services/chart/charts/schemas/per-user-drive.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		/**
-		 * 集計期間時点での、全ドライブファイル数
-		 */
-		totalCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-
-		/**
-		 * 集計期間時点での、全ドライブファイルの合計サイズ
-		 */
-		totalSize: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-
-		/**
-		 * 増加したドライブファイル数
-		 */
-		incCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-
-		/**
-		 * 増加したドライブ使用量
-		 */
-		incSize: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-
-		/**
-		 * 減少したドライブファイル数
-		 */
-		decCount: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-
-		/**
-		 * 減少したドライブ使用量
-		 */
-		decSize: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-	}
-};
-
-export const name = 'perUserDrive';
diff --git a/src/services/chart/charts/schemas/per-user-following.ts b/src/services/chart/charts/schemas/per-user-following.ts
deleted file mode 100644
index eaf74aaf77..0000000000
--- a/src/services/chart/charts/schemas/per-user-following.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-export const logSchema = {
-	/**
-	 * フォローしている
-	 */
-	followings: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			/**
-			 * フォローしている合計
-			 */
-			total: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-
-			/**
-			 * フォローした数
-			 */
-			inc: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-
-			/**
-			 * フォロー解除した数
-			 */
-			dec: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-		}
-	},
-
-	/**
-	 * フォローされている
-	 */
-	followers: {
-		type: 'object' as const,
-		optional: false as const, nullable: false as const,
-		properties: {
-			/**
-			 * フォローされている合計
-			 */
-			total: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-
-			/**
-			 * フォローされた数
-			 */
-			inc: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-
-			/**
-			 * フォロー解除された数
-			 */
-			dec: {
-				type: 'number' as const,
-				optional: false as const, nullable: false as const,
-			},
-		}
-	},
-};
-
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		local: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-	}
-};
-
-export const name = 'perUserFollowing';
diff --git a/src/services/chart/charts/schemas/per-user-notes.ts b/src/services/chart/charts/schemas/per-user-notes.ts
deleted file mode 100644
index 72b3ff0210..0000000000
--- a/src/services/chart/charts/schemas/per-user-notes.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		total: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-
-		inc: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-
-		dec: {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		},
-
-		diffs: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				normal: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-
-				reply: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-
-				renote: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		},
-	}
-};
-
-export const name = 'perUserNotes';
diff --git a/src/services/chart/charts/schemas/per-user-reactions.ts b/src/services/chart/charts/schemas/per-user-reactions.ts
deleted file mode 100644
index 2a8520db37..0000000000
--- a/src/services/chart/charts/schemas/per-user-reactions.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export const logSchema = {
-	/**
-	 * フォローしている合計
-	 */
-	count: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-};
-
-/**
- * ユーザーごとのリアクションに関するチャート
- */
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		local: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-	}
-};
-
-export const name = 'perUserReaction';
diff --git a/src/services/chart/charts/schemas/test-grouped.ts b/src/services/chart/charts/schemas/test-grouped.ts
deleted file mode 100644
index f8c8250e79..0000000000
--- a/src/services/chart/charts/schemas/test-grouped.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		foo: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				total: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-
-				inc: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-
-				dec: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		}
-	}
-};
-
-export const name = 'testGrouped';
diff --git a/src/services/chart/charts/schemas/test-unique.ts b/src/services/chart/charts/schemas/test-unique.ts
deleted file mode 100644
index 51280400ac..0000000000
--- a/src/services/chart/charts/schemas/test-unique.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		foo: {
-			type: 'array' as const,
-			optional: false as const, nullable: false as const,
-			items: {
-				type: 'string' as const,
-				optional: false as const, nullable: false as const,
-			}
-		},
-	}
-};
-
-export const name = 'testUnique';
diff --git a/src/services/chart/charts/schemas/test.ts b/src/services/chart/charts/schemas/test.ts
deleted file mode 100644
index 4b48d4d417..0000000000
--- a/src/services/chart/charts/schemas/test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		foo: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: {
-				total: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-
-				inc: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-
-				dec: {
-					type: 'number' as const,
-					optional: false as const, nullable: false as const,
-				},
-			}
-		}
-	}
-};
-
-export const name = 'test';
diff --git a/src/services/chart/charts/schemas/users.ts b/src/services/chart/charts/schemas/users.ts
deleted file mode 100644
index 2bf9d3c50f..0000000000
--- a/src/services/chart/charts/schemas/users.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-const logSchema = {
-	/**
-	 * 集計期間時点での、全ユーザー数
-	 */
-	total: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	/**
-	 * 増加したユーザー数
-	 */
-	inc: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-
-	/**
-	 * 減少したユーザー数
-	 */
-	dec: {
-		type: 'number' as const,
-		optional: false as const, nullable: false as const,
-	},
-};
-
-export const schema = {
-	type: 'object' as const,
-	optional: false as const, nullable: false as const,
-	properties: {
-		local: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-		remote: {
-			type: 'object' as const,
-			optional: false as const, nullable: false as const,
-			properties: logSchema
-		},
-	}
-};
-
-export const name = 'users';
diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts
deleted file mode 100644
index c0d3280c2b..0000000000
--- a/src/services/chart/core.ts
+++ /dev/null
@@ -1,563 +0,0 @@
-/**
- * チャートエンジン
- *
- * Tests located in test/chart
- */
-
-import * as nestedProperty from 'nested-property';
-import autobind from 'autobind-decorator';
-import Logger from '../logger';
-import { SimpleSchema } from '@/misc/simple-schema';
-import { EntitySchema, getRepository, Repository, LessThan, Between } from 'typeorm';
-import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time';
-import { getChartInsertLock } from '@/misc/app-lock';
-
-const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test');
-
-export type Obj = { [key: string]: any };
-
-export type DeepPartial<T> = {
-	[P in keyof T]?: DeepPartial<T[P]>;
-};
-
-type ArrayValue<T> = {
-	[P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>;
-};
-
-type Log = {
-	id: number;
-
-	/**
-	 * 集計のグループ
-	 */
-	group: string | null;
-
-	/**
-	 * 集計日時のUnixタイムスタンプ(秒)
-	 */
-	date: number;
-};
-
-const camelToSnake = (str: string) => {
-	return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase());
-};
-
-const removeDuplicates = (array: any[]) => Array.from(new Set(array));
-
-/**
- * 様々なチャートの管理を司るクラス
- */
-export default abstract class Chart<T extends Record<string, any>> {
-	private static readonly columnPrefix = '___';
-	private static readonly columnDot = '_';
-
-	private name: string;
-	private buffer: {
-		diff: DeepPartial<T>;
-		group: string | null;
-	}[] = [];
-	public schema: SimpleSchema;
-	protected repository: Repository<Log>;
-
-	protected abstract genNewLog(latest: T): DeepPartial<T>;
-
-	/**
-	 * @param logs 日時が新しい方が先頭
-	 */
-	protected abstract aggregate(logs: T[]): T;
-
-	protected abstract fetchActual(group: string | null): Promise<DeepPartial<T>>;
-
-	@autobind
-	private static convertSchemaToFlatColumnDefinitions(schema: SimpleSchema) {
-		const columns = {} as any;
-		const flatColumns = (x: Obj, path?: string) => {
-			for (const [k, v] of Object.entries(x)) {
-				const p = path ? `${path}${this.columnDot}${k}` : k;
-				if (v.type === 'object') {
-					flatColumns(v.properties, p);
-				} else if (v.type === 'number') {
-					columns[this.columnPrefix + p] = {
-						type: 'bigint',
-					};
-				} else if (v.type === 'array' && v.items.type === 'string') {
-					columns[this.columnPrefix + p] = {
-						type: 'varchar',
-						array: true,
-					};
-				}
-			}
-		};
-		flatColumns(schema.properties!);
-		return columns;
-	}
-
-	@autobind
-	private static convertFlattenColumnsToObject(x: Record<string, any>): Record<string, any> {
-		const obj = {} as any;
-		for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) {
-			// now k is ___x_y_z
-			const path = k.substr(Chart.columnPrefix.length).split(Chart.columnDot).join('.');
-			nestedProperty.set(obj, path, x[k]);
-		}
-		return obj;
-	}
-
-	@autobind
-	private static convertObjectToFlattenColumns(x: Record<string, any>) {
-		const columns = {} as Record<string, number | unknown[]>;
-		const flatten = (x: Obj, path?: string) => {
-			for (const [k, v] of Object.entries(x)) {
-				const p = path ? `${path}${this.columnDot}${k}` : k;
-				if (typeof v === 'object' && !Array.isArray(v)) {
-					flatten(v, p);
-				} else {
-					columns[this.columnPrefix + p] = v;
-				}
-			}
-		};
-		flatten(x);
-		return columns;
-	}
-
-	@autobind
-	private static countUniqueFields(x: Record<string, any>) {
-		const exec = (x: Obj) => {
-			const res = {} as Record<string, any>;
-			for (const [k, v] of Object.entries(x)) {
-				if (typeof v === 'object' && !Array.isArray(v)) {
-					res[k] = exec(v);
-				} else if (Array.isArray(v)) {
-					res[k] = Array.from(new Set(v)).length;
-				} else {
-					res[k] = v;
-				}
-			}
-			return res;
-		};
-		return exec(x);
-	}
-
-	@autobind
-	private static convertQuery(diff: Record<string, number | unknown[]>) {
-		const query: Record<string, Function> = {};
-
-		for (const [k, v] of Object.entries(diff)) {
-			if (typeof v === 'number') {
-				if (v > 0) query[k] = () => `"${k}" + ${v}`;
-				if (v < 0) query[k] = () => `"${k}" - ${Math.abs(v)}`;
-			} else if (Array.isArray(v)) {
-				// TODO: item が文字列以外の場合も対応
-				// TODO: item をSQLエスケープ
-				const items = v.map(item => `"${item}"`).join(',');
-				query[k] = () => `array_cat("${k}", '{${items}}'::varchar[])`;
-			}
-		}
-
-		return query;
-	}
-
-	@autobind
-	private static dateToTimestamp(x: Date): Log['date'] {
-		return Math.floor(x.getTime() / 1000);
-	}
-
-	@autobind
-	private static parseDate(date: Date): [number, number, number, number, number, number, number] {
-		const y = date.getUTCFullYear();
-		const m = date.getUTCMonth();
-		const d = date.getUTCDate();
-		const h = date.getUTCHours();
-		const _m = date.getUTCMinutes();
-		const _s = date.getUTCSeconds();
-		const _ms = date.getUTCMilliseconds();
-
-		return [y, m, d, h, _m, _s, _ms];
-	}
-
-	@autobind
-	private static getCurrentDate() {
-		return Chart.parseDate(new Date());
-	}
-
-	@autobind
-	public static schemaToEntity(name: string, schema: SimpleSchema): EntitySchema {
-		return new EntitySchema({
-			name: `__chart__${camelToSnake(name)}`,
-			columns: {
-				id: {
-					type: 'integer',
-					primary: true,
-					generated: true
-				},
-				date: {
-					type: 'integer',
-				},
-				group: {
-					type: 'varchar',
-					length: 128,
-					nullable: true
-				},
-				...Chart.convertSchemaToFlatColumnDefinitions(schema)
-			},
-			indices: [{
-				columns: ['date', 'group'],
-				unique: true,
-			}, { // groupにnullが含まれると↑のuniqueは機能しないので↓の部分インデックスでカバー
-				columns: ['date'],
-				unique: true,
-				where: '"group" IS NULL'
-			}]
-		});
-	}
-
-	constructor(name: string, schema: SimpleSchema, grouped = false) {
-		this.name = name;
-		this.schema = schema;
-		const entity = Chart.schemaToEntity(name, schema);
-
-		const keys = ['date'];
-		if (grouped) keys.push('group');
-
-		entity.options.uniques = [{
-			columns: keys
-		}];
-
-		this.repository = getRepository<Log>(entity);
-	}
-
-	@autobind
-	private getNewLog(latest: T | null): T {
-		const log = latest ? this.genNewLog(latest) : {};
-		const flatColumns = (x: Obj, path?: string) => {
-			for (const [k, v] of Object.entries(x)) {
-				const p = path ? `${path}.${k}` : k;
-				if (v.type === 'object') {
-					flatColumns(v.properties, p);
-				} else {
-					if (nestedProperty.get(log, p) == null) {
-						const emptyValue = v.type === 'number' ? 0 : [];
-						nestedProperty.set(log, p, emptyValue);
-					}
-				}
-			}
-		};
-		flatColumns(this.schema.properties!);
-		return log as T;
-	}
-
-	@autobind
-	private getLatestLog(group: string | null = null): Promise<Log | null> {
-		return this.repository.findOne({
-			group: group,
-		}, {
-			order: {
-				date: -1
-			}
-		}).then(x => x || null);
-	}
-
-	@autobind
-	private async getCurrentLog(group: string | null = null): Promise<Log> {
-		const [y, m, d, h] = Chart.getCurrentDate();
-
-		const current = dateUTC([y, m, d, h]);
-
-		// 現在(=今のHour)のログ
-		const currentLog = await this.repository.findOne({
-			date: Chart.dateToTimestamp(current),
-			...(group ? { group: group } : {})
-		});
-
-		// ログがあればそれを返して終了
-		if (currentLog != null) {
-			return currentLog;
-		}
-
-		let log: Log;
-		let data: T;
-
-		// 集計期間が変わってから、初めてのチャート更新なら
-		// 最も最近のログを持ってくる
-		// * 例えば集計期間が「日」である場合で考えると、
-		// * 昨日何もチャートを更新するような出来事がなかった場合は、
-		// * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
-		// * 「昨日の」と決め打ちせずに「もっとも最近の」とします
-		const latest = await this.getLatestLog(group);
-
-		if (latest != null) {
-			const obj = Chart.convertFlattenColumnsToObject(latest) as T;
-
-			// 空ログデータを作成
-			data = this.getNewLog(obj);
-		} else {
-			// ログが存在しなかったら
-			// (Misskeyインスタンスを建てて初めてのチャート更新時など)
-
-			// 初期ログデータを作成
-			data = this.getNewLog(null);
-
-			logger.info(`${this.name + (group ? `:${group}` : '')}: Initial commit created`);
-		}
-
-		const date = Chart.dateToTimestamp(current);
-		const lockKey = `${this.name}:${date}:${group}`;
-
-		const unlock = await getChartInsertLock(lockKey);
-		try {
-			// ロック内でもう1回チェックする
-			const currentLog = await this.repository.findOne({
-				date: date,
-				...(group ? { group: group } : {})
-			});
-
-			// ログがあればそれを返して終了
-			if (currentLog != null) return currentLog;
-
-			// 新規ログ挿入
-			log = await this.repository.insert({
-				group: group,
-				date: date,
-				...Chart.convertObjectToFlattenColumns(data)
-			}).then(x => this.repository.findOneOrFail(x.identifiers[0]));
-
-			logger.info(`${this.name + (group ? `:${group}` : '')}: New commit created`);
-
-			return log;
-		} finally {
-			unlock();
-		}
-	}
-
-	@autobind
-	protected commit(diff: DeepPartial<T>, group: string | null = null): void {
-		this.buffer.push({
-			diff, group,
-		});
-	}
-
-	@autobind
-	public async save() {
-		if (this.buffer.length === 0) {
-			logger.info(`${this.name}: Write skipped`);
-			return;
-		}
-
-		// TODO: 前の時間のログがbufferにあった場合のハンドリング
-		// 例えば、save が20分ごとに行われるとして、前回行われたのは 01:50 だったとする。
-		// 次に save が行われるのは 02:10 ということになるが、もし 01:55 に新規ログが buffer に追加されたとすると、
-		// そのログは本来は 01:00~ のログとしてDBに保存されて欲しいのに、02:00~ のログ扱いになってしまう。
-		// これを回避するための実装は複雑になりそうなため、一旦保留。
-
-		const update = async (log: Log) => {
-			const finalDiffs = {} as Record<string, number | unknown[]>;
-
-			for (const diff of this.buffer.filter(q => q.group === log.group).map(q => q.diff)) {
-				const columns = Chart.convertObjectToFlattenColumns(diff);
-
-				for (const [k, v] of Object.entries(columns)) {
-					if (finalDiffs[k] == null) {
-						finalDiffs[k] = v;
-					} else {
-						if (typeof finalDiffs[k] === 'number') {
-							(finalDiffs[k] as number) += v as number;
-						} else {
-							(finalDiffs[k] as unknown[]) = (finalDiffs[k] as unknown[]).concat(v);
-						}
-					}
-				}
-			}
-
-			const query = Chart.convertQuery(finalDiffs);
-
-			// ログ更新
-			await this.repository.createQueryBuilder()
-				.update()
-				.set(query)
-				.where('id = :id', { id: log.id })
-				.execute();
-
-			logger.info(`${this.name + (log.group ? `:${log.group}` : '')}: Updated`);
-
-			// TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする
-			this.buffer = this.buffer.filter(q => q.group !== log.group);
-		};
-
-		const groups = removeDuplicates(this.buffer.map(log => log.group));
-
-		await Promise.all(groups.map(group => this.getCurrentLog(group).then(log => update(log))));
-	}
-
-	@autobind
-	public async resync(group: string | null = null): Promise<any> {
-		const data = await this.fetchActual(group);
-
-		const update = async (log: Log) => {
-			await this.repository.createQueryBuilder()
-				.update()
-				.set(Chart.convertObjectToFlattenColumns(data))
-				.where('id = :id', { id: log.id })
-				.execute();
-		};
-
-		return this.getCurrentLog(group).then(log => update(log));
-	}
-
-	@autobind
-	protected async inc(inc: DeepPartial<T>, group: string | null = null): Promise<void> {
-		await this.commit(inc, group);
-	}
-
-	@autobind
-	public async getChart(span: 'hour' | 'day', amount: number, cursor: Date | null, group: string | null = null): Promise<ArrayValue<T>> {
-		const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
-		const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
-
-		const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
-
-		const gt =
-			span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
-			span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
-			null as never;
-
-		// ログ取得
-		let logs = await this.repository.find({
-			where: {
-				group: group,
-				date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt))
-			},
-			order: {
-				date: -1
-			},
-		});
-
-		// 要求された範囲にログがひとつもなかったら
-		if (logs.length === 0) {
-			// もっとも新しいログを持ってくる
-			// (すくなくともひとつログが無いと隙間埋めできないため)
-			const recentLog = await this.repository.findOne({
-				group: group,
-			}, {
-				order: {
-					date: -1
-				},
-			});
-
-			if (recentLog) {
-				logs = [recentLog];
-			}
-
-		// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
-		} else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) {
-			// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
-			// (隙間埋めできないため)
-			const outdatedLog = await this.repository.findOne({
-				group: group,
-				date: LessThan(Chart.dateToTimestamp(gt))
-			}, {
-				order: {
-					date: -1
-				},
-			});
-
-			if (outdatedLog) {
-				logs.push(outdatedLog);
-			}
-		}
-
-		const chart: T[] = [];
-
-		if (span === 'hour') {
-			for (let i = (amount - 1); i >= 0; i--) {
-				const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour');
-
-				const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
-
-				if (log) {
-					const data = Chart.convertFlattenColumnsToObject(log);
-					chart.unshift(Chart.countUniqueFields(data) as T);
-				} else {
-					// 隙間埋め
-					const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
-					const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
-					chart.unshift(Chart.countUniqueFields(this.getNewLog(data)) as T);
-				}
-			}
-		} else if (span === 'day') {
-			const logsForEachDays: T[][] = [];
-			let currentDay = -1;
-			let currentDayIndex = -1;
-			for (let i = ((amount - 1) * 24) + h; i >= 0; i--) {
-				const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour');
-				const _currentDay = Chart.parseDate(current)[2];
-				if (currentDay != _currentDay) currentDayIndex++;
-				currentDay = _currentDay;
-
-				const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
-
-				if (log) {
-					if (logsForEachDays[currentDayIndex]) {
-						logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log) as T);
-					} else {
-						logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log) as T];
-					}
-				} else {
-					// 隙間埋め
-					const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
-					const data = latest ? Chart.convertFlattenColumnsToObject(latest) as T : null;
-					const newLog = this.getNewLog(data);
-					if (logsForEachDays[currentDayIndex]) {
-						logsForEachDays[currentDayIndex].unshift(newLog);
-					} else {
-						logsForEachDays[currentDayIndex] = [newLog];
-					}
-				}
-			}
-
-			for (const logs of logsForEachDays) {
-				const log = this.aggregate(logs);
-				chart.unshift(Chart.countUniqueFields(log) as T);
-			}
-		}
-
-		const res: ArrayValue<T> = {} as any;
-
-		/**
-		 * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
-		 * を
-		 * { foo: [1, 2, 3], bar: [5, 6, 7] }
-		 * にする
-		 */
-		const compact = (x: Obj, path?: string) => {
-			for (const [k, v] of Object.entries(x)) {
-				const p = path ? `${path}.${k}` : k;
-				if (typeof v === 'object' && !Array.isArray(v)) {
-					compact(v, p);
-				} else {
-					const values = chart.map(s => nestedProperty.get(s, p));
-					nestedProperty.set(res, p, values);
-				}
-			}
-		};
-
-		compact(chart[0]);
-
-		return res;
-	}
-}
-
-export function convertLog(logSchema: SimpleSchema): SimpleSchema {
-	const v: SimpleSchema = JSON.parse(JSON.stringify(logSchema)); // copy
-	if (v.type === 'number') {
-		v.type = 'array';
-		v.items = {
-			type: 'number' as const,
-			optional: false as const, nullable: false as const,
-		};
-	} else if (v.type === 'object') {
-		for (const k of Object.keys(v.properties!)) {
-			v.properties![k] = convertLog(v.properties![k]);
-		}
-	}
-	return v;
-}
diff --git a/src/services/chart/entities.ts b/src/services/chart/entities.ts
deleted file mode 100644
index 23a97607eb..0000000000
--- a/src/services/chart/entities.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { fileURLToPath } from 'url';
-import { dirname } from 'path';
-import Chart from './core';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-export const entities = Object.values(require('require-all')({
-	dirname: _dirname + '/charts/schemas',
-	filter: /^.+\.[jt]s$/,
-	resolve: (x: any) => {
-		return Chart.schemaToEntity(x.name, x.schema);
-	}
-}));
diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts
deleted file mode 100644
index 61eb431ea3..0000000000
--- a/src/services/chart/index.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import FederationChart from './charts/classes/federation';
-import NotesChart from './charts/classes/notes';
-import UsersChart from './charts/classes/users';
-import NetworkChart from './charts/classes/network';
-import ActiveUsersChart from './charts/classes/active-users';
-import InstanceChart from './charts/classes/instance';
-import PerUserNotesChart from './charts/classes/per-user-notes';
-import DriveChart from './charts/classes/drive';
-import PerUserReactionsChart from './charts/classes/per-user-reactions';
-import HashtagChart from './charts/classes/hashtag';
-import PerUserFollowingChart from './charts/classes/per-user-following';
-import PerUserDriveChart from './charts/classes/per-user-drive';
-import { beforeShutdown } from '@/misc/before-shutdown';
-
-export const federationChart = new FederationChart();
-export const notesChart = new NotesChart();
-export const usersChart = new UsersChart();
-export const networkChart = new NetworkChart();
-export const activeUsersChart = new ActiveUsersChart();
-export const instanceChart = new InstanceChart();
-export const perUserNotesChart = new PerUserNotesChart();
-export const driveChart = new DriveChart();
-export const perUserReactionsChart = new PerUserReactionsChart();
-export const hashtagChart = new HashtagChart();
-export const perUserFollowingChart = new PerUserFollowingChart();
-export const perUserDriveChart = new PerUserDriveChart();
-
-const charts = [
-	federationChart,
-	notesChart,
-	usersChart,
-	networkChart,
-	activeUsersChart,
-	instanceChart,
-	perUserNotesChart,
-	driveChart,
-	perUserReactionsChart,
-	hashtagChart,
-	perUserFollowingChart,
-	perUserDriveChart,
-];
-
-// 20分おきにメモリ情報をDBに書き込み
-setInterval(() => {
-	for (const chart of charts) {
-		chart.save();
-	}
-}, 1000 * 60 * 20);
-
-beforeShutdown(() => Promise.all(charts.map(chart => chart.save())));
diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts
deleted file mode 100644
index 5398d486c0..0000000000
--- a/src/services/create-notification.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { publishMainStream } from '@/services/stream';
-import pushSw from './push-notification';
-import { Notifications, Mutings, UserProfiles, Users } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { User } from '@/models/entities/user';
-import { Notification } from '@/models/entities/notification';
-import { sendEmailNotification } from './send-email-notification';
-
-export async function createNotification(
-	notifieeId: User['id'],
-	type: Notification['type'],
-	data: Partial<Notification>
-) {
-	if (data.notifierId && (notifieeId === data.notifierId)) {
-		return null;
-	}
-
-	const profile = await UserProfiles.findOne({ userId: notifieeId });
-
-	const isMuted = profile?.mutingNotificationTypes.includes(type);
-
-	// Create notification
-	const notification = await Notifications.save({
-		id: genId(),
-		createdAt: new Date(),
-		notifieeId: notifieeId,
-		type: type,
-		// 相手がこの通知をミュートしているようなら、既読を予めつけておく
-		isRead: isMuted,
-		...data
-	} as Partial<Notification>);
-
-	const packed = await Notifications.pack(notification, {});
-
-	// Publish notification event
-	publishMainStream(notifieeId, 'notification', packed);
-
-	// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
-	setTimeout(async () => {
-		const fresh = await Notifications.findOne(notification.id);
-		if (fresh == null) return; // 既に削除されているかもしれない
-		if (fresh.isRead) return;
-
-		//#region ただしミュートしているユーザーからの通知なら無視
-		const mutings = await Mutings.find({
-			muterId: notifieeId
-		});
-		if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
-			return;
-		}
-		//#endregion
-
-		publishMainStream(notifieeId, 'unreadNotification', packed);
-
-		pushSw(notifieeId, 'notification', packed);
-		if (type === 'follow') sendEmailNotification.follow(notifieeId, await Users.findOneOrFail(data.notifierId!));
-		if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, await Users.findOneOrFail(data.notifierId!));
-	}, 2000);
-
-	return notification;
-}
diff --git a/src/services/create-system-user.ts b/src/services/create-system-user.ts
deleted file mode 100644
index 71be8d4abf..0000000000
--- a/src/services/create-system-user.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import * as bcrypt from 'bcryptjs';
-import { v4 as uuid } from 'uuid';
-import generateNativeUserToken from '../server/api/common/generate-native-user-token';
-import { genRsaKeyPair } from '@/misc/gen-key-pair';
-import { User } from '@/models/entities/user';
-import { UserProfile } from '@/models/entities/user-profile';
-import { getConnection } from 'typeorm';
-import { genId } from '@/misc/gen-id';
-import { UserKeypair } from '@/models/entities/user-keypair';
-import { UsedUsername } from '@/models/entities/used-username';
-
-export async function createSystemUser(username: string) {
-	const password = uuid();
-
-	// Generate hash of password
-	const salt = await bcrypt.genSalt(8);
-	const hash = await bcrypt.hash(password, salt);
-
-	// Generate secret
-	const secret = generateNativeUserToken();
-
-	const keyPair = await genRsaKeyPair(4096);
-
-	let account!: User;
-
-	// Start transaction
-	await getConnection().transaction(async transactionalEntityManager => {
-		const exist = await transactionalEntityManager.findOne(User, {
-			usernameLower: username.toLowerCase(),
-			host: null
-		});
-
-		if (exist) throw new Error('the user is already exists');
-
-		account = await transactionalEntityManager.insert(User, {
-			id: genId(),
-			createdAt: new Date(),
-			username: username,
-			usernameLower: username.toLowerCase(),
-			host: null,
-			token: secret,
-			isAdmin: false,
-			isLocked: true,
-			isExplorable: false,
-			isBot: true,
-		}).then(x => transactionalEntityManager.findOneOrFail(User, x.identifiers[0]));
-
-		await transactionalEntityManager.insert(UserKeypair, {
-			publicKey: keyPair.publicKey,
-			privateKey: keyPair.privateKey,
-			userId: account.id
-		});
-
-		await transactionalEntityManager.insert(UserProfile, {
-			userId: account.id,
-			autoAcceptFollowed: false,
-			password: hash,
-		});
-
-		await transactionalEntityManager.insert(UsedUsername, {
-			createdAt: new Date(),
-			username: username.toLowerCase(),
-		});
-	});
-
-	return account;
-}
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
deleted file mode 100644
index 6c5fefd4ad..0000000000
--- a/src/services/drive/add-file.ts
+++ /dev/null
@@ -1,466 +0,0 @@
-import * as fs from 'fs';
-
-import { v4 as uuid } from 'uuid';
-
-import { publishMainStream, publishDriveStream } from '@/services/stream';
-import { deleteFile } from './delete-file';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { GenerateVideoThumbnail } from './generate-video-thumbnail';
-import { driveLogger } from './logger';
-import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng, convertSharpToPngOrJpeg } from './image-processor';
-import { contentDisposition } from '@/misc/content-disposition';
-import { getFileInfo } from '@/misc/get-file-info';
-import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index';
-import { InternalStorage } from './internal-storage';
-import { DriveFile } from '@/models/entities/drive-file';
-import { IRemoteUser, User } from '@/models/entities/user';
-import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index';
-import { genId } from '@/misc/gen-id';
-import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
-import * as S3 from 'aws-sdk/clients/s3';
-import { getS3 } from './s3';
-import * as sharp from 'sharp';
-
-const logger = driveLogger.createSubLogger('register', 'yellow');
-
-/***
- * Save file
- * @param path Path for original
- * @param name Name for original
- * @param type Content-Type for original
- * @param hash Hash for original
- * @param size Size for original
- */
-async function save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<DriveFile> {
-	// thunbnail, webpublic を必要なら生成
-	const alts = await generateAlts(path, type, !file.uri);
-
-	const meta = await fetchMeta();
-
-	if (meta.useObjectStorage) {
-		//#region ObjectStorage params
-		let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']);
-
-		if (ext === '') {
-			if (type === 'image/jpeg') ext = '.jpg';
-			if (type === 'image/png') ext = '.png';
-			if (type === 'image/webp') ext = '.webp';
-			if (type === 'image/apng') ext = '.apng';
-			if (type === 'image/vnd.mozilla.apng') ext = '.apng';
-		}
-
-		const baseUrl = meta.objectStorageBaseUrl
-			|| `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
-
-		// for original
-		const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`;
-		const url = `${ baseUrl }/${ key }`;
-
-		// for alts
-		let webpublicKey: string | null = null;
-		let webpublicUrl: string | null = null;
-		let thumbnailKey: string | null = null;
-		let thumbnailUrl: string | null = null;
-		//#endregion
-
-		//#region Uploads
-		logger.info(`uploading original: ${key}`);
-		const uploads = [
-			upload(key, fs.createReadStream(path), type, name)
-		];
-
-		if (alts.webpublic) {
-			webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`;
-			webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
-
-			logger.info(`uploading webpublic: ${webpublicKey}`);
-			uploads.push(upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, name));
-		}
-
-		if (alts.thumbnail) {
-			thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`;
-			thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
-
-			logger.info(`uploading thumbnail: ${thumbnailKey}`);
-			uploads.push(upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type));
-		}
-
-		await Promise.all(uploads);
-		//#endregion
-
-		file.url = url;
-		file.thumbnailUrl = thumbnailUrl;
-		file.webpublicUrl = webpublicUrl;
-		file.accessKey = key;
-		file.thumbnailAccessKey = thumbnailKey;
-		file.webpublicAccessKey = webpublicKey;
-		file.name = name;
-		file.type = type;
-		file.md5 = hash;
-		file.size = size;
-		file.storedInternal = false;
-
-		return await DriveFiles.save(file);
-	} else { // use internal storage
-		const accessKey = uuid();
-		const thumbnailAccessKey = 'thumbnail-' + uuid();
-		const webpublicAccessKey = 'webpublic-' + uuid();
-
-		const url = InternalStorage.saveFromPath(accessKey, path);
-
-		let thumbnailUrl: string | null = null;
-		let webpublicUrl: string | null = null;
-
-		if (alts.thumbnail) {
-			thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data);
-			logger.info(`thumbnail stored: ${thumbnailAccessKey}`);
-		}
-
-		if (alts.webpublic) {
-			webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data);
-			logger.info(`web stored: ${webpublicAccessKey}`);
-		}
-
-		file.storedInternal = true;
-		file.url = url;
-		file.thumbnailUrl = thumbnailUrl;
-		file.webpublicUrl = webpublicUrl;
-		file.accessKey = accessKey;
-		file.thumbnailAccessKey = thumbnailAccessKey;
-		file.webpublicAccessKey = webpublicAccessKey;
-		file.name = name;
-		file.type = type;
-		file.md5 = hash;
-		file.size = size;
-
-		return await DriveFiles.save(file);
-	}
-}
-
-/**
- * Generate webpublic, thumbnail, etc
- * @param path Path for original
- * @param type Content-Type for original
- * @param generateWeb Generate webpublic or not
- */
-export async function generateAlts(path: string, type: string, generateWeb: boolean) {
-	if (type.startsWith('video/')) {
-		try {
-			const thumbnail = await GenerateVideoThumbnail(path);
-			return {
-				webpublic: null,
-				thumbnail
-			};
-		} catch (e) {
-			logger.warn(`GenerateVideoThumbnail failed: ${e}`);
-			return {
-				webpublic: null,
-				thumbnail: null
-			};
-		}
-	}
-
-	if (!['image/jpeg', 'image/png', 'image/webp'].includes(type)) {
-		logger.debug(`web image and thumbnail not created (not an required file)`);
-		return {
-			webpublic: null,
-			thumbnail: null
-		};
-	}
-
-	let img: sharp.Sharp | null = null;
-
-	try {
-		img = sharp(path);
-		const metadata = await img.metadata();
-		const isAnimated = metadata.pages && metadata.pages > 1;
-
-		// skip animated
-		if (isAnimated) {
-			return {
-				webpublic: null,
-				thumbnail: null
-			};
-		}
-	} catch (e) {
-		logger.warn(`sharp failed: ${e}`);
-		return {
-			webpublic: null,
-			thumbnail: null
-		};
-	}
-
-	// #region webpublic
-	let webpublic: IImage | null = null;
-
-	if (generateWeb) {
-		logger.info(`creating web image`);
-
-		try {
-			if (['image/jpeg'].includes(type)) {
-				webpublic = await convertSharpToJpeg(img, 2048, 2048);
-			} else if (['image/webp'].includes(type)) {
-				webpublic = await convertSharpToWebp(img, 2048, 2048);
-			} else if (['image/png'].includes(type)) {
-				webpublic = await convertSharpToPng(img, 2048, 2048);
-			} else {
-				logger.debug(`web image not created (not an required image)`);
-			}
-		} catch (e) {
-			logger.warn(`web image not created (an error occured)`, e);
-		}
-	} else {
-		logger.info(`web image not created (from remote)`);
-	}
-	// #endregion webpublic
-
-	// #region thumbnail
-	let thumbnail: IImage | null = null;
-
-	try {
-		if (['image/jpeg', 'image/webp'].includes(type)) {
-			thumbnail = await convertSharpToJpeg(img, 498, 280);
-		} else if (['image/png'].includes(type)) {
-			thumbnail = await convertSharpToPngOrJpeg(img, 498, 280);
-		} else {
-			logger.debug(`thumbnail not created (not an required file)`);
-		}
-	} catch (e) {
-		logger.warn(`thumbnail not created (an error occured)`, e);
-	}
-	// #endregion thumbnail
-
-	return {
-		webpublic,
-		thumbnail,
-	};
-}
-
-/**
- * Upload to ObjectStorage
- */
-async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) {
-	if (type === 'image/apng') type = 'image/png';
-
-	const meta = await fetchMeta();
-
-	const params = {
-		Bucket: meta.objectStorageBucket,
-		Key: key,
-		Body: stream,
-		ContentType: type,
-		CacheControl: 'max-age=31536000, immutable',
-	} as S3.PutObjectRequest;
-
-	if (filename) params.ContentDisposition = contentDisposition('inline', filename);
-	if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
-
-	const s3 = getS3(meta);
-
-	const upload = s3.upload(params, {
-		partSize: s3.endpoint?.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024
-	});
-
-	const result = await upload.promise();
-	if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
-}
-
-async function deleteOldFile(user: IRemoteUser) {
-	const q = DriveFiles.createQueryBuilder('file')
-		.where('file.userId = :userId', { userId: user.id })
-		.andWhere('file.isLink = FALSE');
-
-	if (user.avatarId) {
-		q.andWhere('file.id != :avatarId', { avatarId: user.avatarId });
-	}
-
-	if (user.bannerId) {
-		q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
-	}
-
-	q.orderBy('file.id', 'ASC');
-
-	const oldFile = await q.getOne();
-
-	if (oldFile) {
-		deleteFile(oldFile, true);
-	}
-}
-
-/**
- * Add file to drive
- *
- * @param user User who wish to add file
- * @param path File path
- * @param name Name
- * @param comment Comment
- * @param folderId Folder ID
- * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
- * @param isLink Do not save file to local
- * @param url URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL)
- * @param uri URL of source (リモートインスタンスのURLからアップロードされた場合の元URL)
- * @param sensitive Mark file as sensitive
- * @return Created drive file
- */
-export default async function(
-	user: { id: User['id']; host: User['host'] } | null,
-	path: string,
-	name: string | null = null,
-	comment: string | null = null,
-	folderId: any = null,
-	force: boolean = false,
-	isLink: boolean = false,
-	url: string | null = null,
-	uri: string | null = null,
-	sensitive: boolean | null = null
-): Promise<DriveFile> {
-	const info = await getFileInfo(path);
-	logger.info(`${JSON.stringify(info)}`);
-
-	// detect name
-	const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled');
-
-	if (user && !force) {
-		// Check if there is a file with the same hash
-		const much = await DriveFiles.findOne({
-			md5: info.md5,
-			userId: user.id,
-		});
-
-		if (much) {
-			logger.info(`file with same hash is found: ${much.id}`);
-			return much;
-		}
-	}
-
-	//#region Check drive usage
-	if (user && !isLink) {
-		const usage = await DriveFiles.calcDriveUsageOf(user);
-
-		const instance = await fetchMeta();
-		const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
-
-		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
-
-		// If usage limit exceeded
-		if (usage + info.size > driveCapacity) {
-			if (Users.isLocalUser(user)) {
-				throw new Error('no-free-space');
-			} else {
-				// (アバターまたはバナーを含まず)最も古いファイルを削除する
-				deleteOldFile(await Users.findOneOrFail(user.id) as IRemoteUser);
-			}
-		}
-	}
-	//#endregion
-
-	const fetchFolder = async () => {
-		if (!folderId) {
-			return null;
-		}
-
-		const driveFolder = await DriveFolders.findOne({
-			id: folderId,
-			userId: user ? user.id : null
-		});
-
-		if (driveFolder == null) throw new Error('folder-not-found');
-
-		return driveFolder;
-	};
-
-	const properties: {
-		width?: number;
-		height?: number;
-	} = {};
-
-	if (info.width) {
-		properties['width'] = info.width;
-		properties['height'] = info.height;
-	}
-
-	const profile = user ? await UserProfiles.findOne(user.id) : null;
-
-	const folder = await fetchFolder();
-
-	let file = new DriveFile();
-	file.id = genId();
-	file.createdAt = new Date();
-	file.userId = user ? user.id : null;
-	file.userHost = user ? user.host : null;
-	file.folderId = folder !== null ? folder.id : null;
-	file.comment = comment;
-	file.properties = properties;
-	file.blurhash = info.blurhash || null;
-	file.isLink = isLink;
-	file.isSensitive = user
-		? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
-			(sensitive !== null && sensitive !== undefined)
-				? sensitive
-				: false
-		: false;
-
-	if (url !== null) {
-		file.src = url;
-
-		if (isLink) {
-			file.url = url;
-			// ローカルプロキシ用
-			file.accessKey = uuid();
-			file.thumbnailAccessKey = 'thumbnail-' + uuid();
-			file.webpublicAccessKey = 'webpublic-' + uuid();
-		}
-	}
-
-	if (uri !== null) {
-		file.uri = uri;
-	}
-
-	if (isLink) {
-		try {
-			file.size = 0;
-			file.md5 = info.md5;
-			file.name = detectedName;
-			file.type = info.type.mime;
-			file.storedInternal = false;
-
-			file = await DriveFiles.save(file);
-		} catch (e) {
-			// duplicate key error (when already registered)
-			if (isDuplicateKeyValueError(e)) {
-				logger.info(`already registered ${file.uri}`);
-
-				file = await DriveFiles.findOne({
-					uri: file.uri,
-					userId: user ? user.id : null
-				}) as DriveFile;
-			} else {
-				logger.error(e);
-				throw e;
-			}
-		}
-	} else {
-		file = await (save(file, path, detectedName, info.type.mime, info.md5, info.size));
-	}
-
-	logger.succ(`drive file has been created ${file.id}`);
-
-	if (user) {
-		DriveFiles.pack(file, { self: true }).then(packedFile => {
-			// Publish driveFileCreated event
-			publishMainStream(user.id, 'driveFileCreated', packedFile);
-			publishDriveStream(user.id, 'fileCreated', packedFile);
-		});
-	}
-
-	// 統計を更新
-	driveChart.update(file, true);
-	perUserDriveChart.update(file, true);
-	if (file.userHost !== null) {
-		instanceChart.updateDrive(file, true);
-		Instances.increment({ host: file.userHost }, 'driveUsage', file.size);
-		Instances.increment({ host: file.userHost }, 'driveFiles', 1);
-	}
-
-	return file;
-}
diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts
deleted file mode 100644
index 2ac11b8295..0000000000
--- a/src/services/drive/delete-file.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { DriveFile } from '@/models/entities/drive-file';
-import { InternalStorage } from './internal-storage';
-import { DriveFiles, Instances } from '@/models/index';
-import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index';
-import { createDeleteObjectStorageFileJob } from '@/queue/index';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { getS3 } from './s3';
-import { v4 as uuid } from 'uuid';
-
-export async function deleteFile(file: DriveFile, isExpired = false) {
-	if (file.storedInternal) {
-		InternalStorage.del(file.accessKey!);
-
-		if (file.thumbnailUrl) {
-			InternalStorage.del(file.thumbnailAccessKey!);
-		}
-
-		if (file.webpublicUrl) {
-			InternalStorage.del(file.webpublicAccessKey!);
-		}
-	} else if (!file.isLink) {
-		createDeleteObjectStorageFileJob(file.accessKey!);
-
-		if (file.thumbnailUrl) {
-			createDeleteObjectStorageFileJob(file.thumbnailAccessKey!);
-		}
-
-		if (file.webpublicUrl) {
-			createDeleteObjectStorageFileJob(file.webpublicAccessKey!);
-		}
-	}
-
-	postProcess(file, isExpired);
-}
-
-export async function deleteFileSync(file: DriveFile, isExpired = false) {
-	if (file.storedInternal) {
-		InternalStorage.del(file.accessKey!);
-
-		if (file.thumbnailUrl) {
-			InternalStorage.del(file.thumbnailAccessKey!);
-		}
-
-		if (file.webpublicUrl) {
-			InternalStorage.del(file.webpublicAccessKey!);
-		}
-	} else if (!file.isLink) {
-		const promises = [];
-
-		promises.push(deleteObjectStorageFile(file.accessKey!));
-
-		if (file.thumbnailUrl) {
-			promises.push(deleteObjectStorageFile(file.thumbnailAccessKey!));
-		}
-
-		if (file.webpublicUrl) {
-			promises.push(deleteObjectStorageFile(file.webpublicAccessKey!));
-		}
-
-		await Promise.all(promises);
-	}
-
-	postProcess(file, isExpired);
-}
-
-async function postProcess(file: DriveFile, isExpired = false) {
-	// リモートファイル期限切れ削除後は直リンクにする
-	if (isExpired && file.userHost !== null && file.uri != null) {
-		DriveFiles.update(file.id, {
-			isLink: true,
-			url: file.uri,
-			thumbnailUrl: null,
-			webpublicUrl: null,
-			storedInternal: false,
-			// ローカルプロキシ用
-			accessKey: uuid(),
-			thumbnailAccessKey: 'thumbnail-' + uuid(),
-			webpublicAccessKey: 'webpublic-' + uuid(),
-		});
-	} else {
-		DriveFiles.delete(file.id);
-	}
-
-	// 統計を更新
-	driveChart.update(file, false);
-	perUserDriveChart.update(file, false);
-	if (file.userHost !== null) {
-		instanceChart.updateDrive(file, false);
-		Instances.decrement({ host: file.userHost }, 'driveUsage', file.size);
-		Instances.decrement({ host: file.userHost }, 'driveFiles', 1);
-	}
-}
-
-export async function deleteObjectStorageFile(key: string) {
-	const meta = await fetchMeta();
-
-	const s3 = getS3(meta);
-
-	await s3.deleteObject({
-		Bucket: meta.objectStorageBucket!,
-		Key: key
-	}).promise();
-}
diff --git a/src/services/drive/generate-video-thumbnail.ts b/src/services/drive/generate-video-thumbnail.ts
deleted file mode 100644
index f0adc7c338..0000000000
--- a/src/services/drive/generate-video-thumbnail.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import * as fs from 'fs';
-import * as tmp from 'tmp';
-import { IImage, convertToJpeg } from './image-processor';
-import * as FFmpeg from 'fluent-ffmpeg';
-
-export async function GenerateVideoThumbnail(path: string): Promise<IImage> {
-	const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => {
-		tmp.dir((e, path, cleanup) => {
-			if (e) return rej(e);
-			res([path, cleanup]);
-		});
-	});
-
-	await new Promise((res, rej) => {
-		FFmpeg({
-			source: path
-		})
-		.on('end', res)
-		.on('error', rej)
-		.screenshot({
-			folder: outDir,
-			filename: 'output.png',
-			count: 1,
-			timestamps: ['5%']
-		});
-	});
-
-	const outPath = `${outDir}/output.png`;
-
-	const thumbnail = await convertToJpeg(outPath, 498, 280);
-
-	// cleanup
-	await fs.promises.unlink(outPath);
-	cleanup();
-
-	return thumbnail;
-}
diff --git a/src/services/drive/image-processor.ts b/src/services/drive/image-processor.ts
deleted file mode 100644
index 493bf5c1cc..0000000000
--- a/src/services/drive/image-processor.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import * as sharp from 'sharp';
-
-export type IImage = {
-	data: Buffer;
-	ext: string | null;
-	type: string;
-};
-
-/**
- * Convert to JPEG
- *   with resize, remove metadata, resolve orientation, stop animation
- */
-export async function convertToJpeg(path: string, width: number, height: number): Promise<IImage> {
-	return convertSharpToJpeg(await sharp(path), width, height);
-}
-
-export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
-	const data = await sharp
-		.resize(width, height, {
-			fit: 'inside',
-			withoutEnlargement: true
-		})
-		.rotate()
-		.jpeg({
-			quality: 85,
-			progressive: true
-		})
-		.toBuffer();
-
-	return {
-		data,
-		ext: 'jpg',
-		type: 'image/jpeg'
-	};
-}
-
-/**
- * Convert to WebP
- *   with resize, remove metadata, resolve orientation, stop animation
- */
-export async function convertToWebp(path: string, width: number, height: number): Promise<IImage> {
-	return convertSharpToWebp(await sharp(path), width, height);
-}
-
-export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
-	const data = await sharp
-		.resize(width, height, {
-			fit: 'inside',
-			withoutEnlargement: true
-		})
-		.rotate()
-		.webp({
-			quality: 85
-		})
-		.toBuffer();
-
-	return {
-		data,
-		ext: 'webp',
-		type: 'image/webp'
-	};
-}
-
-/**
- * Convert to PNG
- *   with resize, remove metadata, resolve orientation, stop animation
- */
-export async function convertToPng(path: string, width: number, height: number): Promise<IImage> {
-	return convertSharpToPng(await sharp(path), width, height);
-}
-
-export async function convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
-	const data = await sharp
-		.resize(width, height, {
-			fit: 'inside',
-			withoutEnlargement: true
-		})
-		.rotate()
-		.png()
-		.toBuffer();
-
-	return {
-		data,
-		ext: 'png',
-		type: 'image/png'
-	};
-}
-
-/**
- * Convert to PNG or JPEG
- *   with resize, remove metadata, resolve orientation, stop animation
- */
-export async function convertToPngOrJpeg(path: string, width: number, height: number): Promise<IImage> {
-	return convertSharpToPngOrJpeg(await sharp(path), width, height);
-}
-
-export async function convertSharpToPngOrJpeg(sharp: sharp.Sharp, width: number, height: number): Promise<IImage> {
-	const stats = await sharp.stats();
-	const metadata = await sharp.metadata();
-
-	// 不透明で300x300pxの範囲を超えていればJPEG
-	if (stats.isOpaque && ((metadata.width && metadata.width >= 300) || (metadata.height && metadata!.height >= 300))) {
-		return await convertSharpToJpeg(sharp, width, height);
-	} else {
-		return await convertSharpToPng(sharp, width, height);
-	}
-}
diff --git a/src/services/drive/internal-storage.ts b/src/services/drive/internal-storage.ts
deleted file mode 100644
index deaf3dc831..0000000000
--- a/src/services/drive/internal-storage.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as fs from 'fs';
-import * as Path from 'path';
-import { fileURLToPath } from 'url';
-import { dirname } from 'path';
-import config from '@/config/index';
-
-//const _filename = fileURLToPath(import.meta.url);
-const _filename = __filename;
-const _dirname = dirname(_filename);
-
-export class InternalStorage {
-	private static readonly path = Path.resolve(_dirname, '../../../files');
-
-	public static resolvePath = (key: string) => Path.resolve(InternalStorage.path, key);
-
-	public static read(key: string) {
-		return fs.createReadStream(InternalStorage.resolvePath(key));
-	}
-
-	public static saveFromPath(key: string, srcPath: string) {
-		fs.mkdirSync(InternalStorage.path, { recursive: true });
-		fs.copyFileSync(srcPath, InternalStorage.resolvePath(key));
-		return `${config.url}/files/${key}`;
-	}
-
-	public static saveFromBuffer(key: string, data: Buffer) {
-		fs.mkdirSync(InternalStorage.path, { recursive: true });
-		fs.writeFileSync(InternalStorage.resolvePath(key), data);
-		return `${config.url}/files/${key}`;
-	}
-
-	public static del(key: string) {
-		fs.unlink(InternalStorage.resolvePath(key), () => {});
-	}
-}
diff --git a/src/services/drive/logger.ts b/src/services/drive/logger.ts
deleted file mode 100644
index 655d074d6e..0000000000
--- a/src/services/drive/logger.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Logger from '../logger';
-
-export const driveLogger = new Logger('drive', 'blue');
diff --git a/src/services/drive/s3.ts b/src/services/drive/s3.ts
deleted file mode 100644
index f473c4a203..0000000000
--- a/src/services/drive/s3.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { URL } from 'url';
-import * as S3 from 'aws-sdk/clients/s3';
-import { Meta } from '@/models/entities/meta';
-import { getAgentByUrl } from '@/misc/fetch';
-
-export function getS3(meta: Meta) {
-	const u = meta.objectStorageEndpoint != null
-		? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
-		: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
-
-	return new S3({
-		endpoint: meta.objectStorageEndpoint || undefined,
-		accessKeyId: meta.objectStorageAccessKey!,
-		secretAccessKey: meta.objectStorageSecretKey!,
-		region: meta.objectStorageRegion || undefined,
-		sslEnabled: meta.objectStorageUseSSL,
-		s3ForcePathStyle: !meta.objectStorageEndpoint	// AWS with endPoint omitted
-			? false
-			: meta.objectStorageS3ForcePathStyle,
-		httpOptions: {
-			agent: getAgentByUrl(new URL(u), !meta.objectStorageUseProxy)
-		}
-	});
-}
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
deleted file mode 100644
index 29788c4af4..0000000000
--- a/src/services/drive/upload-from-url.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { URL } from 'url';
-import create from './add-file';
-import { User } from '@/models/entities/user';
-import { driveLogger } from './logger';
-import { createTemp } from '@/misc/create-temp';
-import { downloadUrl } from '@/misc/download-url';
-import { DriveFolder } from '@/models/entities/drive-folder';
-import { DriveFile } from '@/models/entities/drive-file';
-import { DriveFiles } from '@/models/index';
-
-const logger = driveLogger.createSubLogger('downloader');
-
-export default async (
-	url: string,
-	user: { id: User['id']; host: User['host'] } | null,
-	folderId: DriveFolder['id'] | null = null,
-	uri: string | null = null,
-	sensitive = false,
-	force = false,
-	link = false,
-	comment = null
-): Promise<DriveFile> => {
-	let name = new URL(url).pathname.split('/').pop() || null;
-	if (name == null || !DriveFiles.validateFileName(name)) {
-		name = null;
-	}
-
-	// If the comment is same as the name, skip comment
-	// (image.name is passed in when receiving attachment)
-	if (comment !== null && name == comment) {
-		comment = null;
-	}
-
-	// Create temp file
-	const [path, cleanup] = await createTemp();
-
-	// write content at URL to temp file
-	await downloadUrl(url, path);
-
-	let driveFile: DriveFile;
-	let error;
-
-	try {
-		driveFile = await create(user, path, name, comment, folderId, force, link, url, uri, sensitive);
-		logger.succ(`Got: ${driveFile.id}`);
-	} catch (e) {
-		error = e;
-		logger.error(`Failed to create drive file: ${e}`, {
-			url: url,
-			e: e
-		});
-	}
-
-	// clean-up
-	cleanup();
-
-	if (error) {
-		throw error;
-	} else {
-		return driveFile!;
-	}
-};
diff --git a/src/services/fetch-instance-metadata.ts b/src/services/fetch-instance-metadata.ts
deleted file mode 100644
index 2c401508a9..0000000000
--- a/src/services/fetch-instance-metadata.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-import { DOMWindow, JSDOM } from 'jsdom';
-import fetch from 'node-fetch';
-import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch';
-import { Instance } from '@/models/entities/instance';
-import { Instances } from '@/models/index';
-import { getFetchInstanceMetadataLock } from '@/misc/app-lock';
-import Logger from './logger';
-import { URL } from 'url';
-
-const logger = new Logger('metadata', 'cyan');
-
-export async function fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
-	const unlock = await getFetchInstanceMetadataLock(instance.host);
-
-	if (!force) {
-		const _instance = await Instances.findOne({ host: instance.host });
-		const now = Date.now();
-		if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
-			unlock();
-			return;
-		}
-	}
-
-	logger.info(`Fetching metadata of ${instance.host} ...`);
-
-	try {
-		const [info, dom, manifest] = await Promise.all([
-			fetchNodeinfo(instance).catch(() => null),
-			fetchDom(instance).catch(() => null),
-			fetchManifest(instance).catch(() => null),
-		]);
-
-		const [favicon, icon, themeColor, name, description] = await Promise.all([
-			fetchFaviconUrl(instance, dom).catch(() => null),
-			fetchIconUrl(instance, dom, manifest).catch(() => null),
-			getThemeColor(dom, manifest).catch(() => null),
-			getSiteName(info, dom, manifest).catch(() => null),
-			getDescription(info, dom, manifest).catch(() => null),
-		]);
-
-		logger.succ(`Successfuly fetched metadata of ${instance.host}`);
-
-		const updates = {
-			infoUpdatedAt: new Date(),
-		} as Record<string, any>;
-
-		if (info) {
-			updates.softwareName = info.software?.name.toLowerCase();
-			updates.softwareVersion = info.software?.version;
-			updates.openRegistrations = info.openRegistrations;
-			updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null;
-			updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null;
-		}
-
-		if (name) updates.name = name;
-		if (description) updates.description = description;
-		if (icon || favicon) updates.iconUrl = icon || favicon;
-		if (favicon) updates.faviconUrl = favicon;
-		if (themeColor) updates.themeColor = themeColor;
-
-		await Instances.update(instance.id, updates);
-
-		logger.succ(`Successfuly updated metadata of ${instance.host}`);
-	} catch (e) {
-		logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
-	} finally {
-		unlock();
-	}
-}
-
-type NodeInfo = {
-	openRegistrations?: any;
-	software?: {
-		name?: any;
-		version?: any;
-	};
-	metadata?: {
-		name?: any;
-		nodeName?: any;
-		nodeDescription?: any;
-		description?: any;
-		maintainer?: {
-			name?: any;
-			email?: any;
-		};
-	};
-};
-
-async function fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
-	logger.info(`Fetching nodeinfo of ${instance.host} ...`);
-
-	try {
-		const wellknown = await getJson('https://' + instance.host + '/.well-known/nodeinfo')
-			.catch(e => {
-				if (e.statusCode === 404) {
-					throw 'No nodeinfo provided';
-				} else {
-					throw e.statusCode || e.message;
-				}
-			});
-
-		if (wellknown.links == null || !Array.isArray(wellknown.links)) {
-			throw 'No wellknown links';
-		}
-
-		const links = wellknown.links as any[];
-
-		const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
-		const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
-		const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
-		const link = lnik2_1 || lnik2_0 || lnik1_0;
-
-		if (link == null) {
-			throw 'No nodeinfo link provided';
-		}
-
-		const info = await getJson(link.href)
-			.catch(e => {
-				throw e.statusCode || e.message;
-			});
-
-		logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
-
-		return info;
-	} catch (e) {
-		logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e}`);
-
-		throw e;
-	}
-}
-
-async function fetchDom(instance: Instance): Promise<DOMWindow['document']> {
-	logger.info(`Fetching HTML of ${instance.host} ...`);
-
-	const url = 'https://' + instance.host;
-
-	const html = await getHtml(url);
-
-	const { window } = new JSDOM(html);
-	const doc = window.document;
-
-	return doc;
-}
-
-async function fetchManifest(instance: Instance): Promise<Record<string, any> | null> {
-	const url = 'https://' + instance.host;
-
-	const manifestUrl = url + '/manifest.json';
-
-	const manifest = await getJson(manifestUrl);
-
-	return manifest;
-}
-
-async function fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
-	const url = 'https://' + instance.host;
-
-	if (doc) {
-		const href = doc.querySelector('link[rel="icon"]')?.getAttribute('href');
-
-		if (href) {
-			return (new URL(href, url)).href;
-		}
-	}
-
-	const faviconUrl = url + '/favicon.ico';
-
-	const favicon = await fetch(faviconUrl, {
-		timeout: 10000,
-		agent: getAgentByUrl,
-	});
-
-	if (favicon.ok) {
-		return faviconUrl;
-	}
-
-	return null;
-}
-
-async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
-	if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
-		const url = 'https://' + instance.host;
-		return (new URL(manifest.icons[0].src, url)).href;
-	}
-
-	if (doc) {
-		const url = 'https://' + instance.host;
-
-		const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href');
-		const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href');
-		const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href');
-
-		const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon;
-
-		if (href) {
-			return (new URL(href, url)).href;
-		}
-	}
-
-	return null;
-}
-
-async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
-	if (doc) {
-		const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
-
-		if (themeColor) {
-			return themeColor;
-		}
-	}
-
-	if (manifest) {
-		return manifest.theme_color;
-	}
-
-	return null;
-}
-
-async function getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
-	if (info && info.metadata) {
-		if (info.metadata.nodeName || info.metadata.name) {
-			return info.metadata.nodeName || info.metadata.name;
-		}
-	}
-
-	if (doc) {
-		const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
-
-		if (og) {
-			return og;
-		}
-	}
-
-	if (manifest) {
-		return manifest?.name || manifest?.short_name;
-	}
-
-	return null;
-}
-
-async function getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
-	if (info && info.metadata) {
-		if (info.metadata.nodeDescription || info.metadata.description) {
-			return info.metadata.nodeDescription || info.metadata.description;
-		}
-	}
-
-	if (doc) {
-		const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
-		if (meta) {
-			return meta;
-		}
-
-		const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
-		if (og) {
-			return og;
-		}
-	}
-
-	if (manifest) {
-		return manifest?.name || manifest?.short_name;
-	}
-
-	return null;
-}
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
deleted file mode 100644
index 4d0754b504..0000000000
--- a/src/services/following/create.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { publishMainStream, publishUserEvent } from '@/services/stream';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderFollow from '@/remote/activitypub/renderer/follow';
-import renderAccept from '@/remote/activitypub/renderer/accept';
-import renderReject from '@/remote/activitypub/renderer/reject';
-import { deliver } from '@/queue/index';
-import createFollowRequest from './requests/create';
-import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import Logger from '../logger';
-import { IdentifiableError } from '@/misc/identifiable-error';
-import { User } from '@/models/entities/user';
-import { Followings, Users, FollowRequests, Blockings, Instances, UserProfiles } from '@/models/index';
-import { instanceChart, perUserFollowingChart } from '@/services/chart/index';
-import { genId } from '@/misc/gen-id';
-import { createNotification } from '../create-notification';
-import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
-
-const logger = new Logger('following/create');
-
-export async function insertFollowingDoc(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }, follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox'] }) {
-	if (follower.id === followee.id) return;
-
-	let alreadyFollowed = false;
-
-	await Followings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		followerId: follower.id,
-		followeeId: followee.id,
-
-		// 非正規化
-		followerHost: follower.host,
-		followerInbox: Users.isRemoteUser(follower) ? follower.inbox : null,
-		followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : null,
-		followeeHost: followee.host,
-		followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : null,
-		followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : null
-	}).catch(e => {
-		if (isDuplicateKeyValueError(e) && Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
-			logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
-			alreadyFollowed = true;
-		} else {
-			throw e;
-		}
-	});
-
-	const req = await FollowRequests.findOne({
-		followeeId: followee.id,
-		followerId: follower.id
-	});
-
-	if (req) {
-		await FollowRequests.delete({
-			followeeId: followee.id,
-			followerId: follower.id
-		});
-
-		// 通知を作成
-		createNotification(follower.id, 'followRequestAccepted', {
-			notifierId: followee.id,
-		});
-	}
-
-	if (alreadyFollowed) return;
-
-	//#region Increment counts
-	Users.increment({ id: follower.id }, 'followingCount', 1);
-	Users.increment({ id: followee.id }, 'followersCount', 1);
-	//#endregion
-
-	//#region Update instance stats
-	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
-		registerOrFetchInstanceDoc(follower.host).then(i => {
-			Instances.increment({ id: i.id }, 'followingCount', 1);
-			instanceChart.updateFollowing(i.host, true);
-		});
-	} else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		registerOrFetchInstanceDoc(followee.host).then(i => {
-			Instances.increment({ id: i.id }, 'followersCount', 1);
-			instanceChart.updateFollowers(i.host, true);
-		});
-	}
-	//#endregion
-
-	perUserFollowingChart.update(follower, followee, true);
-
-	// Publish follow event
-	if (Users.isLocalUser(follower)) {
-		Users.pack(followee.id, follower, {
-			detail: true
-		}).then(packed => {
-			publishUserEvent(follower.id, 'follow', packed);
-			publishMainStream(follower.id, 'follow', packed);
-		});
-	}
-
-	// Publish followed event
-	if (Users.isLocalUser(followee)) {
-		Users.pack(follower.id, followee).then(packed => publishMainStream(followee.id, 'followed', packed));
-
-		// 通知を作成
-		createNotification(followee.id, 'follow', {
-			notifierId: follower.id
-		});
-	}
-}
-
-export default async function(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string) {
-	const [follower, followee] = await Promise.all([
-		Users.findOneOrFail(_follower.id),
-		Users.findOneOrFail(_followee.id)
-	]);
-
-	// check blocking
-	const [blocking, blocked] = await Promise.all([
-		Blockings.findOne({
-			blockerId: follower.id,
-			blockeeId: followee.id,
-		}),
-		Blockings.findOne({
-			blockerId: followee.id,
-			blockeeId: follower.id,
-		})
-	]);
-
-	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocked) {
-		// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
-		const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee));
-		deliver(followee , content, follower.inbox);
-		return;
-	} else if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocking) {
-		// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
-		await Blockings.delete(blocking.id);
-	} else {
-		// それ以外は単純に例外
-		if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
-		if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
-	}
-
-	const followeeProfile = await UserProfiles.findOneOrFail(followee.id);
-
-	// フォロー対象が鍵アカウントである or
-	// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
-	// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
-	// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
-	if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) {
-		let autoAccept = false;
-
-		// 鍵アカウントであっても、既にフォローされていた場合はスルー
-		const following = await Followings.findOne({
-			followerId: follower.id,
-			followeeId: followee.id,
-		});
-		if (following) {
-			autoAccept = true;
-		}
-
-		// フォローしているユーザーは自動承認オプション
-		if (!autoAccept && (Users.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
-			const followed = await Followings.findOne({
-				followerId: followee.id,
-				followeeId: follower.id
-			});
-
-			if (followed) autoAccept = true;
-		}
-
-		if (!autoAccept) {
-			await createFollowRequest(follower, followee, requestId);
-			return;
-		}
-	}
-
-	await insertFollowingDoc(followee, follower);
-
-	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
-		const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee));
-		deliver(followee, content, follower.inbox);
-	}
-}
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
deleted file mode 100644
index 29e3372b6a..0000000000
--- a/src/services/following/delete.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { publishMainStream, publishUserEvent } from '@/services/stream';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderFollow from '@/remote/activitypub/renderer/follow';
-import renderUndo from '@/remote/activitypub/renderer/undo';
-import { deliver } from '@/queue/index';
-import Logger from '../logger';
-import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import { User } from '@/models/entities/user';
-import { Followings, Users, Instances } from '@/models/index';
-import { instanceChart, perUserFollowingChart } from '@/services/chart/index';
-
-const logger = new Logger('following/delete');
-
-export default async function(follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, silent = false) {
-	const following = await Followings.findOne({
-		followerId: follower.id,
-		followeeId: followee.id
-	});
-
-	if (following == null) {
-		logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
-		return;
-	}
-
-	await Followings.delete(following.id);
-
-	decrementFollowing(follower, followee);
-
-	// Publish unfollow event
-	if (!silent && Users.isLocalUser(follower)) {
-		Users.pack(followee.id, follower, {
-			detail: true
-		}).then(packed => {
-			publishUserEvent(follower.id, 'unfollow', packed);
-			publishMainStream(follower.id, 'unfollow', packed);
-		});
-	}
-
-	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
-		deliver(follower, content, followee.inbox);
-	}
-}
-
-export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) {
-	//#region Decrement following count
-	Users.decrement({ id: follower.id }, 'followingCount', 1);
-	//#endregion
-
-	//#region Decrement followers count
-	Users.decrement({ id: followee.id }, 'followersCount', 1);
-	//#endregion
-
-	//#region Update instance stats
-	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
-		registerOrFetchInstanceDoc(follower.host).then(i => {
-			Instances.decrement({ id: i.id }, 'followingCount', 1);
-			instanceChart.updateFollowing(i.host, false);
-		});
-	} else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		registerOrFetchInstanceDoc(followee.host).then(i => {
-			Instances.decrement({ id: i.id }, 'followersCount', 1);
-			instanceChart.updateFollowers(i.host, false);
-		});
-	}
-	//#endregion
-
-	perUserFollowingChart.update(follower, followee, false);
-}
diff --git a/src/services/following/requests/accept-all.ts b/src/services/following/requests/accept-all.ts
deleted file mode 100644
index 23b4fd0a46..0000000000
--- a/src/services/following/requests/accept-all.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import accept from './accept';
-import { User } from '@/models/entities/user';
-import { FollowRequests, Users } from '@/models/index';
-
-/**
- * 指定したユーザー宛てのフォローリクエストをすべて承認
- * @param user ユーザー
- */
-export default async function(user: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }) {
-	const requests = await FollowRequests.find({
-		followeeId: user.id
-	});
-
-	for (const request of requests) {
-		const follower = await Users.findOneOrFail(request.followerId);
-		accept(user, follower);
-	}
-}
diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts
deleted file mode 100644
index 316a6f1c12..0000000000
--- a/src/services/following/requests/accept.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderFollow from '@/remote/activitypub/renderer/follow';
-import renderAccept from '@/remote/activitypub/renderer/accept';
-import { deliver } from '@/queue/index';
-import { publishMainStream } from '@/services/stream';
-import { insertFollowingDoc } from '../create';
-import { User, ILocalUser } from '@/models/entities/user';
-import { FollowRequests, Users } from '@/models/index';
-import { IdentifiableError } from '@/misc/identifiable-error';
-
-export default async function(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, follower: User) {
-	const request = await FollowRequests.findOne({
-		followeeId: followee.id,
-		followerId: follower.id
-	});
-
-	if (request == null) {
-		throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
-	}
-
-	await insertFollowingDoc(followee, follower);
-
-	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
-		const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId!), followee));
-		deliver(followee, content, follower.inbox);
-	}
-
-	Users.pack(followee.id, followee, {
-		detail: true
-	}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
-}
diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts
deleted file mode 100644
index 8895849857..0000000000
--- a/src/services/following/requests/cancel.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderFollow from '@/remote/activitypub/renderer/follow';
-import renderUndo from '@/remote/activitypub/renderer/undo';
-import { deliver } from '@/queue/index';
-import { publishMainStream } from '@/services/stream';
-import { IdentifiableError } from '@/misc/identifiable-error';
-import { User, ILocalUser } from '@/models/entities/user';
-import { Users, FollowRequests } from '@/models/index';
-
-export default async function(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox'] }, follower: { id: User['id']; host: User['host']; uri: User['host'] }) {
-	if (Users.isRemoteUser(followee)) {
-		const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
-
-		if (Users.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
-			deliver(follower, content, followee.inbox);
-		}
-	}
-
-	const request = await FollowRequests.findOne({
-		followeeId: followee.id,
-		followerId: follower.id
-	});
-
-	if (request == null) {
-		throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
-	}
-
-	await FollowRequests.delete({
-		followeeId: followee.id,
-		followerId: follower.id
-	});
-
-	Users.pack(followee.id, followee, {
-		detail: true
-	}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
-}
diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts
deleted file mode 100644
index 507cb2b7d1..0000000000
--- a/src/services/following/requests/create.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { publishMainStream } from '@/services/stream';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderFollow from '@/remote/activitypub/renderer/follow';
-import { deliver } from '@/queue/index';
-import { User } from '@/models/entities/user';
-import { Blockings, FollowRequests, Users } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { createNotification } from '../../create-notification';
-
-export default async function(follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, requestId?: string) {
-	if (follower.id === followee.id) return;
-
-	// check blocking
-	const [blocking, blocked] = await Promise.all([
-		Blockings.findOne({
-			blockerId: follower.id,
-			blockeeId: followee.id,
-		}),
-		Blockings.findOne({
-			blockerId: followee.id,
-			blockeeId: follower.id,
-		})
-	]);
-
-	if (blocking != null) throw new Error('blocking');
-	if (blocked != null) throw new Error('blocked');
-
-	const followRequest = await FollowRequests.save({
-		id: genId(),
-		createdAt: new Date(),
-		followerId: follower.id,
-		followeeId: followee.id,
-		requestId,
-
-		// 非正規化
-		followerHost: follower.host,
-		followerInbox: Users.isRemoteUser(follower) ? follower.inbox : undefined,
-		followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : undefined,
-		followeeHost: followee.host,
-		followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined,
-		followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined
-	});
-
-	// Publish receiveRequest event
-	if (Users.isLocalUser(followee)) {
-		Users.pack(follower.id, followee).then(packed => publishMainStream(followee.id, 'receiveFollowRequest', packed));
-
-		Users.pack(followee.id, followee, {
-			detail: true
-		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
-
-		// 通知を作成
-		createNotification(followee.id, 'receiveFollowRequest', {
-			notifierId: follower.id,
-			followRequestId: followRequest.id
-		});
-	}
-
-	if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
-		const content = renderActivity(renderFollow(follower, followee));
-		deliver(follower, content, followee.inbox);
-	}
-}
diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts
deleted file mode 100644
index 41cebd9e41..0000000000
--- a/src/services/following/requests/reject.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderFollow from '@/remote/activitypub/renderer/follow';
-import renderReject from '@/remote/activitypub/renderer/reject';
-import { deliver } from '@/queue/index';
-import { publishMainStream, publishUserEvent } from '@/services/stream';
-import { User, ILocalUser } from '@/models/entities/user';
-import { Users, FollowRequests, Followings } from '@/models/index';
-import { decrementFollowing } from '../delete';
-
-export default async function(followee: { id: User['id']; host: User['host']; uri: User['host'] }, follower: User) {
-	if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
-		const request = await FollowRequests.findOne({
-			followeeId: followee.id,
-			followerId: follower.id
-		});
-
-		const content = renderActivity(renderReject(renderFollow(follower, followee, request!.requestId!), followee));
-		deliver(followee, content, follower.inbox);
-	}
-
-	const request = await FollowRequests.findOne({
-		followeeId: followee.id,
-		followerId: follower.id
-	});
-
-	if (request) {
-		await FollowRequests.delete(request.id);
-	} else {
-		const following = await Followings.findOne({
-			followeeId: followee.id,
-			followerId: follower.id
-		});
-
-		if (following) {
-			await Followings.delete(following.id);
-			decrementFollowing(follower, followee);
-		}
-	}
-
-	Users.pack(followee.id, follower, {
-		detail: true
-	}).then(packed => {
-		publishUserEvent(follower.id, 'unfollow', packed);
-		publishMainStream(follower.id, 'unfollow', packed);
-	});
-}
diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts
deleted file mode 100644
index b31beb6e1a..0000000000
--- a/src/services/i/pin.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import config from '@/config/index';
-import renderAdd from '@/remote/activitypub/renderer/add';
-import renderRemove from '@/remote/activitypub/renderer/remove';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import { IdentifiableError } from '@/misc/identifiable-error';
-import { User } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { Notes, UserNotePinings, Users } from '@/models/index';
-import { UserNotePining } from '@/models/entities/user-note-pining';
-import { genId } from '@/misc/gen-id';
-import { deliverToFollowers } from '@/remote/activitypub/deliver-manager';
-import { deliverToRelays } from '../relay';
-
-/**
- * 指定した投稿をピン留めします
- * @param user
- * @param noteId
- */
-export async function addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) {
-	// Fetch pinee
-	const note = await Notes.findOne({
-		id: noteId,
-		userId: user.id
-	});
-
-	if (note == null) {
-		throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.');
-	}
-
-	const pinings = await UserNotePinings.find({ userId: user.id });
-
-	if (pinings.length >= 5) {
-		throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
-	}
-
-	if (pinings.some(pining => pining.noteId === note.id)) {
-		throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.');
-	}
-
-	await UserNotePinings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: user.id,
-		noteId: note.id
-	} as UserNotePining);
-
-	// Deliver to remote followers
-	if (Users.isLocalUser(user)) {
-		deliverPinnedChange(user.id, note.id, true);
-	}
-}
-
-/**
- * 指定した投稿のピン留めを解除します
- * @param user
- * @param noteId
- */
-export async function removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']) {
-	// Fetch unpinee
-	const note = await Notes.findOne({
-		id: noteId,
-		userId: user.id
-	});
-
-	if (note == null) {
-		throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.');
-	}
-
-	UserNotePinings.delete({
-		userId: user.id,
-		noteId: note.id
-	});
-
-	// Deliver to remote followers
-	if (Users.isLocalUser(user)) {
-		deliverPinnedChange(user.id, noteId, false);
-	}
-}
-
-export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) {
-	const user = await Users.findOne(userId);
-	if (user == null) throw new Error('user not found');
-
-	if (!Users.isLocalUser(user)) return;
-
-	const target = `${config.url}/users/${user.id}/collections/featured`;
-	const item = `${config.url}/notes/${noteId}`;
-	const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
-
-	deliverToFollowers(user, content);
-	deliverToRelays(user, content);
-}
diff --git a/src/services/i/update.ts b/src/services/i/update.ts
deleted file mode 100644
index f700d9b48b..0000000000
--- a/src/services/i/update.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import renderUpdate from '@/remote/activitypub/renderer/update';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import { Users } from '@/models/index';
-import { User } from '@/models/entities/user';
-import { renderPerson } from '@/remote/activitypub/renderer/person';
-import { deliverToFollowers } from '@/remote/activitypub/deliver-manager';
-import { deliverToRelays } from '../relay';
-
-export async function publishToFollowers(userId: User['id']) {
-	const user = await Users.findOne(userId);
-	if (user == null) throw new Error('user not found');
-
-	// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
-	if (Users.isLocalUser(user)) {
-		const content = renderActivity(renderUpdate(await renderPerson(user), user));
-		deliverToFollowers(user, content);
-		deliverToRelays(user, content);
-	}
-}
diff --git a/src/services/insert-moderation-log.ts b/src/services/insert-moderation-log.ts
deleted file mode 100644
index 00397652ee..0000000000
--- a/src/services/insert-moderation-log.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { ModerationLogs } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { User } from '@/models/entities/user';
-
-export async function insertModerationLog(moderator: { id: User['id'] }, type: string, info?: Record<string, any>) {
-	await ModerationLogs.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: moderator.id,
-		type: type,
-		info: info || {}
-	});
-}
diff --git a/src/services/instance-actor.ts b/src/services/instance-actor.ts
deleted file mode 100644
index b3625226c3..0000000000
--- a/src/services/instance-actor.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { createSystemUser } from './create-system-user';
-import { ILocalUser } from '@/models/entities/user';
-import { Users } from '@/models/index';
-import { Cache } from '@/misc/cache';
-
-const ACTOR_USERNAME = 'instance.actor' as const;
-
-const cache = new Cache<ILocalUser>(Infinity);
-
-export async function getInstanceActor(): Promise<ILocalUser> {
-	const cached = cache.get(null);
-	if (cached) return cached;
-
-	const user = await Users.findOne({
-		host: null,
-		username: ACTOR_USERNAME
-	}) as ILocalUser | undefined;
-
-	if (user) {
-		cache.set(null, user);
-		return user;
-	} else {
-		const created = await createSystemUser(ACTOR_USERNAME) as ILocalUser;
-		cache.set(null, created);
-		return created;
-	}
-}
diff --git a/src/services/logger.ts b/src/services/logger.ts
deleted file mode 100644
index 67ee441254..0000000000
--- a/src/services/logger.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import * as cluster from 'cluster';
-import * as chalk from 'chalk';
-import * as dateformat from 'dateformat';
-import { envOption } from '../env';
-import config from '@/config/index';
-
-import * as SyslogPro from 'syslog-pro';
-
-type Domain = {
-	name: string;
-	color?: string;
-};
-
-type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
-
-export default class Logger {
-	private domain: Domain;
-	private parentLogger: Logger | null = null;
-	private store: boolean;
-	private syslogClient: any | null = null;
-
-	constructor(domain: string, color?: string, store = true) {
-		this.domain = {
-			name: domain,
-			color: color,
-		};
-		this.store = store;
-
-		if (config.syslog) {
-			this.syslogClient = new SyslogPro.RFC5424({
-				applacationName: 'Misskey',
-				timestamp: true,
-				encludeStructuredData: true,
-				color: true,
-				extendedColor: true,
-				server: {
-					target: config.syslog.host,
-					port: config.syslog.port,
-				}
-			});
-		}
-	}
-
-	public createSubLogger(domain: string, color?: string, store = true): Logger {
-		const logger = new Logger(domain, color, store);
-		logger.parentLogger = this;
-		return logger;
-	}
-
-	private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subDomains: Domain[] = [], store = true): void {
-		if (envOption.quiet) return;
-		if (!this.store) store = false;
-		if (level === 'debug') store = false;
-
-		if (this.parentLogger) {
-			this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store);
-			return;
-		}
-
-		const time = dateformat(new Date(), 'HH:MM:ss');
-		const worker = cluster.isMaster ? '*' : cluster.worker.id;
-		const l =
-			level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
-			level === 'warning' ? chalk.yellow('WARN') :
-			level === 'success' ? important ? chalk.bgGreen.white('DONE') : chalk.green('DONE') :
-			level === 'debug' ? chalk.gray('VERB') :
-			level === 'info' ? chalk.blue('INFO') :
-			null;
-		const domains = [this.domain].concat(subDomains).map(d => d.color ? chalk.keyword(d.color)(d.name) : chalk.white(d.name));
-		const m =
-			level === 'error' ? chalk.red(message) :
-			level === 'warning' ? chalk.yellow(message) :
-			level === 'success' ? chalk.green(message) :
-			level === 'debug' ? chalk.gray(message) :
-			level === 'info' ? message :
-			null;
-
-		let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`;
-		if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
-
-		console.log(important ? chalk.bold(log) : log);
-
-		if (store) {
-			if (this.syslogClient) {
-				const send =
-					level === 'error' ? this.syslogClient.error :
-					level === 'warning' ? this.syslogClient.warning :
-					level === 'success' ? this.syslogClient.info :
-					level === 'debug' ? this.syslogClient.info :
-					level === 'info' ? this.syslogClient.info :
-					null as never;
-
-				send.bind(this.syslogClient)(message).catch(() => {});
-			}
-		}
-	}
-
-	public error(x: string | Error, data?: Record<string, any> | null, important = false): void { // 実行を継続できない状況で使う
-		if (x instanceof Error) {
-			data = data || {};
-			data.e = x;
-			this.log('error', x.toString(), data, important);
-		} else if (typeof x === 'object') {
-			this.log('error', `${(x as any).message || (x as any).name || x}`, data, important);
-		} else {
-			this.log('error', `${x}`, data, important);
-		}
-	}
-
-	public warn(message: string, data?: Record<string, any> | null, important = false): void { // 実行を継続できるが改善すべき状況で使う
-		this.log('warning', message, data, important);
-	}
-
-	public succ(message: string, data?: Record<string, any> | null, important = false): void { // 何かに成功した状況で使う
-		this.log('success', message, data, important);
-	}
-
-	public debug(message: string, data?: Record<string, any> | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
-		if (process.env.NODE_ENV != 'production' || envOption.verbose) {
-			this.log('debug', message, data, important);
-		}
-	}
-
-	public info(message: string, data?: Record<string, any> | null, important = false): void { // それ以外
-		this.log('info', message, data, important);
-	}
-}
diff --git a/src/services/messages/create.ts b/src/services/messages/create.ts
deleted file mode 100644
index 948b6726b9..0000000000
--- a/src/services/messages/create.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { User } from '@/models/entities/user';
-import { UserGroup } from '@/models/entities/user-group';
-import { DriveFile } from '@/models/entities/drive-file';
-import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream';
-import pushNotification from '../push-notification';
-import { Not } from 'typeorm';
-import { Note } from '@/models/entities/note';
-import renderNote from '@/remote/activitypub/renderer/note';
-import renderCreate from '@/remote/activitypub/renderer/create';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import { deliver } from '@/queue/index';
-
-export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | undefined, file: DriveFile | null, uri?: string) {
-	const message = {
-		id: genId(),
-		createdAt: new Date(),
-		fileId: file ? file.id : null,
-		recipientId: recipientUser ? recipientUser.id : null,
-		groupId: recipientGroup ? recipientGroup.id : null,
-		text: text ? text.trim() : null,
-		userId: user.id,
-		isRead: false,
-		reads: [] as any[],
-		uri
-	} as MessagingMessage;
-
-	await MessagingMessages.insert(message);
-
-	const messageObj = await MessagingMessages.pack(message);
-
-	if (recipientUser) {
-		if (Users.isLocalUser(user)) {
-			// 自分のストリーム
-			publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj);
-			publishMessagingIndexStream(message.userId, 'message', messageObj);
-			publishMainStream(message.userId, 'messagingMessage', messageObj);
-		}
-
-		if (Users.isLocalUser(recipientUser)) {
-			// 相手のストリーム
-			publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj);
-			publishMessagingIndexStream(recipientUser.id, 'message', messageObj);
-			publishMainStream(recipientUser.id, 'messagingMessage', messageObj);
-		}
-	} else if (recipientGroup) {
-		// グループのストリーム
-		publishGroupMessagingStream(recipientGroup.id, 'message', messageObj);
-
-		// メンバーのストリーム
-		const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id });
-		for (const joining of joinings) {
-			publishMessagingIndexStream(joining.userId, 'message', messageObj);
-			publishMainStream(joining.userId, 'messagingMessage', messageObj);
-		}
-	}
-
-	// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
-	setTimeout(async () => {
-		const freshMessage = await MessagingMessages.findOne(message.id);
-		if (freshMessage == null) return; // メッセージが削除されている場合もある
-
-		if (recipientUser && Users.isLocalUser(recipientUser)) {
-			if (freshMessage.isRead) return; // 既読
-
-			//#region ただしミュートされているなら発行しない
-			const mute = await Mutings.find({
-				muterId: recipientUser.id,
-			});
-			if (mute.map(m => m.muteeId).includes(user.id)) return;
-			//#endregion
-
-			publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj);
-			pushNotification(recipientUser.id, 'unreadMessagingMessage', messageObj);
-		} else if (recipientGroup) {
-			const joinings = await UserGroupJoinings.find({ userGroupId: recipientGroup.id, userId: Not(user.id) });
-			for (const joining of joinings) {
-				if (freshMessage.reads.includes(joining.userId)) return; // 既読
-				publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj);
-				pushNotification(joining.userId, 'unreadMessagingMessage', messageObj);
-			}
-		}
-	}, 2000);
-
-	if (recipientUser && Users.isLocalUser(user) && Users.isRemoteUser(recipientUser)) {
-		const note = {
-			id: message.id,
-			createdAt: message.createdAt,
-			fileIds: message.fileId ? [ message.fileId ] : [],
-			text: message.text,
-			userId: message.userId,
-			visibility: 'specified',
-			mentions: [ recipientUser ].map(u => u.id),
-			mentionedRemoteUsers: JSON.stringify([ recipientUser ].map(u => ({
-				uri: u.uri,
-				username: u.username,
-				host: u.host
-			}))),
-		} as Note;
-
-		const activity = renderActivity(renderCreate(await renderNote(note, false, true), note));
-
-		deliver(user, activity, recipientUser.inbox);
-	}
-	return messageObj;
-}
diff --git a/src/services/messages/delete.ts b/src/services/messages/delete.ts
deleted file mode 100644
index 5c299c9a50..0000000000
--- a/src/services/messages/delete.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import config from '@/config/index';
-import { MessagingMessages, Users } from '@/models/index';
-import { MessagingMessage } from '@/models/entities/messaging-message';
-import { publishGroupMessagingStream, publishMessagingStream } from '@/services/stream';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderDelete from '@/remote/activitypub/renderer/delete';
-import renderTombstone from '@/remote/activitypub/renderer/tombstone';
-import { deliver } from '@/queue/index';
-
-export async function deleteMessage(message: MessagingMessage) {
-	await MessagingMessages.delete(message.id);
-	postDeleteMessage(message);
-}
-
-async function postDeleteMessage(message: MessagingMessage) {
-	if (message.recipientId) {
-		const user = await Users.findOneOrFail(message.userId);
-		const recipient = await Users.findOneOrFail(message.recipientId);
-
-		if (Users.isLocalUser(user)) publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id);
-		if (Users.isLocalUser(recipient)) publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
-
-		if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) {
-			const activity = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${message.id}`), user));
-			deliver(user, activity, recipient.inbox);
-		}
-	} else if (message.groupId) {
-		publishGroupMessagingStream(message.groupId, 'deleted', message.id);
-	}
-}
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
deleted file mode 100644
index 98819c69a4..0000000000
--- a/src/services/note/create.ts
+++ /dev/null
@@ -1,646 +0,0 @@
-import * as mfm from 'mfm-js';
-import es from '../../db/elasticsearch';
-import { publishMainStream, publishNotesStream } from '@/services/stream';
-import DeliverManager from '@/remote/activitypub/deliver-manager';
-import renderNote from '@/remote/activitypub/renderer/note';
-import renderCreate from '@/remote/activitypub/renderer/create';
-import renderAnnounce from '@/remote/activitypub/renderer/announce';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import { resolveUser } from '@/remote/resolve-user';
-import config from '@/config/index';
-import { updateHashtags } from '../update-hashtag';
-import { concat } from '@/prelude/array';
-import { insertNoteUnread } from '@/services/note/unread';
-import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import { extractMentions } from '@/misc/extract-mentions';
-import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm';
-import { extractHashtags } from '@/misc/extract-hashtags';
-import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
-import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings, NoteThreadMutings } from '@/models/index';
-import { DriveFile } from '@/models/entities/drive-file';
-import { App } from '@/models/entities/app';
-import { Not, getConnection, In } from 'typeorm';
-import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
-import { genId } from '@/misc/gen-id';
-import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '@/services/chart/index';
-import { Poll, IPoll } from '@/models/entities/poll';
-import { createNotification } from '../create-notification';
-import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
-import { checkHitAntenna } from '@/misc/check-hit-antenna';
-import { checkWordMute } from '@/misc/check-word-mute';
-import { addNoteToAntenna } from '../add-note-to-antenna';
-import { countSameRenotes } from '@/misc/count-same-renotes';
-import { deliverToRelays } from '../relay';
-import { Channel } from '@/models/entities/channel';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-import { getAntennas } from '@/misc/antenna-cache';
-import { fnNameList } from '@/mfm/fn-name-list';
-
-type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
-
-class NotificationManager {
-	private notifier: { id: User['id']; };
-	private note: Note;
-	private queue: {
-		target: ILocalUser['id'];
-		reason: NotificationType;
-	}[];
-
-	constructor(notifier: { id: User['id']; }, note: Note) {
-		this.notifier = notifier;
-		this.note = note;
-		this.queue = [];
-	}
-
-	public push(notifiee: ILocalUser['id'], reason: NotificationType) {
-		// 自分自身へは通知しない
-		if (this.notifier.id === notifiee) return;
-
-		const exist = this.queue.find(x => x.target === notifiee);
-
-		if (exist) {
-			// 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
-			if (reason != 'mention') {
-				exist.reason = reason;
-			}
-		} else {
-			this.queue.push({
-				reason: reason,
-				target: notifiee
-			});
-		}
-	}
-
-	public async deliver() {
-		for (const x of this.queue) {
-			// ミュート情報を取得
-			const mentioneeMutes = await Mutings.find({
-				muterId: x.target
-			});
-
-			const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId);
-
-			// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
-			if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
-				createNotification(x.target, x.reason, {
-					notifierId: this.notifier.id,
-					noteId: this.note.id
-				});
-			}
-		}
-	}
-}
-
-type Option = {
-	createdAt?: Date | null;
-	name?: string | null;
-	text?: string | null;
-	reply?: Note | null;
-	renote?: Note | null;
-	files?: DriveFile[] | null;
-	poll?: IPoll | null;
-	viaMobile?: boolean | null;
-	localOnly?: boolean | null;
-	cw?: string | null;
-	visibility?: string;
-	visibleUsers?: User[] | null;
-	channel?: Channel | null;
-	apMentions?: User[] | null;
-	apHashtags?: string[] | null;
-	apEmojis?: string[] | null;
-	uri?: string | null;
-	url?: string | null;
-	app?: App | null;
-};
-
-export default async (user: { id: User['id']; username: User['username']; host: User['host']; isSilenced: User['isSilenced']; }, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
-	// チャンネル外にリプライしたら対象のスコープに合わせる
-	// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
-	if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
-		if (data.reply.channelId) {
-			data.channel = await Channels.findOne(data.reply.channelId);
-		} else {
-			data.channel = null;
-		}
-	}
-
-	// チャンネル内にリプライしたら対象のスコープに合わせる
-	// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
-	if (data.reply && (data.channel == null) && data.reply.channelId) {
-		data.channel = await Channels.findOne(data.reply.channelId);
-	}
-
-	if (data.createdAt == null) data.createdAt = new Date();
-	if (data.visibility == null) data.visibility = 'public';
-	if (data.viaMobile == null) data.viaMobile = false;
-	if (data.localOnly == null) data.localOnly = false;
-	if (data.channel != null) data.visibility = 'public';
-	if (data.channel != null) data.visibleUsers = [];
-	if (data.channel != null) data.localOnly = true;
-
-	// サイレンス
-	if (user.isSilenced && data.visibility === 'public' && data.channel == null) {
-		data.visibility = 'home';
-	}
-
-	// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
-	if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) {
-		return rej('Renote target is not public or home');
-	}
-
-	// Renote対象がpublicではないならhomeにする
-	if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
-		data.visibility = 'home';
-	}
-
-	// Renote対象がfollowersならfollowersにする
-	if (data.renote && data.renote.visibility === 'followers') {
-		data.visibility = 'followers';
-	}
-
-	// 返信対象がpublicではないならhomeにする
-	if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
-		data.visibility = 'home';
-	}
-
-	// ローカルのみをRenoteしたらローカルのみにする
-	if (data.renote && data.renote.localOnly && data.channel == null) {
-		data.localOnly = true;
-	}
-
-	// ローカルのみにリプライしたらローカルのみにする
-	if (data.reply && data.reply.localOnly && data.channel == null) {
-		data.localOnly = true;
-	}
-
-	if (data.text) {
-		data.text = data.text.trim();
-	}
-
-	let tags = data.apHashtags;
-	let emojis = data.apEmojis;
-	let mentionedUsers = data.apMentions;
-
-	// Parse MFM if needed
-	if (!tags || !emojis || !mentionedUsers) {
-		const tokens = data.text ? mfm.parse(data.text, { fnNameList })! : [];
-		const cwTokens = data.cw ? mfm.parse(data.cw, { fnNameList })! : [];
-		const choiceTokens = data.poll && data.poll.choices
-			? concat(data.poll.choices.map(choice => mfm.parse(choice, { fnNameList })!))
-			: [];
-
-		const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
-
-		tags = data.apHashtags || extractHashtags(combinedTokens);
-
-		emojis = data.apEmojis || extractCustomEmojisFromMfm(combinedTokens);
-
-		mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
-	}
-
-	tags = tags.filter(tag => Array.from(tag || '').length <= 128).splice(0, 32);
-
-	if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
-		mentionedUsers.push(await Users.findOneOrFail(data.reply.userId));
-	}
-
-	if (data.visibility == 'specified') {
-		if (data.visibleUsers == null) throw new Error('invalid param');
-
-		for (const u of data.visibleUsers) {
-			if (!mentionedUsers.some(x => x.id === u.id)) {
-				mentionedUsers.push(u);
-			}
-		}
-
-		if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) {
-			data.visibleUsers.push(await Users.findOneOrFail(data.reply.userId));
-		}
-	}
-
-	const note = await insertNote(user, data, tags, emojis, mentionedUsers);
-
-	res(note);
-
-	// 統計を更新
-	notesChart.update(note, true);
-	perUserNotesChart.update(user, note, true);
-
-	// Register host
-	if (Users.isRemoteUser(user)) {
-		registerOrFetchInstanceDoc(user.host).then(i => {
-			Instances.increment({ id: i.id }, 'notesCount', 1);
-			instanceChart.updateNote(i.host, note, true);
-		});
-	}
-
-	// ハッシュタグ更新
-	if (data.visibility === 'public' || data.visibility === 'home') {
-		updateHashtags(user, tags);
-	}
-
-	// Increment notes count (user)
-	incNotesCountOfUser(user);
-
-	// Word mute
-	// TODO: cache
-	UserProfiles.find({
-		enableWordMute: true
-	}).then(us => {
-		for (const u of us) {
-			checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
-				if (shouldMute) {
-					MutedNotes.insert({
-						id: genId(),
-						userId: u.userId,
-						noteId: note.id,
-						reason: 'word',
-					});
-				}
-			});
-		}
-	});
-
-	// Antenna
-	Followings.createQueryBuilder('following')
-		.andWhere(`following.followeeId = :userId`, { userId: note.userId })
-		.getMany()
-		.then(async followings => {
-			const blockings = await Blockings.find({ blockerId: user.id }); // TODO: キャッシュしたい
-			const followers = followings.map(f => f.followerId);
-			for (const antenna of (await getAntennas())) {
-				if (blockings.some(blocking => blocking.blockeeId === antenna.userId)) continue; // この処理は checkHitAntenna 内でやるようにしてもいいかも
-				checkHitAntenna(antenna, note, user, followers).then(hit => {
-					if (hit) {
-						addNoteToAntenna(antenna, note, user);
-					}
-				});
-			}
-		});
-
-	// Channel
-	if (note.channelId) {
-		ChannelFollowings.find({ followeeId: note.channelId }).then(followings => {
-			for (const following of followings) {
-				insertNoteUnread(following.followerId, note, {
-					isSpecified: false,
-					isMentioned: false,
-				});
-			}
-		});
-	}
-
-	if (data.reply) {
-		saveReply(data.reply, note);
-	}
-
-	// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
-	if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
-		incRenoteCount(data.renote);
-	}
-
-	if (!silent) {
-		// ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい
-		if (Users.isRemoteUser(user)) activeUsersChart.update(user);
-
-		// 未読通知を作成
-		if (data.visibility == 'specified') {
-			if (data.visibleUsers == null) throw new Error('invalid param');
-
-			for (const u of data.visibleUsers) {
-				// ローカルユーザーのみ
-				if (!Users.isLocalUser(u)) continue;
-
-				insertNoteUnread(u.id, note, {
-					isSpecified: true,
-					isMentioned: false,
-				});
-			}
-		} else {
-			for (const u of mentionedUsers) {
-				// ローカルユーザーのみ
-				if (!Users.isLocalUser(u)) continue;
-
-				insertNoteUnread(u.id, note, {
-					isSpecified: false,
-					isMentioned: true,
-				});
-			}
-		}
-
-		// Pack the note
-		const noteObj = await Notes.pack(note);
-
-		publishNotesStream(noteObj);
-
-		const nm = new NotificationManager(user, note);
-		const nmRelatedPromises = [];
-
-		await createMentionedEvents(mentionedUsers, note, nm);
-
-		// If has in reply to note
-		if (data.reply) {
-			// Fetch watchers
-			nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
-
-			// 通知
-			if (data.reply.userHost === null) {
-				const threadMuted = await NoteThreadMutings.findOne({
-					userId: data.reply.userId,
-					threadId: data.reply.threadId || data.reply.id,
-				});
-
-				if (!threadMuted) {
-					nm.push(data.reply.userId, 'reply');
-					publishMainStream(data.reply.userId, 'reply', noteObj);
-				}
-			}
-		}
-
-		// If it is renote
-		if (data.renote) {
-			const type = data.text ? 'quote' : 'renote';
-
-			// Notify
-			if (data.renote.userHost === null) {
-				nm.push(data.renote.userId, type);
-			}
-
-			// Fetch watchers
-			nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type));
-
-			// Publish event
-			if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
-				publishMainStream(data.renote.userId, 'renote', noteObj);
-			}
-		}
-
-		Promise.all(nmRelatedPromises).then(() => {
-			nm.deliver();
-		});
-
-		//#region AP deliver
-		if (Users.isLocalUser(user)) {
-			(async () => {
-				const noteActivity = await renderNoteOrRenoteActivity(data, note);
-				const dm = new DeliverManager(user, noteActivity);
-
-				// メンションされたリモートユーザーに配送
-				for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) {
-					dm.addDirectRecipe(u as IRemoteUser);
-				}
-
-				// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
-				if (data.reply && data.reply.userHost !== null) {
-					const u = await Users.findOne(data.reply.userId);
-					if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
-				}
-
-				// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
-				if (data.renote && data.renote.userHost !== null) {
-					const u = await Users.findOne(data.renote.userId);
-					if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
-				}
-
-				// フォロワーに配送
-				if (['public', 'home', 'followers'].includes(note.visibility)) {
-					dm.addFollowersRecipe();
-				}
-
-				if (['public'].includes(note.visibility)) {
-					deliverToRelays(user, noteActivity);
-				}
-
-				dm.execute();
-			})();
-		}
-		//#endregion
-	}
-
-	if (data.channel) {
-		Channels.increment({ id: data.channel.id }, 'notesCount', 1);
-		Channels.update(data.channel.id, {
-			lastNotedAt: new Date(),
-		});
-
-		Notes.count({
-			userId: user.id,
-			channelId: data.channel.id,
-		}).then(count => {
-			// この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる
-			// TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい
-			if (count === 1) {
-				Channels.increment({ id: data.channel!.id }, 'usersCount', 1);
-			}
-		});
-	}
-
-	// Register to search database
-	index(note);
-});
-
-async function renderNoteOrRenoteActivity(data: Option, note: Note) {
-	if (data.localOnly) return null;
-
-	const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
-		? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note)
-		: renderCreate(await renderNote(note, false), note);
-
-	return renderActivity(content);
-}
-
-function incRenoteCount(renote: Note) {
-	Notes.createQueryBuilder().update()
-		.set({
-			renoteCount: () => '"renoteCount" + 1',
-			score: () => '"score" + 1'
-		})
-		.where('id = :id', { id: renote.id })
-		.execute();
-}
-
-async function insertNote(user: { id: User['id']; host: User['host']; }, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) {
-	const insert = new Note({
-		id: genId(data.createdAt!),
-		createdAt: data.createdAt!,
-		fileIds: data.files ? data.files.map(file => file.id) : [],
-		replyId: data.reply ? data.reply.id : null,
-		renoteId: data.renote ? data.renote.id : null,
-		channelId: data.channel ? data.channel.id : null,
-		threadId: data.reply
-			? data.reply.threadId
-				? data.reply.threadId
-				: data.reply.id
-			: null,
-		name: data.name,
-		text: data.text,
-		hasPoll: data.poll != null,
-		cw: data.cw == null ? null : data.cw,
-		tags: tags.map(tag => normalizeForSearch(tag)),
-		emojis,
-		userId: user.id,
-		viaMobile: data.viaMobile!,
-		localOnly: data.localOnly!,
-		visibility: data.visibility as any,
-		visibleUserIds: data.visibility == 'specified'
-			? data.visibleUsers
-				? data.visibleUsers.map(u => u.id)
-				: []
-			: [],
-
-		attachedFileTypes: data.files ? data.files.map(file => file.type) : [],
-
-		// 以下非正規化データ
-		replyUserId: data.reply ? data.reply.userId : null,
-		replyUserHost: data.reply ? data.reply.userHost : null,
-		renoteUserId: data.renote ? data.renote.userId : null,
-		renoteUserHost: data.renote ? data.renote.userHost : null,
-		userHost: user.host,
-	});
-
-	if (data.uri != null) insert.uri = data.uri;
-	if (data.url != null) insert.url = data.url;
-
-	// Append mentions data
-	if (mentionedUsers.length > 0) {
-		insert.mentions = mentionedUsers.map(u => u.id);
-		const profiles = await UserProfiles.find({ userId: In(insert.mentions) });
-		insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => {
-			const profile = profiles.find(p => p.userId == u.id);
-			const url = profile != null ? profile.url : null;
-			return {
-				uri: u.uri,
-				url: url == null ? undefined : url,
-				username: u.username,
-				host: u.host
-			} as IMentionedRemoteUsers[0];
-		}));
-	}
-
-	// 投稿を作成
-	try {
-		if (insert.hasPoll) {
-			// Start transaction
-			await getConnection().transaction(async transactionalEntityManager => {
-				await transactionalEntityManager.insert(Note, insert);
-
-				const poll = new Poll({
-					noteId: insert.id,
-					choices: data.poll!.choices,
-					expiresAt: data.poll!.expiresAt,
-					multiple: data.poll!.multiple,
-					votes: new Array(data.poll!.choices.length).fill(0),
-					noteVisibility: insert.visibility,
-					userId: user.id,
-					userHost: user.host
-				});
-
-				await transactionalEntityManager.insert(Poll, poll);
-			});
-		} else {
-			await Notes.insert(insert);
-		}
-
-		return insert;
-	} catch (e) {
-		// duplicate key error
-		if (isDuplicateKeyValueError(e)) {
-			const err = new Error('Duplicated note');
-			err.name = 'duplicated';
-			throw err;
-		}
-
-		console.error(e);
-
-		throw e;
-	}
-}
-
-function index(note: Note) {
-	if (note.text == null || config.elasticsearch == null) return;
-
-	es!.index({
-		index: config.elasticsearch.index || 'misskey_note',
-		id: note.id.toString(),
-		body: {
-			text: normalizeForSearch(note.text),
-			userId: note.userId,
-			userHost: note.userHost
-		}
-	});
-}
-
-async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType) {
-	const watchers = await NoteWatchings.find({
-		noteId: renote.id,
-		userId: Not(user.id)
-	});
-
-	for (const watcher of watchers) {
-		nm.push(watcher.userId, type);
-	}
-}
-
-async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, nm: NotificationManager) {
-	const watchers = await NoteWatchings.find({
-		noteId: reply.id,
-		userId: Not(user.id)
-	});
-
-	for (const watcher of watchers) {
-		nm.push(watcher.userId, 'reply');
-	}
-}
-
-async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) {
-	for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) {
-		const threadMuted = await NoteThreadMutings.findOne({
-			userId: u.id,
-			threadId: note.threadId || note.id,
-		});
-
-		if (threadMuted) {
-			continue;
-		}
-
-		const detailPackedNote = await Notes.pack(note, u, {
-			detail: true
-		});
-
-		publishMainStream(u.id, 'mention', detailPackedNote);
-
-		// Create notification
-		nm.push(u.id, 'mention');
-	}
-}
-
-function saveReply(reply: Note, note: Note) {
-	Notes.increment({ id: reply.id }, 'repliesCount', 1);
-}
-
-function incNotesCountOfUser(user: { id: User['id']; }) {
-	Users.createQueryBuilder().update()
-		.set({
-			updatedAt: new Date(),
-			notesCount: () => '"notesCount" + 1'
-		})
-		.where('id = :id', { id: user.id })
-		.execute();
-}
-
-async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise<User[]> {
-	if (tokens == null) return [];
-
-	const mentions = extractMentions(tokens);
-
-	let mentionedUsers = (await Promise.all(mentions.map(m =>
-		resolveUser(m.username, m.host || user.host).catch(() => null)
-	))).filter(x => x != null) as User[];
-
-	// Drop duplicate users
-	mentionedUsers = mentionedUsers.filter((u, i, self) =>
-		i === self.findIndex(u2 => u.id === u2.id)
-	);
-
-	return mentionedUsers;
-}
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
deleted file mode 100644
index a14d84e7b2..0000000000
--- a/src/services/note/delete.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { publishNoteStream } from '@/services/stream';
-import renderDelete from '@/remote/activitypub/renderer/delete';
-import renderAnnounce from '@/remote/activitypub/renderer/announce';
-import renderUndo from '@/remote/activitypub/renderer/undo';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderTombstone from '@/remote/activitypub/renderer/tombstone';
-import config from '@/config/index';
-import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
-import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
-import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
-import { Notes, Users, Instances } from '@/models/index';
-import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index';
-import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager';
-import { countSameRenotes } from '@/misc/count-same-renotes';
-import { deliverToRelays } from '../relay';
-import { Brackets, In } from 'typeorm';
-
-/**
- * 投稿を削除します。
- * @param user 投稿者
- * @param note 投稿
- */
-export default async function(user: User, note: Note, quiet = false) {
-	const deletedAt = new Date();
-
-	// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
-	if (note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
-		Notes.decrement({ id: note.renoteId }, 'renoteCount', 1);
-		Notes.decrement({ id: note.renoteId }, 'score', 1);
-	}
-
-	if (!quiet) {
-		publishNoteStream(note.id, 'deleted', {
-			deletedAt: deletedAt
-		});
-
-		//#region ローカルの投稿なら削除アクティビティを配送
-		if (Users.isLocalUser(user) && !note.localOnly) {
-			let renote: Note | undefined;
-
-			// if deletd note is renote
-			if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) {
-				renote = await Notes.findOne({
-					id: note.renoteId
-				});
-			}
-
-			const content = renderActivity(renote
-				? renderUndo(renderAnnounce(renote.uri || `${config.url}/notes/${renote.id}`, note), user)
-				: renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user));
-
-			deliverToConcerned(user, note, content);
-		}
-
-		// also deliever delete activity to cascaded notes
-		const cascadingNotes = (await findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes
-		for (const cascadingNote of cascadingNotes) {
-			if (!cascadingNote.user) continue;
-			if (!Users.isLocalUser(cascadingNote.user)) continue;
-			const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
-			deliverToConcerned(cascadingNote.user, cascadingNote, content);
-		}
-		//#endregion
-
-		// 統計を更新
-		notesChart.update(note, false);
-		perUserNotesChart.update(user, note, false);
-
-		if (Users.isRemoteUser(user)) {
-			registerOrFetchInstanceDoc(user.host).then(i => {
-				Instances.decrement({ id: i.id }, 'notesCount', 1);
-				instanceChart.updateNote(i.host, note, false);
-			});
-		}
-	}
-
-	await Notes.delete({
-		id: note.id,
-		userId: user.id
-	});
-}
-
-async function findCascadingNotes(note: Note) {
-	const cascadingNotes: Note[] = [];
-
-	const recursive = async (noteId: string) => {
-		const query = Notes.createQueryBuilder('note')
-			.where('note.replyId = :noteId', { noteId })
-			.orWhere(new Brackets(q => {
-				q.where('note.renoteId = :noteId', { noteId })
-				.andWhere('note.text IS NOT NULL');
-			}))
-			.leftJoinAndSelect('note.user', 'user');
-		const replies = await query.getMany();
-		for (const reply of replies) {
-			cascadingNotes.push(reply);
-			await recursive(reply.id);
-		}
-	};
-	await recursive(note.id);
-
-	return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
-}
-
-async function getMentionedRemoteUsers(note: Note) {
-	const where = [] as any[];
-
-	// mention / reply / dm
-	const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
-	if (uris.length > 0) {
-		where.push(
-			{ uri: In(uris) }
-		);
-	}
-
-	// renote / quote
-	if (note.renoteUserId) {
-		where.push({
-			id: note.renoteUserId
-		});
-	}
-
-	if (where.length === 0) return [];
-
-	return await Users.find({
-		where
-	}) as IRemoteUser[];
-}
-
-async function deliverToConcerned(user: ILocalUser, note: Note, content: any) {
-	deliverToFollowers(user, content);
-	deliverToRelays(user, content);
-	const remoteUsers = await getMentionedRemoteUsers(note);
-	for (const remoteUser of remoteUsers) {
-		deliverToUser(user, content, remoteUser);
-	}
-}
diff --git a/src/services/note/polls/update.ts b/src/services/note/polls/update.ts
deleted file mode 100644
index a22ce8e373..0000000000
--- a/src/services/note/polls/update.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import renderUpdate from '@/remote/activitypub/renderer/update';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import renderNote from '@/remote/activitypub/renderer/note';
-import { Users, Notes } from '@/models/index';
-import { Note } from '@/models/entities/note';
-import { deliverToFollowers } from '@/remote/activitypub/deliver-manager';
-import { deliverToRelays } from '../../relay';
-
-export async function deliverQuestionUpdate(noteId: Note['id']) {
-	const note = await Notes.findOne(noteId);
-	if (note == null) throw new Error('note not found');
-
-	const user = await Users.findOne(note.userId);
-	if (user == null) throw new Error('note not found');
-
-	if (Users.isLocalUser(user)) {
-
-		const content = renderActivity(renderUpdate(await renderNote(note, false), user));
-		deliverToFollowers(user, content);
-		deliverToRelays(user, content);
-	}
-}
diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts
deleted file mode 100644
index 886a09dde9..0000000000
--- a/src/services/note/polls/vote.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { publishNoteStream } from '@/services/stream';
-import { User } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { PollVotes, NoteWatchings, Polls, Blockings } from '@/models/index';
-import { Not } from 'typeorm';
-import { genId } from '@/misc/gen-id';
-import { createNotification } from '../../create-notification';
-
-export default async function(user: User, note: Note, choice: number) {
-	const poll = await Polls.findOne(note.id);
-
-	if (poll == null) throw new Error('poll not found');
-
-	// Check whether is valid choice
-	if (poll.choices[choice] == null) throw new Error('invalid choice param');
-
-	// Check blocking
-	if (note.userId !== user.id) {
-		const block = await Blockings.findOne({
-			blockerId: note.userId,
-			blockeeId: user.id,
-		});
-		if (block) {
-			throw new Error('blocked');
-		}
-	}
-
-	// if already voted
-	const exist = await PollVotes.find({
-		noteId: note.id,
-		userId: user.id
-	});
-
-	if (poll.multiple) {
-		if (exist.some(x => x.choice === choice)) {
-			throw new Error('already voted');
-		}
-	} else if (exist.length !== 0) {
-		throw new Error('already voted');
-	}
-
-	// Create vote
-	await PollVotes.insert({
-		id: genId(),
-		createdAt: new Date(),
-		noteId: note.id,
-		userId: user.id,
-		choice: choice
-	});
-
-	// Increment votes count
-	const index = choice + 1; // In SQL, array index is 1 based
-	await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
-
-	publishNoteStream(note.id, 'pollVoted', {
-		choice: choice,
-		userId: user.id
-	});
-
-	// Notify
-	createNotification(note.userId, 'pollVote', {
-		notifierId: user.id,
-		noteId: note.id,
-		choice: choice
-	});
-
-	// Fetch watchers
-	NoteWatchings.find({
-		noteId: note.id,
-		userId: Not(user.id),
-	})
-	.then(watchers => {
-		for (const watcher of watchers) {
-			createNotification(watcher.userId, 'pollVote', {
-				notifierId: user.id,
-				noteId: note.id,
-				choice: choice
-			});
-		}
-	});
-}
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
deleted file mode 100644
index 308bd4dff7..0000000000
--- a/src/services/note/reaction/create.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import { publishNoteStream } from '@/services/stream';
-import { renderLike } from '@/remote/activitypub/renderer/like';
-import DeliverManager from '@/remote/activitypub/deliver-manager';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import { toDbReaction, decodeReaction } from '@/misc/reaction-lib';
-import { User, IRemoteUser } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { NoteReactions, Users, NoteWatchings, Notes, Emojis, Blockings } from '@/models/index';
-import { Not } from 'typeorm';
-import { perUserReactionsChart } from '@/services/chart/index';
-import { genId } from '@/misc/gen-id';
-import { createNotification } from '../../create-notification';
-import deleteReaction from './delete';
-import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
-import { NoteReaction } from '@/models/entities/note-reaction';
-import { IdentifiableError } from '@/misc/identifiable-error';
-
-export default async (user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string) => {
-	// Check blocking
-	if (note.userId !== user.id) {
-		const block = await Blockings.findOne({
-			blockerId: note.userId,
-			blockeeId: user.id,
-		});
-		if (block) {
-			throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
-		}
-	}
-
-	// TODO: cache
-	reaction = await toDbReaction(reaction, user.host);
-
-	const record: NoteReaction = {
-		id: genId(),
-		createdAt: new Date(),
-		noteId: note.id,
-		userId: user.id,
-		reaction
-	};
-
-	// Create reaction
-	try {
-		await NoteReactions.insert(record);
-	} catch (e) {
-		if (isDuplicateKeyValueError(e)) {
-			const exists = await NoteReactions.findOneOrFail({
-				noteId: note.id,
-				userId: user.id,
-			});
-
-			if (exists.reaction !== reaction) {
-				// 別のリアクションがすでにされていたら置き換える
-				await deleteReaction(user, note);
-				await NoteReactions.insert(record);
-			} else {
-				// 同じリアクションがすでにされていたらエラー
-				throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
-			}
-		} else {
-			throw e;
-		}
-	}
-
-	// Increment reactions count
-	const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
-	await Notes.createQueryBuilder().update()
-		.set({
-			reactions: () => sql,
-			score: () => '"score" + 1'
-		})
-		.where('id = :id', { id: note.id })
-		.execute();
-
-	perUserReactionsChart.update(user, note);
-
-	// カスタム絵文字リアクションだったら絵文字情報も送る
-	const decodedReaction = decodeReaction(reaction);
-
-	let emoji = await Emojis.findOne({
-		where: {
-			name: decodedReaction.name,
-			host: decodedReaction.host
-		},
-		select: ['name', 'host', 'url']
-	});
-
-	if (emoji) {
-		emoji = {
-			name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
-			url: emoji.url
-		} as any;
-	}
-
-	publishNoteStream(note.id, 'reacted', {
-		reaction: decodedReaction.reaction,
-		emoji: emoji,
-		userId: user.id
-	});
-
-	// リアクションされたユーザーがローカルユーザーなら通知を作成
-	if (note.userHost === null) {
-		createNotification(note.userId, 'reaction', {
-			notifierId: user.id,
-			noteId: note.id,
-			reaction: reaction
-		});
-	}
-
-	// Fetch watchers
-	NoteWatchings.find({
-		noteId: note.id,
-		userId: Not(user.id)
-	}).then(watchers => {
-		for (const watcher of watchers) {
-			createNotification(watcher.userId, 'reaction', {
-				notifierId: user.id,
-				noteId: note.id,
-				reaction: reaction
-			});
-		}
-	});
-
-	//#region 配信
-	if (Users.isLocalUser(user) && !note.localOnly) {
-		const content = renderActivity(await renderLike(record, note));
-		const dm = new DeliverManager(user, content);
-		if (note.userHost !== null) {
-			const reactee = await Users.findOne(note.userId);
-			dm.addDirectRecipe(reactee as IRemoteUser);
-		}
-		dm.addFollowersRecipe();
-		dm.execute();
-	}
-	//#endregion
-};
diff --git a/src/services/note/reaction/delete.ts b/src/services/note/reaction/delete.ts
deleted file mode 100644
index 062dbad6f1..0000000000
--- a/src/services/note/reaction/delete.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { publishNoteStream } from '@/services/stream';
-import { renderLike } from '@/remote/activitypub/renderer/like';
-import renderUndo from '@/remote/activitypub/renderer/undo';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import DeliverManager from '@/remote/activitypub/deliver-manager';
-import { IdentifiableError } from '@/misc/identifiable-error';
-import { User, IRemoteUser } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { NoteReactions, Users, Notes } from '@/models/index';
-import { decodeReaction } from '@/misc/reaction-lib';
-
-export default async (user: { id: User['id']; host: User['host']; }, note: Note) => {
-	// if already unreacted
-	const exist = await NoteReactions.findOne({
-		noteId: note.id,
-		userId: user.id,
-	});
-
-	if (exist == null) {
-		throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
-	}
-
-	// Delete reaction
-	const result = await NoteReactions.delete(exist.id);
-
-	if (result.affected !== 1) {
-		throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
-	}
-
-	// Decrement reactions count
-	const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
-	await Notes.createQueryBuilder().update()
-		.set({
-			reactions: () => sql,
-		})
-		.where('id = :id', { id: note.id })
-		.execute();
-
-	Notes.decrement({ id: note.id }, 'score', 1);
-
-	publishNoteStream(note.id, 'unreacted', {
-		reaction: decodeReaction(exist.reaction).reaction,
-		userId: user.id
-	});
-
-	//#region 配信
-	if (Users.isLocalUser(user) && !note.localOnly) {
-		const content = renderActivity(renderUndo(await renderLike(exist, note), user));
-		const dm = new DeliverManager(user, content);
-		if (note.userHost !== null) {
-			const reactee = await Users.findOne(note.userId);
-			dm.addDirectRecipe(reactee as IRemoteUser);
-		}
-		dm.addFollowersRecipe();
-		dm.execute();
-	}
-	//#endregion
-};
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
deleted file mode 100644
index f25f86da9c..0000000000
--- a/src/services/note/read.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { publishMainStream } from '@/services/stream';
-import { Note } from '@/models/entities/note';
-import { User } from '@/models/entities/user';
-import { NoteUnreads, AntennaNotes, Users, Followings, ChannelFollowings } from '@/models/index';
-import { Not, IsNull, In } from 'typeorm';
-import { Channel } from '@/models/entities/channel';
-import { checkHitAntenna } from '@/misc/check-hit-antenna';
-import { getAntennas } from '@/misc/antenna-cache';
-import { readNotificationByQuery } from '@/server/api/common/read-notification';
-import { Packed } from '@/misc/schema';
-
-/**
- * Mark notes as read
- */
-export default async function(
-	userId: User['id'],
-	notes: (Note | Packed<'Note'>)[],
-	info?: {
-		following: Set<User['id']>;
-		followingChannels: Set<Channel['id']>;
-	}
-) {
-	const following = info?.following ? info.following : new Set<string>((await Followings.find({
-		where: {
-			followerId: userId
-		},
-		select: ['followeeId']
-	})).map(x => x.followeeId));
-	const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await ChannelFollowings.find({
-		where: {
-			followerId: userId
-		},
-		select: ['followeeId']
-	})).map(x => x.followeeId));
-
-	const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
-	const readMentions: (Note | Packed<'Note'>)[] = [];
-	const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
-	const readChannelNotes: (Note | Packed<'Note'>)[] = [];
-	const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
-
-	for (const note of notes) {
-		if (note.mentions && note.mentions.includes(userId)) {
-			readMentions.push(note);
-		} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
-			readSpecifiedNotes.push(note);
-		}
-
-		if (note.channelId && followingChannels.has(note.channelId)) {
-			readChannelNotes.push(note);
-		}
-
-		if (note.user != null) { // たぶんnullになることは無いはずだけど一応
-			for (const antenna of myAntennas) {
-				if (await checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) {
-					readAntennaNotes.push(note);
-				}
-			}
-		}
-	}
-
-	if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
-		// Remove the record
-		await NoteUnreads.delete({
-			userId: userId,
-			noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
-		});
-
-		// TODO: ↓まとめてクエリしたい
-
-		NoteUnreads.count({
-			userId: userId,
-			isMentioned: true
-		}).then(mentionsCount => {
-			if (mentionsCount === 0) {
-				// 全て既読になったイベントを発行
-				publishMainStream(userId, 'readAllUnreadMentions');
-			}
-		});
-
-		NoteUnreads.count({
-			userId: userId,
-			isSpecified: true
-		}).then(specifiedCount => {
-			if (specifiedCount === 0) {
-				// 全て既読になったイベントを発行
-				publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
-			}
-		});
-
-		NoteUnreads.count({
-			userId: userId,
-			noteChannelId: Not(IsNull())
-		}).then(channelNoteCount => {
-			if (channelNoteCount === 0) {
-				// 全て既読になったイベントを発行
-				publishMainStream(userId, 'readAllChannels');
-			}
-		});
-
-		readNotificationByQuery(userId, {
-			noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
-		});
-	}
-
-	if (readAntennaNotes.length > 0) {
-		await AntennaNotes.update({
-			antennaId: In(myAntennas.map(a => a.id)),
-			noteId: In(readAntennaNotes.map(n => n.id))
-		}, {
-			read: true
-		});
-
-		// TODO: まとめてクエリしたい
-		for (const antenna of myAntennas) {
-			const count = await AntennaNotes.count({
-				antennaId: antenna.id,
-				read: false
-			});
-
-			if (count === 0) {
-				publishMainStream(userId, 'readAntenna', antenna);
-			}
-		}
-
-		Users.getHasUnreadAntenna(userId).then(unread => {
-			if (!unread) {
-				publishMainStream(userId, 'readAllAntennas');
-			}
-		});
-	}
-}
diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts
deleted file mode 100644
index 29d2b54af8..0000000000
--- a/src/services/note/unread.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Note } from '@/models/entities/note';
-import { publishMainStream } from '@/services/stream';
-import { User } from '@/models/entities/user';
-import { Mutings, NoteThreadMutings, NoteUnreads } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-export async function insertNoteUnread(userId: User['id'], note: Note, params: {
-	// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
-	isSpecified: boolean;
-	isMentioned: boolean;
-}) {
-	//#region ミュートしているなら無視
-	// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
-	const mute = await Mutings.find({
-		muterId: userId
-	});
-	if (mute.map(m => m.muteeId).includes(note.userId)) return;
-	//#endregion
-
-	// スレッドミュート
-	const threadMute = await NoteThreadMutings.findOne({
-		userId: userId,
-		threadId: note.threadId || note.id,
-	});
-	if (threadMute) return;
-
-	const unread = {
-		id: genId(),
-		noteId: note.id,
-		userId: userId,
-		isSpecified: params.isSpecified,
-		isMentioned: params.isMentioned,
-		noteChannelId: note.channelId,
-		noteUserId: note.userId,
-	};
-
-	await NoteUnreads.insert(unread);
-
-	// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
-	setTimeout(async () => {
-		const exist = await NoteUnreads.findOne(unread.id);
-
-		if (exist == null) return;
-
-		if (params.isMentioned) {
-			publishMainStream(userId, 'unreadMention', note.id);
-		}
-		if (params.isSpecified) {
-			publishMainStream(userId, 'unreadSpecifiedNote', note.id);
-		}
-		if (note.channelId) {
-			publishMainStream(userId, 'unreadChannel', note.id);
-		}
-	}, 2000);
-}
diff --git a/src/services/note/unwatch.ts b/src/services/note/unwatch.ts
deleted file mode 100644
index 8ea02fe33c..0000000000
--- a/src/services/note/unwatch.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { User } from '@/models/entities/user';
-import { NoteWatchings } from '@/models/index';
-import { Note } from '@/models/entities/note';
-
-export default async (me: User['id'], note: Note) => {
-	await NoteWatchings.delete({
-		noteId: note.id,
-		userId: me
-	});
-};
diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts
deleted file mode 100644
index e457191d99..0000000000
--- a/src/services/note/watch.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { User } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { NoteWatchings } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-import { NoteWatching } from '@/models/entities/note-watching';
-
-export default async (me: User['id'], note: Note) => {
-	// 自分の投稿はwatchできない
-	if (me === note.userId) {
-		return;
-	}
-
-	await NoteWatchings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		noteId: note.id,
-		userId: me,
-		noteUserId: note.userId
-	} as NoteWatching);
-};
diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts
deleted file mode 100644
index 5949d11b3b..0000000000
--- a/src/services/push-notification.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as push from 'web-push';
-import config from '@/config/index';
-import { SwSubscriptions } from '@/models/index';
-import { fetchMeta } from '@/misc/fetch-meta';
-import { Packed } from '@/misc/schema';
-
-type notificationType = 'notification' | 'unreadMessagingMessage';
-type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>;
-
-export default async function(userId: string, type: notificationType, body: notificationBody) {
-	const meta = await fetchMeta();
-
-	if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
-
-	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
-	push.setVapidDetails(config.url,
-		meta.swPublicKey,
-		meta.swPrivateKey);
-
-	// Fetch
-	const subscriptions = await SwSubscriptions.find({
-		userId: userId
-	});
-
-	for (const subscription of subscriptions) {
-		const pushSubscription = {
-			endpoint: subscription.endpoint,
-			keys: {
-				auth: subscription.auth,
-				p256dh: subscription.publickey
-			}
-		};
-
-		push.sendNotification(pushSubscription, JSON.stringify({
-			type, body
-		}), {
-			proxy: config.proxy
-		}).catch((err: any) => {
-			//swLogger.info(err.statusCode);
-			//swLogger.info(err.headers);
-			//swLogger.info(err.body);
-
-			if (err.statusCode === 410) {
-				SwSubscriptions.delete({
-					userId: userId,
-					endpoint: subscription.endpoint,
-					auth: subscription.auth,
-					publickey: subscription.publickey
-				});
-			}
-		});
-	}
-}
diff --git a/src/services/register-or-fetch-instance-doc.ts b/src/services/register-or-fetch-instance-doc.ts
deleted file mode 100644
index a548ab0497..0000000000
--- a/src/services/register-or-fetch-instance-doc.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Instance } from '@/models/entities/instance';
-import { Instances } from '@/models/index';
-import { federationChart } from '@/services/chart/index';
-import { genId } from '@/misc/gen-id';
-import { toPuny } from '@/misc/convert-host';
-import { Cache } from '@/misc/cache';
-
-const cache = new Cache<Instance>(1000 * 60 * 60);
-
-export async function registerOrFetchInstanceDoc(host: string): Promise<Instance> {
-	host = toPuny(host);
-
-	const cached = cache.get(host);
-	if (cached) return cached;
-
-	const index = await Instances.findOne({ host });
-
-	if (index == null) {
-		const i = await Instances.save({
-			id: genId(),
-			host,
-			caughtAt: new Date(),
-			lastCommunicatedAt: new Date(),
-		});
-
-		federationChart.update(true);
-
-		cache.set(host, i);
-		return i;
-	} else {
-		cache.set(host, index);
-		return index;
-	}
-}
diff --git a/src/services/relay.ts b/src/services/relay.ts
deleted file mode 100644
index 04775524fa..0000000000
--- a/src/services/relay.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { createSystemUser } from './create-system-user';
-import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay';
-import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index';
-import renderUndo from '@/remote/activitypub/renderer/undo';
-import { deliver } from '@/queue/index';
-import { ILocalUser, User } from '@/models/entities/user';
-import { Users, Relays } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-const ACTOR_USERNAME = 'relay.actor' as const;
-
-export async function getRelayActor(): Promise<ILocalUser> {
-	const user = await Users.findOne({
-		host: null,
-		username: ACTOR_USERNAME
-	});
-
-	if (user) return user as ILocalUser;
-
-	const created = await createSystemUser(ACTOR_USERNAME);
-	return created as ILocalUser;
-}
-
-export async function addRelay(inbox: string) {
-	const relay = await Relays.save({
-		id: genId(),
-		inbox,
-		status: 'requesting'
-	});
-
-	const relayActor = await getRelayActor();
-	const follow = await renderFollowRelay(relay, relayActor);
-	const activity = renderActivity(follow);
-	deliver(relayActor, activity, relay.inbox);
-
-	return relay;
-}
-
-export async function removeRelay(inbox: string) {
-	const relay = await Relays.findOne({
-		inbox
-	});
-
-	if (relay == null) {
-		throw 'relay not found';
-	}
-
-	const relayActor = await getRelayActor();
-	const follow = renderFollowRelay(relay, relayActor);
-	const undo = renderUndo(follow, relayActor);
-	const activity = renderActivity(undo);
-	deliver(relayActor, activity, relay.inbox);
-
-	await Relays.delete(relay.id);
-}
-
-export async function listRelay() {
-	const relays = await Relays.find();
-	return relays;
-}
-
-export async function relayAccepted(id: string) {
-	const result = await Relays.update(id, {
-		status: 'accepted'
-	});
-
-	return JSON.stringify(result);
-}
-
-export async function relayRejected(id: string) {
-	const result = await Relays.update(id, {
-		status: 'rejected'
-	});
-
-	return JSON.stringify(result);
-}
-
-export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any) {
-	if (activity == null) return;
-
-	const relays = await Relays.find({
-		status: 'accepted'
-	});
-	if (relays.length === 0) return;
-
-	const copy = JSON.parse(JSON.stringify(activity));
-	if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
-
-	const signed = await attachLdSignature(copy, user);
-
-	for (const relay of relays) {
-		deliver(user, signed, relay.inbox);
-	}
-}
diff --git a/src/services/send-email-notification.ts b/src/services/send-email-notification.ts
deleted file mode 100644
index 519d56a06b..0000000000
--- a/src/services/send-email-notification.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { UserProfiles } from '@/models/index';
-import { User } from '@/models/entities/user';
-import { sendEmail } from './send-email';
-import * as locales from '../../locales/index';
-import { I18n } from '@/misc/i18n';
-import { getAcct } from '@/misc/acct';
-
-// TODO: locale ファイルをクライアント用とサーバー用で分けたい
-
-async function follow(userId: User['id'], follower: User) {
-	const userProfile = await UserProfiles.findOneOrFail({ userId: userId });
-	if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return;
-	const locale = locales[userProfile.lang || 'ja-JP'];
-	const i18n = new I18n(locale);
-	// TODO: render user information html
-	sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${getAcct(follower)})`, `${follower.name} (@${getAcct(follower)})`);
-}
-
-async function receiveFollowRequest(userId: User['id'], follower: User) {
-	const userProfile = await UserProfiles.findOneOrFail({ userId: userId });
-	if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return;
-	const locale = locales[userProfile.lang || 'ja-JP'];
-	const i18n = new I18n(locale);
-	// TODO: render user information html
-	sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${getAcct(follower)})`, `${follower.name} (@${getAcct(follower)})`);
-}
-
-export const sendEmailNotification = {
-	follow,
-	receiveFollowRequest,
-};
diff --git a/src/services/send-email.ts b/src/services/send-email.ts
deleted file mode 100644
index d24168ec46..0000000000
--- a/src/services/send-email.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import * as nodemailer from 'nodemailer';
-import { fetchMeta } from '@/misc/fetch-meta';
-import Logger from './logger';
-import config from '@/config/index';
-
-export const logger = new Logger('email');
-
-export async function sendEmail(to: string, subject: string, html: string, text: string) {
-	const meta = await fetchMeta(true);
-
-	const iconUrl = `${config.url}/static-assets/mi-white.png`;
-	const emailSettingUrl = `${config.url}/settings/email`;
-
-	const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
-
-	const transporter = nodemailer.createTransport({
-		host: meta.smtpHost,
-		port: meta.smtpPort,
-		secure: meta.smtpSecure,
-		ignoreTLS: !enableAuth,
-		proxy: config.proxySmtp,
-		auth: enableAuth ? {
-			user: meta.smtpUser,
-			pass: meta.smtpPass
-		} : undefined
-	} as any);
-
-	try {
-		// TODO: htmlサニタイズ
-		const info = await transporter.sendMail({
-			from: meta.email!,
-			to: to,
-			subject: subject,
-			text: text,
-			html: `<!doctype html>
-			<html>
-				<head>
-					<meta charset="utf-8">
-					<title>${ subject }</title>
-					<style>
-						html {
-							background: #eee;
-						}
-
-						body {
-							padding: 16px;
-							margin: 0;
-							font-family: sans-serif;
-							font-size: 14px;
-						}
-
-						a {
-							text-decoration: none;
-							color: #86b300;
-						}
-						a:hover {
-							text-decoration: underline;
-						}
-
-						main {
-							max-width: 500px;
-							margin: 0 auto;
-							background: #fff;
-							color: #555;
-						}
-							main > header {
-								padding: 32px;
-								background: #86b300;
-							}
-								main > header > img {
-									max-width: 128px;
-									max-height: 28px;
-									vertical-align: bottom;
-								}
-							main > article {
-								padding: 32px;
-							}
-								main > article > h1 {
-									margin: 0 0 1em 0;
-								}
-							main > footer {
-								padding: 32px;
-								border-top: solid 1px #eee;
-							}
-
-						nav {
-							box-sizing: border-box;
-							max-width: 500px;
-							margin: 16px auto 0 auto;
-							padding: 0 32px;
-						}
-							nav > a {
-								color: #888;
-							}
-					</style>
-				</head>
-				<body>
-					<main>
-						<header>
-							<img src="${ meta.logoImageUrl || meta.iconUrl || iconUrl }"/>
-						</header>
-						<article>
-							<h1>${ subject }</h1>
-							<div>${ html }</div>
-						</article>
-						<footer>
-							<a href="${ emailSettingUrl }">${ 'Email setting' }</a>
-						</footer>
-					</main>
-					<nav>
-						<a href="${ config.url }">${ config.host }</a>
-					</nav>
-				</body>
-			</html>
-			`
-		});
-
-		logger.info('Message sent: %s', info.messageId);
-	} catch (e) {
-		logger.error(e);
-		throw e;
-	}
-}
diff --git a/src/services/stream.ts b/src/services/stream.ts
deleted file mode 100644
index 2c308a1b54..0000000000
--- a/src/services/stream.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { redisClient } from '../db/redis';
-import { User } from '@/models/entities/user';
-import { Note } from '@/models/entities/note';
-import { UserList } from '@/models/entities/user-list';
-import { ReversiGame } from '@/models/entities/games/reversi/game';
-import { UserGroup } from '@/models/entities/user-group';
-import config from '@/config/index';
-import { Antenna } from '@/models/entities/antenna';
-import { Channel } from '@/models/entities/channel';
-import {
-	StreamChannels,
-	AdminStreamTypes,
-	AntennaStreamTypes,
-	BroadcastTypes,
-	ChannelStreamTypes,
-	DriveStreamTypes,
-	GroupMessagingStreamTypes,
-	InternalStreamTypes,
-	MainStreamTypes,
-	MessagingIndexStreamTypes,
-	MessagingStreamTypes,
-	NoteStreamTypes,
-	ReversiGameStreamTypes,
-	ReversiStreamTypes,
-	UserListStreamTypes,
-	UserStreamTypes
-} from '@/server/api/stream/types';
-import { Packed } from '@/misc/schema';
-
-class Publisher {
-	private publish = (channel: StreamChannels, type: string | null, value?: any): void => {
-		const message = type == null ? value : value == null ?
-			{ type: type, body: null } :
-			{ type: type, body: value };
-
-		redisClient.publish(config.host, JSON.stringify({
-			channel: channel,
-			message: message
-		}));
-	}
-
-	public publishInternalEvent = <K extends keyof InternalStreamTypes>(type: K, value?: InternalStreamTypes[K]): void => {
-		this.publish('internal', type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishUserEvent = <K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void => {
-		this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishBroadcastStream = <K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void => {
-		this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishMainStream = <K extends keyof MainStreamTypes>(userId: User['id'], type: K, value?: MainStreamTypes[K]): void => {
-		this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishDriveStream = <K extends keyof DriveStreamTypes>(userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => {
-		this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishNoteStream = <K extends keyof NoteStreamTypes>(noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => {
-		this.publish(`noteStream:${noteId}`, type, {
-			id: noteId,
-			body: value
-		});
-	}
-
-	public publishChannelStream = <K extends keyof ChannelStreamTypes>(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => {
-		this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishUserListStream = <K extends keyof UserListStreamTypes>(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => {
-		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishAntennaStream = <K extends keyof AntennaStreamTypes>(antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => {
-		this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishMessagingStream = <K extends keyof MessagingStreamTypes>(userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void => {
-		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishGroupMessagingStream = <K extends keyof GroupMessagingStreamTypes>(groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => {
-		this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishMessagingIndexStream = <K extends keyof MessagingIndexStreamTypes>(userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => {
-		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishReversiStream = <K extends keyof ReversiStreamTypes>(userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => {
-		this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishReversiGameStream = <K extends keyof ReversiGameStreamTypes>(gameId: ReversiGame['id'], type: K, value?: ReversiGameStreamTypes[K]): void => {
-		this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
-	}
-
-	public publishNotesStream = (note: Packed<'Note'>): void => {
-		this.publish('notesStream', null, note);
-	}
-
-	public publishAdminStream = <K extends keyof AdminStreamTypes>(userId: User['id'], type: K, value?: AdminStreamTypes[K]): void => {
-		this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
-	}
-}
-
-const publisher = new Publisher();
-
-export default publisher;
-
-export const publishInternalEvent = publisher.publishInternalEvent;
-export const publishUserEvent = publisher.publishUserEvent;
-export const publishBroadcastStream = publisher.publishBroadcastStream;
-export const publishMainStream = publisher.publishMainStream;
-export const publishDriveStream = publisher.publishDriveStream;
-export const publishNoteStream = publisher.publishNoteStream;
-export const publishNotesStream = publisher.publishNotesStream;
-export const publishChannelStream = publisher.publishChannelStream;
-export const publishUserListStream = publisher.publishUserListStream;
-export const publishAntennaStream = publisher.publishAntennaStream;
-export const publishMessagingStream = publisher.publishMessagingStream;
-export const publishGroupMessagingStream = publisher.publishGroupMessagingStream;
-export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
-export const publishReversiStream = publisher.publishReversiStream;
-export const publishReversiGameStream = publisher.publishReversiGameStream;
-export const publishAdminStream = publisher.publishAdminStream;
diff --git a/src/services/suspend-user.ts b/src/services/suspend-user.ts
deleted file mode 100644
index 55be63172f..0000000000
--- a/src/services/suspend-user.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import renderDelete from '@/remote/activitypub/renderer/delete';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import { deliver } from '@/queue/index';
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-import { Users, Followings } from '@/models/index';
-import { Not, IsNull } from 'typeorm';
-
-export async function doPostSuspend(user: { id: User['id']; host: User['host'] }) {
-	if (Users.isLocalUser(user)) {
-		// 知り得る全SharedInboxにDelete配信
-		const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user));
-
-		const queue: string[] = [];
-
-		const followings = await Followings.find({
-			where: [
-				{ followerSharedInbox: Not(IsNull()) },
-				{ followeeSharedInbox: Not(IsNull()) }
-			],
-			select: ['followerSharedInbox', 'followeeSharedInbox']
-		});
-
-		const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox);
-
-		for (const inbox of inboxes) {
-			if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
-		}
-
-		for (const inbox of queue) {
-			deliver(user, content, inbox);
-		}
-	}
-}
diff --git a/src/services/unsuspend-user.ts b/src/services/unsuspend-user.ts
deleted file mode 100644
index bfffa036e5..0000000000
--- a/src/services/unsuspend-user.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import renderDelete from '@/remote/activitypub/renderer/delete';
-import renderUndo from '@/remote/activitypub/renderer/undo';
-import { renderActivity } from '@/remote/activitypub/renderer/index';
-import { deliver } from '@/queue/index';
-import config from '@/config/index';
-import { User } from '@/models/entities/user';
-import { Users, Followings } from '@/models/index';
-import { Not, IsNull } from 'typeorm';
-
-export async function doPostUnsuspend(user: User) {
-	if (Users.isLocalUser(user)) {
-		// 知り得る全SharedInboxにUndo Delete配信
-		const content = renderActivity(renderUndo(renderDelete(`${config.url}/users/${user.id}`, user), user));
-
-		const queue: string[] = [];
-
-		const followings = await Followings.find({
-			where: [
-				{ followerSharedInbox: Not(IsNull()) },
-				{ followeeSharedInbox: Not(IsNull()) }
-			],
-			select: ['followerSharedInbox', 'followeeSharedInbox']
-		});
-
-		const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox);
-
-		for (const inbox of inboxes) {
-			if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
-		}
-
-		for (const inbox of queue) {
-			deliver(user as any, content, inbox);
-		}
-	}
-}
diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts
deleted file mode 100644
index e8504f6ff0..0000000000
--- a/src/services/update-hashtag.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { User } from '@/models/entities/user';
-import { Hashtags, Users } from '@/models/index';
-import { hashtagChart } from '@/services/chart/index';
-import { genId } from '@/misc/gen-id';
-import { Hashtag } from '@/models/entities/hashtag';
-import { normalizeForSearch } from '@/misc/normalize-for-search';
-
-export async function updateHashtags(user: { id: User['id']; host: User['host']; }, tags: string[]) {
-	for (const tag of tags) {
-		await updateHashtag(user, tag);
-	}
-}
-
-export async function updateUsertags(user: User, tags: string[]) {
-	for (const tag of tags) {
-		await updateHashtag(user, tag, true, true);
-	}
-
-	for (const tag of (user.tags || []).filter(x => !tags.includes(x))) {
-		await updateHashtag(user, tag, true, false);
-	}
-}
-
-export async function updateHashtag(user: { id: User['id']; host: User['host']; }, tag: string, isUserAttached = false, inc = true) {
-	tag = normalizeForSearch(tag);
-
-	const index = await Hashtags.findOne({ name: tag });
-
-	if (index == null && !inc) return;
-
-	if (index != null) {
-		const q = Hashtags.createQueryBuilder('tag').update()
-			.where('name = :name', { name: tag });
-
-		const set = {} as any;
-
-		if (isUserAttached) {
-			if (inc) {
-				// 自分が初めてこのタグを使ったなら
-				if (!index.attachedUserIds.some(id => id === user.id)) {
-					set.attachedUserIds = () => `array_append("attachedUserIds", '${user.id}')`;
-					set.attachedUsersCount = () => `"attachedUsersCount" + 1`;
-				}
-				// 自分が(ローカル内で)初めてこのタグを使ったなら
-				if (Users.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) {
-					set.attachedLocalUserIds = () => `array_append("attachedLocalUserIds", '${user.id}')`;
-					set.attachedLocalUsersCount = () => `"attachedLocalUsersCount" + 1`;
-				}
-				// 自分が(リモートで)初めてこのタグを使ったなら
-				if (Users.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) {
-					set.attachedRemoteUserIds = () => `array_append("attachedRemoteUserIds", '${user.id}')`;
-					set.attachedRemoteUsersCount = () => `"attachedRemoteUsersCount" + 1`;
-				}
-			} else {
-				set.attachedUserIds = () => `array_remove("attachedUserIds", '${user.id}')`;
-				set.attachedUsersCount = () => `"attachedUsersCount" - 1`;
-				if (Users.isLocalUser(user)) {
-					set.attachedLocalUserIds = () => `array_remove("attachedLocalUserIds", '${user.id}')`;
-					set.attachedLocalUsersCount = () => `"attachedLocalUsersCount" - 1`;
-				} else {
-					set.attachedRemoteUserIds = () => `array_remove("attachedRemoteUserIds", '${user.id}')`;
-					set.attachedRemoteUsersCount = () => `"attachedRemoteUsersCount" - 1`;
-				}
-			}
-		} else {
-			// 自分が初めてこのタグを使ったなら
-			if (!index.mentionedUserIds.some(id => id === user.id)) {
-				set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`;
-				set.mentionedUsersCount = () => `"mentionedUsersCount" + 1`;
-			}
-			// 自分が(ローカル内で)初めてこのタグを使ったなら
-			if (Users.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) {
-				set.mentionedLocalUserIds = () => `array_append("mentionedLocalUserIds", '${user.id}')`;
-				set.mentionedLocalUsersCount = () => `"mentionedLocalUsersCount" + 1`;
-			}
-			// 自分が(リモートで)初めてこのタグを使ったなら
-			if (Users.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) {
-				set.mentionedRemoteUserIds = () => `array_append("mentionedRemoteUserIds", '${user.id}')`;
-				set.mentionedRemoteUsersCount = () => `"mentionedRemoteUsersCount" + 1`;
-			}
-		}
-
-		if (Object.keys(set).length > 0) {
-			q.set(set);
-			q.execute();
-		}
-	} else {
-		if (isUserAttached) {
-			Hashtags.insert({
-				id: genId(),
-				name: tag,
-				mentionedUserIds: [],
-				mentionedUsersCount: 0,
-				mentionedLocalUserIds: [],
-				mentionedLocalUsersCount: 0,
-				mentionedRemoteUserIds: [],
-				mentionedRemoteUsersCount: 0,
-				attachedUserIds: [user.id],
-				attachedUsersCount: 1,
-				attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [],
-				attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0,
-				attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [],
-				attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
-			} as Hashtag);
-		} else {
-			Hashtags.insert({
-				id: genId(),
-				name: tag,
-				mentionedUserIds: [user.id],
-				mentionedUsersCount: 1,
-				mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [],
-				mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0,
-				mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [],
-				mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
-				attachedUserIds: [],
-				attachedUsersCount: 0,
-				attachedLocalUserIds: [],
-				attachedLocalUsersCount: 0,
-				attachedRemoteUserIds: [],
-				attachedRemoteUsersCount: 0,
-			} as Hashtag);
-		}
-	}
-
-	if (!isUserAttached) {
-		hashtagChart.update(tag, user);
-	}
-}
diff --git a/src/services/user-list/push.ts b/src/services/user-list/push.ts
deleted file mode 100644
index 29d561b519..0000000000
--- a/src/services/user-list/push.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { publishUserListStream } from '@/services/stream';
-import { User } from '@/models/entities/user';
-import { UserList } from '@/models/entities/user-list';
-import { UserListJoinings, Users } from '@/models/index';
-import { UserListJoining } from '@/models/entities/user-list-joining';
-import { genId } from '@/misc/gen-id';
-import { fetchProxyAccount } from '@/misc/fetch-proxy-account';
-import createFollowing from '../following/create';
-
-export async function pushUserToUserList(target: User, list: UserList) {
-	await UserListJoinings.insert({
-		id: genId(),
-		createdAt: new Date(),
-		userId: target.id,
-		userListId: list.id
-	} as UserListJoining);
-
-	publishUserListStream(list.id, 'userAdded', await Users.pack(target));
-
-	// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
-	if (Users.isRemoteUser(target)) {
-		const proxy = await fetchProxyAccount();
-		if (proxy) {
-			createFollowing(proxy, target);
-		}
-	}
-}
diff --git a/src/services/validate-email-for-account.ts b/src/services/validate-email-for-account.ts
deleted file mode 100644
index 1d039fb263..0000000000
--- a/src/services/validate-email-for-account.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import validateEmail from 'deep-email-validator';
-import { UserProfiles } from '@/models';
-
-export async function validateEmailForAccount(emailAddress: string): Promise<{
-	available: boolean;
-	reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
-}> {
-	const exist = await UserProfiles.count({
-		emailVerified: true,
-		email: emailAddress,
-	});
-
-	const validated = await validateEmail({
-		email: emailAddress,
-		validateRegex: true,
-		validateMx: true,
-		validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
-		validateDisposable: true, // 捨てアドかどうかチェック
-		validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
-	});
-
-	const available = exist === 0 && validated.valid;
-
-	return {
-		available,
-		reason: available ? null :
-			exist !== 0 ? 'used' :
-			validated.reason === 'regex' ? 'format' :
-			validated.reason === 'disposable' ? 'disposable' :
-			validated.reason === 'mx' ? 'mx' :
-			validated.reason === 'smtp' ? 'smtp' :
-			null,
-	};
-}
diff --git a/src/tools/accept-migration.ts b/src/tools/accept-migration.ts
deleted file mode 100644
index 2e54fc129f..0000000000
--- a/src/tools/accept-migration.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// ex) node built/tools/accept-migration Yo 1000000000001
-
-import { createConnection } from 'typeorm';
-import config from '@/config/index';
-
-createConnection({
-	type: 'postgres',
-	host: config.db.host,
-	port: config.db.port,
-	username: config.db.user,
-	password: config.db.pass,
-	database: config.db.db,
-	extra: config.db.extra,
-	synchronize: false,
-	dropSchema: false,
-}).then(c => {
-	c.query(`INSERT INTO migrations(timestamp,name) VALUES (${process.argv[3]}, '${process.argv[2]}${process.argv[3]}');`).then(() => {
-		console.log('done');
-		process.exit(0);
-	}).catch(e => {
-		console.log('ERROR:');
-		console.log(e);
-		process.exit(1);
-	});
-});
diff --git a/src/tools/add-emoji.ts b/src/tools/add-emoji.ts
deleted file mode 100644
index 9ffe7dfa81..0000000000
--- a/src/tools/add-emoji.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Emojis } from '@/models/index';
-import { genId } from '@/misc/gen-id';
-
-async function main(name: string, url: string, alias?: string): Promise<any> {
-	const aliases = alias != null ? [ alias ] : [];
-
-	await Emojis.save({
-		id: genId(),
-		host: null,
-		name,
-		url,
-		aliases,
-		updatedAt: new Date()
-	});
-}
-
-const args = process.argv.slice(2);
-const name = args[0];
-const url = args[1];
-
-if (!name) throw new Error('require name');
-if (!url) throw new Error('require url');
-
-main(name, url).then(() => {
-	console.log('success');
-	process.exit(0);
-}).catch(e => {
-	console.warn(e);
-	process.exit(1);
-});
diff --git a/src/tools/demote-admin.ts b/src/tools/demote-admin.ts
deleted file mode 100644
index d7c6d1cec2..0000000000
--- a/src/tools/demote-admin.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { initDb } from '../db/postgre';
-import { getRepository } from 'typeorm';
-import { User } from '@/models/entities/user';
-
-async function main(username: string) {
-	if (!username) throw `username required`;
-	username = username.replace(/^@/, '');
-
-	await initDb();
-	const Users = getRepository(User);
-
-	const res = await Users.update({
-		usernameLower: username.toLowerCase(),
-		host: null
-	}, {
-		isAdmin: false
-	});
-
-	if (res.affected !== 1) {
-		throw 'Failed';
-	}
-}
-
-const args = process.argv.slice(2);
-
-main(args[0]).then(() => {
-	console.log('Success');
-	process.exit(0);
-}).catch(e => {
-	console.error(`Error: ${e.message || e}`);
-	process.exit(1);
-});
diff --git a/src/tools/mark-admin.ts b/src/tools/mark-admin.ts
deleted file mode 100644
index 62ed0f09ee..0000000000
--- a/src/tools/mark-admin.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { initDb } from '../db/postgre';
-import { getRepository } from 'typeorm';
-import { User } from '@/models/entities/user';
-
-async function main(username: string) {
-	if (!username) throw `username required`;
-	username = username.replace(/^@/, '');
-
-	await initDb();
-	const Users = getRepository(User);
-
-	const res = await Users.update({
-		usernameLower: username.toLowerCase(),
-		host: null
-	}, {
-		isAdmin: true
-	});
-
-	if (res.affected !== 1) {
-		throw 'Failed';
-	}
-}
-
-const args = process.argv.slice(2);
-
-main(args[0]).then(() => {
-	console.log('Success');
-	process.exit(0);
-}).catch(e => {
-	console.error(`Error: ${e.message || e}`);
-	process.exit(1);
-});
diff --git a/src/tools/refresh-question.ts b/src/tools/refresh-question.ts
deleted file mode 100644
index 98a3c2865f..0000000000
--- a/src/tools/refresh-question.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { updateQuestion } from '@/remote/activitypub/models/question';
-
-async function main(uri: string): Promise<any> {
-	return await updateQuestion(uri);
-}
-
-const args = process.argv.slice(2);
-const uri = args[0];
-
-main(uri).then(result => {
-	console.log(`Done: ${result}`);
-}).catch(e => {
-	console.warn(e);
-});
diff --git a/src/tools/resync-remote-user.ts b/src/tools/resync-remote-user.ts
deleted file mode 100644
index bc43e250cb..0000000000
--- a/src/tools/resync-remote-user.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { initDb } from '@/db/postgre';
-import { parseAcct } from '@/misc/acct';
-
-async function main(acct: string): Promise<any> {
-	await initDb();
-	const { resolveUser } = await import('@/remote/resolve-user');
-
-	const { username, host } = parseAcct(acct);
-	await resolveUser(username, host, {}, true);
-}
-
-// get args
-const args = process.argv.slice(2);
-let acct = args[0];
-
-// normalize args
-acct = acct.replace(/^@/, '');
-
-// check args
-if (!acct.match(/^\w+@\w/)) {
-	throw `Invalid acct format. Valid format are user@host`;
-}
-
-console.log(`resync ${acct}`);
-
-main(acct).then(() => {
-	console.log('Done');
-}).catch(e => {
-	console.warn(e);
-});
diff --git a/src/tools/show-signin-history.ts b/src/tools/show-signin-history.ts
deleted file mode 100644
index ad92316314..0000000000
--- a/src/tools/show-signin-history.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Users, Signins } from '@/models/index';
-
-// node built/tools/show-signin-history username
-//  => {Success} {Date} {IPAddrsss}
-
-// node built/tools/show-signin-history username user-agent,x-forwarded-for
-//  with user-agent and x-forwarded-for
-
-// node built/tools/show-signin-history username all
-//  with full request headers
-
-async function main(username: string, headers?: string[]) {
-	const user = await Users.findOne({
-		host: null,
-		usernameLower: username.toLowerCase(),
-	});
-
-	if (user == null) throw new Error('User not found');
-
-	const history = await Signins.find({
-		userId: user.id
-	});
-
-	for (const signin of history) {
-		console.log(`${signin.success ? 'OK' : 'NG'} ${signin.createdAt ? signin.createdAt.toISOString() : 'Unknown'} ${signin.ip}`);
-
-		// headers
-		if (headers != null) {
-			for (const key of Object.keys(signin.headers)) {
-				if (headers.includes('all') || headers.includes(key)) {
-					console.log(`   ${key}: ${signin.headers[key]}`);
-				}
-			}
-		}
-	}
-}
-
-// get args
-const args = process.argv.slice(2);
-
-let username = args[0];
-let headers: string[] | undefined;
-
-if (args[1] != null) {
-	headers = args[1].split(/,/).map(header => header.toLowerCase());
-}
-
-// normalize args
-username = username.replace(/^@/, '');
-
-main(username, headers).then(() => {
-	process.exit(0);
-}).catch(e => {
-	console.warn(e);
-	process.exit(1);
-});
diff --git a/src/tsconfig.json b/src/tsconfig.json
deleted file mode 100644
index 4a03a17432..0000000000
--- a/src/tsconfig.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
-	"compilerOptions": {
-		"allowJs": true,
-		"noEmitOnError": false,
-		"noImplicitAny": true,
-		"noImplicitReturns": true,
-		"noUnusedParameters": false,
-		"noUnusedLocals": false,
-		"noFallthroughCasesInSwitch": true,
-		"declaration": false,
-		"sourceMap": true,
-		"target": "es2017",
-		"module": "commonjs",
-		"moduleResolution": "node",
-		"allowSyntheticDefaultImports": true,
-		"removeComments": false,
-		"noLib": false,
-		"strict": true,
-		"strictNullChecks": true,
-		"strictPropertyInitialization": false,
-		"experimentalDecorators": true,
-		"emitDecoratorMetadata": true,
-		"resolveJsonModule": true,
-		"isolatedModules": true,
-		"rootDir": "./",
-		"baseUrl": "./",
-		"paths": {
-			"@/*": ["./*"]
-		},
-		"outDir": "../built",
-		"typeRoots": [
-			"../node_modules/@types",
-			"./@types"
-		],
-		"lib": [
-			"esnext"
-		]
-	},
-	"compileOnSave": false,
-	"include": [
-		"./**/*.ts"
-	],
-	"exclude": [
-		"./client/**/*.ts"
-	]
-}
diff --git a/src/types.ts b/src/types.ts
deleted file mode 100644
index 20f6f8bb88..0000000000
--- a/src/types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
-
-export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
-
-export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
-
-export const ffVisibility = ['public', 'followers', 'private'] as const;
diff --git a/src/well-known-services.ts b/src/well-known-services.ts
deleted file mode 100644
index f47c540516..0000000000
--- a/src/well-known-services.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const wellKnownServices = [
-	['twitter.com', username => `https://twitter.com/${username}`],
-	['github.com', username => `https://github.com/${username}`],
-] as [string, (username: string) => string][];
diff --git a/vetur.config.js b/vetur.config.js
deleted file mode 100644
index 875ec65719..0000000000
--- a/vetur.config.js
+++ /dev/null
@@ -1,41 +0,0 @@
-// vetur.config.js
-/** @type {import('vls').VeturConfig} */
-module.exports = {
-  // **optional** default: `{}`
-  // override vscode settings
-  // Notice: It only affects the settings used by Vetur.
-  settings: {
-    "vetur.useWorkspaceDependencies": true,
-    "vetur.experimental.templateInterpolationService": true
-  },
-  // **optional** default: `[{ root: './' }]`
-  // support monorepos
-  projects: [
-    {
-      // **required**
-      // Where is your project?
-      // It is relative to `vetur.config.js`.
-      root: './src/client',
-      // **optional** default: `'package.json'`
-      // Where is `package.json` in the project?
-      // We use it to determine the version of vue.
-      // It is relative to root property.
-      package: '../../package.json',
-      // **optional**
-      // Where is TypeScript config file in the project?
-      // It is relative to root property.
-      tsconfig: './tsconfig.json',
-      // **optional** default: `'./.vscode/vetur/snippets'`
-      // Where is vetur custom snippets folders?
-      snippetFolder: './.vscode/vetur/snippets',
-      // **optional** default: `[]`
-      // Register globally Vue component glob.
-      // If you set it, you can get completion by that components.
-      // It is relative to root property.
-      // Notice: It won't actually do it. You need to use `require.context` or `Vue.component`
-      globalComponents: [
-        './src/clients/components/global/*.vue'
-      ]
-    }
-  ]
-}
diff --git a/webpack.config.ts b/webpack.config.ts
deleted file mode 100644
index 74426df2e6..0000000000
--- a/webpack.config.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-/**
- * webpack configuration
- */
-
-import * as fs from 'fs';
-import * as webpack from 'webpack';
-const { VueLoaderPlugin } = require('vue-loader');
-
-class WebpackOnBuildPlugin {
-	constructor(readonly callback: (stats: any) => void) {
-	}
-
-	public apply(compiler: any) {
-		compiler.hooks.done.tap('WebpackOnBuildPlugin', this.callback);
-	}
-}
-
-const isProduction = process.env.NODE_ENV === 'production';
-
-const locales = require('./locales');
-const meta = require('./package.json');
-
-const postcss = {
-	loader: 'postcss-loader',
-	options: {
-		postcssOptions: {
-			plugins: [
-				require('cssnano')({
-					preset: 'default'
-				})
-			]
-		}
-	},
-};
-
-module.exports = {
-	entry: {
-		app: './src/client/init.ts',
-		sw: './src/client/sw/sw.ts'
-	},
-	module: {
-		rules: [{
-			test: /\.vue$/,
-			exclude: /node_modules/,
-			use: [{
-				loader: 'vue-loader',
-				options: {
-					cssSourceMap: false,
-					compilerOptions: {
-						preserveWhitespace: false
-					}
-				}
-			}]
-		}, {
-			test: /\.scss?$/,
-			exclude: /node_modules/,
-			oneOf: [{
-				resourceQuery: /module/,
-				use: [{
-					loader: 'vue-style-loader'
-				}, {
-					loader: 'css-loader',
-					options: {
-						modules: true,
-						esModule: false, // TODO: trueにすると壊れる。Vue3移行の折にはtrueにできるかもしれない
-						url: false,
-					}
-				}, postcss, {
-					loader: 'sass-loader',
-					options: {
-						implementation: require('sass'),
-						sassOptions: {
-							fiber: false
-						}
-					}
-				}]
-			}, {
-				use: [{
-					loader: 'vue-style-loader'
-				}, {
-					loader: 'css-loader',
-					options: {
-						url: false,
-						esModule: false, // TODO: trueにすると壊れる。Vue3移行の折にはtrueにできるかもしれない
-					}
-				}, postcss, {
-					loader: 'sass-loader',
-					options: {
-						implementation: require('sass'),
-						sassOptions: {
-							fiber: false
-						}
-					}
-				}]
-			}]
-		}, {
-			test: /\.css$/,
-			oneOf: [{
-				resourceQuery: /module/,
-				use: [{
-					loader: 'vue-style-loader'
-				}, {
-					loader: 'css-loader',
-					options: {
-						modules: true,
-						esModule: false, // TODO: trueにすると壊れる。Vue3移行の折にはtrueにできるかもしれない
-					}
-				}, postcss]
-			}, {
-				use: [{
-					loader: 'vue-style-loader'
-				}, {
-					loader: 'css-loader',
-					options: {
-						esModule: false, // TODO: trueにすると壊れる。Vue3移行の折にはtrueにできるかもしれない
-					}
-				}, postcss]
-			}]
-		}, {
-			test: /\.svg$/,
-			use: [
-				'vue-loader',
-				'vue-svg-loader',
-			],
-		}, {
-			test: /\.(eot|woff|woff2|svg|ttf)([?]?.*)$/,
-			type: 'asset/resource'
-		}, {
-			test: /\.json5$/,
-			loader: 'json5-loader',
-			options: {
-				esModule: false,
-			},
-			type: 'javascript/auto'
-		}, {
-			test: /\.ts$/,
-			exclude: /node_modules/,
-			use: [{
-				loader: 'ts-loader',
-				options: {
-					happyPackMode: true,
-					transpileOnly: true,
-					configFile: __dirname + '/src/client/tsconfig.json',
-					appendTsSuffixTo: [/\.vue$/]
-				}
-			}]
-		}]
-	},
-	plugins: [
-		new webpack.ProgressPlugin({}),
-		new webpack.DefinePlugin({
-			_VERSION_: JSON.stringify(meta.version),
-			_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]: [string, any]) => [k, v._lang_])),
-			_ENV_: JSON.stringify(process.env.NODE_ENV),
-			_DEV_: process.env.NODE_ENV !== 'production',
-			_PERF_PREFIX_: JSON.stringify('Misskey:'),
-			_DATA_TRANSFER_DRIVE_FILE_: JSON.stringify('mk_drive_file'),
-			_DATA_TRANSFER_DRIVE_FOLDER_: JSON.stringify('mk_drive_folder'),
-			_DATA_TRANSFER_DECK_COLUMN_: JSON.stringify('mk_deck_column'),
-			__VUE_OPTIONS_API__: true,
-			__VUE_PROD_DEVTOOLS__: false,
-		}),
-		new VueLoaderPlugin(),
-		new WebpackOnBuildPlugin((stats: any) => {
-			fs.writeFileSync('./built/meta.json', JSON.stringify({ version: meta.version }), 'utf-8');
-		}),
-	],
-	output: {
-		path: __dirname + '/built/assets',
-		filename: `[name].${meta.version}.js`,
-		publicPath: `/assets/`,
-		pathinfo: false,
-	},
-	resolve: {
-		extensions: [
-			'.js', '.ts', '.json'
-		],
-		alias: {
-			'@client': __dirname + '/src/client',
-			'@lib': __dirname + '/lib',
-			'@': __dirname + '/src',
-			'const.styl': __dirname + '/src/client/const.styl'
-		}
-	},
-	resolveLoader: {
-		modules: ['node_modules']
-	},
-	experiments: {
-		topLevelAwait: true
-	},
-	devtool: false, //'source-map',
-	mode: isProduction ? 'production' : 'development'
-};
diff --git a/yarn.lock b/yarn.lock
index 8cbe2f1bac..3d82134974 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,77 +2,6 @@
 # yarn lockfile v1
 
 
-"@babel/code-frame@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
-  integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
-  dependencies:
-    "@babel/highlight" "^7.12.13"
-
-"@babel/helper-validator-identifier@^7.12.11":
-  version "7.12.11"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
-  integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
-
-"@babel/highlight@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c"
-  integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.12.11"
-    chalk "^2.0.0"
-    js-tokens "^4.0.0"
-
-"@babel/parser@^7.15.0":
-  version "7.15.6"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.6.tgz#043b9aa3c303c0722e5377fef9197f4cf1796549"
-  integrity sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q==
-
-"@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
-  version "7.13.9"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99"
-  integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==
-
-"@babel/runtime@^7.16.0":
-  version "7.16.3"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
-  integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
-  dependencies:
-    regenerator-runtime "^0.13.4"
-
-"@babel/runtime@^7.6.2":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d"
-  integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==
-  dependencies:
-    regenerator-runtime "^0.13.4"
-
-"@babel/types@^7.6.1", "@babel/types@^7.9.6":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80"
-  integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.12.11"
-    lodash "^4.17.19"
-    to-fast-properties "^2.0.0"
-
-"@cspotcode/source-map-consumer@0.8.0":
-  version "0.8.0"
-  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
-  integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
-
-"@cspotcode/source-map-support@0.7.0":
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5"
-  integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==
-  dependencies:
-    "@cspotcode/source-map-consumer" "0.8.0"
-
-"@cto.af/textdecoder@^0.0.0":
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/@cto.af/textdecoder/-/textdecoder-0.0.0.tgz#e1e8d84c936c30a0f4619971f19ca41941af9fdc"
-  integrity sha512-sJpx3F5xcVV/9jNYJQtvimo4Vfld/nD3ph+ZWtQzZ03Zo8rJC7QKQTRcIGS13Rcz80DwFNthCWMrd58vpY4ZAQ==
-
 "@cypress/request@^2.88.6":
   version "2.88.6"
   resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.6.tgz#a970dd675befc6bdf8a8921576c01f51cc5798e9"
@@ -106,56 +35,6 @@
     debug "^3.1.0"
     lodash.once "^4.1.1"
 
-"@digitalbazaar/http-client@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-1.1.0.tgz#cac383b24ace04b18b919deab773462b03d3d7b0"
-  integrity sha512-ks7hqa6hm9NyULdbm9qL6TRS8rADzBw8R0lETvUgvdNXu9H62XG2YqoKRDThtfgWzWxLwRJ3Z2o4ev81dZZbyQ==
-  dependencies:
-    esm "^3.2.22"
-    ky "^0.25.1"
-    ky-universal "^0.8.2"
-
-"@discordapp/twemoji@13.1.0":
-  version "13.1.0"
-  resolved "https://registry.yarnpkg.com/@discordapp/twemoji/-/twemoji-13.1.0.tgz#6b25f3958fa8fd68692248c87776bc737fd009a9"
-  integrity sha512-KEw/te+ylD2MHutzigafyptv0kdTU05Dbgxr9Y5J9IAQw8PbFz16nKtlPnJtA23BLp9fZQeNXzUmegkRi7fpDA==
-  dependencies:
-    fs-extra "^8.0.1"
-    jsonfile "^5.0.0"
-    twemoji-parser "13.1.0"
-    universalify "^0.1.2"
-
-"@discoveryjs/json-ext@^0.5.0":
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"
-  integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==
-
-"@elastic/elasticsearch@7.11.0":
-  version "7.11.0"
-  resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.11.0.tgz#e196243d0ed026742fc160d72cc5b4b5b6c7807d"
-  integrity sha512-AFVVuANIdbV1qYjuOi4hnsX/DehWYG+bbhQO4amq9K4/NnzU7mpGWOPgVlRQTiX+vBfBkx7SL6h4QEjIlM3ztA==
-  dependencies:
-    debug "^4.1.1"
-    hpagent "^0.1.1"
-    ms "^2.1.1"
-    pump "^3.0.0"
-    secure-json-parse "^2.1.0"
-
-"@eslint/eslintrc@^1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.4.tgz#dfe0ff7ba270848d10c5add0715e04964c034b31"
-  integrity sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==
-  dependencies:
-    ajv "^6.12.4"
-    debug "^4.3.2"
-    espree "^9.0.0"
-    globals "^13.9.0"
-    ignore "^4.0.6"
-    import-fresh "^3.2.1"
-    js-yaml "^4.1.0"
-    minimatch "^3.0.4"
-    strip-json-comments "^3.1.1"
-
 "@hapi/hoek@^9.0.0":
   version "9.2.0"
   resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
@@ -168,88 +47,6 @@
   dependencies:
     "@hapi/hoek" "^9.0.0"
 
-"@humanwhocodes/config-array@^0.6.0":
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.6.0.tgz#b5621fdb3b32309d2d16575456cbc277fa8f021a"
-  integrity sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==
-  dependencies:
-    "@humanwhocodes/object-schema" "^1.2.0"
-    debug "^4.1.1"
-    minimatch "^3.0.4"
-
-"@humanwhocodes/object-schema@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
-  integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
-
-"@jfonx/console-utils@^1.0.3":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@jfonx/console-utils/-/console-utils-1.0.3.tgz#cbb7f911e4191a4a2fe1ba4807d29f100b5d099f"
-  integrity sha512-/XbnqjWc7yNZVLAJJO9rimfIz9DYte+cj3EF9hwhIv7vw6ok2t3cjl0huYsmD89srKH03vWjeqAcIH86CuYj3g==
-  dependencies:
-    colors "^1.3.3"
-
-"@jfonx/file-utils@^3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@jfonx/file-utils/-/file-utils-3.0.1.tgz#8d3d6e931a283420fe29802ea71c28dd397cd8d3"
-  integrity sha512-qwH0CuzWmghtTHGMyuPHj6SJPQgWeiXFJBfrxCWMbzxVCa3aLZPEfzSdlSnC/UABsk6feRkNdHXw59rVshNPqw==
-  dependencies:
-    "@jfonx/console-utils" "^1.0.3"
-    comment-json "^4.1.0"
-    find-up "^4.1.0"
-
-"@koa/cors@3.1.0":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.1.0.tgz#618bb073438cfdbd3ebd0e648a76e33b84f3a3b2"
-  integrity sha512-7ulRC1da/rBa6kj6P4g2aJfnET3z8Uf3SWu60cjbtxTA5g8lxRdX/Bd2P92EagGwwAhANeNw8T8if99rJliR6Q==
-  dependencies:
-    vary "^1.1.2"
-
-"@koa/multer@3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@koa/multer/-/multer-3.0.0.tgz#439777949f28097d7b329c0b4ce3048074c862f8"
-  integrity sha512-y+OQBmex5D1jIl723gAEUYcAWPEicIXppaAKw/zCMfpllQ08ZNweDPwoCLxEoatqd5pCu2XG6V8dl67JRq3RJw==
-
-"@koa/router@9.0.1":
-  version "9.0.1"
-  resolved "https://registry.yarnpkg.com/@koa/router/-/router-9.0.1.tgz#4090a14223ea7e78aa13b632761209cba69acd95"
-  integrity sha512-OI+OU49CJV4px0WkIMmayBeqVXB/JS1ZMq7UoGlTZt6Y7ijK7kdeQ18+SEHHJPytmtI1y6Hf8XLrpxva3mhv5Q==
-  dependencies:
-    debug "^4.1.1"
-    http-errors "^1.7.3"
-    koa-compose "^4.1.0"
-    methods "^1.1.2"
-    path-to-regexp "^6.1.0"
-
-"@nodelib/fs.scandir@2.1.3":
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
-  integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
-  dependencies:
-    "@nodelib/fs.stat" "2.0.3"
-    run-parallel "^1.1.9"
-
-"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3"
-  integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
-
-"@nodelib/fs.walk@^1.2.3":
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976"
-  integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
-  dependencies:
-    "@nodelib/fs.scandir" "2.1.3"
-    fastq "^1.6.0"
-
-"@npmcli/move-file@^1.0.1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
-  integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
-  dependencies:
-    mkdirp "^1.0.4"
-    rimraf "^3.0.2"
-
 "@redocly/ajv@^8.6.2":
   version "8.6.2"
   resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.2.tgz#8c4e485e72f7864f91fae40093bed548ec2619b2"
@@ -275,69 +72,6 @@
     node-fetch "^2.6.1"
     yaml-ast-parser "0.0.43"
 
-"@sentry/browser@5.29.2":
-  version "5.29.2"
-  resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.29.2.tgz#51adb4005511b1a4a70e4571880fc6653d651f91"
-  integrity sha512-uxZ7y7rp85tJll+RZtXRhXPbnFnOaxZqJEv05vJlXBtBNLQtlczV5iCtU9mZRLVHDtmZ5VVKUV8IKXntEqqDpQ==
-  dependencies:
-    "@sentry/core" "5.29.2"
-    "@sentry/types" "5.29.2"
-    "@sentry/utils" "5.29.2"
-    tslib "^1.9.3"
-
-"@sentry/core@5.29.2":
-  version "5.29.2"
-  resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.29.2.tgz#9e05fe197234161d57aabaf52fab336a7c520d81"
-  integrity sha512-7WYkoxB5IdlNEbwOwqSU64erUKH4laavPsM0/yQ+jojM76ErxlgEF0u//p5WaLPRzh3iDSt6BH+9TL45oNZeZw==
-  dependencies:
-    "@sentry/hub" "5.29.2"
-    "@sentry/minimal" "5.29.2"
-    "@sentry/types" "5.29.2"
-    "@sentry/utils" "5.29.2"
-    tslib "^1.9.3"
-
-"@sentry/hub@5.29.2":
-  version "5.29.2"
-  resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.29.2.tgz#208f10fe6674695575ad74182a1151f71d6df00a"
-  integrity sha512-LaAIo2hwUk9ykeh9RF0cwLy6IRw+DjEee8l1HfEaDFUM6TPGlNNGObMJNXb9/95jzWp7jWwOpQjoIE3jepdQJQ==
-  dependencies:
-    "@sentry/types" "5.29.2"
-    "@sentry/utils" "5.29.2"
-    tslib "^1.9.3"
-
-"@sentry/minimal@5.29.2":
-  version "5.29.2"
-  resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.29.2.tgz#420bebac8d03d30980fdb05c72d7b253d8aa541b"
-  integrity sha512-0aINSm8fGA1KyM7PavOBe1GDZDxrvnKt+oFnU0L+bTcw8Lr+of+v6Kwd97rkLRNOLw621xP076dL/7LSIzMuhw==
-  dependencies:
-    "@sentry/hub" "5.29.2"
-    "@sentry/types" "5.29.2"
-    tslib "^1.9.3"
-
-"@sentry/tracing@5.29.2":
-  version "5.29.2"
-  resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.29.2.tgz#6012788547d2ab7893799d82c4941bda145dcd47"
-  integrity sha512-iumYbVRpvoU3BUuIooxibydeaOOjl5ysc+mzsqhRs2NGW/C3uKAsFXdvyNfqt3bxtRQwJEhwJByLP2u3pLThpw==
-  dependencies:
-    "@sentry/hub" "5.29.2"
-    "@sentry/minimal" "5.29.2"
-    "@sentry/types" "5.29.2"
-    "@sentry/utils" "5.29.2"
-    tslib "^1.9.3"
-
-"@sentry/types@5.29.2":
-  version "5.29.2"
-  resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.29.2.tgz#ac87383df1222c2d9b9f8f9ed7a6b86ea41a098a"
-  integrity sha512-dM9wgt8wy4WRty75QkqQgrw9FV9F+BOMfmc0iaX13Qos7i6Qs2Q0dxtJ83SoR4YGtW8URaHzlDtWlGs5egBiMA==
-
-"@sentry/utils@5.29.2":
-  version "5.29.2"
-  resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.29.2.tgz#99a5cdda2ea19d34a41932f138d470adcb3ee673"
-  integrity sha512-nEwQIDjtFkeE4k6yIk4Ka5XjGRklNLThWLs2xfXlL7uwrYOH2B9UBBOOIRUraBm/g/Xrra3xsam/kRxuiwtXZQ==
-  dependencies:
-    "@sentry/types" "5.29.2"
-    tslib "^1.9.3"
-
 "@sideway/address@^4.1.0":
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.2.tgz#811b84333a335739d3969cfc434736268170cad1"
@@ -355,207 +89,11 @@
   resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
   integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
 
-"@sindresorhus/is@^4.0.0":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4"
-  integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==
-
-"@sinonjs/commons@^1.7.0":
-  version "1.7.2"
-  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
-  integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==
-  dependencies:
-    type-detect "4.0.8"
-
-"@sinonjs/fake-timers@7.1.2":
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5"
-  integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
-  dependencies:
-    "@sinonjs/commons" "^1.7.0"
-
-"@sqltools/formatter@^1.2.2":
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.3.tgz#1185726610acc37317ddab11c3c7f9066966bd20"
-  integrity sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==
-
-"@syuilo/aiscript@0.11.1":
-  version "0.11.1"
-  resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.11.1.tgz#52c14692113c58d1d62e6ae696352ba49abdf2eb"
-  integrity sha512-chwOIA3yLUKvOB0G611hjLArKTeOWNmTm3lHERSaDW1d+dS6do56naX6Lkwy2UpnwWC0qzeNSgg35elk6t2gZg==
-  dependencies:
-    autobind-decorator "2.4.0"
-    chalk "4.0.0"
-    seedrandom "3.0.5"
-    stringz "2.1.0"
-    uuid "7.0.3"
-
-"@szmarczak/http-timer@^4.0.5":
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152"
-  integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==
-  dependencies:
-    defer-to-connect "^2.0.0"
-
-"@tokenizer/token@^0.3.0":
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
-  integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
-
-"@tootallnate/once@1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
-  integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
-
-"@trysound/sax@0.2.0":
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
-  integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
-
-"@tsconfig/node10@^1.0.7":
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.7.tgz#1eb1de36c73478a2479cc661ef5af1c16d86d606"
-  integrity sha512-aBvUmXLQbayM4w3A8TrjwrXs4DZ8iduJnuJLLRGdkWlyakCf1q6uHZJBzXoRA/huAEknG5tcUyQxN3A+In5euQ==
-
-"@tsconfig/node12@^1.0.7":
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.7.tgz#677bd9117e8164dc319987dd6ff5fc1ba6fbf18b"
-  integrity sha512-dgasobK/Y0wVMswcipr3k0HpevxFJLijN03A8mYfEPvWvOs14v0ZlYTR4kIgMx8g4+fTyTFv8/jLCIfRqLDJ4A==
-
-"@tsconfig/node14@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.0.tgz#5bd046e508b1ee90bc091766758838741fdefd6e"
-  integrity sha512-RKkL8eTdPv6t5EHgFKIVQgsDapugbuOptNd9OOunN/HAkzmmTnZELx1kNCK0rSdUYGmiFMM3rRQMAWiyp023LQ==
-
-"@tsconfig/node16@^1.0.2":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
-  integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
-
-"@types/accepts@*":
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
-  integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/anymatch@*":
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
-  integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
-
-"@types/bcryptjs@2.4.2":
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae"
-  integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==
-
-"@types/body-parser@*":
-  version "1.19.0"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
-  integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
-  dependencies:
-    "@types/connect" "*"
-    "@types/node" "*"
-
-"@types/bull@3.15.5":
-  version "3.15.5"
-  resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.5.tgz#a4459c127c5b10fb847531579a2cd5db35751366"
-  integrity sha512-XgJQWJ03jyKMfdoL8IAIoHIo7JkkL74kcxuujTONkSJswm0giIJ9kuVgDNHS0OvD+OiPNcFmbBl0H3scj2+A8A==
-  dependencies:
-    "@types/ioredis" "*"
-
-"@types/cacheable-request@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
-  integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==
-  dependencies:
-    "@types/http-cache-semantics" "*"
-    "@types/keyv" "*"
-    "@types/node" "*"
-    "@types/responselike" "*"
-
-"@types/cbor@6.0.0":
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/@types/cbor/-/cbor-6.0.0.tgz#ddead015e14ef4463287d40cd92a6297a34dac8d"
-  integrity sha512-mGQ1lbYOwVti5Xlarn1bTeBZqgY0kstsdjnkoEovgohYKdBjGejHyNGXHdMBeqyQazIv32Jjp33+5pBEaSRy2w==
-  dependencies:
-    cbor "*"
-
-"@types/cheerio@0.22.18":
-  version "0.22.18"
-  resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.18.tgz#19018dceae691509901e339d63edf1e935978fe6"
-  integrity sha512-Fq7R3fINAPSdUEhOyjG4iVxgHrOnqDJbY0/BUuiN0pvD/rfmZWekVZnv+vcs8TtpA2XF50uv50LaE4EnpEL/Hw==
-  dependencies:
-    "@types/node" "*"
-
 "@types/color-name@^1.1.1":
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
   integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
 
-"@types/connect@*":
-  version "3.4.33"
-  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
-  integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
-  dependencies:
-    "@types/node" "*"
-
-"@types/content-disposition@*":
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.3.tgz#0aa116701955c2faa0717fc69cd1596095e49d96"
-  integrity sha512-P1bffQfhD3O4LW0ioENXUhZ9OIa0Zn+P7M+pWgkCKaT53wVLSq0mrKksCID/FGHpFhRSxRGhgrQmfhRuzwtKdg==
-
-"@types/cookies@*":
-  version "0.7.4"
-  resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.4.tgz#26dedf791701abc0e36b5b79a5722f40e455f87b"
-  integrity sha512-oTGtMzZZAVuEjTwCjIh8T8FrC8n/uwy+PG0yTvQcdZ7etoel7C7/3MSd7qrukENTgQtotG7gvBlBojuVs7X5rw==
-  dependencies:
-    "@types/connect" "*"
-    "@types/express" "*"
-    "@types/keygrip" "*"
-    "@types/node" "*"
-
-"@types/dateformat@3.0.1":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc"
-  integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g==
-
-"@types/disposable-email-domains@^1.0.1":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@types/disposable-email-domains/-/disposable-email-domains-1.0.2.tgz#0280f6b38fa7f14e54b056a434135ecd254483b1"
-  integrity sha512-SDKwyYTjk3y5aZBxxc38yRecpJPjsqn57STz1bNxYYlv4k11bBe7QB8w4llXDTmQXKT1mFvgGmJv+8Zdu3YmJw==
-
-"@types/escape-regexp@0.0.0":
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/@types/escape-regexp/-/escape-regexp-0.0.0.tgz#bff0225f9ef30d0dbdbe0e2a24283ee5342990c3"
-  integrity sha512-HTansGo4tJ7K7W9I9LBdQqnHtPB/Y7tlS+EMrkboaAQLsRPhRpHaqAHe01K1HVXM5e1u1IplRd8EBh+pJrp7Dg==
-
-"@types/eslint-scope@^3.7.0":
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86"
-  integrity sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==
-  dependencies:
-    "@types/eslint" "*"
-    "@types/estree" "*"
-
-"@types/eslint@*":
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.0.tgz#eb5c5b575237334df24c53195e37b53d66478d7b"
-  integrity sha512-LpUXkr7fnmPXWGxB0ZuLEzNeTURuHPavkC5zuU4sg62/TgL5ZEjamr5Y8b6AftwHtx2bPJasI+CL0TT2JwQ7aA==
-  dependencies:
-    "@types/estree" "*"
-    "@types/json-schema" "*"
-
-"@types/estree@*", "@types/estree@^0.0.46":
-  version "0.0.46"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe"
-  integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==
-
-"@types/estree@^0.0.50":
-  version "0.0.50"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
-  integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
-
 "@types/events@*":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@@ -566,24 +104,6 @@
   resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
   integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
 
-"@types/express-serve-static-core@*":
-  version "4.17.5"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz#a00ac7dadd746ae82477443e4d480a6a93ea083c"
-  integrity sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==
-  dependencies:
-    "@types/node" "*"
-    "@types/range-parser" "*"
-
-"@types/express@*":
-  version "4.17.6"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.6.tgz#6bce49e49570507b86ea1b07b806f04697fac45e"
-  integrity sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==
-  dependencies:
-    "@types/body-parser" "*"
-    "@types/express-serve-static-core" "*"
-    "@types/qs" "*"
-    "@types/serve-static" "*"
-
 "@types/fancy-log@1.3.0":
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.0.tgz#a61ab476e5e628cd07a846330df53b85e05c8ce0"
@@ -613,14 +133,6 @@
     "@types/minimatch" "*"
     "@types/node" "*"
 
-"@types/glob@7.2.0":
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
-  integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
-  dependencies:
-    "@types/minimatch" "*"
-    "@types/node" "*"
-
 "@types/gulp-rename@2.0.1":
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/@types/gulp-rename/-/gulp-rename-2.0.1.tgz#c8228fc2c5c4a7500346ea9ce18f27fa988caef5"
@@ -638,395 +150,21 @@
     "@types/vinyl-fs" "*"
     chokidar "^3.3.1"
 
-"@types/http-assert@*":
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b"
-  integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==
-
-"@types/http-cache-semantics@*":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a"
-  integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==
-
-"@types/http-errors@*":
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.0.tgz#682477dbbbd07cd032731cb3b0e7eaee3d026b69"
-  integrity sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==
-
-"@types/ioredis@*":
-  version "4.14.9"
-  resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.14.9.tgz#774387d44d3ad60e1b849044b2b28b96e5813866"
-  integrity sha512-yNdzppM6vY4DYqXCnt4A3PXArxsMWeJCYxFlyl4AJKrNSGMEAP9TPcXR+8Q6zh9glcCtxmwMQhi4pwdqqHH3OA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/is-url@1.2.30":
-  version "1.2.30"
-  resolved "https://registry.yarnpkg.com/@types/is-url/-/is-url-1.2.30.tgz#85567e8bee4fee69202bc3448f9fb34b0d56c50a"
-  integrity sha512-AnlNFwjzC8XLda5VjRl4ItSd8qp8pSNowvsut0WwQyBWHpOxjxRJm8iO6uETWqEyLdYdb9/1j+Qd9gQ4l5I4fw==
-
-"@types/js-yaml@4.0.4":
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.4.tgz#cc38781257612581a1a0eb25f1709d2b06812fce"
-  integrity sha512-AuHubXUmg0AzkXH0Mx6sIxeY/1C110mm/EkE/gB1sTRz3h2dao2W/63q42SlVST+lICxz5Oki2hzYA6+KnnieQ==
-
-"@types/jsdom@16.2.13":
-  version "16.2.13"
-  resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.13.tgz#126c8b7441b159d6234610a48de77b6066f1823f"
-  integrity sha512-8JQCjdeAidptSsOcRWk2iTm9wCcwn9l+kRG6k5bzUacrnm1ezV4forq0kWjUih/tumAeoG+OspOvQEbbRucBTw==
-  dependencies:
-    "@types/node" "*"
-    "@types/parse5" "*"
-    "@types/tough-cookie" "*"
-
-"@types/json-schema@*":
-  version "7.0.5"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
-  integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
-
-"@types/json-schema@^7.0.6":
-  version "7.0.6"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
-  integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
-
-"@types/json-schema@^7.0.7":
-  version "7.0.8"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818"
-  integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==
-
-"@types/json5@^0.0.29":
-  version "0.0.29"
-  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
-  integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
-
-"@types/jsonld@1.5.6":
-  version "1.5.6"
-  resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.6.tgz#4396c0b17128abf5773bb68b5453b88fc565b0d4"
-  integrity sha512-OUcfMjRie5IOrJulUQwVNvV57SOdKcTfBj3pjXNxzXqeOIrY2aGDNGW/Tlp83EQPkz4tCE6YWVrGuc/ZeaAQGg==
-
-"@types/katex@0.11.1":
-  version "0.11.1"
-  resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.1.tgz#34de04477dcf79e2ef6c8d23b41a3d81f9ebeaf5"
-  integrity sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==
-
-"@types/keygrip@*":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
-  integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==
-
-"@types/keyv@*":
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"
-  integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==
-  dependencies:
-    "@types/node" "*"
-
-"@types/koa-bodyparser@4.3.3":
-  version "4.3.3"
-  resolved "https://registry.yarnpkg.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.3.tgz#9c7d4295576bc863d550002f732f1c57dd88cc58"
-  integrity sha512-/ileIpXsy1fFEzgZhZ07eZH8rAVL7jwuk/kaoVEfauO6s80g2LIDIJKEyDbuAL9S/BWflKzEC0PHD6aXkmaSbw==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa-compose@*":
-  version "3.2.5"
-  resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d"
-  integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa-cors@0.0.2":
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/@types/koa-cors/-/koa-cors-0.0.2.tgz#369c753fb383640f225579c70a4f9a286b4931b7"
-  integrity sha512-uNaDY26HUVO+2C6arK8ZFODs9mBjYprD8mlvkVe2bYdX9wzEeKtycVXPafXpUkePhMh4sffIMkhRDyedokG/QA==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa-favicon@2.0.21":
-  version "2.0.21"
-  resolved "https://registry.yarnpkg.com/@types/koa-favicon/-/koa-favicon-2.0.21.tgz#d8a0ed062a6f5e3f838fe09c21e8b3f0490369cd"
-  integrity sha512-paH1nheVhijx/VduoR/RCD/qTCiX+OI/6fHLi3mZae053Ts+gUBOrKtzl3pMTDbdEBqdLolfLje3PZbb6jW0jQ==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa-logger@3.1.2":
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/@types/koa-logger/-/koa-logger-3.1.2.tgz#91e890f405ddb0626bc385767e4cc0cd7226d1a8"
-  integrity sha512-sioTA1xlKYiIgryANWPRHBkG3XGbWftw9slWADUPC+qvPIY/yRLSrhvX7zkJwMrntub5dPO0GuAoyGGf0yitfQ==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa-mount@4.0.1":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@types/koa-mount/-/koa-mount-4.0.1.tgz#2994be86eaa3d9dc97365e6ebfa227cee3c5f157"
-  integrity sha512-HNeg80CVS9Dfq8dGYqCZZCAUm7g6jPCNJ1ydqVLEJxLrjmeburpvq+lOZkE4rxBZ6O38dr3tj9IA3IfbdoI05w==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa-send@4.1.3":
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/@types/koa-send/-/koa-send-4.1.3.tgz#17193c6472ae9e5d1b99ae8086949cc4fd69179d"
-  integrity sha512-daaTqPZlgjIJycSTNjKpHYuKhXYP30atFc1pBcy6HHqB9+vcymDgYTguPdx9tO4HMOqNyz6bz/zqpxt5eLR+VA==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa-views@7.0.0":
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/@types/koa-views/-/koa-views-7.0.0.tgz#5613450c77ab69c980c47104378da4b7669c5f2e"
-  integrity sha512-AB/NB+oFHcLOZJYFv3bG5Af8YbwYCD9/zK0WcKALsbjI/FRKrcXTUTC64RebDrkyOkBm3bpCgpGndhAH/3YQ2Q==
-  dependencies:
-    koa-views "*"
-
-"@types/koa@*", "@types/koa@^2.13.1":
-  version "2.13.1"
-  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.1.tgz#e29877a6b5ad3744ab1024f6ec75b8cbf6ec45db"
-  integrity sha512-Qbno7FWom9nNqu0yHZ6A0+RWt4mrYBhw3wpBAQ3+IuzGcLlfeYkzZrnMq5wsxulN2np8M4KKeUpTodsOsSad5Q==
-  dependencies:
-    "@types/accepts" "*"
-    "@types/content-disposition" "*"
-    "@types/cookies" "*"
-    "@types/http-assert" "*"
-    "@types/http-errors" "*"
-    "@types/keygrip" "*"
-    "@types/koa-compose" "*"
-    "@types/node" "*"
-
-"@types/koa@2.13.4":
-  version "2.13.4"
-  resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
-  integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==
-  dependencies:
-    "@types/accepts" "*"
-    "@types/content-disposition" "*"
-    "@types/cookies" "*"
-    "@types/http-assert" "*"
-    "@types/http-errors" "*"
-    "@types/keygrip" "*"
-    "@types/koa-compose" "*"
-    "@types/node" "*"
-
-"@types/koa__cors@3.0.3":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-3.0.3.tgz#49d75813b443ba3d4da28ea6cf6244b7e99a3b23"
-  integrity sha512-74Xb4hJOPGKlrQ4PRBk1A/p0gfLpgbnpT0o67OMVbwyeMXvlBN+ZCRztAAmkKZs+8hKbgMutUlZVbA52Hr/0IA==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa__multer@2.0.4":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@types/koa__multer/-/koa__multer-2.0.4.tgz#e0f0fd1800a46b51886bebab480a57100f2488b0"
-  integrity sha512-WRkshXhE5rpYFUbbtAjyMhdOOSdbu1XX+2AQlRNM6AZtgxd0/WXMU4lrP7e9tk5HWVTWbx8DOOsVBmfHjSGJ4w==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/koa__router@8.0.8":
-  version "8.0.8"
-  resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-8.0.8.tgz#b1e0e9a512498777d3366bbdf0e853df27ec831c"
-  integrity sha512-9pGCaDtzCsj4HJ8HmGuqzk8+s57sPj4njWd08GG5o92n5Xp9io2snc40CPpXFhoKcZ8OKhuu6ht4gNou9e1C2w==
-  dependencies:
-    "@types/koa" "*"
-
-"@types/matter-js@0.17.6":
-  version "0.17.6"
-  resolved "https://registry.yarnpkg.com/@types/matter-js/-/matter-js-0.17.6.tgz#525bb33a7289105e1981ef51b987972fa9739011"
-  integrity sha512-i6WLNuM7/89SLqO2aOyaUkom9tc3B/qo4ekh7BD99xQ8+wOVVZO0F4RzKNYZCaFwr+xp3pK3oIb6sSVjLpz+pA==
-
-"@types/mime@*":
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
-  integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
-
 "@types/minimatch@*":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
-"@types/mocha@8.2.3":
-  version "8.2.3"
-  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
-  integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
-
-"@types/node-fetch@2.5.12":
-  version "2.5.12"
-  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66"
-  integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==
-  dependencies:
-    "@types/node" "*"
-    form-data "^3.0.0"
-
 "@types/node@*":
   version "16.6.2"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50"
   integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA==
 
-"@types/node@16.11.7":
-  version "16.11.7"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
-  integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==
-
 "@types/node@^14.11.8", "@types/node@^14.14.31", "@types/node@^14.14.41":
   version "14.17.9"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd"
   integrity sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g==
 
-"@types/nodemailer@6.4.4":
-  version "6.4.4"
-  resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
-  integrity sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==
-  dependencies:
-    "@types/node" "*"
-
-"@types/nprogress@0.2.0":
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f"
-  integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A==
-
-"@types/oauth@0.9.1":
-  version "0.9.1"
-  resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.1.tgz#e17221e7f7936b0459ae7d006255dff61adca305"
-  integrity sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==
-  dependencies:
-    "@types/node" "*"
-
-"@types/parse-json@^4.0.0":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
-  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
-
-"@types/parse5@*":
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
-  integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
-
-"@types/parse5@6.0.2":
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.2.tgz#99f6b72d82e34cea03a4d8f2ed72114d909c1c61"
-  integrity sha512-+hQX+WyJAOne7Fh3zF5CxPemILIbuhNcqHHodzK9caYOLnC8pD5efmPleRnw0z++LfKUC/sVNMwk0Gap+B0baA==
-
-"@types/parsimmon@1.10.6":
-  version "1.10.6"
-  resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.6.tgz#8fcf95990514d2a7624aa5f630c13bf2427f9cdd"
-  integrity sha512-FwAQwMRbkhx0J6YELkwIpciVzCcgEqXEbIrIn3a2P5d3kGEHQ3wVhlN3YdVepYP+bZzCYO6OjmD4o9TGOZ40rA==
-
-"@types/portscanner@2.1.1":
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/@types/portscanner/-/portscanner-2.1.1.tgz#89d5094e16f3d941f20f3889dfa5d3a164b3dd3b"
-  integrity sha512-1NsVIbgBKvrqxwtMN0V6CLji1ERwKSI/RWz0J3y++CzSwYNGBStCfpIFgxV3ZwxsDR5PoZqoUWhwraDm+Ztn0Q==
-
-"@types/pug@2.0.5":
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.5.tgz#69bc700934dd473c7ab97270bd2dbacefe562231"
-  integrity sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==
-
-"@types/punycode@2.1.0":
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83"
-  integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g==
-
-"@types/q@^1.5.1":
-  version "1.5.2"
-  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
-  integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
-
-"@types/qrcode@1.4.1":
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.1.tgz#0689f400c3a95d2db040c99c99834faa09ee9dc1"
-  integrity sha512-vxMyr7JM7tYPxu8vUE83NiosWX5DZieCyYeJRoOIg0pAkyofCBzknJ2ycUZkPGDFis2RS8GN/BeJLnRnAPxeCA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/qs@*":
-  version "6.9.1"
-  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7"
-  integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==
-
-"@types/random-seed@0.3.3":
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/@types/random-seed/-/random-seed-0.3.3.tgz#7741f7b0a4513198a9396ce4ad25832f799a6727"
-  integrity sha512-kHsCbIRHNXJo6EN5W8EA5b4i1hdT6jaZke5crBPLUcLqaLdZ0QBq8QVMbafHzhjFF83Cl9qlee2dChD18d/kPg==
-
-"@types/range-parser@*":
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
-  integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
-
-"@types/ratelimiter@3.4.2":
-  version "3.4.2"
-  resolved "https://registry.yarnpkg.com/@types/ratelimiter/-/ratelimiter-3.4.2.tgz#adf1a6d0cbe72d42207efc510a9170602e23456c"
-  integrity sha512-iz+yyY+ViphaM8ZwrX1mUQzelIeC59zyaaLKTJ0YVOOCkCpIYpaysiIM4z5Xv9HdXYqIb80S+DhH7J22A0rW2w==
-
-"@types/readable-stream@^2.3.9":
-  version "2.3.9"
-  resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.9.tgz#40a8349e6ace3afd2dd1b6d8e9b02945de4566a9"
-  integrity sha512-sqsgQqFT7HmQz/V5jH1O0fvQQnXAJO46Gg9LRO/JPfjmVmGUlcx831TZZO3Y3HtWhIkzf3kTsNT0Z0kzIhIvZw==
-  dependencies:
-    "@types/node" "*"
-    safe-buffer "*"
-
-"@types/redis@2.8.32":
-  version "2.8.32"
-  resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11"
-  integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==
-  dependencies:
-    "@types/node" "*"
-
-"@types/rename@1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@types/rename/-/rename-1.0.4.tgz#30c6f0306042591a560361ea02639e89647dd173"
-  integrity sha512-eV81+6bVv2mdCBahkMefjEUwAjKDAP3AuyhqWCWRxcRaeVdUeHUBaoq2zSz+5HNHF2jzTajMcfLvJsy4K3cbwA==
-
-"@types/request-stats@3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/request-stats/-/request-stats-3.0.0.tgz#d3909a9f778b8ae0b42fb8c1ed20cb936ed95f99"
-  integrity sha512-POsDF7nETH8up49iBNvbZuO0pEk9F+TG0rXCkvjxCClcOS99xfF+mKmJteYlwKYpuRKkixzysKlL8rwN1hU2lw==
-  dependencies:
-    "@types/node" "*"
-
-"@types/responselike@*", "@types/responselike@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
-  integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/rimraf@3.0.2":
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8"
-  integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==
-  dependencies:
-    "@types/glob" "*"
-    "@types/node" "*"
-
-"@types/rsvp@^4.0.4":
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/@types/rsvp/-/rsvp-4.0.4.tgz#55e93e7054027f1ad4b4ebc1e60e59eb091e2d32"
-  integrity sha512-J3Ol++HCC7/hwZhanDvggFYU/GtxHxE/e7cGRWxR04BF7Tt3TqJZ84BkzQgDxmX0uu8IagiyfmfoUlBACh2Ilg==
-
-"@types/seedrandom@2.4.28":
-  version "2.4.28"
-  resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f"
-  integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA==
-
-"@types/serve-static@*":
-  version "1.13.3"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1"
-  integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==
-  dependencies:
-    "@types/express-serve-static-core" "*"
-    "@types/mime" "*"
-
-"@types/sharp@0.29.3":
-  version "0.29.3"
-  resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.3.tgz#54ceb81b68bb99a0e62db2b52f4fb0bea84878ac"
-  integrity sha512-83Xp05eK2hvfNnmKLr2Fz0C2A0jrr2TnSLqKRbkLTYuAu+Erj6mKQLoEMGafE73Om8p3q3ryZxtHFM/7hy4Adg==
-  dependencies:
-    "@types/node" "*"
-
-"@types/sinonjs__fake-timers@6.0.4":
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
-  integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
-
 "@types/sinonjs__fake-timers@^6.0.2":
   version "6.0.3"
   resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.3.tgz#79df6f358ae8f79e628fe35a63608a0ea8e7cf08"
@@ -1037,50 +175,6 @@
   resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
   integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
 
-"@types/source-list-map@*":
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
-  integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
-
-"@types/speakeasy@2.0.6":
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/@types/speakeasy/-/speakeasy-2.0.6.tgz#12540f7b64d08180393ae2c5c8c280866a85da61"
-  integrity sha512-2wIXZp5yJUddhsSZarYCZIakCvzwQgTVdtT29DYVdFzc0cHttanaQx9THRhtjY4kDqVaF2jhyFOEofozOioFdQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/tapable@^1":
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4"
-  integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ==
-
-"@types/throttle-debounce@2.1.0":
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776"
-  integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
-
-"@types/tinycolor2@1.4.3":
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706"
-  integrity sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==
-
-"@types/tmp@0.2.2":
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.2.tgz#424537a3b91828cb26aaf697f21ae3cd1b69f7e7"
-  integrity sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==
-
-"@types/tough-cookie@*":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
-  integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
-
-"@types/uglify-js@*":
-  version "3.9.0"
-  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.0.tgz#4490a140ca82aa855ad68093829e7fd6ae94ea87"
-  integrity sha512-3ZcoyPYHVOCcLpnfZwD47KFLr8W/mpUcgjpf1M4Q78TMJIw7KMAHSjiCLJp1z3ZrBR9pTLbe191O0TldFK5zcw==
-  dependencies:
-    source-map "^0.6.1"
-
 "@types/undertaker-registry@*":
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz#4306d4a03d7acedb974b66530832b90729e1d1da"
@@ -1093,11 +187,6 @@
   dependencies:
     "@types/undertaker-registry" "*"
 
-"@types/uuid@8.3.1":
-  version "8.3.1"
-  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
-  integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
-
 "@types/vinyl-fs@*":
   version "2.4.11"
   resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-2.4.11.tgz#b98119b8bb2494141eaf649b09fbfeb311161206"
@@ -1115,65 +204,6 @@
     "@types/expect" "^1.20.4"
     "@types/node" "*"
 
-"@types/web-push@3.3.2":
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.3.2.tgz#8c32434147c0396415862e86405c9edc9c50fc15"
-  integrity sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==
-  dependencies:
-    "@types/node" "*"
-
-"@types/webpack-sources@*":
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.7.tgz#0a330a9456113410c74a5d64180af0cbca007141"
-  integrity sha512-XyaHrJILjK1VHVC4aVlKsdNN5KBTwufMb43cQs+flGxtPAf/1Qwl8+Q0tp5BwEGaI8D6XT1L+9bSWXckgkjTLw==
-  dependencies:
-    "@types/node" "*"
-    "@types/source-list-map" "*"
-    source-map "^0.6.1"
-
-"@types/webpack-stream@3.2.12":
-  version "3.2.12"
-  resolved "https://registry.yarnpkg.com/@types/webpack-stream/-/webpack-stream-3.2.12.tgz#cf13e64067a662a7acd8cd0524b3f64c86b0ecb6"
-  integrity sha512-znMUl4kKT0V0SwkUgRgwUNSAO7J5I/jdTCBNy3utkCsgMJ3IHp4FBTDwsQC+tfQ73TWeKIH05QNmbUYmeGThGw==
-  dependencies:
-    "@types/node" "*"
-    "@types/webpack" "^4"
-
-"@types/webpack@5.28.0":
-  version "5.28.0"
-  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03"
-  integrity sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==
-  dependencies:
-    "@types/node" "*"
-    tapable "^2.2.0"
-    webpack "^5"
-
-"@types/webpack@^4":
-  version "4.41.27"
-  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.27.tgz#f47da488c8037e7f1b2dbf2714fbbacb61ec0ffc"
-  integrity sha512-wK/oi5gcHi72VMTbOaQ70VcDxSQ1uX8S2tukBK9ARuGXrYM/+u4ou73roc7trXDNmCxCoerE8zruQqX/wuHszA==
-  dependencies:
-    "@types/anymatch" "*"
-    "@types/node" "*"
-    "@types/tapable" "^1"
-    "@types/uglify-js" "*"
-    "@types/webpack-sources" "*"
-    source-map "^0.6.0"
-
-"@types/websocket@1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8"
-  integrity sha512-qn1LkcFEKK8RPp459jkjzsfpbsx36BBt3oC3pITYtkoBw/aVX+EZFa5j3ThCRTNpLFvIMr5dSTD4RaMdilIOpA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/ws@8.2.0":
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.0.tgz#75faefbe2328f3b833cb8dc640658328990d04f3"
-  integrity sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==
-  dependencies:
-    "@types/node" "*"
-
 "@types/yauzl@^2.9.1":
   version "2.9.2"
   resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a"
@@ -1181,513 +211,6 @@
   dependencies:
     "@types/node" "*"
 
-"@types/zen-observable@^0.8.2":
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71"
-  integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg==
-
-"@typescript-eslint/parser@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.1.0.tgz#6c7f837d210d2bc0a811e7ea742af414f4e00908"
-  integrity sha512-vx1P+mhCtYw3+bRHmbalq/VKP2Y3gnzNgxGxfEWc6OFpuEL7iQdAeq11Ke3Rhy8NjgB+AHsIWEwni3e+Y7djKA==
-  dependencies:
-    "@typescript-eslint/scope-manager" "5.1.0"
-    "@typescript-eslint/types" "5.1.0"
-    "@typescript-eslint/typescript-estree" "5.1.0"
-    debug "^4.3.2"
-
-"@typescript-eslint/scope-manager@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.1.0.tgz#6f1f26ad66a8f71bbb33b635e74fec43f76b44df"
-  integrity sha512-yYlyVjvn5lvwCL37i4hPsa1s0ORsjkauhTqbb8MnpvUs7xykmcjGqwlNZ2Q5QpoqkJ1odlM2bqHqJwa28qV6Tw==
-  dependencies:
-    "@typescript-eslint/types" "5.1.0"
-    "@typescript-eslint/visitor-keys" "5.1.0"
-
-"@typescript-eslint/types@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.1.0.tgz#a8a75ddfc611660de6be17d3ad950302385607a9"
-  integrity sha512-sEwNINVxcB4ZgC6Fe6rUyMlvsB2jvVdgxjZEjQUQVlaSPMNamDOwO6/TB98kFt4sYYfNhdhTPBEQqNQZjMMswA==
-
-"@typescript-eslint/typescript-estree@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.1.0.tgz#132aea34372df09decda961cb42457433aa6e83d"
-  integrity sha512-SSz+l9YrIIsW4s0ZqaEfnjl156XQ4VRmJsbA0ZE1XkXrD3cRpzuZSVCyqeCMR3EBjF27IisWakbBDGhGNIOvfQ==
-  dependencies:
-    "@typescript-eslint/types" "5.1.0"
-    "@typescript-eslint/visitor-keys" "5.1.0"
-    debug "^4.3.2"
-    globby "^11.0.4"
-    is-glob "^4.0.3"
-    semver "^7.3.5"
-    tsutils "^3.21.0"
-
-"@typescript-eslint/visitor-keys@5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.1.0.tgz#e01a01b27eb173092705ae983aa1451bd1842630"
-  integrity sha512-uqNXepKBg81JVwjuqAxYrXa1Ql/YDzM+8g/pS+TCPxba0wZttl8m5DkrasbfnmJGHs4lQ2jTbcZ5azGhI7kK+w==
-  dependencies:
-    "@typescript-eslint/types" "5.1.0"
-    eslint-visitor-keys "^3.0.0"
-
-"@ungap/promise-all-settled@1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
-  integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
-
-"@vue/compiler-core@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.21.tgz#26566c32b2ad838199d471ef5df620a83846f24e"
-  integrity sha512-NhhiQZNG71KNq1h5pMW/fAXdTF7lJRaSI7LDm2edhHXVz1ROMICo8SreUmQnSf4Fet0UPBVqJ988eF4+936iDQ==
-  dependencies:
-    "@babel/parser" "^7.15.0"
-    "@vue/shared" "3.2.21"
-    estree-walker "^2.0.2"
-    source-map "^0.6.1"
-
-"@vue/compiler-dom@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.21.tgz#d6f6c85364ef8888f9c4e9122bfba11e78fb398c"
-  integrity sha512-gsJD3DpYZSYquiA7UIPsMDSlAooYWDvHPq9VRsqzJEk2PZtFvLvHPb4aaMD8Ufd62xzYn32cnnkzsEOJhyGilA==
-  dependencies:
-    "@vue/compiler-core" "3.2.21"
-    "@vue/shared" "3.2.21"
-
-"@vue/compiler-sfc@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.21.tgz#42639ee49e725afb7d8f1d1940e75dc17a56002c"
-  integrity sha512-+yDlUSebKpz/ovxM2vLRRx7w/gVfY767pOfYTgbIhAs+ogvIV2BsIt4fpxlThnlCNChJ+yE0ERUNoROv2kEGEQ==
-  dependencies:
-    "@babel/parser" "^7.15.0"
-    "@vue/compiler-core" "3.2.21"
-    "@vue/compiler-dom" "3.2.21"
-    "@vue/compiler-ssr" "3.2.21"
-    "@vue/ref-transform" "3.2.21"
-    "@vue/shared" "3.2.21"
-    estree-walker "^2.0.2"
-    magic-string "^0.25.7"
-    postcss "^8.1.10"
-    source-map "^0.6.1"
-
-"@vue/compiler-ssr@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.21.tgz#37d124f89e8adef9fd56b85775de4b5310a0436e"
-  integrity sha512-eU+A0iWYy+1zAo2CRIJ0zSVlv1iuGAIbNRCnllSJ31pV1lX3jypJYzGbJlSRAbB7VP6E+tYveVT1Oq8JKewa3g==
-  dependencies:
-    "@vue/compiler-dom" "3.2.21"
-    "@vue/shared" "3.2.21"
-
-"@vue/reactivity@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.21.tgz#073ad144192ed78a07e151e95a3baa515e4099a2"
-  integrity sha512-7C57zFm/5E3SSTUhVuYj1InDwuJ+GIVQ/z+H43C9sST85gIThGXVhksl1yWTAadf8Yz4T5lSbqi5Ds8U/ueWcw==
-  dependencies:
-    "@vue/shared" "3.2.21"
-
-"@vue/ref-transform@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/ref-transform/-/ref-transform-3.2.21.tgz#b0c554c9f640c3f005f77e676066aa0faba90984"
-  integrity sha512-uiEWWBsrGeun9O7dQExYWzXO3rHm/YdtFNXDVqCSoPypzOVxWxdiL+8hHeWzxMB58fVuV2sT80aUtIVyaBVZgQ==
-  dependencies:
-    "@babel/parser" "^7.15.0"
-    "@vue/compiler-core" "3.2.21"
-    "@vue/shared" "3.2.21"
-    estree-walker "^2.0.2"
-    magic-string "^0.25.7"
-
-"@vue/runtime-core@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.21.tgz#e12dac8c3893b7aebfc37e32066718d8aa686ac5"
-  integrity sha512-7oOxKaU0D2IunOAMOOHZgJVrHg63xwng8BZx3fbgmakqEIMwHhQcp+5GV1sOg/sWW7R4UhaRDIUCukO2GRVK2Q==
-  dependencies:
-    "@vue/reactivity" "3.2.21"
-    "@vue/shared" "3.2.21"
-
-"@vue/runtime-dom@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.21.tgz#33dd15bc85281e773177a30dc6931c294bd77aa1"
-  integrity sha512-apBdriD6QsI4ywbllY8kjr9/0scGuStDuvLbJULPQkFPtHzntd51bP5PQTQVAEIc9kwnTozmj6x6ZdX/cwo7xA==
-  dependencies:
-    "@vue/runtime-core" "3.2.21"
-    "@vue/shared" "3.2.21"
-    csstype "^2.6.8"
-
-"@vue/server-renderer@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.21.tgz#887d0a44de76f72313cff2686a24c0315231d634"
-  integrity sha512-QBgYqVgI7XCSBCqGa4LduV9vpfQFdZBOodFmq5Txk5W/v1KrJ1LoOh2Q0RHiRgtoK/UR9uyvRVcYqOmwHkZNEg==
-  dependencies:
-    "@vue/compiler-ssr" "3.2.21"
-    "@vue/shared" "3.2.21"
-
-"@vue/shared@3.2.21":
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.21.tgz#4cd80c0e62cf65a7adab2449e86b6f0cb33a130b"
-  integrity sha512-5EQmIPK6gw4UVYUbM959B0uPsJ58+xoMESCZs3N89XyvJ9e+fX4pqEPrOGV8OroIk3SbEvJcC+eYc8BH9JQrHA==
-
-"@webassemblyjs/ast@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f"
-  integrity sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==
-  dependencies:
-    "@webassemblyjs/helper-numbers" "1.11.0"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
-
-"@webassemblyjs/ast@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
-  integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==
-  dependencies:
-    "@webassemblyjs/helper-numbers" "1.11.1"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
-
-"@webassemblyjs/floating-point-hex-parser@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz#34d62052f453cd43101d72eab4966a022587947c"
-  integrity sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==
-
-"@webassemblyjs/floating-point-hex-parser@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f"
-  integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==
-
-"@webassemblyjs/helper-api-error@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz#aaea8fb3b923f4aaa9b512ff541b013ffb68d2d4"
-  integrity sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==
-
-"@webassemblyjs/helper-api-error@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16"
-  integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==
-
-"@webassemblyjs/helper-buffer@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz#d026c25d175e388a7dbda9694e91e743cbe9b642"
-  integrity sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==
-
-"@webassemblyjs/helper-buffer@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5"
-  integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==
-
-"@webassemblyjs/helper-numbers@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz#7ab04172d54e312cc6ea4286d7d9fa27c88cd4f9"
-  integrity sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==
-  dependencies:
-    "@webassemblyjs/floating-point-hex-parser" "1.11.0"
-    "@webassemblyjs/helper-api-error" "1.11.0"
-    "@xtuc/long" "4.2.2"
-
-"@webassemblyjs/helper-numbers@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae"
-  integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==
-  dependencies:
-    "@webassemblyjs/floating-point-hex-parser" "1.11.1"
-    "@webassemblyjs/helper-api-error" "1.11.1"
-    "@xtuc/long" "4.2.2"
-
-"@webassemblyjs/helper-wasm-bytecode@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz#85fdcda4129902fe86f81abf7e7236953ec5a4e1"
-  integrity sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==
-
-"@webassemblyjs/helper-wasm-bytecode@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1"
-  integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==
-
-"@webassemblyjs/helper-wasm-section@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz#9ce2cc89300262509c801b4af113d1ca25c1a75b"
-  integrity sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.0"
-    "@webassemblyjs/helper-buffer" "1.11.0"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
-    "@webassemblyjs/wasm-gen" "1.11.0"
-
-"@webassemblyjs/helper-wasm-section@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a"
-  integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.1"
-    "@webassemblyjs/helper-buffer" "1.11.1"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
-    "@webassemblyjs/wasm-gen" "1.11.1"
-
-"@webassemblyjs/ieee754@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz#46975d583f9828f5d094ac210e219441c4e6f5cf"
-  integrity sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==
-  dependencies:
-    "@xtuc/ieee754" "^1.2.0"
-
-"@webassemblyjs/ieee754@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614"
-  integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==
-  dependencies:
-    "@xtuc/ieee754" "^1.2.0"
-
-"@webassemblyjs/leb128@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.0.tgz#f7353de1df38aa201cba9fb88b43f41f75ff403b"
-  integrity sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==
-  dependencies:
-    "@xtuc/long" "4.2.2"
-
-"@webassemblyjs/leb128@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5"
-  integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==
-  dependencies:
-    "@xtuc/long" "4.2.2"
-
-"@webassemblyjs/utf8@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.0.tgz#86e48f959cf49e0e5091f069a709b862f5a2cadf"
-  integrity sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==
-
-"@webassemblyjs/utf8@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff"
-  integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==
-
-"@webassemblyjs/wasm-edit@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz#ee4a5c9f677046a210542ae63897094c2027cb78"
-  integrity sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.0"
-    "@webassemblyjs/helper-buffer" "1.11.0"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
-    "@webassemblyjs/helper-wasm-section" "1.11.0"
-    "@webassemblyjs/wasm-gen" "1.11.0"
-    "@webassemblyjs/wasm-opt" "1.11.0"
-    "@webassemblyjs/wasm-parser" "1.11.0"
-    "@webassemblyjs/wast-printer" "1.11.0"
-
-"@webassemblyjs/wasm-edit@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6"
-  integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.1"
-    "@webassemblyjs/helper-buffer" "1.11.1"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
-    "@webassemblyjs/helper-wasm-section" "1.11.1"
-    "@webassemblyjs/wasm-gen" "1.11.1"
-    "@webassemblyjs/wasm-opt" "1.11.1"
-    "@webassemblyjs/wasm-parser" "1.11.1"
-    "@webassemblyjs/wast-printer" "1.11.1"
-
-"@webassemblyjs/wasm-gen@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe"
-  integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.0"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
-    "@webassemblyjs/ieee754" "1.11.0"
-    "@webassemblyjs/leb128" "1.11.0"
-    "@webassemblyjs/utf8" "1.11.0"
-
-"@webassemblyjs/wasm-gen@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76"
-  integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.1"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
-    "@webassemblyjs/ieee754" "1.11.1"
-    "@webassemblyjs/leb128" "1.11.1"
-    "@webassemblyjs/utf8" "1.11.1"
-
-"@webassemblyjs/wasm-opt@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz#1638ae188137f4bb031f568a413cd24d32f92978"
-  integrity sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.0"
-    "@webassemblyjs/helper-buffer" "1.11.0"
-    "@webassemblyjs/wasm-gen" "1.11.0"
-    "@webassemblyjs/wasm-parser" "1.11.0"
-
-"@webassemblyjs/wasm-opt@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2"
-  integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.1"
-    "@webassemblyjs/helper-buffer" "1.11.1"
-    "@webassemblyjs/wasm-gen" "1.11.1"
-    "@webassemblyjs/wasm-parser" "1.11.1"
-
-"@webassemblyjs/wasm-parser@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz#3e680b8830d5b13d1ec86cc42f38f3d4a7700754"
-  integrity sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.0"
-    "@webassemblyjs/helper-api-error" "1.11.0"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.0"
-    "@webassemblyjs/ieee754" "1.11.0"
-    "@webassemblyjs/leb128" "1.11.0"
-    "@webassemblyjs/utf8" "1.11.0"
-
-"@webassemblyjs/wasm-parser@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199"
-  integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.1"
-    "@webassemblyjs/helper-api-error" "1.11.1"
-    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
-    "@webassemblyjs/ieee754" "1.11.1"
-    "@webassemblyjs/leb128" "1.11.1"
-    "@webassemblyjs/utf8" "1.11.1"
-
-"@webassemblyjs/wast-printer@1.11.0":
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz#680d1f6a5365d6d401974a8e949e05474e1fab7e"
-  integrity sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.0"
-    "@xtuc/long" "4.2.2"
-
-"@webassemblyjs/wast-printer@1.11.1":
-  version "1.11.1"
-  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0"
-  integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==
-  dependencies:
-    "@webassemblyjs/ast" "1.11.1"
-    "@xtuc/long" "4.2.2"
-
-"@webpack-cli/configtest@^1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.1.0.tgz#8342bef0badfb7dfd3b576f2574ab80c725be043"
-  integrity sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==
-
-"@webpack-cli/info@^1.4.0":
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.4.0.tgz#b9179c3227ab09cbbb149aa733475fcf99430223"
-  integrity sha512-F6b+Man0rwE4n0409FyAJHStYA5OIZERxmnUfLVwv0mc0V1wLad3V7jqRlMkgKBeAq07jUvglacNaa6g9lOpuw==
-  dependencies:
-    envinfo "^7.7.3"
-
-"@webpack-cli/serve@^1.6.0":
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.6.0.tgz#2c275aa05c895eccebbfc34cfb223c6e8bd591a2"
-  integrity sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==
-
-"@xtuc/ieee754@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
-  integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
-
-"@xtuc/long@4.2.2":
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
-  integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
-
-abab@^2.0.3, abab@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
-  integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
-
-abbrev@1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
-  integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
-
-abort-controller@3.0.0, abort-controller@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
-  integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
-  dependencies:
-    event-target-shim "^5.0.0"
-
-accepts@^1.3.5:
-  version "1.3.7"
-  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
-  integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
-  dependencies:
-    mime-types "~2.1.24"
-    negotiator "0.6.2"
-
-acorn-globals@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
-  integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==
-  dependencies:
-    acorn "^7.1.1"
-    acorn-walk "^7.1.1"
-
-acorn-import-assertions@^1.7.6:
-  version "1.7.6"
-  resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz#580e3ffcae6770eebeec76c3b9723201e9d01f78"
-  integrity sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==
-
-acorn-jsx@^5.3.1:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
-  integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
-
-acorn-walk@^7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
-  integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
-
-acorn-walk@^8.1.1:
-  version "8.1.1"
-  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.1.tgz#3ddab7f84e4a7e2313f6c414c5b7dac85f4e3ebc"
-  integrity sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==
-
-acorn@^7.1.1:
-  version "7.4.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
-  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-
-acorn@^8.0.4:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe"
-  integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==
-
-acorn@^8.2.4:
-  version "8.2.4"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.4.tgz#caba24b08185c3b56e3168e97d15ed17f4d31fd0"
-  integrity sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg==
-
-acorn@^8.4.1:
-  version "8.4.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
-  integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
-
-acorn@^8.5.0:
-  version "8.5.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
-  integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
-
-agent-base@6:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
-  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
-  dependencies:
-    debug "4"
-
-agentkeepalive@^4.1.3:
-  version "4.1.4"
-  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b"
-  integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ==
-  dependencies:
-    debug "^4.1.0"
-    depd "^1.1.2"
-    humanize-ms "^1.2.1"
-
 aggregate-error@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@@ -1696,12 +219,7 @@ aggregate-error@^3.0.0:
     clean-stack "^2.0.0"
     indent-string "^4.0.0"
 
-ajv-keywords@^3.5.2:
-  version "3.5.2"
-  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
-  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
-
-ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5:
+ajv@^6.5.5:
   version "6.12.5"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
   integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
@@ -1716,11 +234,6 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
   integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
 
-ansi-colors@4.1.1, ansi-colors@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
-  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
-
 ansi-colors@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9"
@@ -1728,6 +241,11 @@ ansi-colors@^1.0.1:
   dependencies:
     ansi-wrap "^0.1.0"
 
+ansi-colors@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
 ansi-escapes@^4.3.0:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@@ -1747,38 +265,16 @@ ansi-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
   integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
 
-ansi-regex@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
-  integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
-
-ansi-regex@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
-  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
-
 ansi-regex@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
   integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
 
-ansi-regex@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
-  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
 ansi-styles@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
   integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
 
-ansi-styles@^3.2.0, ansi-styles@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
-  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
-  dependencies:
-    color-convert "^1.9.0"
-
 ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
@@ -1792,11 +288,6 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
   resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
   integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768=
 
-any-promise@^1.0.0, any-promise@^1.1.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
-  integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
-
 anymatch@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
@@ -1813,11 +304,6 @@ anymatch@~3.1.1:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
-app-root-path@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad"
-  integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==
-
 append-buffer@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1"
@@ -1825,16 +311,6 @@ append-buffer@^1.0.2:
   dependencies:
     buffer-equal "^1.0.0"
 
-append-field@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
-  integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=
-
-aproba@^1.0.3:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
-  integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
-
 arch@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
@@ -1845,19 +321,6 @@ archy@^1.0.0:
   resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
   integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
 
-are-we-there-yet@~1.1.2:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
-  integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
-  dependencies:
-    delegates "^1.0.0"
-    readable-stream "^2.0.6"
-
-arg@^4.1.0:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
-  integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
-
 argparse@^1.0.7:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -1865,11 +328,6 @@ argparse@^1.0.7:
   dependencies:
     sprintf-js "~1.0.2"
 
-argparse@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
-  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
-
 arr-diff@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -1933,36 +391,11 @@ array-sort@^1.0.0:
     get-value "^2.0.6"
     kind-of "^5.0.2"
 
-array-timsort@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926"
-  integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==
-
-array-union@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
-  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
-
 array-unique@^0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
   integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 
-asap@~2.0.3:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
-  integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
-
-asn1.js@^5.3.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.3.0.tgz#439099fe9174e09cff5a54a9dda70260517e8689"
-  integrity sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==
-  dependencies:
-    bn.js "^4.0.0"
-    inherits "^2.0.1"
-    minimalistic-assert "^1.0.0"
-    safer-buffer "^2.1.0"
-
 asn1@~0.2.3:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@@ -1970,11 +403,6 @@ asn1@~0.2.3:
   dependencies:
     safer-buffer "~2.1.0"
 
-assert-never@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe"
-  integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==
-
 assert-plus@1.0.0, assert-plus@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
@@ -2000,6 +428,11 @@ async-done@^1.2.0, async-done@^1.2.2:
     process-nextick-args "^2.0.0"
     stream-exhaust "^1.0.1"
 
+async-each@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
+  integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
+
 async-settle@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b"
@@ -2007,18 +440,6 @@ async-settle@^1.0.0:
   dependencies:
     async-done "^1.2.2"
 
-async@>=0.2.9:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
-  integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
-
-async@^2.6.0:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
-  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
-  dependencies:
-    lodash "^4.17.14"
-
 async@^3.2.0:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
@@ -2039,11 +460,6 @@ atob@^2.1.2:
   resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
   integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
 
-autobind-decorator@2.4.0, autobind-decorator@^2.4.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.4.0.tgz#ea9e1c98708cf3b5b356f7cf9f10f265ff18239c"
-  integrity sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw==
-
 autoprefixer@^6.3.1:
   version "6.7.7"
   resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
@@ -2056,33 +472,6 @@ autoprefixer@^6.3.1:
     postcss "^5.2.16"
     postcss-value-parser "^3.2.3"
 
-autosize@4.0.4:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03"
-  integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==
-
-autwh@0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/autwh/-/autwh-0.1.0.tgz#24a5300923309d105133401a2568f9c8ab7d7e03"
-  integrity sha512-IkGZ4kjVlZMkEmDiVtZpGG3lDGHPqsMBIh4IpQKN7idYOJ5EGedqKPO+ychNqh8zrJEEqYsN0NcBkcmoE2uFAw==
-  dependencies:
-    oauth "0.9.15"
-
-aws-sdk@2.1013.0:
-  version "2.1013.0"
-  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1013.0.tgz#85babc473b0bc90cc1160eb48baf616ddb86e346"
-  integrity sha512-TXxkp/meAdofpC15goFpNuur7fvh/mcMRfHJoP1jYzTtD0wcoB4FK16GLcny0uDYgkQgZuiO9QYv3Rq5bhGCqQ==
-  dependencies:
-    buffer "4.9.2"
-    events "1.1.1"
-    ieee754 "1.1.13"
-    jmespath "0.15.0"
-    querystring "0.2.0"
-    sax "1.2.1"
-    url "0.10.3"
-    uuid "3.3.2"
-    xml2js "0.4.19"
-
 aws-sign2@~0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -2093,13 +482,6 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
   integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
 
-axios@^0.19.2:
-  version "0.19.2"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
-  integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
-  dependencies:
-    follow-redirects "1.5.10"
-
 axios@^0.21.1:
   version "0.21.1"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
@@ -2107,13 +489,6 @@ axios@^0.21.1:
   dependencies:
     follow-redirects "^1.10.0"
 
-babel-walk@3.0.0-canary-5:
-  version "3.0.0-canary-5"
-  resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11"
-  integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==
-  dependencies:
-    "@babel/types" "^7.9.6"
-
 bach@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880"
@@ -2139,21 +514,6 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
-base32.js@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.0.1.tgz#d045736a57b1f6c139f0c7df42518a84e91bb2ba"
-  integrity sha1-0EVzalex9sE58MffQlGKhOkbsro=
-
-base64-js@^1.0.2:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
-  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
-
-base64-js@^1.3.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
-  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
 base@^0.11.1:
   version "0.11.2"
   resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@@ -2174,20 +534,10 @@ bcrypt-pbkdf@^1.0.0:
   dependencies:
     tweetnacl "^0.14.3"
 
-bcryptjs@2.4.3:
-  version "2.4.3"
-  resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
-  integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
-
-big-integer@^1.6.16:
-  version "1.6.48"
-  resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
-  integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
-
-big.js@^5.2.2:
-  version "5.2.2"
-  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
-  integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+binary-extensions@^1.0.0:
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
+  integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
 
 binary-extensions@^2.0.0:
   version "2.0.0"
@@ -2199,14 +549,12 @@ binaryextensions@^2.2.0:
   resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22"
   integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==
 
-bl@^4.0.1, bl@^4.0.3:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
-  integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==
+bindings@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
   dependencies:
-    buffer "^5.5.0"
-    inherits "^2.0.4"
-    readable-stream "^3.4.0"
+    file-uri-to-path "1.0.0"
 
 blob-util@^2.0.2:
   version "2.0.2"
@@ -2218,21 +566,6 @@ bluebird@3.7.2, bluebird@^3.7.2:
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
-blurhash@1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244"
-  integrity sha512-MXIPz6zwYUKayju+Uidf83KhH0vodZfeRl6Ich8Gu+KGl0JgKiFq9LsfqV7cVU5fKD/AotmduZqvOfrGKOfTaA==
-
-bn.js@^4.0.0:
-  version "4.11.8"
-  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
-  integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
-
-boolbase@^1.0.0, boolbase@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
-  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
-
 brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -2241,7 +574,7 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-braces@^2.3.1:
+braces@^2.3.1, braces@^2.3.2:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
   integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
@@ -2257,36 +590,13 @@ braces@^2.3.1:
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
-braces@^3.0.1, braces@~3.0.2:
+braces@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
   integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
   dependencies:
     fill-range "^7.0.1"
 
-broadcast-channel@4.5.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.5.0.tgz#d4717c493e219908fcb7f2f9078fe0baf95b77c1"
-  integrity sha512-jp+VPlQ1HyR0CM3uIYUrdpXupBvhTMFRkjR6mEmt5W4HaGDPFEzrO2Jqvi2PZ6zCC4zwLeco7CC5EUJPrVH8Tw==
-  dependencies:
-    "@babel/runtime" "^7.16.0"
-    detect-node "^2.1.0"
-    microseconds "0.2.0"
-    nano-time "1.0.0"
-    oblivious-set "1.0.0"
-    rimraf "3.0.2"
-    unload "2.3.1"
-
-browser-process-hrtime@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
-  integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
-
-browser-stdout@1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
-  integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
-
 browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
   version "1.7.7"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9"
@@ -2295,183 +605,21 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
     caniuse-db "^1.0.30000639"
     electron-to-chromium "^1.2.7"
 
-browserslist@^4.0.0, browserslist@^4.14.5:
-  version "4.16.3"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
-  integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
-  dependencies:
-    caniuse-lite "^1.0.30001181"
-    colorette "^1.2.1"
-    electron-to-chromium "^1.3.649"
-    escalade "^3.1.1"
-    node-releases "^1.1.70"
-
-browserslist@^4.16.0:
-  version "4.16.4"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.4.tgz#7ebf913487f40caf4637b892b268069951c35d58"
-  integrity sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==
-  dependencies:
-    caniuse-lite "^1.0.30001208"
-    colorette "^1.2.2"
-    electron-to-chromium "^1.3.712"
-    escalade "^3.1.1"
-    node-releases "^1.1.71"
-
-browserslist@^4.16.6:
-  version "4.16.6"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
-  integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
-  dependencies:
-    caniuse-lite "^1.0.30001219"
-    colorette "^1.2.2"
-    electron-to-chromium "^1.3.723"
-    escalade "^3.1.1"
-    node-releases "^1.1.71"
-
-bs-logger@0.x:
-  version "0.2.6"
-  resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
-  integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
-  dependencies:
-    fast-json-stable-stringify "2.x"
-
-buffer-alloc-unsafe@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
-  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
-
-buffer-alloc@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
-  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
-  dependencies:
-    buffer-alloc-unsafe "^1.1.0"
-    buffer-fill "^1.0.0"
-
 buffer-crc32@~0.2.3:
   version "0.2.13"
   resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
   integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
 
-buffer-equal-constant-time@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
-  integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
-
 buffer-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe"
   integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74=
 
-buffer-fill@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
-  integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
-
-buffer-from@1.x:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
-  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
-
-buffer-from@^1.0.0, buffer-from@^1.1.1:
+buffer-from@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
-buffer-writer@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
-  integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
-
-buffer@4.9.2:
-  version "4.9.2"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
-  integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
-  dependencies:
-    base64-js "^1.0.2"
-    ieee754 "^1.1.4"
-    isarray "^1.0.0"
-
-buffer@^5.4.3, buffer@^5.5.0:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
-  integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
-  dependencies:
-    base64-js "^1.0.2"
-    ieee754 "^1.1.4"
-
-buffer@^6.0.3:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
-  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.2.1"
-
-bufferutil@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
-  integrity sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==
-  dependencies:
-    node-gyp-build "~3.7.0"
-
-builtin-modules@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
-  integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
-
-bull@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/bull/-/bull-4.1.0.tgz#ff8f628694e7dbbdf89b6a6860a1f33e990527fd"
-  integrity sha512-rQcLuAmzZIv1dHJO/yKrWu497xcTxMpYeTEBfpStrJFZ1UZpBSGHSx+defbtFVeGEeY8Pn0aMGRvRtldUBVUyQ==
-  dependencies:
-    cron-parser "^2.13.0"
-    debuglog "^1.0.0"
-    get-port "^5.1.1"
-    ioredis "^4.27.0"
-    lodash "^4.17.21"
-    p-timeout "^3.2.0"
-    promise.prototype.finally "^3.1.2"
-    semver "^7.3.2"
-    util.promisify "^1.0.1"
-    uuid "^8.3.0"
-
-busboy@^0.2.11:
-  version "0.2.14"
-  resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
-  integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=
-  dependencies:
-    dicer "0.2.5"
-    readable-stream "1.1.x"
-
-bytes@3.1.0, bytes@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
-  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
-
-cacache@^15.0.5:
-  version "15.1.0"
-  resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.1.0.tgz#164c2f857ee606e4cc793c63018fefd0ea5eba7b"
-  integrity sha512-mfx0C+mCfWjD1PnwQ9yaOrwG1ou9FkKnx0SvzUHWdFt7r7GaRtzT+9M8HAvLu62zIHtnpQ/1m93nWNDCckJGXQ==
-  dependencies:
-    "@npmcli/move-file" "^1.0.1"
-    chownr "^2.0.0"
-    fs-minipass "^2.0.0"
-    glob "^7.1.4"
-    infer-owner "^1.0.4"
-    lru-cache "^6.0.0"
-    minipass "^3.1.1"
-    minipass-collect "^1.0.2"
-    minipass-flush "^1.0.5"
-    minipass-pipeline "^1.2.2"
-    mkdirp "^1.0.3"
-    p-map "^4.0.0"
-    promise-inflight "^1.0.1"
-    rimraf "^3.0.2"
-    ssri "^8.0.1"
-    tar "^6.0.2"
-    unique-filename "^1.1.1"
-
 cache-base@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -2487,67 +635,16 @@ cache-base@^1.0.1:
     union-value "^1.0.0"
     unset-value "^1.0.0"
 
-cache-content-type@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
-  integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==
-  dependencies:
-    mime-types "^2.1.18"
-    ylru "^1.2.0"
-
-cacheable-lookup@6.0.4:
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz#65c0e51721bb7f9f2cb513aed6da4a1b93ad7dc8"
-  integrity sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==
-
-cacheable-lookup@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3"
-  integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==
-
-cacheable-request@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58"
-  integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==
-  dependencies:
-    clone-response "^1.0.2"
-    get-stream "^5.1.0"
-    http-cache-semantics "^4.0.0"
-    keyv "^4.0.0"
-    lowercase-keys "^2.0.0"
-    normalize-url "^4.1.0"
-    responselike "^2.0.0"
-
 cachedir@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
   integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
 
-cafy@15.2.1:
-  version "15.2.1"
-  resolved "https://registry.yarnpkg.com/cafy/-/cafy-15.2.1.tgz#5a55eaeb721c604c7dca652f3d555c392e5f995a"
-  integrity sha512-g2zOmFb63p6XcZ/zeMWKYP8YKQYNWnhJmi6K71Ql4EAFTAay31xF0PBPtdBCCfQ0fiETgWTMxKtySAVI/Od6aQ==
-
-callsites@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
-  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
-
 camelcase@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
   integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo=
 
-camelcase@^5.0.0:
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
-  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-
-camelcase@^6.0.0:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
-  integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
-
 caniuse-api@^1.5.2:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c"
@@ -2558,82 +655,16 @@ caniuse-api@^1.5.2:
     lodash.memoize "^4.1.2"
     lodash.uniq "^4.5.0"
 
-caniuse-api@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
-  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
-  dependencies:
-    browserslist "^4.0.0"
-    caniuse-lite "^1.0.0"
-    lodash.memoize "^4.1.2"
-    lodash.uniq "^4.5.0"
-
 caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
   version "1.0.30001170"
   resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001170.tgz#3417e94e73536b5bf567e9e9c4b7c1901c408c6a"
   integrity sha512-NzQ5cfHv/pCgVbNWl9eIILms/hb57N+8Ku2yf2IOkBCmYSF4pweMtLTbBAwN/LzKPEtxE42hhTz0sb8gBlkPXA==
 
-caniuse-lite@^1.0.0:
-  version "1.0.30001048"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz#4bb4f1bc2eb304e5e1154da80b93dee3f1cf447e"
-  integrity sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg==
-
-caniuse-lite@^1.0.30001181:
-  version "1.0.30001191"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz#bacb432b6701f690c8c5f7c680166b9a9f0843d9"
-  integrity sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==
-
-caniuse-lite@^1.0.30001208:
-  version "1.0.30001208"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9"
-  integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==
-
-caniuse-lite@^1.0.30001219:
-  version "1.0.30001230"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71"
-  integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==
-
-canonicalize@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.1.tgz#657b4f3fa38a6ecb97a9e5b7b26d7a19cc6e0da9"
-  integrity sha512-N3cmB3QLhS5TJ5smKFf1w42rJXWe6C1qP01z4dxJiI5v269buii4fLHWETDyf7yEd0azGLNC63VxNMiPd2u0Cg==
-
 caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
 
-cbor@*:
-  version "7.0.5"
-  resolved "https://registry.yarnpkg.com/cbor/-/cbor-7.0.5.tgz#ed54cdbc19fa7352bb328d00a5393aa7ce45a10f"
-  integrity sha512-0aaAPgW92lLmypb9iCd22k7tSD1FbF6dps8VQzmIBKY6ych2gO09b2vo/SbaLTmezJuB8Kh88Rvpl/Uq52mNZg==
-  dependencies:
-    "@cto.af/textdecoder" "^0.0.0"
-    nofilter "^2.0.3"
-
-cbor@8.1.0:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/cbor/-/cbor-8.1.0.tgz#cfc56437e770b73417a2ecbfc9caf6b771af60d5"
-  integrity sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==
-  dependencies:
-    nofilter "^3.1.0"
-
-chalk@4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
-  integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==
-  dependencies:
-    ansi-styles "^4.1.0"
-    supports-color "^7.1.0"
-
-chalk@4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
-  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
-  dependencies:
-    ansi-styles "^4.1.0"
-    supports-color "^7.1.0"
-
 chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -2645,16 +676,7 @@ chalk@^1.1.3:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
-  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
-  dependencies:
-    ansi-styles "^3.2.1"
-    escape-string-regexp "^1.0.5"
-    supports-color "^5.3.0"
-
-chalk@^4.0.0, chalk@^4.1.0:
+chalk@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
   integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
@@ -2662,87 +684,31 @@ chalk@^4.0.0, chalk@^4.1.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-char-regex@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
-  integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
-
-character-parser@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
-  integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A=
-  dependencies:
-    is-regex "^1.0.3"
-
-chart.js@3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.6.0.tgz#a87fce8431d4e7c5523d721f487f53aada1e42fe"
-  integrity sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ==
-
-chartjs-adapter-date-fns@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
-  integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
-
-chartjs-plugin-zoom@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.1.1.tgz#8a28923a17fcb5eb57a0dc94c5113bf402677647"
-  integrity sha512-1q54WOzK7FtAjkbemQeqvmFUV0btNYIQny2HbQ6Awq9wUtCz7Zmj6vIgp3C1DYMQwN0nqgpC3vnApqiwI7cSdQ==
-  dependencies:
-    hammerjs "^2.0.8"
-
 check-more-types@2.24.0, check-more-types@^2.24.0:
   version "2.24.0"
   resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
   integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
 
-cheerio-httpcli@0.8.2:
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/cheerio-httpcli/-/cheerio-httpcli-0.8.2.tgz#0189bda71c8bd2852de78e154291e2288184fbf2"
-  integrity sha512-grIzTwQg/nE7Oy6VvL19pf0UlM6wiluy/AOpXfQLVFrSi21F8wnO3dLchtaH2hfMF6jz68ot0/ngyQQVrp2VTw==
+chokidar@^2.0.0:
+  version "2.1.8"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
+  integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
   dependencies:
-    "@types/cheerio" "0.22.18"
-    "@types/rsvp" "^4.0.4"
-    async "^3.2.0"
-    cheerio "^0.22.0"
-    colors "^1.4.0"
-    foreach "^2.0.5"
-    he "^1.2.0"
-    iconv-lite "^0.6.3"
-    import-fresh "^3.3.0"
-    jschardet "^3.0.0"
-    object-assign "^4.1.1"
-    os-locale "^5.0.0"
-    prettyjson "^1.2.1"
-    request "^2.88.2"
-    rsvp "^4.8.5"
-    tough-cookie "^2.5.0"
-    type-of "^2.0.1"
-    valid-url "^1.0.9"
-
-cheerio@^0.22.0:
-  version "0.22.0"
-  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
-  integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=
-  dependencies:
-    css-select "~1.2.0"
-    dom-serializer "~0.1.0"
-    entities "~1.1.1"
-    htmlparser2 "^3.9.1"
-    lodash.assignin "^4.0.9"
-    lodash.bind "^4.1.4"
-    lodash.defaults "^4.0.1"
-    lodash.filter "^4.4.0"
-    lodash.flatten "^4.2.0"
-    lodash.foreach "^4.3.0"
-    lodash.map "^4.4.0"
-    lodash.merge "^4.4.0"
-    lodash.pick "^4.2.1"
-    lodash.reduce "^4.4.0"
-    lodash.reject "^4.4.0"
-    lodash.some "^4.4.0"
-
-chokidar@3.5.1, "chokidar@>=3.0.0 <4.0.0", chokidar@^2.0.0, chokidar@^3.3.1, chokidar@^3.5.0:
+    anymatch "^2.0.0"
+    async-each "^1.0.1"
+    braces "^2.3.2"
+    glob-parent "^3.1.0"
+    inherits "^2.0.3"
+    is-binary-path "^1.0.0"
+    is-glob "^4.0.0"
+    normalize-path "^3.0.0"
+    path-is-absolute "^1.0.0"
+    readdirp "^2.2.1"
+    upath "^1.1.1"
+  optionalDependencies:
+    fsevents "^1.2.7"
+
+chokidar@^3.3.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
   integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
@@ -2757,23 +723,6 @@ chokidar@3.5.1, "chokidar@>=3.0.0 <4.0.0", chokidar@^2.0.0, chokidar@^3.3.1, cho
   optionalDependencies:
     fsevents "~2.1.2"
 
-chownr@^1.1.1:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
-  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
-
-chownr@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
-  integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
-
-chrome-trace-event@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
-  integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==
-  dependencies:
-    tslib "^1.9.0"
-
 ci-info@^3.1.1:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6"
@@ -2808,18 +757,6 @@ cli-cursor@^3.1.0:
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-highlight@2.1.11, cli-highlight@^2.1.11:
-  version "2.1.11"
-  resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf"
-  integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==
-  dependencies:
-    chalk "^4.0.0"
-    highlight.js "^10.7.1"
-    mz "^2.4.0"
-    parse5 "^5.1.1"
-    parse5-htmlparser2-tree-adapter "^6.0.0"
-    yargs "^16.0.0"
-
 cli-table3@~0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
@@ -2847,45 +784,11 @@ cliui@^3.2.0:
     strip-ansi "^3.0.1"
     wrap-ansi "^2.0.0"
 
-cliui@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
-  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
-  dependencies:
-    string-width "^3.1.0"
-    strip-ansi "^5.2.0"
-    wrap-ansi "^5.1.0"
-
-cliui@^7.0.2:
-  version "7.0.4"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
-  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
-  dependencies:
-    string-width "^4.2.0"
-    strip-ansi "^6.0.0"
-    wrap-ansi "^7.0.0"
-
 clone-buffer@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
   integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
 
-clone-deep@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
-  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
-  dependencies:
-    is-plain-object "^2.0.4"
-    kind-of "^6.0.2"
-    shallow-clone "^3.0.0"
-
-clone-response@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
-  integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
-  dependencies:
-    mimic-response "^1.0.0"
-
 clone-stats@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
@@ -2910,45 +813,6 @@ cloneable-readable@^1.0.0:
     process-nextick-args "^2.0.0"
     readable-stream "^2.3.5"
 
-cluster-key-slot@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
-  integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
-
-co-body@^5.0.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124"
-  integrity sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==
-  dependencies:
-    inflation "^2.0.0"
-    qs "^6.4.0"
-    raw-body "^2.2.0"
-    type-is "^1.6.14"
-
-co-body@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.0.0.tgz#965b9337d7f5655480787471f4237664820827e3"
-  integrity sha512-9ZIcixguuuKIptnY8yemEOuhb71L/lLf+Rl5JfJEUiDNJk0e02MBt7BPxR2GEh5mw8dPthQYR4jPI/BnS1MQgw==
-  dependencies:
-    inflation "^2.0.0"
-    qs "^6.5.2"
-    raw-body "^2.3.3"
-    type-is "^1.6.16"
-
-co@^4.6.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
-  integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
-
-coa@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
-  integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
-  dependencies:
-    "@types/q" "^1.5.1"
-    chalk "^2.4.1"
-    q "^1.1.2"
-
 coa@~1.0.1:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd"
@@ -2978,7 +842,7 @@ collection-visit@^1.0.0:
     map-visit "^1.0.0"
     object-visit "^1.0.0"
 
-color-convert@^1.3.0, color-convert@^1.9.0:
+color-convert@^1.3.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
   integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -3009,14 +873,6 @@ color-string@^0.3.0:
   dependencies:
     color-name "^1.0.0"
 
-color-string@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
-  integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
-  dependencies:
-    color-name "^1.0.0"
-    simple-swizzle "^0.2.2"
-
 color-support@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
@@ -3031,29 +887,11 @@ color@^0.11.0:
     color-convert "^1.3.0"
     color-string "^0.3.0"
 
-color@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67"
-  integrity sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==
-  dependencies:
-    color-convert "^2.0.1"
-    color-string "^1.6.0"
-
-colord@^2.9.1:
-  version "2.9.1"
-  resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e"
-  integrity sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw==
-
-colorette@^1.2.0, colorette@^1.2.1, colorette@^1.2.2:
+colorette@^1.2.0, colorette@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
   integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
 
-colorette@^2.0.14:
-  version "2.0.15"
-  resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.15.tgz#8e634aa0429b110d24be82eac4d42f5ea65ab2d5"
-  integrity sha512-lIFQhufWaVvwi4wOlX9Gx5b0Nmw3XAZ8HzHNH9dfxhe+JaKNTmX6QLk4o7UHyI+tUY8ClvyfaHUm5bf61O3psA==
-
 colormin@^1.0.5:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133"
@@ -3063,7 +901,7 @@ colormin@^1.0.5:
     css-color-names "0.0.4"
     has "^1.0.1"
 
-colors@^1.1.2, colors@^1.3.3, colors@^1.4.0:
+colors@^1.1.2:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
   integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
@@ -3073,14 +911,14 @@ colors@~1.1.2:
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
   integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
 
-combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
+combined-stream@^1.0.6, combined-stream@~1.0.6:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
   integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@^2.12.1, commander@^2.19.0, commander@^2.20.0:
+commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -3090,42 +928,11 @@ commander@^5.1.0:
   resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
   integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
 
-commander@^6.0.0, commander@^6.2.1:
-  version "6.2.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
-  integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
-
-commander@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-7.0.0.tgz#3e2bbfd8bb6724760980988fb5b22b7ee6b71ab2"
-  integrity sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA==
-
-commander@^7.2.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
-  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
-
-comment-json@^4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.1.1.tgz#49df4948704bebb1cc0ffa6910e25669b668b7c5"
-  integrity sha512-v8gmtPvxhBlhdRBLwdHSjGy9BgA23t9H1FctdQKyUrErPjSrJcdDMqBq9B4Irtm7w3TNYLQJNH6ARKnpyag1sA==
-  dependencies:
-    array-timsort "^1.0.3"
-    core-util-is "^1.0.2"
-    esprima "^4.0.1"
-    has-own-prop "^2.0.0"
-    repeat-string "^1.6.1"
-
 common-tags@^1.8.0:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
   integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==
 
-compare-versions@3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
-  integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
-
 component-emitter@^1.2.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@@ -3136,78 +943,15 @@ concat-map@0.0.1:
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
-concat-stream@^1.5.2, concat-stream@^1.6.0:
+concat-stream@^1.6.0:
   version "1.6.2"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
-  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
-  dependencies:
-    buffer-from "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^2.2.2"
-    typedarray "^0.0.6"
-
-concurrently@6.3.0:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.3.0.tgz#63128cb4a6ed54d3c0ed8528728590a5fe54582a"
-  integrity sha512-k4k1jQGHHKsfbqzkUszVf29qECBrkvBKkcPJEUDTyVR7tZd1G/JOfnst4g1sYbFvJ4UjHZisj1aWQR8yLKpGPw==
-  dependencies:
-    chalk "^4.1.0"
-    date-fns "^2.16.1"
-    lodash "^4.17.21"
-    rxjs "^6.6.3"
-    spawn-command "^0.0.2-1"
-    supports-color "^8.1.0"
-    tree-kill "^1.2.2"
-    yargs "^16.2.0"
-
-condense-newlines@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f"
-  integrity sha1-PemFVTE5R10yUCyDsC9gaE0kxV8=
-  dependencies:
-    extend-shallow "^2.0.1"
-    is-whitespace "^0.3.0"
-    kind-of "^3.0.2"
-
-config-chain@^1.1.12:
-  version "1.1.12"
-  resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
-  integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
-  dependencies:
-    ini "^1.3.4"
-    proto-list "~1.2.1"
-
-console-control-strings@^1.0.0, console-control-strings@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
-  integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
-
-consolidate@^0.16.0:
-  version "0.16.0"
-  resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16"
-  integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==
-  dependencies:
-    bluebird "^3.7.2"
-
-constantinople@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151"
-  integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==
-  dependencies:
-    "@babel/parser" "^7.6.0"
-    "@babel/types" "^7.6.1"
-
-content-disposition@0.5.3, content-disposition@~0.5.2:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
-  integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
   dependencies:
-    safe-buffer "5.1.2"
-
-content-type@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
-  integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+    buffer-from "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
 
 convert-source-map@^1.5.0:
   version "1.7.0"
@@ -3216,14 +960,6 @@ convert-source-map@^1.5.0:
   dependencies:
     safe-buffer "~5.1.1"
 
-cookies@~0.8.0:
-  version "0.8.0"
-  resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
-  integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
-  dependencies:
-    depd "~2.0.0"
-    keygrip "~1.1.0"
-
 copy-descriptor@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
@@ -3237,53 +973,11 @@ copy-props@^2.0.1:
     each-props "^1.3.0"
     is-plain-object "^2.0.1"
 
-copy-to@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5"
-  integrity sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=
-
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
 
-core-util-is@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
-  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
-
-cosmiconfig@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
-  integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==
-  dependencies:
-    "@types/parse-json" "^4.0.0"
-    import-fresh "^3.2.1"
-    parse-json "^5.0.0"
-    path-type "^4.0.0"
-    yaml "^1.10.0"
-
-crc-32@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
-  integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==
-  dependencies:
-    exit-on-epipe "~1.0.1"
-    printj "~1.1.0"
-
-create-require@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
-  integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
-
-cron-parser@^2.13.0:
-  version "2.13.0"
-  resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.13.0.tgz#6f930bb6f2931790d2a9eec83b3ec276e27a6725"
-  integrity sha512-UWeIpnRb0eyoWPVk+pD3TDpNx3KCFQeezO224oJIkktBrcW6RoAPOx5zIKprZGfk6vcYSmA8yQXItejSaDBhbQ==
-  dependencies:
-    is-nan "^1.2.1"
-    moment-timezone "^0.5.25"
-
 cross-env@7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
@@ -3291,7 +985,7 @@ cross-env@7.0.3:
   dependencies:
     cross-spawn "^7.0.1"
 
-cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
+cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -3305,162 +999,6 @@ css-color-names@0.0.4:
   resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
   integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=
 
-css-color-names@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67"
-  integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==
-
-css-declaration-sorter@^6.0.3:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz#9dfd8ea0df4cc7846827876fafb52314890c21a9"
-  integrity sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==
-  dependencies:
-    timsort "^0.3.0"
-
-css-loader@6.5.1:
-  version "6.5.1"
-  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.5.1.tgz#0c43d4fbe0d97f699c91e9818cb585759091d1b1"
-  integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==
-  dependencies:
-    icss-utils "^5.1.0"
-    postcss "^8.2.15"
-    postcss-modules-extract-imports "^3.0.0"
-    postcss-modules-local-by-default "^4.0.0"
-    postcss-modules-scope "^3.0.0"
-    postcss-modules-values "^4.0.0"
-    postcss-value-parser "^4.1.0"
-    semver "^7.3.5"
-
-css-select-base-adapter@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
-  integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
-
-css-select@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef"
-  integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
-  dependencies:
-    boolbase "^1.0.0"
-    css-what "^3.2.1"
-    domutils "^1.7.0"
-    nth-check "^1.0.2"
-
-css-select@^4.1.3:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
-  integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
-  dependencies:
-    boolbase "^1.0.0"
-    css-what "^5.0.0"
-    domhandler "^4.2.0"
-    domutils "^2.6.0"
-    nth-check "^2.0.0"
-
-css-select@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
-  integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
-  dependencies:
-    boolbase "~1.0.0"
-    css-what "2.1"
-    domutils "1.5.1"
-    nth-check "~1.0.1"
-
-css-tree@1.0.0-alpha.37:
-  version "1.0.0-alpha.37"
-  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
-  integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
-  dependencies:
-    mdn-data "2.0.4"
-    source-map "^0.6.1"
-
-css-tree@1.0.0-alpha.39:
-  version "1.0.0-alpha.39"
-  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb"
-  integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
-  dependencies:
-    mdn-data "2.0.6"
-    source-map "^0.6.1"
-
-css-tree@^1.1.2, css-tree@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
-  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
-  dependencies:
-    mdn-data "2.0.14"
-    source-map "^0.6.1"
-
-css-what@2.1:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
-  integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
-
-css-what@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1"
-  integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==
-
-css-what@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
-  integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
-
-cssesc@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
-  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
-
-cssnano-preset-default@^5.1.6:
-  version "5.1.6"
-  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.6.tgz#1bdb83be6a6b1fee6dc5e9ec2e61286bcadcc7a6"
-  integrity sha512-X2nDeNGBXc0486oHjT2vSj+TdeyVsxRvJUxaOH50hOM6vSDLkKd0+59YXpSZRInJ4sNtBOykS4KsPfhdrU/35w==
-  dependencies:
-    css-declaration-sorter "^6.0.3"
-    cssnano-utils "^2.0.1"
-    postcss-calc "^8.0.0"
-    postcss-colormin "^5.2.1"
-    postcss-convert-values "^5.0.2"
-    postcss-discard-comments "^5.0.1"
-    postcss-discard-duplicates "^5.0.1"
-    postcss-discard-empty "^5.0.1"
-    postcss-discard-overridden "^5.0.1"
-    postcss-merge-longhand "^5.0.3"
-    postcss-merge-rules "^5.0.2"
-    postcss-minify-font-values "^5.0.1"
-    postcss-minify-gradients "^5.0.3"
-    postcss-minify-params "^5.0.1"
-    postcss-minify-selectors "^5.1.0"
-    postcss-normalize-charset "^5.0.1"
-    postcss-normalize-display-values "^5.0.1"
-    postcss-normalize-positions "^5.0.1"
-    postcss-normalize-repeat-style "^5.0.1"
-    postcss-normalize-string "^5.0.1"
-    postcss-normalize-timing-functions "^5.0.1"
-    postcss-normalize-unicode "^5.0.1"
-    postcss-normalize-url "^5.0.2"
-    postcss-normalize-whitespace "^5.0.1"
-    postcss-ordered-values "^5.0.2"
-    postcss-reduce-initial "^5.0.1"
-    postcss-reduce-transforms "^5.0.1"
-    postcss-svgo "^5.0.3"
-    postcss-unique-selectors "^5.0.1"
-
-cssnano-utils@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2"
-  integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==
-
-cssnano@5.0.10:
-  version "5.0.10"
-  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.10.tgz#92207eb7c9c6dc08d318050726f9fad0adf7220b"
-  integrity sha512-YfNhVJJ04imffOpbPbXP2zjIoByf0m8E2c/s/HnvSvjXgzXMfgopVjAEGvxYOjkOpWuRQDg/OZFjO7WW94Ri8w==
-  dependencies:
-    cssnano-preset-default "^5.1.6"
-    is-resolvable "^1.1.0"
-    lilconfig "^2.0.3"
-    yaml "^1.10.2"
-
 cssnano@^3.0.0:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
@@ -3499,20 +1037,6 @@ cssnano@^3.0.0:
     postcss-value-parser "^3.2.3"
     postcss-zindex "^2.0.1"
 
-csso@^4.0.2:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903"
-  integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==
-  dependencies:
-    css-tree "1.0.0-alpha.39"
-
-csso@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
-  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
-  dependencies:
-    css-tree "^1.1.2"
-
 csso@~2.3.1:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
@@ -3521,28 +1045,6 @@ csso@~2.3.1:
     clap "^1.0.9"
     source-map "^0.5.3"
 
-cssom@^0.4.4:
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
-  integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
-
-cssom@~0.3.6:
-  version "0.3.8"
-  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
-  integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
-
-cssstyle@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
-  integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
-  dependencies:
-    cssom "~0.3.6"
-
-csstype@^2.6.8:
-  version "2.6.13"
-  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f"
-  integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A==
-
 cypress@8.5.0:
   version "8.5.0"
   resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.5.0.tgz#5712ca170913f8344bf167301205c4217c1eb9bd"
@@ -3606,54 +1108,11 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
-data-uri-to-buffer@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636"
-  integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==
-
-data-urls@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
-  integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==
-  dependencies:
-    abab "^2.0.3"
-    whatwg-mimetype "^2.3.0"
-    whatwg-url "^8.0.0"
-
-date-fns@2.25.0:
-  version "2.25.0"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
-  integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
-
-date-fns@^2.16.1:
-  version "2.19.0"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
-  integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg==
-
-dateformat@4.5.1:
-  version "4.5.1"
-  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.5.1.tgz#c20e7a9ca77d147906b6dc2261a8be0a5bd2173c"
-  integrity sha512-OD0TZ+B7yP7ZgpJf5K2DIbj3FZvFvxgFUuaqA/V5zTjAtAAXZ1E8bktHxmAGs4x5b7PflqA9LeQ84Og7wYtF7Q==
-
 dayjs@^1.10.4:
   version "1.10.6"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.6.tgz#288b2aa82f2d8418a6c9d4df5898c0737ad02a63"
   integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==
 
-debug@2, debug@^2.2.0, debug@^2.3.3, debug@^2.5.2:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
-  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
-  dependencies:
-    ms "2.0.0"
-
-debug@4, debug@4.3.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
-  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
-  dependencies:
-    ms "2.1.2"
-
 debug@4.3.2, debug@^4.3.2:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
@@ -3661,10 +1120,10 @@ debug@4.3.2, debug@^4.3.2:
   dependencies:
     ms "2.1.2"
 
-debug@=3.1.0, debug@~3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
-  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+debug@^2.2.0, debug@^2.3.3:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
   dependencies:
     ms "2.0.0"
 
@@ -3675,79 +1134,23 @@ debug@^3.1.0:
   dependencies:
     ms "^2.1.1"
 
-debug@^3.2.6:
-  version "3.2.6"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
-  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+debug@^4.1.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
   dependencies:
-    ms "^2.1.1"
-
-debuglog@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
-  integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
+    ms "2.1.2"
 
-decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
+decamelize@^1.1.1, decamelize@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
 
-decamelize@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
-  integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
-
-decimal.js@^10.2.1:
-  version "10.2.1"
-  resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
-  integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==
-
 decode-uri-component@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
   integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
 
-decompress-response@^4.2.0:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
-  integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
-  dependencies:
-    mimic-response "^2.0.0"
-
-decompress-response@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
-  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
-  dependencies:
-    mimic-response "^3.1.0"
-
-deep-email-validator@0.1.18:
-  version "0.1.18"
-  resolved "https://registry.yarnpkg.com/deep-email-validator/-/deep-email-validator-0.1.18.tgz#a072a93f28e11863cc6b9ca3ae964e0e45b3ece8"
-  integrity sha512-eo2WEUidQvppg6Qdek8iwOqmXvaxRJ2D2VJKbIOwUgLZNFveDDdJMBsFc+yq0S+lILEUcmzrJRrCWbyoe7QUzQ==
-  dependencies:
-    "@types/disposable-email-domains" "^1.0.1"
-    axios "^0.19.2"
-    disposable-email-domains "^1.0.53"
-    lodash "^4.17.15"
-    mailcheck "^1.1.1"
-    ts-jest "^25.2.1"
-
-deep-equal@~1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
-  integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
-
-deep-extend@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
-  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
-
-deep-is@^0.1.3, deep-is@~0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
-  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
-
 default-compare@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f"
@@ -3760,12 +1163,7 @@ default-resolution@^2.0.0:
   resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684"
   integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=
 
-defer-to-connect@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1"
-  integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==
-
-define-properties@^1.1.2, define-properties@^1.1.3:
+define-properties@^1.1.2:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
   integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -3804,196 +1202,11 @@ delayed-stream@~1.0.0:
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
 
-delegates@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
-  integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
-
-denque@^1.1.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
-  integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
-
-denque@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
-  integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
-
-depd@^1.1.2, depd@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
-  integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
-
-depd@^2.0.0, depd@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
-  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
-
-destroy@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
-  integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
-
 detect-file@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
   integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
 
-detect-libc@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
-  integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
-
-detect-node@2.1.0, detect-node@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
-  integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
-
-dicer@0.2.5:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
-  integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=
-  dependencies:
-    readable-stream "1.1.x"
-    streamsearch "0.1.2"
-
-diff@5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
-  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
-
-diff@^4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
-  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
-
-dijkstrajs@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
-  integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
-
-dir-glob@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
-  integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
-  dependencies:
-    path-type "^4.0.0"
-
-disposable-email-domains@^1.0.53:
-  version "1.0.58"
-  resolved "https://registry.yarnpkg.com/disposable-email-domains/-/disposable-email-domains-1.0.58.tgz#ac9c879c02c4f0898bfb6c0c80b959c0b0b7bc51"
-  integrity sha512-frnNCPqTjk6t/sosPoco6EIFHbP9SazHQkeltJNfZeUyNgewaVf+kFjEfVkVDVd436Vln43YElJPb8JozhBs7Q==
-
-doctrine@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
-  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
-  dependencies:
-    esutils "^2.0.2"
-
-doctypes@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
-  integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
-
-dom-serializer@0:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
-  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
-  dependencies:
-    domelementtype "^2.0.1"
-    entities "^2.0.0"
-
-dom-serializer@^1.0.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be"
-  integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==
-  dependencies:
-    domelementtype "^2.0.1"
-    domhandler "^4.0.0"
-    entities "^2.0.0"
-
-dom-serializer@~0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
-  integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
-  dependencies:
-    domelementtype "^1.3.0"
-    entities "^1.1.1"
-
-domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
-  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
-
-domelementtype@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
-  integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
-
-domelementtype@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
-  integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
-
-domexception@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
-  integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==
-  dependencies:
-    webidl-conversions "^5.0.0"
-
-domhandler@^2.3.0:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
-  integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
-  dependencies:
-    domelementtype "1"
-
-domhandler@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.1.0.tgz#c1d8d494d5ec6db22de99e46a149c2a4d23ddd43"
-  integrity sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==
-  dependencies:
-    domelementtype "^2.2.0"
-
-domhandler@^4.2.0:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
-  integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==
-  dependencies:
-    domelementtype "^2.2.0"
-
-domutils@1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
-  integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
-  dependencies:
-    dom-serializer "0"
-    domelementtype "1"
-
-domutils@^1.5.1, domutils@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
-  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
-  dependencies:
-    dom-serializer "0"
-    domelementtype "1"
-
-domutils@^2.6.0:
-  version "2.8.0"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
-  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
-  dependencies:
-    dom-serializer "^1.0.1"
-    domelementtype "^2.2.0"
-    domhandler "^4.2.0"
-
-dotenv@^8.2.0:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
-  integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
-
 duplexer@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
@@ -4025,181 +1238,37 @@ ecc-jsbn@~0.1.1:
     jsbn "~0.1.0"
     safer-buffer "^2.1.0"
 
-ecdsa-sig-formatter@1.0.11:
-  version "1.0.11"
-  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
-  integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
-  dependencies:
-    safe-buffer "^5.0.1"
-
-editorconfig@^0.15.3:
-  version "0.15.3"
-  resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
-  integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==
-  dependencies:
-    commander "^2.19.0"
-    lru-cache "^4.1.5"
-    semver "^5.6.0"
-    sigmund "^1.0.1"
-
-ee-first@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-  integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-
 electron-to-chromium@^1.2.7:
   version "1.3.633"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe"
   integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA==
 
-electron-to-chromium@^1.3.649:
-  version "1.3.672"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.672.tgz#3a6e335016dab4bc584d5292adc4f98f54541f6a"
-  integrity sha512-gFQe7HBb0lbOMqK2GAS5/1F+B0IMdYiAgB9OT/w1F4M7lgJK2aNOMNOM622aEax+nS1cTMytkiT0uMOkbtFmHw==
-
-electron-to-chromium@^1.3.712:
-  version "1.3.717"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz#78d4c857070755fb58ab64bcc173db1d51cbc25f"
-  integrity sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==
-
-electron-to-chromium@^1.3.723:
-  version "1.3.742"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.742.tgz#7223215acbbd3a5284962ebcb6df85d88b95f200"
-  integrity sha512-ihL14knI9FikJmH2XUIDdZFWJxvr14rPSdOhJ7PpS27xbz8qmaRwCwyg/bmFwjWKmWK9QyamiCZVCvXm5CH//Q==
-
-emoji-regex@^7.0.1:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
-  integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
-
 emoji-regex@^8.0.0:
   version "8.0.0"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
-emojis-list@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
-  integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
-
-encodeurl@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
-  integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
-
-encoding@^0.1.12:
-  version "0.1.13"
-  resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
-  integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
-  dependencies:
-    iconv-lite "^0.6.2"
-
-end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
   dependencies:
     once "^1.4.0"
 
-enhanced-resolve@^5.0.0:
-  version "5.8.0"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.0.tgz#d9deae58f9d3773b6a111a5a46831da5be5c9ac0"
-  integrity sha512-Sl3KRpJA8OpprrtaIswVki3cWPiPKxXuFxJXBp+zNb6s6VwNWwFRUdtmzd2ReUut8n+sCPx7QCtQ7w5wfJhSgQ==
-  dependencies:
-    graceful-fs "^4.2.4"
-    tapable "^2.2.0"
-
-enhanced-resolve@^5.7.0:
-  version "5.7.0"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz#525c5d856680fbd5052de453ac83e32049958b5c"
-  integrity sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==
-  dependencies:
-    graceful-fs "^4.2.4"
-    tapable "^2.2.0"
-
-enhanced-resolve@^5.8.3:
-  version "5.8.3"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0"
-  integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==
-  dependencies:
-    graceful-fs "^4.2.4"
-    tapable "^2.2.0"
-
-enquirer@^2.3.5, enquirer@^2.3.6:
+enquirer@^2.3.6:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
   integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
   dependencies:
     ansi-colors "^4.1.1"
 
-entities@^1.1.1, entities@~1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
-  integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
-
-entities@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
-  integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
-
-env-paths@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
-  integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
-
-envinfo@^7.7.3:
-  version "7.7.3"
-  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.7.3.tgz#4b2d8622e3e7366afb8091b23ed95569ea0208cc"
-  integrity sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA==
-
-err-code@^2.0.2:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
-  integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
-
-error-ex@^1.2.0, error-ex@^1.3.1:
+error-ex@^1.2.0:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
   integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
-  version "1.17.5"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
-  integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
-  dependencies:
-    es-to-primitive "^1.2.1"
-    function-bind "^1.1.1"
-    has "^1.0.3"
-    has-symbols "^1.0.1"
-    is-callable "^1.1.5"
-    is-regex "^1.0.5"
-    object-inspect "^1.7.0"
-    object-keys "^1.1.1"
-    object.assign "^4.1.0"
-    string.prototype.trimleft "^2.1.1"
-    string.prototype.trimright "^2.1.1"
-
-es-module-lexer@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.4.0.tgz#21f4181cc8b7eee06855f1c59e6087c7bc4f77b0"
-  integrity sha512-iuEGihqqhKWFgh72Q/Jtch7V2t/ft8w8IPP2aEN8ArYKO+IWyo6hsi96hCdgyeEDQIV3InhYQ9BlwUFPGXrbEQ==
-
-es-module-lexer@^0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.0.tgz#fe4c4621977bc668e285c5f1f70ca3b451095fda"
-  integrity sha512-qU2eN/XHsrl3E4y7mK1wdWnyy5c8gXtCbfP6Xcsemm7fPUR1PIV1JhZfP7ojcN0Fzp69CfrS3u76h2tusvfKiQ==
-
-es-to-primitive@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
-  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
-  dependencies:
-    is-callable "^1.1.4"
-    is-date-object "^1.0.1"
-    is-symbol "^1.0.2"
-
 es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50:
   version "0.10.53"
   resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
@@ -4231,198 +1300,26 @@ es6-weak-map@^2.0.1:
   resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53"
   integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
   dependencies:
-    d "1"
-    es5-ext "^0.10.46"
-    es6-iterator "^2.0.3"
-    es6-symbol "^3.1.1"
-
-escalade@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
-  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-escape-html@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
-  integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
-
-escape-regexp@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/escape-regexp/-/escape-regexp-0.0.1.tgz#f44bda12d45bbdf9cb7f862ee7e4827b3dd32254"
-  integrity sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ=
-
-escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
-  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
-  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
-
-escodegen@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
-  integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==
-  dependencies:
-    esprima "^4.0.1"
-    estraverse "^5.2.0"
-    esutils "^2.0.2"
-    optionator "^0.8.1"
-  optionalDependencies:
-    source-map "~0.6.1"
-
-eslint-plugin-vue@8.0.3:
-  version "8.0.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-8.0.3.tgz#791cc4543940319e612ea61a1d779e8c87cf749a"
-  integrity sha512-Rlhhy5ltzde0sRwSkqHuNePTXLMMaJ5+qsQubM4RYloYsQ8cXlnJT5MDaCzSirkGADipOHtmQXIbbPFAzUrADg==
-  dependencies:
-    eslint-utils "^3.0.0"
-    natural-compare "^1.4.0"
-    semver "^7.3.5"
-    vue-eslint-parser "^8.0.1"
-
-eslint-scope@5.1.1, eslint-scope@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
-  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
-  dependencies:
-    esrecurse "^4.3.0"
-    estraverse "^4.1.1"
-
-eslint-scope@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-6.0.0.tgz#9cf45b13c5ac8f3d4c50f46a5121f61b3e318978"
-  integrity sha512-uRDL9MWmQCkaFus8RF5K9/L/2fn+80yoW3jkD53l4shjCh26fCtvJGasxjUqP5OT87SYTxCVA3BwTUzuELx9kA==
-  dependencies:
-    esrecurse "^4.3.0"
-    estraverse "^5.2.0"
-
-eslint-utils@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
-  integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
-  dependencies:
-    eslint-visitor-keys "^2.0.0"
-
-eslint-visitor-keys@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
-  integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
+    d "1"
+    es5-ext "^0.10.46"
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.1"
 
-eslint-visitor-keys@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186"
-  integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==
-
-eslint@8.2.0:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.2.0.tgz#44d3fb506d0f866a506d97a0fc0e90ee6d06a815"
-  integrity sha512-erw7XmM+CLxTOickrimJ1SiF55jiNlVSp2qqm0NuBWPtHYQCegD5ZMaW0c3i5ytPqL+SSLaCxdvQXFPLJn+ABw==
-  dependencies:
-    "@eslint/eslintrc" "^1.0.4"
-    "@humanwhocodes/config-array" "^0.6.0"
-    ajv "^6.10.0"
-    chalk "^4.0.0"
-    cross-spawn "^7.0.2"
-    debug "^4.3.2"
-    doctrine "^3.0.0"
-    enquirer "^2.3.5"
-    escape-string-regexp "^4.0.0"
-    eslint-scope "^6.0.0"
-    eslint-utils "^3.0.0"
-    eslint-visitor-keys "^3.0.0"
-    espree "^9.0.0"
-    esquery "^1.4.0"
-    esutils "^2.0.2"
-    fast-deep-equal "^3.1.3"
-    file-entry-cache "^6.0.1"
-    functional-red-black-tree "^1.0.1"
-    glob-parent "^6.0.1"
-    globals "^13.6.0"
-    ignore "^4.0.6"
-    import-fresh "^3.0.0"
-    imurmurhash "^0.1.4"
-    is-glob "^4.0.0"
-    js-yaml "^4.1.0"
-    json-stable-stringify-without-jsonify "^1.0.1"
-    levn "^0.4.1"
-    lodash.merge "^4.6.2"
-    minimatch "^3.0.4"
-    natural-compare "^1.4.0"
-    optionator "^0.9.1"
-    progress "^2.0.0"
-    regexpp "^3.2.0"
-    semver "^7.2.1"
-    strip-ansi "^6.0.1"
-    strip-json-comments "^3.1.0"
-    text-table "^0.2.0"
-    v8-compile-cache "^2.0.3"
-
-esm@^3.2.22:
-  version "3.2.25"
-  resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
-  integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
-
-espree@^9.0.0:
-  version "9.0.0"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.0.0.tgz#e90a2965698228502e771c7a58489b1a9d107090"
-  integrity sha512-r5EQJcYZ2oaGbeR0jR0fFVijGOcwai07/690YRXLINuhmVeRY4UKSAsQPe/0BNuDgwP7Ophoc1PRsr2E3tkbdQ==
-  dependencies:
-    acorn "^8.5.0"
-    acorn-jsx "^5.3.1"
-    eslint-visitor-keys "^3.0.0"
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
 esprima@^2.6.0:
   version "2.7.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
   integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=
 
-esprima@^4.0.0, esprima@^4.0.1:
+esprima@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
-esquery@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
-  integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
-  dependencies:
-    estraverse "^5.1.0"
-
-esrecurse@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
-  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
-  dependencies:
-    estraverse "^5.2.0"
-
-estraverse@^4.1.1:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
-  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
-
-estraverse@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
-  integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
-
-estraverse@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
-  integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
-
-estree-walker@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
-  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
-
-esutils@^2.0.2:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
-  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
-
 event-stream@=3.3.4:
   version "3.3.4"
   resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
@@ -4436,32 +1333,12 @@ event-stream@=3.3.4:
     stream-combiner "~0.0.4"
     through "~2.3.1"
 
-event-target-shim@^5.0.0:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
-  integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
-
 eventemitter2@^6.4.3:
   version "6.4.4"
   resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b"
   integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==
 
-eventemitter3@4.0.7, eventemitter3@^4.0.7:
-  version "4.0.7"
-  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
-  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
-
-events@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
-  integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
-
-events@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
-  integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
-
-execa@4.1.0, execa@^4.0.0:
+execa@4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
   integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
@@ -4491,21 +1368,6 @@ execa@5.1.1:
     signal-exit "^3.0.3"
     strip-final-newline "^2.0.0"
 
-execa@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376"
-  integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==
-  dependencies:
-    cross-spawn "^7.0.3"
-    get-stream "^6.0.0"
-    human-signals "^2.1.0"
-    is-stream "^2.0.0"
-    merge-stream "^2.0.0"
-    npm-run-path "^4.0.1"
-    onetime "^5.1.2"
-    signal-exit "^3.0.3"
-    strip-final-newline "^2.0.0"
-
 executable@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
@@ -4513,11 +1375,6 @@ executable@^4.1.1:
   dependencies:
     pify "^2.2.0"
 
-exit-on-epipe@~1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
-  integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
-
 expand-brackets@^2.1.4:
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@@ -4531,11 +1388,6 @@ expand-brackets@^2.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
-expand-template@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
-  integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
-
 expand-tilde@^2.0.0, expand-tilde@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
@@ -4620,50 +1472,11 @@ fast-deep-equal@^3.1.1:
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
   integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
 
-fast-deep-equal@^3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
-  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-
-fast-glob@^3.1.1:
-  version "3.2.4"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
-  integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
-  dependencies:
-    "@nodelib/fs.stat" "^2.0.2"
-    "@nodelib/fs.walk" "^1.2.3"
-    glob-parent "^5.1.0"
-    merge2 "^1.3.0"
-    micromatch "^4.0.2"
-    picomatch "^2.2.1"
-
-fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
+fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
-fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
-  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
-
-fast-xml-parser@^3.19.0:
-  version "3.19.0"
-  resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01"
-  integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==
-
-fastest-levenshtein@^1.0.12:
-  version "1.0.12"
-  resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
-  integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
-
-fastq@^1.6.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481"
-  integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==
-  dependencies:
-    reusify "^1.0.4"
-
 fd-slicer@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@@ -4671,18 +1484,6 @@ fd-slicer@~1.1.0:
   dependencies:
     pend "~1.2.0"
 
-feed@4.2.2:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
-  integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==
-  dependencies:
-    xml-js "^1.6.11"
-
-fetch-blob@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c"
-  integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==
-
 figures@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@@ -4690,21 +1491,10 @@ figures@^3.2.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
-file-entry-cache@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
-  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
-  dependencies:
-    flat-cache "^3.0.4"
-
-file-type@16.5.3:
-  version "16.5.3"
-  resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.3.tgz#474b7e88c74724046abb505e9b8ed4db30c4fc06"
-  integrity sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==
-  dependencies:
-    readable-web-to-node-stream "^3.0.0"
-    strtok3 "^6.2.4"
-    token-types "^4.1.1"
+file-uri-to-path@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
 
 fill-range@^4.0.0:
   version "4.0.0"
@@ -4723,22 +1513,6 @@ fill-range@^7.0.1:
   dependencies:
     to-regex-range "^5.0.1"
 
-find-node-modules@^2.1.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-2.1.2.tgz#57565a3455baf671b835bc6b2134a9b938b9c53c"
-  integrity sha512-x+3P4mbtRPlSiVE1Qco0Z4YLU8WFiFcuWTf3m75OV9Uzcfs2Bg+O9N+r/K0AnmINBW06KpfqKwYJbFlFq4qNug==
-  dependencies:
-    findup-sync "^4.0.0"
-    merge "^2.1.0"
-
-find-up@5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
-  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
-  dependencies:
-    locate-path "^6.0.0"
-    path-exists "^4.0.0"
-
 find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -4747,21 +1521,6 @@ find-up@^1.0.0:
     path-exists "^2.0.0"
     pinkie-promise "^2.0.0"
 
-find-up@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
-  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
-  dependencies:
-    locate-path "^3.0.0"
-
-find-up@^4.0.0, find-up@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
-  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
-  dependencies:
-    locate-path "^5.0.0"
-    path-exists "^4.0.0"
-
 findup-sync@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
@@ -4782,16 +1541,6 @@ findup-sync@^3.0.0:
     micromatch "^3.0.4"
     resolve-dir "^1.0.1"
 
-findup-sync@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-4.0.0.tgz#956c9cdde804052b881b428512905c4a5f2cdef0"
-  integrity sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==
-  dependencies:
-    detect-file "^1.0.0"
-    is-glob "^4.0.0"
-    micromatch "^4.0.2"
-    resolve-dir "^1.0.1"
-
 fined@^1.0.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b"
@@ -4808,37 +1557,11 @@ flagged-respawn@^1.0.0:
   resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41"
   integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==
 
-flat-cache@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
-  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
-  dependencies:
-    flatted "^3.1.0"
-    rimraf "^3.0.2"
-
-flat@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
-  integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
-
-flatted@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067"
-  integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==
-
 flatten@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
   integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==
 
-fluent-ffmpeg@2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz#c952de2240f812ebda0aa8006d7776ee2acf7d74"
-  integrity sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=
-  dependencies:
-    async ">=0.2.9"
-    which "^1.1.1"
-
 flush-write-stream@^1.0.2:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@@ -4847,13 +1570,6 @@ flush-write-stream@^1.0.2:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
-follow-redirects@1.5.10:
-  version "1.5.10"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
-  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
-  dependencies:
-    debug "=3.1.0"
-
 follow-redirects@^1.10.0:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
@@ -4871,25 +1587,11 @@ for-own@^1.0.0:
   dependencies:
     for-in "^1.0.1"
 
-foreach@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
-  integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
-
 forever-agent@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
   integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
 
-form-data@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
-  integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
-  dependencies:
-    asynckit "^0.4.0"
-    combined-stream "^1.0.8"
-    mime-types "^2.1.12"
-
 form-data@~2.3.2:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@@ -4906,30 +1608,11 @@ fragment-cache@^0.2.1:
   dependencies:
     map-cache "^0.2.2"
 
-fresh@~0.5.2:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
-  integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
-
 from@~0:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
   integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
 
-fs-constants@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
-  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-
-fs-extra@^8.0.1:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
-  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
-  dependencies:
-    graceful-fs "^4.2.0"
-    jsonfile "^4.0.0"
-    universalify "^0.1.0"
-
 fs-extra@^9.1.0:
   version "9.1.0"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
@@ -4940,13 +1623,6 @@ fs-extra@^9.1.0:
     jsonfile "^6.0.1"
     universalify "^2.0.0"
 
-fs-minipass@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
-  integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
-  dependencies:
-    minipass "^3.0.0"
-
 fs-mkdirp-stream@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb"
@@ -4960,6 +1636,14 @@ fs.realpath@^1.0.0:
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
+fsevents@^1.2.7:
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
+  integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
+  dependencies:
+    bindings "^1.5.0"
+    nan "^2.12.1"
+
 fsevents@~2.1.2:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
@@ -4970,47 +1654,11 @@ function-bind@^1.1.1:
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
-functional-red-black-tree@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
-  integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
-
-gauge@~2.7.3:
-  version "2.7.4"
-  resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
-  integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
-  dependencies:
-    aproba "^1.0.3"
-    console-control-strings "^1.0.0"
-    has-unicode "^2.0.0"
-    object-assign "^4.1.0"
-    signal-exit "^3.0.0"
-    string-width "^1.0.1"
-    strip-ansi "^3.0.1"
-    wide-align "^1.1.0"
-
 get-caller-file@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
   integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
 
-get-caller-file@^2.0.1, get-caller-file@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
-  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-get-paths@0.0.7:
-  version "0.0.7"
-  resolved "https://registry.yarnpkg.com/get-paths/-/get-paths-0.0.7.tgz#15331086752077cf130166ccd233a1cdbeefcf38"
-  integrity sha512-0wdJt7C1XKQxuCgouqd+ZvLJ56FQixKoki9MrFaO4EriqzXOiH9gbukaDE1ou08S8Ns3/yDzoBAISNPqj6e6tA==
-  dependencies:
-    pify "^4.0.1"
-
-get-port@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
-  integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
-
 get-stream@^5.0.0, get-stream@^5.1.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
@@ -5042,11 +1690,6 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
-github-from-package@0.0.0:
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
-  integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
-
 glob-parent@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -5055,20 +1698,13 @@ glob-parent@^3.1.0:
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
 
-glob-parent@^5.1.0, glob-parent@~5.1.0:
+glob-parent@~5.1.0:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
   integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
   dependencies:
     is-glob "^4.0.1"
 
-glob-parent@^6.0.1:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
-  integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
-  dependencies:
-    is-glob "^4.0.3"
-
 glob-stream@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4"
@@ -5085,11 +1721,6 @@ glob-stream@^6.1.0:
     to-absolute-glob "^2.0.0"
     unique-stream "^2.0.2"
 
-glob-to-regexp@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
-  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
-
 glob-watcher@^5.0.3:
   version "5.0.3"
   resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.3.tgz#88a8abf1c4d131eb93928994bc4a593c2e5dd626"
@@ -5102,7 +1733,7 @@ glob-watcher@^5.0.3:
     just-debounce "^1.0.0"
     object.defaults "^1.1.0"
 
-glob@7.1.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
+glob@^7.1.1, glob@^7.1.3:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -5114,18 +1745,6 @@ glob@7.1.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@7.2.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
-  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 global-dirs@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
@@ -5153,32 +1772,6 @@ global-prefix@^1.0.1:
     is-windows "^1.0.1"
     which "^1.2.14"
 
-globals@^13.6.0:
-  version "13.7.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-13.7.0.tgz#aed3bcefd80ad3ec0f0be2cf0c895110c0591795"
-  integrity sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA==
-  dependencies:
-    type-fest "^0.20.2"
-
-globals@^13.9.0:
-  version "13.9.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb"
-  integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==
-  dependencies:
-    type-fest "^0.20.2"
-
-globby@^11.0.2, globby@^11.0.4:
-  version "11.0.4"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
-  integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
-  dependencies:
-    array-union "^2.1.0"
-    dir-glob "^3.0.1"
-    fast-glob "^3.1.1"
-    ignore "^5.1.4"
-    merge2 "^1.3.0"
-    slash "^3.0.0"
-
 glogg@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.2.tgz#2d7dd702beda22eb3bffadf880696da6d846313f"
@@ -5186,24 +1779,7 @@ glogg@^1.0.0:
   dependencies:
     sparkles "^1.0.0"
 
-got@11.8.2:
-  version "11.8.2"
-  resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
-  integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
-  dependencies:
-    "@sindresorhus/is" "^4.0.0"
-    "@szmarczak/http-timer" "^4.0.5"
-    "@types/cacheable-request" "^6.0.1"
-    "@types/responselike" "^1.0.0"
-    cacheable-lookup "^5.0.3"
-    cacheable-request "^7.0.1"
-    decompress-response "^6.0.0"
-    http2-wrapper "^1.0.0-beta.5.2"
-    lowercase-keys "^2.0.0"
-    p-cancelable "^2.0.0"
-    responselike "^2.0.0"
-
-graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4:
+graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
   version "4.2.4"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
   integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
@@ -5213,16 +1789,6 @@ graceful-fs@^4.2.0:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
   integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
 
-graceful-fs@^4.2.6:
-  version "4.2.6"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
-  integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
-
-growl@1.10.5:
-  version "1.10.5"
-  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
-  integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
-
 gulp-cli@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-2.2.0.tgz#5533126eeb7fe415a7e3e84a297d334d5cf70ebc"
@@ -5313,11 +1879,6 @@ gulplog@^1.0.0:
   dependencies:
     glogg "^1.0.0"
 
-hammerjs@^2.0.8:
-  version "2.0.8"
-  resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
-  integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
-
 har-schema@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@@ -5343,31 +1904,16 @@ has-flag@^1.0.0:
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
   integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=
 
-has-flag@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
-  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
-
 has-flag@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-own-prop@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af"
-  integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==
-
-has-symbols@^1.0.0, has-symbols@^1.0.1:
+has-symbols@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
   integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
 
-has-unicode@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
-  integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
-
 has-value@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@@ -5406,26 +1952,6 @@ has@^1.0.1, has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
-hash-sum@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
-  integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=
-
-hash-sum@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a"
-  integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==
-
-he@1.2.0, he@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
-  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
-
-highlight.js@^10.7.1:
-  version "10.7.2"
-  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360"
-  integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg==
-
 homedir-polyfill@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
@@ -5438,104 +1964,11 @@ hosted-git-info@^2.1.4:
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
   integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
 
-hpagent@0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9"
-  integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==
-
-hpagent@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.1.tgz#66f67f16e5c7a8b59a068e40c2658c2c749ad5e2"
-  integrity sha512-IxJWQiY0vmEjetHdoE9HZjD4Cx+mYTr25tR7JCxXaiI3QxW0YqYyM11KyZbHufoa/piWhMb2+D3FGpMgmA2cFQ==
-
 html-comment-regex@^1.1.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
   integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
 
-html-encoding-sniffer@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
-  integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==
-  dependencies:
-    whatwg-encoding "^1.0.5"
-
-html-entities@2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488"
-  integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==
-
-htmlparser2@^3.9.1:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
-  integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
-  dependencies:
-    domelementtype "^1.3.1"
-    domhandler "^2.3.0"
-    domutils "^1.5.1"
-    entities "^1.1.1"
-    inherits "^2.0.1"
-    readable-stream "^3.1.1"
-
-http-assert@^1.3.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878"
-  integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==
-  dependencies:
-    deep-equal "~1.0.1"
-    http-errors "~1.7.2"
-
-http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
-  integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
-
-http-errors@1.7.3, http-errors@^1.6.3, http-errors@^1.7.3, http-errors@~1.7.2:
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
-  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
-  dependencies:
-    depd "~1.1.2"
-    inherits "2.0.4"
-    setprototypeof "1.1.1"
-    statuses ">= 1.5.0 < 2"
-    toidentifier "1.0.0"
-
-http-errors@~1.6.2:
-  version "1.6.3"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
-  integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
-  dependencies:
-    depd "~1.1.2"
-    inherits "2.0.3"
-    setprototypeof "1.1.0"
-    statuses ">= 1.4.0 < 2"
-
-http-headers@^3.0.1:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/http-headers/-/http-headers-3.0.2.tgz#5147771292f0b39d6778d930a3a59a76fc7ef44d"
-  integrity sha512-87E1I+2Wg4dxxz4rcxElo3dxO/w1ZtgL1yA0Sb6vH3qU16vRKq1NjWQv9SCY3ly2OQROcoxHZOUpmelS+k6wOw==
-  dependencies:
-    next-line "^1.1.0"
-
-http-proxy-agent@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
-  integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==
-  dependencies:
-    "@tootallnate/once" "1"
-    agent-base "6"
-    debug "4"
-
-http-signature@1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"
-  integrity sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw==
-  dependencies:
-    assert-plus "^1.0.0"
-    jsprim "^1.2.2"
-    sshpk "^1.14.1"
-
 http-signature@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@@ -5545,29 +1978,6 @@ http-signature@~1.2.0:
     jsprim "^1.2.2"
     sshpk "^1.7.0"
 
-http2-wrapper@^1.0.0-beta.5.2:
-  version "1.0.0-beta.5.2"
-  resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3"
-  integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==
-  dependencies:
-    quick-lru "^5.1.1"
-    resolve-alpn "^1.0.0"
-
-http_ece@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75"
-  integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==
-  dependencies:
-    urlsafe-base64 "~1.0.0"
-
-https-proxy-agent@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
-  integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
-  dependencies:
-    agent-base "6"
-    debug "4"
-
 human-signals@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@@ -5578,105 +1988,6 @@ human-signals@^2.1.0:
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
   integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
 
-humanize-ms@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
-  integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=
-  dependencies:
-    ms "^2.0.0"
-
-humanize-number@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/humanize-number/-/humanize-number-0.0.2.tgz#11c0af6a471643633588588048f1799541489c18"
-  integrity sha1-EcCvakcWQ2M1iFiASPF5lUFInBg=
-
-iconv-lite@0.4.24, iconv-lite@^0.4.4:
-  version "0.4.24"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
-  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
-  dependencies:
-    safer-buffer ">= 2.1.2 < 3"
-
-iconv-lite@^0.6.2:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
-  integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
-  dependencies:
-    safer-buffer ">= 2.1.2 < 3.0.0"
-
-iconv-lite@^0.6.3:
-  version "0.6.3"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
-  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
-  dependencies:
-    safer-buffer ">= 2.1.2 < 3.0.0"
-
-icss-utils@^5.0.0, icss-utils@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
-  integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
-
-idb-keyval@5.1.3:
-  version "5.1.3"
-  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.1.3.tgz#6ef5dff371897c23f144322dc6374eadd6a345d9"
-  integrity sha512-N9HbCK/FaXSRVI+k6Xq4QgWxbcZRUv+SfG1y7HJ28JdV8yEJu6k+C/YLea7npGckX2DQJeEVuMc4bKOBeU/2LQ==
-  dependencies:
-    safari-14-idb-fix "^1.0.4"
-
-ieee754@1.1.13, ieee754@^1.1.4:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
-  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
-
-ieee754@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
-  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-ignore@^4.0.6:
-  version "4.0.6"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
-  integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
-
-ignore@^5.1.4:
-  version "5.1.8"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
-  integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
-
-immutable@^3.8.2:
-  version "3.8.2"
-  resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
-  integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
-
-import-fresh@^3.0.0, import-fresh@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
-  integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
-  dependencies:
-    parent-module "^1.0.0"
-    resolve-from "^4.0.0"
-
-import-fresh@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
-  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
-  dependencies:
-    parent-module "^1.0.0"
-    resolve-from "^4.0.0"
-
-import-local@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
-  integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
-  dependencies:
-    pkg-dir "^4.2.0"
-    resolve-cwd "^3.0.0"
-
-imurmurhash@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
-  integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
-
 indent-string@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
@@ -5687,16 +1998,6 @@ indexes-of@^1.0.1:
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
   integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
 
-infer-owner@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
-  integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
-
-inflation@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f"
-  integrity sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=
-
 inflight@^1.0.4:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -5705,113 +2006,36 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
-inherits@2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
-  integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
-
 ini@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
   integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
 
-ini@^1.3.4, ini@~1.3.0:
+ini@^1.3.4:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
   integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
 
-insert-text-at-cursor@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/insert-text-at-cursor/-/insert-text-at-cursor-0.3.0.tgz#1819607680ec1570618347c4cd475e791faa25da"
-  integrity sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==
-
-install-artifact-from-github@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.2.0.tgz#adcbd123c16a4337ec44ea76d0ebf253cc16b074"
-  integrity sha512-3OxCPcY55XlVM3kkfIpeCgmoSKnMsz2A3Dbhsq0RXpIknKQmrX1YiznCeW9cD2ItFmDxziA3w6Eg8d80AoL3oA==
-
 interpret@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
   integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
 
-interpret@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
-  integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
-
 invert-kv@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
-  integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
-
-invert-kv@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-3.0.1.tgz#a93c7a3d4386a1dc8325b97da9bb1620c0282523"
-  integrity sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==
-
-ioredis@^4.27.0:
-  version "4.27.6"
-  resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.6.tgz#a53d427d3fe75fbd10ed7ad150ce00559df8dcf8"
-  integrity sha512-6W3ZHMbpCa8ByMyC1LJGOi7P2WiOKP9B3resoZOVLDhi+6dDBOW+KNsRq3yI36Hmnb2sifCxHX+YSarTeXh48A==
-  dependencies:
-    cluster-key-slot "^1.1.0"
-    debug "^4.3.1"
-    denque "^1.1.0"
-    lodash.defaults "^4.2.0"
-    lodash.flatten "^4.4.0"
-    p-map "^2.1.0"
-    redis-commands "1.7.0"
-    redis-errors "^1.2.0"
-    redis-parser "^3.0.0"
-    standard-as-callback "^2.1.0"
-
-ip-address@^7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-7.1.0.tgz#4a9c699e75b51cbeb18b38de8ed216efa1a490c5"
-  integrity sha512-V9pWC/VJf2lsXqP7IWJ+pe3P1/HCYGBMZrrnT62niLGjAfCbeiwXMUxaeHvnVlz19O27pvXP4azs+Pj/A0x+SQ==
-  dependencies:
-    jsbn "1.1.0"
-    sprintf-js "1.1.2"
-
-ip-cidr@3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.4.tgz#a915c47e00f47ea8d5f8ed662ea6161471c44375"
-  integrity sha512-pKNiqmBlTvEkhaLAa3+FOmYSY0/jjADVxxjA3NbujZZTT8mjLI90Q+6mwg6kd0fNm0RuAOkWJ1u1a/ETmlrPNQ==
-  dependencies:
-    ip-address "^7.1.0"
-    jsbn "^1.1.0"
-
-ip-regex@^4.0.0, ip-regex@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
-  integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
-
-ip@^1.1.5:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
-  integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
-
-ipaddr.js@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
-  integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
+  integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
 
 is-absolute-url@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
   integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
 
-is-absolute-url@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
-  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
-
 is-absolute@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576"
@@ -5839,10 +2063,12 @@ is-arrayish@^0.2.1:
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
   integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
 
-is-arrayish@^0.3.1:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
-  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+is-binary-path@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+  integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+  dependencies:
+    binary-extensions "^1.0.0"
 
 is-binary-path@~2.1.0:
   version "2.1.0"
@@ -5856,11 +2082,6 @@ is-buffer@^1.1.5:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-callable@^1.1.4, is-callable@^1.1.5:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
-  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
-
 is-ci@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994"
@@ -5875,13 +2096,6 @@ is-core-module@^2.0.0:
   dependencies:
     has "^1.0.3"
 
-is-core-module@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
-  integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
-  dependencies:
-    has "^1.0.3"
-
 is-data-descriptor@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -5896,11 +2110,6 @@ is-data-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
-is-date-object@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
-  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
-
 is-descriptor@^0.1.0:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
@@ -5919,14 +2128,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2:
     is-data-descriptor "^1.0.0"
     kind-of "^6.0.2"
 
-is-expression@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab"
-  integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==
-  dependencies:
-    acorn "^7.1.1"
-    object-assign "^4.1.1"
-
 is-extendable@^0.1.0, is-extendable@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@@ -5951,21 +2152,11 @@ is-fullwidth-code-point@^1.0.0:
   dependencies:
     number-is-nan "^1.0.0"
 
-is-fullwidth-code-point@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
-  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
-
 is-fullwidth-code-point@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
   integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
 
-is-generator-function@^1.0.7:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522"
-  integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==
-
 is-glob@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
@@ -5980,13 +2171,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
   dependencies:
     is-extglob "^2.1.1"
 
-is-glob@^4.0.3:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
-  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
-  dependencies:
-    is-extglob "^2.1.1"
-
 is-installed-globally@~0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
@@ -5995,37 +2179,11 @@ is-installed-globally@~0.4.0:
     global-dirs "^3.0.0"
     is-path-inside "^3.0.2"
 
-is-ip@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8"
-  integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==
-  dependencies:
-    ip-regex "^4.0.0"
-
-is-lambda@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
-  integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=
-
-is-nan@^1.2.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03"
-  integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==
-  dependencies:
-    define-properties "^1.1.3"
-
 is-negated-glob@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2"
   integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=
 
-is-number-like@^1.0.3:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3"
-  integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==
-  dependencies:
-    lodash.isfinite "^3.3.2"
-
 is-number@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -6053,11 +2211,6 @@ is-plain-obj@^1.0.0:
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
   integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
 
-is-plain-obj@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
-  integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
-
 is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -6065,23 +2218,6 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   dependencies:
     isobject "^3.0.1"
 
-is-potential-custom-element-name@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
-  integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
-
-is-promise@^2.0.0:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
-  integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
-
-is-regex@^1.0.3, is-regex@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
-  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
-  dependencies:
-    has "^1.0.3"
-
 is-relative@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d"
@@ -6089,23 +2225,11 @@ is-relative@^1.0.0:
   dependencies:
     is-unc-path "^1.0.0"
 
-is-resolvable@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
-  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
-
 is-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
   integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
 
-is-svg@4.3.1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.1.tgz#8c63ec8c67c8c7f0a8de0a71c8c7d58eccf4406b"
-  integrity sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA==
-  dependencies:
-    fast-xml-parser "^3.19.0"
-
 is-svg@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9"
@@ -6113,14 +2237,7 @@ is-svg@^2.0.0:
   dependencies:
     html-comment-regex "^1.1.0"
 
-is-symbol@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
-  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
-  dependencies:
-    has-symbols "^1.0.1"
-
-is-typedarray@^1.0.0, is-typedarray@~1.0.0:
+is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
@@ -6147,31 +2264,16 @@ is-valid-glob@^1.0.0:
   resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa"
   integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=
 
-is-whitespace@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f"
-  integrity sha1-Fjnssb4DauxppUy7QBz77XEUq38=
-
 is-windows@^1.0.1, is-windows@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
   integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
 
-isarray@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
-  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
-
-isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+isarray@1.0.0, isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
   integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
 
-isarray@^2.0.1:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
-  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
-
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -6202,29 +2304,6 @@ istextorbinary@^3.0.0:
     binaryextensions "^2.2.0"
     textextensions "^3.2.0"
 
-jest-worker@^26.6.2:
-  version "26.6.2"
-  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
-  integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
-  dependencies:
-    "@types/node" "*"
-    merge-stream "^2.0.0"
-    supports-color "^7.0.0"
-
-jest-worker@^27.0.2:
-  version "27.0.6"
-  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed"
-  integrity sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==
-  dependencies:
-    "@types/node" "*"
-    merge-stream "^2.0.0"
-    supports-color "^8.0.0"
-
-jmespath@0.15.0:
-  version "0.15.0"
-  resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
-  integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
-
 joi@^17.4.0:
   version "17.4.2"
   resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.2.tgz#02f4eb5cf88e515e614830239379dcbbe28ce7f7"
@@ -6236,57 +2315,17 @@ joi@^17.4.0:
     "@sideway/formula" "^3.0.0"
     "@sideway/pinpoint" "^2.0.0"
 
-jpeg-js@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.1.tgz#937a3ae911eb6427f151760f8123f04c8bfe6ef7"
-  integrity sha512-jA55yJiB5tCXEddos8JBbvW+IMrqY0y1tjjx9KNVtA+QPmu7ND5j0zkKopClpUTsaETL135uOM2XfcYG4XRjmw==
-
 js-base64@^2.1.9:
   version "2.6.4"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
   integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
 
-js-beautify@^1.6.12:
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.11.0.tgz#afb873dc47d58986360093dcb69951e8bcd5ded2"
-  integrity sha512-a26B+Cx7USQGSWnz9YxgJNMmML/QG2nqIaL7VVYPCXbqiKz8PN0waSNvroMtvAK6tY7g/wPdNWGEP+JTNIBr6A==
-  dependencies:
-    config-chain "^1.1.12"
-    editorconfig "^0.15.3"
-    glob "^7.1.3"
-    mkdirp "~1.0.3"
-    nopt "^4.0.3"
-
 js-levenshtein@^1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
   integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
 
-js-stringify@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
-  integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds=
-
-js-tokens@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
-  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-
-js-yaml@4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
-  integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
-  dependencies:
-    argparse "^2.0.1"
-
-js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
-  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
-  dependencies:
-    argparse "^2.0.1"
-
-js-yaml@^3.13.1, js-yaml@^3.14.1:
+js-yaml@^3.14.1:
   version "3.14.1"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
   integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
@@ -6302,69 +2341,11 @@ js-yaml@~3.7.0:
     argparse "^1.0.7"
     esprima "^2.6.0"
 
-jsbn@1.1.0, jsbn@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
-  integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA=
-
 jsbn@~0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
-jschardet@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882"
-  integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==
-
-jsdom@16.7.0:
-  version "16.7.0"
-  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710"
-  integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==
-  dependencies:
-    abab "^2.0.5"
-    acorn "^8.2.4"
-    acorn-globals "^6.0.0"
-    cssom "^0.4.4"
-    cssstyle "^2.3.0"
-    data-urls "^2.0.0"
-    decimal.js "^10.2.1"
-    domexception "^2.0.1"
-    escodegen "^2.0.0"
-    form-data "^3.0.0"
-    html-encoding-sniffer "^2.0.1"
-    http-proxy-agent "^4.0.1"
-    https-proxy-agent "^5.0.0"
-    is-potential-custom-element-name "^1.0.1"
-    nwsapi "^2.2.0"
-    parse5 "6.0.1"
-    saxes "^5.0.1"
-    symbol-tree "^3.2.4"
-    tough-cookie "^4.0.0"
-    w3c-hr-time "^1.0.2"
-    w3c-xmlserializer "^2.0.0"
-    webidl-conversions "^6.1.0"
-    whatwg-encoding "^1.0.5"
-    whatwg-mimetype "^2.3.0"
-    whatwg-url "^8.5.0"
-    ws "^7.4.6"
-    xml-name-validator "^3.0.0"
-
-json-buffer@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
-  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
-
-json-parse-better-errors@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
-  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
-
-json-parse-even-better-errors@^2.3.0:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
-  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
-
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -6385,57 +2366,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
   resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
   integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
 
-json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
+json-stringify-safe@~5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
   integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
 
-json5-loader@4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/json5-loader/-/json5-loader-4.0.1.tgz#6d17a1181e8f3c3d9204dca2a4ce4627306c8498"
-  integrity sha512-c9viNZlZTz0MTIcf/4qvek5Dz1/PU3DNCB4PwUhlEZIV3qb1bSD6vQQymlV17/Wm6ncra1aCvmIPsuRj+KfEEg==
-  dependencies:
-    json5 "^2.1.3"
-    loader-utils "^2.0.0"
-    schema-utils "^3.0.0"
-
-json5@2.2.0, json5@2.x:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
-  integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
-  dependencies:
-    minimist "^1.2.5"
-
-json5@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
-  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
-  dependencies:
-    minimist "^1.2.0"
-
-json5@^2.1.2, json5@^2.1.3:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
-  integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
-  dependencies:
-    minimist "^1.2.5"
-
-jsonfile@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
-  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
-jsonfile@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
-  integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
-  dependencies:
-    universalify "^0.1.2"
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
 jsonfile@^6.0.1:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@@ -6445,16 +2380,6 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
-jsonld@5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-5.2.0.tgz#d1e8af38a334cb95edf0f2ae4e2b58baf8d2b5a9"
-  integrity sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw==
-  dependencies:
-    "@digitalbazaar/http-client" "^1.1.0"
-    canonicalize "^1.0.1"
-    lru-cache "^6.0.0"
-    rdf-canonize "^3.0.0"
-
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -6465,62 +2390,11 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
-jsrsasign@8.0.20:
-  version "8.0.20"
-  resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.20.tgz#37d8029c9d8f794d8ac8d8998bce319921491f11"
-  integrity sha512-JTXt9+nqdynIB8wFsS6e8ffHhIjilhywXwdaEVHSj9OVmwldG2H0EoCqkQ+KXkm2tVqREfH/HEmklY4k1/6Rcg==
-
-jstransformer@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
-  integrity sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=
-  dependencies:
-    is-promise "^2.0.0"
-    promise "^7.0.1"
-
 just-debounce@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea"
   integrity sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=
 
-jwa@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
-  integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
-  dependencies:
-    buffer-equal-constant-time "1.0.1"
-    ecdsa-sig-formatter "1.0.11"
-    safe-buffer "^5.0.1"
-
-jws@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
-  integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
-  dependencies:
-    jwa "^2.0.0"
-    safe-buffer "^5.0.1"
-
-katex@0.13.18:
-  version "0.13.18"
-  resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.18.tgz#ba89e8e4b70cc2325e25e019a62b9fe71e5c2931"
-  integrity sha512-a3dC4NSVSDU3O1WZbTnOiA8rVNJ2lSiomOl0kmckCIGObccIHXof7gAseIY0o1gjEspe+34ZeSEX2D1ChFKIvA==
-  dependencies:
-    commander "^6.0.0"
-
-keygrip@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
-  integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==
-  dependencies:
-    tsscmp "1.0.6"
-
-keyv@^4.0.0:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254"
-  integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==
-  dependencies:
-    json-buffer "3.0.1"
-
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -6545,199 +2419,6 @@ kind-of@^6.0.0, kind-of@^6.0.2:
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
-klona@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
-  integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
-
-koa-bodyparser@4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz#274c778555ff48fa221ee7f36a9fbdbace22759a"
-  integrity sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==
-  dependencies:
-    co-body "^6.0.0"
-    copy-to "^2.0.1"
-
-koa-compose@^3.0.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
-  integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=
-  dependencies:
-    any-promise "^1.1.0"
-
-koa-compose@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
-  integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
-
-koa-convert@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
-  integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=
-  dependencies:
-    co "^4.6.0"
-    koa-compose "^3.0.0"
-
-koa-convert@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5"
-  integrity sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==
-  dependencies:
-    co "^4.6.0"
-    koa-compose "^4.1.0"
-
-koa-favicon@2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/koa-favicon/-/koa-favicon-2.1.0.tgz#c430cc594614fb494adcb5ee1196a2f7f53ea442"
-  integrity sha512-LvukcooYjxKtnZq0RXdBup+JDhaHwLgnLlDHB/xvjwQEjbc4rbp/0WkmOzpOvaHujc+fIwPear0dpKX1V+dHVg==
-  dependencies:
-    mz "^2.7.0"
-
-koa-json-body@5.3.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/koa-json-body/-/koa-json-body-5.3.0.tgz#64aad3f400adfb81df54b63f7a5eb38bad62d980"
-  integrity sha1-ZKrT9ACt+4HfVLY/el6zi61i2YA=
-  dependencies:
-    co-body "^5.0.0"
-
-koa-logger@3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/koa-logger/-/koa-logger-3.2.1.tgz#ab9db879526db3837cc9ce4fd983c025b1689f22"
-  integrity sha512-MjlznhLLKy9+kG8nAXKJLM0/ClsQp/Or2vI3a5rbSQmgl8IJBQO0KI5FA70BvW+hqjtxjp49SpH2E7okS6NmHg==
-  dependencies:
-    bytes "^3.1.0"
-    chalk "^2.4.2"
-    humanize-number "0.0.2"
-    passthrough-counter "^1.0.0"
-
-koa-mount@4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/koa-mount/-/koa-mount-4.0.0.tgz#e0265e58198e1a14ef889514c607254ff386329c"
-  integrity sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==
-  dependencies:
-    debug "^4.0.1"
-    koa-compose "^4.1.0"
-
-koa-send@5.0.1, koa-send@^5.0.0:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79"
-  integrity sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==
-  dependencies:
-    debug "^4.1.1"
-    http-errors "^1.7.3"
-    resolve-path "^1.4.0"
-
-koa-slow@2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/koa-slow/-/koa-slow-2.1.0.tgz#39007ca628c620f2b307b90dbf423d7a0c9be971"
-  integrity sha1-OQB8pijGIPKzB7kNv0I9egyb6XE=
-  dependencies:
-    lodash.isregexp "3.0.5"
-    q "1.4.1"
-
-koa-views@*:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/koa-views/-/koa-views-7.0.1.tgz#0c8f8e65d5cd2e08249430cb83dc361e49a17a5a"
-  integrity sha512-yS8751DXHXXDbdl/oUZd0PsgnxR0MLiguu77Eqrgu6yawE9Hi99wNKiVENb0Kfgsmvq/8px7YCI+USgxaTB1LA==
-  dependencies:
-    "@types/koa" "^2.13.1"
-    consolidate "^0.16.0"
-    debug "^4.1.0"
-    get-paths "0.0.7"
-    koa-send "^5.0.0"
-    mz "^2.4.0"
-    pretty "^2.0.0"
-    resolve-path "^1.4.0"
-
-koa-views@7.0.2:
-  version "7.0.2"
-  resolved "https://registry.yarnpkg.com/koa-views/-/koa-views-7.0.2.tgz#c96fd9e2143ef00c29dc5160c5ed639891aa723d"
-  integrity sha512-dvx3mdVeSVuIPEaKAoGbxLcenudvhl821xxyuRbcoA+bOJ2dvN8wlGjkLu0ZFMlkCscXZV6lzxy28rafeazI/w==
-  dependencies:
-    consolidate "^0.16.0"
-    debug "^4.1.0"
-    get-paths "0.0.7"
-    koa-send "^5.0.0"
-    mz "^2.4.0"
-    pretty "^2.0.0"
-    resolve-path "^1.4.0"
-
-koa@2.13.1:
-  version "2.13.1"
-  resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.1.tgz#6275172875b27bcfe1d454356a5b6b9f5a9b1051"
-  integrity sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w==
-  dependencies:
-    accepts "^1.3.5"
-    cache-content-type "^1.0.0"
-    content-disposition "~0.5.2"
-    content-type "^1.0.4"
-    cookies "~0.8.0"
-    debug "~3.1.0"
-    delegates "^1.0.0"
-    depd "^2.0.0"
-    destroy "^1.0.4"
-    encodeurl "^1.0.2"
-    escape-html "^1.0.3"
-    fresh "~0.5.2"
-    http-assert "^1.3.0"
-    http-errors "^1.6.3"
-    is-generator-function "^1.0.7"
-    koa-compose "^4.1.0"
-    koa-convert "^1.2.0"
-    on-finished "^2.3.0"
-    only "~0.0.2"
-    parseurl "^1.3.2"
-    statuses "^1.5.0"
-    type-is "^1.6.16"
-    vary "^1.1.2"
-
-koa@2.13.4:
-  version "2.13.4"
-  resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e"
-  integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==
-  dependencies:
-    accepts "^1.3.5"
-    cache-content-type "^1.0.0"
-    content-disposition "~0.5.2"
-    content-type "^1.0.4"
-    cookies "~0.8.0"
-    debug "^4.3.2"
-    delegates "^1.0.0"
-    depd "^2.0.0"
-    destroy "^1.0.4"
-    encodeurl "^1.0.2"
-    escape-html "^1.0.3"
-    fresh "~0.5.2"
-    http-assert "^1.3.0"
-    http-errors "^1.6.3"
-    is-generator-function "^1.0.7"
-    koa-compose "^4.1.0"
-    koa-convert "^2.0.0"
-    on-finished "^2.3.0"
-    only "~0.0.2"
-    parseurl "^1.3.2"
-    statuses "^1.5.0"
-    type-is "^1.6.16"
-    vary "^1.1.2"
-
-ky-universal@^0.8.2:
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.8.2.tgz#edc398d54cf495d7d6830aa1ab69559a3cc7f824"
-  integrity sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ==
-  dependencies:
-    abort-controller "^3.0.0"
-    node-fetch "3.0.0-beta.9"
-
-ky@^0.25.1:
-  version "0.25.1"
-  resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc"
-  integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA==
-
-langmap@0.0.16:
-  version "0.0.16"
-  resolved "https://registry.yarnpkg.com/langmap/-/langmap-0.0.16.tgz#2fe3e98a531fec0fec546624ebe168c2855bab56"
-  integrity sha512-AtYvBK7BsDvWwnSfmO7CfgeUy7GUT1wK3QX8eKH/Ey/eXodqoHuAtvdQ82hmWD9QVFVKnuiNjym9fGY4qSJeLA==
-
 last-run@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/last-run/-/last-run-1.1.1.tgz#45b96942c17b1c79c772198259ba943bebf8ca5b"
@@ -6765,13 +2446,6 @@ lcid@^1.0.0:
   dependencies:
     invert-kv "^1.0.0"
 
-lcid@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/lcid/-/lcid-3.1.1.tgz#9030ec479a058fc36b5e8243ebaac8b6ac582fd0"
-  integrity sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==
-  dependencies:
-    invert-kv "^3.0.0"
-
 lead@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42"
@@ -6779,22 +2453,6 @@ lead@^1.0.0:
   dependencies:
     flush-write-stream "^1.0.2"
 
-levn@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
-  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
-  dependencies:
-    prelude-ls "^1.2.1"
-    type-check "~0.4.0"
-
-levn@~0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
-  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
-  dependencies:
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-
 liftoff@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3"
@@ -6809,16 +2467,6 @@ liftoff@^3.1.0:
     rechoir "^0.6.2"
     resolve "^1.1.7"
 
-lilconfig@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
-  integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
-
-lines-and-columns@^1.1.6:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
-  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
-
 listr2@^3.8.3:
   version "3.11.0"
   resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.11.0.tgz#9771b02407875aa78e73d6e0ff6541bbec0aaee9"
@@ -6843,158 +2491,31 @@ load-json-file@^1.0.0:
     pinkie-promise "^2.0.0"
     strip-bom "^2.0.0"
 
-loader-runner@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
-  integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
-
-loader-utils@^1.0.2:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
-  integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
-  dependencies:
-    big.js "^5.2.2"
-    emojis-list "^3.0.0"
-    json5 "^1.0.1"
-
-loader-utils@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
-  integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
-  dependencies:
-    big.js "^5.2.2"
-    emojis-list "^3.0.0"
-    json5 "^2.1.2"
-
-locate-path@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
-  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
-  dependencies:
-    p-locate "^3.0.0"
-    path-exists "^3.0.0"
-
-locate-path@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
-  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
-  dependencies:
-    p-locate "^4.1.0"
-
-locate-path@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
-  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
-  dependencies:
-    p-locate "^5.0.0"
-
-lodash.assignin@^4.0.9:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
-  integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI=
-
-lodash.bind@^4.1.4:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
-  integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=
-
-lodash.defaults@^4.0.1, lodash.defaults@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
-  integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
-
-lodash.filter@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
-  integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=
-
-lodash.flatten@^4.2.0, lodash.flatten@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
-  integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
-
-lodash.foreach@^4.3.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
-  integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=
-
 lodash.isequal@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
   integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
 
-lodash.isfinite@^3.3.2:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
-  integrity sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=
-
-lodash.isregexp@3.0.5:
-  version "3.0.5"
-  resolved "https://registry.yarnpkg.com/lodash.isregexp/-/lodash.isregexp-3.0.5.tgz#e0f596242f2fa228a840086b6c8ad82e4b71fd2d"
-  integrity sha1-4PWWJC8voiioQAhrbIrYLktx/S0=
-
-lodash.map@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
-  integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=
-
-lodash.memoize@4.x, lodash.memoize@^4.1.2:
+lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
 
-lodash.merge@^4.4.0, lodash.merge@^4.6.2:
-  version "4.6.2"
-  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
-  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
-
 lodash.once@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
   integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
 
-lodash.pick@^4.2.1:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
-  integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
-
-lodash.reduce@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
-  integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=
-
-lodash.reject@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
-  integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=
-
-lodash.some@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
-  integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=
-
-lodash.sortby@^4.7.0:
-  version "4.7.0"
-  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
-  integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
-
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0:
+lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-log-symbols@4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
-  integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
-  dependencies:
-    chalk "^4.0.0"
-
 log-symbols@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
@@ -7013,64 +2534,6 @@ log-update@^4.0.0:
     slice-ansi "^4.0.0"
     wrap-ansi "^6.2.0"
 
-lowercase-keys@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
-  integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
-
-lru-cache@^4.1.5:
-  version "4.1.5"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
-  integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
-  dependencies:
-    pseudomap "^1.0.2"
-    yallist "^2.1.2"
-
-lru-cache@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
-  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
-  dependencies:
-    yallist "^4.0.0"
-
-magic-string@^0.25.7:
-  version "0.25.7"
-  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
-  integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
-  dependencies:
-    sourcemap-codec "^1.4.4"
-
-mailcheck@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/mailcheck/-/mailcheck-1.1.1.tgz#d87cf6ba0b64ba512199dbf93f1489f479591e34"
-  integrity sha1-2Hz2ugtkulEhmdv5PxSJ9HlZHjQ=
-
-make-error@1.x, make-error@^1.1.1:
-  version "1.3.6"
-  resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
-  integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
-
-make-fetch-happen@^8.0.14:
-  version "8.0.14"
-  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222"
-  integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==
-  dependencies:
-    agentkeepalive "^4.1.3"
-    cacache "^15.0.5"
-    http-cache-semantics "^4.1.0"
-    http-proxy-agent "^4.0.1"
-    https-proxy-agent "^5.0.0"
-    is-lambda "^1.0.1"
-    lru-cache "^6.0.0"
-    minipass "^3.1.3"
-    minipass-collect "^1.0.2"
-    minipass-fetch "^1.3.2"
-    minipass-flush "^1.0.5"
-    minipass-pipeline "^1.2.4"
-    promise-retry "^2.0.1"
-    socks-proxy-agent "^5.0.0"
-    ssri "^8.0.0"
-
 make-iterator@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6"
@@ -7078,13 +2541,6 @@ make-iterator@^1.0.0:
   dependencies:
     kind-of "^6.0.2"
 
-map-age-cleaner@^0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
-  integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
-  dependencies:
-    p-defer "^1.0.0"
-
 map-cache@^0.2.0, map-cache@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@@ -7117,81 +2573,17 @@ matchdep@^2.0.0:
     resolve "^1.4.0"
     stack-trace "0.0.10"
 
-math-expression-evaluator@^1.2.14:
-  version "1.3.7"
-  resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.3.7.tgz#1b62225db86af06f7ea1fd9576a34af605a5b253"
-  integrity sha512-nrbaifCl42w37hYd6oRLvoymFK42tWB+WQTMFtksDGQMi5GvlJwnz/CsS30FFAISFLtX+A0csJ0xLiuuyyec7w==
-
-matter-js@0.17.1:
-  version "0.17.1"
-  resolved "https://registry.yarnpkg.com/matter-js/-/matter-js-0.17.1.tgz#b30ac4c708116258fbcaacd2efd8a94e34a91c7f"
-  integrity sha512-pSquoENJgvSAlQGcA0s5UkmEohGXZaUww2g3B6qG87x0iEcVf+aigMXn5UkFPdnh6w3B+C4vXSLaYqhHwKrOLA==
-
-mdn-data@2.0.14:
-  version "2.0.14"
-  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
-  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
-
-mdn-data@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
-  integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
-
-mdn-data@2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
-  integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
-
-media-typer@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
-  integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
-
-mem@^5.0.0:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/mem/-/mem-5.1.1.tgz#7059b67bf9ac2c924c9f1cff7155a064394adfb3"
-  integrity sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==
-  dependencies:
-    map-age-cleaner "^0.1.3"
-    mimic-fn "^2.1.0"
-    p-is-promise "^2.1.0"
+math-expression-evaluator@^1.2.14:
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.3.7.tgz#1b62225db86af06f7ea1fd9576a34af605a5b253"
+  integrity sha512-nrbaifCl42w37hYd6oRLvoymFK42tWB+WQTMFtksDGQMi5GvlJwnz/CsS30FFAISFLtX+A0csJ0xLiuuyyec7w==
 
 merge-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
   integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
 
-merge2@^1.3.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
-  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-
-merge@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98"
-  integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==
-
-methods@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-  integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
-
-mfm-js@0.20.0:
-  version "0.20.0"
-  resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.20.0.tgz#3afdcd7959461fd825aa8af9b9e8a57cdbddc290"
-  integrity sha512-1+3tV3nWUKQNh/ztX3wXu5iLBtdsg6q3wUhl+XyOhc2H3sQdG+sih/w2c0nR9TIawjN+Z1/pvgGzxMJHfmKQmA==
-  dependencies:
-    twemoji-parser "13.1.x"
-
-micromatch@4.x:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
-  integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
-  dependencies:
-    braces "^3.0.1"
-    picomatch "^2.2.3"
-
-micromatch@^3.0.4, micromatch@^3.1.4:
+micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
   integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
@@ -7210,25 +2602,12 @@ micromatch@^3.0.4, micromatch@^3.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-micromatch@^4.0.0, micromatch@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
-  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
-  dependencies:
-    braces "^3.0.1"
-    picomatch "^2.0.5"
-
-microseconds@0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
-  integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
-
 mime-db@1.44.0:
   version "1.44.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
   integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
 
-mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24:
+mime-types@^2.1.12, mime-types@~2.1.19:
   version "2.1.27"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
   integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
@@ -7240,108 +2619,18 @@ mimic-fn@^2.1.0:
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
-mimic-response@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
-  integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
-
-mimic-response@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
-  integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
-
-mimic-response@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
-  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
-
-minimalistic-assert@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
-  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
-
-minimatch@3.0.4, minimatch@^3.0.4:
+minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
+minimist@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
-minipass-collect@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
-  integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
-  dependencies:
-    minipass "^3.0.0"
-
-minipass-fetch@^1.3.2:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a"
-  integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==
-  dependencies:
-    minipass "^3.1.0"
-    minipass-sized "^1.0.3"
-    minizlib "^2.0.0"
-  optionalDependencies:
-    encoding "^0.1.12"
-
-minipass-flush@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
-  integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
-  dependencies:
-    minipass "^3.0.0"
-
-minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
-  integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
-  dependencies:
-    minipass "^3.0.0"
-
-minipass-sized@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70"
-  integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==
-  dependencies:
-    minipass "^3.0.0"
-
-minipass@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5"
-  integrity sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==
-  dependencies:
-    yallist "^4.0.0"
-
-minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
-  integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
-  dependencies:
-    yallist "^4.0.0"
-
-minizlib@^2.0.0, minizlib@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
-  integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
-  dependencies:
-    minipass "^3.0.0"
-    yallist "^4.0.0"
-
-misskey-js@0.0.8:
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.8.tgz#b47a1ec54bff96b23cc2a6b307a3895f99a94424"
-  integrity sha512-Q1L6FaroVz8kpW7T4xQyJmJKSwjOYPbNY3TspOUWmbIBDf2JP0HVeKEFLI9dvdSL0kSkdQNz3MSVLjlHiyPqLQ==
-  dependencies:
-    autobind-decorator "^2.4.0"
-    eventemitter3 "^4.0.7"
-    reconnecting-websocket "^4.4.0"
-
 mixin-deep@^1.2.0:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@@ -7350,71 +2639,13 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
-mkdirp-classic@^0.5.2:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz#54c441ce4c96cd7790e10b41a87aa51068ecab2b"
-  integrity sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==
-
-mkdirp-classic@^0.5.3:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
-  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
-
-mkdirp@0.x, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1:
+mkdirp@~0.5.1:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
   integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
   dependencies:
     minimist "^1.2.5"
 
-mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.3:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
-  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
-mocha@8.4.0:
-  version "8.4.0"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.4.0.tgz#677be88bf15980a3cae03a73e10a0fc3997f0cff"
-  integrity sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==
-  dependencies:
-    "@ungap/promise-all-settled" "1.1.2"
-    ansi-colors "4.1.1"
-    browser-stdout "1.3.1"
-    chokidar "3.5.1"
-    debug "4.3.1"
-    diff "5.0.0"
-    escape-string-regexp "4.0.0"
-    find-up "5.0.0"
-    glob "7.1.6"
-    growl "1.10.5"
-    he "1.2.0"
-    js-yaml "4.0.0"
-    log-symbols "4.0.0"
-    minimatch "3.0.4"
-    ms "2.1.3"
-    nanoid "3.1.20"
-    serialize-javascript "5.0.1"
-    strip-json-comments "3.1.1"
-    supports-color "8.1.1"
-    which "2.0.2"
-    wide-align "1.1.3"
-    workerpool "6.1.0"
-    yargs "16.2.0"
-    yargs-parser "20.2.4"
-    yargs-unparser "2.0.0"
-
-moment-timezone@^0.5.25:
-  version "0.5.28"
-  resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338"
-  integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw==
-  dependencies:
-    moment ">= 2.9.0"
-
-"moment@>= 2.9.0", moment@^2.22.2:
-  version "2.24.0"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
-  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
-
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -7425,65 +2656,20 @@ ms@2.1.2:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
+ms@^2.1.1:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
-multer@1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b"
-  integrity sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==
-  dependencies:
-    append-field "^1.0.0"
-    busboy "^0.2.11"
-    concat-stream "^1.5.2"
-    mkdirp "^0.5.4"
-    object-assign "^4.1.1"
-    on-finished "^2.3.0"
-    type-is "^1.6.4"
-    xtend "^4.0.0"
-
 mute-stdout@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331"
   integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==
 
-mz@^2.4.0, mz@^2.7.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
-  integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
-  dependencies:
-    any-promise "^1.0.0"
-    object-assign "^4.0.1"
-    thenify-all "^1.0.0"
-
-nan@^2.14.2:
-  version "2.14.2"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
-  integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
-
-nano-time@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
-  integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
-  dependencies:
-    big-integer "^1.6.16"
-
-nanoid@3.1.20, nanoid@^3.1.20:
-  version "3.1.20"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
-  integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
-
-nanoid@^3.1.23:
-  version "3.1.23"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
-  integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
-
-nanoid@^3.1.30:
-  version "3.1.30"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
-  integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
+nan@^2.12.1:
+  version "2.15.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
+  integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -7502,138 +2688,16 @@ nanomatch@^1.2.9:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
-napi-build-utils@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
-  integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
-
-natural-compare@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
-  integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
-
-needle@^2.5.2:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.2.tgz#cf1a8fce382b5a280108bba90a14993c00e4010a"
-  integrity sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==
-  dependencies:
-    debug "^3.2.6"
-    iconv-lite "^0.4.4"
-    sax "^1.2.4"
-
-negotiator@0.6.2:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
-  integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
-
-neo-async@^2.6.2:
-  version "2.6.2"
-  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
-  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
-
-nested-property@4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-4.0.0.tgz#a67b5a31991e701e03cdbaa6453bc5b1011bb88d"
-  integrity sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==
-
-netmask@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
-  integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
-
-next-line@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603"
-  integrity sha1-/K5XhTBStqm66CCOQN19PC0wRgM=
-
 next-tick@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
   integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
 
-node-abi@^2.21.0:
-  version "2.21.0"
-  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.21.0.tgz#c2dc9ebad6f4f53d6ea9b531e7b8faad81041d48"
-  integrity sha512-smhrivuPqEM3H5LmnY3KU6HfYv0u4QklgAxfFyRNujKUzbUcYZ+Jc2EhukB9SRcD2VpqhxM7n/MIcp1Ua1/JMg==
-  dependencies:
-    semver "^5.4.1"
-
-node-addon-api@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87"
-  integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==
-
-node-fetch@2.6.1, node-fetch@^2.6.1:
+node-fetch@^2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
   integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
 
-node-fetch@3.0.0-beta.9:
-  version "3.0.0-beta.9"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0-beta.9.tgz#0a7554cfb824380dd6812864389923c783c80d9b"
-  integrity sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==
-  dependencies:
-    data-uri-to-buffer "^3.0.1"
-    fetch-blob "^2.1.1"
-
-node-gyp-build@~3.7.0:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
-  integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
-
-node-gyp@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.0.0.tgz#225af2b06b8419ae81f924bf25ae4c167f6378a5"
-  integrity sha512-Jod6NxyWtcwrpAQe0O/aXOpC5QfncotgtG73dg65z6VW/C6g/G4jiajXQUBIJ8pk/VfM6mBYE9BN/HvudTunUQ==
-  dependencies:
-    env-paths "^2.2.0"
-    glob "^7.1.4"
-    graceful-fs "^4.2.6"
-    make-fetch-happen "^8.0.14"
-    nopt "^5.0.0"
-    npmlog "^4.1.2"
-    rimraf "^3.0.2"
-    semver "^7.3.5"
-    tar "^6.1.0"
-    which "^2.0.2"
-
-node-releases@^1.1.70, node-releases@^1.1.71:
-  version "1.1.71"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb"
-  integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==
-
-nodemailer@6.7.0:
-  version "6.7.0"
-  resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.0.tgz#86614722c4e0c33d1b5b02aecb90d6d629932b0d"
-  integrity sha512-AtiTVUFHLiiDnMQ43zi0YgkzHOEWUkhDgPlBXrsDzJiJvB29Alo4OKxHQ0ugF3gRqRQIneCLtZU3yiUo7pItZw==
-
-nofilter@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-2.0.3.tgz#f5460f3cb33147005883e3f5d4476239501fa187"
-  integrity sha512-FbuXC+lK+GU2+63D1kC1ETiZo+Z7SIi7B+mxKTCH1byrh6WFvfBCN/wpherFz0a0bjGd7EKTst/cz0yLeNngug==
-  dependencies:
-    "@cto.af/textdecoder" "^0.0.0"
-
-nofilter@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-3.1.0.tgz#c757ba68801d41ff930ba2ec55bab52ca184aa66"
-  integrity sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==
-
-nopt@^4.0.3:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
-  integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
-  dependencies:
-    abbrev "1"
-    osenv "^0.1.4"
-
-nopt@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
-  integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
-  dependencies:
-    abbrev "1"
-
 normalize-package-data@^2.3.2:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -7671,16 +2735,6 @@ normalize-url@^1.4.0:
     query-string "^4.1.0"
     sort-keys "^1.0.0"
 
-normalize-url@^4.1.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
-  integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
-
-normalize-url@^6.0.1:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
-  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
-
 now-and-later@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c"
@@ -7695,30 +2749,6 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1:
   dependencies:
     path-key "^3.0.0"
 
-npmlog@^4.0.1, npmlog@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
-  integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
-  dependencies:
-    are-we-there-yet "~1.1.2"
-    console-control-strings "~1.1.0"
-    gauge "~2.7.3"
-    set-blocking "~2.0.0"
-
-nth-check@^1.0.2, nth-check@~1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
-  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
-  dependencies:
-    boolbase "~1.0.0"
-
-nth-check@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
-  integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
-  dependencies:
-    boolbase "^1.0.0"
-
 num2fraction@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@@ -7729,22 +2759,7 @@ number-is-nan@^1.0.0:
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
   integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
 
-nwsapi@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
-  integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
-
-oauth-sign@~0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
-  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
-
-oauth@0.9.15:
-  version "0.9.15"
-  resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
-  integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
-
-object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+object-assign@^4.0.1, object-assign@^4.1.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@@ -7758,12 +2773,7 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-inspect@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
-  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
-
-object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+object-keys@^1.0.11, object-keys@^1.0.12:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -7795,14 +2805,6 @@ object.defaults@^1.0.0, object.defaults@^1.1.0:
     for-own "^1.0.0"
     isobject "^3.0.0"
 
-object.getownpropertydescriptors@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
-  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-
 object.map@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37"
@@ -7826,28 +2828,6 @@ object.reduce@^1.0.0:
     for-own "^1.0.0"
     make-iterator "^1.0.0"
 
-object.values@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
-  integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
-    has "^1.0.3"
-
-oblivious-set@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
-  integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
-
-on-finished@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
-  integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
-  dependencies:
-    ee-first "1.1.1"
-
 once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -7862,40 +2842,6 @@ onetime@^5.1.0, onetime@^5.1.2:
   dependencies:
     mimic-fn "^2.1.0"
 
-only@~0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
-  integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
-
-opentype.js@^0.4.3:
-  version "0.4.11"
-  resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.4.11.tgz#281a2390639cc15931c955d8d63c14a7c7772b41"
-  integrity sha1-KBojkGOcwVkxyVXY1jwUp8d3K0E=
-
-optionator@^0.8.1:
-  version "0.8.3"
-  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
-  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
-  dependencies:
-    deep-is "~0.1.3"
-    fast-levenshtein "~2.0.6"
-    levn "~0.3.0"
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-    word-wrap "~1.2.3"
-
-optionator@^0.9.1:
-  version "0.9.1"
-  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
-  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
-  dependencies:
-    deep-is "^0.1.3"
-    fast-levenshtein "^2.0.6"
-    levn "^0.4.1"
-    prelude-ls "^1.2.1"
-    type-check "^0.4.0"
-    word-wrap "^1.2.3"
-
 ordered-read-streams@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
@@ -7903,11 +2849,6 @@ ordered-read-streams@^1.0.0:
   dependencies:
     readable-stream "^2.0.1"
 
-os-homedir@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
-
 os-locale@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
@@ -7915,105 +2856,11 @@ os-locale@^1.4.0:
   dependencies:
     lcid "^1.0.0"
 
-os-locale@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-5.0.0.tgz#6d26c1d95b6597c5d5317bf5fba37eccec3672e0"
-  integrity sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==
-  dependencies:
-    execa "^4.0.0"
-    lcid "^3.0.0"
-    mem "^5.0.0"
-
-os-tmpdir@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
-  integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
-
-os-utils@0.0.14:
-  version "0.0.14"
-  resolved "https://registry.yarnpkg.com/os-utils/-/os-utils-0.0.14.tgz#29e511697b1982b8c627722175fe39797ef64156"
-  integrity sha1-KeURaXsZgrjGJ3Ihdf45eX72QVY=
-
-osenv@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
-  integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
-  dependencies:
-    os-homedir "^1.0.0"
-    os-tmpdir "^1.0.0"
-
 ospath@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
   integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=
 
-p-cancelable@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e"
-  integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==
-
-p-defer@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
-  integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
-
-p-finally@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
-  integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
-
-p-is-promise@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
-  integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
-
-p-limit@^2.0.0, p-limit@^2.2.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
-  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
-  dependencies:
-    p-try "^2.0.0"
-
-p-limit@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe"
-  integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==
-  dependencies:
-    p-try "^2.0.0"
-
-p-limit@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
-  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
-  dependencies:
-    yocto-queue "^0.1.0"
-
-p-locate@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
-  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
-  dependencies:
-    p-limit "^2.0.0"
-
-p-locate@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
-  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
-  dependencies:
-    p-limit "^2.2.0"
-
-p-locate@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
-  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
-  dependencies:
-    p-limit "^3.0.2"
-
-p-map@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
-  integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
-
 p-map@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
@@ -8021,30 +2868,6 @@ p-map@^4.0.0:
   dependencies:
     aggregate-error "^3.0.0"
 
-p-timeout@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
-  integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
-  dependencies:
-    p-finally "^1.0.0"
-
-p-try@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
-  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-
-packet-reader@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74"
-  integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==
-
-parent-module@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
-  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
-  dependencies:
-    callsites "^3.0.0"
-
 parse-filepath@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
@@ -8061,16 +2884,6 @@ parse-json@^2.2.0:
   dependencies:
     error-ex "^1.2.0"
 
-parse-json@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646"
-  integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==
-  dependencies:
-    "@babel/code-frame" "^7.0.0"
-    error-ex "^1.3.1"
-    json-parse-even-better-errors "^2.3.0"
-    lines-and-columns "^1.1.6"
-
 parse-node-version@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
@@ -8081,38 +2894,11 @@ parse-passwd@^1.0.0:
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
   integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
 
-parse5-htmlparser2-tree-adapter@^6.0.0:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
-  integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
-  dependencies:
-    parse5 "^6.0.1"
-
-parse5@6.0.1, parse5@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
-  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
-
-parse5@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
-  integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
-
-parseurl@^1.3.2:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
-  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-
 pascalcase@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
   integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
 
-passthrough-counter@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/passthrough-counter/-/passthrough-counter-1.0.0.tgz#1967d9e66da572b5c023c787db112a387ab166fa"
-  integrity sha1-GWfZ5m2lcrXAI8eH2xEqOHqxZvo=
-
 path-dirname@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
@@ -8125,17 +2911,7 @@ path-exists@^2.0.0:
   dependencies:
     pinkie-promise "^2.0.0"
 
-path-exists@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
-  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
-
-path-exists@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
-  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-is-absolute@1.0.1, path-is-absolute@^1.0.0:
+path-is-absolute@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
@@ -8162,11 +2938,6 @@ path-root@^0.1.1:
   dependencies:
     path-root-regex "^0.1.0"
 
-path-to-regexp@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427"
-  integrity sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==
-
 path-type@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -8176,11 +2947,6 @@ path-type@^1.0.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
-path-type@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
-  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-
 pause-stream@0.0.11:
   version "0.0.11"
   resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
@@ -8188,11 +2954,6 @@ pause-stream@0.0.11:
   dependencies:
     through "~2.3"
 
-peek-readable@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.0.1.tgz#9a045f291db254111c3412c1ce4fec27ddd4d202"
-  integrity sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ==
-
 pend@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
@@ -8203,86 +2964,16 @@ performance-now@^2.1.0:
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
-pg-connection-string@^2.5.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
-  integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
-
-pg-int8@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
-  integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
-
-pg-pool@^3.4.1:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.4.1.tgz#0e71ce2c67b442a5e862a9c182172c37eda71e9c"
-  integrity sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==
-
-pg-protocol@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0"
-  integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==
-
-pg-types@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
-  integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
-  dependencies:
-    pg-int8 "1.0.1"
-    postgres-array "~2.0.0"
-    postgres-bytea "~1.0.0"
-    postgres-date "~1.0.4"
-    postgres-interval "^1.1.0"
-
-pg@8.7.1:
-  version "8.7.1"
-  resolved "https://registry.yarnpkg.com/pg/-/pg-8.7.1.tgz#9ea9d1ec225980c36f94e181d009ab9f4ce4c471"
-  integrity sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==
-  dependencies:
-    buffer-writer "2.0.0"
-    packet-reader "1.0.0"
-    pg-connection-string "^2.5.0"
-    pg-pool "^3.4.1"
-    pg-protocol "^1.5.0"
-    pg-types "^2.1.0"
-    pgpass "1.x"
-
-pgpass@1.x:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306"
-  integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=
-  dependencies:
-    split "^1.0.0"
-
-"photoswipe@git://github.com/dimsemenov/photoswipe#v5-beta":
-  version "5.1.7"
-  resolved "git://github.com/dimsemenov/photoswipe#60040164333bd257409669e715e4327afdb3aec7"
-
-picocolors@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
-  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
-
-picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7, picomatch@^2.2.1:
+picomatch@^2.0.4, picomatch@^2.0.7:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
   integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
 
-picomatch@^2.2.3:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
-  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
-
 pify@^2.0.0, pify@^2.2.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
   integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
 
-pify@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
-  integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
-
 pinkie-promise@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
@@ -8295,13 +2986,6 @@ pinkie@^2.0.0:
   resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
   integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
 
-pkg-dir@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
-  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
-  dependencies:
-    find-up "^4.0.0"
-
 plugin-error@1.0.1, plugin-error@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c"
@@ -8312,19 +2996,6 @@ plugin-error@1.0.1, plugin-error@^1.0.1:
     arr-union "^3.1.0"
     extend-shallow "^3.0.2"
 
-pngjs@^3.3.0, pngjs@^3.3.1:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
-  integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
-
-portscanner@2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1"
-  integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==
-  dependencies:
-    async "^2.6.0"
-    is-number-like "^1.0.3"
-
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -8339,14 +3010,6 @@ postcss-calc@^5.2.0:
     postcss-message-helpers "^2.0.0"
     reduce-css-calc "^1.2.6"
 
-postcss-calc@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a"
-  integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==
-  dependencies:
-    postcss-selector-parser "^6.0.2"
-    postcss-value-parser "^4.0.2"
-
 postcss-colormin@^2.1.8:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b"
@@ -8356,16 +3019,6 @@ postcss-colormin@^2.1.8:
     postcss "^5.0.13"
     postcss-value-parser "^3.2.3"
 
-postcss-colormin@^5.2.1:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.1.tgz#6e444a806fd3c578827dbad022762df19334414d"
-  integrity sha512-VVwMrEYLcHYePUYV99Ymuoi7WhKrMGy/V9/kTS0DkCoJYmmjdOMneyhzYUxcNgteKDVbrewOkSM7Wje/MFwxzA==
-  dependencies:
-    browserslist "^4.16.6"
-    caniuse-api "^3.0.0"
-    colord "^2.9.1"
-    postcss-value-parser "^4.1.0"
-
 postcss-convert-values@^2.3.4:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d"
@@ -8374,13 +3027,6 @@ postcss-convert-values@^2.3.4:
     postcss "^5.0.11"
     postcss-value-parser "^3.1.2"
 
-postcss-convert-values@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz#879b849dc3677c7d6bc94b6a2c1a3f0808798059"
-  integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
 postcss-discard-comments@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d"
@@ -8388,11 +3034,6 @@ postcss-discard-comments@^2.0.4:
   dependencies:
     postcss "^5.0.14"
 
-postcss-discard-comments@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe"
-  integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==
-
 postcss-discard-duplicates@^2.0.1:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932"
@@ -8400,11 +3041,6 @@ postcss-discard-duplicates@^2.0.1:
   dependencies:
     postcss "^5.0.4"
 
-postcss-discard-duplicates@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d"
-  integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==
-
 postcss-discard-empty@^2.0.1:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5"
@@ -8412,11 +3048,6 @@ postcss-discard-empty@^2.0.1:
   dependencies:
     postcss "^5.0.14"
 
-postcss-discard-empty@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8"
-  integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==
-
 postcss-discard-overridden@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58"
@@ -8424,11 +3055,6 @@ postcss-discard-overridden@^0.1.1:
   dependencies:
     postcss "^5.0.16"
 
-postcss-discard-overridden@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6"
-  integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==
-
 postcss-discard-unused@^2.2.1:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433"
@@ -8442,16 +3068,7 @@ postcss-filter-plugins@^2.0.0:
   resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz#82245fdf82337041645e477114d8e593aa18b8ec"
   integrity sha512-T53GVFsdinJhgwm7rg1BzbeBRomOg9y5MBVhGcsV0CxurUdVj1UlPdKtn7aqYA/c/QVkzKMjq2bSV5dKG5+AwQ==
   dependencies:
-    postcss "^5.0.4"
-
-postcss-loader@6.2.0:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.0.tgz#714370a3f567141cf4cadcdf9575f5234d186bc5"
-  integrity sha512-H9hv447QjQJVDbHj3OUdciyAXY3v5+UDduzEytAlZCVHCpNAAg/mCSwhYYqZr9BiGYhmYspU8QXxZwiHTLn3yA==
-  dependencies:
-    cosmiconfig "^7.0.0"
-    klona "^2.0.4"
-    semver "^7.3.5"
+    postcss "^5.0.4"
 
 postcss-merge-idents@^2.1.5:
   version "2.1.7"
@@ -8469,15 +3086,6 @@ postcss-merge-longhand@^2.0.1:
   dependencies:
     postcss "^5.0.4"
 
-postcss-merge-longhand@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.3.tgz#42194a5ffbaa5513edbf606ef79c44958564658b"
-  integrity sha512-kmB+1TjMTj/bPw6MCDUiqSA5e/x4fvLffiAdthra3a0m2/IjTrWsTmD3FdSskzUjEwkj5ZHBDEbv5dOcqD7CMQ==
-  dependencies:
-    css-color-names "^1.0.1"
-    postcss-value-parser "^4.1.0"
-    stylehacks "^5.0.1"
-
 postcss-merge-rules@^2.0.3:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721"
@@ -8489,17 +3097,6 @@ postcss-merge-rules@^2.0.3:
     postcss-selector-parser "^2.2.2"
     vendors "^1.0.0"
 
-postcss-merge-rules@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a"
-  integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==
-  dependencies:
-    browserslist "^4.16.6"
-    caniuse-api "^3.0.0"
-    cssnano-utils "^2.0.1"
-    postcss-selector-parser "^6.0.5"
-    vendors "^1.0.3"
-
 postcss-message-helpers@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e"
@@ -8514,13 +3111,6 @@ postcss-minify-font-values@^1.0.2:
     postcss "^5.0.4"
     postcss-value-parser "^3.0.2"
 
-postcss-minify-font-values@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf"
-  integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
 postcss-minify-gradients@^1.0.1:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1"
@@ -8529,15 +3119,6 @@ postcss-minify-gradients@^1.0.1:
     postcss "^5.0.12"
     postcss-value-parser "^3.3.0"
 
-postcss-minify-gradients@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.3.tgz#f970a11cc71e08e9095e78ec3a6b34b91c19550e"
-  integrity sha512-Z91Ol22nB6XJW+5oe31+YxRsYooxOdFKcbOqY/V8Fxse1Y3vqlNRpi1cxCqoACZTQEhl+xvt4hsbWiV5R+XI9Q==
-  dependencies:
-    colord "^2.9.1"
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
 postcss-minify-params@^1.0.4:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3"
@@ -8548,17 +3129,6 @@ postcss-minify-params@^1.0.4:
     postcss-value-parser "^3.0.2"
     uniqs "^2.0.0"
 
-postcss-minify-params@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c"
-  integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==
-  dependencies:
-    alphanum-sort "^1.0.2"
-    browserslist "^4.16.0"
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-    uniqs "^2.0.0"
-
 postcss-minify-selectors@^2.0.4:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf"
@@ -8569,42 +3139,6 @@ postcss-minify-selectors@^2.0.4:
     postcss "^5.0.14"
     postcss-selector-parser "^2.0.0"
 
-postcss-minify-selectors@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54"
-  integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==
-  dependencies:
-    alphanum-sort "^1.0.2"
-    postcss-selector-parser "^6.0.5"
-
-postcss-modules-extract-imports@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
-  integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
-
-postcss-modules-local-by-default@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
-  integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
-  dependencies:
-    icss-utils "^5.0.0"
-    postcss-selector-parser "^6.0.2"
-    postcss-value-parser "^4.1.0"
-
-postcss-modules-scope@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
-  integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
-  dependencies:
-    postcss-selector-parser "^6.0.4"
-
-postcss-modules-values@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
-  integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
-  dependencies:
-    icss-utils "^5.0.0"
-
 postcss-normalize-charset@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1"
@@ -8612,57 +3146,6 @@ postcss-normalize-charset@^1.1.0:
   dependencies:
     postcss "^5.0.5"
 
-postcss-normalize-charset@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0"
-  integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==
-
-postcss-normalize-display-values@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd"
-  integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-positions@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5"
-  integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-repeat-style@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5"
-  integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-string@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0"
-  integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-timing-functions@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c"
-  integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-unicode@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37"
-  integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==
-  dependencies:
-    browserslist "^4.16.0"
-    postcss-value-parser "^4.1.0"
-
 postcss-normalize-url@^3.0.7:
   version "3.0.8"
   resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222"
@@ -8673,22 +3156,6 @@ postcss-normalize-url@^3.0.7:
     postcss "^5.0.14"
     postcss-value-parser "^3.2.3"
 
-postcss-normalize-url@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz#ddcdfb7cede1270740cf3e4dfc6008bd96abc763"
-  integrity sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==
-  dependencies:
-    is-absolute-url "^3.0.3"
-    normalize-url "^6.0.1"
-    postcss-value-parser "^4.1.0"
-
-postcss-normalize-whitespace@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a"
-  integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-
 postcss-ordered-values@^2.1.0:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d"
@@ -8697,14 +3164,6 @@ postcss-ordered-values@^2.1.0:
     postcss "^5.0.4"
     postcss-value-parser "^3.0.1"
 
-postcss-ordered-values@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044"
-  integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
 postcss-reduce-idents@^2.2.2:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3"
@@ -8720,14 +3179,6 @@ postcss-reduce-initial@^1.0.0:
   dependencies:
     postcss "^5.0.4"
 
-postcss-reduce-initial@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946"
-  integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==
-  dependencies:
-    browserslist "^4.16.0"
-    caniuse-api "^3.0.0"
-
 postcss-reduce-transforms@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1"
@@ -8737,14 +3188,6 @@ postcss-reduce-transforms@^1.0.3:
     postcss "^5.0.8"
     postcss-value-parser "^3.0.1"
 
-postcss-reduce-transforms@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640"
-  integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==
-  dependencies:
-    cssnano-utils "^2.0.1"
-    postcss-value-parser "^4.1.0"
-
 postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90"
@@ -8754,24 +3197,6 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
     indexes-of "^1.0.1"
     uniq "^1.0.1"
 
-postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
-  integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
-  dependencies:
-    cssesc "^3.0.0"
-    indexes-of "^1.0.1"
-    uniq "^1.0.1"
-    util-deprecate "^1.0.2"
-
-postcss-selector-parser@^6.0.5:
-  version "6.0.6"
-  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea"
-  integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==
-  dependencies:
-    cssesc "^3.0.0"
-    util-deprecate "^1.0.2"
-
 postcss-svgo@^2.1.1:
   version "2.1.6"
   resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
@@ -8782,14 +3207,6 @@ postcss-svgo@^2.1.1:
     postcss-value-parser "^3.2.3"
     svgo "^0.7.0"
 
-postcss-svgo@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30"
-  integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA==
-  dependencies:
-    postcss-value-parser "^4.1.0"
-    svgo "^2.7.0"
-
 postcss-unique-selectors@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d"
@@ -8799,25 +3216,11 @@ postcss-unique-selectors@^2.0.2:
     postcss "^5.0.4"
     uniqs "^2.0.0"
 
-postcss-unique-selectors@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc"
-  integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==
-  dependencies:
-    alphanum-sort "^1.0.2"
-    postcss-selector-parser "^6.0.5"
-    uniqs "^2.0.0"
-
 postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
   integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
 
-postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
-  integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
-
 postcss-zindex@^2.0.1:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22"
@@ -8827,15 +3230,6 @@ postcss-zindex@^2.0.1:
     postcss "^5.0.4"
     uniqs "^2.0.0"
 
-postcss@8.3.11:
-  version "8.3.11"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858"
-  integrity sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==
-  dependencies:
-    nanoid "^3.1.30"
-    picocolors "^1.0.0"
-    source-map-js "^0.6.2"
-
 postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.2.16:
   version "5.2.18"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5"
@@ -8846,75 +3240,6 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
     source-map "^0.5.6"
     supports-color "^3.2.3"
 
-postcss@^8.1.10:
-  version "8.2.8"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
-  integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
-  dependencies:
-    colorette "^1.2.2"
-    nanoid "^3.1.20"
-    source-map "^0.6.1"
-
-postcss@^8.2.15:
-  version "8.3.0"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f"
-  integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==
-  dependencies:
-    colorette "^1.2.2"
-    nanoid "^3.1.23"
-    source-map-js "^0.6.2"
-
-postgres-array@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
-  integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
-
-postgres-bytea@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35"
-  integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=
-
-postgres-date@~1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.5.tgz#710b27de5f27d550f6e80b5d34f7ba189213c2ee"
-  integrity sha512-pdau6GRPERdAYUQwkBnGKxEfPyhVZXG/JiS44iZWiNdSOWE09N2lUgN6yshuq6fVSon4Pm0VMXd1srUUkLe9iA==
-
-postgres-interval@^1.1.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695"
-  integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==
-  dependencies:
-    xtend "^4.0.0"
-
-prebuild-install@^6.1.4:
-  version "6.1.4"
-  resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f"
-  integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==
-  dependencies:
-    detect-libc "^1.0.3"
-    expand-template "^2.0.3"
-    github-from-package "0.0.0"
-    minimist "^1.2.3"
-    mkdirp-classic "^0.5.3"
-    napi-build-utils "^1.0.1"
-    node-abi "^2.21.0"
-    npmlog "^4.0.1"
-    pump "^3.0.0"
-    rc "^1.2.7"
-    simple-get "^3.0.3"
-    tar-fs "^2.0.0"
-    tunnel-agent "^0.6.0"
-
-prelude-ls@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
-  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
-
-prelude-ls@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
-  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
-
 prepend-http@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
@@ -8930,101 +3255,11 @@ pretty-hrtime@^1.0.0:
   resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
   integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
 
-pretty@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/pretty/-/pretty-2.0.0.tgz#adbc7960b7bbfe289a557dc5f737619a220d06a5"
-  integrity sha1-rbx5YLe7/iiaVX3F9zdhmiINBqU=
-  dependencies:
-    condense-newlines "^0.2.1"
-    extend-shallow "^2.0.1"
-    js-beautify "^1.6.12"
-
-prettyjson@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289"
-  integrity sha1-/P+rQdGcq0365eV15kJGYZsS0ok=
-  dependencies:
-    colors "^1.1.2"
-    minimist "^1.2.0"
-
-printj@~1.1.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
-  integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
-
-prismjs@1.25.0:
-  version "1.25.0"
-  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
-  integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
-
-private-ip@2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/private-ip/-/private-ip-2.3.3.tgz#1e80ff8443e5ac78f555631aec3ea6ff027fa6aa"
-  integrity sha512-5zyFfekIVUOTVbL92hc8LJOtE/gyGHeREHkJ2yTyByP8Q2YZVoBqLg3EfYLeF0oVvGqtaEX2t2Qovja0/gStXw==
-  dependencies:
-    ip-regex "^4.3.0"
-    ipaddr.js "^2.0.1"
-    is-ip "^3.1.0"
-    netmask "^2.0.2"
-
-probe-image-size@7.2.1:
-  version "7.2.1"
-  resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.1.tgz#df0c924e67e247bc94f8fcb0fad7f0081061fc44"
-  integrity sha512-d+6L3NvQBCNt4peRDoEfA7r9bPm6/qy18FnLKwg4NWBC5JrJm0pMLRg1kF4XNsPe1bUdt3WIMonPJzQWN2HXjQ==
-  dependencies:
-    lodash.merge "^4.6.2"
-    needle "^2.5.2"
-    stream-parser "~0.3.1"
-
 process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
   integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
 
-progress@^2.0.0:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
-  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
-
-promise-inflight@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
-  integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
-
-promise-limit@2.7.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/promise-limit/-/promise-limit-2.7.0.tgz#eb5737c33342a030eaeaecea9b3d3a93cb592b26"
-  integrity sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==
-
-promise-retry@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
-  integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
-  dependencies:
-    err-code "^2.0.2"
-    retry "^0.12.0"
-
-promise.prototype.finally@^3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.2.tgz#b8af89160c9c673cefe3b4c4435b53cfd0287067"
-  integrity sha512-A2HuJWl2opDH0EafgdjwEw7HysI8ff/n4lW4QEVBCUXFk9QeGecBWv0Deph0UmLe3tTNYegz8MOjsVuE6SMoJA==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.0"
-    function-bind "^1.1.1"
-
-promise@^7.0.1:
-  version "7.3.1"
-  resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
-  integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
-  dependencies:
-    asap "~2.0.3"
-
-proto-list@~1.2.1:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
-  integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
-
 proxy-from-env@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
@@ -9037,119 +3272,11 @@ ps-tree@1.2.0:
   dependencies:
     event-stream "=3.3.4"
 
-pseudomap@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
-  integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
-
-psl@^1.1.28, psl@^1.1.33:
+psl@^1.1.28:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
   integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
 
-pug-attrs@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41"
-  integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==
-  dependencies:
-    constantinople "^4.0.1"
-    js-stringify "^1.0.2"
-    pug-runtime "^3.0.0"
-
-pug-code-gen@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce"
-  integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==
-  dependencies:
-    constantinople "^4.0.1"
-    doctypes "^1.1.0"
-    js-stringify "^1.0.2"
-    pug-attrs "^3.0.0"
-    pug-error "^2.0.0"
-    pug-runtime "^3.0.0"
-    void-elements "^3.1.0"
-    with "^7.0.0"
-
-pug-error@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5"
-  integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==
-
-pug-filters@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e"
-  integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==
-  dependencies:
-    constantinople "^4.0.1"
-    jstransformer "1.0.0"
-    pug-error "^2.0.0"
-    pug-walk "^2.0.0"
-    resolve "^1.15.1"
-
-pug-lexer@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5"
-  integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==
-  dependencies:
-    character-parser "^2.2.0"
-    is-expression "^4.0.0"
-    pug-error "^2.0.0"
-
-pug-linker@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708"
-  integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==
-  dependencies:
-    pug-error "^2.0.0"
-    pug-walk "^2.0.0"
-
-pug-load@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662"
-  integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==
-  dependencies:
-    object-assign "^4.1.1"
-    pug-walk "^2.0.0"
-
-pug-parser@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260"
-  integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==
-  dependencies:
-    pug-error "^2.0.0"
-    token-stream "1.0.0"
-
-pug-runtime@^3.0.0, pug-runtime@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7"
-  integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==
-
-pug-strip-comments@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e"
-  integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==
-  dependencies:
-    pug-error "^2.0.0"
-
-pug-walk@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe"
-  integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==
-
-pug@3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535"
-  integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==
-  dependencies:
-    pug-code-gen "^3.0.2"
-    pug-filters "^4.0.0"
-    pug-lexer "^5.0.1"
-    pug-linker "^4.0.0"
-    pug-load "^3.0.0"
-    pug-parser "^6.0.0"
-    pug-runtime "^3.0.1"
-    pug-strip-comments "^2.0.0"
-
 pump@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
@@ -9180,48 +3307,16 @@ punycode@1.3.2:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
   integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
 
-punycode@2.1.1, punycode@^2.1.0, punycode@^2.1.1:
+punycode@^2.1.0, punycode@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-pureimage@0.3.5:
-  version "0.3.5"
-  resolved "https://registry.yarnpkg.com/pureimage/-/pureimage-0.3.5.tgz#cd5e91f7b6409fcf4880297aaa3e7fc0afc24d5e"
-  integrity sha512-+CFUEpoX6GemlKlHihI7Ii4IqKqF5KZjd682sAxwzbc4t4zU4Gwhxd4W3UMZW94nJzf0n4nA9zJrwTR4jZB4TA==
-  dependencies:
-    jpeg-js "^0.4.1"
-    opentype.js "^0.4.3"
-    pngjs "^3.3.1"
-
-q@1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
-  integrity sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=
-
 q@^1.1.2:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
 
-qrcode@1.4.4:
-  version "1.4.4"
-  resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
-  integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
-  dependencies:
-    buffer "^5.4.3"
-    buffer-alloc "^1.2.0"
-    buffer-from "^1.1.1"
-    dijkstrajs "^1.0.1"
-    isarray "^2.0.1"
-    pngjs "^3.3.0"
-    yargs "^13.2.4"
-
-qs@^6.4.0, qs@^6.5.2:
-  version "6.9.3"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e"
-  integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==
-
 qs@~6.5.2:
   version "6.5.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@@ -9240,76 +3335,11 @@ querystring@0.2.0:
   resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
   integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
 
-quick-lru@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
-  integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
-
 ramda@~0.27.1:
   version "0.27.1"
   resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
   integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
 
-random-seed@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/random-seed/-/random-seed-0.3.0.tgz#d945f2e1f38f49e8d58913431b8bf6bb937556cd"
-  integrity sha1-2UXy4fOPSejViRNDG4v2u5N1Vs0=
-  dependencies:
-    json-stringify-safe "^5.0.1"
-
-randombytes@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
-  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
-  dependencies:
-    safe-buffer "^5.1.0"
-
-rangestr@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/rangestr/-/rangestr-0.0.1.tgz#f72ff9246f10f2a7d7c16e14616f617be2c2635a"
-  integrity sha1-9y/5JG8Q8qfXwW4UYW9he+LCY1o=
-
-ratelimiter@3.4.1:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/ratelimiter/-/ratelimiter-3.4.1.tgz#fa69e94937413382a926aaa17aaeaa6263af4659"
-  integrity sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==
-
-raw-body@^2.2.0, raw-body@^2.3.3:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c"
-  integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==
-  dependencies:
-    bytes "3.1.0"
-    http-errors "1.7.3"
-    iconv-lite "0.4.24"
-    unpipe "1.0.0"
-
-rc@^1.2.7:
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
-  integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
-  dependencies:
-    deep-extend "^0.6.0"
-    ini "~1.3.0"
-    minimist "^1.2.0"
-    strip-json-comments "~2.0.1"
-
-rdf-canonize@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-3.0.0.tgz#f5bade563e5e58f5cc5881afcba3c43839e8c747"
-  integrity sha512-LXRkhab1QaPJnhUIt1gtXXKswQCZ9zpflsSZFczG7mCLAkMvVjdqCGk9VXCUss0aOUeEyV2jtFxGcdX8DSkj9w==
-  dependencies:
-    setimmediate "^1.0.5"
-
-re2@1.16.0:
-  version "1.16.0"
-  resolved "https://registry.yarnpkg.com/re2/-/re2-1.16.0.tgz#f311eb4865b1296123800ea8e013cec8dab25590"
-  integrity sha512-eizTZL2ZO0ZseLqfD4t3Qd0M3b3Nr0MBWpX81EbPMIud/1d/CSfUIx2GQK8fWiAeHoSekO5EOeFib2udTZLwYw==
-  dependencies:
-    install-artifact-from-github "^1.2.0"
-    nan "^2.14.2"
-    node-gyp "^8.0.0"
-
 read-pkg-up@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@@ -9327,17 +3357,7 @@ read-pkg@^1.0.0:
     normalize-package-data "^2.3.2"
     path-type "^1.0.0"
 
-readable-stream@1.1.x:
-  version "1.1.14"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
-  integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
-readable-stream@3, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
+readable-stream@3:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
   integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@@ -9346,7 +3366,7 @@ readable-stream@3, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stre
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
-readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
+readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
   integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -9359,13 +3379,14 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable
     string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
-readable-web-to-node-stream@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.0.tgz#4ca5408e70471069119d691934141a52de413955"
-  integrity sha512-HNmLb3n0SteGAs8HQlErYPGeO+y7cvL/mVUKtXeUkl0iCZ/2GIgKGrCFHyS7UXFnO8uc9U+0y3pYIzAPsjFfvA==
+readdirp@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+  integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
   dependencies:
-    "@types/readable-stream" "^2.3.9"
-    readable-stream "^3.6.0"
+    graceful-fs "^4.1.11"
+    micromatch "^3.1.10"
+    readable-stream "^2.0.2"
 
 readdirp@~3.3.0:
   version "3.3.0"
@@ -9381,50 +3402,6 @@ rechoir@^0.6.2:
   dependencies:
     resolve "^1.1.6"
 
-rechoir@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca"
-  integrity sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==
-  dependencies:
-    resolve "^1.9.0"
-
-reconnecting-websocket@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
-  integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
-
-redis-commands@1.7.0, redis-commands@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
-  integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
-
-redis-errors@^1.0.0, redis-errors@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
-  integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
-
-redis-lock@0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/redis-lock/-/redis-lock-0.1.4.tgz#e83590bee22b5f01cdb65bfbd88d988045356272"
-  integrity sha512-7/+zu86XVQfJVx1nHTzux5reglDiyUCDwmW7TSlvVezfhH2YLc/Rc8NE0ejQG+8/0lwKzm29/u/4+ogKeLosiA==
-
-redis-parser@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
-  integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
-  dependencies:
-    redis-errors "^1.0.0"
-
-redis@3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c"
-  integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==
-  dependencies:
-    denque "^1.5.0"
-    redis-commands "^1.7.0"
-    redis-errors "^1.2.0"
-    redis-parser "^3.0.0"
-
 reduce-css-calc@^1.2.6:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
@@ -9441,16 +3418,6 @@ reduce-function-call@^1.0.1:
   dependencies:
     balanced-match "^1.0.0"
 
-reflect-metadata@0.1.13, reflect-metadata@^0.1.13:
-  version "0.1.13"
-  resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
-  integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
-
-regenerator-runtime@^0.13.4:
-  version "0.13.7"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
-  integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
-
 regex-not@^1.0.0, regex-not@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@@ -9459,11 +3426,6 @@ regex-not@^1.0.0, regex-not@^1.0.2:
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
-regexpp@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
-  integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
-
 remove-bom-buffer@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53"
@@ -9486,13 +3448,6 @@ remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0:
   resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
   integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
 
-rename@1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/rename/-/rename-1.0.4.tgz#a0f25078fa4195e650f73050c7c12ccf689f430b"
-  integrity sha1-oPJQePpBleZQ9zBQx8Esz2ifQws=
-  dependencies:
-    debug "^2.5.2"
-
 repeat-element@^1.1.2:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
@@ -9533,61 +3488,6 @@ request-progress@^3.0.0:
   dependencies:
     throttleit "^1.0.0"
 
-request-promise-core@1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
-  integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
-  dependencies:
-    lodash "^4.17.19"
-
-request-promise-native@1.0.9:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
-  integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
-  dependencies:
-    request-promise-core "1.1.4"
-    stealthy-require "^1.1.1"
-    tough-cookie "^2.3.3"
-
-request-stats@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/request-stats/-/request-stats-3.0.0.tgz#769155dc8974d78d4a1cb87bbf14eaab985afe25"
-  integrity sha1-dpFV3Il0141KHLh7vxTqq5ha/iU=
-  dependencies:
-    http-headers "^3.0.1"
-    once "^1.4.0"
-
-request@2.88.2, request@^2.88.2:
-  version "2.88.2"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
-  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.8.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.6"
-    extend "~3.0.2"
-    forever-agent "~0.6.1"
-    form-data "~2.3.2"
-    har-validator "~5.1.3"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.19"
-    oauth-sign "~0.9.0"
-    performance-now "^2.1.0"
-    qs "~6.5.2"
-    safe-buffer "^5.1.2"
-    tough-cookie "~2.5.0"
-    tunnel-agent "^0.6.0"
-    uuid "^3.3.2"
-
-require-all@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/require-all/-/require-all-3.0.0.tgz#473d49704be310115ce124f77383b1ebd8671312"
-  integrity sha1-Rz1JcEvjEBFc4ST3c4Ox69hnExI=
-
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -9598,27 +3498,10 @@ require-from-string@^2.0.2:
   resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
   integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
 
-require-main-filename@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
-  integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
-
-require-main-filename@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
-  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-
-resolve-alpn@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c"
-  integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==
-
-resolve-cwd@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
-  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
-  dependencies:
-    resolve-from "^5.0.0"
+require-main-filename@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+  integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
 
 resolve-dir@^1.0.0, resolve-dir@^1.0.1:
   version "1.0.1"
@@ -9628,16 +3511,6 @@ resolve-dir@^1.0.0, resolve-dir@^1.0.1:
     expand-tilde "^2.0.0"
     global-modules "^1.0.0"
 
-resolve-from@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
-  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
-
-resolve-from@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
-  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
-
 resolve-options@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131"
@@ -9645,20 +3518,12 @@ resolve-options@^1.1.0:
   dependencies:
     value-or-function "^3.0.0"
 
-resolve-path@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
-  integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=
-  dependencies:
-    http-errors "~1.6.2"
-    path-is-absolute "1.0.1"
-
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
-resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.9.0:
+resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.4.0:
   version "1.18.1"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130"
   integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==
@@ -9666,21 +3531,6 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0,
     is-core-module "^2.0.0"
     path-parse "^1.0.6"
 
-resolve@^1.15.1:
-  version "1.20.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
-  integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
-  dependencies:
-    is-core-module "^2.2.0"
-    path-parse "^1.0.6"
-
-responselike@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723"
-  integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==
-  dependencies:
-    lowercase-keys "^2.0.0"
-
 restore-cursor@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
@@ -9694,48 +3544,13 @@ ret@~0.1.10:
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
-retry@^0.12.0:
-  version "0.12.0"
-  resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
-  integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
-
-reusify@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
-  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-
-rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
+rimraf@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
   integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
   dependencies:
     glob "^7.1.3"
 
-rndstr@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/rndstr/-/rndstr-1.0.0.tgz#77e66fa8f9b4836853fdd91e50719591bb67d349"
-  integrity sha1-d+ZvqPm0g2hT/dkeUHGVkbtn00k=
-  dependencies:
-    rangestr "0.0.1"
-    seedrandom "2.4.2"
-
-rsvp@^4.8.5:
-  version "4.8.5"
-  resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
-  integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
-
-run-parallel@^1.1.9:
-  version "1.1.9"
-  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
-  integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
-
-rxjs@^6.6.3:
-  version "6.6.6"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70"
-  integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==
-  dependencies:
-    tslib "^1.9.0"
-
 rxjs@^6.6.7:
   version "6.6.7"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
@@ -9750,31 +3565,16 @@ rxjs@^7.1.0:
   dependencies:
     tslib "~2.1.0"
 
-s-age@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/s-age/-/s-age-1.1.2.tgz#c0cf15233ccc93f41de92ea42c36d957977d1ea2"
-  integrity sha512-aSN2TlF39WLoZA/6cgYSJZhKt63kJ4EaadejPWjWY9/h4rksIqvfWY3gfd+3uAegSM1IXsA9aWeEhJtkxkFQtA==
-
-safari-14-idb-fix@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-1.0.4.tgz#5c68ba63e2a8ae0d89a0aa1e13fe89e3aef7da19"
-  integrity sha512-4+Y2baQdgJpzu84d0QjySl70Kyygzf0pepVg8NVg4NnQEPpfC91fAn0baNvtStlCjUUxxiu0BOMiafa98fRRuA==
-
-safe-buffer@*:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
-  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-
-safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
-  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
 safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
   integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
 
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
 safe-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@@ -9782,76 +3582,16 @@ safe-regex@^1.1.0:
   dependencies:
     ret "~0.1.10"
 
-"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-sass-loader@12.3.0:
-  version "12.3.0"
-  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.3.0.tgz#93278981c189c36a58cbfc37d4b9cef0cdc02871"
-  integrity sha512-6l9qwhdOb7qSrtOu96QQ81LVl8v6Dp9j1w3akOm0aWHyrTYtagDt5+kS32N4yq4hHk3M+rdqoRMH+lIdqvW6HA==
-  dependencies:
-    klona "^2.0.4"
-    neo-async "^2.6.2"
-
-sass@1.43.4:
-  version "1.43.4"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.4.tgz#68c7d6a1b004bef49af0d9caf750e9b252105d1f"
-  integrity sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==
-  dependencies:
-    chokidar ">=3.0.0 <4.0.0"
-
-sax@1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
-  integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
-
-sax@>=0.6.0, sax@^1.2.4, sax@~1.2.1, sax@~1.2.4:
+sax@~1.2.1:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
-saxes@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
-  integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
-  dependencies:
-    xmlchars "^2.2.0"
-
-schema-utils@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
-  integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
-  dependencies:
-    "@types/json-schema" "^7.0.6"
-    ajv "^6.12.5"
-    ajv-keywords "^3.5.2"
-
-schema-utils@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.0.tgz#95986eb604f66daadeed56e379bfe7a7f963cdb9"
-  integrity sha512-tTEaeYkyIhEZ9uWgAjDerWov3T9MgX8dhhy2r0IGeeX4W8ngtGl1++dUve/RUqzuaASSh7shwCDJjEzthxki8w==
-  dependencies:
-    "@types/json-schema" "^7.0.7"
-    ajv "^6.12.5"
-    ajv-keywords "^3.5.2"
-
-secure-json-parse@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20"
-  integrity sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA==
-
-seedrandom@2.4.2:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.2.tgz#18d78c41287d13aff8eadb29e235938b248aa9ff"
-  integrity sha1-GNeMQSh9E6/46tsp4jWTiySKqf8=
-
-seedrandom@3.0.5:
-  version "3.0.5"
-  resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
-  integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
-
 semver-greatest-satisfied-range@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b"
@@ -9859,45 +3599,12 @@ semver-greatest-satisfied-range@^1.1.0:
   dependencies:
     sver-compat "^1.5.0"
 
-"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.6.0:
+"semver@2 || 3 || 4 || 5":
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
-semver@6.x:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
-  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
-  version "7.3.4"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
-  integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
-  dependencies:
-    lru-cache "^6.0.0"
-
-semver@^7.3.5:
-  version "7.3.5"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
-  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
-  dependencies:
-    lru-cache "^6.0.0"
-
-serialize-javascript@5.0.1, serialize-javascript@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
-  integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
-  dependencies:
-    randombytes "^2.1.0"
-
-serialize-javascript@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
-  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
-  dependencies:
-    randombytes "^2.1.0"
-
-set-blocking@^2.0.0, set-blocking@~2.0.0:
+set-blocking@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
   integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
@@ -9912,50 +3619,6 @@ set-value@^2.0.0, set-value@^2.0.1:
     is-plain-object "^2.0.3"
     split-string "^3.0.1"
 
-setimmediate@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
-  integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
-
-setprototypeof@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
-  integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
-
-setprototypeof@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
-  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
-
-sha.js@^2.4.11:
-  version "2.4.11"
-  resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
-  integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
-  dependencies:
-    inherits "^2.0.1"
-    safe-buffer "^5.0.1"
-
-shallow-clone@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
-  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
-  dependencies:
-    kind-of "^6.0.2"
-
-sharp@0.29.2:
-  version "0.29.2"
-  resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.2.tgz#e8c003cd9cb321585b32dbda6eed3baa7d6f2308"
-  integrity sha512-XWRdiYLIJ3tDUejRyG24KERnJzMfIoyiJBntd2S6/uj3NEeNgRFRLgiBlvPxMa8aml14dKKD98yHinSNKp1xzQ==
-  dependencies:
-    color "^4.0.1"
-    detect-libc "^1.0.3"
-    node-addon-api "^4.2.0"
-    prebuild-install "^6.1.4"
-    semver "^7.3.5"
-    simple-get "^3.1.0"
-    tar-fs "^2.1.1"
-    tunnel-agent "^0.6.0"
-
 shebang-command@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -9968,42 +3631,11 @@ shebang-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
-sigmund@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
-  integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=
-
-signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
+signal-exit@^3.0.2, signal-exit@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
   integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
 
-simple-concat@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
-  integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
-
-simple-get@^3.0.3, simple-get@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3"
-  integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==
-  dependencies:
-    decompress-response "^4.2.0"
-    once "^1.3.1"
-    simple-concat "^1.0.0"
-
-simple-swizzle@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
-  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
-  dependencies:
-    is-arrayish "^0.3.1"
-
-slash@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
-  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
-
 slice-ansi@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
@@ -10022,11 +3654,6 @@ slice-ansi@^4.0.0:
     astral-regex "^2.0.0"
     is-fullwidth-code-point "^3.0.0"
 
-smart-buffer@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba"
-  integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==
-
 snapdragon-node@^2.0.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -10057,23 +3684,6 @@ snapdragon@^0.8.1:
     source-map-resolve "^0.5.0"
     use "^3.1.0"
 
-socks-proxy-agent@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60"
-  integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==
-  dependencies:
-    agent-base "6"
-    debug "4"
-    socks "^2.3.3"
-
-socks@^2.3.3:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e"
-  integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==
-  dependencies:
-    ip "^1.1.5"
-    smart-buffer "^4.1.0"
-
 sort-keys@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
@@ -10081,21 +3691,6 @@ sort-keys@^1.0.0:
   dependencies:
     is-plain-obj "^1.0.0"
 
-sortablejs@1.10.2:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
-  integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
-
-source-list-map@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
-  integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
-
-source-map-js@^0.6.2:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
-  integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
-
 source-map-resolve@^0.5.0:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
@@ -10107,14 +3702,6 @@ source-map-resolve@^0.5.0:
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
-source-map-support@~0.5.19:
-  version "0.5.19"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
-  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
-  dependencies:
-    buffer-from "^1.0.0"
-    source-map "^0.6.0"
-
 source-map-support@~0.5.20:
   version "0.5.20"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
@@ -10133,7 +3720,7 @@ source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
 
-source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+source-map@^0.6.0:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -10143,21 +3730,11 @@ source-map@~0.7.2:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
   integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
 
-sourcemap-codec@^1.4.4:
-  version "1.4.8"
-  resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
-  integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
-
 sparkles@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c"
   integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==
 
-spawn-command@^0.0.2-1:
-  version "0.0.2-1"
-  resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
-  integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=
-
 spdx-correct@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
@@ -10184,13 +3761,6 @@ spdx-license-ids@^3.0.0:
   resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
   integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
 
-speakeasy@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/speakeasy/-/speakeasy-2.0.0.tgz#85c91a071b09a5cb8642590d983566165f57613a"
-  integrity sha1-hckaBxsJpcuGQlkNmDVmFl9XYTo=
-  dependencies:
-    base32.js "0.0.1"
-
 split-string@^3.0.1, split-string@^3.0.2:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -10205,24 +3775,12 @@ split@0.3:
   dependencies:
     through "2"
 
-split@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
-  integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
-  dependencies:
-    through "2"
-
-sprintf-js@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
-  integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
-
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
-sshpk@^1.14.1, sshpk@^1.7.0:
+sshpk@^1.7.0:
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
   integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
@@ -10237,28 +3795,11 @@ sshpk@^1.14.1, sshpk@^1.7.0:
     safer-buffer "^2.0.2"
     tweetnacl "~0.14.0"
 
-ssri@^8.0.0, ssri@^8.0.1:
-  version "8.0.1"
-  resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
-  integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
-  dependencies:
-    minipass "^3.1.1"
-
-stable@^0.1.8:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
-  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
-
 stack-trace@0.0.10:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
   integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
 
-standard-as-callback@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
-  integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
-
 start-server-and-test@1.14.0:
   version "1.14.0"
   resolved "https://registry.yarnpkg.com/start-server-and-test/-/start-server-and-test-1.14.0.tgz#c57f04f73eac15dd51733b551d775b40837fdde3"
@@ -10280,16 +3821,6 @@ static-extend@^0.1.1:
     define-property "^0.2.5"
     object-copy "^0.1.0"
 
-"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
-  integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
-
-stealthy-require@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
-  integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
-
 stream-combiner@~0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
@@ -10302,28 +3833,11 @@ stream-exhaust@^1.0.1:
   resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d"
   integrity sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==
 
-stream-parser@~0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
-  integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=
-  dependencies:
-    debug "2"
-
 stream-shift@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
   integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
 
-streamsearch@0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
-  integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
-
-strict-event-emitter-types@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f"
-  integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==
-
 strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@@ -10338,23 +3852,6 @@ string-width@^1.0.1, string-width@^1.0.2:
     is-fullwidth-code-point "^1.0.0"
     strip-ansi "^3.0.0"
 
-"string-width@^1.0.2 || 2":
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
-  integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
-  dependencies:
-    is-fullwidth-code-point "^2.0.0"
-    strip-ansi "^4.0.0"
-
-string-width@^3.0.0, string-width@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
-  integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
-  dependencies:
-    emoji-regex "^7.0.1"
-    is-fullwidth-code-point "^2.0.0"
-    strip-ansi "^5.1.0"
-
 string-width@^4.1.0, string-width@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
@@ -10364,40 +3861,6 @@ string-width@^4.1.0, string-width@^4.2.0:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string.prototype.trimend@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
-  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-
-string.prototype.trimleft@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc"
-  integrity sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-    string.prototype.trimstart "^1.0.0"
-
-string.prototype.trimright@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3"
-  integrity sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-    string.prototype.trimend "^1.0.0"
-
-string.prototype.trimstart@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
-  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.5"
-
 string_decoder@^1.1.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -10405,11 +3868,6 @@ string_decoder@^1.1.1:
   dependencies:
     safe-buffer "~5.2.0"
 
-string_decoder@~0.10.x:
-  version "0.10.31"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-  integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
-
 string_decoder@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@@ -10417,13 +3875,6 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
-stringz@2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/stringz/-/stringz-2.1.0.tgz#5896b4713eac31157556040fb90258fb02c1630c"
-  integrity sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==
-  dependencies:
-    char-regex "^1.0.2"
-
 strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -10431,20 +3882,6 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   dependencies:
     ansi-regex "^2.0.0"
 
-strip-ansi@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
-  integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
-  dependencies:
-    ansi-regex "^3.0.0"
-
-strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
-  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
-  dependencies:
-    ansi-regex "^4.1.0"
-
 strip-ansi@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
@@ -10452,13 +3889,6 @@ strip-ansi@^6.0.0:
   dependencies:
     ansi-regex "^5.0.0"
 
-strip-ansi@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
 strip-bom@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -10466,69 +3896,11 @@ strip-bom@^2.0.0:
   dependencies:
     is-utf8 "^0.2.0"
 
-strip-bom@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
-  integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
-
 strip-final-newline@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
   integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
 
-strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
-  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
-
-strip-json-comments@~2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
-  integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
-
-strtok3@^6.2.4:
-  version "6.2.4"
-  resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.2.4.tgz#302aea64c0fa25d12a0385069ba66253fdc38a81"
-  integrity sha512-GO8IcFF9GmFDvqduIspUBwCzCbqzegyVKIsSymcMgiZKeCfrN9SowtUoi8+b59WZMAjIzVZic/Ft97+pynR3Iw==
-  dependencies:
-    "@tokenizer/token" "^0.3.0"
-    peek-readable "^4.0.1"
-
-style-loader@3.3.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
-  integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
-
-stylehacks@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb"
-  integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==
-  dependencies:
-    browserslist "^4.16.0"
-    postcss-selector-parser "^6.0.4"
-
-summaly@2.4.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.4.1.tgz#d2a8fa6bad10c1651eb0b849aab3009e87216a3d"
-  integrity sha512-1gETEQXqK5RD7yIGgdGeTwGL1uh+uj14u99atzNLNmvsxwdtZbPvDHZBPXkAW0cqsd8teoBJln5Dh1QeAhvGIg==
-  dependencies:
-    cheerio-httpcli "0.8.2"
-    debug "4.3.2"
-    escape-regexp "0.0.1"
-    html-entities "2.3.2"
-    koa "2.13.1"
-    request "2.88.2"
-    request-promise-native "1.0.9"
-    require-all "3.0.0"
-    trace-redirect "1.0.6"
-
-supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.0, supports-color@^8.1.1:
-  version "8.1.1"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
-  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
-  dependencies:
-    has-flag "^4.0.0"
-
 supports-color@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -10541,20 +3913,20 @@ supports-color@^3.2.3:
   dependencies:
     has-flag "^1.0.0"
 
-supports-color@^5.3.0:
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
-  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
-  dependencies:
-    has-flag "^3.0.0"
-
-supports-color@^7.0.0, supports-color@^7.1.0:
+supports-color@^7.1.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
   integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
   dependencies:
     has-flag "^4.0.0"
 
+supports-color@^8.1.1:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
 sver-compat@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8"
@@ -10565,183 +3937,16 @@ sver-compat@^1.5.0:
 
 svgo@^0.7.0:
   version "0.7.2"
-  resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
-  integrity sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=
-  dependencies:
-    coa "~1.0.1"
-    colors "~1.1.2"
-    csso "~2.3.1"
-    js-yaml "~3.7.0"
-    mkdirp "~0.5.1"
-    sax "~1.2.1"
-    whet.extend "~0.9.9"
-
-svgo@^1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
-  integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
-  dependencies:
-    chalk "^2.4.1"
-    coa "^2.0.2"
-    css-select "^2.0.0"
-    css-select-base-adapter "^0.1.1"
-    css-tree "1.0.0-alpha.37"
-    csso "^4.0.2"
-    js-yaml "^3.13.1"
-    mkdirp "~0.5.1"
-    object.values "^1.1.0"
-    sax "~1.2.4"
-    stable "^0.1.8"
-    unquote "~1.1.1"
-    util.promisify "~1.0.0"
-
-svgo@^2.7.0:
-  version "2.8.0"
-  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
-  integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
-  dependencies:
-    "@trysound/sax" "0.2.0"
-    commander "^7.2.0"
-    css-select "^4.1.3"
-    css-tree "^1.1.3"
-    csso "^4.2.0"
-    picocolors "^1.0.0"
-    stable "^0.1.8"
-
-symbol-tree@^3.2.4:
-  version "3.2.4"
-  resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
-  integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
-
-syslog-pro@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/syslog-pro/-/syslog-pro-1.0.0.tgz#e46bfd39f58937352645091e84a3b903f39e12ea"
-  integrity sha512-7SNMJKtQBJlwBUp1jxFT7bXya71cnINXPCYJ2AVhlQE4MKL7o2QiPdAXbMdWRiLeykQ2rx+7TNrnoGzvzhO+eA==
-  dependencies:
-    moment "^2.22.2"
-
-systeminformation@5.9.9:
-  version "5.9.9"
-  resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.9.9.tgz#aa8234a138363bd988f438fed3273370f79d7e30"
-  integrity sha512-xciy6NKCLfs4dqMD1Tdlo7v1/g0NfdA1EKsIptUQjlcVvpwHyjifAbNOF7ppFezGSMXxYE8me+l2+RlFF4lyTg==
-
-syuilo-password-strength@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/syuilo-password-strength/-/syuilo-password-strength-0.0.1.tgz#08f71a8f0ecb77db649f3d9a6424510d9d945f52"
-  integrity sha1-CPcajw7Ld9tknz2aZCRRDZ2UX1I=
-
-tapable@^2.1.1, tapable@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
-  integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
-
-tar-fs@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2"
-  integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==
-  dependencies:
-    chownr "^1.1.1"
-    mkdirp-classic "^0.5.2"
-    pump "^3.0.0"
-    tar-stream "^2.0.0"
-
-tar-fs@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
-  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
-  dependencies:
-    chownr "^1.1.1"
-    mkdirp-classic "^0.5.2"
-    pump "^3.0.0"
-    tar-stream "^2.1.4"
-
-tar-stream@^2.0.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325"
-  integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==
-  dependencies:
-    bl "^4.0.1"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
-tar-stream@^2.1.4:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
-  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
-  dependencies:
-    bl "^4.0.3"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
-tar@^6.0.2:
-  version "6.0.5"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
-  integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
-  dependencies:
-    chownr "^2.0.0"
-    fs-minipass "^2.0.0"
-    minipass "^3.0.0"
-    minizlib "^2.1.1"
-    mkdirp "^1.0.3"
-    yallist "^4.0.0"
-
-tar@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
-  integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
-  dependencies:
-    chownr "^2.0.0"
-    fs-minipass "^2.0.0"
-    minipass "^3.0.0"
-    minizlib "^2.1.1"
-    mkdirp "^1.0.3"
-    yallist "^4.0.0"
-
-terser-webpack-plugin@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz#7effadee06f7ecfa093dbbd3e9ab23f5f3ed8673"
-  integrity sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q==
-  dependencies:
-    jest-worker "^26.6.2"
-    p-limit "^3.1.0"
-    schema-utils "^3.0.0"
-    serialize-javascript "^5.0.1"
-    source-map "^0.6.1"
-    terser "^5.5.1"
-
-terser-webpack-plugin@^5.1.3:
-  version "5.1.4"
-  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1"
-  integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==
-  dependencies:
-    jest-worker "^27.0.2"
-    p-limit "^3.1.0"
-    schema-utils "^3.0.0"
-    serialize-javascript "^6.0.0"
-    source-map "^0.6.1"
-    terser "^5.7.0"
-
-terser@^5.5.1:
-  version "5.5.1"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289"
-  integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==
-  dependencies:
-    commander "^2.20.0"
-    source-map "~0.7.2"
-    source-map-support "~0.5.19"
-
-terser@^5.7.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.1.tgz#2dc7a61009b66bb638305cb2a824763b116bf784"
-  integrity sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
+  integrity sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=
   dependencies:
-    commander "^2.20.0"
-    source-map "~0.7.2"
-    source-map-support "~0.5.19"
+    coa "~1.0.1"
+    colors "~1.1.2"
+    csso "~2.3.1"
+    js-yaml "~3.7.0"
+    mkdirp "~0.5.1"
+    sax "~1.2.1"
+    whet.extend "~0.9.9"
 
 terser@^5.9.0:
   version "5.9.0"
@@ -10752,45 +3957,11 @@ terser@^5.9.0:
     source-map "~0.7.2"
     source-map-support "~0.5.20"
 
-text-table@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
-  integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
-
-textarea-caret@3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.1.0.tgz#5d5a35bb035fd06b2ff0e25d5359e97f2655087f"
-  integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==
-
 textextensions@^3.2.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-3.3.0.tgz#03530d5287b86773c08b77458589148870cc71d3"
   integrity sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==
 
-thenify-all@^1.0.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
-  integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
-  dependencies:
-    thenify ">= 3.1.0 < 4"
-
-"thenify@>= 3.1.0 < 4":
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
-  integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
-  dependencies:
-    any-promise "^1.0.0"
-
-three@0.117.1:
-  version "0.117.1"
-  resolved "https://registry.yarnpkg.com/three/-/three-0.117.1.tgz#a49bcb1a6ddea2f250003e42585dc3e78e92b9d3"
-  integrity sha512-t4zeJhlNzUIj9+ub0l6nICVimSuRTZJOqvk3Rmlu+YGdTOJ49Wna8p7aumpkXJakJfITiybfpYE1XN1o1Z34UQ==
-
-throttle-debounce@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
-  integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
-
 throttleit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
@@ -10829,17 +4000,7 @@ time-stamp@^1.0.0:
   resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
   integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=
 
-timsort@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
-  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
-
-tinycolor2@1.4.2:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
-  integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
-
-tmp@0.2.1, tmp@~0.2.1:
+tmp@~0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
   integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
@@ -10854,11 +4015,6 @@ to-absolute-glob@^2.0.0:
     is-absolute "^1.0.0"
     is-negated-glob "^1.0.0"
 
-to-fast-properties@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
-  integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
-
 to-object-path@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
@@ -10898,25 +4054,7 @@ to-through@^2.0.0:
   dependencies:
     through2 "^2.0.3"
 
-toidentifier@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
-  integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
-
-token-stream@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
-  integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=
-
-token-types@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.1.1.tgz#ef9e8c8e2e0ded9f1b3f8dbaa46a3228b113ba1a"
-  integrity sha512-hD+QyuUAyI2spzsI0B7gf/jJ2ggR4RjkAo37j3StuePhApJUwcWDjnHDOFdIWYSwNR28H14hpwm4EI+V1Ted1w==
-  dependencies:
-    "@tokenizer/token" "^0.3.0"
-    ieee754 "^1.2.1"
-
-tough-cookie@^2.3.3, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
+tough-cookie@~2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
   integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
@@ -10924,169 +4062,16 @@ tough-cookie@^2.3.3, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
     psl "^1.1.28"
     punycode "^2.1.1"
 
-tough-cookie@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
-  integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==
-  dependencies:
-    psl "^1.1.33"
-    punycode "^2.1.1"
-    universalify "^0.1.2"
-
-tr46@^2.0.0, tr46@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479"
-  integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==
-  dependencies:
-    punycode "^2.1.1"
-
-trace-redirect@1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/trace-redirect/-/trace-redirect-1.0.6.tgz#ac629b5bf8247d30dde5a35fe9811b811075b504"
-  integrity sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==
-
-tree-kill@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
-  integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
-
-ts-jest@^25.2.1:
-  version "25.5.1"
-  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7"
-  integrity sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==
-  dependencies:
-    bs-logger "0.x"
-    buffer-from "1.x"
-    fast-json-stable-stringify "2.x"
-    json5 "2.x"
-    lodash.memoize "4.x"
-    make-error "1.x"
-    micromatch "4.x"
-    mkdirp "0.x"
-    semver "6.x"
-    yargs-parser "18.x"
-
-ts-loader@9.2.6:
-  version "9.2.6"
-  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74"
-  integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw==
-  dependencies:
-    chalk "^4.1.0"
-    enhanced-resolve "^5.0.0"
-    micromatch "^4.0.0"
-    semver "^7.3.4"
-
-ts-node@10.4.0:
-  version "10.4.0"
-  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7"
-  integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==
-  dependencies:
-    "@cspotcode/source-map-support" "0.7.0"
-    "@tsconfig/node10" "^1.0.7"
-    "@tsconfig/node12" "^1.0.7"
-    "@tsconfig/node14" "^1.0.0"
-    "@tsconfig/node16" "^1.0.2"
-    acorn "^8.4.1"
-    acorn-walk "^8.1.1"
-    arg "^4.1.0"
-    create-require "^1.1.0"
-    diff "^4.0.1"
-    make-error "^1.1.1"
-    yn "3.1.1"
-
-tsc-alias@1.3.10:
-  version "1.3.10"
-  resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.3.10.tgz#6ccf81c644092387ab9be3a3a75549a95eeffd80"
-  integrity sha512-7SF56qiV7Oh/bON+XjF/uAzEFqbmwCuEIHQyoTyVJAK80WnxaIyhO9TBwD/x8InIMU8lnvExQBOrgKkRPsHH+w==
-  dependencies:
-    "@jfonx/console-utils" "^1.0.3"
-    "@jfonx/file-utils" "^3.0.1"
-    chokidar "^3.5.0"
-    commander "^6.2.1"
-    find-node-modules "^2.1.0"
-    globby "^11.0.2"
-    normalize-path "^3.0.0"
-
-tsconfig-paths@3.11.0:
-  version "3.11.0"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
-  integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==
-  dependencies:
-    "@types/json5" "^0.0.29"
-    json5 "^1.0.1"
-    minimist "^1.2.0"
-    strip-bom "^3.0.0"
-
-tslib@^1.13.0:
-  version "1.13.0"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
-  integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
-
-tslib@^1.8.1, tslib@^1.9.0:
+tslib@^1.9.0:
   version "1.11.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
   integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
 
-tslib@^1.9.3:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
-  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-
-tslib@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
-  integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
-
 tslib@~2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
   integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
 
-tslint-sonarts@1.9.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/tslint-sonarts/-/tslint-sonarts-1.9.0.tgz#feb593e92db328c0328b430b838adbe65d504de9"
-  integrity sha512-CJWt+IiYI8qggb2O/JPkS6CkC5DY1IcqRsm9EHJ+AxoWK70lvtP7jguochyNDMP2vIz/giGdWCfEM39x/I/Vnw==
-  dependencies:
-    immutable "^3.8.2"
-
-tslint@6.1.3:
-  version "6.1.3"
-  resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904"
-  integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==
-  dependencies:
-    "@babel/code-frame" "^7.0.0"
-    builtin-modules "^1.1.1"
-    chalk "^2.3.0"
-    commander "^2.12.1"
-    diff "^4.0.1"
-    glob "^7.1.1"
-    js-yaml "^3.13.1"
-    minimatch "^3.0.4"
-    mkdirp "^0.5.3"
-    resolve "^1.3.2"
-    semver "^5.3.0"
-    tslib "^1.13.0"
-    tsutils "^2.29.0"
-
-tsscmp@1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
-  integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
-
-tsutils@^2.29.0:
-  version "2.29.0"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
-  integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==
-  dependencies:
-    tslib "^1.8.1"
-
-tsutils@^3.21.0:
-  version "3.21.0"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
-  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
-  dependencies:
-    tslib "^1.8.1"
-
 tunnel-agent@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -11099,53 +4084,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
-twemoji-parser@13.1.0, twemoji-parser@13.1.x:
-  version "13.1.0"
-  resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4"
-  integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg==
-
-type-check@^0.4.0, type-check@~0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
-  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
-  dependencies:
-    prelude-ls "^1.2.1"
-
-type-check@~0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
-  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
-  dependencies:
-    prelude-ls "~1.1.2"
-
-type-detect@4.0.8:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
-  integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
-
-type-fest@^0.20.2:
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
-  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
-
 type-fest@^0.21.3:
   version "0.21.3"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
   integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
 
-type-is@^1.6.14, type-is@^1.6.16, type-is@^1.6.4:
-  version "1.6.18"
-  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
-  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
-  dependencies:
-    media-typer "0.3.0"
-    mime-types "~2.1.24"
-
-type-of@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/type-of/-/type-of-2.0.1.tgz#e72a1741896568e9f628378d816d6912f7f23972"
-  integrity sha1-5yoXQYllaOn2KDeNgW1pEvfyOXI=
-
 type@^1.0.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
@@ -11156,50 +4099,11 @@ type@^2.0.0:
   resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
   integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
 
-typedarray-to-buffer@^3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
-  integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
-  dependencies:
-    is-typedarray "^1.0.0"
-
 typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typeorm@0.2.39:
-  version "0.2.39"
-  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.39.tgz#4d22fc68d114b2ca88a8d7b064f31af15e836ade"
-  integrity sha512-yQdvDpmmmn8wp1We25V76KIBPYR/lDbymNbGC++Uq8mSRhpHIPnlg26VAT4CF6Ypqx72zn8eqr+/72uSo7HdJQ==
-  dependencies:
-    "@sqltools/formatter" "^1.2.2"
-    app-root-path "^3.0.0"
-    buffer "^6.0.3"
-    chalk "^4.1.0"
-    cli-highlight "^2.1.11"
-    debug "^4.3.1"
-    dotenv "^8.2.0"
-    glob "^7.1.6"
-    js-yaml "^4.0.0"
-    mkdirp "^1.0.4"
-    reflect-metadata "^0.1.13"
-    sha.js "^2.4.11"
-    tslib "^2.1.0"
-    xml2js "^0.4.23"
-    yargs "^17.0.1"
-    zen-observable-ts "^1.0.0"
-
-typescript@4.4.4:
-  version "4.4.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
-  integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
-
-ulid@2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f"
-  integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==
-
 unc-path-regex@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
@@ -11245,20 +4149,6 @@ uniqs@^2.0.0:
   resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
   integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
 
-unique-filename@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
-  integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
-  dependencies:
-    unique-slug "^2.0.0"
-
-unique-slug@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
-  integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
-  dependencies:
-    imurmurhash "^0.1.4"
-
 unique-stream@^2.0.2:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac"
@@ -11267,34 +4157,11 @@ unique-stream@^2.0.2:
     json-stable-stringify-without-jsonify "^1.0.1"
     through2-filter "^3.0.0"
 
-universalify@^0.1.0, universalify@^0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
-  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
-
 universalify@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
   integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
 
-unload@2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/unload/-/unload-2.3.1.tgz#9d16862d372a5ce5cb630ad1309c2fd6e35dacfe"
-  integrity sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA==
-  dependencies:
-    "@babel/runtime" "^7.6.2"
-    detect-node "2.1.0"
-
-unpipe@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
-  integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
-
-unquote@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
-  integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
-
 unset-value@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
@@ -11308,6 +4175,11 @@ untildify@^4.0.0:
   resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
   integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
 
+upath@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
+  integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
+
 uri-js@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@@ -11320,14 +4192,6 @@ urix@^0.1.0:
   resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
   integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
 
-url@0.10.3:
-  version "0.10.3"
-  resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
-  integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
-  dependencies:
-    punycode "1.3.2"
-    querystring "0.2.0"
-
 url@^0.11.0:
   version "0.11.0"
   resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
@@ -11336,68 +4200,21 @@ url@^0.11.0:
     punycode "1.3.2"
     querystring "0.2.0"
 
-urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6"
-  integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY=
-
 use@^3.1.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
-utf-8-validate@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.2.tgz#63cfbccd85dc1f2b66cf7a1d0eebc08ed056bfb3"
-  integrity sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==
-  dependencies:
-    node-gyp-build "~3.7.0"
-
-util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
 
-util.promisify@^1.0.1, util.promisify@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee"
-  integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.2"
-    has-symbols "^1.0.1"
-    object.getownpropertydescriptors "^2.1.0"
-
-uuid@3.3.2:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
-  integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
-
-uuid@7.0.3:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
-  integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
-
-uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2:
+uuid@^8.3.2:
   version "8.3.2"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
 
-uuid@^3.3.2:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
-  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
-
-v-debounce@0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/v-debounce/-/v-debounce-0.1.2.tgz#cab75df7def2783215bf449ef85c69c2decf0a55"
-  integrity sha1-yrdd997yeDIVv0Se+Fxpwt7PClU=
-
-v8-compile-cache@^2.0.3:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
-  integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
-
 v8flags@^3.0.1:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.1.3.tgz#fc9dc23521ca20c5433f81cc4eb9b3033bb105d8"
@@ -11405,11 +4222,6 @@ v8flags@^3.0.1:
   dependencies:
     homedir-polyfill "^1.0.1"
 
-valid-url@^1.0.9:
-  version "1.0.9"
-  resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
-  integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=
-
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -11423,17 +4235,7 @@ value-or-function@^3.0.0:
   resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813"
   integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=
 
-vanilla-tilt@1.7.2:
-  version "1.7.2"
-  resolved "https://registry.yarnpkg.com/vanilla-tilt/-/vanilla-tilt-1.7.2.tgz#59a5565d9f1f6d392a36969f223fb600dd101a81"
-  integrity sha512-arf2wY2Y65rP6Zxve9PnUUnRl9nQ1KenPNae6QRaVq/PEvaIto2bC4jYirNJ19U7nLkzI1H9O+nYtcQlX7BTsA==
-
-vary@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
-  integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
-
-vendors@^1.0.0, vendors@^1.0.3:
+vendors@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
   integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
@@ -11502,92 +4304,6 @@ vinyl@^2.0.0:
     remove-trailing-separator "^1.0.1"
     replace-ext "^1.0.0"
 
-void-elements@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
-  integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
-
-vue-eslint-parser@^8.0.1:
-  version "8.0.1"
-  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-8.0.1.tgz#25e08b20a414551531f3e19f999902e1ecf45f13"
-  integrity sha512-lhWjDXJhe3UZw2uu3ztX51SJAPGPey1Tff2RK3TyZURwbuI4vximQLzz4nQfCv8CZq4xx7uIiogHMMoSJPr33A==
-  dependencies:
-    debug "^4.3.2"
-    eslint-scope "^6.0.0"
-    eslint-visitor-keys "^3.0.0"
-    espree "^9.0.0"
-    esquery "^1.4.0"
-    lodash "^4.17.21"
-    semver "^7.3.5"
-
-vue-loader@16.7.0:
-  version "16.7.0"
-  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.7.0.tgz#ee161b2f6c27b0b459264c3c23d34068b471d574"
-  integrity sha512-43I0grWtwSCE8fiH/hAwFK+6sNlmvDuHhXScYH8HVSVAMS81IM66tgUOcxbPCeqhhz/1BE51YPxX59eZKGallQ==
-  dependencies:
-    chalk "^4.1.0"
-    hash-sum "^2.0.0"
-    loader-utils "^2.0.0"
-
-vue-prism-editor@2.0.0-alpha.2:
-  version "2.0.0-alpha.2"
-  resolved "https://registry.yarnpkg.com/vue-prism-editor/-/vue-prism-editor-2.0.0-alpha.2.tgz#aa53a88efaaed628027cbb282c2b1d37fc7c5c69"
-  integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
-
-vue-router@4.0.5:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.5.tgz#dd0a4134bc950c37aef64b973e9ee1008428d8fa"
-  integrity sha512-AQq+pllb6FCc7rS6vh4PPcce3XA1jgK3hKNkQ4hXHwoVN7jOeAOMKCnX7XAX3etV9rmN7iNW8iIwgPk95ckBjw==
-
-vue-style-loader@4.1.3:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
-  integrity sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==
-  dependencies:
-    hash-sum "^1.0.2"
-    loader-utils "^1.0.2"
-
-vue-svg-loader@0.17.0-beta.2:
-  version "0.17.0-beta.2"
-  resolved "https://registry.yarnpkg.com/vue-svg-loader/-/vue-svg-loader-0.17.0-beta.2.tgz#954b2a08b5488998dd81ec371ab5fb5ea4182ef7"
-  integrity sha512-iMUGJTKEcuNAG8VXOchjA8443IqEmEi2Aw6EVIHWma2cC4TUQ7Oet5Yry9IFfqXQXXvyzXz5EyttVvfRGTNH4Q==
-  dependencies:
-    loader-utils "^2.0.0"
-    semver "^7.3.2"
-    svgo "^1.3.2"
-
-vue@3.2.21:
-  version "3.2.21"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.21.tgz#55f5665172d95cf97e806b9aad0a375180be23a1"
-  integrity sha512-jpy7ckXdyclfRzqLjL4mtq81AkzQleE54KjZsJg/9OorNVurAxdlU5XpD49GpjKdnftuffKUvx2C5jDOrgc/zg==
-  dependencies:
-    "@vue/compiler-dom" "3.2.21"
-    "@vue/compiler-sfc" "3.2.21"
-    "@vue/runtime-dom" "3.2.21"
-    "@vue/server-renderer" "3.2.21"
-    "@vue/shared" "3.2.21"
-
-vuedraggable@4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.0.1.tgz#3bcaab0808b7944030b7d9a29f9a63d59dfa12c5"
-  integrity sha512-7qN5jhB1SLfx5P+HCm3JUW+pvgA1bSLgYLSVOeLWBDH9z+zbaEH0OlyZBVMLOxFR+JUHJjwDD0oy7T4r9TEgDA==
-  dependencies:
-    sortablejs "1.10.2"
-
-w3c-hr-time@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
-  integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
-  dependencies:
-    browser-process-hrtime "^1.0.0"
-
-w3c-xmlserializer@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"
-  integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==
-  dependencies:
-    xml-name-validator "^3.0.0"
-
 wait-on@6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.0.tgz#7e9bf8e3d7fe2daecbb7a570ac8ca41e9311c7e7"
@@ -11599,184 +4315,6 @@ wait-on@6.0.0:
     minimist "^1.2.5"
     rxjs "^7.1.0"
 
-watchpack@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.0.0.tgz#b12248f32f0fd4799b7be0802ad1f6573a45955c"
-  integrity sha512-xSdCxxYZWNk3VK13bZRYhsQpfa8Vg63zXG+3pyU8ouqSLRCv4IGXIp9Kr226q6GBkGRlZrST2wwKtjfKz2m7Cg==
-  dependencies:
-    glob-to-regexp "^0.4.1"
-    graceful-fs "^4.1.2"
-
-watchpack@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"
-  integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==
-  dependencies:
-    glob-to-regexp "^0.4.1"
-    graceful-fs "^4.1.2"
-
-web-push@3.4.5:
-  version "3.4.5"
-  resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.5.tgz#f94074ff150538872c7183e4d8881c8305920cf1"
-  integrity sha512-2njbTqZ6Q7ZqqK14YpK1GGmaZs3NmuGYF5b7abCXulUIWFSlSYcZ3NBJQRFcMiQDceD7vQknb8FUuvI1F7Qe/g==
-  dependencies:
-    asn1.js "^5.3.0"
-    http_ece "1.1.0"
-    https-proxy-agent "^5.0.0"
-    jws "^4.0.0"
-    minimist "^1.2.5"
-    urlsafe-base64 "^1.0.0"
-
-webidl-conversions@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
-  integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
-
-webidl-conversions@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
-  integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
-
-webpack-cli@4.9.1:
-  version "4.9.1"
-  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.9.1.tgz#b64be825e2d1b130f285c314caa3b1ba9a4632b3"
-  integrity sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==
-  dependencies:
-    "@discoveryjs/json-ext" "^0.5.0"
-    "@webpack-cli/configtest" "^1.1.0"
-    "@webpack-cli/info" "^1.4.0"
-    "@webpack-cli/serve" "^1.6.0"
-    colorette "^2.0.14"
-    commander "^7.0.0"
-    execa "^5.0.0"
-    fastest-levenshtein "^1.0.12"
-    import-local "^3.0.2"
-    interpret "^2.2.0"
-    rechoir "^0.7.0"
-    webpack-merge "^5.7.3"
-
-webpack-merge@^5.7.3:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213"
-  integrity sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==
-  dependencies:
-    clone-deep "^4.0.1"
-    wildcard "^2.0.0"
-
-webpack-sources@^2.1.1:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac"
-  integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==
-  dependencies:
-    source-list-map "^2.0.1"
-    source-map "^0.6.1"
-
-webpack-sources@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d"
-  integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==
-
-webpack@5.63.0:
-  version "5.63.0"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.63.0.tgz#4b074115800e0526d85112985e46c64b95e04aaf"
-  integrity sha512-HYrw6bkj/MDmphAXvqLEvn2fVoDZsYu6O638WjK6lSNgIpjb5jl/KtOrqJyU9EC/ZV9mLUmZW5h4mASB+CVA4A==
-  dependencies:
-    "@types/eslint-scope" "^3.7.0"
-    "@types/estree" "^0.0.50"
-    "@webassemblyjs/ast" "1.11.1"
-    "@webassemblyjs/wasm-edit" "1.11.1"
-    "@webassemblyjs/wasm-parser" "1.11.1"
-    acorn "^8.4.1"
-    acorn-import-assertions "^1.7.6"
-    browserslist "^4.14.5"
-    chrome-trace-event "^1.0.2"
-    enhanced-resolve "^5.8.3"
-    es-module-lexer "^0.9.0"
-    eslint-scope "5.1.1"
-    events "^3.2.0"
-    glob-to-regexp "^0.4.1"
-    graceful-fs "^4.2.4"
-    json-parse-better-errors "^1.0.2"
-    loader-runner "^4.2.0"
-    mime-types "^2.1.27"
-    neo-async "^2.6.2"
-    schema-utils "^3.1.0"
-    tapable "^2.1.1"
-    terser-webpack-plugin "^5.1.3"
-    watchpack "^2.2.0"
-    webpack-sources "^3.2.0"
-
-webpack@^5:
-  version "5.33.2"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.33.2.tgz#c049717c9b038febf5a72fd2f53319ad59a8c1fc"
-  integrity sha512-X4b7F1sYBmJx8mlh2B7mV5szEkE0jYNJ2y3akgAP0ERi0vLCG1VvdsIxt8lFd4st6SUy0lf7W0CCQS566MBpJg==
-  dependencies:
-    "@types/eslint-scope" "^3.7.0"
-    "@types/estree" "^0.0.46"
-    "@webassemblyjs/ast" "1.11.0"
-    "@webassemblyjs/wasm-edit" "1.11.0"
-    "@webassemblyjs/wasm-parser" "1.11.0"
-    acorn "^8.0.4"
-    browserslist "^4.14.5"
-    chrome-trace-event "^1.0.2"
-    enhanced-resolve "^5.7.0"
-    es-module-lexer "^0.4.0"
-    eslint-scope "^5.1.1"
-    events "^3.2.0"
-    glob-to-regexp "^0.4.1"
-    graceful-fs "^4.2.4"
-    json-parse-better-errors "^1.0.2"
-    loader-runner "^4.2.0"
-    mime-types "^2.1.27"
-    neo-async "^2.6.2"
-    schema-utils "^3.0.0"
-    tapable "^2.1.1"
-    terser-webpack-plugin "^5.1.1"
-    watchpack "^2.0.0"
-    webpack-sources "^2.1.1"
-
-websocket@1.0.34:
-  version "1.0.34"
-  resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111"
-  integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==
-  dependencies:
-    bufferutil "^4.0.1"
-    debug "^2.2.0"
-    es5-ext "^0.10.50"
-    typedarray-to-buffer "^3.1.5"
-    utf-8-validate "^5.0.2"
-    yaeti "^0.0.6"
-
-whatwg-encoding@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
-  integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
-  dependencies:
-    iconv-lite "0.4.24"
-
-whatwg-mimetype@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
-  integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
-
-whatwg-url@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.0.0.tgz#37f256cb746398e19b107bd6ef820b4ae2d15871"
-  integrity sha512-41ou2Dugpij8/LPO5Pq64K5q++MnRCBpEHvQr26/mArEKTkCV5aoXIqyhuYtE0pkqScXwhf2JP57rkRTYM29lQ==
-  dependencies:
-    lodash.sortby "^4.7.0"
-    tr46 "^2.0.0"
-    webidl-conversions "^5.0.0"
-
-whatwg-url@^8.5.0:
-  version "8.5.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.5.0.tgz#7752b8464fc0903fec89aa9846fc9efe07351fd3"
-  integrity sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==
-  dependencies:
-    lodash "^4.7.0"
-    tr46 "^2.0.2"
-    webidl-conversions "^6.1.0"
-
 whet.extend@~0.9.9:
   version "0.9.9"
   resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
@@ -11787,56 +4325,19 @@ which-module@^1.0.0:
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
   integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=
 
-which-module@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
-
-which@2.0.2, which@^2.0.1, which@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
-  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
-  dependencies:
-    isexe "^2.0.0"
-
-which@^1.1.1, which@^1.2.14:
+which@^1.2.14:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
   dependencies:
     isexe "^2.0.0"
 
-wide-align@1.1.3, wide-align@^1.1.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
-  integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
-  dependencies:
-    string-width "^1.0.2 || 2"
-
-wildcard@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
-  integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
-
-with@^7.0.0:
-  version "7.0.2"
-  resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac"
-  integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
   dependencies:
-    "@babel/parser" "^7.9.6"
-    "@babel/types" "^7.9.6"
-    assert-never "^1.2.1"
-    babel-walk "3.0.0-canary-5"
-
-word-wrap@^1.2.3, word-wrap@~1.2.3:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
-  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
-
-workerpool@6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b"
-  integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==
+    isexe "^2.0.0"
 
 wrap-ansi@^2.0.0:
   version "2.1.0"
@@ -11846,15 +4347,6 @@ wrap-ansi@^2.0.0:
     string-width "^1.0.1"
     strip-ansi "^3.0.1"
 
-wrap-ansi@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
-  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
-  dependencies:
-    ansi-styles "^3.2.0"
-    string-width "^3.0.0"
-    strip-ansi "^5.0.0"
-
 wrap-ansi@^6.2.0:
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -11878,65 +4370,7 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-ws@8.2.3:
-  version "8.2.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
-  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
-
-ws@^7.4.6:
-  version "7.5.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
-  integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
-
-xev@2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/xev/-/xev-2.0.1.tgz#24484173a22115bc8a990ef5d4d5129695b827a7"
-  integrity sha512-icDf9M67bDge0F2qf02WKZq+s7mMO/SbPv67ZQPym6JThLEOdlWWLdB7VTVgRJp3ekgaiVItCAyH6aoKCPvfIA==
-
-xml-js@^1.6.11:
-  version "1.6.11"
-  resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
-  integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
-  dependencies:
-    sax "^1.2.4"
-
-xml-name-validator@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
-  integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
-
-xml2js@0.4.19:
-  version "0.4.19"
-  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
-  integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
-  dependencies:
-    sax ">=0.6.0"
-    xmlbuilder "~9.0.1"
-
-xml2js@^0.4.23:
-  version "0.4.23"
-  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
-  integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
-  dependencies:
-    sax ">=0.6.0"
-    xmlbuilder "~11.0.0"
-
-xmlbuilder@~11.0.0:
-  version "11.0.1"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
-  integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
-
-xmlbuilder@~9.0.1:
-  version "9.0.7"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
-  integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
-
-xmlchars@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
-  integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
-
-xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
+xtend@~4.0.0, xtend@~4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
@@ -11946,72 +4380,16 @@ y18n@^3.2.1:
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
   integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==
 
-y18n@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
-  integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
-
-y18n@^5.0.5:
-  version "5.0.5"
-  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
-  integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
-
-yaeti@^0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
-  integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=
-
-yallist@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
-  integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
-
-yallist@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
-  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
 yaml-ast-parser@0.0.43:
   version "0.0.43"
   resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb"
   integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
 
-yaml@^1.10.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
-  integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
-
-yaml@^1.10.2:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
-  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
-
-yargs-parser@18.x:
-  version "18.1.3"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
-  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
-  dependencies:
-    camelcase "^5.0.0"
-    decamelize "^1.2.0"
-
-yargs-parser@20.2.4, yargs-parser@^20.2.2:
-  version "20.2.4"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
-  integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
-
 yargs-parser@>=5.0.0-security.0:
   version "20.2.7"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
   integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
 
-yargs-parser@^13.1.2:
-  version "13.1.2"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
-  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
-  dependencies:
-    camelcase "^5.0.0"
-    decamelize "^1.2.0"
-
 yargs-parser@^5.0.0:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394"
@@ -12020,58 +4398,6 @@ yargs-parser@^5.0.0:
     camelcase "^3.0.0"
     object.assign "^4.1.0"
 
-yargs-unparser@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
-  integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
-  dependencies:
-    camelcase "^6.0.0"
-    decamelize "^4.0.0"
-    flat "^5.0.2"
-    is-plain-obj "^2.1.0"
-
-yargs@16.2.0, yargs@^16.0.0, yargs@^16.2.0:
-  version "16.2.0"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
-  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
-  dependencies:
-    cliui "^7.0.2"
-    escalade "^3.1.1"
-    get-caller-file "^2.0.5"
-    require-directory "^2.1.1"
-    string-width "^4.2.0"
-    y18n "^5.0.5"
-    yargs-parser "^20.2.2"
-
-yargs@^13.2.4:
-  version "13.3.2"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
-  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
-  dependencies:
-    cliui "^5.0.0"
-    find-up "^3.0.0"
-    get-caller-file "^2.0.1"
-    require-directory "^2.1.1"
-    require-main-filename "^2.0.0"
-    set-blocking "^2.0.0"
-    string-width "^3.0.0"
-    which-module "^2.0.0"
-    y18n "^4.0.0"
-    yargs-parser "^13.1.2"
-
-yargs@^17.0.1:
-  version "17.1.1"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.1.tgz#c2a8091564bdb196f7c0a67c1d12e5b85b8067ba"
-  integrity sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==
-  dependencies:
-    cliui "^7.0.2"
-    escalade "^3.1.1"
-    get-caller-file "^2.0.5"
-    require-directory "^2.1.1"
-    string-width "^4.2.0"
-    y18n "^5.0.5"
-    yargs-parser "^20.2.2"
-
 yargs@^7.1.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
@@ -12098,31 +4424,3 @@ yauzl@^2.10.0:
   dependencies:
     buffer-crc32 "~0.2.3"
     fd-slicer "~1.1.0"
-
-ylru@^1.2.0:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
-  integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==
-
-yn@3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
-  integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
-
-yocto-queue@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
-  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
-
-zen-observable-ts@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.0.0.tgz#30d1202b81d8ba4c489e3781e8ca09abf0075e70"
-  integrity sha512-KmWcbz+9kKUeAQ8btY8m1SsEFgBcp7h/Uf3V5quhan7ZWdjGsf0JcGLULQiwOZibbFWnHkYq8Nn2AZbJabovQg==
-  dependencies:
-    "@types/zen-observable" "^0.8.2"
-    zen-observable "^0.8.15"
-
-zen-observable@^0.8.15:
-  version "0.8.15"
-  resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
-  integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
-- 
cgit v1.2.3-freya

